public inbox for bpf@vger.kernel.org
 help / color / mirror / Atom feed
From: "Emil Tsalapatis" <emil@etsalapatis.com>
To: "Alexei Starovoitov" <alexei.starovoitov@gmail.com>,
	"Emil Tsalapatis" <emil@etsalapatis.com>
Cc: "bpf" <bpf@vger.kernel.org>,
	"Alexei Starovoitov" <ast@kernel.org>,
	"Andrii Nakryiko" <andrii@kernel.org>,
	"Kumar Kartikeya Dwivedi" <memxor@gmail.com>,
	"Daniel Borkmann" <daniel@iogearbox.net>,
	"Eduard" <eddyz87@gmail.com>, "Song Liu" <song@kernel.org>
Subject: Re: [PATCH bpf-next v4 6/9] selftests/bpf: Add arena ASAN runtime to libarena
Date: Wed, 08 Apr 2026 20:38:10 -0400	[thread overview]
Message-ID: <DHO7DJ574EL1.8O0NIINEBJMR@etsalapatis.com> (raw)
In-Reply-To: <CAADnVQLHFZE3AFzP3gtZKvcaX+3hbp3pCw3JLqEtpdoQ+OUf7g@mail.gmail.com>

On Tue Apr 7, 2026 at 12:39 PM EDT, Alexei Starovoitov wrote:
> On Mon, Apr 6, 2026 at 9:57 PM Emil Tsalapatis <emil@etsalapatis.com> wrote:
>>
>> Add an address sanitizer (ASAN) runtime to the arena library. The
>> ASAN runtime implements the functions injected into BPF binaries
>> by LLVM sanitization when ASAN is enabled during compilation.
>>
>> The runtime also includes functions called explicitly by memory
>> allocation code to mark memory as poisoned/unpoisoned to ASAN.
>> This code is a no-op when sanitization is turned off.
>>
>> Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
>> ---
>>  .../selftests/bpf/libarena/include/asan.h     | 124 ++++
>>  .../selftests/bpf/libarena/include/common.h   |   1 +
>>  .../selftests/bpf/libarena/src/asan.bpf.c     | 539 ++++++++++++++++++
>>  .../selftests/bpf/libarena/src/common.bpf.c   |   1 +
>>  4 files changed, 665 insertions(+)
>>  create mode 100644 tools/testing/selftests/bpf/libarena/include/asan.h
>>  create mode 100644 tools/testing/selftests/bpf/libarena/src/asan.bpf.c
>>
>> diff --git a/tools/testing/selftests/bpf/libarena/include/asan.h b/tools/testing/selftests/bpf/libarena/include/asan.h
>> new file mode 100644
>> index 000000000000..d2eeabba3ffc
>> --- /dev/null
>> +++ b/tools/testing/selftests/bpf/libarena/include/asan.h
>> @@ -0,0 +1,124 @@
>> +// 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);
>> +
>> +/* Parameters usable by userspace. */
>
> not a helpful comment.
>
>> +extern volatile u64 __asan_shadow_memory_dynamic_address;
>> +extern volatile u32 asan_reported;
>> +extern volatile bool asan_inited;
>> +extern volatile bool asan_report_once;
>> +extern volatile bool asan_emit_stack;
>> +
>> +#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
>> +
>> +/*
>> + * Defined as char * to get 1-byte granularity for pointer arithmetic.
>> + */
>
> not a good comment either.
>
>> +typedef s8 __arena s8a;
>> +
>> +/*
>> + * Address to shadow map translation.
>> + */
>
> delete it too.
>
>> +static inline
>> +s8a *mem_to_shadow(void __arena __arg_arena *addr)
>> +{
>> +       return (s8a *)(((u32)(u64)addr >> ASAN_SHADOW_SHIFT) + __asan_shadow_memory_dynamic_address);
>> +}
>> +
>> +/*
>> + * Helper for directly reading the shadow map.
>> + */
>> +static inline __noasan
>> +s8 asan_shadow_value(void __arena __arg_arena *addr)
>> +{
>> +       return *(s8a *)mem_to_shadow(addr);
>> +}
>
> what? Whole new helper just to avoid typing *(s8a *) ?
>
>> +
>> +__weak __noasan
>> +bool asan_ready(void)
>> +{
>> +       return __asan_shadow_memory_dynamic_address;
>> +}
>> +
>> +/*
>> + * Shadow map manipulation helpers.
>> + */
>
> delete the comment.
>
>> +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(void *addr);                            \
>> +       void __asan_store##size##_noabort(void *addr);  \
>> +       void __asan_load##size(void *addr);                             \
>> +       void __asan_load##size##_noabort(void *addr);   \
>> +       void __asan_report_store##size(void *addr);                     \
>> +       void __asan_report_store##size##_noabort(void *addr);           \
>> +       void __asan_report_load##size(void *addr);                      \
>> +       void __asan_report_load##size##_noabort(void *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);
>> +
>> +#define ASAN_DUMMY_CALLS_SIZE(size, arg)               \
>> +do {                                                   \
>> +       __asan_store##size((arg));                      \
>> +       __asan_store##size##_noabort((arg));            \
>> +       __asan_load##size((arg));                       \
>> +       __asan_load##size##_noabort((arg));             \
>> +       __asan_report_store##size((arg));               \
>> +       __asan_report_store##size##_noabort((arg));     \
>> +       __asan_report_load##size((arg));                \
>> +       __asan_report_load##size##_noabort((arg));      \
>> +} while (0)
>> +
>> +#define ASAN_DUMMY_CALLS_ALL(arg)      \
>> +do {                                   \
>> +       ASAN_DUMMY_CALLS_SIZE(1, (arg));        \
>> +       ASAN_DUMMY_CALLS_SIZE(2, (arg));        \
>> +       ASAN_DUMMY_CALLS_SIZE(4, (arg));        \
>> +       ASAN_DUMMY_CALLS_SIZE(8, (arg));        \
>> +} while (0)
>> +
>> +__weak __noasan
>> +int asan_dummy_call(void) {
>> +       /* Use the shadow map base to prevent it from being optimized out. */
>> +       if (__asan_shadow_memory_dynamic_address)
>> +               ASAN_DUMMY_CALLS_ALL(NULL);
>> +
>> +       return 0;
>> +}
>
> so every bpf prog with arena will have this asan_dummy_call() function
> just for BTF info?
> It doesn't smell right.
> I think it's a bug on LLVM side. It should be preserving BTF
> when it emits calls to __asan*.

