From: Emil Tsalapatis <emil@etsalapatis.com>
To: bpf@vger.kernel.org
Cc: ast@kernel.org, andrii@kernel.org, memxor@gmail.com,
daniel@iogearbox.net, eddyz87@gmail.com,
nickolay.lysenko@gmail.com,
Emil Tsalapatis <emil@etsalapatis.com>
Subject: [PATCH bpf-next 5/5] selftests/bpf: Add arena allocation race tests
Date: Wed, 2 Sep 2026 03:02:39 -0400 [thread overview]
Message-ID: <20260902070239.16968-6-emil@etsalapatis.com> (raw)
In-Reply-To: <20260902070239.16968-1-emil@etsalapatis.com>
Add selftests to handle arena page allocation-related races.
Ensure that concurrent frees and nonsleepable/sleepable page
allocations, as well as allocations from userspace, do not
lead to inconsistent or lost data.
Signed-off-by: Emil Tsalapatis <emil@etsalapatis.com>
---
.../selftests/bpf/prog_tests/arena_race.c | 251 ++++++++++++++++++
.../testing/selftests/bpf/progs/arena_race.c | 163 ++++++++++++
2 files changed, 414 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/arena_race.c
create mode 100644 tools/testing/selftests/bpf/progs/arena_race.c
diff --git a/tools/testing/selftests/bpf/prog_tests/arena_race.c b/tools/testing/selftests/bpf/prog_tests/arena_race.c
new file mode 100644
index 000000000000..c3a2a4397315
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/arena_race.c
@@ -0,0 +1,251 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <sys/syscall.h>
+#include <test_progs.h>
+
+#include "arena_race.skel.h"
+
+struct free_thread_ctx {
+ struct arena_race *skel;
+ int err;
+ __u32 retval;
+};
+
+struct fault_thread_ctx {
+ __u64 *addr;
+ int stop;
+};
+
+static int run_prog(struct bpf_program *prog, const char *name)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ int err;
+
+ err = bpf_prog_test_run_opts(bpf_program__fd(prog), &opts);
+ return ASSERT_OK(err, name) && ASSERT_OK(opts.retval, name) ? 0 : -1;
+}
+
+/* Trigger the sleepable free page path. */
+static void *run_free_thread(void *arg)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ struct free_thread_ctx *ctx = arg;
+
+ ctx->skel->bss->target_tid = sys_gettid();
+ ctx->err = bpf_prog_test_run_opts(
+ bpf_program__fd(ctx->skel->progs.free_page), &opts);
+ ctx->retval = opts.retval;
+ return NULL;
+}
+
+/* Continuously fault in the address. */
+static void *fault_reader_thread(void *arg)
+{
+ struct fault_thread_ctx *ctx = arg;
+
+ while (!READ_ONCE(ctx->stop))
+ (void)READ_ONCE(*ctx->addr);
+ return NULL;
+}
+
+static int wait_for(int *p)
+{
+ __u64 deadline = get_time_ns() + 5ULL * 1000 * 1000 * 1000;
+
+ while (!READ_ONCE(*p)) {
+ if (get_time_ns() > deadline)
+ return -ETIMEDOUT;
+ }
+ return 0;
+}
+
+static struct arena_race *setup_arena(__u64 **addr)
+{
+ struct arena_race *skel;
+ size_t arena_sz;
+ char *base;
+ int err;
+
+ skel = arena_race__open();
+ if (!ASSERT_OK_PTR(skel, "open"))
+ return NULL;
+
+ err = arena_race__load(skel);
+ if (!ASSERT_OK(err, "load"))
+ goto err_out;
+ err = arena_race__attach(skel);
+ if (!ASSERT_OK(err, "attach"))
+ goto err_out;
+
+ if (run_prog(skel->progs.alloc_old, "alloc_old"))
+ goto err_out;
+ if (skel->bss->skip) {
+ test__skip();
+ goto err_out;
+ }
+
+ base = bpf_map__initial_value(skel->maps.arena, &arena_sz);
+ if (!ASSERT_OK_PTR(base, "arena_base"))
+ goto err_out;
+ *addr = (__u64 *)(base + getpagesize());
+ if (!ASSERT_EQ((unsigned long)skel->bss->ptr, (unsigned long)*addr,
+ "arena_ptr"))
+ goto err_out;
+ return skel;
+
+err_out:
+ arena_race__destroy(skel);
+ return NULL;
+}
+
+static void test_free_before_flush(bool deferred)
+{
+ struct free_thread_ctx ctx = {};
+ struct arena_race *skel;
+ pthread_t thread;
+ __u64 *addr;
+ bool thread_created = false, flush_seen = false, completed = false;
+ int err;
+
+ skel = setup_arena(&addr);
+ if (!skel)
+ return;
+
+ /* Pause during a TLB flush to widen the race window. */
+ skel->bss->pause_on_flush = 1;
+
+ if (deferred) {
+ /*
+ * Test the nonsleepable free path that gets
+ * deferred to a worker in the kernel. We do
+ * so by triggering the arena operation from
+ * a nonsleepable tracepoint context.
+ */
+ skel->bss->trigger_pid_tgid =
+ ((__u64)getpid() << 32) | (__u32)sys_gettid();
+ skel->bss->trigger_syscall = SYS_getpgid;
+ skel->bss->deferred_free = 1;
+ if (!ASSERT_GE(syscall(SYS_getpgid, 0), 0, "deferred_free"))
+ goto release;
+ } else {
+ /*
+ * Test the sleepable arena free path through a
+ * syscall test prog.
+ */
+ ctx.skel = skel;
+ err = pthread_create(&thread, NULL, run_free_thread, &ctx);
+ if (!ASSERT_OK(err, "pthread_create")) {
+ skel->bss->release = 1;
+ goto out;
+ }
+ thread_created = true;
+ }
+
+ /* Wait until the worker thread triggers a flush. */
+ err = wait_for(&skel->bss->flush_entered);
+ if (!ASSERT_OK(err, "flush_entered"))
+ goto release;
+
+ flush_seen = true;
+
+ /* Force a reallocation during the flush. */
+ run_prog(skel->progs.try_realloc, "realloc_before_flush");
+ ASSERT_NULL(skel->bss->realloc_ptr, "realloc_before_flush");
+
+release:
+ skel->bss->release = 1;
+ if (thread_created) {
+ ASSERT_OK(pthread_join(thread, NULL), "pthread_join");
+ thread_created = false;
+ completed = ASSERT_OK(ctx.err, "free_run") &&
+ ASSERT_OK(ctx.retval, "free_retval");
+ } else if (skel->bss->target_tid) {
+ err = wait_for(&skel->bss->worker_exited);
+ completed = ASSERT_OK(err, "worker_exited");
+ }
+ ASSERT_FALSE(skel->bss->timed_out, "flush_timed_out");
+
+ if (flush_seen && completed &&
+ !run_prog(skel->progs.try_realloc, "realloc_after_flush")) {
+ ASSERT_EQ((unsigned long)skel->bss->realloc_ptr,
+ (unsigned long)addr, "realloc_after_flush");
+ ASSERT_EQ(*addr, skel->rodata->new_marker, "new_marker");
+ }
+out:
+
+ if (thread_created)
+ pthread_join(thread, NULL);
+
+ arena_race__destroy(skel);
+}
+
+/*
+ * Force a race between a faulting thread in userspace and a
+ * free operation on the arena.
+ */
+static void test_fault_free_realloc(void)
+{
+ struct fault_thread_ctx fault = {};
+ struct arena_race *skel;
+ pthread_t fault_thread;
+ bool fault_created = false;
+ __u64 *addr;
+ __u64 expected, value;
+ int err, i;
+
+ skel = setup_arena(&addr);
+ if (!skel)
+ return;
+
+ skel->bss->realloc_after_free = 1;
+
+ fault.addr = addr;
+ err = pthread_create(&fault_thread, NULL, fault_reader_thread, &fault);
+ if (!ASSERT_OK(err, "pthread_create_fault"))
+ goto out;
+ fault_created = true;
+
+ for (i = 0; i < 1000 && !READ_ONCE(fault.stop); i++) {
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+
+ err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.free_page),
+ &opts);
+ if (err) {
+ ASSERT_OK(err, "free_realloc");
+ break;
+ }
+ if (opts.retval) {
+ ASSERT_OK(opts.retval, "free_realloc");
+ break;
+ }
+ value = *addr;
+ expected = skel->bss->current_marker;
+ if (value != expected) {
+ ASSERT_EQ(value, expected, "marker_after_realloc");
+ break;
+ }
+ }
+
+ WRITE_ONCE(fault.stop, 1);
+ if (fault_created) {
+ ASSERT_OK(pthread_join(fault_thread, NULL), "pthread_join_fault");
+ fault_created = false;
+ }
+ ASSERT_GE(i, 1, "race_iterations");
+out:
+ WRITE_ONCE(fault.stop, 1);
+ if (fault_created)
+ pthread_join(fault_thread, NULL);
+ arena_race__destroy(skel);
+}
+
+void serial_test_arena_race(void)
+{
+ if (test__start_subtest("free_before_flush"))
+ test_free_before_flush(false);
+ if (test__start_subtest("deferred_free_before_flush"))
+ test_free_before_flush(true);
+ if (test__start_subtest("fault_free_realloc"))
+ test_fault_free_realloc();
+}
diff --git a/tools/testing/selftests/bpf/progs/arena_race.c b/tools/testing/selftests/bpf/progs/arena_race.c
new file mode 100644
index 000000000000..df5b54ef5b4c
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/arena_race.c
@@ -0,0 +1,163 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define BPF_NO_KFUNC_PROTOTYPES
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include "bpf_experimental.h"
+#include <bpf_arena_common.h>
+
+const volatile __u64 old_marker = 0x1111222233334444ULL;
+const volatile __u64 new_marker = 0x5555666677778888ULL;
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ __uint(max_entries, 4);
+#ifdef __TARGET_ARCH_arm64
+ __ulong(map_extra, 0x1ull << 32);
+#else
+ __ulong(map_extra, 0x1ull << 44);
+#endif
+} arena SEC(".maps");
+
+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST) || defined(BPF_ARENA_FORCE_ASM)
+bool skip;
+#else
+bool skip = true;
+#endif
+
+void __arena *ptr;
+void __arena *realloc_ptr;
+bool realloc_after_free;
+int free_started;
+__u64 current_marker;
+__u64 marker_seq;
+
+int target_tid;
+int pause_on_flush;
+int flush_entered;
+int release;
+int timed_out;
+
+__u64 trigger_pid_tgid;
+long trigger_syscall;
+int deferred_free;
+int worker_armed;
+int worker_exited;
+
+static __always_inline void wait_for_release(void)
+{
+ while (!*(volatile int *)&release && can_loop)
+ ;
+ if (!*(volatile int *)&release)
+ timed_out = 1;
+}
+
+SEC("syscall")
+int alloc_old(void *ctx)
+{
+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST) || defined(BPF_ARENA_FORCE_ASM)
+ __u64 __arena *p;
+ char __arena *base = arena_base(&arena);
+
+ realloc_ptr = NULL;
+ ptr = bpf_arena_alloc_pages(&arena, base + __PAGE_SIZE, 1,
+ NUMA_NO_NODE, 0);
+ if (!ptr)
+ return 1;
+ p = (__u64 __arena *)ptr;
+ *p = old_marker;
+ current_marker = old_marker;
+#endif
+ return 0;
+}
+
+SEC("syscall")
+int free_page(void *ctx)
+{
+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST) || defined(BPF_ARENA_FORCE_ASM)
+ __u64 __arena *p;
+ __u64 marker;
+
+ if (!ptr)
+ return 1;
+ free_started = 1;
+ bpf_arena_free_pages(&arena, ptr, 1);
+ if (!realloc_after_free)
+ return 0;
+
+ marker = new_marker + ++marker_seq;
+ realloc_ptr = bpf_arena_alloc_pages(&arena, ptr, 1, NUMA_NO_NODE, 0);
+ if (realloc_ptr)
+ ptr = realloc_ptr;
+ else
+ realloc_ptr = ptr;
+ p = (__u64 __arena *)ptr;
+ *p = marker;
+ current_marker = marker;
+#endif
+ return 0;
+}
+
+SEC("syscall")
+int try_realloc(void *ctx)
+{
+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST) || defined(BPF_ARENA_FORCE_ASM)
+ __u64 __arena *p;
+
+ realloc_ptr = bpf_arena_alloc_pages(&arena, ptr, 1, NUMA_NO_NODE, 0);
+ if (realloc_ptr) {
+ ptr = realloc_ptr;
+ p = (__u64 __arena *)realloc_ptr;
+ *p = new_marker;
+ current_marker = new_marker;
+ }
+#endif
+ return 0;
+}
+
+SEC("tp_btf/sys_enter")
+int BPF_PROG(deferred_free_prog, struct pt_regs *regs, long id)
+{
+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST) || defined(BPF_ARENA_FORCE_ASM)
+ if (!deferred_free || bpf_get_current_pid_tgid() != trigger_pid_tgid ||
+ id != trigger_syscall)
+ return 0;
+
+ deferred_free = 0;
+ /* The worker can run on another CPU before the kfunc returns. */
+ worker_armed = 1;
+ bpf_arena_free_pages(&arena, ptr, 1);
+#endif
+ return 0;
+}
+
+SEC("fentry/arena_free_worker")
+int BPF_PROG(trace_free_worker, struct work_struct *work)
+{
+ if (worker_armed && !target_tid)
+ target_tid = (__u32)bpf_get_current_pid_tgid();
+ return 0;
+}
+
+SEC("fexit/arena_free_worker")
+int BPF_PROG(trace_free_worker_ret, struct work_struct *work)
+{
+ if ((__u32)bpf_get_current_pid_tgid() == target_tid)
+ worker_exited = 1;
+ return 0;
+}
+
+SEC("fentry/flush_tlb_kernel_range")
+int BPF_PROG(trace_flush, unsigned long start, unsigned long end)
+{
+ if (!pause_on_flush ||
+ (__u32)bpf_get_current_pid_tgid() != target_tid)
+ return 0;
+ flush_entered = 1;
+ wait_for_release();
+ return 0;
+}
+
+char _license[] SEC("license") = "GPL";
--
2.55.0
next prev parent reply other threads:[~2026-09-02 7:02 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-02 7:02 [PATCH bpf-next 0/5] bpf: Fix arena memory incoherence Emil Tsalapatis
2026-09-02 7:02 ` [PATCH bpf-next 1/5] bpf: Update is_range_tree_set to work for consecutive ranges Emil Tsalapatis
2026-09-02 8:01 ` bot+bpf-ci
2026-09-02 7:02 ` [PATCH bpf-next 2/5] bpf: Track availability information for ranges in range tree Emil Tsalapatis
2026-09-02 8:20 ` bot+bpf-ci
2026-09-02 7:02 ` [PATCH bpf-next 3/5] bpf: Fix arena race between page free and alloc leading to incoherency Emil Tsalapatis
2026-09-02 8:20 ` bot+bpf-ci
2026-09-02 7:02 ` [PATCH bpf-next 4/5] bpf: Atomically update PTE and range tree in arena VM fault handler Emil Tsalapatis
2026-09-02 7:19 ` sashiko-bot
2026-09-02 7:02 ` Emil Tsalapatis [this message]
2026-09-02 7:14 ` [PATCH bpf-next 5/5] selftests/bpf: Add arena allocation race tests sashiko-bot
2026-09-02 8:20 ` bot+bpf-ci
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260902070239.16968-6-emil@etsalapatis.com \
--to=emil@etsalapatis.com \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=memxor@gmail.com \
--cc=nickolay.lysenko@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox