[PATCH bpf v4 2/2] selftests/bpf: Add test for arena VMA use-after-free on fork

Weiming Shi
2026-04-12T02:27:33Z

Add a selftest that reproduces the arena VMA use-after-free fixed in
the previous commit. The test creates an arena, mmaps it, allocates
pages via BPF, forks, has the parent munmap the arena, then has the
child call bpf_arena_free_pages. Without the fix this triggers a
KASAN slab-use-after-free in zap_page_range_single.

Note: the UAF occurs entirely in kernel space and is not observable
from userspace, so this test relies on KASAN to detect the bug.
Without KASAN the test passes regardless of whether the fix is
applied.

Signed-off-by: Weiming Shi <bestswngs@gmail.com>
---
.../selftests/bpf/prog_tests/arena_fork.c | 80 +++++++++++++++++++
.../testing/selftests/bpf/progs/arena_fork.c | 41 ++++++++++
2 files changed, 121 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/arena_fork.c
create mode 100644 tools/testing/selftests/bpf/progs/arena_fork.c

diff --git a/tools/testing/selftests/bpf/prog_tests/arena_fork.c b/tools/testing/selftests/bpf/prog_tests/arena_fork.c
new file mode 100644
index 000000000000..0235884c5906
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/arena_fork.c
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+/*
+ * Test that forking a process with an arena mmap does not cause a
+ * use-after-free when the parent unmaps and the child frees arena pages.
+ */
+#include <test_progs.h>
+#include <sys/mman.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "arena_fork.skel.h"
+
+void test_arena_fork(void)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ struct bpf_map_info info = {};
+ __u32 info_len = sizeof(info);
+ struct arena_fork *skel;
+ long page_size;
+ size_t arena_sz;
+ void *arena_addr;
+ int arena_fd, ret, status;
+ pid_t pid;
+
+ page_size = sysconf(_SC_PAGESIZE);
+ if (!ASSERT_GT(page_size, 0, "page_size"))
+ return;
+
+ skel = arena_fork__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "open_and_load"))
+ return;
+
+ arena_fd = bpf_map__fd(skel->maps.arena);
+
+ /* libbpf mmaps the arena via initial_value */
+ arena_addr = bpf_map__initial_value(skel->maps.arena, &arena_sz);
+ if (!ASSERT_OK_PTR(arena_addr, "arena_mmap"))
+ goto out;
+
+ /* Get real arena byte size for munmap */
+ bpf_map_get_info_by_fd(arena_fd, &info, &info_len);
+ arena_sz = (size_t)info.max_entries * page_size;
+
+ /* Allocate 4 pages in the arena via BPF */
+ ret = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.arena_alloc),
+ &opts);
+ if (!ASSERT_OK(ret, "alloc_run") ||
+ !ASSERT_OK(opts.retval, "alloc_ret"))
+ goto out;
+
+ /* Fault in a page so zap_pages has work to do */
+ ((volatile char *)arena_addr)[0] = 'A';
+
+ /* Fork: child inherits the arena VMA */
+ pid = fork();
+ if (!ASSERT_GE(pid, 0, "fork"))
+ goto out;
+
+ if (pid == 0) {
+ /* Child: parent will unmap first, then we free pages. */
+ LIBBPF_OPTS(bpf_test_run_opts, child_opts);
+ int free_fd = bpf_program__fd(skel->progs.arena_free);
+
+ usleep(200000); /* let parent munmap first */
+
+ ret = bpf_prog_test_run_opts(free_fd, &child_opts);
+ _exit(ret || child_opts.retval);
+ }
+
+ /* Parent: unmap the arena */
+ munmap(arena_addr, arena_sz);
+
+ waitpid(pid, &status, 0);
+ ASSERT_TRUE(WIFEXITED(status), "child_exited");
+ ASSERT_EQ(WEXITSTATUS(status), 0, "child_exit_code");
+out:
+ arena_fork__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/progs/arena_fork.c b/tools/testing/selftests/bpf/progs/arena_fork.c
new file mode 100644
index 000000000000..783c935a0af8
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/arena_fork.c
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <linux/bpf.h>
+#include <bpf/bpf_helpers.h>
+#include "bpf_arena_common.h"
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ __uint(max_entries, 16); /* number of pages */
+#ifdef __TARGET_ARCH_arm64
+ __ulong(map_extra, 0x1ull << 32); /* start of mmap() region */
+#else
+ __ulong(map_extra, 0x1ull << 44); /* start of mmap() region */
+#endif
+} arena SEC(".maps");
+
+void __arena *alloc_addr;
+
+SEC("syscall")
+int arena_alloc(void *ctx)
+{
+ void __arena *p;
+
+ p = bpf_arena_alloc_pages(&arena, NULL, 4, NUMA_NO_NODE, 0);
+ if (!p)
+ return 1;
+ alloc_addr = p;
+ return 0;
+}
+
+SEC("syscall")
+int arena_free(void *ctx)
+{
+ if (!alloc_addr)
+ return 1;
+ bpf_arena_free_pages(&arena, alloc_addr, 4);
+ return 0;
+}
+
+char _license[] SEC("license") = "GPL";
--
2.43.0