* [PATCH 2/2] selftests/bpf: Add test validating arena VMA tracking across fork
2026-05-13 19:13 [PATCH 1/2] bpf: arena: fix use-after-free in VMA tracking on fork Daniel Hodges
@ 2026-05-13 19:13 ` Daniel Hodges
2026-05-14 11:57 ` sashiko-bot
2026-05-14 11:21 ` [PATCH 1/2] bpf: arena: fix use-after-free in VMA tracking on fork sashiko-bot
1 sibling, 1 reply; 4+ messages in thread
From: Daniel Hodges @ 2026-05-13 19:13 UTC (permalink / raw)
To: bpf
Cc: linux-kselftest, linux-kernel, ast, daniel, andrii, martin.lau,
eddyz87, memxor, song, yonghong.song, jolsa, shuah, git, brho,
hodgesd
Validate that arena page frees don't trigger a use-after-free when a
forked child holds an inherited arena mmap. The test allocates arena
pages, forks, munmaps the arena in the parent, then frees the arena
pages via BPF. With KASAN enabled, any stale VMA dereference in
zap_pages() will be caught.
Signed-off-by: Daniel Hodges <git@danielhodges.dev>
Assisted-by: Claude-Code:claude-opus-4-6
---
.../bpf/prog_tests/arena_fork_free.c | 114 ++++++++++++++++++
.../selftests/bpf/progs/arena_fork_free.c | 71 +++++++++++
2 files changed, 185 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/arena_fork_free.c
create mode 100644 tools/testing/selftests/bpf/progs/arena_fork_free.c
diff --git a/tools/testing/selftests/bpf/prog_tests/arena_fork_free.c b/tools/testing/selftests/bpf/prog_tests/arena_fork_free.c
new file mode 100644
index 000000000000..6a6e1d4197cf
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/arena_fork_free.c
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+/*
+ * Validate arena VMA tracking across fork.
+ *
+ * When a process with an arena mmap forks, each VMA must be independently
+ * tracked. This test verifies that freeing arena pages after the parent
+ * munmaps does not access stale VMA pointers.
+ *
+ * Sequence:
+ * 1. Load arena, allocate pages via BPF
+ * 2. fork() — child inherits arena mmap
+ * 3. Parent: munmap the arena
+ * 4. Parent: free arena pages via BPF — zap_pages() must only touch
+ * live VMAs
+ *
+ * With KASAN enabled, any stale VMA access will be caught.
+ */
+#include <test_progs.h>
+#include <sys/wait.h>
+#include <sys/mman.h>
+#include <unistd.h>
+#include <sys/user.h>
+#ifndef PAGE_SIZE
+#define PAGE_SIZE getpagesize()
+#endif
+#include "arena_fork_free.skel.h"
+
+void test_arena_fork_free(void)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+ struct arena_fork_free *skel;
+ int pipe_fds[2] = {-1, -1};
+ size_t arena_sz;
+ void *arena_area;
+ pid_t child;
+ int ret, status;
+ char buf;
+
+ skel = arena_fork_free__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "open_and_load"))
+ return;
+
+ /* Step 1: allocate arena pages via BPF */
+ ret = bpf_prog_test_run_opts(
+ bpf_program__fd(skel->progs.arena_alloc), &opts);
+ if (!ASSERT_OK(ret, "alloc_run"))
+ goto out;
+ if (!ASSERT_OK(opts.retval, "alloc_retval"))
+ goto out;
+ if (skel->bss->skip) {
+ printf("%s:SKIP:compiler doesn't support arena_cast\n",
+ __func__);
+ test__skip();
+ goto out;
+ }
+
+ arena_area = bpf_map__initial_value(skel->maps.arena, &arena_sz);
+ if (!ASSERT_OK_PTR(arena_area, "arena_area"))
+ goto out;
+ arena_sz = bpf_map__max_entries(skel->maps.arena) * PAGE_SIZE;
+
+ if (!ASSERT_OK(pipe(pipe_fds), "pipe"))
+ goto out;
+
+ /* Step 2: fork — child inherits arena mmap */
+ child = fork();
+ if (!ASSERT_GE(child, 0, "fork")) {
+ close(pipe_fds[0]);
+ close(pipe_fds[1]);
+ goto out;
+ }
+
+ if (child == 0) {
+ /* Child: keep arena mmap alive, wait for parent to signal */
+ close(pipe_fds[1]);
+ read(pipe_fds[0], &buf, 1);
+ close(pipe_fds[0]);
+ _exit(0);
+ }
+
+ /* Parent continues */
+ close(pipe_fds[0]);
+ pipe_fds[0] = -1;
+
+ /* Step 3: munmap the arena in the parent */
+ ret = munmap(arena_area, arena_sz);
+ if (!ASSERT_OK(ret, "munmap"))
+ goto signal_child;
+
+ /*
+ * Step 4: free arena pages via BPF.
+ *
+ * Wait for the RCU grace period so the parent's VMA slab memory
+ * is actually freed (VMA freeing is deferred via call_rcu).
+ * This ensures KASAN can detect any stale VMA dereference in
+ * zap_pages().
+ */
+ usleep(200000);
+ opts.retval = 0;
+ ret = bpf_prog_test_run_opts(
+ bpf_program__fd(skel->progs.arena_free), &opts);
+ ASSERT_OK(ret, "free_run");
+ ASSERT_OK(opts.retval, "free_retval");
+
+signal_child:
+ close(pipe_fds[1]);
+ pipe_fds[1] = -1;
+ waitpid(child, &status, 0);
+ ASSERT_TRUE(WIFEXITED(status), "child_exited");
+
+out:
+ arena_fork_free__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/progs/arena_fork_free.c b/tools/testing/selftests/bpf/progs/arena_fork_free.c
new file mode 100644
index 000000000000..81b4f9a4e94b
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/arena_fork_free.c
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+/*
+ * Validate arena VMA tracking across fork.
+ *
+ * Provides BPF programs to allocate and free arena pages, exercised by
+ * the userspace test to verify that zap_pages() correctly handles VMA
+ * lifecycle when a forked child holds an inherited arena mmap.
+ */
+#include "vmlinux.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, 10); /* 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");
+
+bool skip = false;
+
+#ifdef __BPF_FEATURE_ADDR_SPACE_CAST
+
+void __arena *alloc_ptr;
+int alloc_page_cnt;
+
+SEC("syscall")
+int arena_alloc(void *ctx)
+{
+ alloc_ptr = bpf_arena_alloc_pages(&arena, NULL, 2, NUMA_NO_NODE, 0);
+ if (!alloc_ptr)
+ return 1;
+ alloc_page_cnt = 2;
+ return 0;
+}
+
+SEC("syscall")
+int arena_free(void *ctx)
+{
+ if (!alloc_ptr || !alloc_page_cnt)
+ return 1;
+ bpf_arena_free_pages(&arena, alloc_ptr, alloc_page_cnt);
+ alloc_ptr = NULL;
+ alloc_page_cnt = 0;
+ return 0;
+}
+
+#else
+
+SEC("syscall")
+int arena_alloc(void *ctx)
+{
+ skip = true;
+ return 0;
+}
+
+SEC("syscall")
+int arena_free(void *ctx)
+{
+ skip = true;
+ return 0;
+}
+
+#endif
+
+char _license[] SEC("license") = "GPL";
--
2.52.0
^ permalink raw reply related [flat|nested] 4+ messages in thread* Re: [PATCH 1/2] bpf: arena: fix use-after-free in VMA tracking on fork
2026-05-13 19:13 [PATCH 1/2] bpf: arena: fix use-after-free in VMA tracking on fork Daniel Hodges
2026-05-13 19:13 ` [PATCH 2/2] selftests/bpf: Add test validating arena VMA tracking across fork Daniel Hodges
@ 2026-05-14 11:21 ` sashiko-bot
1 sibling, 0 replies; 4+ messages in thread
From: sashiko-bot @ 2026-05-14 11:21 UTC (permalink / raw)
To: Daniel Hodges; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] zap_pages() accesses user page tables locklessly, causing page table Use-After-Free during concurrent VMA teardown.
- [High] Silent allocation failure in arena_vm_open() leads to memory leak and physical page desynchronization.
--
commit 305653246e6ae24222d49bb8aa21f98a5bc22903
Author: Daniel Hodges <git@danielhodges.dev>
bpf: arena: fix use-after-free in VMA tracking on fork
This commit addresses a use-after-free by giving each vma its own vma_list
entry in arena_vm_open, rather than sharing a refcounted entry. It also
ensures arena_vm_close removes and frees the respective entry.
> diff --git a/kernel/bpf/arena.c b/kernel/bpf/arena.c
> --- a/kernel/bpf/arena.c
> +++ b/kernel/bpf/arena.c
[ ... ]
> @@ -336,9 +334,19 @@ static int remember_vma(struct bpf_arena *arena, struct vm_area_struct *vma)
>
> static void arena_vm_open(struct vm_area_struct *vma)
> {
> - struct vma_list *vml = vma->vm_private_data;
> + struct bpf_map *map = vma->vm_file->private_data;
> + struct bpf_arena *arena = container_of(map, struct bpf_arena, map);
> + struct vma_list *vml;
>
> - refcount_inc(&vml->mmap_count);
> + vml = kmalloc_obj(*vml);
> + if (!vml) {
> + vma->vm_private_data = NULL;
> + return;
> + }
Because vm_ops->open has a void return type, returning early here does not
abort the vma creation. The child vma will successfully map the arena memory
but remain omitted from arena->vma_list.
Later, when bpf_arena_free_pages() calls zap_pages(), it iterates only over
arena->vma_list. Does this omission cause the child process to retain valid
user-space ptes pointing to the old physical pages?
If so, this could prevent the physical pages from returning to the buddy
allocator, leaking the memory. It may also break shared memory coherency if
new physical pages are reallocated at the same virtual offset while the
child still accesses the old pages.
> + vml->vma = vma;
> + vma->vm_private_data = vml;
> + guard(mutex)(&arena->lock);
> + list_add(&vml->head, &arena->vma_list);
> }
This is an existing issue not introduced by this patch, but zap_pages()
iterates over arena->vma_list and calls zap_vma_range() holding only
arena->lock.
The memory management subsystem requires mmap_lock or the per-vma lock to
be held for vma->vm_mm when traversing a vma's page tables. Concurrently,
processes may exit or unmap the arena. In the teardown sequence,
free_pgtables() unlinks and frees the pud, pmd, and pte pages before
remove_vma() invokes vma->vm_ops->close.
Could zap_pages() execute during this teardown window and attempt to walk
page tables that have already been freed, leading to a use-after-free on
the page directory and table pages themselves?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260513191322.21319-1-git@danielhodges.dev?part=1
^ permalink raw reply [flat|nested] 4+ messages in thread