summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexei Starovoitov <ast@kernel.org>2026-04-26 18:12:22 -0700
committerAlexei Starovoitov <ast@kernel.org>2026-04-26 18:12:22 -0700
commit7c8d208d816d0504aa916138ae097d9cb4ed4e56 (patch)
tree803d9494183fa691b0f88d017f42f1e6a0929c7f
parentfbb5ba99e273843ae8d8bfcc6e2b700c8f5d933a (diff)
parent554e4eb9e4b75358f73733e2be7a59aaf4b7875e (diff)
Merge branch 'introduce-arena-library-and-runtime'
Emil Tsalapatis says: ==================== Introduce arena library and runtime Add a new subdirectory to tools/testing/selftests/bpf called libarena, along with programs useful for writing arena-based BPF code. This patchset adds the following: 1) libarena, a subdirectory where arena BPF code that is generally useful to BPF arena programs can be easily added and tested. 2) An ASAN runtime for BPF arena programs. BPF arenas allow for accessing memory after it has been freed or if it is out of bounds, making it more difficult to triage bugs combined to regular BPF. Use LLVM's recently added support for address-space based sanitization to selectively sanitize just the arena accesses. 3) A buddy memory allocator that can be reused by BPF programs to handle memory allocation/deletion. The allocator uses the ASAN runtime to add address sanitization if requested. The patch includes testing for the new allocators and ASAN features that can be built from the top directory using "make libarena_test" and "make libarena_test_asan". The generated binaries reside in libarena/. The patch also adds test-progs-based selftests to the codebase for the libarena code, so the new tests are run by ./test_progs. The patchset has the following stucture: 1-3: Create basic libarena scaffolding and refactor existing headers. 4-5: Add the ASAN runtime and associated scaffolding. 6-8: Add the new buddy memory allocator along with selftests. Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com> HISTORY ======= v8->v9 (https://lore.kernel.org/bpf/20260421165037.4736-1-emil@etsalapatis.com) - Added Matt's Acked-by for Patch 1 - Replaced open coded fls with __builtin based calculation (Matt) - Add comment explaining the reasoning behind the zero variable (Matt) - Remove the self-contained runner and present selftests as examples (Kumar). The reasoning is that including selftests with the library is not effective because the library will be sync'ed outside the tree infrequently, so it would be ineffective in catching issues before they land. - Adjust syscall API for clarity and forward compatibility (Matt) - Reset asan_validate state after failed check (Sashiko) - Fix printf format specifier (Sashiko) - Rename syscall to be generic arena_info (Matt) - Namespace libarena headers by moving them to their own directory - Rename selftest_helpers to libarena/userspace.h to reflect it is not just for selftests. v7->v8 (https://lore.kernel.org/bpf/20260412174546.18684-1-emil@etsalapatis.com) - Duplicate READ_ONCE/WRITE_ONCE instead of moving it to bpf_experimental.h to keep libarena self-contained (Kumar) - Add libarena_asan test to test_progs and conditionally compile it if suppported (Kumar) - Add stderr parsing for buddy tests when run under test_progs (Kumar) - Move all arena-related headers into libarena and add its include/ subdirectory in the standard include path (Kumar) - Remove silent-by-default ASAN, add help message on test_libarena explaining that -v emits the messages (Kumar) - Add run_prog_args as a libarena helper - Add explanation on the use of __weak for the spinlock qnodes v6->v7 (https://lore.kernel.org/bpf/20260412011857.3387-1-emil@etsalapatis.com) - Modify patch 1 to allow operations between PTR_TO_ARENA src_reg and dst_reg of any type. Adjust selftests accordingly (Alexei) - Remove unnecessary include in patch 5 (Song) - Removed unused definitions/assignments in patches 8/9, update patch descriptions v5->v6 (https://lore.kernel.org/bpf/20260410163041.8063-1-emil@etsalapatis.com) - Fix subreg_def management for SCALAR += PTR_TO_ARENA operations (AI) - Add more selftests for the SCALAR += PTR_TO_ARENA patch (Sashiko) - Adjust fls() operation to be in line with the kernel version (Sashiko) - Address Sashiko selftests and debugging nits - Add ASAN loadN and storeN _noabort variants and associated BTF anchor - Remove unnecessary bit freeing of buddies during block splitting v4->v5 (https://lore.kernel.org/bpf/20260407045730.13359-1-emil@etsalapatis.com) Omitting various nits and fixups. - Properly adjust subreg_def for scalar += ptr_to_arena calls (Sashiko) - Remove extraneous definition from prog_tests/arena_spin_lock.c (Song) - Trim extraneous comments from ASAN and buddy (Alexei) - Remove asan_dummy call and replace with function pointer array (Alexei) - Remove usersapi.h header and merge it into common.h (Alexei) - Replace ASAN macros with function calls (Alexei) - Embed buddy lock into the struct and move the buddy allocator to __arena_global (Alexei) - Add commenting for buddy allocator constants (Alexei) - Add default buddy allocator directly in common.bpf.c, so that the user does not need to define it. - Expand test harnesses to dynamically find individual selftests. Now the selftests also reports each test individually (e.g., 5 entries for the buddy allocator instead of 1). This brings them to par with the rest of the test_progs. v3->v4 (https://lore.kernel.org/bpf/20260403042720.18862-1-emil@etsalapatis.com) - Add Acks by Song to patches 1-4. - Expand the verifier's handling of scalar/arena operations to include all 3-operand operations in Patch 1 (Alexei) - Add additional tests for arena/arena (allowed) and arena/pointer (not allowed) operations in Patch 2 - Remove ASAN version of the library from default compilation since it requires LLVM 22 and up (CI) - Rework buddy allocator locking for clarity and add comments - Fix From: email to be consistent with SOB - Address (most) Sashiko comments v2->v3 (https://lore.kernel.org/bpf/20260127181610.86376-1-emil@etsalapatis.com) Nonexhaustive due to significant patch rework. - Do not duplicate WRITE_ONCE macro (Mykyta, Kumar) - Add SPDX headers (Alexei) - Remove bump/stack allocators (Alexei) - Integrate testing with test_progs (Kumar) - Add short description of ASAN algorithm at the top of the file (Alexei) v1->v2 (https://lore.kernel.org/bpf/20260122160131.2238331-1-etsal@meta.com/) - Added missing format string argument (AI) - Fix outdated selftests prog name check (AI) - Fixed stack allocation check for segment creation (AI) - Fix errors in non-ASAN bump allocator selftests (AI) - Propagate error value from individual selftests in selftest.c - Removed embedded metadata from bump allocator as it was needlessly complicating its behavior ==================== Link: https://patch.msgid.link/20260426190338.4615-1-emil@etsalapatis.com Signed-off-by: Alexei Starovoitov <ast@kernel.org>
-rw-r--r--tools/testing/selftests/bpf/Makefile53
-rw-r--r--tools/testing/selftests/bpf/bpf_arena_alloc.h2
-rw-r--r--tools/testing/selftests/bpf/bpf_arena_list.h2
-rw-r--r--tools/testing/selftests/bpf/bpf_arena_strsearch.h2
-rw-r--r--tools/testing/selftests/bpf/bpf_experimental.h84
-rw-r--r--tools/testing/selftests/bpf/default.profrawbin0 -> 160 bytes
-rw-r--r--tools/testing/selftests/bpf/libarena/Makefile94
-rw-r--r--tools/testing/selftests/bpf/libarena/include/bpf_arena_common.h (renamed from tools/testing/selftests/bpf/bpf_arena_common.h)0
-rw-r--r--tools/testing/selftests/bpf/libarena/include/bpf_arena_spin_lock.h (renamed from tools/testing/selftests/bpf/progs/bpf_arena_spin_lock.h)11
-rw-r--r--tools/testing/selftests/bpf/libarena/include/bpf_atomic.h (renamed from tools/testing/selftests/bpf/bpf_atomic.h)4
-rw-r--r--tools/testing/selftests/bpf/libarena/include/bpf_may_goto.h84
-rw-r--r--tools/testing/selftests/bpf/libarena/include/libarena/asan.h103
-rw-r--r--tools/testing/selftests/bpf/libarena/include/libarena/buddy.h92
-rw-r--r--tools/testing/selftests/bpf/libarena/include/libarena/common.h94
-rw-r--r--tools/testing/selftests/bpf/libarena/include/libarena/userspace.h132
-rw-r--r--tools/testing/selftests/bpf/libarena/selftests/st_asan_buddy.bpf.c258
-rw-r--r--tools/testing/selftests/bpf/libarena/selftests/st_asan_common.h52
-rw-r--r--tools/testing/selftests/bpf/libarena/selftests/st_buddy.bpf.c209
-rw-r--r--tools/testing/selftests/bpf/libarena/selftests/test_progs_compat.h15
-rw-r--r--tools/testing/selftests/bpf/libarena/src/asan.bpf.c553
-rw-r--r--tools/testing/selftests/bpf/libarena/src/buddy.bpf.c903
-rw-r--r--tools/testing/selftests/bpf/libarena/src/common.bpf.c52
-rw-r--r--tools/testing/selftests/bpf/prog_tests/arena_spin_lock.c7
-rw-r--r--tools/testing/selftests/bpf/prog_tests/libarena.c66
-rw-r--r--tools/testing/selftests/bpf/prog_tests/libarena_asan.c93
-rw-r--r--tools/testing/selftests/bpf/progs/arena_atomics.c2
-rw-r--r--tools/testing/selftests/bpf/progs/arena_spin_lock.c2
-rw-r--r--tools/testing/selftests/bpf/progs/compute_live_registers.c2
-rw-r--r--tools/testing/selftests/bpf/progs/lpm_trie_bench.c2
-rw-r--r--tools/testing/selftests/bpf/progs/stream.c2
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_arena.c2
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_arena_globals1.c2
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_arena_globals2.c2
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_arena_large.c2
-rw-r--r--tools/testing/selftests/bpf/progs/verifier_ldsx.c2
-rw-r--r--tools/testing/selftests/bpf/test_loader.c51
-rw-r--r--tools/testing/selftests/bpf/test_progs.h2
37 files changed, 2917 insertions, 121 deletions
diff --git a/tools/testing/selftests/bpf/Makefile b/tools/testing/selftests/bpf/Makefile
index ac676d2a4a29..97ee61f2ade5 100644
--- a/tools/testing/selftests/bpf/Makefile
+++ b/tools/testing/selftests/bpf/Makefile
@@ -56,7 +56,8 @@ CFLAGS += -g $(OPT_FLAGS) -rdynamic -std=gnu11 \
-Wno-unused-but-set-variable \
$(GENFLAGS) $(SAN_CFLAGS) $(LIBELF_CFLAGS) \
-I$(CURDIR) -I$(INCLUDE_DIR) -I$(GENDIR) -I$(LIBDIR) \
- -I$(TOOLSINCDIR) -I$(TOOLSARCHINCDIR) -I$(APIDIR) -I$(OUTPUT)
+ -I$(TOOLSINCDIR) -I$(TOOLSARCHINCDIR) -I$(APIDIR) -I$(OUTPUT) \
+ -I$(CURDIR)/libarena/include
LDFLAGS += $(SAN_LDFLAGS)
LDLIBS += $(LIBELF_LIBS) -lz -lrt -lpthread
@@ -78,6 +79,12 @@ ifneq ($(shell $(CLANG) --target=bpf -mcpu=help 2>&1 | grep 'v4'),)
CLANG_CPUV4 := 1
endif
+# Check whether clang supports BPF address sanitizer (requires LLVM 22+)
+CLANG_HAS_ARENA_ASAN := $(shell echo 'int x;' | \
+ $(CLANG) --target=bpf -fsanitize=kernel-address \
+ -mllvm -asan-shadow-addr-space=1 \
+ -x c -c - -o /dev/null 2>/dev/null && echo 1)
+
# Order correspond to 'make run_tests' order
TEST_GEN_PROGS = test_verifier test_tag test_maps test_lru_map test_progs \
test_sockmap \
@@ -151,6 +158,7 @@ override define CLEAN
$(Q)$(RM) -r $(TEST_KMODS)
$(Q)$(RM) -r $(EXTRA_CLEAN)
$(Q)$(MAKE) -C test_kmods clean
+ $(Q)$(MAKE) -C libarena clean
$(Q)$(MAKE) docs-clean
endef
@@ -443,6 +451,7 @@ endif
CLANG_SYS_INCLUDES = $(call get_sys_includes,$(CLANG),$(CLANG_TARGET_ARCH))
BPF_CFLAGS = -g -Wall -Werror -D__TARGET_ARCH_$(SRCARCH) $(MENDIAN) \
-I$(INCLUDE_DIR) -I$(CURDIR) -I$(APIDIR) \
+ -I$(CURDIR)/libarena/include \
-I$(abspath $(OUTPUT)/../usr/include) \
-std=gnu11 \
-fno-strict-aliasing \
@@ -522,6 +531,7 @@ LINKED_BPF_OBJS := $(foreach skel,$(LINKED_SKELS),$($(skel)-deps))
LINKED_BPF_SRCS := $(patsubst %.bpf.o,%.c,$(LINKED_BPF_OBJS))
HEADERS_FOR_BPF_OBJS := $(wildcard $(BPFDIR)/*.bpf.h) \
+ $(wildcard $(CURDIR)/libarena/include/*.[ch]) \
$(addprefix $(BPFDIR)/, bpf_core_read.h \
bpf_endian.h \
bpf_helpers.h \
@@ -737,6 +747,37 @@ $(VERIFY_SIG_HDR): $(VERIFICATION_CERT)
echo "};"; \
echo "unsigned int test_progs_verification_cert_len = $$(wc -c < $<);") > $@
+LIBARENA_MAKE_ARGS = \
+ BPFTOOL="$(BPFTOOL)" \
+ INCLUDE_DIR="$(INCLUDE_DIR)" \
+ LIBBPF_INCLUDE="$(HOST_INCLUDE_DIR)" \
+ BPFOBJ="$(BPFOBJ)" \
+ LDLIBS="$(LDLIBS) -lzstd" \
+ CLANG="$(CLANG)" \
+ BPF_CFLAGS="$(BPF_CFLAGS) $(CLANG_CFLAGS)" \
+ BPF_TARGET_ENDIAN="$(BPF_TARGET_ENDIAN)" \
+ Q="$(Q)"
+
+LIBARENA_BPF_DEPS := $(wildcard libarena/Makefile \
+ libarena/include/* \
+ libarena/include/libarena/* \
+ libarena/src/* \
+ libarena/selftests/* \
+ libarena/*.bpf.o)
+
+LIBARENA_SKEL := libarena/libarena.skel.h
+
+$(LIBARENA_SKEL): $(INCLUDE_DIR)/vmlinux.h $(BPFOBJ) $(LIBARENA_BPF_DEPS)
+ +$(MAKE) -C libarena libarena.skel.h $(LIBARENA_MAKE_ARGS)
+
+ifneq ($(CLANG_HAS_ARENA_ASAN),)
+LIBARENA_ASAN_SKEL := libarena/libarena_asan.skel.h
+CFLAGS += -DHAS_BPF_ARENA_ASAN
+
+$(LIBARENA_ASAN_SKEL): $(INCLUDE_DIR)/vmlinux.h $(BPFOBJ) $(LIBARENA_BPF_DEPS)
+ +$(MAKE) -C libarena libarena_asan.skel.h $(LIBARENA_MAKE_ARGS)
+endif
+
# Define test_progs test runner.
TRUNNER_TESTS_DIR := prog_tests
TRUNNER_BPF_PROGS_DIR := progs
@@ -761,7 +802,9 @@ TRUNNER_EXTRA_SOURCES := test_progs.c \
flow_dissector_load.h \
ip_check_defrag_frags.h \
bpftool_helpers.c \
- usdt_1.c usdt_2.c
+ usdt_1.c usdt_2.c \
+ $(LIBARENA_SKEL) \
+ $(LIBARENA_ASAN_SKEL)
TRUNNER_LIB_SOURCES := find_bit.c
TRUNNER_EXTRA_FILES := $(OUTPUT)/urandom_read \
$(OUTPUT)/liburandom_read.so \
@@ -930,3 +973,9 @@ override define INSTALL_RULE
rsync -a $(OUTPUT)/$$DIR/*.bpf.o $(INSTALL_PATH)/$$DIR;\
done
endef
+
+libarena: $(LIBARENA_SKEL)
+
+ifneq ($(CLANG_HAS_ARENA_ASAN),)
+libarena_asan: $(LIBARENA_ASAN_SKEL)
+endif
diff --git a/tools/testing/selftests/bpf/bpf_arena_alloc.h b/tools/testing/selftests/bpf/bpf_arena_alloc.h
index c27678299e0c..cda147fd9d25 100644
--- a/tools/testing/selftests/bpf/bpf_arena_alloc.h
+++ b/tools/testing/selftests/bpf/bpf_arena_alloc.h
@@ -1,7 +1,7 @@
/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
/* Copyright (c) 2024 Meta Platforms, Inc. and affiliates. */
#pragma once
-#include "bpf_arena_common.h"
+#include <bpf_arena_common.h>
#ifndef __round_mask
#define __round_mask(x, y) ((__typeof__(x))((y)-1))
diff --git a/tools/testing/selftests/bpf/bpf_arena_list.h b/tools/testing/selftests/bpf/bpf_arena_list.h
index e16fa7d95fcf..1af2ffc27d9c 100644
--- a/tools/testing/selftests/bpf/bpf_arena_list.h
+++ b/tools/testing/selftests/bpf/bpf_arena_list.h
@@ -1,7 +1,7 @@
/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
/* Copyright (c) 2024 Meta Platforms, Inc. and affiliates. */
#pragma once
-#include "bpf_arena_common.h"
+#include <bpf_arena_common.h>
struct arena_list_node;
diff --git a/tools/testing/selftests/bpf/bpf_arena_strsearch.h b/tools/testing/selftests/bpf/bpf_arena_strsearch.h
index c1b6eaa905bb..f0d575daef5a 100644
--- a/tools/testing/selftests/bpf/bpf_arena_strsearch.h
+++ b/tools/testing/selftests/bpf/bpf_arena_strsearch.h
@@ -1,7 +1,7 @@
/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
/* Copyright (c) 2025 Meta Platforms, Inc. and affiliates. */
#pragma once
-#include "bpf_arena_common.h"
+#include <bpf_arena_common.h>
__noinline int bpf_arena_strlen(const char __arena *s __arg_arena)
{
diff --git a/tools/testing/selftests/bpf/bpf_experimental.h b/tools/testing/selftests/bpf/bpf_experimental.h
index 2234bd6bc9d3..d1db355e872b 100644
--- a/tools/testing/selftests/bpf/bpf_experimental.h
+++ b/tools/testing/selftests/bpf/bpf_experimental.h
@@ -5,6 +5,7 @@
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
+#include <bpf_may_goto.h>
#define __contains(name, node) __attribute__((btf_decl_tag("contains:" #name ":" #node)))
@@ -204,89 +205,6 @@ l_true: \
})
#endif
-/*
- * Note that cond_break can only be portably used in the body of a breakable
- * construct, whereas can_loop can be used anywhere.
- */
-#ifdef __BPF_FEATURE_MAY_GOTO
-#define can_loop \
- ({ __label__ l_break, l_continue; \
- bool ret = true; \
- asm volatile goto("may_goto %l[l_break]" \
- :::: l_break); \
- goto l_continue; \
- l_break: ret = false; \
- l_continue:; \
- ret; \
- })
-
-#define __cond_break(expr) \
- ({ __label__ l_break, l_continue; \
- asm volatile goto("may_goto %l[l_break]" \
- :::: l_break); \
- goto l_continue; \
- l_break: expr; \
- l_continue:; \
- })
-#else
-#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
-#define can_loop \
- ({ __label__ l_break, l_continue; \
- bool ret = true; \
- asm volatile goto("1:.byte 0xe5; \
- .byte 0; \
- .long ((%l[l_break] - 1b - 8) / 8) & 0xffff; \
- .short 0" \
- :::: l_break); \
- goto l_continue; \
- l_break: ret = false; \
- l_continue:; \
- ret; \
- })
-
-#define __cond_break(expr) \
- ({ __label__ l_break, l_continue; \
- asm volatile goto("1:.byte 0xe5; \
- .byte 0; \
- .long ((%l[l_break] - 1b - 8) / 8) & 0xffff; \
- .short 0" \
- :::: l_break); \
- goto l_continue; \
- l_break: expr; \
- l_continue:; \
- })
-#else
-#define can_loop \
- ({ __label__ l_break, l_continue; \
- bool ret = true; \
- asm volatile goto("1:.byte 0xe5; \
- .byte 0; \
- .long (((%l[l_break] - 1b - 8) / 8) & 0xffff) << 16; \
- .short 0" \
- :::: l_break); \
- goto l_continue; \
- l_break: ret = false; \
- l_continue:; \
- ret; \
- })
-
-#define __cond_break(expr) \
- ({ __label__ l_break, l_continue; \
- asm volatile goto("1:.byte 0xe5; \
- .byte 0; \
- .long (((%l[l_break] - 1b - 8) / 8) & 0xffff) << 16; \
- .short 0" \
- :::: l_break); \
- goto l_continue; \
- l_break: expr; \
- l_continue:; \
- })
-#endif
-#endif
-
-#define cond_break __cond_break(break)
-#define cond_break_label(label) __cond_break(goto label)
-
#ifndef bpf_nop_mov
#define bpf_nop_mov(var) \
asm volatile("%[reg]=%[reg]"::[reg]"r"((short)var))
diff --git a/tools/testing/selftests/bpf/default.profraw b/tools/testing/selftests/bpf/default.profraw
new file mode 100644
index 000000000000..e865e87829f8
--- /dev/null
+++ b/tools/testing/selftests/bpf/default.profraw
Binary files differ
diff --git a/tools/testing/selftests/bpf/libarena/Makefile b/tools/testing/selftests/bpf/libarena/Makefile
new file mode 100644
index 000000000000..3c695f9c0054
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/Makefile
@@ -0,0 +1,94 @@
+# SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+# Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
+
+.PHONY: clean
+
+# Defaults for standalone builds
+
+CLANG ?= clang
+BPFTOOL ?= bpftool
+LDLIBS ?= -lbpf -lelf -lz -lrt -lpthread -lzstd
+
+ifeq ($(V),1)
+Q =
+msg =
+else
+Q ?= @
+msg = @printf ' %-8s%s %s%s\n' "$(1)" "$(if $(2), [$(2)])" "$(notdir $(3))" "$(if $(4), $(4))";
+endif
+
+IS_LITTLE_ENDIAN = $(shell $(CC) -dM -E - </dev/null | \
+ grep 'define __BYTE_ORDER__ __ORDER_LITTLE_ENDIAN__')
+BPF_TARGET_ENDIAN ?= $(if $(IS_LITTLE_ENDIAN),--target=bpfel,--target=bpfeb)
+
+LIBARENA=$(abspath .)
+BPFDIR=$(abspath $(LIBARENA)/..)
+
+INCLUDE_DIR ?= $(BPFDIR)/tools/include
+LIBBPF_INCLUDE ?= $(INCLUDE_DIR)
+
+# Scan src/ and selftests/ to generate the final binaries
+LIBARENA_SOURCES = $(wildcard $(LIBARENA)/src/*.bpf.c) $(wildcard $(LIBARENA)/selftests/*.bpf.c)
+LIBARENA_OBJECTS = $(notdir $(LIBARENA_SOURCES:.bpf.c=.bpf.o))
+LIBARENA_OBJECTS_ASAN = $(notdir $(LIBARENA_SOURCES:.bpf.c=_asan.bpf.o))
+
+INCLUDES = -I$(LIBARENA)/include -I$(BPFDIR)
+ifneq ($(INCLUDE_DIR),)
+INCLUDES += -I$(INCLUDE_DIR)
+endif
+ifneq ($(LIBBPF_INCLUDE),)
+INCLUDES += -I$(LIBBPF_INCLUDE)
+endif
+
+ASAN_FLAGS = -fsanitize=kernel-address -fno-stack-protector -fno-builtin
+ASAN_FLAGS += -mllvm -asan-instrument-address-spaces=1 -mllvm -asan-shadow-addr-space=1
+ASAN_FLAGS += -mllvm -asan-use-stack-safety=0 -mllvm -asan-stack=0
+ASAN_FLAGS += -mllvm -asan-kernel=1
+ASAN_FLAGS += -mllvm -asan-constructor-kind=none
+ASAN_FLAGS += -mllvm -asan-destructor-kind=none
+
+# ENABLE_ATOMICS_TESTS required because we use arena spinlocks
+override BPF_CFLAGS += -DENABLE_ATOMICS_TESTS
+override BPF_CFLAGS += -O2 -g
+override BPF_CFLAGS += -Wno-incompatible-pointer-types-discards-qualifiers
+# Required to define our own arena-based free()
+override BPF_CFLAGS += -Wno-incompatible-library-redeclaration
+# Required for suppressing harmless vmlinux.h-related warnings.
+override BPF_CFLAGS += -Wno-missing-declarations
+override BPF_CFLAGS += $(INCLUDES)
+
+CFLAGS = -O2 -no-pie
+CFLAGS += $(INCLUDES)
+
+vpath %.bpf.c $(LIBARENA)/src $(LIBARENA)/selftests
+vpath %.c $(LIBARENA)/src $(LIBARENA)/selftests
+
+skeletons: libarena.skel.h libarena_asan.skel.h
+.PHONY: skeletons
+
+libarena_asan.skel.h: libarena_asan.bpf.o
+ $(call msg,GEN-SKEL,libarena,$@)
+ $(Q)$(BPFTOOL) gen skeleton $< name "libarena_asan" > $@
+
+libarena.skel.h: libarena.bpf.o
+ $(call msg,GEN-SKEL,libarena,$@)
+ $(Q)$(BPFTOOL) gen skeleton $< name "libarena" > $@
+
+libarena_asan.bpf.o: $(LIBARENA_OBJECTS_ASAN)
+ $(call msg,GEN-OBJ,libarena,$@)
+ $(Q)$(BPFTOOL) gen object $@ $^
+
+libarena.bpf.o: $(LIBARENA_OBJECTS)
+ $(call msg,GEN-OBJ,libarena,$@)
+ $(Q)$(BPFTOOL) gen object $@ $^
+
+%_asan.bpf.o: %.bpf.c
+ $(call msg,CLNG-BPF,libarena,$@)
+ $(Q)$(CLANG) $(BPF_CFLAGS) $(ASAN_FLAGS) -DBPF_ARENA_ASAN $(BPF_TARGET_ENDIAN) -c $< -o $@
+
+%.bpf.o: %.bpf.c
+ $(call msg,CLNG-BPF,libarena,$@)
+ $(Q)$(CLANG) $(BPF_CFLAGS) $(BPF_TARGET_ENDIAN) -c $< -o $@
+
+clean:
+ $(Q)rm -f *.skel.h *.bpf.o *.linked*.o
diff --git a/tools/testing/selftests/bpf/bpf_arena_common.h b/tools/testing/selftests/bpf/libarena/include/bpf_arena_common.h
index 16f8ce832004..16f8ce832004 100644
--- a/tools/testing/selftests/bpf/bpf_arena_common.h
+++ b/tools/testing/selftests/bpf/libarena/include/bpf_arena_common.h
diff --git a/tools/testing/selftests/bpf/progs/bpf_arena_spin_lock.h b/tools/testing/selftests/bpf/libarena/include/bpf_arena_spin_lock.h
index f90531cf3ee5..164638690a4d 100644
--- a/tools/testing/selftests/bpf/progs/bpf_arena_spin_lock.h
+++ b/tools/testing/selftests/bpf/libarena/include/bpf_arena_spin_lock.h
@@ -5,7 +5,7 @@
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
-#include "bpf_atomic.h"
+#include <bpf_atomic.h>
#define arch_mcs_spin_lock_contended_label(l, label) smp_cond_load_acquire_label(l, VAL, label)
#define arch_mcs_spin_unlock_contended(l) smp_store_release((l), 1)
@@ -107,7 +107,12 @@ struct arena_qnode {
#define _Q_LOCKED_VAL (1U << _Q_LOCKED_OFFSET)
#define _Q_PENDING_VAL (1U << _Q_PENDING_OFFSET)
-struct arena_qnode __arena qnodes[_Q_MAX_CPUS][_Q_MAX_NODES];
+/*
+ * The qnodes are marked __weak so we can define them in the header
+ * while still ensuring all compilation units use the same struct
+ * instance.
+ */
+struct arena_qnode __weak __arena __hidden qnodes[_Q_MAX_CPUS][_Q_MAX_NODES];
static inline u32 encode_tail(int cpu, int idx)
{
@@ -240,7 +245,7 @@ static __always_inline int arena_spin_trylock(arena_spinlock_t __arena *lock)
return likely(atomic_try_cmpxchg_acquire(&lock->val, &val, _Q_LOCKED_VAL));
}
-__noinline
+__noinline __weak
int arena_spin_lock_slowpath(arena_spinlock_t __arena __arg_arena *lock, u32 val)
{
struct arena_mcs_spinlock __arena *prev, *next, *node0, *node;
diff --git a/tools/testing/selftests/bpf/bpf_atomic.h b/tools/testing/selftests/bpf/libarena/include/bpf_atomic.h
index c550e5711967..b7b230431929 100644
--- a/tools/testing/selftests/bpf/bpf_atomic.h
+++ b/tools/testing/selftests/bpf/libarena/include/bpf_atomic.h
@@ -5,7 +5,7 @@
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
-#include "bpf_experimental.h"
+#include <bpf_may_goto.h>
extern bool CONFIG_X86_64 __kconfig __weak;
@@ -42,7 +42,9 @@ extern bool CONFIG_X86_64 __kconfig __weak;
#define READ_ONCE(x) (*(volatile typeof(x) *)&(x))
+#ifndef WRITE_ONCE
#define WRITE_ONCE(x, val) ((*(volatile typeof(x) *)&(x)) = (val))
+#endif
#define cmpxchg(p, old, new) __sync_val_compare_and_swap((p), old, new)
diff --git a/tools/testing/selftests/bpf/libarena/include/bpf_may_goto.h b/tools/testing/selftests/bpf/libarena/include/bpf_may_goto.h
new file mode 100644
index 000000000000..9ba90689d6ba
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/include/bpf_may_goto.h
@@ -0,0 +1,84 @@
+#pragma once
+
+/*
+ * Note that cond_break can only be portably used in the body of a breakable
+ * construct, whereas can_loop can be used anywhere.
+ */
+#ifdef __BPF_FEATURE_MAY_GOTO
+#define can_loop \
+ ({ __label__ l_break, l_continue; \
+ bool ret = true; \
+ asm volatile goto("may_goto %l[l_break]" \
+ :::: l_break); \
+ goto l_continue; \
+ l_break: ret = false; \
+ l_continue:; \
+ ret; \
+ })
+
+#define __cond_break(expr) \
+ ({ __label__ l_break, l_continue; \
+ asm volatile goto("may_goto %l[l_break]" \
+ :::: l_break); \
+ goto l_continue; \
+ l_break: expr; \
+ l_continue:; \
+ })
+#else
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+#define can_loop \
+ ({ __label__ l_break, l_continue; \
+ bool ret = true; \
+ asm volatile goto("1:.byte 0xe5; \
+ .byte 0; \
+ .long ((%l[l_break] - 1b - 8) / 8) & 0xffff; \
+ .short 0" \
+ :::: l_break); \
+ goto l_continue; \
+ l_break: ret = false; \
+ l_continue:; \
+ ret; \
+ })
+
+#define __cond_break(expr) \
+ ({ __label__ l_break, l_continue; \
+ asm volatile goto("1:.byte 0xe5; \
+ .byte 0; \
+ .long ((%l[l_break] - 1b - 8) / 8) & 0xffff; \
+ .short 0" \
+ :::: l_break); \
+ goto l_continue; \
+ l_break: expr; \
+ l_continue:; \
+ })
+#else
+#define can_loop \
+ ({ __label__ l_break, l_continue; \
+ bool ret = true; \
+ asm volatile goto("1:.byte 0xe5; \
+ .byte 0; \
+ .long (((%l[l_break] - 1b - 8) / 8) & 0xffff) << 16; \
+ .short 0" \
+ :::: l_break); \
+ goto l_continue; \
+ l_break: ret = false; \
+ l_continue:; \
+ ret; \
+ })
+
+#define __cond_break(expr) \
+ ({ __label__ l_break, l_continue; \
+ asm volatile goto("1:.byte 0xe5; \
+ .byte 0; \
+ .long (((%l[l_break] - 1b - 8) / 8) & 0xffff) << 16; \
+ .short 0" \
+ :::: l_break); \
+ goto l_continue; \
+ l_break: expr; \
+ l_continue:; \
+ })
+#endif
+#endif
+
+#define cond_break __cond_break(break)
+#define cond_break_label(label) __cond_break(goto label)
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/asan.h b/tools/testing/selftests/bpf/libarena/include/libarena/asan.h
new file mode 100644
index 000000000000..eb9fc69d9eb0
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/include/libarena/asan.h
@@ -0,0 +1,103 @@
+// 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__ */
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
new file mode 100644
index 000000000000..00e2437128ef
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/include/libarena/buddy.h
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#pragma once
+
+struct buddy_chunk;
+typedef struct buddy_chunk __arena buddy_chunk_t;
+
+struct buddy_header;
+typedef struct buddy_header __arena buddy_header_t;
+
+enum buddy_consts {
+ /*
+ * Minimum allocation is 1 << BUDDY_MIN_ALLOC_SHIFT.
+ * Larger sizes increase internal fragmentation, but smaller
+ * sizes increase the space overhead of the block metadata.
+ */
+ BUDDY_MIN_ALLOC_SHIFT = 4,
+ BUDDY_MIN_ALLOC_BYTES = 1 << BUDDY_MIN_ALLOC_SHIFT,
+
+ /*
+ * How many orders the buddy allocator can serve. Minimum block
+ * size is 1 << BUDDY_MIN_ALLOC_SHIFT, maximum block size is
+ * 1 << (BUDDY_MIN_ALLOC_SHIFT + BUDDY_CHUNK_NUM_ORDERS - 1):
+ * Each block has size 1 << BUDDY_MIN_ALLOC_SHIFT, and the
+ * allocation orders are in [0, BUDDY_CHUNK_NUM_ORDERS).
+ * We keep two blocks of the maximum size to retain the
+ * property in the code that all blocks have a buddy.
+ * Higher values increase the maximum allocation size,
+ * but also the size of the metadata for each block.
+ */
+ BUDDY_CHUNK_NUM_ORDERS = 1 << 4,
+ BUDDY_CHUNK_BYTES = BUDDY_MIN_ALLOC_BYTES << (BUDDY_CHUNK_NUM_ORDERS),
+
+ /* Offset of the buddy header within a free block, see buddy.bpf.c for details */
+ BUDDY_HEADER_OFF = 8,
+
+ /* The maximum number of blocks a chunk may have to track. */
+ BUDDY_CHUNK_ITEMS = 1 << (BUDDY_CHUNK_NUM_ORDERS),
+ BUDDY_CHUNK_OFFSET_MASK = BUDDY_CHUNK_BYTES - 1,
+
+ /*
+ * Alignment for chunk allocations based on bpf_arena_alloc_pages.
+ * The arena allocation kfunc does not have an alignment argument,
+ * but that is required for all block calculations in the chunk to
+ * work.
+ */
+ BUDDY_VADDR_OFFSET = BUDDY_CHUNK_BYTES,
+
+ /* Total arena virtual address space the allocator can consume. */
+ BUDDY_VADDR_SIZE = BUDDY_CHUNK_BYTES << 10
+};
+
+struct buddy_header {
+ u32 prev_index; /* "Pointer" to the previous available allocation of the same size. */
+ u32 next_index; /* Same for the next allocation. */
+};
+
+/*
+ * We bring memory into the allocator 1 MiB at a time.
+ */
+struct buddy_chunk {
+ /* The order of the current allocation for a item. 4 bits per order. */
+ u8 orders[BUDDY_CHUNK_ITEMS / 2];
+ /*
+ * Bit to denote whether chunk is allocated. Size of the allocated/free
+ * chunk found from the orders array.
+ */
+ u8 allocated[BUDDY_CHUNK_ITEMS / 8];
+ /* Freelists for O(1) allocation. */
+ u64 freelists[BUDDY_CHUNK_NUM_ORDERS];
+ buddy_chunk_t *next;
+};
+
+struct buddy {
+ buddy_chunk_t *first_chunk; /* Pointer to the chunk linked list. */
+ arena_spinlock_t lock; /* Allocator lock */
+ u64 vaddr; /* Allocation into reserved vaddr */
+};
+
+typedef struct buddy __arena buddy_t;
+
+#ifdef __BPF__
+
+int buddy_init(buddy_t *buddy);
+int buddy_destroy(buddy_t *buddy);
+int buddy_free_internal(buddy_t *buddy, u64 free);
+#define buddy_free(buddy, ptr) buddy_free_internal((buddy), (u64)(ptr))
+u64 buddy_alloc_internal(buddy_t *buddy, size_t size);
+#define buddy_alloc(alloc, size) ((void __arena *)buddy_alloc_internal((alloc), (size)))
+
+
+#endif /* __BPF__ */
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/common.h b/tools/testing/selftests/bpf/libarena/include/libarena/common.h
new file mode 100644
index 000000000000..e54cb7b869bd
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/include/libarena/common.h
@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#pragma once
+
+#ifdef __BPF__
+
+#include <vmlinux.h>
+
+#include <bpf_arena_common.h>
+#include <bpf_arena_spin_lock.h>
+
+#include <asm-generic/errno.h>
+
+#ifndef __BPF_FEATURE_ADDR_SPACE_CAST
+#error "Arena allocators require bpf_addr_space_cast feature"
+#endif
+
+#define arena_stdout(fmt, ...) bpf_stream_printk(1, (fmt), ##__VA_ARGS__)
+#define arena_stderr(fmt, ...) bpf_stream_printk(2, (fmt), ##__VA_ARGS__)
+
+#ifndef __maybe_unused
+#define __maybe_unused __attribute__((__unused__))
+#endif
+
+#define private(name) SEC(".data." #name) __hidden __attribute__((aligned(8)))
+
+#define ARENA_PAGES (1UL << (32 - __builtin_ffs(__PAGE_SIZE) + 1))
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ __uint(max_entries, ARENA_PAGES); /* number of pages */
+#if defined(__TARGET_ARCH_arm64) || defined(__aarch64__)
+ __ulong(map_extra, (1ull << 32)); /* start of mmap() region */
+#else
+ __ulong(map_extra, (1ull << 44)); /* start of mmap() region */
+#endif
+} arena __weak SEC(".maps");
+
+/*
+ * This is a variable used to aid verification. The may_goto directive
+ * permits open-coded for loops, but requires that the index variable is
+ * imprecise. To force the variable to be imprecise, initialize it with
+ * the opaque volatile variable 0 instead of the constant 0.
+ */
+extern const volatile u32 zero;
+extern volatile u64 asan_violated;
+
+int arena_fls(__u64 word);
+
+u64 malloc_internal(size_t size);
+#define malloc(size) ((void __arena *)malloc_internal((size)))
+void free(void __arena *ptr);
+
+/*
+ * The verifier associates arenas with programs by checking LD.IMM
+ * instruction operands for an arena and populating the program state
+ * with the first instance it finds. This requires accessing our global
+ * arena variable, but subprogs do not necessarily do so while still
+ * using pointers from that arena. Insert an LD.IMM instruction to
+ * access the arena and help the verifier.
+ */
+#define arena_subprog_init() do { asm volatile ("" :: "r"(&arena)); } while (0)
+
+#else /* ! __BPF__ */
+
+#include <stdint.h>
+
+#define __arena
+
+typedef uint8_t u8;
+typedef uint16_t u16;
+typedef uint32_t u32;
+typedef uint64_t u64;
+typedef int8_t s8;
+typedef int16_t s16;
+typedef int32_t s32;
+typedef int64_t s64;
+
+/* Dummy "definition" for userspace. */
+#define arena_spinlock_t int
+
+#endif /* __BPF__ */
+
+struct arena_get_info_args {
+ void __arena *arena_base;
+};
+
+struct arena_alloc_reserve_args {
+ u64 nr_pages;
+};
+
+/* Reasonable default number of pages reserved by arena_alloc_reserve. */
+#define ARENA_RESERVE_PAGES_DFL (8)
diff --git a/tools/testing/selftests/bpf/libarena/include/libarena/userspace.h b/tools/testing/selftests/bpf/libarena/include/libarena/userspace.h
new file mode 100644
index 000000000000..88b68ac73cca
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/include/libarena/userspace.h
@@ -0,0 +1,132 @@
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#pragma once
+
+#include <errno.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <sys/mman.h>
+
+#include <bpf/libbpf.h>
+#include <bpf/bpf.h>
+
+static inline int libarena_run_prog(int prog_fd)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ int ret;
+
+ ret = bpf_prog_test_run_opts(prog_fd, &opts);
+ if (ret)
+ return ret;
+
+ return opts.retval;
+}
+
+static inline bool libarena_is_test_prog(const char *name)
+{
+ return strstr(name, "test_") == name;
+}
+
+static inline bool libarena_is_asan_test_prog(const char *name)
+{
+ return strstr(name, "asan_test") == name;
+}
+
+static inline int libarena_run_prog_args(int prog_fd, void *args, size_t argsize)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ int ret;
+
+ opts.ctx_in = args;
+ opts.ctx_size_in = argsize;
+
+ ret = bpf_prog_test_run_opts(prog_fd, &opts);
+
+ return ret ?: opts.retval;
+}
+
+static inline int libarena_get_arena_base(int arena_get_info_fd,
+ void **arena_base)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ struct arena_get_info_args args = { .arena_base = NULL };
+ int ret;
+
+ opts.ctx_in = &args;
+ opts.ctx_size_in = sizeof(args);
+
+ ret = bpf_prog_test_run_opts(arena_get_info_fd, &opts);
+ if (ret)
+ return ret;
+ if (opts.retval)
+ return opts.retval;
+
+ *arena_base = args.arena_base;
+ return 0;
+}
+
+static inline int libarena_get_globals_pages(int arena_get_globals_fd,
+ size_t arena_all_pages,
+ u64 *globals_pages)
+{
+ size_t pgsize = sysconf(_SC_PAGESIZE);
+ void *arena_base;
+ ssize_t i;
+ u8 *vec;
+ int ret;
+
+ ret = libarena_get_arena_base(arena_get_globals_fd, &arena_base);
+ if (ret)
+ return ret;
+
+ if (!arena_base)
+ return -EINVAL;
+
+ vec = calloc(arena_all_pages, sizeof(*vec));
+ if (!vec)
+ return -ENOMEM;
+
+ if (mincore(arena_base, arena_all_pages * pgsize, vec) < 0) {
+ ret = -errno;
+ free(vec);
+ return ret;
+ }
+
+ *globals_pages = 0;
+ for (i = arena_all_pages - 1; i >= 0; i--) {
+ if (!(vec[i] & 0x1))
+ break;
+ *globals_pages += 1;
+ }
+
+ free(vec);
+ return 0;
+}
+
+static inline int libarena_asan_init(int arena_asan_init_fd,
+ int asan_init_fd,
+ size_t arena_all_pages)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ struct asan_init_args args;
+ u64 globals_pages;
+ int ret;
+
+ ret = libarena_get_globals_pages(arena_asan_init_fd,
+ arena_all_pages, &globals_pages);
+ if (ret)
+ return ret;
+
+ args = (struct asan_init_args){
+ .arena_all_pages = arena_all_pages,
+ .arena_globals_pages = globals_pages,
+ };
+
+ opts.ctx_in = &args;
+ opts.ctx_size_in = sizeof(args);
+
+ ret = bpf_prog_test_run_opts(asan_init_fd, &opts);
+ if (ret)
+ return ret;
+ return opts.retval;
+}
diff --git a/tools/testing/selftests/bpf/libarena/selftests/st_asan_buddy.bpf.c b/tools/testing/selftests/bpf/libarena/selftests/st_asan_buddy.bpf.c
new file mode 100644
index 000000000000..97acd50ffa5c
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/selftests/st_asan_buddy.bpf.c
@@ -0,0 +1,258 @@
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <libarena/common.h>
+#include <libarena/asan.h>
+#include <libarena/buddy.h>
+
+/* Required for parsing the ASAN call stacks. */
+#include "test_progs_compat.h"
+
+extern buddy_t buddy;
+
+#ifdef BPF_ARENA_ASAN
+
+#include "st_asan_common.h"
+
+static __always_inline int asan_test_buddy_oob_single(size_t alloc_size)
+{
+ u8 __arena *mem;
+ int ret, i;
+
+ ret = asan_validate();
+ if (ret < 0)
+ return ret;
+
+ mem = buddy_alloc(&buddy, alloc_size);
+ if (!mem) {
+ arena_stdout("buddy_alloc failed for size %lu", alloc_size);
+ return -ENOMEM;
+ }
+
+ ret = asan_validate();
+ if (ret < 0)
+ return ret;
+
+ for (i = zero; i < alloc_size && can_loop; i++) {
+ mem[i] = 0xba;
+ ret = asan_validate_addr(false, &mem[i]);
+ if (ret < 0)
+ return ret;
+ }
+
+ mem[alloc_size] = 0xba;
+ ret = asan_validate_addr(true, &mem[alloc_size]);
+ if (ret < 0)
+ return ret;
+
+ buddy_free(&buddy, mem);
+
+ return 0;
+}
+
+/*
+ * Factored out because asan_validate_addr is complex enough to cause
+ * verification failures if verified with the rest of asan_test_buddy_uaf_single.
+ */
+__weak int asan_test_buddy_byte(u8 __arena __arg_arena *mem, int i, bool freed)
+{
+ int ret;
+
+ /* The header in freed blocks doesn't get poisoned. */
+ if (freed && BUDDY_HEADER_OFF <= i &&
+ i < BUDDY_HEADER_OFF + sizeof(struct buddy_header))
+ return 0;
+
+ mem[i] = 0xba;
+ ret = asan_validate_addr(freed, &mem[i]);
+ if (ret < 0)
+ return ret;
+
+ return 0;
+}
+
+__weak int asan_test_buddy_uaf_single(size_t alloc_size)
+{
+ u8 __arena *mem;
+ int ret;
+ int i;
+
+ mem = buddy_alloc(&buddy, alloc_size);
+ if (!mem) {
+ arena_stdout("buddy_alloc failed for size %lu", alloc_size);
+ return -ENOMEM;
+ }
+
+ ret = asan_validate();
+ if (ret < 0)
+ return ret;
+
+ for (i = zero; i < alloc_size && can_loop; i++) {
+ ret = asan_test_buddy_byte(mem, i, false);
+ if (ret)
+ return ret;
+ }
+
+ ret = asan_validate();
+ if (ret < 0)
+ return ret;
+
+ buddy_free(&buddy, mem);
+
+ for (i = zero; i < alloc_size && can_loop; i++) {
+ ret = asan_test_buddy_byte(mem, i, true);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+struct buddy_blob {
+ volatile u8 mem[48];
+ u8 oob;
+};
+
+static __always_inline int asan_test_buddy_blob_single(void)
+{
+ volatile struct buddy_blob __arena *blob;
+ const size_t alloc_size = sizeof(struct buddy_blob) - 1;
+ int ret;
+
+ blob = buddy_alloc(&buddy, alloc_size);
+ if (!blob)
+ return -ENOMEM;
+
+ blob->mem[0] = 0xba;
+ ret = asan_validate_addr(false, &blob->mem[0]);
+ if (ret < 0)
+ return ret;
+
+ blob->mem[47] = 0xba;
+ ret = asan_validate_addr(false, &blob->mem[47]);
+ if (ret < 0)
+ return ret;
+
+ blob->oob = 0;
+ ret = asan_validate_addr(true, &blob->oob);
+ if (ret < 0)
+ return ret;
+
+ buddy_free(&buddy, (void __arena *)blob);
+
+ return 0;
+}
+
+SEC("syscall")
+__stderr("Memory violation for address {{.*}} for write of size 1")
+__stderr("CPU: {{[0-9]+}} UID: 0 PID: {{[0-9]+}} Comm: {{.*}}")
+__stderr("Call trace:\n"
+"{{([a-zA-Z_][a-zA-Z0-9_]*\\+0x[0-9a-fA-F]+/0x[0-9a-fA-F]+\n"
+"|[ \t]+[^\n]+\n)*}}")
+__weak int asan_test_buddy_oob(void)
+{
+ size_t sizes[] = {
+ 7, 8, 17, 18, 64, 256, 317, 512, 1024,
+ };
+ int ret, i;
+
+ ret = buddy_init(&buddy);
+ if (ret) {
+ arena_stdout("buddy_init failed with %d", ret);
+ return ret;
+ }
+
+ for (i = zero; i < sizeof(sizes) / sizeof(sizes[0]) && can_loop; i++) {
+ ret = asan_test_buddy_oob_single(sizes[i]);
+ if (ret) {
+ arena_stdout("%s:%d Failed for size %lu", __func__,
+ __LINE__, sizes[i]);
+ buddy_destroy(&buddy);
+ return ret;
+ }
+ }
+
+ buddy_destroy(&buddy);
+
+ ret = asan_validate();
+ if (ret < 0)
+ return ret;
+
+ return 0;
+}
+
+SEC("syscall")
+__stderr("Memory violation for address {{.*}} for write of size 1")
+__stderr("CPU: {{[0-9]+}} UID: 0 PID: {{[0-9]+}} Comm: {{.*}}")
+__stderr("Call trace:\n"
+"{{([a-zA-Z_][a-zA-Z0-9_]*\\+0x[0-9a-fA-F]+/0x[0-9a-fA-F]+\n"
+"|[ \t]+[^\n]+\n)*}}")
+__weak int asan_test_buddy_uaf(void)
+{
+ size_t sizes[] = { 16, 32, 64, 128, 256, 512, 1024, 16384 };
+ int ret, i;
+
+ ret = buddy_init(&buddy);
+ if (ret) {
+ arena_stdout("buddy_init failed with %d", ret);
+ return ret;
+ }
+
+ for (i = zero; i < sizeof(sizes) / sizeof(sizes[0]) && can_loop; i++) {
+ ret = asan_test_buddy_uaf_single(sizes[i]);
+ if (ret) {
+ arena_stdout("%s:%d Failed for size %lu", __func__,
+ __LINE__, sizes[i]);
+ buddy_destroy(&buddy);
+ return ret;
+ }
+ }
+
+ buddy_destroy(&buddy);
+
+ ret = asan_validate();
+ if (ret < 0)
+ return ret;
+
+ return 0;
+}
+
+SEC("syscall")
+__stderr("Memory violation for address {{.*}} for write of size 1")
+__stderr("CPU: {{[0-9]+}} UID: 0 PID: {{[0-9]+}} Comm: {{.*}}")
+__stderr("Call trace:\n"
+"{{([a-zA-Z_][a-zA-Z0-9_]*\\+0x[0-9a-fA-F]+/0x[0-9a-fA-F]+\n"
+"|[ \t]+[^\n]+\n)*}}")
+__weak int asan_test_buddy_blob(void)
+{
+ const int iters = 10;
+ int ret, i;
+
+ ret = buddy_init(&buddy);
+ if (ret) {
+ arena_stdout("buddy_init failed with %d", ret);
+ return ret;
+ }
+
+ for (i = zero; i < iters && can_loop; i++) {
+ ret = asan_test_buddy_blob_single();
+ if (ret) {
+ arena_stdout("%s:%d Failed on iteration %d", __func__,
+ __LINE__, i);
+ buddy_destroy(&buddy);
+ return ret;
+ }
+ }
+
+ buddy_destroy(&buddy);
+
+ ret = asan_validate();
+ if (ret < 0)
+ return ret;
+
+ return 0;
+}
+
+#endif
+
+__weak char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/libarena/selftests/st_asan_common.h b/tools/testing/selftests/bpf/libarena/selftests/st_asan_common.h
new file mode 100644
index 000000000000..1d3edc4372ac
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/selftests/st_asan_common.h
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#pragma once
+
+#define ST_PAGES 64
+
+static inline void print_asan_map_state(void __arena *addr)
+{
+ arena_stdout("%s:%d ASAN %p -> (val: %x gran: %x set: [%s])",
+ __func__, __LINE__, addr,
+ *(s8a *)(addr), ASAN_GRANULE(addr),
+ asan_shadow_set(addr) ? "yes" : "no");
+}
+
+/*
+ * Emit an error and force the current function to exit if the ASAN
+ * violation state is unexpected. Reset the violation state after.
+ */
+static inline int asan_validate_addr(bool cond, void __arena *addr)
+{
+ if ((asan_violated != 0) == cond) {
+ asan_violated = 0;
+ return 0;
+ }
+
+ arena_stdout("%s:%d ASAN asan_violated %lx", __func__, __LINE__,
+ (u64)asan_violated);
+ print_asan_map_state(addr);
+
+ asan_violated = 0;
+
+ return -EINVAL;
+}
+
+static inline int asan_validate(void)
+{
+ if (!asan_violated)
+ return 0;
+
+ arena_stdout("%s:%d Found ASAN violation at %lx", __func__, __LINE__,
+ asan_violated);
+
+ asan_violated = 0;
+
+ return -EINVAL;
+}
+
+struct blob {
+ volatile u8 mem[59];
+ u8 oob;
+};
diff --git a/tools/testing/selftests/bpf/libarena/selftests/st_buddy.bpf.c b/tools/testing/selftests/bpf/libarena/selftests/st_buddy.bpf.c
new file mode 100644
index 000000000000..79e6f0baabfe
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/selftests/st_buddy.bpf.c
@@ -0,0 +1,209 @@
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <libarena/common.h>
+
+#include <libarena/asan.h>
+#include <libarena/buddy.h>
+
+extern buddy_t buddy;
+
+struct segarr_entry {
+ u8 __arena *block;
+ size_t sz;
+ u8 poison;
+};
+
+#define SEGARRLEN (512)
+static struct segarr_entry __arena segarr[SEGARRLEN];
+static void __arena *ptrs[17];
+size_t __arena alloc_sizes[] = { 3, 17, 1025, 129, 16350, 333, 9, 517 };
+size_t __arena alloc_multiple_sizes[] = { 3, 17, 1025, 129, 16350, 333, 9, 517, 2099 };
+size_t __arena alloc_free_sizes[] = { 3, 17, 64, 129, 256, 333, 512, 517 };
+size_t __arena alignment_sizes[] = { 1, 3, 7, 8, 9, 15, 16, 17, 31,
+ 32, 64, 100, 128, 255, 256, 512, 1000 };
+
+SEC("syscall")
+__weak int test_buddy_create(void)
+{
+ const int iters = 10;
+ int ret, i;
+
+ for (i = zero; i < iters && can_loop; i++) {
+ ret = buddy_init(&buddy);
+ if (ret)
+ return ret;
+
+ ret = buddy_destroy(&buddy);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+SEC("syscall")
+__weak int test_buddy_alloc(void)
+{
+ void __arena *mem;
+ int ret, i;
+
+ for (i = zero; i < 8 && can_loop; i++) {
+ ret = buddy_init(&buddy);
+ if (ret)
+ return ret;
+
+ mem = buddy_alloc(&buddy, alloc_sizes[i]);
+ if (!mem) {
+ buddy_destroy(&buddy);
+ return -ENOMEM;
+ }
+
+ buddy_destroy(&buddy);
+ }
+
+ return 0;
+}
+
+SEC("syscall")
+__weak int test_buddy_alloc_free(void)
+{
+ const int iters = 800;
+ void __arena *mem;
+ int ret, i;
+
+ ret = buddy_init(&buddy);
+ if (ret)
+ return ret;
+
+ for (i = zero; i < iters && can_loop; i++) {
+ mem = buddy_alloc(&buddy, alloc_free_sizes[(i * 5) % 8]);
+ if (!mem) {
+ buddy_destroy(&buddy);
+ return -ENOMEM;
+ }
+
+ buddy_free(&buddy, mem);
+ }
+
+ buddy_destroy(&buddy);
+
+ return 0;
+}
+
+SEC("syscall")
+__weak int test_buddy_alloc_multiple(void)
+{
+ int ret, j;
+ u32 i, idx;
+ u8 __arena *mem;
+ size_t sz;
+ u8 poison;
+
+ ret = buddy_init(&buddy);
+ if (ret)
+ return ret;
+
+ /*
+ * Cycle through each size, allocating an entry in the
+ * segarr. Continue for SEGARRLEN iterations. For every
+ * allocation write down the size, use the current index
+ * as a poison value, and log it with the pointer in the
+ * segarr entry. Use the poison value to poison the entire
+ * allocated memory according to the size given.
+ */
+ for (i = zero; i < SEGARRLEN && can_loop; i++) {
+ sz = alloc_multiple_sizes[i % 9];
+ poison = (u8)i;
+
+ mem = buddy_alloc(&buddy, sz);
+ if (!mem) {
+ buddy_destroy(&buddy);
+ arena_stdout("%s:%d", __func__, __LINE__);
+ return -ENOMEM;
+ }
+
+ segarr[i].block = mem;
+ segarr[i].sz = sz;
+ segarr[i].poison = poison;
+
+ for (j = zero; j < sz && can_loop; j++) {
+ mem[j] = poison;
+ if (mem[j] != poison) {
+ buddy_destroy(&buddy);
+ return -EINVAL;
+ }
+ }
+ }
+
+ /*
+ * Go to (i * 17) % SEGARRLEN, and free the block pointed to.
+ * Before freeing, check all bytes have the poisoned value
+ * corresponding to the element. If any values are unexpected,
+ * return an error. Skip some elements to test destroying the
+ * buddy allocator while data is still allocated.
+ */
+ for (i = 10; i < SEGARRLEN && can_loop; i++) {
+ idx = (i * 17) % SEGARRLEN;
+
+ mem = segarr[idx].block;
+ sz = segarr[idx].sz;
+ poison = segarr[idx].poison;
+
+ for (j = zero; j < sz && can_loop; j++) {
+ if (mem[j] != poison) {
+ buddy_destroy(&buddy);
+ arena_stdout("%s:%d %lx %u vs %u", __func__,
+ __LINE__, (uintptr_t)&mem[j],
+ mem[j], poison);
+ return -EINVAL;
+ }
+ }
+
+ buddy_free(&buddy, mem);
+ }
+
+ buddy_destroy(&buddy);
+
+ return 0;
+}
+
+SEC("syscall")
+__weak int test_buddy_alignment(void)
+{
+ int ret, i;
+
+ ret = buddy_init(&buddy);
+ if (ret)
+ return ret;
+
+ /* Allocate various sizes and check alignment */
+ for (i = zero; i < 17 && can_loop; i++) {
+ ptrs[i] = buddy_alloc(&buddy, alignment_sizes[i]);
+ if (!ptrs[i]) {
+ arena_stdout("alignment test: alloc failed for size %lu",
+ alignment_sizes[i]);
+ buddy_destroy(&buddy);
+ return -ENOMEM;
+ }
+
+ /* Check 8-byte alignment */
+ if ((u64)ptrs[i] & 0x7) {
+ arena_stdout(
+ "alignment test: ptr %llx not 8-byte aligned (size %lu)",
+ (u64)ptrs[i], alignment_sizes[i]);
+ buddy_destroy(&buddy);
+ return -EINVAL;
+ }
+ }
+
+ /* Free all allocations */
+ for (i = zero; i < 17 && can_loop; i++)
+ buddy_free(&buddy, ptrs[i]);
+
+ buddy_destroy(&buddy);
+
+ return 0;
+}
+
+__weak char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/libarena/selftests/test_progs_compat.h b/tools/testing/selftests/bpf/libarena/selftests/test_progs_compat.h
new file mode 100644
index 000000000000..9d431376c42f
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/selftests/test_progs_compat.h
@@ -0,0 +1,15 @@
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#pragma once
+
+#ifdef __BPF__
+
+/* Selftests use these tags for compatibility with test_progs. */
+#define __test_tag(tag) __attribute__((btf_decl_tag("comment:" XSTR(__COUNTER__) ":" tag)))
+#define __stderr(msg) __test_tag("test_expect_stderr=" msg)
+#define __stderr_unpriv(msg) __test_tag("test_expect_stderr_unpriv=" msg)
+
+#define XSTR(s) STR(s)
+#define STR(s) #s
+
+#endif
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..64c5b990086c
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/src/asan.bpf.c
@@ -0,0 +1,553 @@
+// 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";
diff --git a/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
new file mode 100644
index 000000000000..865e00803daa
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/src/buddy.bpf.c
@@ -0,0 +1,903 @@
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <libarena/common.h>
+#include <libarena/asan.h>
+#include <libarena/buddy.h>
+
+/*
+ * Buddy allocator arena-based implementation.
+ *
+ * Memory is organized into chunks. These chunks
+ * cannot be coalesced or split. Allocating
+ * chunks allocates their memory eagerly.
+ *
+ * Internally, each chunk is organized into blocks.
+ * Blocks _can_ be coalesced/split, but only inside
+ * the chunk. Each block can be allocated or
+ * unallocated. If allocated, the entire block holds
+ * user data. If unallocated, the block is mostly
+ * invalid memory, with the exception of a header
+ * used for freelist tracking.
+ *
+ * The header is placed at an offset inside the block
+ * to prevent off-by-one errors from the previous block
+ * from trivially overwriting the header. Such an error
+ * is also not catchable by ASAN, since the header remains
+ * valid memory even after the block is freed. It is still
+ * theoretically possible for the header to be corrupted
+ * without being caught by ASAN, but harder.
+ *
+ * Since the allocator needs to track order information for
+ * both allocated and free blocks, and allocated blocks cannot
+ * store a header, the allocator also stores per-chunk order
+ * information in a reserved region at the beginning of the
+ * chunk. The header includes a bitmap with the order of blocks
+ * and their allocation state. It also includes the freelist
+ * heads for the allocation itself.
+ */
+
+
+enum {
+ BUDDY_POISONED = (s8)0xef,
+
+ /* Number of pages to be allocated per chunk. */
+ BUDDY_CHUNK_PAGES = BUDDY_CHUNK_BYTES / __PAGE_SIZE
+};
+
+static inline int buddy_lock(buddy_t *buddy)
+{
+ return arena_spin_lock(&buddy->lock);
+}
+
+static inline void buddy_unlock(buddy_t *buddy)
+{
+ arena_spin_unlock(&buddy->lock);
+}
+
+/*
+ * Reserve part of the arena address space for the allocator. We use
+ * this to get aligned addresses for the chunks, since the arena
+ * page alloc kfuncs do not support aligning to a boundary (in this
+ * case 1 MiB, see buddy.h on how this is derived).
+ */
+static int buddy_reserve_arena_vaddr(buddy_t *buddy)
+{
+ buddy->vaddr = 0;
+
+ return bpf_arena_reserve_pages(&arena,
+ (void __arena *)BUDDY_VADDR_OFFSET,
+ BUDDY_VADDR_SIZE / __PAGE_SIZE);
+}
+
+/*
+ * Free up any unused address space. Used only during teardown.
+ */
+static void buddy_unreserve_arena_vaddr(buddy_t *buddy)
+{
+ bpf_arena_free_pages(
+ &arena, (void __arena *)(BUDDY_VADDR_OFFSET + buddy->vaddr),
+ (BUDDY_VADDR_SIZE - buddy->vaddr) / __PAGE_SIZE);
+
+ buddy->vaddr = 0;
+}
+
+/*
+ * Carve out part of the reserved address space and hand it over
+ * to the buddy allocator.
+ *
+ * We are assuming the buddy allocator is the only allocator in the
+ * system, so there is no race between this function reserving a
+ * page range and some other allocator actually making the BPF call
+ * to really create and reserve it.
+ *
+ * However, bump allocation must still be atomic because this function
+ * is called without the buddy lock from multiple threads concurrently.
+ */
+__weak int buddy_alloc_arena_vaddr(buddy_t __arg_arena *buddy, u64 *vaddrp)
+{
+ u64 vaddr, old, new;
+
+ if (!buddy || !vaddrp)
+ return -EINVAL;
+
+ do {
+ vaddr = buddy->vaddr;
+ new = vaddr + BUDDY_CHUNK_BYTES;
+
+ if (new > BUDDY_VADDR_SIZE)
+ return -EINVAL;
+
+ old = __sync_val_compare_and_swap(&buddy->vaddr, vaddr, new);
+ } while (old != vaddr && can_loop);
+
+ if (old != vaddr)
+ return -EINVAL;
+
+ *vaddrp = BUDDY_VADDR_OFFSET + vaddr;
+
+ return 0;
+}
+
+static u64 arena_next_pow2(__u64 n)
+{
+ n--;
+ n |= n >> 1;
+ n |= n >> 2;
+ n |= n >> 4;
+ n |= n >> 8;
+ n |= n >> 16;
+ n |= n >> 32;
+ n++;
+
+ return n;
+}
+
+__weak
+int idx_set_allocated(buddy_chunk_t __arg_arena *chunk, u64 idx, bool allocated)
+{
+ bool already_allocated;
+
+ if (unlikely(idx >= BUDDY_CHUNK_ITEMS)) {
+ arena_stderr("setting state of invalid idx (%ld, max %d)\n", idx,
+ BUDDY_CHUNK_ITEMS);
+ return -EINVAL;
+ }
+
+ already_allocated = chunk->allocated[idx / 8] & (1 << (idx % 8));
+ if (unlikely(already_allocated == allocated)) {
+ arena_stderr("Double %s of idx %ld for chunk %p",
+ allocated ? "alloc" : "free",
+ idx, chunk);
+ return -EINVAL;
+ }
+
+ if (allocated)
+ chunk->allocated[idx / 8] |= 1 << (idx % 8);
+ else
+ chunk->allocated[idx / 8] &= ~(1 << (idx % 8));
+
+ return 0;
+}
+
+static int idx_is_allocated(buddy_chunk_t *chunk, u64 idx, bool *allocated)
+{
+ if (unlikely(idx >= BUDDY_CHUNK_ITEMS)) {
+ arena_stderr("getting state of invalid idx (%llu, max %d)\n", idx,
+ BUDDY_CHUNK_ITEMS);
+ return -EINVAL;
+ }
+
+ *allocated = chunk->allocated[idx / 8] & (1 << (idx % 8));
+ return 0;
+}
+
+__weak
+int idx_set_order(buddy_chunk_t __arg_arena *chunk, u64 idx, u8 order)
+{
+ u8 prev_order;
+
+ if (unlikely(order >= BUDDY_CHUNK_NUM_ORDERS)) {
+ arena_stderr("setting invalid order %u\n", order);
+ return -EINVAL;
+ }
+
+ if (unlikely(idx >= BUDDY_CHUNK_ITEMS)) {
+ arena_stderr("setting order of invalid idx (%d, max %d)\n", idx,
+ BUDDY_CHUNK_ITEMS);
+ return -EINVAL;
+ }
+
+ /*
+ * We store two order instances per byte, one per nibble.
+ * Retain the existing nibble.
+ */
+ prev_order = chunk->orders[idx / 2];
+ if (idx & 0x1) {
+ order &= 0xf;
+ order |= (prev_order & 0xf0);
+ } else {
+ order <<= 4;
+ order |= (prev_order & 0xf);
+ }
+
+ chunk->orders[idx / 2] = order;
+
+ return 0;
+}
+
+static u8 idx_get_order(buddy_chunk_t *chunk, u64 idx)
+{
+ u8 result;
+
+ _Static_assert(BUDDY_CHUNK_NUM_ORDERS <= 16,
+ "order must fit in 4 bits");
+
+ if (unlikely(idx >= BUDDY_CHUNK_ITEMS)) {
+ arena_stderr("getting order of invalid idx %u\n", idx);
+ return BUDDY_CHUNK_NUM_ORDERS;
+ }
+
+ result = chunk->orders[idx / 2];
+
+ return (idx & 0x1) ? (result & 0xf) : (result >> 4);
+}
+
+static void __arena *idx_to_addr(buddy_chunk_t *chunk, size_t idx)
+{
+ u64 address;
+
+ if (unlikely(idx >= BUDDY_CHUNK_ITEMS)) {
+ arena_stderr("translating invalid idx %u\n", idx);
+ return NULL;
+ }
+
+ /*
+ * The data blocks start in the chunk after the metadata block.
+ * We find the actual address by indexing into the region at an
+ * BUDDY_MIN_ALLOC_BYTES granularity, the minimum allowed.
+ * The index number already accounts for the fact that the first
+ * blocks in the chunk are occupied by the metadata, so we do
+ * not need to offset it.
+ */
+
+ address = (u64)chunk + (idx * BUDDY_MIN_ALLOC_BYTES);
+
+ return (void __arena *)address;
+}
+
+static buddy_header_t *idx_to_header(buddy_chunk_t *chunk, size_t idx)
+{
+ bool allocated;
+ u64 address;
+
+ if (unlikely(idx_is_allocated(chunk, idx, &allocated))) {
+ arena_stderr("accessing invalid idx 0x%lx\n", idx);
+ return NULL;
+ }
+
+ if (unlikely(allocated)) {
+ arena_stderr("accessing allocated idx 0x%lx as header\n", idx);
+ return NULL;
+ }
+
+ address = (u64)idx_to_addr(chunk, idx);
+ if (!address)
+ return NULL;
+
+ /*
+ * Offset the header within the block. This avoids accidental overwrites
+ * to the header because of off-by-one errors when using adjacent blocks.
+ *
+ * The offset has been chosen as a compromise between ASAN effectiveness
+ * and allocator granularity:
+ * 1) ASAN dictates valid data runs are 8-byte aligned.
+ * 2) We want to keep a low minimum allocation size (currently 16).
+ *
+ * As a result, we have only two possible positions for the header: Bytes
+ * 0 and 8. Keeping the header in byte 0 means off-by-ones from the previous
+ * block touch the header, and, since the header must be accessible, ASAN
+ * will not trigger. Keeping the header on byte 8 means off-by-one errors from
+ * the previous block are caught by ASAN. Negative offsets are rarer, so
+ * while accesses into the block from the next block are possible, they are
+ * less probable.
+ */
+
+ return (buddy_header_t *)(address + BUDDY_HEADER_OFF);
+}
+
+static void header_add_freelist(buddy_chunk_t *chunk, buddy_header_t *header,
+ u64 idx, u8 order)
+{
+ buddy_header_t *tmp_header;
+
+ idx_set_order(chunk, idx, order);
+
+ header->next_index = chunk->freelists[order];
+ header->prev_index = BUDDY_CHUNK_ITEMS;
+
+ if (header->next_index != BUDDY_CHUNK_ITEMS) {
+ tmp_header = idx_to_header(chunk, header->next_index);
+ tmp_header->prev_index = idx;
+ }
+
+ chunk->freelists[order] = idx;
+}
+
+static void header_remove_freelist(buddy_chunk_t *chunk,
+ buddy_header_t *header, u8 order)
+{
+ buddy_header_t *tmp_header;
+
+ if (header->prev_index != BUDDY_CHUNK_ITEMS) {
+ tmp_header = idx_to_header(chunk, header->prev_index);
+ tmp_header->next_index = header->next_index;
+ }
+
+ if (header->next_index != BUDDY_CHUNK_ITEMS) {
+ tmp_header = idx_to_header(chunk, header->next_index);
+ tmp_header->prev_index = header->prev_index;
+ }
+
+ /* Pop off the list head if necessary. */
+ if (idx_to_header(chunk, chunk->freelists[order]) == header)
+ chunk->freelists[order] = header->next_index;
+
+ header->prev_index = BUDDY_CHUNK_ITEMS;
+ header->next_index = BUDDY_CHUNK_ITEMS;
+}
+
+static u64 size_to_order(size_t size)
+{
+ u64 order;
+
+ /*
+ * Legal sizes are [1, 4GiB] (the biggest possible arena).
+ * Of course, sizes close to GiB are practically impossible
+ * to fulfill and allocation will fail, but that's taken care
+ * of by the caller.
+ */
+
+ if (unlikely(size == 0 || size > (1UL << 32))) {
+ arena_stderr("illegal size request %lu\n", size);
+ return 64;
+ }
+ /*
+ * To find the order of the allocation we find the first power of two
+ * >= the requested size, take the log2, then adjust it for the minimum
+ * allocation size by removing the minimum shift from it. Requests
+ * smaller than the minimum allocation size are rounded up.
+ */
+ order = arena_fls(arena_next_pow2(size)) - 1;
+ if (order < BUDDY_MIN_ALLOC_SHIFT)
+ return 0;
+
+ return order - BUDDY_MIN_ALLOC_SHIFT;
+}
+
+__weak
+int add_leftovers_to_freelist(buddy_chunk_t __arg_arena *chunk, u32 cur_idx,
+ u64 min_order, u64 max_order)
+{
+ buddy_header_t *header;
+ u64 ord;
+ u32 idx;
+
+ for (ord = min_order; ord < max_order && can_loop; ord++) {
+ /* Mark the buddy as free and add it to the freelists. */
+ idx = cur_idx + (1 << ord);
+
+ header = idx_to_header(chunk, idx);
+ if (unlikely(!header)) {
+ arena_stderr("idx %u has no header", idx);
+ return -EINVAL;
+ }
+
+ asan_unpoison(header, sizeof(*header));
+
+ header_add_freelist(chunk, header, idx, ord);
+ }
+
+ return 0;
+}
+
+static buddy_chunk_t *buddy_chunk_get(buddy_t *buddy)
+{
+ u64 order, ord, min_order, max_order;
+ buddy_chunk_t *chunk;
+ size_t left;
+ int power2;
+ u64 vaddr;
+ u32 idx;
+ int ret;
+
+ /*
+ * Step 1: Allocate a properly aligned chunk, and
+ * prep it for insertion into the buddy allocator.
+ * We don't need the allocator lock until step 2.
+ */
+
+ ret = buddy_alloc_arena_vaddr(buddy, &vaddr);
+ if (ret)
+ return NULL;
+
+ /* Addresses must be aligned to the chunk boundary. */
+ if (vaddr % BUDDY_CHUNK_BYTES)
+ return NULL;
+
+ /* Unreserve the address space. */
+ bpf_arena_free_pages(&arena, (void __arena *)vaddr,
+ BUDDY_CHUNK_PAGES);
+
+ chunk = bpf_arena_alloc_pages(&arena, (void __arena *)vaddr,
+ BUDDY_CHUNK_PAGES, NUMA_NO_NODE, 0);
+ if (!chunk) {
+ arena_stderr("[ALLOC FAILED]");
+ return NULL;
+ }
+
+ if (buddy_lock(buddy)) {
+ /*
+ * We cannot reclaim the vaddr space, but that is ok - this
+ * operation should always succeed. The error path is to catch
+ * accidental deadlocks that will cause -ENOMEMs to the program as
+ * the allocator fails to refill itself, in which case vaddr usage
+ * is the least of our worries.
+ */
+ bpf_arena_free_pages(&arena, (void __arena *)vaddr, BUDDY_CHUNK_PAGES);
+ return NULL;
+ }
+
+ asan_poison(chunk, BUDDY_POISONED, BUDDY_CHUNK_PAGES * __PAGE_SIZE);
+
+ /* Unpoison the chunk itself. */
+ asan_unpoison(chunk, sizeof(*chunk));
+
+ /* Mark all freelists as empty. */
+ for (ord = zero; ord < BUDDY_CHUNK_NUM_ORDERS && can_loop; ord++)
+ chunk->freelists[ord] = BUDDY_CHUNK_ITEMS;
+
+ /*
+ * Initialize the chunk by carving out a page range to hold the metadata
+ * struct above, then dumping the rest of the pages into the allocator.
+ */
+
+ _Static_assert(BUDDY_CHUNK_PAGES * __PAGE_SIZE >=
+ BUDDY_MIN_ALLOC_BYTES *
+ BUDDY_CHUNK_ITEMS,
+ "chunk must fit within the allocation");
+
+ /*
+ * Step 2: Reserve a chunk for the chunk metadata, then breaks
+ * the rest of the full allocation into the different buckets.
+ * We allocating the memory by grabbing blocks of progressively
+ * smaller sizes from the allocator, which are guaranteed to be
+ * continuous.
+ *
+ * This operation also populates the allocator.
+ *
+ * Algorithm:
+ *
+ * - max_order: The last order allocation we made
+ * - left: How many bytes are left to allocate
+ * - cur_index: Current index into the top-level block we are
+ * allocating from.
+ *
+ * Step 3:
+ * - Find the largest power-of-2 allocation still smaller than left (infimum)
+ * - Reserve a chunk of that size, along with its buddy
+ * - For every order from [infimum + 1, last order), carve out a block
+ * and put it into the allocator.
+ *
+ * Example: Chunk size 0b1010000 (80 bytes)
+ *
+ * Step 1:
+ *
+ * idx infimum 1 << max_order
+ * 0 64 128 1 << 20
+ * |________|_________|______________________|
+ *
+ * Blocks set aside:
+ * [0, 64) - Completely allocated
+ * [64, 128) - Will be further split in the next iteration
+ *
+ * Blocks added to the allocator:
+ * [128, 256)
+ * [256, 512)
+ * ...
+ * [1 << 18, 1 << 19)
+ * [1 << 19, 1 << 20)
+ *
+ * Step 2:
+ *
+ * idx infimum idx + 1 << max_order
+ * 64 80 96 64 + 1 << 6 = 128
+ * |________|_________|______________________|
+ *
+ * Blocks set aside:
+ * [64, 80) - Completely allocated
+ *
+ * Blocks added to the allocator:
+ * [80, 96) - left == 0 so the buddy is unused and marked as freed
+ * [96, 128)
+ */
+ max_order = BUDDY_CHUNK_NUM_ORDERS;
+ left = sizeof(*chunk);
+ idx = 0;
+ while (left && can_loop) {
+ power2 = arena_fls(left) - 1;
+ /*
+ * Note: The condition below only triggers to catch serious bugs
+ * early. There is no sane way to undo any block insertions from
+ * the allocated chunk, so just leak any leftover allocations,
+ * emit a diagnostic, unlock and exit.
+ *
+ */
+ if (unlikely(power2 >= BUDDY_CHUNK_NUM_ORDERS)) {
+ arena_stderr(
+ "buddy chunk metadata require allocation of order %d\n",
+ power2);
+ arena_stderr(
+ "chunk has size of 0x%lx bytes (left %lx bytes)\n",
+ sizeof(*chunk), left);
+ buddy_unlock(buddy);
+
+ return NULL;
+ }
+
+ /* Round up allocations that are too small. */
+
+ left -= (power2 >= BUDDY_MIN_ALLOC_SHIFT) ? 1 << power2 : left;
+ order = (power2 >= BUDDY_MIN_ALLOC_SHIFT) ? power2 - BUDDY_MIN_ALLOC_SHIFT : 0;
+
+ if (idx_set_allocated(chunk, idx, true)) {
+ buddy_unlock(buddy);
+ return NULL;
+ }
+
+ /*
+ * Starting an order above the one we allocated, populate
+ * the allocator with free blocks. If this is the last
+ * allocation (left == 0), also mark the buddy as free.
+ *
+ * See comment above about error handling: The error path
+ * is only there as a way to mitigate deeply buggy allocator
+ * states by emitting a diagnostic in add_leftovers_to_freelist()
+ * and leaking any memory not added in the freelists.
+ */
+ min_order = left ? order + 1 : order;
+ if (add_leftovers_to_freelist(chunk, idx, min_order, max_order)) {
+ buddy_unlock(buddy);
+ return NULL;
+ }
+
+ /* Adjust the index. */
+ idx += 1 << order;
+ max_order = order;
+ }
+
+ buddy_unlock(buddy);
+
+ return chunk;
+}
+
+__weak int buddy_init(buddy_t __arg_arena *buddy)
+{
+ buddy_chunk_t *chunk;
+ int ret;
+
+ if (!asan_ready())
+ return -EINVAL;
+
+ /* Reserve enough address space to ensure allocations are aligned. */
+ ret = buddy_reserve_arena_vaddr(buddy);
+ if (ret)
+ return ret;
+
+ _Static_assert(BUDDY_CHUNK_PAGES > 0,
+ "chunk must use one or more pages");
+
+ chunk = buddy_chunk_get(buddy);
+
+ if (buddy_lock(buddy)) {
+ bpf_arena_free_pages(&arena, chunk, BUDDY_CHUNK_PAGES);
+ return -EINVAL;
+ }
+
+ /* Chunk is already properly unpoisoned if allocated. */
+ if (chunk)
+ chunk->next = buddy->first_chunk;
+
+ /* Put the chunk at the beginning of the list. */
+ buddy->first_chunk = chunk;
+
+ buddy_unlock(buddy);
+
+ return chunk ? 0 : -ENOMEM;
+}
+
+/*
+ * Destroy the allocator. This does not check whether there are any allocations
+ * currently in use, so any pages being accessed will start taking arena faults.
+ * We do not take a lock because we are freeing arena pages, and nobody should
+ * be using the allocator at that point in the execution.
+ */
+__weak int buddy_destroy(buddy_t __arg_arena *buddy)
+{
+ buddy_chunk_t *chunk, *next;
+
+ if (!buddy)
+ return -EINVAL;
+
+ /*
+ * Traverse all buddy chunks and free them back to the arena
+ * with the same granularity they were allocated with.
+ */
+ for (chunk = buddy->first_chunk; chunk && can_loop; chunk = next) {
+ next = chunk->next;
+
+ /* Wholesale poison the entire block. */
+ asan_poison(chunk, BUDDY_POISONED,
+ BUDDY_CHUNK_PAGES * __PAGE_SIZE);
+ bpf_arena_free_pages(&arena, chunk, BUDDY_CHUNK_PAGES);
+ }
+
+ /* Free up any part of the address space that did not get used. */
+ buddy_unreserve_arena_vaddr(buddy);
+
+ /* Clear all fields. */
+ buddy->first_chunk = NULL;
+
+ return 0;
+}
+
+__weak u64 buddy_chunk_alloc(buddy_chunk_t __arg_arena *chunk, int order_req)
+{
+ buddy_header_t *header, *tmp_header, *next_header;
+ u32 idx, tmpidx, retidx;
+ u64 address;
+ u64 order = 0;
+ u64 i;
+
+ for (order = order_req; order < BUDDY_CHUNK_NUM_ORDERS && can_loop; order++) {
+ if (chunk->freelists[order] != BUDDY_CHUNK_ITEMS)
+ break;
+ }
+
+ if (order >= BUDDY_CHUNK_NUM_ORDERS)
+ return (u64)NULL;
+
+ retidx = chunk->freelists[order];
+ header = idx_to_header(chunk, retidx);
+ if (unlikely(!header))
+ return (u64) NULL;
+
+ chunk->freelists[order] = header->next_index;
+
+ if (header->next_index != BUDDY_CHUNK_ITEMS) {
+ next_header = idx_to_header(chunk, header->next_index);
+ next_header->prev_index = BUDDY_CHUNK_ITEMS;
+ }
+
+ header->prev_index = BUDDY_CHUNK_ITEMS;
+ header->next_index = BUDDY_CHUNK_ITEMS;
+ if (idx_set_order(chunk, retidx, order_req))
+ return (u64)NULL;
+
+ if (idx_set_allocated(chunk, retidx, true))
+ return (u64)NULL;
+
+ /*
+ * Do not unpoison the address yet, will be done by the caller
+ * because the caller has the exact allocation size requested.
+ */
+ address = (u64)idx_to_addr(chunk, retidx);
+ if (!address)
+ return (u64)NULL;
+
+ /* If we allocated from a larger-order chunk, split the buddies. */
+ for (i = order_req; i < order && can_loop; i++) {
+ /*
+ * Flip the bit for the current order (the bit is guaranteed
+ * to be 0, so just add 1 << i).
+ */
+ idx = retidx + (1 << i);
+
+ /* Add the buddy of the allocation to the free list. */
+ header = idx_to_header(chunk, idx);
+ /* Unpoison the buddy header */
+ asan_unpoison(header, sizeof(*header));
+
+ if (idx_set_order(chunk, idx, i))
+ return (u64)NULL;
+
+ /* Push the header to the beginning of the freelists list. */
+ tmpidx = chunk->freelists[i];
+
+ header->prev_index = BUDDY_CHUNK_ITEMS;
+ header->next_index = tmpidx;
+
+ if (tmpidx != BUDDY_CHUNK_ITEMS) {
+ tmp_header = idx_to_header(chunk, tmpidx);
+ tmp_header->prev_index = idx;
+ }
+
+ chunk->freelists[i] = idx;
+ }
+
+ return address;
+}
+
+/* Scan the existing chunks for available memory. */
+static u64 buddy_alloc_from_existing_chunks(buddy_t *buddy, int order)
+{
+ buddy_chunk_t *chunk;
+ u64 address;
+
+ for (chunk = buddy->first_chunk; chunk != NULL && can_loop;
+ chunk = chunk->next) {
+ address = buddy_chunk_alloc(chunk, order);
+ if (address)
+ return address;
+ }
+
+ return (u64)NULL;
+}
+
+/*
+ * Try an allocation from a newly allocated chunk. Also
+ * incorporate the chunk into the linked list.
+ */
+static u64 buddy_alloc_from_new_chunk(buddy_t *buddy, buddy_chunk_t *chunk, int order)
+{
+ u64 address;
+
+ if (buddy_lock(buddy))
+ return (u64)NULL;
+
+
+ /*
+ * Add the chunk into the allocator and try
+ * to allocate specifically from that chunk.
+ */
+ chunk->next = buddy->first_chunk;
+ buddy->first_chunk = chunk;
+
+ address = buddy_chunk_alloc(buddy->first_chunk, order);
+
+ buddy_unlock(buddy);
+
+ return (u64)address;
+}
+__weak
+u64 buddy_alloc_internal(buddy_t __arg_arena *buddy, size_t size)
+{
+ buddy_chunk_t *chunk;
+ u64 address = (u64)NULL;
+ int order;
+
+ if (!buddy)
+ return (u64)NULL;
+
+ order = size_to_order(size);
+ if (order >= BUDDY_CHUNK_NUM_ORDERS || order < 0) {
+ arena_stderr("invalid order %d (sz %lu)\n", order, size);
+ return (u64)NULL;
+ }
+
+ if (buddy_lock(buddy))
+ return (u64)NULL;
+
+ address = buddy_alloc_from_existing_chunks(buddy, order);
+ buddy_unlock(buddy);
+ if (address)
+ goto done;
+
+ /* Get a new chunk. */
+ chunk = buddy_chunk_get(buddy);
+ if (chunk)
+ address = buddy_alloc_from_new_chunk(buddy, chunk, order);
+
+done:
+ /* If we failed to allocate memory, return NULL. */
+ if (!address)
+ return (u64)NULL;
+
+ /*
+ * Unpoison exactly the amount of bytes requested. If the
+ * data is smaller than the header, we must poison any
+ * unused bytes that were part of the header.
+ */
+ if (size < BUDDY_HEADER_OFF + sizeof(buddy_header_t))
+ asan_poison((u8 __arena *)address + BUDDY_HEADER_OFF,
+ BUDDY_POISONED, sizeof(buddy_header_t));
+
+ asan_unpoison((u8 __arena *)address, size);
+
+ return address;
+}
+
+static __always_inline int buddy_free_unlocked(buddy_t *buddy, u64 addr)
+{
+ buddy_header_t *header, *buddy_header;
+ u64 idx, buddy_idx, tmp_idx;
+ buddy_chunk_t *chunk;
+ bool allocated;
+ u8 order;
+ int ret;
+
+ if (!buddy)
+ return -EINVAL;
+
+ if (addr & (BUDDY_MIN_ALLOC_BYTES - 1)) {
+ arena_stderr("Freeing unaligned address %llx\n", addr);
+ return -EINVAL;
+ }
+
+ /* Get (chunk, idx) out of the address. */
+ chunk = (void __arena *)(addr & ~BUDDY_CHUNK_OFFSET_MASK);
+ idx = (addr & BUDDY_CHUNK_OFFSET_MASK) / BUDDY_MIN_ALLOC_BYTES;
+
+ /* Mark the block as unallocated so we can access the header. */
+ ret = idx_set_allocated(chunk, idx, false);
+ if (ret)
+ return ret;
+
+ order = idx_get_order(chunk, idx);
+ header = idx_to_header(chunk, idx);
+
+ /* The header is in the block itself, keep it unpoisoned. */
+ asan_poison((u8 __arena *)addr, BUDDY_POISONED,
+ BUDDY_MIN_ALLOC_BYTES << order);
+ asan_unpoison(header, sizeof(*header));
+
+ /*
+ * Coalescing loop. Merge with free buddies of equal order.
+ * For every coalescing step, keep the left buddy and
+ * drop the right buddy's header.
+ */
+ for (; order < BUDDY_CHUNK_NUM_ORDERS && can_loop; order++) {
+ buddy_idx = idx ^ (1 << order);
+
+ /* Check if the buddy is actually free. */
+ idx_is_allocated(chunk, buddy_idx, &allocated);
+ if (allocated)
+ break;
+
+ /*
+ * If buddy is not the same order as the chunk
+ * being freed, then we're done coalescing.
+ */
+ if (idx_get_order(chunk, buddy_idx) != order)
+ break;
+
+ buddy_header = idx_to_header(chunk, buddy_idx);
+ header_remove_freelist(chunk, buddy_header, order);
+
+ /* Keep the left header out of the two buddies, drop the other one. */
+ if (buddy_idx < idx) {
+ tmp_idx = idx;
+ idx = buddy_idx;
+ buddy_idx = tmp_idx;
+ }
+
+ /* Remove the buddy from the freelists so that we can merge it. */
+ idx_set_order(chunk, buddy_idx, order);
+
+ buddy_header = idx_to_header(chunk, buddy_idx);
+ asan_poison(buddy_header, BUDDY_POISONED,
+ sizeof(*buddy_header));
+ }
+
+ /* Header properly freed but not in any freelists yet .*/
+ idx_set_order(chunk, idx, order);
+
+ header = idx_to_header(chunk, idx);
+ header_add_freelist(chunk, header, idx, order);
+
+ return 0;
+}
+
+__weak int buddy_free_internal(buddy_t __arg_arena *buddy, u64 addr)
+{
+ int ret;
+
+ if (!buddy)
+ return -EINVAL;
+
+ /* Freeing NULL is a valid no-op. */
+ if (!addr)
+ return 0;
+
+ ret = buddy_lock(buddy);
+ if (ret)
+ return ret;
+
+ ret = buddy_free_unlocked(buddy, addr);
+
+ buddy_unlock(buddy);
+
+ return ret;
+}
+
+__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
new file mode 100644
index 000000000000..e5da1e37e83e
--- /dev/null
+++ b/tools/testing/selftests/bpf/libarena/src/common.bpf.c
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <libarena/common.h>
+#include <libarena/asan.h>
+#include <libarena/buddy.h>
+
+const volatile u32 zero = 0;
+
+buddy_t buddy;
+
+int arena_fls(__u64 word)
+{
+ if (!word)
+ return 0;
+
+ return 64 - __builtin_clzll(word);
+}
+
+SEC("syscall")
+__weak int arena_get_info(struct arena_get_info_args *args)
+{
+ args->arena_base = arena_base(&arena);
+
+ return 0;
+}
+
+SEC("syscall")
+__weak int arena_alloc_reserve(struct arena_alloc_reserve_args *args)
+{
+ return bpf_arena_reserve_pages(&arena, NULL, args->nr_pages);
+}
+
+SEC("syscall")
+__weak int arena_buddy_reset(void)
+{
+ buddy_destroy(&buddy);
+
+ return buddy_init(&buddy);
+}
+
+__weak u64 malloc_internal(size_t size)
+{
+ return buddy_alloc_internal(&buddy, size);
+}
+
+__weak void free(void __arg_arena __arena *ptr)
+{
+ buddy_free_internal(&buddy, (u64)ptr);
+}
+
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/prog_tests/arena_spin_lock.c b/tools/testing/selftests/bpf/prog_tests/arena_spin_lock.c
index 693fd86fbde6..acb9d53b5973 100644
--- a/tools/testing/selftests/bpf/prog_tests/arena_spin_lock.c
+++ b/tools/testing/selftests/bpf/prog_tests/arena_spin_lock.c
@@ -5,13 +5,6 @@
#include <sys/sysinfo.h>
struct __qspinlock { int val; };
-typedef struct __qspinlock arena_spinlock_t;
-
-struct arena_qnode {
- unsigned long next;
- int count;
- int locked;
-};
#include "arena_spin_lock.skel.h"
diff --git a/tools/testing/selftests/bpf/prog_tests/libarena.c b/tools/testing/selftests/bpf/prog_tests/libarena.c
new file mode 100644
index 000000000000..81bdb084c271
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/libarena.c
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <test_progs.h>
+#include <unistd.h>
+
+#include <libarena/common.h>
+#include <libarena/asan.h>
+#include <libarena/buddy.h>
+#include <libarena/userspace.h>
+
+#include "libarena/libarena.skel.h"
+
+static void run_libarena_test(struct libarena *skel, struct bpf_program *prog,
+ const char *name)
+{
+ int ret;
+
+ if (!strstr(name, "test_buddy")) {
+ ret = libarena_run_prog(bpf_program__fd(skel->progs.arena_buddy_reset));
+ if (!ASSERT_OK(ret, "arena_buddy_reset"))
+ return;
+ }
+
+ ret = libarena_run_prog(bpf_program__fd(prog));
+
+ ASSERT_OK(ret, name);
+
+}
+
+void test_libarena(void)
+{
+ struct arena_alloc_reserve_args args;
+ struct libarena *skel;
+ struct bpf_program *prog;
+ int ret;
+
+ skel = libarena__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "open_and_load"))
+ return;
+
+ ret = libarena__attach(skel);
+ if (!ASSERT_OK(ret, "attach"))
+ goto out;
+
+ args.nr_pages = ARENA_RESERVE_PAGES_DFL;
+
+ ret = libarena_run_prog_args(bpf_program__fd(skel->progs.arena_alloc_reserve),
+ &args, sizeof(args));
+ if (!ASSERT_OK(ret, "arena_alloc_reserve"))
+ goto out;
+
+ bpf_object__for_each_program(prog, skel->obj) {
+ const char *name = bpf_program__name(prog);
+
+ if (!libarena_is_test_prog(name))
+ continue;
+
+ if (!test__start_subtest(name))
+ continue;
+
+ run_libarena_test(skel, prog, name);
+ }
+
+out:
+ libarena__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/prog_tests/libarena_asan.c b/tools/testing/selftests/bpf/prog_tests/libarena_asan.c
new file mode 100644
index 000000000000..d59d9dd12ef2
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/libarena_asan.c
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <test_progs.h>
+
+#ifdef HAS_BPF_ARENA_ASAN
+#include <unistd.h>
+
+#include <libarena/common.h>
+#include <libarena/asan.h>
+#include <libarena/buddy.h>
+#include <libarena/userspace.h>
+
+#include "libarena/libarena_asan.skel.h"
+
+static void run_libarena_asan_test(struct libarena_asan *skel,
+ struct bpf_program *prog, const char *name)
+{
+ int ret;
+
+ if (!strstr(name, "test_buddy")) {
+ ret = libarena_run_prog(bpf_program__fd(skel->progs.arena_buddy_reset));
+ if (!ASSERT_OK(ret, "arena_buddy_reset"))
+ return;
+ }
+
+ ret = libarena_run_prog(bpf_program__fd(prog));
+ ASSERT_OK(ret, name);
+
+ verify_test_stderr(skel->obj, prog);
+}
+
+static void run_test(void)
+{
+ struct arena_alloc_reserve_args args;
+ struct libarena_asan *skel;
+ struct bpf_program *prog;
+ int ret;
+
+ skel = libarena_asan__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "open_and_load"))
+ return;
+
+ ret = libarena_asan__attach(skel);
+ if (!ASSERT_OK(ret, "attach"))
+ goto out;
+
+ args.nr_pages = ARENA_RESERVE_PAGES_DFL;
+
+ ret = libarena_run_prog_args(bpf_program__fd(skel->progs.arena_alloc_reserve),
+ &args, sizeof(args));
+ if (!ASSERT_OK(ret, "arena_alloc_reserve"))
+ goto out;
+
+ ret = libarena_asan_init(
+ bpf_program__fd(skel->progs.arena_get_info),
+ bpf_program__fd(skel->progs.asan_init),
+ (1ULL << 32) / sysconf(_SC_PAGESIZE));
+ if (!ASSERT_OK(ret, "libarena_asan_init"))
+ goto out;
+
+ bpf_object__for_each_program(prog, skel->obj) {
+ const char *name = bpf_program__name(prog);
+
+ if (!libarena_is_asan_test_prog(name))
+ continue;
+
+ if (!test__start_subtest(name))
+ continue;
+
+ run_libarena_asan_test(skel, prog, name);
+ }
+
+out:
+ libarena_asan__destroy(skel);
+}
+
+#endif /* HAS_BPF_ARENA_ASAN */
+
+/*
+ * Run the test depending on whether LLVM can compile arena ASAN
+ * programs.
+ */
+void test_libarena_asan(void)
+{
+#ifdef HAS_BPF_ARENA_ASAN
+ run_test();
+#else
+ test__skip();
+#endif
+
+ return;
+}
+
diff --git a/tools/testing/selftests/bpf/progs/arena_atomics.c b/tools/testing/selftests/bpf/progs/arena_atomics.c
index d1841aac94a2..2e7751a85399 100644
--- a/tools/testing/selftests/bpf/progs/arena_atomics.c
+++ b/tools/testing/selftests/bpf/progs/arena_atomics.c
@@ -5,7 +5,7 @@
#include <bpf/bpf_tracing.h>
#include <stdbool.h>
#include <stdatomic.h>
-#include "bpf_arena_common.h"
+#include <bpf_arena_common.h>
#include "../../../include/linux/filter.h"
#include "bpf_misc.h"
diff --git a/tools/testing/selftests/bpf/progs/arena_spin_lock.c b/tools/testing/selftests/bpf/progs/arena_spin_lock.c
index 086b57a426cf..7236d92d382f 100644
--- a/tools/testing/selftests/bpf/progs/arena_spin_lock.c
+++ b/tools/testing/selftests/bpf/progs/arena_spin_lock.c
@@ -4,7 +4,7 @@
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_helpers.h>
#include "bpf_misc.h"
-#include "bpf_arena_spin_lock.h"
+#include <bpf_arena_spin_lock.h>
struct {
__uint(type, BPF_MAP_TYPE_ARENA);
diff --git a/tools/testing/selftests/bpf/progs/compute_live_registers.c b/tools/testing/selftests/bpf/progs/compute_live_registers.c
index f05e120f3450..d055fc7b3b95 100644
--- a/tools/testing/selftests/bpf/progs/compute_live_registers.c
+++ b/tools/testing/selftests/bpf/progs/compute_live_registers.c
@@ -3,7 +3,7 @@
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include "../../../include/linux/filter.h"
-#include "bpf_arena_common.h"
+#include <bpf_arena_common.h>
#include "bpf_misc.h"
struct {
diff --git a/tools/testing/selftests/bpf/progs/lpm_trie_bench.c b/tools/testing/selftests/bpf/progs/lpm_trie_bench.c
index a0e6ebd5507a..2831cf4445e8 100644
--- a/tools/testing/selftests/bpf/progs/lpm_trie_bench.c
+++ b/tools/testing/selftests/bpf/progs/lpm_trie_bench.c
@@ -7,7 +7,7 @@
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_core_read.h>
#include "bpf_misc.h"
-#include "bpf_atomic.h"
+#include <bpf_atomic.h>
#include "progs/lpm_trie.h"
#define BPF_OBJ_NAME_LEN 16U
diff --git a/tools/testing/selftests/bpf/progs/stream.c b/tools/testing/selftests/bpf/progs/stream.c
index 6f999ba951a3..92ba1d72e0ec 100644
--- a/tools/testing/selftests/bpf/progs/stream.c
+++ b/tools/testing/selftests/bpf/progs/stream.c
@@ -5,7 +5,7 @@
#include <bpf/bpf_helpers.h>
#include "bpf_misc.h"
#include "bpf_experimental.h"
-#include "bpf_arena_common.h"
+#include <bpf_arena_common.h>
struct arr_elem {
struct bpf_res_spin_lock lock;
diff --git a/tools/testing/selftests/bpf/progs/verifier_arena.c b/tools/testing/selftests/bpf/progs/verifier_arena.c
index 62e282f4448a..89d72c8d756a 100644
--- a/tools/testing/selftests/bpf/progs/verifier_arena.c
+++ b/tools/testing/selftests/bpf/progs/verifier_arena.c
@@ -8,7 +8,7 @@
#include <bpf/bpf_tracing.h>
#include "bpf_misc.h"
#include "bpf_experimental.h"
-#include "bpf_arena_common.h"
+#include <bpf_arena_common.h>
#define private(name) SEC(".bss." #name) __hidden __attribute__((aligned(8)))
diff --git a/tools/testing/selftests/bpf/progs/verifier_arena_globals1.c b/tools/testing/selftests/bpf/progs/verifier_arena_globals1.c
index 83182ddbfb95..45d364b0bc85 100644
--- a/tools/testing/selftests/bpf/progs/verifier_arena_globals1.c
+++ b/tools/testing/selftests/bpf/progs/verifier_arena_globals1.c
@@ -6,7 +6,7 @@
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "bpf_experimental.h"
-#include "bpf_arena_common.h"
+#include <bpf_arena_common.h>
#include "bpf_misc.h"
#define ARENA_PAGES (1UL<< (32 - __builtin_ffs(__PAGE_SIZE) + 1))
diff --git a/tools/testing/selftests/bpf/progs/verifier_arena_globals2.c b/tools/testing/selftests/bpf/progs/verifier_arena_globals2.c
index e6bd7b61f9f1..b51594dbc005 100644
--- a/tools/testing/selftests/bpf/progs/verifier_arena_globals2.c
+++ b/tools/testing/selftests/bpf/progs/verifier_arena_globals2.c
@@ -7,7 +7,7 @@
#include <bpf/bpf_tracing.h>
#include "bpf_misc.h"
#include "bpf_experimental.h"
-#include "bpf_arena_common.h"
+#include <bpf_arena_common.h>
#define ARENA_PAGES (32)
diff --git a/tools/testing/selftests/bpf/progs/verifier_arena_large.c b/tools/testing/selftests/bpf/progs/verifier_arena_large.c
index 5f7e7afee169..6ab8730d4878 100644
--- a/tools/testing/selftests/bpf/progs/verifier_arena_large.c
+++ b/tools/testing/selftests/bpf/progs/verifier_arena_large.c
@@ -7,7 +7,7 @@
#include <bpf/bpf_tracing.h>
#include "bpf_misc.h"
#include "bpf_experimental.h"
-#include "bpf_arena_common.h"
+#include <bpf_arena_common.h>
#define ARENA_SIZE (1ull << 32)
diff --git a/tools/testing/selftests/bpf/progs/verifier_ldsx.c b/tools/testing/selftests/bpf/progs/verifier_ldsx.c
index c8494b682c31..1026524a1983 100644
--- a/tools/testing/selftests/bpf/progs/verifier_ldsx.c
+++ b/tools/testing/selftests/bpf/progs/verifier_ldsx.c
@@ -3,7 +3,7 @@
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include "bpf_misc.h"
-#include "bpf_arena_common.h"
+#include <bpf_arena_common.h>
#if (defined(__TARGET_ARCH_arm64) || defined(__TARGET_ARCH_x86) || \
(defined(__TARGET_ARCH_riscv) && __riscv_xlen == 64) || \
diff --git a/tools/testing/selftests/bpf/test_loader.c b/tools/testing/selftests/bpf/test_loader.c
index c4c34cae6102..ee637809a1d4 100644
--- a/tools/testing/selftests/bpf/test_loader.c
+++ b/tools/testing/selftests/bpf/test_loader.c
@@ -93,7 +93,7 @@ void test_loader_fini(struct test_loader *tester)
free(tester->log_buf);
}
-static void free_msgs(struct expected_msgs *msgs)
+void free_msgs(struct expected_msgs *msgs)
{
int i;
@@ -789,6 +789,43 @@ static void emit_stderr(const char *stderr, bool force)
fprintf(stdout, "STDERR:\n=============\n%s=============\n", stderr);
}
+static void verify_stderr(int prog_fd, struct expected_msgs *msgs)
+{
+ LIBBPF_OPTS(bpf_prog_stream_read_opts, ropts);
+ char *buf;
+ int ret;
+
+ if (!msgs->cnt)
+ return;
+
+ buf = malloc(TEST_LOADER_LOG_BUF_SZ);
+ if (!ASSERT_OK_PTR(buf, "malloc"))
+ return;
+
+ ret = bpf_prog_stream_read(prog_fd, 2, buf, TEST_LOADER_LOG_BUF_SZ - 1,
+ &ropts);
+ if (ret > 0) {
+ buf[ret] = '\0';
+ emit_stderr(buf, false);
+ validate_msgs(buf, msgs, emit_stderr);
+ } else {
+ ASSERT_GT(ret, 0, "stderr stream read");
+ }
+
+ free(buf);
+}
+
+void verify_test_stderr(struct bpf_object *obj, struct bpf_program *prog)
+{
+ struct test_spec spec = {};
+
+ if (parse_test_spec(NULL, obj, prog, &spec))
+ return;
+
+ verify_stderr(bpf_program__fd(prog), &spec.priv.stderr);
+ free_test_spec(&spec);
+}
+
static void emit_stdout(const char *bpf_stdout, bool force)
{
if (!force && env.verbosity == VERBOSE_NONE)
@@ -1314,17 +1351,7 @@ void run_subtest(struct test_loader *tester,
goto tobj_cleanup;
}
- if (subspec->stderr.cnt) {
- err = get_stream(2, bpf_program__fd(tprog),
- tester->log_buf, tester->log_buf_sz);
- if (err <= 0) {
- PRINT_FAIL("Unexpected retval from get_stream(): %d, errno = %d\n",
- err, errno);
- goto tobj_cleanup;
- }
- emit_stderr(tester->log_buf, false /*force*/);
- validate_msgs(tester->log_buf, &subspec->stderr, emit_stderr);
- }
+ verify_stderr(bpf_program__fd(tprog), &subspec->stderr);
if (subspec->stdout.cnt) {
err = get_stream(1, bpf_program__fd(tprog),
diff --git a/tools/testing/selftests/bpf/test_progs.h b/tools/testing/selftests/bpf/test_progs.h
index 1a44467f4310..37955a8ad385 100644
--- a/tools/testing/selftests/bpf/test_progs.h
+++ b/tools/testing/selftests/bpf/test_progs.h
@@ -563,5 +563,7 @@ struct expected_msgs {
void validate_msgs(const char *log_buf, struct expected_msgs *msgs,
void (*emit_fn)(const char *buf, bool force));
+void free_msgs(struct expected_msgs *msgs);
+void verify_test_stderr(struct bpf_object *obj, struct bpf_program *prog);
#endif /* __TEST_PROGS_H */