selftests/bpf: Add selftests for libarena buddy allocator

Introduce selftests for the buddy allocator with and 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.

ASAN for libarena requires LLVM 22. Add logic in the top-level
selftests Makefile to only compile the ASAN variant if the
compiler supports it, otherwise skip the test.

Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
Link: https://lore.kernel.org/r/20260426190338.4615-8-emil@etsalapatis.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
This commit is contained in:
Emil Tsalapatis 2026-04-26 15:03:37 -04:00 committed by Alexei Starovoitov
parent 86426a28c5
commit b1487dc1b1
4 changed files with 606 additions and 0 deletions

View File

@ -0,0 +1,240 @@
// 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;
#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")
__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")
__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")
__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";

View File

@ -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";

View File

@ -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);
}

View File

@ -0,0 +1,91 @@
// 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);
}
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;
}