Add testing for the libarena bump allocator. The testing covers the behavior of the allocator both by itself and in combination with ASAN to test that there are no false positives or negatives. Signed-off-by: Emil Tsalapatis (Meta) --- .../bpf/libarena/selftests/selftest.c | 12 + .../bpf/libarena/selftests/st_asan_bump.bpf.c | 193 ++++++++++++ .../bpf/libarena/selftests/st_bump.bpf.c | 275 ++++++++++++++++++ 3 files changed, 480 insertions(+) create mode 100644 tools/testing/selftests/bpf/libarena/selftests/st_asan_bump.bpf.c create mode 100644 tools/testing/selftests/bpf/libarena/selftests/st_bump.bpf.c diff --git a/tools/testing/selftests/bpf/libarena/selftests/selftest.c b/tools/testing/selftests/bpf/libarena/selftests/selftest.c index 4d7db66e75ed..278978795b60 100644 --- a/tools/testing/selftests/bpf/libarena/selftests/selftest.c +++ b/tools/testing/selftests/bpf/libarena/selftests/selftest.c @@ -265,6 +265,12 @@ error: \ return ret; \ } +TEST(bump_selftest); + +#ifdef BPF_ARENA_ASAN +TEST(asan_test_bump); +#endif + static void banner(const char *progpath) { @@ -306,5 +312,11 @@ int main(int argc, char *argv[]) libbpf_set_print(libbpf_print_fn); + run_bump_selftest(); + +#ifdef BPF_ARENA_ASAN + run_asan_test_bump(); +#endif + return 0; } diff --git a/tools/testing/selftests/bpf/libarena/selftests/st_asan_bump.bpf.c b/tools/testing/selftests/bpf/libarena/selftests/st_asan_bump.bpf.c new file mode 100644 index 000000000000..a8f0e6e01e4b --- /dev/null +++ b/tools/testing/selftests/bpf/libarena/selftests/st_asan_bump.bpf.c @@ -0,0 +1,193 @@ +/* + * SPDX-License-Identifier: GPL-2.0 + * Copyright (c) 2025 Meta Platforms, Inc. and affiliates. + * Copyright (c) 2025 Emil Tsalapatis + */ + +#include + +#include +#include + +#include "selftest.h" + +#ifdef BPF_ARENA_ASAN + +#include "st_asan_common.h" + +int asan_test_bump_blob_one(void) +{ + volatile struct blob __arena *blob; + const size_t alignment = 1; + + blob = bump_alloc(sizeof(blob) - 1, alignment); + if (!blob) + return -ENOMEM; + + blob->mem[0] = 0xba; + ASAN_VALIDATE_ADDR(false, &blob->mem[0]); + + blob->oob = 0; + ASAN_VALIDATE_ADDR(true, &blob->oob); + + blob = (volatile struct blob __arena *)&blob->oob; + blob->mem[0] = 0xba; + ASAN_VALIDATE_ADDR(true, &blob->mem[0]); + + blob->oob = 4; + ASAN_VALIDATE_ADDR(true, &blob->oob); + + /* + * Go even further, cast the OOB variable into + * another struct blob and access its own oob. + */ + blob = (volatile struct blob __arena *)&blob->oob; + blob->oob = 5; + ASAN_VALIDATE_ADDR(true, &blob->oob); + + return 0; +} + +int asan_test_bump_blob(void) +{ + const int iters = 20; + int ret, i; + + ret = bump_init(ST_PAGES); + if (ret) { + bpf_printk("bump_init failed with %d", ret); + return ret; + } + + for (i = 0; i < iters && can_loop; i++) { + ret = asan_test_bump_blob_one(); + if (ret) { + bpf_printk("%s:%d Failed on iteration %d", __func__, + __LINE__, i); + return ret; + } + } + + bump_destroy(); + + ASAN_VALIDATE(); + + return 0; +} + +int asan_test_bump_array_one(void) +{ + size_t bytes = 37; + size_t overrun = 13; + size_t alignment = 1; + char __arena *mem; + int i; + + mem = bump_alloc(sizeof(*mem) * bytes, alignment); + if (!mem) + return -ENOMEM; + + for (i = 0; i < bytes + overrun && can_loop; i++) { + mem[i] = 0xba; + ASAN_VALIDATE_ADDR(i >= bytes, &mem[i]); + } + + ASAN_VALIDATE(); + + return 0; +} + +int asan_test_bump_array(void) +{ + const size_t iters = 20; + int ret, i; + + ret = bump_init(ST_PAGES); + if (ret) { + bpf_printk("bump_init failed with %d", ret); + return ret; + } + + for (i = 0; i < iters && can_loop; i++) { + ret = asan_test_bump_array_one(); + if (ret) { + bpf_printk("%s:%d Failed on iteration %d", __func__, + __LINE__, i); + return ret; + } + } + + bump_destroy(); + + return 0; +} + +int asan_test_bump_all(void) +{ + const int iters = 50; + int ret, i; + + ret = bump_init(ST_PAGES); + if (ret) { + bpf_printk("bump_init failed with %d", ret); + return ret; + } + + for (i = 0; i < iters && can_loop; i++) { + ret = asan_test_bump_array_one(); + if (ret) { + bpf_printk("%s:%d Failed on iteration %d", __func__, + __LINE__, i); + return ret; + } + + ret = asan_test_bump_blob_one(); + if (ret) { + bpf_printk("%s:%d Failed on iteration %d", __func__, + __LINE__, i); + return ret; + } + } + + bump_destroy(); + + return 0; +} + +SEC("syscall") +int asan_test_bump(void) +{ + int ret; + + ret = asan_test_bump_blob(); + if (ret) { + bpf_printk("%s:%d test failed", __func__, __LINE__); + return ret; + } + + ret = asan_test_bump_array(); + if (ret) { + bpf_printk("%s:%d test failed", __func__, __LINE__); + return ret; + } + + ret = asan_test_bump_all(); + if (ret) { + bpf_printk("%s:%d test failed", __func__, __LINE__); + return ret; + } + + return 0; +} + +#else + +SEC("syscall") +int asan_test_bump(void) +{ + return -EOPNOTSUPP; +} + +#endif /* BPF_ARENA_ASAN */ + +__weak char _license[] SEC("license") = "GPL"; diff --git a/tools/testing/selftests/bpf/libarena/selftests/st_bump.bpf.c b/tools/testing/selftests/bpf/libarena/selftests/st_bump.bpf.c new file mode 100644 index 000000000000..99caae452343 --- /dev/null +++ b/tools/testing/selftests/bpf/libarena/selftests/st_bump.bpf.c @@ -0,0 +1,275 @@ +/* + * SPDX-License-Identifier: GPL-2.0 + * Copyright (c) 2025 Meta Platforms, Inc. and affiliates. + * Copyright (c) 2025 Emil Tsalapatis + */ + +#include +#include +#include + +#include "selftest.h" + +#define ST_MAX_PAGES 8 +#define ST_MAX_BYTES (ST_MAX_PAGES * __PAGE_SIZE) +#define ST_MAX_ALIGNMENT (ST_MAX_BYTES >> 4) + +#define ST_CYCLES 5 + +#define ST_PATTERN1 0xAA +#define ST_PATTERN2 0x55 + +#define ST_EXHAUST_ALLOCS 16 +#define ST_WRAP_PAGES 4 +#define ST_WRAP_BYTES 2048 + +static inline void st_memset(void __arena *mem, u8 byte, size_t size) +{ + u8 __arena *bytes = (u8 __arena *)mem; + int i; + + for (i = 0; i < size && can_loop; i++) { + bytes[i] = byte; + } +} + +static inline bool st_isset(void __arena *mem, u8 byte, size_t size) +{ + u8 __arena *bytes = (u8 __arena *)mem; + int i; + + for (i = 0; i < size && can_loop; i++) { + if (bytes[i] != byte) + return false; + } + + return true; +} + +/* + * Defining oft-repeated snippets as macros to avoid having to propagate + * errors to the caller. Both GCC and Clang support statement expressions. + */ + +#define ALLOC_OR_FAIL(bytes, alignment) \ + ({ \ + void __arena *mem; \ + mem = bump_alloc((bytes), (alignment)); \ + if (!mem) { \ + bpf_printk("%s:%d bump_alloc failed", __func__, \ + __LINE__); \ + bump_destroy(); \ + return -ENOMEM; \ + } \ + mem; \ + }) + +#define INIT_OR_FAIL(bytes) \ + do { \ + if (bump_init(((bytes) / __PAGE_SIZE))) { \ + bpf_printk("%s:%d bump_init failed", __func__, \ + __LINE__); \ + return -ENOMEM; \ + } \ + } while (0) + +#define CHECK_OR_FAIL(mem, val, size) \ + do { \ + if (st_isset((mem), (val), (size))) { \ + bpf_printk("%s:%d val %d missing", __func__, \ + __LINE__); \ + return -EINVAL; \ + } \ + } while (0) + +#define CMP_OR_FAIL(mem1, mem2, size) \ + do { \ + if (st_memcmp((mem1), (mem2), (size))) { \ + bpf_printk("%s:%d regions differ", __func__, \ + __LINE__); \ + return -EINVAL; \ + } \ + } while (0) + +#define ALIGNED_OR_FAIL(mem, alignment) \ + do { \ + if ((u64)(mem) & ((alignment) - 1)) { \ + bpf_printk("%s:%d invalid alignment", __func__, \ + __LINE__); \ + return -EINVAL; \ + } \ + } while (0) + +/* + * Basic test: + * + * - Create the allocator + * - Make a single allocation, + * - Ensure proper alignment + * - Ensure allocation succeeds and values are all 0s. + * - Destroy the allocator. Ensure the allocator returns + * zeroed out memory. + */ +static int bump_selftest_alloc_single(u64 bytes, u64 alignment) +{ + u8 __arena *barray; + void __arena *mem; + int i; + + for (i = 0; i < ST_CYCLES && can_loop; i++) { + INIT_OR_FAIL(bytes); + + mem = ALLOC_OR_FAIL(bytes, alignment); + + /* Alignment is assumed to be 2^n. */ + ALIGNED_OR_FAIL(mem, alignment); + + barray = (u8 __arena *)mem; + CHECK_OR_FAIL(barray, 0, bytes); + + /* Check whether we're touching unallocated memory. */ + st_memset(barray, ST_PATTERN1, bytes); + CHECK_OR_FAIL(barray, ST_PATTERN1, bytes); + + bump_destroy(); + } + + return 0; +} + +static int bump_selftest_alloc_multiple(u64 bytes, u64 alignment) +{ + void __arena *mem1, *mem2; + int ret; + + /* Initialize the allocator */ + ret = bump_init(ST_MAX_PAGES); + if (ret) { + bpf_printk("bump_init failed with %d", ret); + return ret; + } + + mem1 = ALLOC_OR_FAIL(bytes, alignment); + st_memset(mem1, ST_PATTERN1, bytes); + + mem2 = ALLOC_OR_FAIL(bytes, alignment); + st_memset(mem2, ST_PATTERN1, ST_PATTERN2); + + ALIGNED_OR_FAIL(mem1, alignment); + ALIGNED_OR_FAIL(mem2, alignment); + + /* Verify first block still has pattern1 */ + CHECK_OR_FAIL(mem1, ST_PATTERN1, bytes); + CHECK_OR_FAIL(mem2, ST_PATTERN2, bytes); + + bump_destroy(); + return 0; +} + +static int bump_selftest_alloc_aligned(void) +{ + void __arena *mem; + u64 alignment; + int round; + + INIT_OR_FAIL(ST_MAX_PAGES * __PAGE_SIZE); + + /* + * Allocate 1 byte at a time to test allocator alignment. + * Test ascending and descending allocation orders. + */ + for (round = 0; round < 2 && can_loop; round++) { + for (alignment = 1; alignment <= __PAGE_SIZE && can_loop; + alignment <<= 1) { + mem = ALLOC_OR_FAIL(1, alignment); + ALIGNED_OR_FAIL(mem, alignment); + } + + for (alignment = __PAGE_SIZE; alignment >= 1 && can_loop; + alignment >>= 1) { + mem = ALLOC_OR_FAIL(1, alignment); + ALIGNED_OR_FAIL(mem, alignment); + } + } + + bump_destroy(); + + return 0; +} + +static int bump_selftest_alloc_exhaustion(u64 bytes, u64 alignment) +{ + size_t padded = round_up(bytes, alignment); + size_t allocs = bytes / padded; + void __arena *mem; + int i; + + /* Allocate one page at a time here. */ + INIT_OR_FAIL(__PAGE_SIZE); + + if (bump_memlimit(bytes)) { + bpf_printk("%s:%d bump_memlimit failed", __func__, + __LINE__); + return -EINVAL; + } + + /* Make an unfullfilable allocation. */ + mem = bump_alloc(bytes + 1, 1); + if (mem) { + bpf_printk("%s:%d bump_alloc succeeded", __func__, + __LINE__); + bump_destroy(); + return -EINVAL; + } + + /* + * Amounts to allocations of size alignment, but also + * checks that alignment padding is properly accounted for. + */ + for (i = 0; i < allocs && can_loop; i++) + ALLOC_OR_FAIL(1, alignment); + + /* Even a single byte allocation should fail. */ + mem = bump_alloc(1, 1); + if (mem) { + bpf_printk("%s:%d bump_alloc succeeded", __func__, + __LINE__); + bump_destroy(); + return -EINVAL; + } + + bump_destroy(); + return 0; +} + +#define BUMP_ALLOC_SELFTEST(suffix, ...) \ + ALLOC_SELFTEST(bump_selftest_##suffix, __VA_ARGS__) + + +SEC("syscall") +int bump_selftest(void) +{ + u64 bytes = 128; + u64 alignment = 1; + + for (bytes = __PAGE_SIZE; bytes <= ST_MAX_PAGES && can_loop; + bytes <<= 1) { + for (alignment = 1; alignment <= ST_MAX_ALIGNMENT && can_loop; + alignment <<= 1) { + /* Each test manages its own allocator lifecycle */ + BUMP_ALLOC_SELFTEST(alloc_single, bytes, alignment); + BUMP_ALLOC_SELFTEST(alloc_multiple, bytes, alignment); + } + } + + BUMP_ALLOC_SELFTEST(alloc_aligned); + + for (alignment = __PAGE_SIZE; bytes <= ST_MAX_PAGES && can_loop; + bytes <<= 1) + BUMP_ALLOC_SELFTEST(alloc_exhaustion, + ST_MAX_PAGES * __PAGE_SIZE, alignment); + + return 0; +} + +__weak char _license[] SEC("license") = "GPL"; -- 2.47.3