Introduce selftests for the buddy allocator without ASAN. Add the libarena selftests both to the libarena test runner and to test_progs, so that they are a) available when libarena is pulled as a standalone library, and b) exercised along with all other test programs in this directory. Currently ASAN for libarena requires LLVM 22, which is not available in the CI. Delay testing the ASAN case directly through test_progs until that is the case. The ASAN version is available for testing on demand through the library's own harness. Signed-off-by: Emil Tsalapatis --- .../bpf/libarena/selftests/selftest.c | 12 + .../libarena/selftests/st_asan_buddy.bpf.c | 251 ++++++++++++++++++ .../bpf/libarena/selftests/st_buddy.bpf.c | 230 ++++++++++++++++ .../selftests/bpf/prog_tests/libarena.c | 44 +++ 4 files changed, 537 insertions(+) create mode 100644 tools/testing/selftests/bpf/libarena/selftests/st_asan_buddy.bpf.c create mode 100644 tools/testing/selftests/bpf/libarena/selftests/st_buddy.bpf.c create mode 100644 tools/testing/selftests/bpf/prog_tests/libarena.c diff --git a/tools/testing/selftests/bpf/libarena/selftests/selftest.c b/tools/testing/selftests/bpf/libarena/selftests/selftest.c index 2bd8363ea614..ad7e00c2de0f 100644 --- a/tools/testing/selftests/bpf/libarena/selftests/selftest.c +++ b/tools/testing/selftests/bpf/libarena/selftests/selftest.c @@ -137,6 +137,12 @@ error_no_destroy: \ return ret; \ } +TEST(test_buddy); + +#ifdef BPF_ARENA_ASAN +TEST(asan_test_buddy); +#endif + static void banner(const char *progpath) { @@ -174,5 +180,11 @@ int main(int argc, char *argv[]) libbpf_set_print(libbpf_print_fn); + run_test_buddy(); + +#ifdef BPF_ARENA_ASAN + run_asan_test_buddy(); +#endif + return 0; } 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..61b551639f6a --- /dev/null +++ b/tools/testing/selftests/bpf/libarena/selftests/st_asan_buddy.bpf.c @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */ + +#include + +#include +#include + +#include "selftest.h" + + +#ifdef BPF_ARENA_ASAN + +#include "st_asan_common.h" + +private(ST_BUDDY) struct buddy st_buddy_asan; + +u64 __arena st_asan_buddy_lock; + +static __always_inline int asan_test_buddy_oob_single(size_t alloc_size) +{ + u8 __arena *mem; + int i; + + ASAN_VALIDATE(); + + mem = buddy_alloc(&st_buddy_asan, alloc_size); + if (!mem) { + arena_stdout("buddy_alloc failed for size %lu", alloc_size); + return -ENOMEM; + } + + ASAN_VALIDATE(); + + for (i = zero; i < alloc_size && can_loop; i++) { + mem[i] = 0xba; + ASAN_VALIDATE_ADDR(false, &mem[i]); + } + + mem[alloc_size] = 0xba; + ASAN_VALIDATE_ADDR(true, &mem[alloc_size]); + + buddy_free(&st_buddy_asan, 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) +{ + /* 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; + ASAN_VALIDATE_ADDR(freed, &mem[i]); + + return 0; +} + +__weak int asan_test_buddy_uaf_single(size_t alloc_size) +{ + u8 __arena *mem; + int ret; + int i; + + mem = buddy_alloc(&st_buddy_asan, alloc_size); + if (!mem) { + arena_stdout("buddy_alloc failed for size %lu", alloc_size); + return -ENOMEM; + } + + ASAN_VALIDATE(); + + for (i = zero; i < alloc_size && can_loop; i++) { + ret = asan_test_buddy_byte(mem, i, false); + if (ret) + return ret; + } + + ASAN_VALIDATE(); + + buddy_free(&st_buddy_asan, 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; + + blob = buddy_alloc(&st_buddy_asan, alloc_size); + if (!blob) + return -ENOMEM; + + blob->mem[0] = 0xba; + ASAN_VALIDATE_ADDR(false, &blob->mem[0]); + + blob->mem[47] = 0xba; + ASAN_VALIDATE_ADDR(false, &blob->mem[47]); + + blob->oob = 0; + ASAN_VALIDATE_ADDR(true, &blob->oob); + + buddy_free(&st_buddy_asan, (void __arena *)blob); + + return 0; +} + +static __always_inline int asan_test_buddy_oob(void) +{ + size_t sizes[] = { + 7, 8, 17, 18, 64, 256, 317, 512, 1024, + }; + int ret, i; + + ret = buddy_init(&st_buddy_asan, + (arena_spinlock_t __arena *)&st_asan_buddy_lock); + 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(&st_buddy_asan); + return ret; + } + } + + buddy_destroy(&st_buddy_asan); + + ASAN_VALIDATE(); + + return 0; +} + +__weak int asan_test_buddy_uaf(void) +{ + size_t sizes[] = { 16, 32, 64, 128, 256, 512, 128, 1024, 16384 }; + int ret, i; + + ret = buddy_init(&st_buddy_asan, + (arena_spinlock_t __arena *)&st_asan_buddy_lock); + 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(&st_buddy_asan); + return ret; + } + } + + buddy_destroy(&st_buddy_asan); + + ASAN_VALIDATE(); + + return 0; +} + +static __always_inline int asan_test_buddy_blob(void) +{ + const int iters = 10; + int ret, i; + + ret = buddy_init(&st_buddy_asan, + (arena_spinlock_t __arena *)&st_asan_buddy_lock); + 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(&st_buddy_asan); + return ret; + } + } + + buddy_destroy(&st_buddy_asan); + + ASAN_VALIDATE(); + + return 0; +} + +SEC("syscall") +int asan_test_buddy(void) +{ + int ret; + + ret = asan_test_buddy_oob(); + if (ret) { + arena_stdout("%s:%d OOB test failed", __func__, __LINE__); + return ret; + } + + ret = asan_test_buddy_uaf(); + if (ret) { + arena_stdout("%s:%d UAF test failed", __func__, __LINE__); + return ret; + } + + ret = asan_test_buddy_blob(); + if (ret) { + arena_stdout("%s:%d blob test failed", __func__, __LINE__); + return ret; + } + + return 0; +} + +#else + +SEC("syscall") +int asan_test_buddy(void) +{ + return -EOPNOTSUPP; +} + +#endif /* BPF_ARENA_ASAN */ + +__weak char _license[] SEC("license") = "GPL"; 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..d8354dd2d7fc --- /dev/null +++ b/tools/testing/selftests/bpf/libarena/selftests/st_buddy.bpf.c @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */ + +#include + +#include +#include + +#include "selftest.h" + + +private(ST_BUDDY) struct buddy st_buddy; +static u64 __arena st_buddy_lock; + +struct segarr_entry { + u8 __arena *block; + size_t sz; + u8 poison; +}; + +typedef struct segarr_entry __arena segarr_entry_t; + +#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 }; + +static int test_buddy_create(void) +{ + const int iters = 10; + int ret, i; + + for (i = zero; i < iters && can_loop; i++) { + ret = buddy_init( + &st_buddy, (arena_spinlock_t __arena *)&st_buddy_lock); + if (ret) + return ret; + + ret = buddy_destroy(&st_buddy); + if (ret) + return ret; + } + + return 0; +} + +static int test_buddy_alloc(void) +{ + void __arena *mem; + int ret, i; + + for (i = zero; i < 8 && can_loop; i++) { + ret = buddy_init( + &st_buddy, (arena_spinlock_t __arena *)&st_buddy_lock); + if (ret) + return ret; + + mem = buddy_alloc(&st_buddy, alloc_sizes[i]); + if (!mem) { + buddy_destroy(&st_buddy); + return -ENOMEM; + } + + buddy_destroy(&st_buddy); + } + + return 0; +} + +static int test_buddy_alloc_free(void) +{ + const int iters = 800; + void __arena *mem; + int ret, i; + + ret = buddy_init(&st_buddy, + (arena_spinlock_t __arena *)&st_buddy_lock); + if (ret) + return ret; + + for (i = zero; i < iters && can_loop; i++) { + mem = buddy_alloc(&st_buddy, alloc_free_sizes[(i * 5) % 8]); + if (!mem) { + buddy_destroy(&st_buddy); + return -ENOMEM; + } + + buddy_free(&st_buddy, mem); + } + + buddy_destroy(&st_buddy); + + return 0; +} + +static int test_buddy_alloc_multiple(void) +{ + int ret, j; + u32 i, idx; + u8 __arena *mem; + size_t sz; + u8 poison; + + ret = buddy_init(&st_buddy, + (arena_spinlock_t __arena *)&st_buddy_lock); + 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. + */ + idx = 0; + for (i = zero; i < SEGARRLEN && can_loop; i++) { + sz = alloc_multiple_sizes[i % 9]; + poison = (u8)i; + + mem = buddy_alloc(&st_buddy, sz); + if (!mem) { + buddy_destroy(&st_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(&st_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(&st_buddy); + arena_stdout("%s:%d %lx %u vs %u", __func__, + __LINE__, &mem[j], mem[j], poison); + return -EINVAL; + } + } + + buddy_free(&st_buddy, mem); + } + + buddy_destroy(&st_buddy); + + return 0; +} + +static int test_buddy_alignment(void) +{ + int ret, i; + + ret = buddy_init(&st_buddy, + (arena_spinlock_t __arena *)&st_buddy_lock); + if (ret) + return ret; + + /* Allocate various sizes and check alignment */ + for (i = zero; i < 17 && can_loop; i++) { + ptrs[i] = buddy_alloc(&st_buddy, alignment_sizes[i]); + if (!ptrs[i]) { + arena_stdout("alignment test: alloc failed for size %lu", + alignment_sizes[i]); + buddy_destroy(&st_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(&st_buddy); + return -EINVAL; + } + } + + /* Free all allocations */ + for (i = zero; i < 17 && can_loop; i++) { + buddy_free(&st_buddy, ptrs[i]); + } + + buddy_destroy(&st_buddy); + + return 0; +} + +#define BUDDY_ALLOC_SELFTEST(suffix) ALLOC_SELFTEST(test_buddy_##suffix) + +SEC("syscall") +__weak int test_buddy(void) +{ + BUDDY_ALLOC_SELFTEST(create); + BUDDY_ALLOC_SELFTEST(alloc); + BUDDY_ALLOC_SELFTEST(alloc_free); + BUDDY_ALLOC_SELFTEST(alloc_multiple); + BUDDY_ALLOC_SELFTEST(alignment); + + return 0; +} + +__weak char _license[] SEC("license") = "GPL"; 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..ab0153f8baed --- /dev/null +++ b/tools/testing/selftests/bpf/prog_tests/libarena.c @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: LGPL-2.1 OR BSD-2-Clause +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */ +#include +#include + +#define __arena +typedef uint64_t u64; +typedef uint8_t u8; + +#include "libarena/include/common.h" +#include "libarena/include/asan.h" +#include "libarena/include/selftest_helpers.h" + +#include "libarena/libarena.skel.h" + +static void test_libarena_buddy(void) +{ + struct libarena *skel; + 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; + + ret = libarena_run_prog(bpf_program__fd(skel->progs.arena_alloc_reserve)); + if (!ASSERT_OK(ret, "arena_alloc_reserve")) + goto out; + + ret = libarena_run_prog(bpf_program__fd(skel->progs.test_buddy)); + ASSERT_OK(ret, "test_buddy"); + +out: + libarena__destroy(skel); +} + +void test_libarena(void) +{ + if (test__start_subtest("buddy")) + test_libarena_buddy(); +} -- 2.53.0