* [PATCH bpf-next v2 00/13] Fix some arena paper cuts
@ 2026-10-13 8:19 Kumar Kartikeya Dwivedi
2026-10-13 8:19 ` [PATCH bpf-next v2 01/13] libbpf: Keep the fd_array fill index out of bpf_program__clone() Kumar Kartikeya Dwivedi
` (12 more replies)
0 siblings, 13 replies; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:19 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai, Tejun Heo, kkd,
kernel-team
Kernel code that shares arena memory with BPF programs runs into two
paper cuts, both visible in sched_ext's cid-form schedulers today.
A kernel struct cannot publish an arena pointer: a pointer member reaches
the program typed as a kernel pointer, so sched_ext hands out a plain u64
holding a pre-translated arena address and every program casts it before
use. __arena and __nullable become BTF type tags like __user and __rcu.
A load through an __arena member of a kernel struct yields PTR_TO_ARENA:
the member holds the kernel address and the fixup pass appends the
conversion to the arena pointer form that the struct_ops trampoline
applies to __arena arguments, with a NULL test for __nullable members.
A __nullable member on its own keeps PTR_MAYBE_NULL on trusted and RCU
pointers, where NULL would otherwise be assumed away. libbpf renders the
arena tag with its address space, so the member arrives in a program
built against vmlinux.h ready to use, without a cast.
A program only has an arena when one of its insns refers to the arena
map, yet the kernel hands programs arena pointers through struct_ops
arguments, tagged members and kfunc return values, and a struct_ops
callback that touches nothing but its arguments fails verification until
it refers to the arena artificially. libbpf binds the arena of an object
to every program loaded from it, through a continuous fd_array. So that
programs which never touch the arena pay nothing for it, the verifier
records which functions have insns that work with the arena base and the
JITs set it up only for those.
Tested with sched_ext for-next as well: a typed cmask member next to the
existing cmask_arena_addr in struct scx_enable_args passes the
enable_cmask selftest read straight from the member, without a cast and
without the asm volatile arena reference.
The series opens with a fix the review of v1 led to: bpf_program__clone()
passed the fd_array fill index of the object as fd_array_cnt, which made
every clone from an object with module kfuncs fail.
See commit logs for details.
Changelog:
----------
v1 -> v2
v1: https://lore.kernel.org/bpf/20261012133711.4030594-1-memxor@gmail.com
* Leave the flags of members walked from a map pointer alone: the
review found that an __rcu member comes out of that path as MEM_RCU,
neither nullable nor probed, which is an older problem of the path
and of the bare PTR_TO_BTF_ID results it accepts, left to a separate
series after a wider audit. (Sashiko)
* Record the non-arena side of a shared load insn for every load, not
only for loads through a kernel struct pointer, since a load insn may
also serve a memory pointer on another path; test that case. (Sashiko)
* Return early in assert_prog_map() when the program lists no map, so a
failing check does not read uninitialized memory. (Sashiko)
* Mark the __arena argument of a kfunc call where the call is verified,
the kfunc pre-pass marked subprogram 0 whatever subprogram held the
call, and test a subprogram whose only arena use is such a call.
(BPF CI)
* Bind the arena only when libbpf creates it, so a program whose arena
has autocreate off still loads; leave out an arena exclusive to
another program and offloaded programs; reset fd_array_cnt after the
load; bind it from bpf_program__clone() as well, so veristat loads
what bpf_object__load() loads. (BPF CI)
* Stop bpf_program__clone() from passing the fd_array fill index of the
object as fd_array_cnt, and rename the index so that it no longer
reads like the uapi field, as a fix patch with a selftest: slot 0
holds no fd, so every clone from an object with module kfuncs failed.
* Skip the __arena and __nullable member tests when the module BTF
carries no type tags, as with a GCC build. (BPF CI)
* Initialize map_ids in the autobind test for GCC's
-Wmaybe-uninitialized. (BPF CI)
* Note in the libbpf commit log that every program of an object with an
arena now needs the JIT and CAP_PERFMON.
* Fix the description of the store failure test. (BPF CI)
Kumar Kartikeya Dwivedi (13):
libbpf: Keep the fd_array fill index out of bpf_program__clone()
selftests/bpf: Clone a program that calls a module kfunc
compiler_types: Add __arena and __nullable BTF type tag qualifiers
bpf: Recognize __nullable on pointer members of kernel structs
bpf: Turn loads of __arena pointer members into arena pointers
selftests/bpf: Test __nullable pointer members
selftests/bpf: Test __arena pointer members
libbpf: Bind the arena of an object to every program in it
selftests/bpf: Test arena binding by libbpf
bpf: Set up the arena base only in functions that use it
libbpf: Render the arena type tag with its address space
selftests/bpf: Test the address space rendering of the arena type tag
selftests/bpf: Test an arena kfunc argument passed from a subprogram
arch/arm64/net/bpf_jit_comp.c | 6 +-
arch/loongarch/net/bpf_jit.c | 6 +-
arch/powerpc/net/bpf_jit_comp.c | 6 +-
arch/riscv/net/bpf_jit_core.c | 6 +-
arch/s390/net/bpf_jit_comp.c | 7 +-
arch/x86/net/bpf_jit_comp.c | 14 +-
include/linux/bpf.h | 2 +
include/linux/bpf_verifier.h | 10 ++
include/linux/compiler_types.h | 10 ++
include/linux/filter.h | 11 ++
kernel/bpf/btf.c | 48 +++++-
kernel/bpf/fixups.c | 51 ++++++-
kernel/bpf/verifier.c | 75 ++++++++++
tools/lib/bpf/btf_dump.c | 3 +
tools/lib/bpf/features.c | 40 +++++
tools/lib/bpf/libbpf.c | 76 ++++++++--
tools/lib/bpf/libbpf_internal.h | 2 +
.../selftests/bpf/prog_tests/btf_dump.c | 16 +-
.../bpf/prog_tests/clone_module_kfunc.c | 30 ++++
.../bpf/prog_tests/test_struct_ops_arena.c | 139 ++++++++++++++++++
.../bpf/prog_tests/test_struct_ops_nullable.c | 68 +++++++++
.../selftests/bpf/prog_tests/verifier.c | 2 +
.../testing/selftests/bpf/progs/arena_kfunc.c | 33 ++++-
.../selftests/bpf/progs/arena_noarena.c | 39 +++++
.../bpf/progs/struct_ops_arena_autobind.c | 64 ++++++++
.../bpf/progs/struct_ops_arena_member.c | 92 ++++++++++++
.../struct_ops_arena_member_fail__load_acq.c | 38 +++++
.../struct_ops_arena_member_fail__mismatch.c | 45 ++++++
...ruct_ops_arena_member_fail__mismatch_mem.c | 54 +++++++
.../struct_ops_arena_member_fail__no_arena.c | 23 +++
.../struct_ops_arena_member_fail__union.c | 34 +++++
.../struct_ops_arena_member_fail__write.c | 32 ++++
.../progs/struct_ops_arena_member_render.c | 82 +++++++++++
.../selftests/bpf/progs/struct_ops_nullable.c | 45 ++++++
.../bpf/progs/struct_ops_nullable_fail.c | 28 ++++
.../bpf/progs/verifier_arena_scalar.c | 13 --
.../selftests/bpf/test_kmods/bpf_testmod.c | 53 +++++++
.../selftests/bpf/test_kmods/bpf_testmod.h | 46 ++++++
.../bpf/test_kmods/bpf_testmod_kfunc.h | 2 +
39 files changed, 1300 insertions(+), 51 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/clone_module_kfunc.c
create mode 100644 tools/testing/selftests/bpf/prog_tests/test_struct_ops_nullable.c
create mode 100644 tools/testing/selftests/bpf/progs/arena_noarena.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_autobind.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__load_acq.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__no_arena.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__union.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__write.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_render.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_nullable.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_nullable_fail.c
base-commit: 0d5e3fc78a6f2522e6339743b8e5ecc02351a9dd
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 22+ messages in thread
* [PATCH bpf-next v2 01/13] libbpf: Keep the fd_array fill index out of bpf_program__clone()
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
@ 2026-10-13 8:19 ` Kumar Kartikeya Dwivedi
2026-10-13 13:00 ` Mykyta Yatsenko
2026-10-13 8:19 ` [PATCH bpf-next v2 02/13] selftests/bpf: Clone a program that calls a module kfunc Kumar Kartikeya Dwivedi
` (11 subsequent siblings)
12 siblings, 1 reply; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:19 UTC (permalink / raw)
To: bpf
Cc: Mykyta Yatsenko, Alexei Starovoitov, Andrii Nakryiko,
Daniel Borkmann, Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai,
Tejun Heo, kkd, kernel-team
bpf_program__clone() hands the kernel the fd_array of the object along
with obj->fd_array_cnt when the caller supplies neither. That counter is
not a count the kernel can use: it is the index of the next free slot
of an array built for the sparse fd_array API, where module BTF fds
start at slot 1 and slot 0 stays empty, since a kfunc insn with offset
0 names vmlinux BTF rather than a slot. A non-zero fd_array_cnt turns
the array into a continuous one, which the kernel resolves slot by
slot, and slot 0 holds 0. Cloning a program from an object that calls a
module kfunc thus fails with "fd 0 is not pointing to valid bpf_map or
btf", for every program of the object, which is what veristat runs into
on such an object. bpf_object__load() passes the array without a count.
Do the same in bpf_program__clone(), leaving the count to callers who
pass an array of their own, and rename the counter to fd_array_used so
that it no longer reads like the uapi field.
Fixes: 970bd2dced35 ("libbpf: Introduce bpf_program__clone()")
Cc: Mykyta Yatsenko <yatsenko@meta.com>
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
tools/lib/bpf/libbpf.c | 21 +++++++++++++--------
1 file changed, 13 insertions(+), 8 deletions(-)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index cb09ded90773..fcedb78abe00 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -794,7 +794,12 @@ struct bpf_object {
int *fd_array;
size_t fd_array_cap;
- size_t fd_array_cnt;
+ /*
+ * Slots handed out so far, slot 0 included. A kfunc insn names a module
+ * BTF slot through its offset, slot 0 stands for vmlinux BTF and stays
+ * empty, so the array goes to the kernel without a count.
+ */
+ size_t fd_array_used;
struct usdt_manager *usdt_man;
@@ -9716,22 +9721,22 @@ static int bpf_object__resolve_ksym_func_btf_id(struct bpf_object *obj,
/* set index for module BTF fd in fd_array, if unset */
if (mod_btf && !mod_btf->fd_array_idx) {
/* insn->off is s16 */
- if (obj->fd_array_cnt == INT16_MAX) {
+ if (obj->fd_array_used == INT16_MAX) {
pr_warn("extern (func ksym) '%s': module BTF fd index %d too big to fit in bpf_insn offset\n",
ext->name, mod_btf->fd_array_idx);
return -E2BIG;
}
/* Cannot use index 0 for module BTF fd */
- if (!obj->fd_array_cnt)
- obj->fd_array_cnt = 1;
+ if (!obj->fd_array_used)
+ obj->fd_array_used = 1;
ret = libbpf_ensure_mem((void **)&obj->fd_array, &obj->fd_array_cap, sizeof(int),
- obj->fd_array_cnt + 1);
+ obj->fd_array_used + 1);
if (ret)
return ret;
- mod_btf->fd_array_idx = obj->fd_array_cnt;
+ mod_btf->fd_array_idx = obj->fd_array_used;
/* we assume module BTF FD is always >0 */
- obj->fd_array[obj->fd_array_cnt++] = mod_btf->fd;
+ obj->fd_array[obj->fd_array_used++] = mod_btf->fd;
}
ext->is_set = true;
@@ -10940,7 +10945,7 @@ int bpf_program__clone(struct bpf_program *prog, const struct bpf_prog_load_opts
attr.prog_ifindex = OPTS_GET(opts, prog_ifindex, 0) ?: prog->prog_ifindex;
attr.kern_version = OPTS_GET(opts, kern_version, 0) ?: obj->kern_version;
attr.fd_array = OPTS_GET(opts, fd_array, NULL) ?: obj->fd_array;
- attr.fd_array_cnt = OPTS_GET(opts, fd_array_cnt, 0) ?: obj->fd_array_cnt;
+ attr.fd_array_cnt = OPTS_GET(opts, fd_array_cnt, 0);
attr.token_fd = OPTS_GET(opts, token_fd, 0) ?: obj->token_fd;
if (attr.token_fd)
attr.prog_flags |= BPF_F_TOKEN_FD;
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH bpf-next v2 02/13] selftests/bpf: Clone a program that calls a module kfunc
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
2026-10-13 8:19 ` [PATCH bpf-next v2 01/13] libbpf: Keep the fd_array fill index out of bpf_program__clone() Kumar Kartikeya Dwivedi
@ 2026-10-13 8:19 ` Kumar Kartikeya Dwivedi
2026-10-13 8:19 ` [PATCH bpf-next v2 03/13] compiler_types: Add __arena and __nullable BTF type tag qualifiers Kumar Kartikeya Dwivedi
` (10 subsequent siblings)
12 siblings, 0 replies; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:19 UTC (permalink / raw)
To: bpf
Cc: Mykyta Yatsenko, Alexei Starovoitov, Andrii Nakryiko,
Daniel Borkmann, Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai,
Tejun Heo, kkd, kernel-team
Prepare an object whose program calls a kfunc of bpf_testmod and load
the program through bpf_program__clone(), the way veristat loads
programs. The module BTF fd sits in slot 1 of the fd_array of the
object and slot 0 is empty; the clone used to pass the fill index of
that array as fd_array_cnt, which made the kernel resolve slot 0 and
fail the load with "fd 0 is not pointing to valid bpf_map or btf".
Cc: Mykyta Yatsenko <yatsenko@meta.com>
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
.../bpf/prog_tests/clone_module_kfunc.c | 30 +++++++++++++++++++
1 file changed, 30 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/clone_module_kfunc.c
diff --git a/tools/testing/selftests/bpf/prog_tests/clone_module_kfunc.c b/tools/testing/selftests/bpf/prog_tests/clone_module_kfunc.c
new file mode 100644
index 000000000000..fa6ccbaa7fd9
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/clone_module_kfunc.c
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <test_progs.h>
+#include "kfunc_call_test.skel.h"
+
+/*
+ * bpf_program__clone() loads a program from a prepared object with the
+ * fd_array of the object, where module BTF fds start at slot 1 and slot 0 is
+ * empty. It used to pass the fill index of that array as fd_array_cnt, which
+ * made the kernel resolve slot 0 and fail the load.
+ */
+void test_clone_module_kfunc(void)
+{
+ struct kfunc_call_test *skel;
+ int err, fd;
+
+ skel = kfunc_call_test__open();
+ if (!ASSERT_OK_PTR(skel, "skel_open"))
+ return;
+
+ err = bpf_object__prepare(skel->obj);
+ if (!ASSERT_OK(err, "prepare"))
+ goto out;
+
+ fd = bpf_program__clone(skel->progs.kfunc_call_test1, NULL);
+ if (ASSERT_GE(fd, 0, "clone"))
+ close(fd);
+out:
+ kfunc_call_test__destroy(skel);
+}
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH bpf-next v2 03/13] compiler_types: Add __arena and __nullable BTF type tag qualifiers
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
2026-10-13 8:19 ` [PATCH bpf-next v2 01/13] libbpf: Keep the fd_array fill index out of bpf_program__clone() Kumar Kartikeya Dwivedi
2026-10-13 8:19 ` [PATCH bpf-next v2 02/13] selftests/bpf: Clone a program that calls a module kfunc Kumar Kartikeya Dwivedi
@ 2026-10-13 8:19 ` Kumar Kartikeya Dwivedi
2026-10-13 8:19 ` [PATCH bpf-next v2 04/13] bpf: Recognize __nullable on pointer members of kernel structs Kumar Kartikeya Dwivedi
` (9 subsequent siblings)
12 siblings, 0 replies; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:19 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai, Tejun Heo, kkd,
kernel-team
Kernel structures increasingly want to publish pointers into a BPF arena
to the BPF programs they are shared with, and some pointer members that
reach programs as trusted are legitimately NULL at times. Neither can be
expressed today: a pointer member reaches the program as an ordinary
kernel pointer, and sched_ext resorts to a plain u64 holding a
pre-translated arena address that programs cast before use.
Define __arena and __nullable as BTF type tags, the way __user, __rcu
and __percpu already are, so that tagged members survive into vmlinux
and module BTF and the verifier can act on them. To kernel code and to
sparse the qualifiers are empty: the pointers are ordinary kernel
pointers. The verifier side follows in the next patches.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
include/linux/compiler_types.h | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/include/linux/compiler_types.h b/include/linux/compiler_types.h
index c5921f139007..e97ee2d4d1df 100644
--- a/include/linux/compiler_types.h
+++ b/include/linux/compiler_types.h
@@ -51,6 +51,8 @@
# define __iomem __attribute__((noderef, address_space(__iomem)))
# define __percpu __attribute__((noderef, address_space(__percpu)))
# define __rcu __attribute__((noderef, address_space(__rcu)))
+# define __arena
+# define __nullable
static inline void __chk_user_ptr(const volatile void __user *ptr) { }
static inline void __chk_io_ptr(const volatile void __iomem *ptr) { }
/* other */
@@ -70,6 +72,14 @@ static inline void __chk_io_ptr(const volatile void __iomem *ptr) { }
# define __iomem
# define __percpu __percpu_qual BTF_TYPE_TAG(percpu)
# define __rcu BTF_TYPE_TAG(rcu)
+/*
+ * Qualifiers for the BPF verifier rather than address spaces: __arena marks
+ * a pointer into a BPF arena, __nullable a pointer that may be NULL. They
+ * reach BPF programs as BTF type tags on kernel struct members; to kernel
+ * code the pointers are ordinary.
+ */
+# define __arena BTF_TYPE_TAG(arena)
+# define __nullable BTF_TYPE_TAG(nullable)
# define __chk_user_ptr(x) (void)0
# define __chk_io_ptr(x) (void)0
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH bpf-next v2 04/13] bpf: Recognize __nullable on pointer members of kernel structs
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
` (2 preceding siblings ...)
2026-10-13 8:19 ` [PATCH bpf-next v2 03/13] compiler_types: Add __arena and __nullable BTF type tag qualifiers Kumar Kartikeya Dwivedi
@ 2026-10-13 8:19 ` Kumar Kartikeya Dwivedi
2026-10-13 8:52 ` sashiko-bot
2026-10-13 8:20 ` [PATCH bpf-next v2 05/13] bpf: Turn loads of __arena pointer members into arena pointers Kumar Kartikeya Dwivedi
` (8 subsequent siblings)
12 siblings, 1 reply; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:19 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai, Tejun Heo, kkd,
kernel-team
A pointer member of a kernel struct that the verifier derives as trusted
or RCU-protected is assumed to be non-NULL, so a load through it is a
plain dereference. The BTF_TYPE_SAFE_*_OR_NULL lists exist for members
known to be NULL at times, but they live in the verifier and have to be
extended by hand.
Teach btf_struct_walk() to report the "nullable" BTF type tag on a
pointer member as PTR_MAYBE_NULL. The tag combines with any other tag
on the member; it only says that the pointer may be NULL. The verifier
keeps the flag where NULL would otherwise be assumed away, on trusted
and RCU results, so a program has to check such a pointer before
dereferencing it or passing it to a kfunc that needs a trusted
argument. Untrusted results are probed anyway and lose the flag.
The next patch uses the tag on __arena members to decide whether the
injected address translation has to preserve NULL.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
kernel/bpf/btf.c | 18 ++++++++++++++++--
kernel/bpf/verifier.c | 7 +++++++
2 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index b09c04ccdc6d..bcbd49c7ca0b 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -3557,7 +3557,8 @@ static int btf_type_tag_walk(const struct btf *btf,
return 0;
}
-bool btf_type_is_arena_ptr(const struct btf *btf, const struct btf_type *t)
+/* Whether the pointee of pointer type @t carries the type tag @name */
+static bool btf_ptr_has_type_tag(const struct btf *btf, const struct btf_type *t, const char *name)
{
if (!btf_type_is_ptr(t))
return false;
@@ -3566,13 +3567,18 @@ bool btf_type_is_arena_ptr(const struct btf *btf, const struct btf_type *t)
t = btf_type_by_id(btf, t->type)) {
if (!btf_type_is_type_tag(t) || btf_type_kflag(t))
continue;
- if (!strcmp(__btf_name_by_offset(btf, t->name_off), "arena"))
+ if (!strcmp(__btf_name_by_offset(btf, t->name_off), name))
return true;
}
return false;
}
+bool btf_type_is_arena_ptr(const struct btf *btf, const struct btf_type *t)
+{
+ return btf_ptr_has_type_tag(btf, t, "arena");
+}
+
static int btf_find_kptr(const struct btf *btf, const struct btf_type *t,
u32 off, int sz, struct btf_field_info *info, u32 field_mask)
{
@@ -7961,6 +7967,14 @@ static int btf_struct_walk(struct bpf_verifier_log *log, const struct btf *btf,
id = ctx.id;
stype = ctx.t;
+ /*
+ * __nullable combines with any of the above: it only says
+ * that the pointer may be NULL. The caller decides whether
+ * that matters for the register type it derives.
+ */
+ if (btf_ptr_has_type_tag(btf, mtype, "nullable"))
+ tmp_flag |= PTR_MAYBE_NULL;
+
if (btf_type_is_struct(stype)) {
*next_btf_id = id;
*flag |= tmp_flag;
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 2f190abe3448..a9c47441fba9 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -6721,6 +6721,13 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
clear_trusted_flags(&flag);
}
+ /*
+ * A __nullable member only matters where NULL would otherwise be
+ * assumed away, on trusted and RCU pointers; the rest are probed.
+ */
+ if (!(flag & (PTR_TRUSTED | MEM_RCU)))
+ flag &= ~PTR_MAYBE_NULL;
+
if (atype == BPF_READ && value_regno >= 0) {
ret = mark_btf_ld_reg(env, regs, value_regno, ret, reg->btf, btf_id, flag);
if (ret < 0)
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH bpf-next v2 05/13] bpf: Turn loads of __arena pointer members into arena pointers
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
` (3 preceding siblings ...)
2026-10-13 8:19 ` [PATCH bpf-next v2 04/13] bpf: Recognize __nullable on pointer members of kernel structs Kumar Kartikeya Dwivedi
@ 2026-10-13 8:20 ` Kumar Kartikeya Dwivedi
2026-10-13 8:40 ` sashiko-bot
2026-10-13 8:20 ` [PATCH bpf-next v2 06/13] selftests/bpf: Test __nullable pointer members Kumar Kartikeya Dwivedi
` (7 subsequent siblings)
12 siblings, 1 reply; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:20 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai, Tejun Heo, kkd,
kernel-team
A kernel struct shared with BPF programs may want to point at arena
memory, e.g. a sched_ext argument struct that the kernel builds in the
scheduler's arena and that refers to another arena object. A pointer
member reaches the program typed as a kernel pointer, so sched_ext
publishes a plain u64 holding a pre-translated arena address that the
program casts before use.
Translate in the verifier instead. When a load through a kernel struct
pointer lands on a member tagged __arena, btf_struct_walk() reports it
and the destination register becomes PTR_TO_ARENA. The member holds the
kernel address of the arena memory; the fixup pass appends the
conversion to the arena pointer form that the struct_ops trampoline
applies to __arena arguments:
r1 = *(u64 *)(r2 + off)
if r1 == 0 goto +1 // __arena __nullable members only
w1 -= lo32(kern_vm_start)
Stores to kernel structs are rejected as before, and so are atomic
loads of such a member, which the fixup pass does not rewrite. A member
inside a multi-member union is rejected too: an offset into a union
resolves to its first member, so a program reading another member there
would receive an arena pointer it did not ask for. Loads through
program-allocated objects and program BTF are unaffected, there the tag
is what the compiler emits for the arena address space.
The rewrite is per insn while verification is per path, so the aux data
records what an insn loaded on any path: an insn that loaded an arena
pointer member on one path and a different member on another is
rejected by the fixup pass, while paths disagreeing only on __nullable
keep the NULL test, which a never-NULL member can afford. As for
__arena arguments, the program needs an associated arena, which also
supplies the base to subtract.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
include/linux/bpf_verifier.h | 8 ++++++
kernel/bpf/btf.c | 30 ++++++++++++++++++++++
kernel/bpf/fixups.c | 50 +++++++++++++++++++++++++++++++++---
kernel/bpf/verifier.c | 50 ++++++++++++++++++++++++++++++++++++
4 files changed, 134 insertions(+), 4 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 54cb6d3d0653..eb9300221c4a 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -791,6 +791,14 @@ struct bpf_insn_aux_data {
*/
u8 fastcall_spills_num:3;
u8 arg_prog:4;
+ /*
+ * What a load found, on any path: an __arena pointer member of a
+ * kernel struct, which arena_member_fixup() rewrites, one that is
+ * __nullable as well, or anything else through any pointer.
+ */
+ u8 arena_member:1;
+ u8 arena_member_nullable:1;
+ u8 non_arena_load:1;
/* below fields are initialized once */
unsigned int orig_idx; /* original instruction index */
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index bcbd49c7ca0b..b992027b60b0 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -7695,6 +7695,7 @@ enum bpf_struct_walk_result {
WALK_SCALAR = 0,
WALK_PTR,
WALK_PTR_UNTRUSTED,
+ WALK_PTR_ARENA,
WALK_STRUCT,
};
@@ -7975,6 +7976,30 @@ static int btf_struct_walk(struct bpf_verifier_log *log, const struct btf *btf,
if (btf_ptr_has_type_tag(btf, mtype, "nullable"))
tmp_flag |= PTR_MAYBE_NULL;
+ /*
+ * An __arena member of a kernel struct holds the kernel
+ * address of arena memory, which the verifier turns into
+ * an arena pointer on load. Program BTF is not affected:
+ * there the tag is what the compiler emits for the arena
+ * address space. An offset into a union resolves to its
+ * first member, so a program reading another member there
+ * would get an arena pointer it did not ask for; the walk
+ * marked such a union untrusted.
+ */
+ if (btf_is_kernel(btf) && btf_type_is_arena_ptr(btf, mtype)) {
+ if (*flag & PTR_UNTRUSTED) {
+ bpf_log(log,
+ "arena pointer member %s of %s is inside a union\n",
+ mname, tname);
+ return -EACCES;
+ }
+ *next_btf_id = id;
+ *flag |= tmp_flag & PTR_MAYBE_NULL;
+ if (field_name)
+ *field_name = mname;
+ return WALK_PTR_ARENA;
+ }
+
if (btf_type_is_struct(stype)) {
*next_btf_id = id;
*flag |= tmp_flag;
@@ -8077,6 +8102,11 @@ int btf_struct_access(struct bpf_verifier_log *log,
*flag = MEM_RDONLY | PTR_UNTRUSTED;
ret = PTR_TO_MEM;
goto check_variable_offset;
+ case WALK_PTR_ARENA:
+ *next_btf_id = id;
+ *flag = tmp_flag;
+ ret = PTR_TO_ARENA;
+ goto check_variable_offset;
case WALK_SCALAR:
ret = SCALAR_VALUE;
goto check_variable_offset;
diff --git a/kernel/bpf/fixups.c b/kernel/bpf/fixups.c
index b2fa7e8bedf3..3c95b6c8bbbc 100644
--- a/kernel/bpf/fixups.c
+++ b/kernel/bpf/fixups.c
@@ -870,6 +870,36 @@ int bpf_opt_subreg_zext_lo32_rnd_hi32(struct bpf_verifier_env *env,
* struct __sk_buff -> struct sk_buff
* struct bpf_sock_ops -> struct sock
*/
+/*
+ * A load through an __arena pointer member of a kernel struct reads the kernel
+ * address of arena memory. Append the conversion to the arena pointer form,
+ * (u32)(kaddr - kern_vm_start), that the struct_ops trampoline applies to
+ * __arena arguments. A __nullable member keeps NULL as NULL, tested on the
+ * full kernel address. Returns the number of insns in @buf, 0 when the insn
+ * loads no such member, or an error when it does so on some paths only.
+ */
+static int arena_member_fixup(struct bpf_verifier_env *env, int idx, struct bpf_insn *insn,
+ struct bpf_insn *buf)
+{
+ struct bpf_insn_aux_data *aux = &env->insn_aux_data[idx];
+ int cnt = 0;
+ u32 base;
+
+ if (!aux->arena_member)
+ return 0;
+ if (aux->non_arena_load) {
+ verbose(env, "an insn loads an arena pointer member on one path and something else on another\n");
+ return -EINVAL;
+ }
+
+ base = bpf_arena_get_kern_vm_start(env->prog->aux->arena);
+ buf[cnt++] = *insn;
+ if (aux->arena_member_nullable)
+ buf[cnt++] = BPF_JMP_IMM(BPF_JEQ, insn->dst_reg, 0, 1);
+ buf[cnt++] = BPF_ALU32_IMM(BPF_SUB, insn->dst_reg, (s32)base);
+ return cnt;
+}
+
int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)
{
struct bpf_subprog_info *subprogs = env->subprog_info;
@@ -1093,11 +1123,23 @@ int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)
insn->code = BPF_LDX | BPF_PROBE_MEMSX |
BPF_SIZE(insn->code);
env->prog->aux->num_exentries++;
- continue;
- }
- if (verifier_bug_if(bpf_may_fault_on_deref(ptr_type), env,
- "access to a fault prone pointer is not rewritten as a probed one"))
+ } else if (verifier_bug_if(bpf_may_fault_on_deref(ptr_type), env,
+ "access to a fault prone pointer is not rewritten as a probed one")) {
return -EFAULT;
+ }
+
+ cnt = arena_member_fixup(env, i + delta, insn, insn_buf);
+ if (cnt < 0)
+ return cnt;
+ if (!cnt)
+ continue;
+ new_prog = bpf_patch_insn_data(env, i + delta, insn_buf, cnt);
+ if (!new_prog)
+ return -ENOMEM;
+
+ delta += cnt - 1;
+ env->prog = new_prog;
+ insn = new_prog->insnsi + i + delta;
continue;
}
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index a9c47441fba9..d9ba42b50d89 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -6552,6 +6552,42 @@ static bool type_is_trusted_or_null(struct bpf_verifier_env *env,
"__safe_trusted_or_null");
}
+/*
+ * The member holds the kernel address of arena memory, which the rewritten
+ * load turns into an arena pointer, see arena_member_fixup(). NULL needs no
+ * tracking, PTR_TO_ARENA covers it.
+ */
+static int check_arena_member_load(struct bpf_verifier_env *env, struct bpf_reg_state *regs,
+ int value_regno, enum bpf_type_flag flag, const char *tname,
+ const char *field_name)
+{
+ struct bpf_insn_aux_data *aux = &env->insn_aux_data[env->insn_idx];
+
+ /* a helper reading the member as bytes needs no rewrite */
+ if (value_regno < 0)
+ return 0;
+ if (!env->prog->aux->arena) {
+ verbose(env,
+ "arena pointer member %s of struct %s requires a program with an associated arena\n",
+ field_name, tname);
+ return -EINVAL;
+ }
+ /* only a plain load is rewritten */
+ if (BPF_CLASS(env->prog->insnsi[env->insn_idx].code) != BPF_LDX) {
+ verbose(env,
+ "arena pointer member %s of struct %s can only be read with a plain load\n",
+ field_name, tname);
+ return -EACCES;
+ }
+
+ aux->arena_member = true;
+ if (flag & PTR_MAYBE_NULL)
+ aux->arena_member_nullable = true;
+ mark_reg_unknown(env, regs, value_regno);
+ regs[value_regno].type = PTR_TO_ARENA;
+ return 0;
+}
+
static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
struct bpf_reg_state *regs, struct bpf_reg_state *reg,
argno_t argno, int off, int size,
@@ -6661,6 +6697,9 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
if (ret < 0)
return ret;
+ if (ret == PTR_TO_ARENA)
+ return check_arena_member_load(env, regs, value_regno, flag, tname, field_name);
+
if (ret != PTR_TO_BTF_ID) {
/* just mark; */
@@ -7439,6 +7478,17 @@ static int check_load_mem(struct bpf_verifier_env *env, struct bpf_insn *insn,
err = check_mem_access(env, env->insn_idx, src_reg, argno_from_reg(insn->src_reg),
insn->off, BPF_SIZE(insn->code), BPF_READ, insn->dst_reg,
strict_alignment_once, is_ldsx);
+ /*
+ * Paths through a kernel struct pointer and through other pointers
+ * may share the insn, see save_aux_ptr_type(). Record a load that did
+ * not produce an arena pointer from an __arena member, the only load
+ * through a kernel struct pointer that does, next to the one
+ * check_arena_member_load() records, so that arena_member_fixup() can
+ * tell when the paths disagree.
+ */
+ if (!err && !(base_type(src_reg_type) == PTR_TO_BTF_ID &&
+ regs[insn->dst_reg].type == PTR_TO_ARENA))
+ env->insn_aux_data[env->insn_idx].non_arena_load = true;
err = err ?: save_aux_ptr_type(env, src_reg_type,
allow_trust_mismatch);
err = err ?: reg_bounds_sanity_check(env, ®s[insn->dst_reg], ctx);
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH bpf-next v2 06/13] selftests/bpf: Test __nullable pointer members
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
` (4 preceding siblings ...)
2026-10-13 8:20 ` [PATCH bpf-next v2 05/13] bpf: Turn loads of __arena pointer members into arena pointers Kumar Kartikeya Dwivedi
@ 2026-10-13 8:20 ` Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 07/13] selftests/bpf: Test __arena " Kumar Kartikeya Dwivedi
` (6 subsequent siblings)
12 siblings, 0 replies; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:20 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai, Tejun Heo, kkd,
kernel-team
Add a struct_ops member to bpf_testmod whose argument struct carries two
task pointers, one plain and one __nullable, both made trusted through a
__safe_trusted alias of the struct in the module BTF, and a kfunc that
calls the member once with the nullable pointer unset and once set.
The callback acquires the plain pointer as is and the nullable one only
after a check, driven through the kfunc from a syscall program. The
failure case passes the nullable pointer to bpf_task_acquire() unchecked
and is rejected as possibly NULL.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
.../bpf/prog_tests/test_struct_ops_nullable.c | 68 +++++++++++++++++++
.../selftests/bpf/progs/struct_ops_nullable.c | 45 ++++++++++++
.../bpf/progs/struct_ops_nullable_fail.c | 28 ++++++++
.../selftests/bpf/test_kmods/bpf_testmod.c | 33 +++++++++
.../selftests/bpf/test_kmods/bpf_testmod.h | 15 ++++
.../bpf/test_kmods/bpf_testmod_kfunc.h | 1 +
6 files changed, 190 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/test_struct_ops_nullable.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_nullable.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_nullable_fail.c
diff --git a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_nullable.c b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_nullable.c
new file mode 100644
index 000000000000..17d5de78273a
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_nullable.c
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <test_progs.h>
+#include <bpf/btf.h>
+
+#include "struct_ops_nullable.skel.h"
+#include "struct_ops_nullable_fail.skel.h"
+
+/*
+ * Attach a callback that gets a trusted and a trusted __nullable task pointer
+ * through its argument struct and drive it through the
+ * bpf_testmod_ops3_call_test_nullable() kfunc.
+ */
+static void nullable_member(void)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, topts);
+ struct struct_ops_nullable *skel;
+ struct bpf_link *link;
+ int err;
+
+ skel = struct_ops_nullable__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "struct_ops_nullable__open_and_load"))
+ return;
+
+ link = bpf_map__attach_struct_ops(skel->maps.testmod_nullable);
+ if (!ASSERT_OK_PTR(link, "attach_struct_ops"))
+ goto out;
+
+ err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.trigger), &topts);
+ ASSERT_OK(err, "test_run");
+ ASSERT_EQ(topts.retval, 0, "trigger_retval");
+
+ bpf_link__destroy(link);
+out:
+ struct_ops_nullable__destroy(skel);
+}
+
+/* Serialized because it attaches the singleton bpf_testmod_ops3. */
+/*
+ * The tag on the member of struct bpf_testmod_nullable_args reaches the module
+ * BTF only from a toolchain with btf_type_tag support.
+ */
+static bool testmod_has_nullable_tag(void)
+{
+ struct btf *vmlinux_btf, *module_btf;
+ bool found = false;
+
+ vmlinux_btf = btf__load_vmlinux_btf();
+ if (!ASSERT_OK_PTR(vmlinux_btf, "vmlinux_btf"))
+ return false;
+ module_btf = btf__load_module_btf("bpf_testmod", vmlinux_btf);
+ if (ASSERT_OK_PTR(module_btf, "module_btf"))
+ found = btf__find_by_name_kind(module_btf, "nullable", BTF_KIND_TYPE_TAG) > 0;
+ btf__free(module_btf);
+ btf__free(vmlinux_btf);
+ return found;
+}
+
+void serial_test_struct_ops_nullable(void)
+{
+ if (!testmod_has_nullable_tag()) {
+ test__skip();
+ return;
+ }
+ if (test__start_subtest("nullable_member"))
+ nullable_member();
+ RUN_TESTS(struct_ops_nullable_fail);
+}
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_nullable.c b/tools/testing/selftests/bpf/progs/struct_ops_nullable.c
new file mode 100644
index 000000000000..387792924c09
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_nullable.c
@@ -0,0 +1,45 @@
+// 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 "../test_kmods/bpf_testmod.h"
+#include "../test_kmods/bpf_testmod_kfunc.h"
+
+char _license[] SEC("license") = "GPL";
+
+SEC("struct_ops/test_nullable")
+int BPF_PROG(test_nullable_cb, struct bpf_testmod_nullable_args *args)
+{
+ struct task_struct *t;
+ int ret = 0;
+
+ /* trusted, so good for a kfunc as is */
+ t = bpf_task_acquire(args->task);
+ if (!t)
+ return -1;
+ bpf_task_release(t);
+
+ /* trusted but __nullable, so a check has to come first */
+ if (args->task_or_null) {
+ t = bpf_task_acquire(args->task_or_null);
+ if (!t)
+ return -1;
+ bpf_task_release(t);
+ ret = 1;
+ }
+ return ret;
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_nullable = {
+ .test_nullable = (void *)test_nullable_cb,
+};
+
+SEC("syscall")
+int trigger(void *ctx)
+{
+ /* one call without task_or_null, one with */
+ return bpf_testmod_ops3_call_test_nullable() != 2;
+}
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_nullable_fail.c b/tools/testing/selftests/bpf/progs/struct_ops_nullable_fail.c
new file mode 100644
index 000000000000..c15b3a8c8d39
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_nullable_fail.c
@@ -0,0 +1,28 @@
+// 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_misc.h"
+#include "../test_kmods/bpf_testmod.h"
+
+char _license[] SEC("license") = "GPL";
+
+/* The member is trusted, but __nullable, so it needs a check before the kfunc. */
+SEC("struct_ops/test_nullable")
+__failure __msg("Possibly NULL pointer passed to trusted")
+int BPF_PROG(unchecked_nullable, struct bpf_testmod_nullable_args *args)
+{
+ struct task_struct *t;
+
+ t = bpf_task_acquire(args->task_or_null);
+ if (t)
+ bpf_task_release(t);
+ return 0;
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_nullable_fail = {
+ .test_nullable = (void *)unchecked_nullable,
+};
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
index b4f0af2aeca4..0c0152885da3 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
@@ -460,6 +460,11 @@ static int bpf_testmod_ops3__test_arena_multislot(struct bpf_testmod_arena_pair
return 0;
}
+static int bpf_testmod_ops3__test_nullable(struct bpf_testmod_nullable_args *args)
+{
+ return 0;
+}
+
static struct bpf_testmod_ops3 __bpf_testmod_ops3 = {
.test_1 = bpf_testmod_test_3,
.test_2 = bpf_testmod_test_4,
@@ -467,6 +472,7 @@ static struct bpf_testmod_ops3 __bpf_testmod_ops3 = {
.test_arena_nullable = bpf_testmod_ops3__test_arena_nullable,
.test_arena_stack = bpf_testmod_ops3__test_arena_stack,
.test_arena_multislot = bpf_testmod_ops3__test_arena_multislot,
+ .test_nullable = bpf_testmod_ops3__test_nullable,
};
static void bpf_testmod_test_struct_ops3(void)
@@ -507,6 +513,32 @@ __bpf_kfunc int bpf_testmod_ops3_call_test_arena_multislot(u64 *ptr__arena)
return st_ops3->test_arena_multislot(p, ptr__arena);
}
+/*
+ * The members of struct bpf_testmod_nullable_args are trusted the way the
+ * verifier's BTF_TYPE_SAFE_TRUSTED() lists vmlinux members: through a struct
+ * of the same name with the __safe_trusted suffix in the BTF of the module.
+ */
+struct bpf_testmod_nullable_args__safe_trusted {
+ struct task_struct *task;
+ struct task_struct *task_or_null;
+};
+
+/*
+ * Call test_nullable() with @task_or_null unset, then set. The callback
+ * returns 1 when it found a task there, so the expected result is 2.
+ */
+__bpf_kfunc int bpf_testmod_ops3_call_test_nullable(void)
+{
+ struct bpf_testmod_nullable_args args = { .task = current };
+ int ret;
+
+ BTF_TYPE_EMIT(struct bpf_testmod_nullable_args__safe_trusted);
+
+ ret = st_ops3->test_nullable(&args);
+ args.task_or_null = current;
+ return ret | st_ops3->test_nullable(&args) << 1;
+}
+
struct bpf_testmod_btf_type_tag_1 {
int a;
};
@@ -926,6 +958,7 @@ BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena)
BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_nullable)
BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_stack)
BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_multislot)
+BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_nullable)
BTF_ID_FLAGS(func, bpf_kfunc_get_default_trusted_ptr_test);
BTF_ID_FLAGS(func, bpf_kfunc_put_default_trusted_ptr_test);
BTF_ID_FLAGS(func, bpf_kfunc_kasan_poison)
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h
index 2b12f81bf748..b905c35490eb 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h
@@ -114,6 +114,19 @@ struct bpf_testmod_arena_pair {
u64 b;
};
+/* The kernel defines the BPF pointer qualifiers, user space and BPF programs need them empty. */
+#ifndef __nullable
+#define __nullable
+#endif
+
+/* Arguments of test_nullable(), trusted through the __safe_trusted alias in the module */
+struct bpf_testmod_nullable_args {
+ /* never NULL */
+ struct task_struct *task;
+ /* NULL on the first call, set on the second */
+ struct task_struct __nullable *task_or_null;
+};
+
struct bpf_testmod_ops3 {
int (*test_1)(void);
int (*test_2)(void);
@@ -125,6 +138,8 @@ struct bpf_testmod_ops3 {
u64 g, u64 h, u64 *ptr);
/* a multi-slot leading arg, so @ptr is not at the slot its arg index suggests */
int (*test_arena_multislot)(struct bpf_testmod_arena_pair p, u64 *ptr);
+ /* Used to test __nullable pointer members. */
+ int (*test_nullable)(struct bpf_testmod_nullable_args *args);
};
struct st_ops_args {
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h b/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
index 67c02a421d13..d88c99109c31 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
@@ -218,6 +218,7 @@ int bpf_testmod_ops3_call_test_arena(__u64 *ptr__arena) __ksym;
int bpf_testmod_ops3_call_test_arena_nullable(__u64 *ptr__arena__nullable) __ksym;
int bpf_testmod_ops3_call_test_arena_stack(__u64 *ptr__arena) __ksym;
int bpf_testmod_ops3_call_test_arena_multislot(__u64 *ptr__arena) __ksym;
+int bpf_testmod_ops3_call_test_nullable(void) __ksym;
__u64 bpf_kfunc_call_test1(struct sock *sk, __u32 a, __u64 b,
__u32 c, __u64 d) __ksym;
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH bpf-next v2 07/13] selftests/bpf: Test __arena pointer members
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
` (5 preceding siblings ...)
2026-10-13 8:20 ` [PATCH bpf-next v2 06/13] selftests/bpf: Test __nullable pointer members Kumar Kartikeya Dwivedi
@ 2026-10-13 8:20 ` Kumar Kartikeya Dwivedi
2026-10-13 9:14 ` bot+bpf-ci
2026-10-13 8:20 ` [PATCH bpf-next v2 08/13] libbpf: Bind the arena of an object to every program in it Kumar Kartikeya Dwivedi
` (5 subsequent siblings)
12 siblings, 1 reply; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:20 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai, Tejun Heo, kkd,
kernel-team
Add a struct_ops member to bpf_testmod whose argument struct publishes
arena pointers: a plain one, a __nullable one that is left NULL, one
behind a nested struct, one inside a union, next to a scalar, together
with a kfunc that fills the struct with the kernel address of an __arena
argument and calls the member.
Two callbacks read the pointers back. One shares the header with the
module, so the members are arena pointers on the program side as well
and need no cast; it also records the register a load produces, which
must be the arena offset with the upper 32 bits clear, and hands the
pointer to an __arena kfunc. The other mirrors the vmlinux.h rendering
of the struct, a kernel pointer with the type tag as an attribute, and
casts the members to the arena address space the way a program built
against vmlinux.h has to. Both increment the value through each pointer
and the trigger checks the count.
The failure cases cover a store to the member, a load from the
union, an atomic load, one insn loading the arena pointer on one path
and the scalar on another, and a program without an arena.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
.../bpf/prog_tests/test_struct_ops_arena.c | 91 ++++++++++++++++++
.../bpf/progs/struct_ops_arena_member.c | 92 +++++++++++++++++++
.../struct_ops_arena_member_fail__load_acq.c | 38 ++++++++
.../struct_ops_arena_member_fail__mismatch.c | 45 +++++++++
...ruct_ops_arena_member_fail__mismatch_mem.c | 54 +++++++++++
.../struct_ops_arena_member_fail__no_arena.c | 23 +++++
.../struct_ops_arena_member_fail__union.c | 34 +++++++
.../struct_ops_arena_member_fail__write.c | 32 +++++++
.../progs/struct_ops_arena_member_render.c | 82 +++++++++++++++++
.../selftests/bpf/test_kmods/bpf_testmod.c | 20 ++++
.../selftests/bpf/test_kmods/bpf_testmod.h | 33 ++++++-
.../bpf/test_kmods/bpf_testmod_kfunc.h | 1 +
12 files changed, 544 insertions(+), 1 deletion(-)
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__load_acq.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__no_arena.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__union.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__write.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_member_render.c
diff --git a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
index 7f9f54ba3fbe..1cc1b3df13bc 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
@@ -1,10 +1,19 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#include <test_progs.h>
+#include <bpf/btf.h>
#include "struct_ops_arena.skel.h"
#include "struct_ops_arena_attach.skel.h"
#include "struct_ops_arena_fail.skel.h"
+#include "struct_ops_arena_member.skel.h"
+#include "struct_ops_arena_member_render.skel.h"
+#include "struct_ops_arena_member_fail__union.skel.h"
+#include "struct_ops_arena_member_fail__load_acq.skel.h"
+#include "struct_ops_arena_member_fail__mismatch.skel.h"
+#include "struct_ops_arena_member_fail__mismatch_mem.skel.h"
+#include "struct_ops_arena_member_fail__write.skel.h"
+#include "struct_ops_arena_member_fail__no_arena.skel.h"
#if defined(__x86_64__) || defined(__aarch64__)
/*
@@ -101,6 +110,76 @@ static void arena_arg_attach(void)
struct_ops_arena__destroy(skel);
}
+
+/* Attach @st_ops and run @trigger once, it returns 0 when everything checked out. */
+static void attach_and_trigger(struct bpf_map *st_ops, struct bpf_program *trigger)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, topts);
+ struct bpf_link *link;
+ int err;
+
+ link = bpf_map__attach_struct_ops(st_ops);
+ if (!ASSERT_OK_PTR(link, "attach_struct_ops"))
+ return;
+
+ err = bpf_prog_test_run_opts(bpf_program__fd(trigger), &topts);
+ ASSERT_OK(err, "test_run");
+ ASSERT_EQ(topts.retval, 0, "trigger_retval");
+
+ bpf_link__destroy(link);
+}
+
+/*
+ * The tags on the members of struct bpf_testmod_arena_member_args reach the
+ * module BTF only from a toolchain with btf_type_tag support.
+ */
+static bool testmod_has_type_tag(const char *tag)
+{
+ struct btf *vmlinux_btf, *module_btf;
+ bool found = false;
+
+ vmlinux_btf = btf__load_vmlinux_btf();
+ if (!ASSERT_OK_PTR(vmlinux_btf, "vmlinux_btf"))
+ return false;
+ module_btf = btf__load_module_btf("bpf_testmod", vmlinux_btf);
+ if (ASSERT_OK_PTR(module_btf, "module_btf"))
+ found = btf__find_by_name_kind(module_btf, tag, BTF_KIND_TYPE_TAG) > 0;
+ if (!found)
+ printf("%s:SKIP: no %s type tag in the bpf_testmod BTF\n", __func__, tag);
+ btf__free(module_btf);
+ btf__free(vmlinux_btf);
+ return found;
+}
+
+/*
+ * Attach a callback that reads arena pointers out of its argument struct.
+ * The header is shared with the module, so the members are arena pointers
+ * on the program side as well.
+ */
+static void arena_member(void)
+{
+ struct struct_ops_arena_member *skel;
+
+ skel = struct_ops_arena_member__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "struct_ops_arena_member__open_and_load"))
+ return;
+
+ attach_and_trigger(skel->maps.testmod_arena_member, skel->progs.trigger);
+ struct_ops_arena_member__destroy(skel);
+}
+
+/* The same through the vmlinux.h rendering of the members, which takes casts. */
+static void arena_member_render(void)
+{
+ struct struct_ops_arena_member_render *skel;
+
+ skel = struct_ops_arena_member_render__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "struct_ops_arena_member_render__open_and_load"))
+ return;
+
+ attach_and_trigger(skel->maps.testmod_arena_member_render, skel->progs.trigger);
+ struct_ops_arena_member_render__destroy(skel);
+}
#endif
/*
@@ -122,6 +201,18 @@ void serial_test_struct_ops_arena(void)
arena_arg_fail();
if (test__start_subtest("arena_arg_attach"))
arena_arg_attach();
+ if (testmod_has_type_tag("arena")) {
+ if (test__start_subtest("arena_member"))
+ arena_member();
+ if (test__start_subtest("arena_member_render"))
+ arena_member_render();
+ RUN_TESTS(struct_ops_arena_member_fail__union);
+ RUN_TESTS(struct_ops_arena_member_fail__load_acq);
+ RUN_TESTS(struct_ops_arena_member_fail__mismatch);
+ RUN_TESTS(struct_ops_arena_member_fail__mismatch_mem);
+ RUN_TESTS(struct_ops_arena_member_fail__write);
+ RUN_TESTS(struct_ops_arena_member_fail__no_arena);
+ }
#else
test__skip();
#endif
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member.c
new file mode 100644
index 000000000000..469e62e383f2
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member.c
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#define BPF_NO_KFUNC_PROTOTYPES
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include "bpf_experimental.h"
+#include <bpf_arena_common.h>
+#include "../test_kmods/bpf_testmod.h"
+#include "../test_kmods/bpf_testmod_kfunc.h"
+
+char _license[] SEC("license") = "GPL";
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ /* page 0 hosts the arena globals, page 1 is for allocations */
+ __uint(max_entries, 2);
+} arena SEC(".maps");
+
+/* also associates the callback with the arena */
+u64 __arena arena_touch;
+/* the register value a load of args->ptr produced in the callback */
+u64 __arena member_ptr_val;
+
+/*
+ * The header is shared with the module, so the members are u64 __arena * on
+ * this side too and the loads need no cast. The verifier derives the type of
+ * the loaded value from the module BTF, where the members carry the tag.
+ */
+SEC("struct_ops/test_arena_member")
+int BPF_PROG(test_arena_member_cb, struct bpf_testmod_arena_member_args *args)
+{
+ u64 __arena *ptr = args->ptr;
+ u64 raw;
+
+ arena_touch++;
+
+ /* the register right after the load, untouched by the compiler */
+ asm volatile("%[raw] = *(u64 *)(%[args] + %[off])"
+ : [raw]"=r"(raw)
+ : [args]"r"(args),
+ [off]"i"(offsetof(struct bpf_testmod_arena_member_args, ptr)));
+ member_ptr_val = raw;
+
+ *ptr += 1;
+ if (args->null_ptr)
+ return 0xbad;
+ *args->nested.ptr += 1;
+ if (args->cookie != 0xc00c1e)
+ return 0xbad;
+ /* an arena pointer to kfuncs as well, the kfunc adds one */
+ bpf_kfunc_arena_arg_test((u64 *)args->ptr);
+ return 0;
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_arena_member = {
+ .test_arena_member = (void *)test_arena_member_cb,
+};
+
+SEC("syscall")
+int trigger(void *ctx)
+{
+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST)
+ u64 __arena *val;
+ int ret;
+
+ val = bpf_arena_alloc_pages(&arena, NULL, 1, NUMA_NO_NODE, 0);
+ if (!val)
+ return 1;
+
+ *val = 41;
+ ret = bpf_testmod_ops3_call_test_arena_member((u64 *)val);
+ if (ret)
+ return 2;
+ /* ptr, nested.ptr and the kfunc each added one */
+ if (*val != 44)
+ return 3;
+
+ /*
+ * The callback saw (u32)(kaddr - kern_vm_start), the arena offset of
+ * val with the upper 32 bits clear, the same as an __arena argument.
+ */
+ if (member_ptr_val != (u32)(u64)val)
+ return 4;
+
+ bpf_arena_free_pages(&arena, (void __arena *)val, 1);
+#endif
+ return 0;
+}
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__load_acq.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__load_acq.c
new file mode 100644
index 000000000000..9d7972ae5c55
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__load_acq.c
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf_arena_common.h>
+#include "bpf_misc.h"
+#include "../test_kmods/bpf_testmod.h"
+
+char _license[] SEC("license") = "GPL";
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ __uint(max_entries, 1);
+} arena SEC(".maps");
+
+/* Only a plain load is rewritten to translate the member. */
+SEC("struct_ops/test_arena_member")
+__failure __msg("arena pointer member ptr of struct bpf_testmod_arena_member_args can only be read with a plain load")
+__naked int load_acq_member(void)
+{
+ asm volatile (
+ "r1 = *(u64 *)(r1 + 0);"
+ "r2 = %[arena] ll;"
+ /* r0 = load_acquire((u64 *)(r1 + 0)): BPF_STX | BPF_ATOMIC | BPF_DW, imm BPF_LOAD_ACQ */
+ ".8byte 0x00000100000010db;"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm_addr(arena)
+ : __clobber_all);
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_arena_member_fail = {
+ .test_arena_member = (void *)load_acq_member,
+};
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch.c
new file mode 100644
index 000000000000..0b4147894122
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch.c
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf_arena_common.h>
+#include "bpf_misc.h"
+#include "../test_kmods/bpf_testmod.h"
+
+char _license[] SEC("license") = "GPL";
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ __uint(max_entries, 1);
+} arena SEC(".maps");
+
+/*
+ * The same load reaches args->ptr on one path and args->cookie on the other,
+ * the branch on the unknown cookie value makes the verifier walk both. The
+ * rewrite is per insn, so the paths have to agree on what they load.
+ */
+SEC("struct_ops/test_arena_member")
+__failure __msg("loads an arena pointer member on one path and something else on another")
+__naked int mismatch_member(void)
+{
+ asm volatile (
+ "r6 = *(u64 *)(r1 + 0);"
+ "r2 = %[arena] ll;"
+ "r0 = *(u64 *)(r6 + %[cookie_off]);"
+ "if r0 == 0 goto 1f;"
+ "r6 += %[cookie_off];"
+"1: r0 = *(u64 *)(r6 + 0);"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm_addr(arena),
+ __imm_const(cookie_off, offsetof(struct bpf_testmod_arena_member_args, cookie))
+ : __clobber_all);
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_arena_member_fail = {
+ .test_arena_member = (void *)mismatch_member,
+};
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c
new file mode 100644
index 000000000000..ff6d4db47cfd
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c
@@ -0,0 +1,54 @@
+// 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_arena_common.h>
+#include "bpf_misc.h"
+#include "../test_kmods/bpf_testmod.h"
+
+char _license[] SEC("license") = "GPL";
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ __uint(max_entries, 1);
+} arena SEC(".maps");
+
+/*
+ * The same load reads args->ptr on one path and the memory argument on the
+ * other. The verifier lets a load insn serve a kernel struct pointer and a
+ * memory pointer, see save_aux_ptr_type(), but the rewrite of the arena
+ * member would corrupt the value the other path loads. A global function
+ * is the only place a struct_ops program of this type gets a memory pointer.
+ * The loaded value goes nowhere: an ALU insn on it would be an arena
+ * pointer on one path and a scalar on the other, which the verifier rejects
+ * on its own before the fixup pass gets to the load.
+ */
+__noinline int load_either(unsigned long long *ctx __arg_ctx, u64 *mem __arg_nonnull)
+{
+ struct bpf_testmod_arena_member_args *args = (void *)ctx[0];
+ void *p = args;
+
+ if (args->cookie == 0)
+ p = mem;
+ /* one load insn, reached with both pointers */
+ asm volatile("r0 = *(u64 *)(%[p] + 0)" :: [p]"r"(p) : "r0");
+ return 0;
+}
+
+SEC("struct_ops/test_arena_member")
+__failure __msg("loads an arena pointer member on one path and something else on another")
+int BPF_PROG(mismatch_mem, struct bpf_testmod_arena_member_args *args)
+{
+ u64 local = 0;
+
+ asm volatile("" :: "r"(&arena));
+ return load_either(ctx, &local);
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_arena_member_fail = {
+ .test_arena_member = (void *)mismatch_mem,
+};
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__no_arena.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__no_arena.c
new file mode 100644
index 000000000000..3cf76e661583
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__no_arena.c
@@ -0,0 +1,23 @@
+// 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_misc.h"
+#include "../test_kmods/bpf_testmod.h"
+
+char _license[] SEC("license") = "GPL";
+
+/* No arena in the object: the member cannot be translated into anything. */
+SEC("struct_ops/test_arena_member")
+__failure __msg("arena pointer member ptr of struct bpf_testmod_arena_member_args requires a program with an associated arena")
+int BPF_PROG(no_arena_member, struct bpf_testmod_arena_member_args *args)
+{
+ return args->ptr != NULL;
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_arena_member_fail = {
+ .test_arena_member = (void *)no_arena_member,
+};
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__union.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__union.c
new file mode 100644
index 000000000000..d45666fd482c
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__union.c
@@ -0,0 +1,34 @@
+// 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_arena_common.h>
+#include "bpf_misc.h"
+#include "../test_kmods/bpf_testmod.h"
+
+char _license[] SEC("license") = "GPL";
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ __uint(max_entries, 1);
+} arena SEC(".maps");
+
+/*
+ * An offset into a union resolves to its first member, so a program reading
+ * raw there would get an arena pointer it did not ask for.
+ */
+SEC("struct_ops/test_arena_member")
+__failure __msg("arena pointer member ptr of bpf_testmod_arena_union is inside a union")
+int BPF_PROG(union_member, struct bpf_testmod_arena_member_args *args)
+{
+ asm volatile("" :: "r"(&arena));
+ return *args->u.ptr != 0;
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_arena_member_fail = {
+ .test_arena_member = (void *)union_member,
+};
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__write.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__write.c
new file mode 100644
index 000000000000..bcf1f6586e19
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__write.c
@@ -0,0 +1,32 @@
+// 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_arena_common.h>
+#include "bpf_misc.h"
+#include "../test_kmods/bpf_testmod.h"
+
+char _license[] SEC("license") = "GPL";
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ __uint(max_entries, 1);
+} arena SEC(".maps");
+
+/* The kernel owns the member, a program only reads it. */
+SEC("struct_ops/test_arena_member")
+__failure __msg("only read is supported")
+int BPF_PROG(write_member, struct bpf_testmod_arena_member_args *args)
+{
+ asm volatile("" :: "r"(&arena));
+ args->ptr = NULL;
+ return 0;
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_arena_member_fail = {
+ .test_arena_member = (void *)write_member,
+};
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_render.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_render.c
new file mode 100644
index 000000000000..0a6b7c5f494b
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_render.c
@@ -0,0 +1,82 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#define BPF_NO_KFUNC_PROTOTYPES
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include "bpf_experimental.h"
+#include <bpf_arena_common.h>
+
+char _license[] SEC("license") = "GPL";
+
+/*
+ * The module types as vmlinux.h renders them: the tag is an attribute on a
+ * pointer in the kernel address space, so the program has to cast the member
+ * to the arena address space before using it. That is what a program built
+ * against vmlinux.h sees for a kernel struct with an __arena member.
+ */
+struct bpf_testmod_arena_member_args {
+ __u64 __attribute__((btf_type_tag("arena"))) *ptr;
+ __u64 __attribute__((btf_type_tag("arena"))) __attribute__((btf_type_tag("nullable"))) *null_ptr;
+ struct {
+ __u64 __attribute__((btf_type_tag("arena"))) *ptr;
+ } nested;
+} __attribute__((preserve_access_index));
+
+struct bpf_testmod_ops3 {
+ int (*test_arena_member)(struct bpf_testmod_arena_member_args *args);
+};
+
+int bpf_testmod_ops3_call_test_arena_member(__u64 *ptr__arena) __ksym;
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ __uint(max_entries, 2);
+} arena SEC(".maps");
+
+/* also associates the callback with the arena */
+u64 __arena arena_touch;
+
+SEC("struct_ops/test_arena_member")
+int BPF_PROG(test_arena_member_render_cb, struct bpf_testmod_arena_member_args *args)
+{
+ u64 __arena *ptr = (u64 __arena *)args->ptr;
+
+ arena_touch++;
+ *ptr += 1;
+ if (args->null_ptr)
+ return 0xbad;
+ *(u64 __arena *)args->nested.ptr += 1;
+ return 0;
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_arena_member_render = {
+ .test_arena_member = (void *)test_arena_member_render_cb,
+};
+
+SEC("syscall")
+int trigger(void *ctx)
+{
+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST)
+ u64 __arena *val;
+ int ret;
+
+ val = bpf_arena_alloc_pages(&arena, NULL, 1, NUMA_NO_NODE, 0);
+ if (!val)
+ return 1;
+
+ *val = 41;
+ ret = bpf_testmod_ops3_call_test_arena_member((u64 *)val);
+ if (ret)
+ return 2;
+ /* ptr and nested.ptr each added one */
+ if (*val != 43)
+ return 3;
+
+ bpf_arena_free_pages(&arena, (void __arena *)val, 1);
+#endif
+ return 0;
+}
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
index 0c0152885da3..5a0556d4ce31 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
@@ -460,6 +460,11 @@ static int bpf_testmod_ops3__test_arena_multislot(struct bpf_testmod_arena_pair
return 0;
}
+static int bpf_testmod_ops3__test_arena_member(struct bpf_testmod_arena_member_args *args)
+{
+ return 0;
+}
+
static int bpf_testmod_ops3__test_nullable(struct bpf_testmod_nullable_args *args)
{
return 0;
@@ -472,6 +477,7 @@ static struct bpf_testmod_ops3 __bpf_testmod_ops3 = {
.test_arena_nullable = bpf_testmod_ops3__test_arena_nullable,
.test_arena_stack = bpf_testmod_ops3__test_arena_stack,
.test_arena_multislot = bpf_testmod_ops3__test_arena_multislot,
+ .test_arena_member = bpf_testmod_ops3__test_arena_member,
.test_nullable = bpf_testmod_ops3__test_nullable,
};
@@ -513,6 +519,19 @@ __bpf_kfunc int bpf_testmod_ops3_call_test_arena_multislot(u64 *ptr__arena)
return st_ops3->test_arena_multislot(p, ptr__arena);
}
+/* The callback gets the kernel address of @ptr__arena behind each __arena member. */
+__bpf_kfunc int bpf_testmod_ops3_call_test_arena_member(u64 *ptr__arena)
+{
+ struct bpf_testmod_arena_member_args args = {
+ .ptr = ptr__arena,
+ .nested.ptr = ptr__arena,
+ .u.ptr = ptr__arena,
+ .cookie = 0xc00c1e,
+ };
+
+ return st_ops3->test_arena_member(&args);
+}
+
/*
* The members of struct bpf_testmod_nullable_args are trusted the way the
* verifier's BTF_TYPE_SAFE_TRUSTED() lists vmlinux members: through a struct
@@ -958,6 +977,7 @@ BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena)
BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_nullable)
BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_stack)
BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_multislot)
+BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_member)
BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_nullable)
BTF_ID_FLAGS(func, bpf_kfunc_get_default_trusted_ptr_test);
BTF_ID_FLAGS(func, bpf_kfunc_put_default_trusted_ptr_test);
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h
index b905c35490eb..e92f9fbfe6a6 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h
@@ -114,11 +114,40 @@ struct bpf_testmod_arena_pair {
u64 b;
};
-/* The kernel defines the BPF pointer qualifiers, user space and BPF programs need them empty. */
+/*
+ * The kernel defines the BPF pointer qualifiers, a BPF program gets __arena
+ * from bpf_arena_common.h and user space needs neither.
+ */
+#ifndef __arena
+#define __arena
+#endif
#ifndef __nullable
#define __nullable
#endif
+struct bpf_testmod_arena_nested {
+ u64 __arena *ptr;
+};
+
+union bpf_testmod_arena_union {
+ u64 __arena *ptr;
+ u64 raw;
+};
+
+/* Arguments of test_arena_member(), the pointers hold kernel addresses of arena memory */
+struct bpf_testmod_arena_member_args {
+ /* the value the caller passed, never NULL */
+ u64 __arena *ptr;
+ /* always NULL */
+ u64 __arena __nullable *null_ptr;
+ /* the same value again, behind a nested struct */
+ struct bpf_testmod_arena_nested nested;
+ /* the same value again, but a load from a union is rejected */
+ union bpf_testmod_arena_union u;
+ /* a scalar, loaded by the same insn as @ptr in a verifier test */
+ u64 cookie;
+};
+
/* Arguments of test_nullable(), trusted through the __safe_trusted alias in the module */
struct bpf_testmod_nullable_args {
/* never NULL */
@@ -138,6 +167,8 @@ struct bpf_testmod_ops3 {
u64 g, u64 h, u64 *ptr);
/* a multi-slot leading arg, so @ptr is not at the slot its arg index suggests */
int (*test_arena_multislot)(struct bpf_testmod_arena_pair p, u64 *ptr);
+ /* Used to test __arena pointer members. */
+ int (*test_arena_member)(struct bpf_testmod_arena_member_args *args);
/* Used to test __nullable pointer members. */
int (*test_nullable)(struct bpf_testmod_nullable_args *args);
};
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h b/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
index d88c99109c31..8b85b2d73e1c 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
@@ -218,6 +218,7 @@ int bpf_testmod_ops3_call_test_arena(__u64 *ptr__arena) __ksym;
int bpf_testmod_ops3_call_test_arena_nullable(__u64 *ptr__arena__nullable) __ksym;
int bpf_testmod_ops3_call_test_arena_stack(__u64 *ptr__arena) __ksym;
int bpf_testmod_ops3_call_test_arena_multislot(__u64 *ptr__arena) __ksym;
+int bpf_testmod_ops3_call_test_arena_member(__u64 *ptr__arena) __ksym;
int bpf_testmod_ops3_call_test_nullable(void) __ksym;
__u64 bpf_kfunc_call_test1(struct sock *sk, __u32 a, __u64 b,
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH bpf-next v2 08/13] libbpf: Bind the arena of an object to every program in it
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
` (6 preceding siblings ...)
2026-10-13 8:20 ` [PATCH bpf-next v2 07/13] selftests/bpf: Test __arena " Kumar Kartikeya Dwivedi
@ 2026-10-13 8:20 ` Kumar Kartikeya Dwivedi
2026-10-13 9:14 ` bot+bpf-ci
2026-10-13 8:20 ` [PATCH bpf-next v2 09/13] selftests/bpf: Test arena binding by libbpf Kumar Kartikeya Dwivedi
` (4 subsequent siblings)
12 siblings, 1 reply; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:20 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai, Tejun Heo, kkd,
kernel-team
A program has an arena only when one of its insns refers to the arena
map. The kernel can hand a program arena pointers all the same: as
__arena struct_ops arguments, through __arena struct members and as
kfunc return values. The verifier rejects those in a program without an
arena, since it has nowhere to place them, and so a struct_ops callback
that touches nothing but its arguments fails to load until it refers to
the arena artificially, e.g. with asm volatile("" :: "r"(&arena)).
Bind the arena of an object to every program loaded from it. The kernel
binds the maps of a continuous fd_array (fd_array_cnt) like maps an insn
refers to, so pass the arena in slot 0, which is free: a kfunc insn with
offset 0 names vmlinux BTF rather than a slot. The module BTF slots that
follow stay as they are. A feature probe keeps older kernels, which
reject a non-zero fd_array_cnt, on the previous behavior.
An object has at most one arena, libbpf refuses a second one, so there
is no ambiguity about which arena a program gets. Programs that never
touch the arena pay for the binding with the arena base setup in their
JIT prologue and with the arena showing up among their maps, and the
arena now lives as long as any program of the object. Where the JIT
is disabled or the loader lacks CAP_PERFMON, every program of such an
object now fails to load, since an arena needs both, where before only
the programs using the arena did. bpf_program__clone() binds the arena
the same way, so a program loads through veristat as it does through
bpf_object__load(). The light skeleton loader is left alone.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
tools/lib/bpf/features.c | 40 ++++++++++++++++++++++++
tools/lib/bpf/libbpf.c | 55 ++++++++++++++++++++++++++++++++-
tools/lib/bpf/libbpf_internal.h | 2 ++
3 files changed, 96 insertions(+), 1 deletion(-)
diff --git a/tools/lib/bpf/features.c b/tools/lib/bpf/features.c
index 9f589980beef..71c6a666afcb 100644
--- a/tools/lib/bpf/features.c
+++ b/tools/lib/bpf/features.c
@@ -77,6 +77,43 @@ static int probe_kern_global_data(int token_fd)
return probe_fd(ret);
}
+/*
+ * A continuous fd_array binds every map in it to the program, whether an
+ * insn refers to the map or not. Kernels without the attribute reject a
+ * non-zero fd_array_cnt.
+ */
+static int probe_kern_fd_array_cnt(int token_fd)
+{
+ struct bpf_insn insns[] = {
+ BPF_MOV64_IMM(BPF_REG_0, 0),
+ BPF_EXIT_INSN(),
+ };
+ LIBBPF_OPTS(bpf_map_create_opts, map_opts,
+ .token_fd = token_fd,
+ .map_flags = token_fd ? BPF_F_TOKEN_FD : 0,
+ );
+ LIBBPF_OPTS(bpf_prog_load_opts, prog_opts,
+ .token_fd = token_fd,
+ .prog_flags = token_fd ? BPF_F_TOKEN_FD : 0,
+ .fd_array_cnt = 1,
+ );
+ int ret, map, insn_cnt = ARRAY_SIZE(insns);
+
+ map = bpf_map_create(BPF_MAP_TYPE_ARRAY, "libbpf_det_bind", sizeof(int), sizeof(int), 1,
+ &map_opts);
+ if (map < 0) {
+ ret = -errno;
+ pr_warn("Error in %s(): %s. Couldn't create simple array map.\n",
+ __func__, errstr(ret));
+ return ret;
+ }
+
+ prog_opts.fd_array = ↦
+ ret = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, NULL, "GPL", insns, insn_cnt, &prog_opts);
+ close(map);
+ return probe_fd(ret);
+}
+
static int probe_kern_btf(int token_fd)
{
static const char strs[] = "\0int";
@@ -742,6 +779,9 @@ static struct kern_feature_desc {
[FEAT_PERCPU_DATA] = {
"kernel supports percpu data", probe_kern_percpu_data,
},
+ [FEAT_FD_ARRAY_CNT] = {
+ "continuous fd_array (fd_array_cnt)", probe_kern_fd_array_cnt,
+ },
};
bool feat_supported(struct kern_feature_cache *cache, enum kern_feature_id feat_id)
diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
index fcedb78abe00..0cd2c25a120d 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -8804,6 +8804,46 @@ static int libbpf_prepare_prog_load(struct bpf_program *prog,
static void fixup_verifier_log(struct bpf_program *prog, char *buf, size_t buf_sz);
+/*
+ * Bind the arena of the object to the program about to be loaded. A program
+ * has an arena only when an insn refers to the map, but the kernel can hand
+ * it arena pointers all the same, as struct_ops arguments, through tagged
+ * struct members or as kfunc return values, and the verifier rejects those
+ * when the program has no arena to place them in. A continuous fd_array
+ * binds every map in it, the arena takes slot 0, which is free: a kfunc insn
+ * with offset 0 names vmlinux BTF rather than a slot.
+ */
+static int bpf_object__bind_arena(struct bpf_object *obj, struct bpf_program *prog,
+ struct bpf_prog_load_opts *load_attr)
+{
+ struct bpf_map *map;
+ int err;
+
+ if (obj->arena_map_idx < 0 || !kernel_supports(obj, FEAT_FD_ARRAY_CNT))
+ return 0;
+ map = &obj->maps[obj->arena_map_idx];
+ /*
+ * Nothing to bind when the arena is not created, when it is exclusive
+ * to another program, or for an offloaded program, which the kernel
+ * does not let near an arena.
+ */
+ if (!map->autocreate || (map->excl_prog && map->excl_prog != prog) ||
+ load_attr->prog_ifindex)
+ return 0;
+
+ if (!obj->fd_array_used) {
+ err = libbpf_ensure_mem((void **)&obj->fd_array, &obj->fd_array_cap, sizeof(int), 1);
+ if (err)
+ return err;
+ obj->fd_array_used = 1;
+ }
+ obj->fd_array[0] = map->fd;
+ load_attr->fd_array = obj->fd_array;
+ /* with slot 0 filled every slot handed out is valid, so the array can go with a count */
+ load_attr->fd_array_cnt = obj->fd_array_used;
+ return 0;
+}
+
static int bpf_object_load_prog(struct bpf_object *obj, struct bpf_program *prog,
struct bpf_insn *insns, int insns_cnt,
const char *license, __u32 kern_version, int *prog_fd)
@@ -8867,6 +8907,11 @@ static int bpf_object_load_prog(struct bpf_object *obj, struct bpf_program *prog
if (obj->data_in_arena)
load_attr.prog_flags |= BPF_F_ARENA_SCALAR;
load_attr.fd_array = obj->fd_array;
+ if (!obj->gen_loader) {
+ err = bpf_object__bind_arena(obj, prog, &load_attr);
+ if (err)
+ return err;
+ }
load_attr.token_fd = obj->token_fd;
if (obj->token_fd)
@@ -9966,8 +10011,10 @@ static void bpf_object_cleanup_btf(struct bpf_object *obj)
static void bpf_object_post_load_cleanup(struct bpf_object *obj)
{
- /* clean up fd_array */
+ /* clean up fd_array, and the slots handed out with it */
zfree(&obj->fd_array);
+ obj->fd_array_cap = 0;
+ obj->fd_array_used = 0;
/* clean up BTF */
bpf_object_cleanup_btf(obj);
@@ -10946,6 +10993,12 @@ int bpf_program__clone(struct bpf_program *prog, const struct bpf_prog_load_opts
attr.kern_version = OPTS_GET(opts, kern_version, 0) ?: obj->kern_version;
attr.fd_array = OPTS_GET(opts, fd_array, NULL) ?: obj->fd_array;
attr.fd_array_cnt = OPTS_GET(opts, fd_array_cnt, 0);
+ /* the arena of the object goes with the program, as in bpf_object__load() */
+ if (!obj->gen_loader && !OPTS_GET(opts, fd_array, NULL)) {
+ err = bpf_object__bind_arena(obj, prog, &attr);
+ if (err)
+ return libbpf_err(err);
+ }
attr.token_fd = OPTS_GET(opts, token_fd, 0) ?: obj->token_fd;
if (attr.token_fd)
attr.prog_flags |= BPF_F_TOKEN_FD;
diff --git a/tools/lib/bpf/libbpf_internal.h b/tools/lib/bpf/libbpf_internal.h
index 4e45dfacb548..22d9fe512ea1 100644
--- a/tools/lib/bpf/libbpf_internal.h
+++ b/tools/lib/bpf/libbpf_internal.h
@@ -404,6 +404,8 @@ enum kern_feature_id {
FEAT_BPF_SYSCALL_COMMON_ATTRS,
/* Kernel supports percpu data */
FEAT_PERCPU_DATA,
+ /* Kernel binds every map in a continuous fd_array (fd_array_cnt) to the program */
+ FEAT_FD_ARRAY_CNT,
__FEAT_CNT,
};
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH bpf-next v2 09/13] selftests/bpf: Test arena binding by libbpf
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
` (7 preceding siblings ...)
2026-10-13 8:20 ` [PATCH bpf-next v2 08/13] libbpf: Bind the arena of an object to every program in it Kumar Kartikeya Dwivedi
@ 2026-10-13 8:20 ` Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 10/13] bpf: Set up the arena base only in functions that use it Kumar Kartikeya Dwivedi
` (3 subsequent siblings)
12 siblings, 0 replies; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:20 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai, Tejun Heo, kkd,
kernel-team
Attach a struct_ops callback with an __arena argument from an object
whose programs never refer to the arena and drive it through the kernel,
and check that an unrelated program of the object got the arena among
its maps as well.
Two existing programs expected to fail for want of an arena while living
next to an arena map, the kfunc argument case in arena_kfunc.c and the
scalar access case in verifier_arena_scalar.c. They load now, so move
them to an object without an arena map, where the kernel checks still
apply.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
.../bpf/prog_tests/test_struct_ops_arena.c | 48 ++++++++++++++
.../selftests/bpf/prog_tests/verifier.c | 2 +
.../testing/selftests/bpf/progs/arena_kfunc.c | 10 ---
.../selftests/bpf/progs/arena_noarena.c | 39 +++++++++++
.../bpf/progs/struct_ops_arena_autobind.c | 64 +++++++++++++++++++
.../bpf/progs/verifier_arena_scalar.c | 13 ----
6 files changed, 153 insertions(+), 23 deletions(-)
create mode 100644 tools/testing/selftests/bpf/progs/arena_noarena.c
create mode 100644 tools/testing/selftests/bpf/progs/struct_ops_arena_autobind.c
diff --git a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
index 1cc1b3df13bc..149b130f8cdb 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
@@ -14,6 +14,7 @@
#include "struct_ops_arena_member_fail__mismatch_mem.skel.h"
#include "struct_ops_arena_member_fail__write.skel.h"
#include "struct_ops_arena_member_fail__no_arena.skel.h"
+#include "struct_ops_arena_autobind.skel.h"
#if defined(__x86_64__) || defined(__aarch64__)
/*
@@ -168,6 +169,51 @@ static void arena_member(void)
struct_ops_arena_member__destroy(skel);
}
+/* @prog_fd lists exactly one map, the one with @map_id. */
+static void assert_prog_map(int prog_fd, __u32 map_id, const char *name)
+{
+ __u32 map_ids[2] = {}, len = sizeof(struct bpf_prog_info);
+ struct bpf_prog_info info = {
+ .nr_map_ids = ARRAY_SIZE(map_ids),
+ .map_ids = ptr_to_u64(map_ids),
+ };
+ int err;
+
+ err = bpf_prog_get_info_by_fd(prog_fd, &info, &len);
+ if (!ASSERT_OK(err, name))
+ return;
+ if (!ASSERT_EQ(info.nr_map_ids, 1, name))
+ return;
+ ASSERT_EQ(map_ids[0], map_id, name);
+}
+
+/*
+ * libbpf binds the arena of an object to every program in it: a callback
+ * with an __arena argument loads without referring to the arena, and an
+ * unrelated program gets the arena as well.
+ */
+static void arena_autobind(void)
+{
+ struct struct_ops_arena_autobind *skel;
+ struct bpf_map_info info = {};
+ __u32 len = sizeof(info);
+ int err;
+
+ skel = struct_ops_arena_autobind__open_and_load();
+ if (!ASSERT_OK_PTR(skel, "struct_ops_arena_autobind__open_and_load"))
+ return;
+
+ err = bpf_map_get_info_by_fd(bpf_map__fd(skel->maps.arena), &info, &len);
+ if (!ASSERT_OK(err, "arena_info"))
+ goto out;
+ assert_prog_map(bpf_program__fd(skel->progs.test_arena_cb), info.id, "test_arena_cb");
+ assert_prog_map(bpf_program__fd(skel->progs.unrelated), info.id, "unrelated");
+
+ attach_and_trigger(skel->maps.testmod_arena_autobind, skel->progs.trigger);
+out:
+ struct_ops_arena_autobind__destroy(skel);
+}
+
/* The same through the vmlinux.h rendering of the members, which takes casts. */
static void arena_member_render(void)
{
@@ -213,6 +259,8 @@ void serial_test_struct_ops_arena(void)
RUN_TESTS(struct_ops_arena_member_fail__write);
RUN_TESTS(struct_ops_arena_member_fail__no_arena);
}
+ if (test__start_subtest("arena_autobind"))
+ arena_autobind();
#else
test__skip();
#endif
diff --git a/tools/testing/selftests/bpf/prog_tests/verifier.c b/tools/testing/selftests/bpf/prog_tests/verifier.c
index c69c0f7e9cc8..6df440d82675 100644
--- a/tools/testing/selftests/bpf/prog_tests/verifier.c
+++ b/tools/testing/selftests/bpf/prog_tests/verifier.c
@@ -3,6 +3,7 @@
#include <test_progs.h>
#include "arena_kfunc.skel.h"
+#include "arena_noarena.skel.h"
#include "arena_kfunc_jit.skel.h"
#include "cap_helpers.h"
#include "cb_refs.skel.h"
@@ -193,6 +194,7 @@ static void run_tests_aux(const char *skel_name,
} while (0)
void test_arena_kfunc(void) { RUN_TESTS(arena_kfunc); }
+void test_arena_noarena(void) { RUN_TESTS(arena_noarena); }
void test_arena_kfunc_jit(void) { RUN_TESTS(arena_kfunc_jit); }
diff --git a/tools/testing/selftests/bpf/progs/arena_kfunc.c b/tools/testing/selftests/bpf/progs/arena_kfunc.c
index 96f7aacf4d9f..a049d6371d09 100644
--- a/tools/testing/selftests/bpf/progs/arena_kfunc.c
+++ b/tools/testing/selftests/bpf/progs/arena_kfunc.c
@@ -192,16 +192,6 @@ int arena_arg_unpopulated(void *ctx)
return 0;
}
-SEC("syscall")
-__arch_x86_64
-__arch_arm64
-__failure __msg("arena pointer requires a program with an associated arena")
-int arena_arg_no_arena(void *ctx)
-{
- bpf_kfunc_arena_arg_test((u64 *)1);
- return 0;
-}
-
SEC("syscall")
__arch_x86_64
__arch_arm64
diff --git a/tools/testing/selftests/bpf/progs/arena_noarena.c b/tools/testing/selftests/bpf/progs/arena_noarena.c
new file mode 100644
index 000000000000..edbd40cd5be8
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/arena_noarena.c
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include "bpf_misc.h"
+#include "../test_kmods/bpf_testmod_kfunc.h"
+
+/*
+ * Programs without an arena. They cannot share an object with an arena map,
+ * libbpf binds the arena of an object to every program in it.
+ */
+
+SEC("syscall")
+__arch_x86_64
+__arch_arm64
+__failure __msg("arena pointer requires a program with an associated arena")
+int arena_arg_no_arena(void *ctx)
+{
+ bpf_kfunc_arena_arg_test((u64 *)1);
+ return 0;
+}
+
+SEC("syscall")
+__flag(BPF_F_ARENA_SCALAR)
+__arch_x86_64
+__arch_arm64
+__description("arena_scalar: no arena, no access through a number")
+__failure __msg("R6 invalid mem access 'scalar'")
+__naked void no_arena(void)
+{
+ asm volatile (" \
+ r6 = 0x100000000000 ll; \
+ r0 = *(u64 *)(r6 + 0); \
+ exit; \
+" ::: __clobber_all);
+}
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_autobind.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_autobind.c
new file mode 100644
index 000000000000..495faa0f4a8b
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_autobind.c
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#define BPF_NO_KFUNC_PROTOTYPES
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include "bpf_experimental.h"
+#include <bpf_arena_common.h>
+#include "../test_kmods/bpf_testmod.h"
+#include "../test_kmods/bpf_testmod_kfunc.h"
+
+char _license[] SEC("license") = "GPL";
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ __uint(max_entries, 1);
+} arena SEC(".maps");
+
+/*
+ * Neither the callback nor the unrelated program refers to the arena. libbpf
+ * binds it to them all the same, so the callback can use the __arena argument
+ * the kernel hands it.
+ */
+SEC("struct_ops/test_arena")
+int test_arena_cb(unsigned long long *ctx)
+{
+ u64 __arena *ptr = (u64 __arena *)ctx[0];
+
+ *ptr += 1;
+ return 0;
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_arena_autobind = {
+ .test_arena = (void *)test_arena_cb,
+};
+
+SEC("syscall")
+int unrelated(void *ctx)
+{
+ return 0;
+}
+
+SEC("syscall")
+int trigger(void *ctx)
+{
+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST)
+ u64 __arena *val;
+
+ val = bpf_arena_alloc_pages(&arena, NULL, 1, NUMA_NO_NODE, 0);
+ if (!val)
+ return 1;
+
+ *val = 41;
+ if (bpf_testmod_ops3_call_test_arena((u64 *)val))
+ return 2;
+ if (*val != 42)
+ return 3;
+
+ bpf_arena_free_pages(&arena, (void __arena *)val, 1);
+#endif
+ return 0;
+}
diff --git a/tools/testing/selftests/bpf/progs/verifier_arena_scalar.c b/tools/testing/selftests/bpf/progs/verifier_arena_scalar.c
index bebc37f501b7..a4f15eb21809 100644
--- a/tools/testing/selftests/bpf/progs/verifier_arena_scalar.c
+++ b/tools/testing/selftests/bpf/progs/verifier_arena_scalar.c
@@ -735,19 +735,6 @@ __naked void no_flag(void)
: __clobber_all);
}
-SEC("syscall")
-__arena_scalar
-__description("arena_scalar: no arena, no access through a number")
-__failure __msg("R6 invalid mem access 'scalar'")
-__naked void no_arena(void)
-{
- asm volatile (" \
- r6 = 0x100000000000 ll; \
- r0 = *(u64 *)(r6 + 0); \
- exit; \
-" ::: __clobber_all);
-}
-
SEC("syscall")
__arena_scalar
__description("arena_scalar: pointer that is NULL is an address in arena")
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH bpf-next v2 10/13] bpf: Set up the arena base only in functions that use it
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
` (8 preceding siblings ...)
2026-10-13 8:20 ` [PATCH bpf-next v2 09/13] selftests/bpf: Test arena binding by libbpf Kumar Kartikeya Dwivedi
@ 2026-10-13 8:20 ` Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 11/13] libbpf: Render the arena type tag with its address space Kumar Kartikeya Dwivedi
` (2 subsequent siblings)
12 siblings, 0 replies; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:20 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai, Tejun Heo, kkd,
kernel-team
A JIT sets up the arena base in the prologue of every function of a
program that has an arena, a callee-saved register loaded with a 64-bit
immediate and spilled around the function on x86. That is paid for by
every program with an arena, whether it touches the arena or not, and
now that libbpf binds the arena of an object to every program in it, by
every program of an object that happens to contain an arena.
Have the verifier record whether a function has an insn that works with
the base: an access through an arena pointer or through a number in a
program loaded with BPF_F_ARENA_SCALAR, an addr_space_cast, or a kfunc
call with an __arena argument, which the JIT rebases. A load of an
__arena struct member is rewritten with an immediate and needs none;
dereferencing the result does.
uses_arena_insns is set per function in bpf_subprog_info and copied to
the JIT image of each subprogram, and per program in bpf_prog_aux for
the single function case. It implies that the program has an arena, as
every site that sets it checked for one. The JITs with arena support
pick up the arena base and user address only when
bpf_prog_uses_arena_insns() says so, and so generate the prologue and
epilogue of a program bound to an arena it never touches as if it had
none.
The other users of the arena address of a program stay as they are on
purpose. The struct_ops trampoline converts __arena arguments on entry
and arena_member_fixup() converts loaded __arena members after the load,
both with the base as an immediate. Both have to happen whether or not
the program dereferences the result, it may compare, store or pass it
on, so neither depends on the base register nor on the flag. The arena
fault report reads the user address of the arena the faulting insn
belongs to, and such an insn sets the flag anyway.
check_map_prog_compatibility() checks the user address when the arena
is bound, before any use is known. The rest, in arena.c and sched_ext,
works with the map and not with a program.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
arch/arm64/net/bpf_jit_comp.c | 6 ++++--
arch/loongarch/net/bpf_jit.c | 6 ++++--
arch/powerpc/net/bpf_jit_comp.c | 6 ++++--
arch/riscv/net/bpf_jit_core.c | 6 ++++--
arch/s390/net/bpf_jit_comp.c | 7 ++++---
arch/x86/net/bpf_jit_comp.c | 14 ++++++++------
include/linux/bpf.h | 2 ++
include/linux/bpf_verifier.h | 2 ++
include/linux/filter.h | 11 +++++++++++
kernel/bpf/fixups.c | 1 +
kernel/bpf/verifier.c | 18 ++++++++++++++++++
11 files changed, 62 insertions(+), 17 deletions(-)
diff --git a/arch/arm64/net/bpf_jit_comp.c b/arch/arm64/net/bpf_jit_comp.c
index 506c4201fec8..1d5f4cf88d33 100644
--- a/arch/arm64/net/bpf_jit_comp.c
+++ b/arch/arm64/net/bpf_jit_comp.c
@@ -2284,8 +2284,10 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
if (ctx.offset == NULL)
goto out_off;
- ctx.user_vm_start = bpf_arena_get_user_vm_start(prog->aux->arena);
- ctx.arena_vm_start = bpf_arena_get_kern_vm_start(prog->aux->arena);
+ if (bpf_prog_uses_arena_insns(prog)) {
+ ctx.user_vm_start = bpf_arena_get_user_vm_start(prog->aux->arena);
+ ctx.arena_vm_start = bpf_arena_get_kern_vm_start(prog->aux->arena);
+ }
out_cnt = bpf_out_stack_arg_cnt(env, prog);
if (out_cnt) {
diff --git a/arch/loongarch/net/bpf_jit.c b/arch/loongarch/net/bpf_jit.c
index e78eb582c300..9393e4b0db8e 100644
--- a/arch/loongarch/net/bpf_jit.c
+++ b/arch/loongarch/net/bpf_jit.c
@@ -2245,8 +2245,10 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
memset(&ctx, 0, sizeof(ctx));
ctx.prog = prog;
- ctx.arena_vm_start = bpf_arena_get_kern_vm_start(prog->aux->arena);
- ctx.user_vm_start = bpf_arena_get_user_vm_start(prog->aux->arena);
+ if (bpf_prog_uses_arena_insns(prog)) {
+ ctx.arena_vm_start = bpf_arena_get_kern_vm_start(prog->aux->arena);
+ ctx.user_vm_start = bpf_arena_get_user_vm_start(prog->aux->arena);
+ }
ctx.offset = kvcalloc(prog->len + 1, sizeof(u32), GFP_KERNEL);
if (ctx.offset == NULL)
diff --git a/arch/powerpc/net/bpf_jit_comp.c b/arch/powerpc/net/bpf_jit_comp.c
index 7a612688e7a2..0d6ea6df0106 100644
--- a/arch/powerpc/net/bpf_jit_comp.c
+++ b/arch/powerpc/net/bpf_jit_comp.c
@@ -241,8 +241,10 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
/* Make sure that the stack is quadword aligned. */
cgctx.stack_size = round_up(fp->aux->stack_depth, 16);
- cgctx.arena_vm_start = bpf_arena_get_kern_vm_start(fp->aux->arena);
- cgctx.user_vm_start = bpf_arena_get_user_vm_start(fp->aux->arena);
+ if (bpf_prog_uses_arena_insns(fp)) {
+ cgctx.arena_vm_start = bpf_arena_get_kern_vm_start(fp->aux->arena);
+ cgctx.user_vm_start = bpf_arena_get_user_vm_start(fp->aux->arena);
+ }
cgctx.is_subprog = bpf_is_subprog(fp);
cgctx.exception_boundary = fp->aux->exception_boundary;
cgctx.exception_cb = fp->aux->exception_cb;
diff --git a/arch/riscv/net/bpf_jit_core.c b/arch/riscv/net/bpf_jit_core.c
index 5265dd5bd39e..f2497ec7d378 100644
--- a/arch/riscv/net/bpf_jit_core.c
+++ b/arch/riscv/net/bpf_jit_core.c
@@ -73,8 +73,10 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr
goto skip_init_ctx;
}
- ctx->arena_vm_start = bpf_arena_get_kern_vm_start(prog->aux->arena);
- ctx->user_vm_start = bpf_arena_get_user_vm_start(prog->aux->arena);
+ if (bpf_prog_uses_arena_insns(prog)) {
+ ctx->arena_vm_start = bpf_arena_get_kern_vm_start(prog->aux->arena);
+ ctx->user_vm_start = bpf_arena_get_user_vm_start(prog->aux->arena);
+ }
stack_arg_cnt = bpf_out_stack_arg_cnt(env, prog);
/* First 3 stack args in regs, rest on stack */
diff --git a/arch/s390/net/bpf_jit_comp.c b/arch/s390/net/bpf_jit_comp.c
index 1b2566012b63..0fe629f0b66f 100644
--- a/arch/s390/net/bpf_jit_comp.c
+++ b/arch/s390/net/bpf_jit_comp.c
@@ -2293,10 +2293,11 @@ static int bpf_jit_prog(struct bpf_jit *jit, struct bpf_prog *fp,
else
jit->frame_off = 0;
- kern_arena = bpf_arena_get_kern_vm_start(fp->aux->arena);
- if (kern_arena)
+ if (bpf_prog_uses_arena_insns(fp)) {
+ kern_arena = bpf_arena_get_kern_vm_start(fp->aux->arena);
jit->kern_arena = _EMIT_CONST_U64(kern_arena);
- jit->user_arena = bpf_arena_get_user_vm_start(fp->aux->arena);
+ jit->user_arena = bpf_arena_get_user_vm_start(fp->aux->arena);
+ }
bpf_jit_prologue(jit, fp);
if (bpf_set_addr(jit, 0) < 0)
diff --git a/arch/x86/net/bpf_jit_comp.c b/arch/x86/net/bpf_jit_comp.c
index 083fcd6cf15b..3130d9ecbb99 100644
--- a/arch/x86/net/bpf_jit_comp.c
+++ b/arch/x86/net/bpf_jit_comp.c
@@ -878,7 +878,7 @@ static void emit_bpf_tail_call_indirect(struct bpf_prog *bpf_prog,
pop_r12(&prog);
} else {
pop_callee_regs(&prog, callee_regs_used);
- if (bpf_arena_get_kern_vm_start(bpf_prog->aux->arena))
+ if (bpf_prog_uses_arena_insns(bpf_prog))
pop_r12(&prog);
}
@@ -945,7 +945,7 @@ static void emit_bpf_tail_call_direct(struct bpf_prog *bpf_prog,
pop_r12(&prog);
} else {
pop_callee_regs(&prog, callee_regs_used);
- if (bpf_arena_get_kern_vm_start(bpf_prog->aux->arena))
+ if (bpf_prog_uses_arena_insns(bpf_prog))
pop_r12(&prog);
}
@@ -1980,7 +1980,7 @@ static int emit_kfunc_arena_args(struct bpf_prog *bpf_prog,
if (!(flags & BTF_FMODEL_ARENA_ARG))
continue;
- if (WARN_ON_ONCE(!bpf_prog->aux->arena))
+ if (WARN_ON_ONCE(!bpf_prog_uses_arena_insns(bpf_prog)))
return -EINVAL;
/* mov eN, eN: truncate and clear the upper 32 bits */
@@ -2011,7 +2011,7 @@ static int do_jit(struct bpf_verifier_env *env, struct bpf_prog *bpf_prog, int *
u8 temp[BPF_MAX_INSN_SIZE + BPF_INSN_SAFETY];
void __percpu *priv_frame_ptr = NULL;
u16 out_stack_arg_cnt, outgoing_rsp;
- u64 arena_vm_start, user_vm_start;
+ u64 arena_vm_start = 0, user_vm_start = 0;
void __percpu *priv_stack_ptr;
int i, excnt = 0;
int ilen, proglen = 0;
@@ -2051,8 +2051,10 @@ static int do_jit(struct bpf_verifier_env *env, struct bpf_prog *bpf_prog, int *
* [below] callee-saved regs
* [below] outgoing args 7+ (= rsp)
*/
- arena_vm_start = bpf_arena_get_kern_vm_start(bpf_prog->aux->arena);
- user_vm_start = bpf_arena_get_user_vm_start(bpf_prog->aux->arena);
+ if (bpf_prog_uses_arena_insns(bpf_prog)) {
+ arena_vm_start = bpf_arena_get_kern_vm_start(bpf_prog->aux->arena);
+ user_vm_start = bpf_arena_get_user_vm_start(bpf_prog->aux->arena);
+ }
detect_reg_usage(insn, insn_cnt, callee_regs_used);
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 54144372281c..b0a5b2d5b756 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -1883,6 +1883,8 @@ struct bpf_prog_aux {
bool changes_pkt_data;
bool might_sleep;
bool kprobe_write_ctx;
+ /* has insns that work with the arena base, see bpf_prog_uses_arena_insns() */
+ bool uses_arena_insns;
struct {
s32 keyring_serial;
u8 keyring_type;
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index eb9300221c4a..948daef06c7d 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -966,6 +966,8 @@ struct bpf_subprog_info {
bool keep_fastcall_stack: 1;
bool changes_pkt_data: 1;
bool might_sleep: 1;
+ /* true if an insn works with the arena base, see mark_uses_arena_insn() */
+ bool uses_arena_insns: 1;
u8 arg_slot_cnt:4;
enum priv_stack_mode priv_stack_mode;
diff --git a/include/linux/filter.h b/include/linux/filter.h
index 9339c6131f8f..f5518bae4855 100644
--- a/include/linux/filter.h
+++ b/include/linux/filter.h
@@ -1110,6 +1110,17 @@ static inline unsigned int bpf_prog_size(unsigned int proglen)
offsetof(struct bpf_prog, insns[proglen]));
}
+/*
+ * Whether a JIT has to set up the arena base for @prog. A program bound to
+ * an arena need not touch it, libbpf binds the arena of an object to every
+ * program in it. The verifier sets the flag for every function with an insn
+ * that works with the base, and only in a program that has an arena.
+ */
+static inline bool bpf_prog_uses_arena_insns(const struct bpf_prog *prog)
+{
+ return prog->aux->uses_arena_insns;
+}
+
static inline bool bpf_prog_was_classic(const struct bpf_prog *prog)
{
/* When classic BPF programs have been loaded and the arch
diff --git a/kernel/bpf/fixups.c b/kernel/bpf/fixups.c
index 3c95b6c8bbbc..a1f2cfefda76 100644
--- a/kernel/bpf/fixups.c
+++ b/kernel/bpf/fixups.c
@@ -1408,6 +1408,7 @@ static int jit_subprogs(struct bpf_verifier_env *env)
func[i]->aux->jited_linfo = prog->aux->jited_linfo;
func[i]->aux->linfo_idx = env->subprog_info[i].linfo_idx;
func[i]->aux->arena = prog->aux->arena;
+ func[i]->aux->uses_arena_insns = env->subprog_info[i].uses_arena_insns;
func[i]->aux->used_maps = env->used_maps;
func[i]->aux->used_map_cnt = env->used_map_cnt;
num_exentries = 0;
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index d9ba42b50d89..63dd5da0708f 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -5436,6 +5436,18 @@ static bool is_arena_scalar(struct bpf_verifier_env *env, int regno)
return reg->type == SCALAR_VALUE && env->arena_scalar && env->prog->aux->arena;
}
+/*
+ * The insn being verified works with the arena base, which a JIT sets up in
+ * the prologue of the function the insn belongs to, see
+ * bpf_prog_uses_arena_insns(). Every caller checked that the program has an
+ * arena, so uses_arena_insns implies one.
+ */
+static void mark_uses_arena_insn(struct bpf_verifier_env *env)
+{
+ bpf_find_containing_subprog(env, env->insn_idx)->uses_arena_insns = true;
+ env->prog->aux->uses_arena_insns = true;
+}
+
static bool is_load_acq_unsafe(struct bpf_verifier_env *env, int regno,
struct bpf_insn *insn)
{
@@ -7376,6 +7388,7 @@ static int check_mem_access(struct bpf_verifier_env *env, int insn_idx, struct b
if (!err && value_regno >= 0 && (rdonly_mem || t == BPF_READ))
mark_reg_unknown(env, regs, value_regno);
} else if (reg->type == PTR_TO_ARENA) {
+ mark_uses_arena_insn(env);
if (t == BPF_READ && value_regno >= 0)
mark_reg_unknown(env, regs, value_regno);
} else {
@@ -7435,6 +7448,7 @@ static struct bpf_reg_state *mem_access_reg(struct bpf_verifier_env *env, int re
memset(arena, 0, sizeof(*arena));
arena->type = PTR_TO_ARENA;
env->insn_aux_data[env->insn_idx].arena_scalar = true;
+ mark_uses_arena_insn(env);
return arena;
}
@@ -9924,6 +9938,9 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
}
break;
case ARG_PTR_TO_ARENA:
+ /* the JIT rebases the __arena argument of a kfunc on the arena base */
+ if (is_kfunc(meta))
+ mark_uses_arena_insn(env);
break;
case ARG_PTR_TO_ALLOC_BTF_ID:
if (reg->type == (PTR_TO_BTF_ID | MEM_ALLOC)) {
@@ -17535,6 +17552,7 @@ static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
verbose(env, "addr_space_cast insn can only be used in a program that has an associated arena\n");
return -EINVAL;
}
+ mark_uses_arena_insn(env);
}
/* check src operand */
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH bpf-next v2 11/13] libbpf: Render the arena type tag with its address space
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
` (9 preceding siblings ...)
2026-10-13 8:20 ` [PATCH bpf-next v2 10/13] bpf: Set up the arena base only in functions that use it Kumar Kartikeya Dwivedi
@ 2026-10-13 8:20 ` Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 12/13] selftests/bpf: Test the address space rendering of the arena type tag Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 13/13] selftests/bpf: Test an arena kfunc argument passed from a subprogram Kumar Kartikeya Dwivedi
12 siblings, 0 replies; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:20 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai, Tejun Heo, kkd,
kernel-team
A kernel struct member tagged __arena reaches a program built against
vmlinux.h as a pointer in the kernel address space with the tag as an
attribute, struct scx_cmask __attribute__((btf_type_tag("arena"))) *cmask,
so the program casts it to the arena address space before dereferencing
it, although the verifier already hands it an arena pointer.
The tag denotes a pointer into the BPF arena, address_space(1) to the
compiler, so render it as such: __attribute__((address_space(1)))
followed by the tag attribute as before, which keeps the dump faithful
to the BTF. A program then reads the member as an arena pointer and
hands it to anything declared with its own __arena without a cast.
Nothing else changes for consumers of the C dump. Skeletons strip the
modifiers of variable types and never carried the tag. A compiler
without casts between address spaces still accepts the declaration and
only a program touching such a member would need a cast there, as it
did for every arena pointer before.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
tools/lib/bpf/btf_dump.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tools/lib/bpf/btf_dump.c b/tools/lib/bpf/btf_dump.c
index 996e61b5f11f..7dedd3975c39 100644
--- a/tools/lib/bpf/btf_dump.c
+++ b/tools/lib/bpf/btf_dump.c
@@ -1595,6 +1595,9 @@ static void btf_dump_emit_type_chain(struct btf_dump *d,
name = btf_name_of(d, t->name_off);
if (btf_kflag(t))
btf_dump_printf(d, " __attribute__((%s))", name);
+ else if (strcmp(name, "arena") == 0)
+ /* the tag denotes a pointer into the BPF arena address space */
+ btf_dump_printf(d, " __attribute__((address_space(1))) __attribute__((btf_type_tag(\"arena\")))");
else
btf_dump_printf(d, " __attribute__((btf_type_tag(\"%s\")))", name);
break;
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH bpf-next v2 12/13] selftests/bpf: Test the address space rendering of the arena type tag
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
` (10 preceding siblings ...)
2026-10-13 8:20 ` [PATCH bpf-next v2 11/13] libbpf: Render the arena type tag with its address space Kumar Kartikeya Dwivedi
@ 2026-10-13 8:20 ` Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 13/13] selftests/bpf: Test an arena kfunc argument passed from a subprogram Kumar Kartikeya Dwivedi
12 siblings, 0 replies; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:20 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai, Tejun Heo, kkd,
kernel-team
Add an arena-tagged pointer member to the type tag dump test and expect
the address space attribute in front of the tag. The struct_ops test
that mirrors the previous rendering now describes it as the output of
an older libbpf.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
.../testing/selftests/bpf/prog_tests/btf_dump.c | 16 +++++++++++++---
.../bpf/progs/struct_ops_arena_member_render.c | 8 ++++----
2 files changed, 17 insertions(+), 7 deletions(-)
diff --git a/tools/testing/selftests/bpf/prog_tests/btf_dump.c b/tools/testing/selftests/bpf/prog_tests/btf_dump.c
index 17fa44cb9c91..35f214e254da 100644
--- a/tools/testing/selftests/bpf/prog_tests/btf_dump.c
+++ b/tools/testing/selftests/bpf/prog_tests/btf_dump.c
@@ -604,8 +604,10 @@ static void test_btf_dump_type_tags(void)
* struct s {
* void __attribute__((btf_type_tag(\"void_tag\"))) *p1;
* void __attribute__((void_attr)) *p2;
+ * void __arena *p3;
* };
*
+ * The arena tag is rendered with the address space it denotes.
*/
id = btf__add_type_tag(btf, "void_tag", 0);
@@ -618,17 +620,25 @@ static void test_btf_dump_type_tags(void)
id = btf__add_ptr(btf, id);
ASSERT_EQ(id, 4, "void_ptr_id2");
- id = btf__add_struct(btf, "s", 8);
- ASSERT_EQ(id, 5, "struct_id");
+ id = btf__add_type_tag(btf, "arena", 0);
+ ASSERT_EQ(id, 5, "arena_tag_id");
+ id = btf__add_ptr(btf, id);
+ ASSERT_EQ(id, 6, "void_ptr_id3");
+
+ id = btf__add_struct(btf, "s", 24);
+ ASSERT_EQ(id, 7, "struct_id");
err = btf__add_field(btf, "p1", 2, 0, 0);
ASSERT_OK(err, "field_ok1");
- err = btf__add_field(btf, "p2", 4, 0, 0);
+ err = btf__add_field(btf, "p2", 4, 64, 0);
ASSERT_OK(err, "field_ok2");
+ err = btf__add_field(btf, "p3", 6, 128, 0);
+ ASSERT_OK(err, "field_ok3");
test_ctx__dump_and_compare(&t,
"struct s {\n"
" void __attribute__((btf_type_tag(\"void_tag\"))) *p1;\n"
" void __attribute__((void_attr)) *p2;\n"
+" void __attribute__((address_space(1))) __attribute__((btf_type_tag(\"arena\"))) *p3;\n"
"};\n\n", "dump_and_compare");
test_ctx__free(&t);
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_render.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_render.c
index 0a6b7c5f494b..58516fdc1351 100644
--- a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_render.c
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_render.c
@@ -11,10 +11,10 @@
char _license[] SEC("license") = "GPL";
/*
- * The module types as vmlinux.h renders them: the tag is an attribute on a
- * pointer in the kernel address space, so the program has to cast the member
- * to the arena address space before using it. That is what a program built
- * against vmlinux.h sees for a kernel struct with an __arena member.
+ * The module types as a vmlinux.h generated by a libbpf that does not render
+ * the arena tag with its address space shows them: an attribute on a pointer
+ * in the kernel address space, so the program has to cast the member to the
+ * arena address space before using it.
*/
struct bpf_testmod_arena_member_args {
__u64 __attribute__((btf_type_tag("arena"))) *ptr;
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 22+ messages in thread
* [PATCH bpf-next v2 13/13] selftests/bpf: Test an arena kfunc argument passed from a subprogram
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
` (11 preceding siblings ...)
2026-10-13 8:20 ` [PATCH bpf-next v2 12/13] selftests/bpf: Test the address space rendering of the arena type tag Kumar Kartikeya Dwivedi
@ 2026-10-13 8:20 ` Kumar Kartikeya Dwivedi
12 siblings, 0 replies; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 8:20 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai, Tejun Heo, kkd,
kernel-team
Call a kfunc with an __arena argument from a global subprogram that does
nothing else with the arena, the address arrives as a number and no cast
is involved. The subprogram needs the arena base set up by its own
prologue for the JIT to rebase the argument, which the BPF CI review
found was not recorded for it.
Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
---
.../testing/selftests/bpf/progs/arena_kfunc.c | 37 +++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/arena_kfunc.c b/tools/testing/selftests/bpf/progs/arena_kfunc.c
index a049d6371d09..168ae8d7e303 100644
--- a/tools/testing/selftests/bpf/progs/arena_kfunc.c
+++ b/tools/testing/selftests/bpf/progs/arena_kfunc.c
@@ -192,6 +192,43 @@ int arena_arg_unpopulated(void *ctx)
return 0;
}
+/*
+ * A subprogram whose only arena use is the argument of a kfunc needs the
+ * arena base too, the JIT rebases the argument on it. The address arrives
+ * as a number, so no cast marks the subprogram either.
+ */
+__noinline int arena_arg_from_subprog(unsigned long addr)
+{
+ return bpf_kfunc_arena_arg_test((u64 *)addr) != 41;
+}
+
+SEC("syscall")
+__arch_x86_64
+__arch_arm64
+__success __retval(0)
+int arena_arg_subprog(void *ctx)
+{
+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST)
+ u64 __arena *val;
+ int ret;
+
+ val = bpf_arena_alloc_pages(&arena, NULL, 1, NUMA_NO_NODE, 0);
+ if (!val)
+ return 1;
+
+ *val = 41;
+ if (arena_arg_from_subprog((unsigned long)val))
+ ret = 2;
+ else
+ ret = *val != 42 ? 3 : 0;
+
+ bpf_arena_free_pages(&arena, (void __arena *)val, 1);
+ return ret;
+#else
+ return 0;
+#endif
+}
+
SEC("syscall")
__arch_x86_64
__arch_arm64
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 22+ messages in thread
* Re: [PATCH bpf-next v2 05/13] bpf: Turn loads of __arena pointer members into arena pointers
2026-10-13 8:20 ` [PATCH bpf-next v2 05/13] bpf: Turn loads of __arena pointer members into arena pointers Kumar Kartikeya Dwivedi
@ 2026-10-13 8:40 ` sashiko-bot
2026-10-13 9:04 ` Kumar Kartikeya Dwivedi
0 siblings, 1 reply; 22+ messages in thread
From: sashiko-bot @ 2026-10-13 8:40 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi, bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] bpf: PROBE_MEM fault bypass via non-nullable __arena pointers
--
commit fdb573c7b91be34110bf569fb46f6159ad42b9b8
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Turn loads of __arena pointer members into arena pointers
This commit translates the loads of __arena tagged pointer members in
kernel structs into arena pointers during verification. If the member
is also tagged as __nullable, a NULL test is kept before subtracting
the arena base address.
> diff --git a/kernel/bpf/fixups.c b/kernel/bpf/fixups.c
> index b2fa7e8bedf3a..3c95b6c8bbbc5 100644
> --- a/kernel/bpf/fixups.c
> +++ b/kernel/bpf/fixups.c
[ ... ]
> @@ -870,6 +870,36 @@ int bpf_opt_subreg_zext_lo32_rnd_hi32(struct bpf_verifier_env *env,
> * struct __sk_buff -> struct sk_buff
> * struct bpf_sock_ops -> struct sock
> */
> +/*
> + * A load through an __arena pointer member of a kernel struct reads the kernel
> + * address of arena memory. Append the conversion to the arena pointer form,
[ ... ]
> +static int arena_member_fixup(struct bpf_verifier_env *env, int idx, struct bpf_insn *insn,
> + struct bpf_insn *buf)
> +{
> + struct bpf_insn_aux_data *aux = &env->insn_aux_data[idx];
> + int cnt = 0;
> + u32 base;
> +
> + if (!aux->arena_member)
> + return 0;
> + if (aux->non_arena_load) {
> + verbose(env, "an insn loads an arena pointer member on one path and something else on another\n");
> + return -EINVAL;
> + }
> +
> + base = bpf_arena_get_kern_vm_start(env->prog->aux->arena);
> + buf[cnt++] = *insn;
> + if (aux->arena_member_nullable)
> + buf[cnt++] = BPF_JMP_IMM(BPF_JEQ, insn->dst_reg, 0, 1);
> + buf[cnt++] = BPF_ALU32_IMM(BPF_SUB, insn->dst_reg, (s32)base);
[Severity: High]
Could this allow a bypass of the PROBE_MEM zeroing mechanism when a fault
occurs on a non-nullable arena pointer?
If a PROBE_MEM read of a non-nullable __arena pointer faults, the exception
handler zeroes the destination register and resumes execution at the next
instruction. Because the pointer lacks __nullable, the zero-check branch
is omitted and the BPF_SUB instruction executes unconditionally.
This executes on the zeroed register, changing its value to -(u32)base.
Since base (kern_vm_start) is page-aligned but shifted by GUARD_SZ / 2 (32KB),
the truncated 32-bit offset calculates to 4GB - (base % 4GB). This
consistently points to the next 4GB-aligned address in the kernel.
Because base % 4GB is at least 32KB, this mathematically guarantees the
address falls strictly within the 4GB valid, user-mapped bounds of the arena.
Could an attacker map this arena offset, populate it with a malicious
payload, and spoof the BPF program into using attacker-controlled data?
> + return cnt;
> +}
> +
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261013082012.1107446-1-memxor@gmail.com?part=5
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH bpf-next v2 04/13] bpf: Recognize __nullable on pointer members of kernel structs
2026-10-13 8:19 ` [PATCH bpf-next v2 04/13] bpf: Recognize __nullable on pointer members of kernel structs Kumar Kartikeya Dwivedi
@ 2026-10-13 8:52 ` sashiko-bot
2026-10-13 9:19 ` Kumar Kartikeya Dwivedi
0 siblings, 1 reply; 22+ messages in thread
From: sashiko-bot @ 2026-10-13 8:52 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi, bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] bpf: MEM_PERCPU pointers are stripped of PTR_MAYBE_NULL flag causing Oops
--
commit 2e4273929b840decae1764fc52e2d82686a2b6c3
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Recognize __nullable on pointer members of kernel structs
This patch updates btf_struct_walk() to report the __nullable BTF type tag
on pointer members as PTR_MAYBE_NULL. The verifier is also modified to keep
the PTR_MAYBE_NULL flag on trusted and RCU pointers only, under the
assumption that untrusted results are probed anyway.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 2f190abe34489..a9c47441fba91 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -6721,6 +6721,13 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
> clear_trusted_flags(&flag);
> }
>
> + /*
> + * A __nullable member only matters where NULL would otherwise be
> + * assumed away, on trusted and RCU pointers; the rest are probed.
> + */
> + if (!(flag & (PTR_TRUSTED | MEM_RCU)))
> + flag &= ~PTR_MAYBE_NULL;
> +
[Severity: High]
Does this unintentionally strip the NULL-checking requirement from
MEM_PERCPU pointers?
Struct members annotated with __percpu __nullable will receive the
MEM_PERCPU | PTR_MAYBE_NULL flags from btf_struct_walk(), but lack
PTR_TRUSTED or MEM_RCU.
Because the pointer loses PTR_MAYBE_NULL here, does the BPF verifier no
longer force the BPF program to verify it against NULL before passing it to
helpers like bpf_per_cpu_ptr() or bpf_this_cpu_ptr()?
Since these helpers do not check for NULL internally, calling them with NULL
simply returns the base address of the CPU's per-CPU memory area. A
subsequent NULL check by the BPF program would then pass, which might allow
it to dereference the base of the per-CPU area and corrupt memory.
> if (atype == BPF_READ && value_regno >= 0) {
> ret = mark_btf_ld_reg(env, regs, value_regno, ret, reg->btf, btf_id, flag);
> if (ret < 0)
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261013082012.1107446-1-memxor@gmail.com?part=4
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH bpf-next v2 05/13] bpf: Turn loads of __arena pointer members into arena pointers
2026-10-13 8:40 ` sashiko-bot
@ 2026-10-13 9:04 ` Kumar Kartikeya Dwivedi
0 siblings, 0 replies; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 9:04 UTC (permalink / raw)
To: sashiko-reviews, bpf
On Tue Oct 13, 2026 at 10:40 AM CEST, sashiko-bot wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [High] bpf: PROBE_MEM fault bypass via non-nullable __arena pointers
> --
>
> commit fdb573c7b91be34110bf569fb46f6159ad42b9b8
> Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
>
> bpf: Turn loads of __arena pointer members into arena pointers
>
> This commit translates the loads of __arena tagged pointer members in
> kernel structs into arena pointers during verification. If the member
> is also tagged as __nullable, a NULL test is kept before subtracting
> the arena base address.
>
>> diff --git a/kernel/bpf/fixups.c b/kernel/bpf/fixups.c
>> index b2fa7e8bedf3a..3c95b6c8bbbc5 100644
>> --- a/kernel/bpf/fixups.c
>> +++ b/kernel/bpf/fixups.c
> [ ... ]
>> @@ -870,6 +870,36 @@ int bpf_opt_subreg_zext_lo32_rnd_hi32(struct bpf_verifier_env *env,
>> * struct __sk_buff -> struct sk_buff
>> * struct bpf_sock_ops -> struct sock
>> */
>> +/*
>> + * A load through an __arena pointer member of a kernel struct reads the kernel
>> + * address of arena memory. Append the conversion to the arena pointer form,
> [ ... ]
>> +static int arena_member_fixup(struct bpf_verifier_env *env, int idx, struct bpf_insn *insn,
>> + struct bpf_insn *buf)
>> +{
>> + struct bpf_insn_aux_data *aux = &env->insn_aux_data[idx];
>> + int cnt = 0;
>> + u32 base;
>> +
>> + if (!aux->arena_member)
>> + return 0;
>> + if (aux->non_arena_load) {
>> + verbose(env, "an insn loads an arena pointer member on one path and something else on another\n");
>> + return -EINVAL;
>> + }
>> +
>> + base = bpf_arena_get_kern_vm_start(env->prog->aux->arena);
>> + buf[cnt++] = *insn;
>> + if (aux->arena_member_nullable)
>> + buf[cnt++] = BPF_JMP_IMM(BPF_JEQ, insn->dst_reg, 0, 1);
>> + buf[cnt++] = BPF_ALU32_IMM(BPF_SUB, insn->dst_reg, (s32)base);
>
> [Severity: High]
> Could this allow a bypass of the PROBE_MEM zeroing mechanism when a fault
> occurs on a non-nullable arena pointer?
>
> If a PROBE_MEM read of a non-nullable __arena pointer faults, the exception
> handler zeroes the destination register and resumes execution at the next
> instruction. Because the pointer lacks __nullable, the zero-check branch
> is omitted and the BPF_SUB instruction executes unconditionally.
>
> This executes on the zeroed register, changing its value to -(u32)base.
> Since base (kern_vm_start) is page-aligned but shifted by GUARD_SZ / 2 (32KB),
> the truncated 32-bit offset calculates to 4GB - (base % 4GB). This
> consistently points to the next 4GB-aligned address in the kernel.
>
> Because base % 4GB is at least 32KB, this mathematically guarantees the
> address falls strictly within the 4GB valid, user-mapped bounds of the arena.
> Could an attacker map this arena offset, populate it with a malicious
> payload, and spoof the BPF program into using attacker-controlled data?
>
Valid, fixed and added a small test case.
>> + return cnt;
>> +}
>> +
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH bpf-next v2 08/13] libbpf: Bind the arena of an object to every program in it
2026-10-13 8:20 ` [PATCH bpf-next v2 08/13] libbpf: Bind the arena of an object to every program in it Kumar Kartikeya Dwivedi
@ 2026-10-13 9:14 ` bot+bpf-ci
0 siblings, 0 replies; 22+ messages in thread
From: bot+bpf-ci @ 2026-10-13 9:14 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, ihor.solodrai, tj, kkd,
kernel-team, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1975 bytes --]
> libbpf: Bind the arena of an object to every program in it
TL;DR: Nit only: the fd_array_used comment in struct bpf_object says slot 0
stays empty and no count is passed, which the new arena binding in libbpf.c
contradicts. Comment probably needs updating.
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
[ ... ]
> +static int bpf_object__bind_arena(struct bpf_object *obj, struct bpf_program *prog,
> + struct bpf_prog_load_opts *load_attr)
> +{
[ ... ]
> + if (!obj->fd_array_used) {
> + err = libbpf_ensure_mem((void **)&obj->fd_array, &obj->fd_array_cap, sizeof(int), 1);
> + if (err)
> + return err;
> + obj->fd_array_used = 1;
> + }
> + obj->fd_array[0] = map->fd;
> + load_attr->fd_array = obj->fd_array;
> + /* with slot 0 filled every slot handed out is valid, so the array can go with a count */
> + load_attr->fd_array_cnt = obj->fd_array_used;
> + return 0;
> +}
This isn't a bug, but is the comment on struct bpf_object's fd_array_used
still accurate?
/*
* Slots handed out so far, slot 0 included. A kfunc insn names a module
* BTF slot through its offset, slot 0 stands for vmlinux BTF and stays
* empty, so the array goes to the kernel without a count.
*/
size_t fd_array_used;
With this patch bpf_object__bind_arena() stores the arena fd in slot 0
(obj->fd_array[0] = map->fd) and hands the array to the kernel with a count
(load_attr->fd_array_cnt = obj->fd_array_used), including when the object
has no module BTFs at all.
For an object with an arena on a kernel with FEAT_FD_ARRAY_CNT, that
comment now describes the opposite of what the code does. Should it be
updated along with this new use of slot 0?
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/38370395582
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH bpf-next v2 07/13] selftests/bpf: Test __arena pointer members
2026-10-13 8:20 ` [PATCH bpf-next v2 07/13] selftests/bpf: Test __arena " Kumar Kartikeya Dwivedi
@ 2026-10-13 9:14 ` bot+bpf-ci
2026-10-13 9:19 ` Kumar Kartikeya Dwivedi
0 siblings, 1 reply; 22+ messages in thread
From: bot+bpf-ci @ 2026-10-13 9:14 UTC (permalink / raw)
To: memxor, bpf
Cc: ast, andrii, daniel, eddyz87, emil, ihor.solodrai, tj, kkd,
kernel-team, ast, andrii, daniel, martin.lau, eddyz87,
yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 3381 bytes --]
> selftests/bpf: Test __arena pointer members
TL;DR: Test issue in test_struct_ops_arena.c: if the bpf_testmod BTF lacks the
"arena" type tag, the new arena member tests are silently dropped,
test__skip() is never called, and the test still reports OK.
> diff --git a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
> --- a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
> +++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
[ ... ]
> +/*
> + * The tags on the members of struct bpf_testmod_arena_member_args reach the
> + * module BTF only from a toolchain with btf_type_tag support.
> + */
> +static bool testmod_has_type_tag(const char *tag)
> +{
> + struct btf *vmlinux_btf, *module_btf;
> + bool found = false;
> +
> + vmlinux_btf = btf__load_vmlinux_btf();
> + if (!ASSERT_OK_PTR(vmlinux_btf, "vmlinux_btf"))
> + return false;
> + module_btf = btf__load_module_btf("bpf_testmod", vmlinux_btf);
> + if (ASSERT_OK_PTR(module_btf, "module_btf"))
> + found = btf__find_by_name_kind(module_btf, tag, BTF_KIND_TYPE_TAG) > 0;
> + if (!found)
> + printf("%s:SKIP: no %s type tag in the bpf_testmod BTF\n", __func__, tag);
> + btf__free(module_btf);
> + btf__free(vmlinux_btf);
> + return found;
> +}
[ ... ]
> @@ -122,6 +201,18 @@ void serial_test_struct_ops_arena(void)
> arena_arg_fail();
> if (test__start_subtest("arena_arg_attach"))
> arena_arg_attach();
> + if (testmod_has_type_tag("arena")) {
> + if (test__start_subtest("arena_member"))
> + arena_member();
> + if (test__start_subtest("arena_member_render"))
> + arena_member_render();
> + RUN_TESTS(struct_ops_arena_member_fail__union);
> + RUN_TESTS(struct_ops_arena_member_fail__load_acq);
> + RUN_TESTS(struct_ops_arena_member_fail__mismatch);
> + RUN_TESTS(struct_ops_arena_member_fail__mismatch_mem);
> + RUN_TESTS(struct_ops_arena_member_fail__write);
> + RUN_TESTS(struct_ops_arena_member_fail__no_arena);
> + }
> #else
> test__skip();
> #endif
When the bpf_testmod BTF has no "arena" type tag, testmod_has_type_tag()
prints a SKIP line but never calls test__skip(). Is the skip reported
anywhere?
serial_test_struct_ops_arena() then leaves out the arena_member and
arena_member_render subtests and all six RUN_TESTS() groups, and the parent
test still reports OK. test_progs only prints a test's captured stdout when
the test fails or runs verbose (dump_test_log() uses print_test = verbose()
|| force_log || test_failed), so in a normal run the message is never shown
and the skip is never counted.
That seems realistic for kernels built with a toolchain that lacks
btf_type_tag support, where this coverage would be silently dropped.
prog_tests/btf_tag.c does the same check
(btf__find_by_name_kind(*module_btf, "user", BTF_KIND_TYPE_TAG) <= 0) and
pairs the printf with test__skip().
Could the arena_member and arena_member_render subtests still be started
unconditionally, calling test__skip() from inside each one when the tag is
missing? That would keep the arena_arg* results intact while making the
skip visible.
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/38370395582
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH bpf-next v2 04/13] bpf: Recognize __nullable on pointer members of kernel structs
2026-10-13 8:52 ` sashiko-bot
@ 2026-10-13 9:19 ` Kumar Kartikeya Dwivedi
0 siblings, 0 replies; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 9:19 UTC (permalink / raw)
To: sashiko-reviews, bpf
On Tue Oct 13, 2026 at 10:52 AM CEST, sashiko-bot wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [High] bpf: MEM_PERCPU pointers are stripped of PTR_MAYBE_NULL flag causing Oops
> --
>
> commit 2e4273929b840decae1764fc52e2d82686a2b6c3
> Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
>
> bpf: Recognize __nullable on pointer members of kernel structs
>
> This patch updates btf_struct_walk() to report the __nullable BTF type tag
> on pointer members as PTR_MAYBE_NULL. The verifier is also modified to keep
> the PTR_MAYBE_NULL flag on trusted and RCU pointers only, under the
> assumption that untrusted results are probed anyway.
>
>> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
>> index 2f190abe34489..a9c47441fba91 100644
>> --- a/kernel/bpf/verifier.c
>> +++ b/kernel/bpf/verifier.c
>
> [ ... ]
>
>> @@ -6721,6 +6721,13 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
>> clear_trusted_flags(&flag);
>> }
>>
>> + /*
>> + * A __nullable member only matters where NULL would otherwise be
>> + * assumed away, on trusted and RCU pointers; the rest are probed.
>> + */
>> + if (!(flag & (PTR_TRUSTED | MEM_RCU)))
>> + flag &= ~PTR_MAYBE_NULL;
>> +
>
> [Severity: High]
> Does this unintentionally strip the NULL-checking requirement from
> MEM_PERCPU pointers?
>
> Struct members annotated with __percpu __nullable will receive the
> MEM_PERCPU | PTR_MAYBE_NULL flags from btf_struct_walk(), but lack
> PTR_TRUSTED or MEM_RCU.
>
> Because the pointer loses PTR_MAYBE_NULL here, does the BPF verifier no
> longer force the BPF program to verify it against NULL before passing it to
> helpers like bpf_per_cpu_ptr() or bpf_this_cpu_ptr()?
>
> Since these helpers do not check for NULL internally, calling them with NULL
> simply returns the base address of the CPU's per-CPU memory area. A
> subsequent NULL check by the BPF program would then pass, which might allow
> it to dereference the base of the per-CPU area and corrupt memory.
>
Will just drop the stripping logic, I don't think it makes too much sense and
code is simpler without it.
>> if (atype == BPF_READ && value_regno >= 0) {
>> ret = mark_btf_ld_reg(env, regs, value_regno, ret, reg->btf, btf_id, flag);
>> if (ret < 0)
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH bpf-next v2 07/13] selftests/bpf: Test __arena pointer members
2026-10-13 9:14 ` bot+bpf-ci
@ 2026-10-13 9:19 ` Kumar Kartikeya Dwivedi
0 siblings, 0 replies; 22+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-13 9:19 UTC (permalink / raw)
To: bot+bpf-ci, bpf
Cc: ast, andrii, daniel, eddyz87, emil, ihor.solodrai, tj, kkd,
kernel-team, martin.lau, yonghong.song, mason
On Tue Oct 13, 2026 at 11:14 AM CEST, bot+bpf-ci wrote:
>> selftests/bpf: Test __arena pointer members
>
> TL;DR: Test issue in test_struct_ops_arena.c: if the bpf_testmod BTF lacks the
> "arena" type tag, the new arena member tests are silently dropped,
> test__skip() is never called, and the test still reports OK.
>
>> diff --git a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
>> --- a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
>> +++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
>
> [ ... ]
>
>> +/*
>> + * The tags on the members of struct bpf_testmod_arena_member_args reach the
>> + * module BTF only from a toolchain with btf_type_tag support.
>> + */
>> +static bool testmod_has_type_tag(const char *tag)
>> +{
>> + struct btf *vmlinux_btf, *module_btf;
>> + bool found = false;
>> +
>> + vmlinux_btf = btf__load_vmlinux_btf();
>> + if (!ASSERT_OK_PTR(vmlinux_btf, "vmlinux_btf"))
>> + return false;
>> + module_btf = btf__load_module_btf("bpf_testmod", vmlinux_btf);
>> + if (ASSERT_OK_PTR(module_btf, "module_btf"))
>> + found = btf__find_by_name_kind(module_btf, tag, BTF_KIND_TYPE_TAG) > 0;
>> + if (!found)
>> + printf("%s:SKIP: no %s type tag in the bpf_testmod BTF\n", __func__, tag);
>> + btf__free(module_btf);
>> + btf__free(vmlinux_btf);
>> + return found;
>> +}
>
> [ ... ]
>
>> @@ -122,6 +201,18 @@ void serial_test_struct_ops_arena(void)
>> arena_arg_fail();
>> if (test__start_subtest("arena_arg_attach"))
>> arena_arg_attach();
>> + if (testmod_has_type_tag("arena")) {
>> + if (test__start_subtest("arena_member"))
>> + arena_member();
>> + if (test__start_subtest("arena_member_render"))
>> + arena_member_render();
>> + RUN_TESTS(struct_ops_arena_member_fail__union);
>> + RUN_TESTS(struct_ops_arena_member_fail__load_acq);
>> + RUN_TESTS(struct_ops_arena_member_fail__mismatch);
>> + RUN_TESTS(struct_ops_arena_member_fail__mismatch_mem);
>> + RUN_TESTS(struct_ops_arena_member_fail__write);
>> + RUN_TESTS(struct_ops_arena_member_fail__no_arena);
>> + }
>> #else
>> test__skip();
>> #endif
>
> When the bpf_testmod BTF has no "arena" type tag, testmod_has_type_tag()
> prints a SKIP line but never calls test__skip(). Is the skip reported
> anywhere?
>
> serial_test_struct_ops_arena() then leaves out the arena_member and
> arena_member_render subtests and all six RUN_TESTS() groups, and the parent
> test still reports OK. test_progs only prints a test's captured stdout when
> the test fails or runs verbose (dump_test_log() uses print_test = verbose()
> || force_log || test_failed), so in a normal run the message is never shown
> and the skip is never counted.
>
> That seems realistic for kernels built with a toolchain that lacks
> btf_type_tag support, where this coverage would be silently dropped.
> prog_tests/btf_tag.c does the same check
> (btf__find_by_name_kind(*module_btf, "user", BTF_KIND_TYPE_TAG) <= 0) and
> pairs the printf with test__skip().
>
> Could the arena_member and arena_member_render subtests still be started
> unconditionally, calling test__skip() from inside each one when the tag is
> missing? That would keep the arena_arg* results intact while making the
> skip visible.
>
It's fine.
>
> ---
> AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
> See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
>
> CI run summary: https://github.com/kernel-patches/bpf/actions/runs/38370395582
^ permalink raw reply [flat|nested] 22+ messages in thread
* Re: [PATCH bpf-next v2 01/13] libbpf: Keep the fd_array fill index out of bpf_program__clone()
2026-10-13 8:19 ` [PATCH bpf-next v2 01/13] libbpf: Keep the fd_array fill index out of bpf_program__clone() Kumar Kartikeya Dwivedi
@ 2026-10-13 13:00 ` Mykyta Yatsenko
0 siblings, 0 replies; 22+ messages in thread
From: Mykyta Yatsenko @ 2026-10-13 13:00 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi, bpf
Cc: Mykyta Yatsenko, Alexei Starovoitov, Andrii Nakryiko,
Daniel Borkmann, Eduard Zingerman, Emil Tsalapatis, Ihor Solodrai,
Tejun Heo, kkd, kernel-team
On 10/13/26 9:19 AM, Kumar Kartikeya Dwivedi wrote:
> bpf_program__clone() hands the kernel the fd_array of the object along
> with obj->fd_array_cnt when the caller supplies neither. That counter is
> not a count the kernel can use: it is the index of the next free slot
> of an array built for the sparse fd_array API, where module BTF fds
> start at slot 1 and slot 0 stays empty, since a kfunc insn with offset
> 0 names vmlinux BTF rather than a slot. A non-zero fd_array_cnt turns
> the array into a continuous one, which the kernel resolves slot by
> slot, and slot 0 holds 0. Cloning a program from an object that calls a
> module kfunc thus fails with "fd 0 is not pointing to valid bpf_map or
> btf", for every program of the object, which is what veristat runs into
> on such an object. bpf_object__load() passes the array without a count.
>
> Do the same in bpf_program__clone(), leaving the count to callers who
> pass an array of their own, and rename the counter to fd_array_used so
> that it no longer reads like the uapi field.
>
> Fixes: 970bd2dced35 ("libbpf: Introduce bpf_program__clone()")
> Cc: Mykyta Yatsenko <yatsenko@meta.com>
> Signed-off-by: Kumar Kartikeya Dwivedi <memxor@gmail.com>
> ---
Acked-by: Mykyta Yatsenko <yatsenko@meta.com>
> tools/lib/bpf/libbpf.c | 21 +++++++++++++--------
> 1 file changed, 13 insertions(+), 8 deletions(-)
>
> diff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c
> index cb09ded90773..fcedb78abe00 100644
> --- a/tools/lib/bpf/libbpf.c
> +++ b/tools/lib/bpf/libbpf.c
> @@ -794,7 +794,12 @@ struct bpf_object {
>
> int *fd_array;
> size_t fd_array_cap;
> - size_t fd_array_cnt;
> + /*
> + * Slots handed out so far, slot 0 included. A kfunc insn names a module
> + * BTF slot through its offset, slot 0 stands for vmlinux BTF and stays
> + * empty, so the array goes to the kernel without a count.
> + */
> + size_t fd_array_used;
>
> struct usdt_manager *usdt_man;
>
> @@ -9716,22 +9721,22 @@ static int bpf_object__resolve_ksym_func_btf_id(struct bpf_object *obj,
> /* set index for module BTF fd in fd_array, if unset */
> if (mod_btf && !mod_btf->fd_array_idx) {
> /* insn->off is s16 */
> - if (obj->fd_array_cnt == INT16_MAX) {
> + if (obj->fd_array_used == INT16_MAX) {
> pr_warn("extern (func ksym) '%s': module BTF fd index %d too big to fit in bpf_insn offset\n",
> ext->name, mod_btf->fd_array_idx);
> return -E2BIG;
> }
> /* Cannot use index 0 for module BTF fd */
> - if (!obj->fd_array_cnt)
> - obj->fd_array_cnt = 1;
> + if (!obj->fd_array_used)
> + obj->fd_array_used = 1;
>
> ret = libbpf_ensure_mem((void **)&obj->fd_array, &obj->fd_array_cap, sizeof(int),
> - obj->fd_array_cnt + 1);
> + obj->fd_array_used + 1);
> if (ret)
> return ret;
> - mod_btf->fd_array_idx = obj->fd_array_cnt;
> + mod_btf->fd_array_idx = obj->fd_array_used;
> /* we assume module BTF FD is always >0 */
> - obj->fd_array[obj->fd_array_cnt++] = mod_btf->fd;
> + obj->fd_array[obj->fd_array_used++] = mod_btf->fd;
> }
>
> ext->is_set = true;
> @@ -10940,7 +10945,7 @@ int bpf_program__clone(struct bpf_program *prog, const struct bpf_prog_load_opts
> attr.prog_ifindex = OPTS_GET(opts, prog_ifindex, 0) ?: prog->prog_ifindex;
> attr.kern_version = OPTS_GET(opts, kern_version, 0) ?: obj->kern_version;
> attr.fd_array = OPTS_GET(opts, fd_array, NULL) ?: obj->fd_array;
> - attr.fd_array_cnt = OPTS_GET(opts, fd_array_cnt, 0) ?: obj->fd_array_cnt;
> + attr.fd_array_cnt = OPTS_GET(opts, fd_array_cnt, 0);
> attr.token_fd = OPTS_GET(opts, token_fd, 0) ?: obj->token_fd;
> if (attr.token_fd)
> attr.prog_flags |= BPF_F_TOKEN_FD;
^ permalink raw reply [flat|nested] 22+ messages in thread
end of thread, other threads:[~2026-10-13 13:00 UTC | newest]
Thread overview: 22+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-13 8:19 [PATCH bpf-next v2 00/13] Fix some arena paper cuts Kumar Kartikeya Dwivedi
2026-10-13 8:19 ` [PATCH bpf-next v2 01/13] libbpf: Keep the fd_array fill index out of bpf_program__clone() Kumar Kartikeya Dwivedi
2026-10-13 13:00 ` Mykyta Yatsenko
2026-10-13 8:19 ` [PATCH bpf-next v2 02/13] selftests/bpf: Clone a program that calls a module kfunc Kumar Kartikeya Dwivedi
2026-10-13 8:19 ` [PATCH bpf-next v2 03/13] compiler_types: Add __arena and __nullable BTF type tag qualifiers Kumar Kartikeya Dwivedi
2026-10-13 8:19 ` [PATCH bpf-next v2 04/13] bpf: Recognize __nullable on pointer members of kernel structs Kumar Kartikeya Dwivedi
2026-10-13 8:52 ` sashiko-bot
2026-10-13 9:19 ` Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 05/13] bpf: Turn loads of __arena pointer members into arena pointers Kumar Kartikeya Dwivedi
2026-10-13 8:40 ` sashiko-bot
2026-10-13 9:04 ` Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 06/13] selftests/bpf: Test __nullable pointer members Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 07/13] selftests/bpf: Test __arena " Kumar Kartikeya Dwivedi
2026-10-13 9:14 ` bot+bpf-ci
2026-10-13 9:19 ` Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 08/13] libbpf: Bind the arena of an object to every program in it Kumar Kartikeya Dwivedi
2026-10-13 9:14 ` bot+bpf-ci
2026-10-13 8:20 ` [PATCH bpf-next v2 09/13] selftests/bpf: Test arena binding by libbpf Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 10/13] bpf: Set up the arena base only in functions that use it Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 11/13] libbpf: Render the arena type tag with its address space Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 12/13] selftests/bpf: Test the address space rendering of the arena type tag Kumar Kartikeya Dwivedi
2026-10-13 8:20 ` [PATCH bpf-next v2 13/13] selftests/bpf: Test an arena kfunc argument passed from a subprogram Kumar Kartikeya Dwivedi
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.