--- /dev/null
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#pragma once
+
+struct asan_init_args {
+ u64 arena_all_pages;
+ u64 arena_globals_pages;
+};
+
+int asan_init(struct asan_init_args *args);
+
+extern volatile u64 __asan_shadow_memory_dynamic_address;
+extern volatile u32 asan_reported;
+extern volatile bool asan_inited;
+extern volatile bool asan_report_once;
+
+#ifdef __BPF__
+
+#define ASAN_SHADOW_SHIFT 3
+#define ASAN_SHADOW_SCALE (1ULL << ASAN_SHADOW_SHIFT)
+#define ASAN_GRANULE_MASK ((1ULL << ASAN_SHADOW_SHIFT) - 1)
+#define ASAN_GRANULE(addr) ((s8)((u32)(u64)((addr)) & ASAN_GRANULE_MASK))
+
+#define __noasan __attribute__((no_sanitize("address")))
+
+#ifdef BPF_ARENA_ASAN
+
+typedef s8 __arena s8a;
+
+static inline
+s8a *mem_to_shadow(void __arena __arg_arena *addr)
+{
+ return (s8a *)(((u32)(u64)addr >> ASAN_SHADOW_SHIFT) +
+ __asan_shadow_memory_dynamic_address);
+}
+
+__weak __noasan
+bool asan_ready(void)
+{
+ return __asan_shadow_memory_dynamic_address;
+}
+
+int asan_poison(void __arena *addr, s8 val, size_t size);
+int asan_unpoison(void __arena *addr, size_t size);
+bool asan_shadow_set(void __arena *addr);
+
+/*
+ * Dummy calls to ensure the ASAN runtime's BTF information is present
+ * in every object file when compiling the runtime and local BPF code
+ * separately. The runtime calls are injected into the LLVM IR file
+ */
+#define DECLARE_ASAN_LOAD_STORE_SIZE(size) \
+ void __asan_store##size(intptr_t addr); \
+ void __asan_store##size##_noabort(intptr_t addr); \
+ void __asan_load##size(intptr_t addr); \
+ void __asan_load##size##_noabort(intptr_t addr); \
+ void __asan_report_store##size(intptr_t addr); \
+ void __asan_report_store##size##_noabort(intptr_t addr); \
+ void __asan_report_load##size(intptr_t addr); \
+ void __asan_report_load##size##_noabort(intptr_t addr);
+
+DECLARE_ASAN_LOAD_STORE_SIZE(1);
+DECLARE_ASAN_LOAD_STORE_SIZE(2);
+DECLARE_ASAN_LOAD_STORE_SIZE(4);
+DECLARE_ASAN_LOAD_STORE_SIZE(8);
+
+void __asan_storeN(intptr_t addr, ssize_t size);
+void __asan_storeN_noabort(intptr_t addr, ssize_t size);
+void __asan_loadN(intptr_t addr, ssize_t size);
+void __asan_loadN_noabort(intptr_t addr, ssize_t size);
+
+/*
+ * Force LLVM to emit BTF information for the stubs,
+ * because the ASAN pass in LLVM by itself doesn't.
+ */
+#define ASAN_LOAD_STORE_SIZE(size) \
+ __asan_store##size, \
+ __asan_store##size##_noabort, \
+ __asan_load##size, \
+ __asan_load##size##_noabort, \
+ __asan_report_store##size, \
+ __asan_report_store##size##_noabort, \
+ __asan_report_load##size, \
+ __asan_report_load##size##_noabort
+
+__attribute__((used))
+static void (*__asan_btf_anchors[])(intptr_t) = {
+ ASAN_LOAD_STORE_SIZE(1),
+ ASAN_LOAD_STORE_SIZE(2),
+ ASAN_LOAD_STORE_SIZE(4),
+ ASAN_LOAD_STORE_SIZE(8),
+};
+
+#else /* BPF_ARENA_ASAN */
+
+static inline int asan_poison(void __arena *addr, s8 val, size_t size) { return 0; }
+static inline int asan_unpoison(void __arena *addr, size_t size) { return 0; }
+static inline bool asan_shadow_set(void __arena *addr) { return 0; }
+__weak bool asan_ready(void) { return true; }
+
+#endif /* BPF_ARENA_ASAN */
+
+#endif /* __BPF__ */
--- /dev/null
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <vmlinux.h>
+#include <libarena/common.h>
+#include <libarena/asan.h>
+
+
+enum {
+ /*
+ * Is the access checked by check_region_inline
+ * a read or a write?
+ */
+ ASAN_READ = 0x0U,
+ ASAN_WRITE = 0x1U,
+};
+
+/*
+ * Address sanitizer (ASAN) for arena-based BPF programs, inspired
+ * by KASAN.
+ *
+ * The API
+ * -------
+ *
+ * The implementation includes two kinds of components: Implementation
+ * of ASAN hooks injected by LLVM into the program, and API calls that
+ * allocators use to mark memory as valid or invalid. The full list is:
+ *
+ * LLVM stubs:
+ *
+ * void __asan_{load, store}<size>(intptr_t addr)
+ * Checks whether an access is valid. All variations covered
+ * by check_region_inline().
+ *
+ * void __asan_{store, load}((intptr_t addr, ssize_t size)
+ *
+ * void __asan_report_{load, store}<size>(intptr_t addr)
+ * Report an access violation for the program. Used when LLVM
+ * uses direct code generation for shadow map checks.
+ *
+ * void *__asan_memcpy(void *d, const void *s, size_t n)
+ * void *__asan_memmove(void *d, const void *s, size_t n)
+ * void *__asan_memset(void *p, int c, size_t n)
+ * Hooks for ASAN instrumentation of the LLVM mem* builtins.
+ * Currently unimplemented just like the builtins themselves.
+ *
+ * API methods:
+ *
+ * asan_init()
+ * Initialize the ASAN map for the arena.
+ *
+ * asan_poison()
+ * Mark a region of memory as poisoned. Accessing poisoned memory
+ * causes asan_report() to fire. Invoked during free().
+ *
+ * asan_unpoison()
+ * Mark a region as unpoisoned after alloc().
+ *
+ * asan_shadow_set()
+ * Check a byte's validity directly.
+ *
+ * The Algorithm In Brief
+ * ----------------------
+ * Each group of 8 bytes is mapped to a "granule" in the shadow map. This
+ * granule is the size of the byte and describes which bytes are valid.
+ * Possible values are:
+ *
+ * 0: All bytes are valid. Makes checks in the middle of an allocated region
+ * (most of them) fast.
+ * (0, 7]: How many consecutive bytes are valid, starting from the lowest one.
+ * The tradeoff is that we can't poison individual bytes in the middle of a
+ * valid region.
+ * [0x80, 0xff]: Special poison values, can be used to denote specific error
+ * modes (e.g., recently freed vs uninitialized memory).
+ *
+ * The mapping between a memory location and its shadow is:
+ * shadow_addr = shadow_base + (addr >> 3). We retain the 8:1 data:shadow
+ * ratio of existing ASAN implementations as a compromise between tracking
+ * granularity and space usage/scan overhead.
+ */
+
+#ifdef BPF_ARENA_ASAN
+
+#pragma clang attribute push(__attribute__((no_sanitize("address"))), \
+ apply_to = function)
+
+#define SHADOW_ALL_ZEROES ((u64)-1)
+
+/*
+ * Canary variable for ASAN violations. Set to the offending address.
+ */
+volatile u64 asan_violated = 0;
+
+/*
+ * Shadow map occupancy map.
+ */
+volatile u64 __asan_shadow_memory_dynamic_address;
+
+volatile u32 asan_reported = false;
+volatile bool asan_inited = false;
+
+/*
+ * Set during program load.
+ */
+volatile bool asan_report_once = false;
+
+/*
+ * BPF does not currently support the memset/memcpy/memcmp intrinsics.
+ * For large sequential copies, or assignments of large data structures,
+ * the frontend will generate an intrinsic that causes the BPF backend
+ * to exit due to a missing implementation. Provide a simple implementation
+ * just for memset to use it for poisoning/unpoisoning the map.
+ */
+__weak int asan_memset(s8a __arg_arena *dst, s8 val, size_t size)
+{
+ size_t i;
+
+ for (i = zero; i < size && can_loop; i++)
+ dst[i] = val;
+
+ return 0;
+}
+
+/* Validate a 1-byte access, always within a single byte. */
+static __always_inline bool memory_is_poisoned_1(s8a *addr)
+{
+ s8 shadow_value = *(s8a *)mem_to_shadow(addr);
+
+ /* Byte is 0, access is valid. */
+ if (likely(!shadow_value))
+ return false;
+
+ /*
+ * Byte is non-zero. Access is valid if granule offset in [0, shadow_value),
+ * so the memory is poisoned if shadow_value is negative or smaller than
+ * the granule's value.
+ */
+
+ return ASAN_GRANULE(addr) >= shadow_value;
+}
+
+/* Validate a 2- 4-, 8-byte access, shadow spans up to 2 bytes. */
+static __always_inline bool memory_is_poisoned_2_4_8(s8a *addr, u64 size)
+{
+ u64 end = (u64)addr + size - 1;
+
+ /*
+ * Region fully within a single byte (addition didn't
+ * overflow above ASAN_GRANULE).
+ */
+ if (likely(ASAN_GRANULE(end) >= size - 1))
+ return memory_is_poisoned_1((s8a *)end);
+
+ /*
+ * Otherwise first byte must be fully unpoisoned, and second byte
+ * must be unpoisoned up to the end of the accessed region.
+ */
+
+ return *(s8a *)mem_to_shadow(addr) || memory_is_poisoned_1((s8a *)end);
+}
+
+__weak bool asan_shadow_set(void __arena __arg_arena *addr)
+{
+ return memory_is_poisoned_1(addr);
+}
+
+static __always_inline u64 first_nonzero_byte(u64 addr, size_t size)
+{
+ while (size && can_loop) {
+ if (unlikely(*(s8a *)addr))
+ return addr;
+ addr += 1;
+ size -= 1;
+ }
+
+ return SHADOW_ALL_ZEROES;
+}
+
+static __always_inline bool memory_is_poisoned_n(s8a *addr, u64 size)
+{
+ u64 ret;
+ u64 start;
+ u64 end;
+
+ /* Size of [start, end] is end - start + 1. */
+ start = (u64)mem_to_shadow(addr);
+ end = (u64)mem_to_shadow(addr + size - 1);
+
+ ret = first_nonzero_byte(start, (end - start) + 1);
+ if (likely(ret == SHADOW_ALL_ZEROES))
+ return false;
+
+ return unlikely(ret != end || ASAN_GRANULE(addr + size - 1) >= *(s8a *)end);
+}
+
+__weak int asan_report(s8a __arg_arena *addr, size_t sz, u32 flags)
+{
+ u32 reported = __sync_val_compare_and_swap(&asan_reported, false, true);
+
+ /* Only report the first ASAN violation. */
+ if (reported && asan_report_once)
+ return 0;
+
+ asan_violated = (u64)addr;
+
+ arena_stderr("Memory violation for address %p (0x%lx) for %s of size %ld\n",
+ addr, (u64)addr,
+ (flags & ASAN_WRITE) ? "write" : "read",
+ sz);
+ bpf_stream_print_stack(BPF_STDERR);
+
+ return 0;
+}
+
+static __always_inline bool check_asan_args(s8a *addr, size_t size,
+ bool *result)
+{
+ bool valid = true;
+
+ /* Size 0 accesses are valid even if the address is invalid. */
+ if (unlikely(size == 0))
+ goto confirmed_valid;
+
+ /*
+ * Wraparound is possible for values close to the the edge of the
+ * 4GiB boundary of the arena (last valid address is 1UL << 32 - 1).
+ *
+ *
+ * The wraparound detection below works for small sizes. check_asan_args is
+ * always called from the builtin ASAN checks, so 1 <= size <= 64. Even
+ * for storeN/loadN that we do not expect to encounter the intrinsics will
+ * not have a large enough size that:
+ *
+ * - addr + size > MAX_U32
+ * - (u32)(addr + size) > (u32) addr
+ *
+ * which would defeat wraparound detection.
+ */
+ if (unlikely((u32)(u64)(addr + size) < (u32)(u64)addr))
+ goto confirmed_invalid;
+
+ return false;
+
+confirmed_invalid:
+ valid = false;
+
+ /* FALLTHROUGH */
+confirmed_valid:
+ *result = valid;
+
+ return true;
+}
+
+static __always_inline bool check_region_inline(intptr_t ptr, size_t size,
+ u32 flags)
+{
+ s8a *addr = (s8a *)(u64)ptr;
+ bool is_poisoned, is_valid;
+
+ if (check_asan_args(addr, size, &is_valid)) {
+ if (!is_valid)
+ asan_report(addr, size, flags);
+ return is_valid;
+ }
+
+ switch (size) {
+ case 1:
+ is_poisoned = memory_is_poisoned_1(addr);
+ break;
+ case 2:
+ case 4:
+ case 8:
+ is_poisoned = memory_is_poisoned_2_4_8(addr, size);
+ break;
+ default:
+ is_poisoned = memory_is_poisoned_n(addr, size);
+ }
+
+ if (is_poisoned) {
+ asan_report(addr, size, flags);
+ return false;
+ }
+
+ return true;
+}
+
+/*
+ * __alias is not supported for BPF so define *__noabort() variants as wrappers.
+ */
+#define DEFINE_ASAN_LOAD_STORE(size) \
+ __hidden void __asan_store##size(intptr_t addr) \
+ { \
+ check_region_inline(addr, size, ASAN_WRITE); \
+ } \
+ __hidden void __asan_store##size##_noabort(intptr_t addr) \
+ { \
+ check_region_inline(addr, size, ASAN_WRITE); \
+ } \
+ __hidden void __asan_load##size(intptr_t addr) \
+ { \
+ check_region_inline(addr, size, ASAN_READ); \
+ } \
+ __hidden void __asan_load##size##_noabort(intptr_t addr) \
+ { \
+ check_region_inline(addr, size, ASAN_READ); \
+ } \
+ __hidden void __asan_report_store##size(intptr_t addr) \
+ { \
+ asan_report((s8a *)addr, size, ASAN_WRITE); \
+ } \
+ __hidden void __asan_report_store##size##_noabort(intptr_t addr) \
+ { \
+ asan_report((s8a *)addr, size, ASAN_WRITE); \
+ } \
+ __hidden void __asan_report_load##size(intptr_t addr) \
+ { \
+ asan_report((s8a *)addr, size, ASAN_READ); \
+ } \
+ __hidden void __asan_report_load##size##_noabort(intptr_t addr) \
+ { \
+ asan_report((s8a *)addr, size, ASAN_READ); \
+ }
+
+DEFINE_ASAN_LOAD_STORE(1);
+DEFINE_ASAN_LOAD_STORE(2);
+DEFINE_ASAN_LOAD_STORE(4);
+DEFINE_ASAN_LOAD_STORE(8);
+
+void __asan_storeN(intptr_t addr, ssize_t size)
+{
+ check_region_inline(addr, size, ASAN_WRITE);
+}
+
+void __asan_storeN_noabort(intptr_t addr, ssize_t size)
+{
+ check_region_inline(addr, size, ASAN_WRITE);
+}
+
+void __asan_loadN(intptr_t addr, ssize_t size)
+{
+ check_region_inline(addr, size, ASAN_READ);
+}
+
+void __asan_loadN_noabort(intptr_t addr, ssize_t size)
+{
+ check_region_inline(addr, size, ASAN_READ);
+}
+
+/*
+ * We currently do not sanitize globals.
+ */
+void __asan_register_globals(intptr_t globals, size_t n)
+{
+}
+
+void __asan_unregister_globals(intptr_t globals, size_t n)
+{
+}
+
+/*
+ * We do not currently have memcpy/memmove/memset intrinsics
+ * in LLVM. Do not implement sanitization.
+ */
+void *__asan_memcpy(void *d, const void *s, size_t n)
+{
+ arena_stderr("ASAN: Unexpected %s call", __func__);
+ return NULL;
+}
+
+void *__asan_memmove(void *d, const void *s, size_t n)
+{
+ arena_stderr("ASAN: Unexpected %s call", __func__);
+ return NULL;
+}
+
+void *__asan_memset(void *p, int c, size_t n)
+{
+ arena_stderr("ASAN: Unexpected %s call", __func__);
+ return NULL;
+}
+
+/*
+ * Poisoning code, used when we add more freed memory to the allocator by:
+ * a) pulling memory from the arena segment using bpf_arena_alloc_pages()
+ * b) freeing memory from application code
+ */
+__hidden __noasan int asan_poison(void __arena *addr, s8 val, size_t size)
+{
+ s8a *shadow;
+ size_t len;
+
+ /*
+ * Poisoning from a non-granule address makes no sense: We can only allocate
+ * memory to the application that has a granule-aligned starting address,
+ * and bpf_arena_alloc_pages returns page-aligned memory. A non-aligned
+ * addr then implies we're freeing a different address than the one we
+ * allocated.
+ */
+ if (unlikely((u64)addr & ASAN_GRANULE_MASK))
+ return -EINVAL;
+
+ /*
+ * We cannot free an unaligned region because it'd be possible that we
+ * cannot describe the resulting poisoning state of the granule in
+ * the ASAN encoding.
+ *
+ * Every granule represents a region of memory that looks like the
+ * following (P for poisoned bytes, C for clear):
+ *
+ * <Clear> <Poisoned>
+ * [ C C C ... P P ]
+ *
+ * The value of the granule's shadow map is the number of clear bytes in
+ * it. We cannot represent granules with the following state:
+ *
+ * [ P P ... C C ... P P ]
+ *
+ * That would be possible if we could free unaligned regions, so prevent that.
+ */
+ if (unlikely(size & ASAN_GRANULE_MASK))
+ return -EINVAL;
+
+ shadow = mem_to_shadow(addr);
+ len = size >> ASAN_SHADOW_SHIFT;
+
+ asan_memset(shadow, val, len);
+
+ return 0;
+}
+
+/*
+ * Unpoisoning code for marking memory as valid during allocation calls.
+ *
+ * Very similar to asan_poison, except we need to round up instead of
+ * down, then partially poison the last granule if necessary.
+ *
+ * Partial poisoning is useful for keeping the padding poisoned. Allocations
+ * are granule-aligned, so we we're reserving granule-aligned sizes for the
+ * allocation. However, we want to still treat accesses to the padding as
+ * invalid. Partial poisoning takes care of that. Freeing and poisoning the
+ * memory is still done in granule-aligned sizes and repoisons the already
+ * poisoned padding.
+ */
+__hidden __noasan int asan_unpoison(void __arena *addr, size_t size)
+{
+ size_t partial = size & ASAN_GRANULE_MASK;
+ s8a *shadow;
+ size_t len;
+
+ /*
+ * We cannot allocate in the middle of the granule. The ASAN shadow
+ * map encoding only describes regions of memory where every granule
+ * follows this format (P for poisoned, C for clear):
+ *
+ * <Clear> <Poisoned>
+ * [ C C C ... P P ]
+ *
+ * This is so we can use a single number in [0, ASAN_SHADOW_SCALE)
+ * to represent the poison state of the granule.
+ */
+ if (unlikely((u64)addr & ASAN_GRANULE_MASK))
+ return -EINVAL;
+
+ shadow = mem_to_shadow(addr);
+ len = size >> ASAN_SHADOW_SHIFT;
+
+ asan_memset(shadow, 0, len);
+
+ /*
+ * If we are allocating a non-granule aligned region, we need to adjust
+ * the last byte of the shadow map to list how many bytes in the granule
+ * are unpoisoned. If the region is aligned, then the memset call above
+ * was enough.
+ */
+ if (partial)
+ shadow[len] = partial;
+
+ return 0;
+}
+
+/*
+ * Initialize ASAN state when necessary. Triggered from userspace before
+ * allocator startup.
+ */
+SEC("syscall")
+__weak __noasan int asan_init(struct asan_init_args *args)
+{
+ u64 globals_pages = args->arena_globals_pages;
+ u64 all_pages = args->arena_all_pages;
+ u64 shadow_map, shadow_pgoff;
+ u64 shadow_pages;
+
+ if (asan_inited)
+ return 0;
+
+ /*
+ * Round up the shadow map size to the nearest page.
+ */
+ shadow_pages = all_pages >> ASAN_SHADOW_SHIFT;
+ if ((all_pages & ((1 << ASAN_SHADOW_SHIFT) - 1)))
+ shadow_pages += 1;
+
+ if (all_pages > (1ULL << 32) / __PAGE_SIZE) {
+ arena_stderr("error: arena size %lx too large", all_pages);
+ return -EINVAL;
+ }
+
+ if (globals_pages > all_pages) {
+ arena_stderr("error: globals %lx do not fit in arena %lx",
+ globals_pages, all_pages);
+ return -EINVAL;
+ }
+
+ if (globals_pages + shadow_pages >= all_pages) {
+ arena_stderr("error: globals %lx do not leave room for shadow map %lx "
+ "(arena pages %lx)",
+ globals_pages, shadow_pages, all_pages);
+ return -EINVAL;
+ }
+
+ shadow_pgoff = all_pages - shadow_pages - globals_pages;
+ __asan_shadow_memory_dynamic_address = shadow_pgoff * __PAGE_SIZE;
+
+ /*
+ * Allocate the last (1/ASAN_SHADOW_SCALE)th of an arena's pages for the map
+ * We find the offset and size from the arena map.
+ *
+ * The allocated map pages are zeroed out, meaning all memory is marked as valid
+ * even if it's not allocated already. This is expected: Since the actual memory
+ * pages are not allocated, accesses to it will trigger page faults and will be
+ * reported through BPF streams. Any pages allocated through bpf_arena_alloc_pages
+ * should be poisoned by the allocator right after the call succeeds.
+ */
+ shadow_map = (u64)bpf_arena_alloc_pages(
+ &arena, (void __arena *)__asan_shadow_memory_dynamic_address,
+ shadow_pages, NUMA_NO_NODE, 0);
+ if (!shadow_map) {
+ arena_stderr("Could not allocate shadow map\n");
+
+ __asan_shadow_memory_dynamic_address = 0;
+
+ return -ENOMEM;
+ }
+
+ asan_inited = true;
+
+ return 0;
+}
+
+#pragma clang attribute pop
+
+#endif /* BPF_ARENA_ASAN */
+
+__weak char _license[] SEC("license") = "GPL";