BPF List
 help / color / mirror / Atom feed
* [PATCH 1/2] bpf: arena: fix use-after-free in VMA tracking on fork
@ 2026-05-13 19:13 Daniel Hodges
  2026-05-13 19:13 ` [PATCH 2/2] selftests/bpf: Add test validating arena VMA tracking across fork Daniel Hodges
  0 siblings, 1 reply; 2+ 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

arena_vm_open() only increments a refcount on the existing vma_list
entry without creating a new entry for the child's VMA. After fork,
vml->vma still points to the parent's VMA. When the parent unmaps
(arena_vm_close decrements refcount but doesn't remove the entry),
vml->vma becomes a dangling pointer. A subsequent bpf_arena_free_pages
call reaches zap_pages() which dereferences the freed VMA via
zap_vma_range(vml->vma, ...), causing a use-after-free:

  BUG: KASAN: slab-use-after-free in zap_vma_range+0xf2/0x100
  Read of size 8 at addr ff11000113ec9b10 by task test_progs/198
  Call Trace:
   zap_vma_range+0xf2/0x100
   arena_free_pages+0x6de/0x970
   bpf_prog_a2b540a82b1066f3_arena_free+0x8b/0xb6
   bpf_prog_test_run_syscall+0x3d3/0x8a0

The same issue is triggered by __split_vma (partial munmap) and
copy_vma (mremap), both of which call vm_ops->open.

Fix this by giving each VMA its own vma_list entry instead of sharing
one with a refcount. arena_vm_open now allocates a new entry for the
new VMA, and arena_vm_close always removes and frees its own entry.
If the allocation fails in arena_vm_open, vm_private_data is set to
NULL and arena_vm_close handles this gracefully, meaning the VMA
simply won't be zapped during arena page frees.

Fixes: 317460317a02 ("bpf: Introduce bpf_arena.")
Signed-off-by: Daniel Hodges <git@danielhodges.dev>
Assisted-by: Claude-Code:claude-opus-4-6
---
 kernel/bpf/arena.c | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/kernel/bpf/arena.c b/kernel/bpf/arena.c
index 49a8f7b1beef..a3c46100dd12 100644
--- a/kernel/bpf/arena.c
+++ b/kernel/bpf/arena.c
@@ -317,7 +317,6 @@ static u64 arena_map_mem_usage(const struct bpf_map *map)
 struct vma_list {
 	struct vm_area_struct *vma;
 	struct list_head head;
-	refcount_t mmap_count;
 };
 
 static int remember_vma(struct bpf_arena *arena, struct vm_area_struct *vma)
@@ -327,7 +326,6 @@ static int remember_vma(struct bpf_arena *arena, struct vm_area_struct *vma)
 	vml = kmalloc_obj(*vml);
 	if (!vml)
 		return -ENOMEM;
-	refcount_set(&vml->mmap_count, 1);
 	vma->vm_private_data = vml;
 	vml->vma = vma;
 	list_add(&vml->head, &arena->vma_list);
@@ -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;
+	}
+	vml->vma = vma;
+	vma->vm_private_data = vml;
+	guard(mutex)(&arena->lock);
+	list_add(&vml->head, &arena->vma_list);
 }
 
 static int arena_vm_may_split(struct vm_area_struct *vma, unsigned long addr)
@@ -357,10 +365,9 @@ static void arena_vm_close(struct vm_area_struct *vma)
 	struct bpf_arena *arena = container_of(map, struct bpf_arena, map);
 	struct vma_list *vml = vma->vm_private_data;
 
-	if (!refcount_dec_and_test(&vml->mmap_count))
+	if (!vml)
 		return;
 	guard(mutex)(&arena->lock);
-	/* update link list under lock */
 	list_del(&vml->head);
 	vma->vm_private_data = NULL;
 	kfree(vml);
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 2+ messages in thread

* [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
  0 siblings, 0 replies; 2+ 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] 2+ messages in thread

end of thread, other threads:[~2026-05-13 19:16 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox