[PATCH bpf-next v1 3/3] selftests/bpf: Test PROBE_MEM loads from invalid addresses
From: Kumar Kartikeya Dwivedi
Date: Thu Oct 08 2026 - 22:50:53 EST
Add a test that performs PROBE_MEM loads of three sizes through a
bpf_core_cast() pointer whose value is chosen by userspace: NULL, a low
user address, the last user page, a non-canonical address and, on x86-64,
the vsyscall page and an offset into it. Each load must read zero and the
kernel must survive. A load through the current task pointer checks that
the same loads read real values when the address is valid.
The test passes on kernels that reject the addresses with the JIT's range
check and on kernels that rely on the fault handler; with only the JIT
change of the previous patch applied, the first subtest oopses, which is
what the fault handler change prevents.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@xxxxxxxxx>
---
.../bpf/prog_tests/probe_mem_fault.c | 69 +++++++++++++++++++
.../selftests/bpf/progs/probe_mem_fault.c | 41 +++++++++++
2 files changed, 110 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/probe_mem_fault.c
create mode 100644 tools/testing/selftests/bpf/progs/probe_mem_fault.c
diff --git a/tools/testing/selftests/bpf/prog_tests/probe_mem_fault.c b/tools/testing/selftests/bpf/prog_tests/probe_mem_fault.c
new file mode 100644
index 000000000000..57a313e35eb6
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/probe_mem_fault.c
@@ -0,0 +1,69 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <test_progs.h>
+#include "probe_mem_fault.skel.h"
+
+#if defined(__x86_64__)
+#include <asm/vsyscall.h>
+#endif
+
+/*
+ * Addresses a PROBE_MEM load has to survive. Either the JIT's address check
+ * or the fault handler must turn each load into a zero result.
+ */
+static const struct {
+ const char *name;
+ unsigned long addr;
+} bad_addrs[] = {
+ { "null", 0 },
+ { "low_user", 4096 },
+ { "last_user_page", (1UL << 47) - 4096 },
+ { "non_canonical", 1UL << 63 },
+#if defined(__x86_64__)
+ { "vsyscall", VSYSCALL_ADDR },
+ { "vsyscall_tail", VSYSCALL_ADDR + 0x800 },
+#endif
+};
+
+static void trigger(struct probe_mem_fault *skel, int *runs)
+{
+ skel->bss->val_dw = ~0ULL;
+ skel->bss->val_w = ~0U;
+ skel->bss->val_b = ~0;
+ usleep(1);
+ ASSERT_EQ(skel->bss->runs, ++*runs, "runs");
+}
+
+void test_probe_mem_fault(void)
+{
+ struct probe_mem_fault *skel;
+ int runs = 0, i;
+
+ skel = probe_mem_fault__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "open_and_load"))
+ return;
+
+ skel->bss->target_pid = getpid();
+ if (!ASSERT_OK(probe_mem_fault__attach(skel), "attach"))
+ goto out;
+
+ /* A valid kernel address is read for real. */
+ skel->bss->use_current_task = true;
+ trigger(skel, &runs);
+ ASSERT_EQ(skel->bss->val_w, getpid(), "pid");
+ ASSERT_NEQ(skel->bss->val_dw, 0, "start_time");
+ ASSERT_NEQ(skel->bss->val_b, 0, "comm");
+
+ skel->bss->use_current_task = false;
+ for (i = 0; i < ARRAY_SIZE(bad_addrs); i++) {
+ if (!test__start_subtest(bad_addrs[i].name))
+ continue;
+ skel->bss->addr = bad_addrs[i].addr;
+ trigger(skel, &runs);
+ ASSERT_EQ(skel->bss->val_dw, 0, "start_time");
+ ASSERT_EQ(skel->bss->val_w, 0, "pid");
+ ASSERT_EQ(skel->bss->val_b, 0, "comm");
+ }
+out:
+ probe_mem_fault__destroy(skel);
+}
diff --git a/tools/testing/selftests/bpf/progs/probe_mem_fault.c b/tools/testing/selftests/bpf/progs/probe_mem_fault.c
new file mode 100644
index 000000000000..748be45418f5
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/probe_mem_fault.c
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include <bpf/bpf_core_read.h>
+#include "bpf_misc.h"
+
+char _license[] SEC("license") = "GPL";
+
+int target_pid;
+bool use_current_task;
+unsigned long addr;
+int runs;
+__u64 val_dw;
+__u32 val_w;
+__u8 val_b;
+
+SEC("fentry/" SYS_PREFIX "sys_nanosleep")
+int probe_mem_fault(void *ctx)
+{
+ struct task_struct *task;
+ unsigned long p = addr;
+
+ if ((bpf_get_current_pid_tgid() >> 32) != target_pid)
+ return 0;
+
+ if (use_current_task)
+ p = (unsigned long)bpf_get_current_task_btf();
+ /*
+ * bpf_core_cast() yields an untrusted pointer, so every load through it
+ * is a PROBE_MEM load. Whatever the address is, the load must either
+ * read the field or produce zero; the kernel must not oops.
+ */
+ task = bpf_core_cast((void *)p, struct task_struct);
+ val_dw = task->start_time;
+ val_w = task->pid;
+ val_b = task->comm[0];
+ runs++;
+ return 0;
+}
--
2.53.0-Meta