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

From: Emil Tsalapatis

Date: Sat Apr 11 2026 - 13:33:54 EST


On Sat Apr 11, 2026 at 7:29 AM EDT, Weiming Shi wrote:
> 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.
>
> Signed-off-by: Weiming Shi <bestswngs@xxxxxxxxx>
> ---
> .../selftests/bpf/prog_tests/arena_fork.c | 86 +++++++++++++++++++
> .../testing/selftests/bpf/progs/arena_fork.c | 41 +++++++++
> 2 files changed, 127 insertions(+)
> create mode 100644 tools/testing/selftests/bpf/prog_tests/arena_fork.c
> create mode 100644 tools/testing/selftests/bpf/progs/arena_fork.c
>

The test doesn't work for me as advertised. Does it fail for you under
vmtest without patch 1/2?

The test doesn't fail on base vmtest for me, even without the previous patch,
because KASAN isn't turned on for the CI. With KASAN the test triggers
the splat just fine.

Should we maybe turn on KASAN and panic_on_warn by default on vmtest?

> 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..445574827891
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/arena_fork.c
> @@ -0,0 +1,86 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Copyright (c) 2026 */
> +

No actual copyright holder.

> +/*
> + * 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.
> + *
> + * The bug: arena_vm_open() only incremented a refcount but never registered
> + * the child's VMA. After parent munmap, vml->vma pointed to a freed
> + * vm_area_struct. bpf_arena_free_pages -> zap_pages would then UAF.

Remove this, there is no point keeping information about past bugs in
the tests. The first sentence is enough.

> + */
> +#include <test_progs.h>
> +#include <sys/mman.h>
> +#include <sys/wait.h>
> +#include <unistd.h>
> +#include <sys/user.h>
> +#ifndef PAGE_SIZE
> +#define PAGE_SIZE getpagesize()
> +#endif

Use sysconf to get the page size.

> +
> +#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;
> + size_t arena_sz;
> + void *arena_addr;
> + int arena_fd, ret, status;
> + pid_t pid;
> +
> + 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 */
> + ((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.
> + * Without the fix, this triggers UAF in zap_pages.

Again, "the fix" isn't clear. Remove the second sentence.

> + */
> + LIBBPF_OPTS(bpf_test_run_opts, child_opts);
> + int free_fd = bpf_program__fd(skel->progs.arena_free);
> +
> + usleep(200000); /* let parent munmap first */

This is dependable on vmtest, and we do use usleep in other tests
so I'd say it's a valid use.

> +
> + ret = bpf_prog_test_run_opts(free_fd, &child_opts);
> + _exit(ret || child_opts.retval);
> + }
> +
> + /* Parent: unmap the arena, making vml->vma stale */
> + munmap(arena_addr, arena_sz);
> +
> + /* Wait for child -- if kernel UAFs, child will crash/hang */

That's not the case. The test exits with a success for me.

> + 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..b1f8435f1834
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/arena_fork.c
> @@ -0,0 +1,41 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Copyright (c) 2026 */

There's no copyright holder here, add one or remove the line it.

> +#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";