This only gets emitted for programs compiled with ASAN, so it's not
there by default. That being said, the issue is ultimately that LLVM
doesn't inject debug info for the __asan functions. I've been able to 
remove the dummy calls by adding a postprocessing step in LLVM's
AddressSanitizer (github.com/etsal/llvm-project/tree/asan-emit-dinfo).
I've also gotten the dummy definitions removed by replacing them with
a function pointer array, like so:

/* Only required when compiled with ASAN flags */
static void (* __asan_btf[])(void *) {
	ASAN_LOAD_STORE_SIZE(1),
	ASAN_LOAD_STORE_SIZE(2),
	ASAN_LOAD_STORE_SIZE(4),
	ASAN_LOAD_STORE_SIZE(8),
};

Wdyt about either keeping the dummy calls or using the function pointer
array for now, then removing it if/once an LLVM-side solution gets accepted?

>
>> +#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; }
>> +static inline s8 asan_shadow_value(void __arena *addr) { return 0; }
>> +__weak bool asan_ready(void) { return true; }
>> +
>> +#endif /* BPF_ARENA_ASAN */
>> +
>> +#endif /* __BPF__ */
>> diff --git a/tools/testing/selftests/bpf/libarena/include/common.h b/tools/testing/selftests/bpf/libarena/include/common.h
>> index 544a398a0d1e..f48395358d1d 100644
>> --- a/tools/testing/selftests/bpf/libarena/include/common.h
>> +++ b/tools/testing/selftests/bpf/libarena/include/common.h
>> @@ -39,6 +39,7 @@ struct {
>>  } arena __weak SEC(".maps");
>>
>>  extern const volatile u32 zero;
>> +extern volatile u64 asan_violated;
>>
>>  int arena_fls(__u64 word);
>>
>> diff --git a/tools/testing/selftests/bpf/libarena/src/asan.bpf.c b/tools/testing/selftests/bpf/libarena/src/asan.bpf.c
>> new file mode 100644
>> index 000000000000..b3fae0ed020c
>> --- /dev/null
>> +++ b/tools/testing/selftests/bpf/libarena/src/asan.bpf.c
>> @@ -0,0 +1,539 @@
>> +// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
>> +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
>> +#include <vmlinux.h>
>> +#include <common.h>
>> +#include <asan.h>
>> +
>> +/*
>> + * Address sanitizer (ASAN) for arena-based BPF programs. The
>> + * sanitizer tracks valid arena memory and triggers an error
>> + * when poisoned memory is read or written to. Starting point
>> + * was the KASAN implementation in mm/.
>
> KASAN part of the comment looks obsolete and not really helpful
> in isolation. "Starting point was.." so?
> How is it different ?
> If it said "inspired by KASAN" that would be read differently.
> Or just delete it.
>
>> + *
>> + * 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>(void *addr)
>> + *     Checks whether an access is valid. All variations covered
>> + *     by check_region_inline().
>> + *
>> + * void __asan_{store, load}((void *addr, ssize_t size)
>> + *
>> + * void __asan_report_{load, store}<size>(void *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;
>> +volatile bool asan_emit_stack = false;
>> +
>> +/*
>> + * BPF does not currently support the memset/memcpy/memcmp intrinsics.
>
> and? I don't get what comment is trying to say.
>
>> + */
>> +__weak int asan_memset(s8a __arg_arena *dst, s8 val, size_t size)
>
> right above the comment said __asan_memset is not implemented.
> Yet this is an implementation. What is significance of double underscore?
>
>> +{
>> +       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 = asan_shadow_value(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 asan_shadow_value(addr) || memory_is_poisoned_1((s8a *)end);
>> +}
>> +
>> +/*
>> + * Explicit ASAN check.
>
> the comment is too terse to be useful.
>
>> + */
>> +__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 __builtin_expect(ret != end || ASAN_GRANULE(addr + size - 1) >=
>> +                                                     *(s8a *)end, false);
>
> __builtin_expect ? Use unlikely().
>
>> +}
>> +
>> +__weak int asan_report(s8a __arg_arena *addr, size_t sz,
>> +                                      bool write)
>> +{
>> +       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;
>> +
>> +       if (asan_emit_stack) {
>> +               arena_stderr("Memory violation for address %p (0x%lx) for %s of size %ld",
>> +                               addr, (u64)addr, 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. We do
>> +        * not implement storeN/loadN, so size is guaranteed to be in that range.
>> +        * Even if we did, sane storeN/loadN intrinsics are not expected to 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;
>
> in this path the result will stay uninitialized. Bug or not?
>
>> +
>> +confirmed_invalid:
>> +       valid = false;
>> +
>> +       /* FALLTHROUGH */
>> +confirmed_valid:
>> +       *result = valid;
>> +
>> +       return true;
>> +}
>> +
>> +static __always_inline bool check_region_inline(void *ptr, size_t size,
>> +                                               bool write)
>
> I have allergy to bool arguments.
> Please use 'u32 flags' from the day one and human readable
> enum, so the callsites are easy to read.
>
>> +{
>> +       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, write);
>> +               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, write);
>> +               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(void *addr)                  \
>> +       {                                                             \
>> +               check_region_inline(addr, size, true);                \
>> +       }                                                             \
>> +       __hidden void __asan_store##size##_noabort(void *addr)        \
>> +       {                                                             \
>> +               check_region_inline(addr, size, true);                \
>> +       }                                                             \
>> +       __hidden void __asan_load##size(void *addr)                   \
>> +       {                                                             \
>> +               check_region_inline(addr, size, false);               \
>> +       }                                                             \
>> +       __hidden void __asan_load##size##_noabort(void *addr)         \
>> +       {                                                             \
>> +               check_region_inline(addr, size, false);               \
>
> unlike here where false vs true is not meaningful without
> reading the prototype.
>
>> +       }                                                             \
>> +       __hidden void __asan_report_store##size(void *addr)           \
>> +       {                                                             \
>> +               asan_report((s8a *)addr, size, true);                 \
>> +       }                                                             \
>> +       __hidden void __asan_report_store##size##_noabort(void *addr) \
>> +       {                                                             \
>> +               asan_report((s8a *)addr, size, true);                 \
>> +       }                                                             \
>> +       __hidden void __asan_report_load##size(void *addr)            \
>> +       {                                                             \
>> +               asan_report((s8a *)addr, size, false);                \
>> +       }                                                             \
>> +       __hidden void __asan_report_load##size##_noabort(void *addr)  \
>> +       {                                                             \
>> +               asan_report((s8a *)addr, size, false);                \
>> +       }
>> +
>> +DEFINE_ASAN_LOAD_STORE(1);
>> +DEFINE_ASAN_LOAD_STORE(2);
>> +DEFINE_ASAN_LOAD_STORE(4);
>> +DEFINE_ASAN_LOAD_STORE(8);
>> +
>> +void __asan_storeN(void *addr, ssize_t size)
>> +{
>> +       check_region_inline(addr, size, true);
>> +}
>> +
>> +void __asan_loadN(void *addr, ssize_t size)
>> +{
>> +       check_region_inline(addr, size, false);
>> +}
>> +
>> +/*
>> + * We currently do not sanitize globals.
>> + */
>> +void __asan_register_globals(void *globals, size_t n)
>> +{
>> +}
>> +
>> +void __asan_unregister_globals(void *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")
>> +__hidden __noasan int asan_init(struct asan_init_args *args)
>> +{
>> +       u64 globals_pages = args->arena_globals_pages;
>> +       u64 all_pages = args->arena_all_pages;
>> +       u64 shadowmap, shadow_pgoff;
>
> shadowmap stands out. shadow_map ?
>
>> +       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;
>> +
>> +       /*
>> +        * Make sure the numbers provided by userspace are sane.
>> +        */
>
> not a helpful comment.
>
>> +       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.
>> +        */
>> +       shadowmap = (u64)bpf_arena_alloc_pages(
>> +               &arena, (void __arena *)__asan_shadow_memory_dynamic_address,
>> +               shadow_pages, NUMA_NO_NODE, 0);
>> +       if (!shadowmap) {
>> +               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";
>> diff --git a/tools/testing/selftests/bpf/libarena/src/common.bpf.c b/tools/testing/selftests/bpf/libarena/src/common.bpf.c
>> index cbb729290d3c..31acb259ec5a 100644
>> --- a/tools/testing/selftests/bpf/libarena/src/common.bpf.c
>> +++ b/tools/testing/selftests/bpf/libarena/src/common.bpf.c
>> @@ -1,6 +1,7 @@
>>  // SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
>>  /* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
>>  #include <common.h>
>> +#include <asan.h>
>>
>>  const volatile u32 zero = 0;
>>
>> --
>> 2.53.0
>>


  reply	other threads:[~2026-04-09  0:38 UTC|newest]

Thread overview: 30+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-04-07  4:57 [PATCH bpf-next v4 0/9] Introduce arena library and runtime Emil Tsalapatis
2026-04-07  4:57 ` [PATCH bpf-next v4 1/9] bpf: Upgrade scalar to PTR_TO_ARENA on arena pointer addition Emil Tsalapatis
2026-04-07  5:43   ` bot+bpf-ci
2026-04-07  5:52   ` Leon Hwang
2026-04-07 16:10   ` Alexei Starovoitov
2026-04-09  0:15     ` Emil Tsalapatis
2026-04-09  2:17       ` Alexei Starovoitov
2026-04-07  4:57 ` [PATCH bpf-next v4 2/9] selftests/bpf: Add test for scalar/arena " Emil Tsalapatis
2026-04-07  4:57 ` [PATCH bpf-next v4 3/9] selftests/bpf: Move bpf_arena_spin_lock.h to the top level Emil Tsalapatis
2026-04-09  1:22   ` Song Liu
2026-04-07  4:57 ` [PATCH bpf-next v4 4/9] selftests/bpf: Deduplicate WRITE_ONCE macro between headers Emil Tsalapatis
2026-04-07  4:57 ` [PATCH bpf-next v4 5/9] selftests/bpf: Add basic libarena scaffolding Emil Tsalapatis
2026-04-07 16:21   ` Alexei Starovoitov
2026-04-09  0:18     ` Emil Tsalapatis
2026-04-09  2:21       ` Alexei Starovoitov
2026-04-07  4:57 ` [PATCH bpf-next v4 6/9] selftests/bpf: Add arena ASAN runtime to libarena Emil Tsalapatis
2026-04-07 16:39   ` Alexei Starovoitov
2026-04-09  0:38     ` Emil Tsalapatis [this message]
2026-04-09  2:28       ` Alexei Starovoitov
2026-04-07  4:57 ` [PATCH bpf-next v4 7/9] selftests/bpf: Add ASAN support for libarena selftests Emil Tsalapatis
2026-04-07 17:12   ` Alexei Starovoitov
2026-04-07  4:57 ` [PATCH bpf-next v4 8/9] selftests/bpf: Add buddy allocator for libarena Emil Tsalapatis
2026-04-07  5:43   ` bot+bpf-ci
2026-04-07 17:07   ` Alexei Starovoitov
2026-04-09  0:53     ` Emil Tsalapatis
2026-04-09  2:33       ` Alexei Starovoitov
2026-04-09 10:25         ` Puranjay Mohan
2026-04-09 10:40           ` Puranjay Mohan
2026-04-07  4:57 ` [PATCH bpf-next v4 9/9] selftests/bpf: Add selftests for libarena buddy allocator Emil Tsalapatis
2026-04-07 17:14   ` Alexei Starovoitov

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=DHO7DJ574EL1.8O0NIINEBJMR@etsalapatis.com \
    --to=emil@etsalapatis.com \
    --cc=alexei.starovoitov@gmail.com \
    --cc=andrii@kernel.org \
    --cc=ast@kernel.org \
    --cc=bpf@vger.kernel.org \
    --cc=daniel@iogearbox.net \
    --cc=eddyz87@gmail.com \
    --cc=memxor@gmail.com \
    --cc=song@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox