BPF List
 help / color / mirror / Atom feed
* [PATCH bpf-next v1 00/10] Fix some arena paper cuts
@ 2026-10-12 13:36 Kumar Kartikeya Dwivedi
  2026-10-12 13:36 ` [PATCH bpf-next v1 01/10] compiler_types: Add __arena and __nullable BTF type tag qualifiers Kumar Kartikeya Dwivedi
                   ` (9 more replies)
  0 siblings, 10 replies; 20+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-12 13:36 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.

See commit logs for details.

Kumar Kartikeya Dwivedi (10):
  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

 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                         |  67 +++++++++++
 tools/lib/bpf/btf_dump.c                      |   3 +
 tools/lib/bpf/features.c                      |  40 +++++++
 tools/lib/bpf/libbpf.c                        |  37 ++++++
 tools/lib/bpf/libbpf_internal.h               |   2 +
 .../selftests/bpf/prog_tests/btf_dump.c       |  16 ++-
 .../bpf/prog_tests/test_struct_ops_arena.c    | 111 ++++++++++++++++++
 .../bpf/prog_tests/test_struct_ops_nullable.c |  43 +++++++
 .../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/struct_ops_arena_member.c       |  92 +++++++++++++++
 .../struct_ops_arena_member_fail__load_acq.c  |  38 ++++++
 .../struct_ops_arena_member_fail__mismatch.c  |  45 +++++++
 .../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 +
 37 files changed, 1095 insertions(+), 49 deletions(-)
 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__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] 20+ messages in thread

* [PATCH bpf-next v1 01/10] compiler_types: Add __arena and __nullable BTF type tag qualifiers
  2026-10-12 13:36 [PATCH bpf-next v1 00/10] Fix some arena paper cuts Kumar Kartikeya Dwivedi
@ 2026-10-12 13:36 ` Kumar Kartikeya Dwivedi
  2026-10-12 13:36 ` [PATCH bpf-next v1 02/10] bpf: Recognize __nullable on pointer members of kernel structs Kumar Kartikeya Dwivedi
                   ` (8 subsequent siblings)
  9 siblings, 0 replies; 20+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-12 13:36 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] 20+ messages in thread

* [PATCH bpf-next v1 02/10] bpf: Recognize __nullable on pointer members of kernel structs
  2026-10-12 13:36 [PATCH bpf-next v1 00/10] Fix some arena paper cuts Kumar Kartikeya Dwivedi
  2026-10-12 13:36 ` [PATCH bpf-next v1 01/10] compiler_types: Add __arena and __nullable BTF type tag qualifiers Kumar Kartikeya Dwivedi
@ 2026-10-12 13:36 ` Kumar Kartikeya Dwivedi
  2026-10-12 13:54   ` sashiko-bot
  2026-10-12 13:37 ` [PATCH bpf-next v1 03/10] bpf: Turn loads of __arena pointer members into arena pointers Kumar Kartikeya Dwivedi
                   ` (7 subsequent siblings)
  9 siblings, 1 reply; 20+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-12 13:36 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, as
does anything walked from a map pointer.

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 |  9 +++++++++
 2 files changed, 25 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..70fd44a6e1c5 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)
@@ -6785,6 +6792,8 @@ static int check_ptr_to_map_access(struct bpf_verifier_env *env,
 	ret = btf_struct_access(&env->log, &map_reg, off, size, atype, &btf_id, &flag, NULL);
 	if (ret < 0)
 		return ret;
+	/* Nothing walked from a map is trusted, a __nullable member changes nothing. */
+	flag &= ~PTR_MAYBE_NULL;
 
 	if (value_regno >= 0) {
 		ret = mark_btf_ld_reg(env, regs, value_regno, ret, btf_vmlinux, btf_id, flag);
-- 
2.53.0-Meta


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

* [PATCH bpf-next v1 03/10] bpf: Turn loads of __arena pointer members into arena pointers
  2026-10-12 13:36 [PATCH bpf-next v1 00/10] Fix some arena paper cuts Kumar Kartikeya Dwivedi
  2026-10-12 13:36 ` [PATCH bpf-next v1 01/10] compiler_types: Add __arena and __nullable BTF type tag qualifiers Kumar Kartikeya Dwivedi
  2026-10-12 13:36 ` [PATCH bpf-next v1 02/10] bpf: Recognize __nullable on pointer members of kernel structs Kumar Kartikeya Dwivedi
@ 2026-10-12 13:37 ` Kumar Kartikeya Dwivedi
  2026-10-12 14:00   ` sashiko-bot
  2026-10-12 14:27   ` bot+bpf-ci
  2026-10-12 13:37 ` [PATCH bpf-next v1 04/10] selftests/bpf: Test __nullable pointer members Kumar Kartikeya Dwivedi
                   ` (6 subsequent siblings)
  9 siblings, 2 replies; 20+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-12 13:37 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        | 41 +++++++++++++++++++++++++++++
 4 files changed, 125 insertions(+), 4 deletions(-)

diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 54cb6d3d0653..fb949b58c231 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 through a kernel struct pointer found, on any path: an
+	 * __arena pointer member, which arena_member_fixup() rewrites, one
+	 * that is __nullable as well, or any other member.
+	 */
+	u8 arena_member:1;
+	u8 arena_member_nullable:1;
+	u8 non_arena_member: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..bbb653df6b6f 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_member) {
+		verbose(env, "an insn loads an arena pointer member on one path and a different member 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 70fd44a6e1c5..a24a2bab802b 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,11 @@ 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 (atype == BPF_READ && value_regno >= 0)
+		env->insn_aux_data[env->insn_idx].non_arena_member = true;
+
 	if (ret != PTR_TO_BTF_ID) {
 		/* just mark; */
 
-- 
2.53.0-Meta


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

* [PATCH bpf-next v1 04/10] selftests/bpf: Test __nullable pointer members
  2026-10-12 13:36 [PATCH bpf-next v1 00/10] Fix some arena paper cuts Kumar Kartikeya Dwivedi
                   ` (2 preceding siblings ...)
  2026-10-12 13:37 ` [PATCH bpf-next v1 03/10] bpf: Turn loads of __arena pointer members into arena pointers Kumar Kartikeya Dwivedi
@ 2026-10-12 13:37 ` Kumar Kartikeya Dwivedi
  2026-10-12 14:27   ` bot+bpf-ci
  2026-10-12 13:37 ` [PATCH bpf-next v1 05/10] selftests/bpf: Test __arena " Kumar Kartikeya Dwivedi
                   ` (5 subsequent siblings)
  9 siblings, 1 reply; 20+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-12 13:37 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 | 43 ++++++++++++++++++
 .../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, 165 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..fc52870782b7
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_nullable.c
@@ -0,0 +1,43 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <test_progs.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. */
+void serial_test_struct_ops_nullable(void)
+{
+	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] 20+ messages in thread

* [PATCH bpf-next v1 05/10] selftests/bpf: Test __arena pointer members
  2026-10-12 13:36 [PATCH bpf-next v1 00/10] Fix some arena paper cuts Kumar Kartikeya Dwivedi
                   ` (3 preceding siblings ...)
  2026-10-12 13:37 ` [PATCH bpf-next v1 04/10] selftests/bpf: Test __nullable pointer members Kumar Kartikeya Dwivedi
@ 2026-10-12 13:37 ` Kumar Kartikeya Dwivedi
  2026-10-12 14:27   ` bot+bpf-ci
  2026-10-12 13:37 ` [PATCH bpf-next v1 06/10] libbpf: Bind the arena of an object to every program in it Kumar Kartikeya Dwivedi
                   ` (4 subsequent siblings)
  9 siblings, 1 reply; 20+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-12 13:37 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 through 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    | 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 +++++++++
 .../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 +
 11 files changed, 463 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__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..1a7fe0d6c5a4 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
@@ -5,6 +5,13 @@
 #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__write.skel.h"
+#include "struct_ops_arena_member_fail__no_arena.skel.h"
 
 #if defined(__x86_64__) || defined(__aarch64__)
 /*
@@ -101,6 +108,54 @@ 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);
+}
+
+/*
+ * 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 +177,15 @@ void serial_test_struct_ops_arena(void)
 		arena_arg_fail();
 	if (test__start_subtest("arena_arg_attach"))
 		arena_arg_attach();
+	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__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..88ed0fa7cc33
--- /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 a different member 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__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] 20+ messages in thread

* [PATCH bpf-next v1 06/10] libbpf: Bind the arena of an object to every program in it
  2026-10-12 13:36 [PATCH bpf-next v1 00/10] Fix some arena paper cuts Kumar Kartikeya Dwivedi
                   ` (4 preceding siblings ...)
  2026-10-12 13:37 ` [PATCH bpf-next v1 05/10] selftests/bpf: Test __arena " Kumar Kartikeya Dwivedi
@ 2026-10-12 13:37 ` Kumar Kartikeya Dwivedi
  2026-10-12 14:27   ` bot+bpf-ci
  2026-10-12 13:37 ` [PATCH bpf-next v1 07/10] selftests/bpf: Test arena binding by libbpf Kumar Kartikeya Dwivedi
                   ` (3 subsequent siblings)
  9 siblings, 1 reply; 20+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-12 13:37 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. 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          | 37 ++++++++++++++++++++++++++++++
 tools/lib/bpf/libbpf_internal.h |  2 ++
 3 files changed, 79 insertions(+)

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 = &map;
+	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 cb09ded90773..2360c8cc94d3 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -8799,6 +8799,38 @@ 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_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];
+	if (map->fd < 0)
+		return 0;
+
+	if (!obj->fd_array_cnt) {
+		err = libbpf_ensure_mem((void **)&obj->fd_array, &obj->fd_array_cap, sizeof(int), 1);
+		if (err)
+			return err;
+		obj->fd_array_cnt = 1;
+	}
+	obj->fd_array[0] = map->fd;
+	load_attr->fd_array = obj->fd_array;
+	load_attr->fd_array_cnt = obj->fd_array_cnt;
+	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)
@@ -8862,6 +8894,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, &load_attr);
+		if (err)
+			return err;
+	}
 
 	load_attr.token_fd = obj->token_fd;
 	if (obj->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] 20+ messages in thread

* [PATCH bpf-next v1 07/10] selftests/bpf: Test arena binding by libbpf
  2026-10-12 13:36 [PATCH bpf-next v1 00/10] Fix some arena paper cuts Kumar Kartikeya Dwivedi
                   ` (5 preceding siblings ...)
  2026-10-12 13:37 ` [PATCH bpf-next v1 06/10] libbpf: Bind the arena of an object to every program in it Kumar Kartikeya Dwivedi
@ 2026-10-12 13:37 ` Kumar Kartikeya Dwivedi
  2026-10-12 13:44   ` sashiko-bot
  2026-10-12 14:27   ` bot+bpf-ci
  2026-10-12 13:37 ` [PATCH bpf-next v1 08/10] bpf: Set up the arena base only in functions that use it Kumar Kartikeya Dwivedi
                   ` (2 subsequent siblings)
  9 siblings, 2 replies; 20+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-12 13:37 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    | 47 ++++++++++++++
 .../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, 152 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 1a7fe0d6c5a4..a45b360b4c65 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
@@ -12,6 +12,7 @@
 #include "struct_ops_arena_member_fail__mismatch.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__)
 /*
@@ -144,6 +145,50 @@ 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;
+	ASSERT_EQ(info.nr_map_ids, 1, name);
+	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)
 {
@@ -186,6 +231,8 @@ void serial_test_struct_ops_arena(void)
 	RUN_TESTS(struct_ops_arena_member_fail__mismatch);
 	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] 20+ messages in thread

* [PATCH bpf-next v1 08/10] bpf: Set up the arena base only in functions that use it
  2026-10-12 13:36 [PATCH bpf-next v1 00/10] Fix some arena paper cuts Kumar Kartikeya Dwivedi
                   ` (6 preceding siblings ...)
  2026-10-12 13:37 ` [PATCH bpf-next v1 07/10] selftests/bpf: Test arena binding by libbpf Kumar Kartikeya Dwivedi
@ 2026-10-12 13:37 ` Kumar Kartikeya Dwivedi
  2026-10-12 14:27   ` bot+bpf-ci
  2026-10-12 13:37 ` [PATCH bpf-next v1 09/10] libbpf: Render the arena type tag with its address space Kumar Kartikeya Dwivedi
  2026-10-12 13:37 ` [PATCH bpf-next v1 10/10] selftests/bpf: Test the address space rendering of the arena type tag Kumar Kartikeya Dwivedi
  9 siblings, 1 reply; 20+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-12 13:37 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           | 17 +++++++++++++++++
 11 files changed, 61 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 fb949b58c231..bb1541fd6799 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 bbb653df6b6f..75aefa5813e2 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 a24a2bab802b..9e4aedab71ed 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)
 {
@@ -7380,6 +7392,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 {
@@ -7439,6 +7452,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;
 }
 
@@ -13848,6 +13862,8 @@ get_kfunc_arg_type(struct bpf_verifier_env *env, struct bpf_call_arg_meta *meta,
 				reg_arg_name(env, argno));
 			return -EINVAL;
 		}
+		/* the JIT rebases the argument on the arena base */
+		mark_uses_arena_insn(env);
 		if (reg_from_argno(argno) < 0) {
 			verbose(env, "%s arena pointer cannot be a stack argument\n",
 				reg_arg_name(env, argno));
@@ -17528,6 +17544,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] 20+ messages in thread

* [PATCH bpf-next v1 09/10] libbpf: Render the arena type tag with its address space
  2026-10-12 13:36 [PATCH bpf-next v1 00/10] Fix some arena paper cuts Kumar Kartikeya Dwivedi
                   ` (7 preceding siblings ...)
  2026-10-12 13:37 ` [PATCH bpf-next v1 08/10] bpf: Set up the arena base only in functions that use it Kumar Kartikeya Dwivedi
@ 2026-10-12 13:37 ` Kumar Kartikeya Dwivedi
  2026-10-12 13:37 ` [PATCH bpf-next v1 10/10] selftests/bpf: Test the address space rendering of the arena type tag Kumar Kartikeya Dwivedi
  9 siblings, 0 replies; 20+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-12 13:37 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] 20+ messages in thread

* [PATCH bpf-next v1 10/10] selftests/bpf: Test the address space rendering of the arena type tag
  2026-10-12 13:36 [PATCH bpf-next v1 00/10] Fix some arena paper cuts Kumar Kartikeya Dwivedi
                   ` (8 preceding siblings ...)
  2026-10-12 13:37 ` [PATCH bpf-next v1 09/10] libbpf: Render the arena type tag with its address space Kumar Kartikeya Dwivedi
@ 2026-10-12 13:37 ` Kumar Kartikeya Dwivedi
  9 siblings, 0 replies; 20+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-10-12 13:37 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] 20+ messages in thread

* Re: [PATCH bpf-next v1 07/10] selftests/bpf: Test arena binding by libbpf
  2026-10-12 13:37 ` [PATCH bpf-next v1 07/10] selftests/bpf: Test arena binding by libbpf Kumar Kartikeya Dwivedi
@ 2026-10-12 13:44   ` sashiko-bot
  2026-10-12 14:27   ` bot+bpf-ci
  1 sibling, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-12 13:44 UTC (permalink / raw)
  To: Kumar Kartikeya Dwivedi; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] selftests/bpf: uninitialized stack memory read in assert_prog_map() on test failure
--

commit 2eeeb2c231d34a25c0bfb615692f8a717edbdc53
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>

selftests/bpf: Test arena binding by libbpf

This commit introduces a test to ensure libbpf correctly binds an arena
to all programs in an object, even if they don't explicitly refer to it.
It also relocates programs that were previously expected to fail without
an arena to a new object without an arena map to preserve their negative
tests.

> 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 1a7fe0d6c5a4c..a45b360b4c65d 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
[ ... ]
> @@ -144,6 +145,50 @@ 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;
> +	ASSERT_EQ(info.nr_map_ids, 1, name);
> +	ASSERT_EQ(map_ids[0], map_id, name);

[Severity: Low]
Does this read uninitialized stack memory on test failure?

If bpf_prog_get_info_by_fd() successfully returns but the program has 0 maps,
it will set info.nr_map_ids = 0 and leave the map_ids array unwritten.
Since ASSERT_EQ() does not abort the test on failure, execution will continue
to the next assertion, which evaluates map_ids[0].

Could we return early if info.nr_map_ids != 1, or initialize the map_ids
array to zero?

> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261012133711.4030594-1-memxor@gmail.com?part=7

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

* Re: [PATCH bpf-next v1 02/10] bpf: Recognize __nullable on pointer members of kernel structs
  2026-10-12 13:36 ` [PATCH bpf-next v1 02/10] bpf: Recognize __nullable on pointer members of kernel structs Kumar Kartikeya Dwivedi
@ 2026-10-12 13:54   ` sashiko-bot
  0 siblings, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-12 13:54 UTC (permalink / raw)
  To: Kumar Kartikeya Dwivedi; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] bpf: MEM_RCU map pointers bypass PROBE_MEM and NULL checks
--

commit cd9a005edc33a27f4ea8950cfa7c36fe9d72a985
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>

bpf: Recognize __nullable on pointer members of kernel structs

This patch teaches the BPF verifier to recognize the __nullable BTF type tag
on pointer members of kernel structs, assigning them the PTR_MAYBE_NULL flag.
The flag is kept on trusted and RCU pointers, forcing BPF programs to
explicitly check them before use.

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 2f190abe34489..70fd44a6e1c56 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c

[ ... ]

> @@ -6785,6 +6792,8 @@ static int check_ptr_to_map_access(struct bpf_verifier_env *env,
>  	ret = btf_struct_access(&env->log, &map_reg, off, size, atype, &btf_id, &flag, NULL);
>  	if (ret < 0)
>  		return ret;
> +	/* Nothing walked from a map is trusted, a __nullable member changes nothing. */
> +	flag &= ~PTR_MAYBE_NULL;
        ^^^^^^^^^^^^^^^^^^^^^^^^

[Severity: High]
Does stripping PTR_MAYBE_NULL here allow MEM_RCU map pointers to bypass both
PROBE_MEM and NULL checks?

When a CAP_PERFMON BPF program reads an __rcu-tagged pointer field from a map
structure (e.g., trie->root in struct lpm_trie), btf_struct_access() evaluates
the tag and returns MEM_RCU in flag.

If we explicitly clear PTR_MAYBE_NULL here without clearing MEM_RCU or adding
PTR_UNTRUSTED, the verifier assigns PTR_TO_BTF_ID | MEM_RCU to the destination
register.

Since bpf_may_fault_on_deref(PTR_TO_BTF_ID | MEM_RCU) evaluates to false (as it
expects exactly PTR_TO_BTF_ID or the PTR_UNTRUSTED flag), the load is not
rewritten to BPF_PROBE_MEM. Because PTR_MAYBE_NULL was stripped, the verifier
will permit the dereference without a NULL check.

At runtime, reading from a NULL pointer (like a NULL trie->root) would then
perform a bare memory load and crash the kernel. Does this active stripping
prevent the new __nullable tag from protecting these vulnerable pointers?

>  
>  	if (value_regno >= 0) {
>  		ret = mark_btf_ld_reg(env, regs, value_regno, ret, btf_vmlinux, btf_id, flag);
>  		if (ret < 0)
>  			return ret;

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261012133711.4030594-1-memxor@gmail.com?part=2

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

* Re: [PATCH bpf-next v1 03/10] bpf: Turn loads of __arena pointer members into arena pointers
  2026-10-12 13:37 ` [PATCH bpf-next v1 03/10] bpf: Turn loads of __arena pointer members into arena pointers Kumar Kartikeya Dwivedi
@ 2026-10-12 14:00   ` sashiko-bot
  2026-10-12 14:27   ` bot+bpf-ci
  1 sibling, 0 replies; 20+ messages in thread
From: sashiko-bot @ 2026-10-12 14:00 UTC (permalink / raw)
  To: Kumar Kartikeya Dwivedi; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] bpf: arena_member fixup corrupts PTR_TO_MEM scalar loads
--

commit 5458ef081d9f29cc60192d4b6fde6d8fd3989b75
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>

bpf: Turn loads of __arena pointer members into arena pointers

This commit translates loads of __arena pointer members in kernel structs
into arena pointers in the verifier. It appends a fixup to subtract the
kernel's virtual memory start address from the loaded value, and adds logic
to ensure paths disagreeing on member types are rejected.

> diff --git a/kernel/bpf/fixups.c b/kernel/bpf/fixups.c
> index b2fa7e8bedf3a..bbb653df6b6f0 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_member) {
> +		verbose(env, "an insn loads an arena pointer member on one path and a different member on another\n");
> +		return -EINVAL;
> +	}

[Severity: High]
Because non_arena_member might not be set by the PTR_TO_MEM path (as noted 
below), could this check fail to reject the mismatch?

If it fails to reject, arena_member_fixup() blindly appends a
SUB kern_vm_start instruction to the shared load. At runtime, if the
PTR_TO_MEM path executes, its scalar load would be corrupted by the
subtraction, potentially leading to out-of-bounds access or data corruption
if the result is used as an index or offset.

> +
> +	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;
> +}
[ ... ]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 70fd44a6e1c56..a24a2bab802b3 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -6661,6 +6697,11 @@ 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 (atype == BPF_READ && value_regno >= 0)
> +		env->insn_aux_data[env->insn_idx].non_arena_member = true;

[Severity: High]
Does this code miss setting the conflict flag for non-BTF pointers? 

The verifier's save_aux_ptr_type() permits sharing an instruction across 
branches where one path uses PTR_TO_BTF_ID and the other uses PTR_TO_MEM
(by merging them via allow_trust_mismatch). 

If the PTR_TO_BTF_ID path reads an __arena member, check_ptr_to_btf_access() 
sets aux->arena_member = true. However, when the PTR_TO_MEM path is
evaluated, check_mem_access() does not set aux->non_arena_member = true for 
memory pointers.

> +
>  	if (ret != PTR_TO_BTF_ID) {
>  		/* just mark; */
>

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261012133711.4030594-1-memxor@gmail.com?part=3

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

* Re: [PATCH bpf-next v1 04/10] selftests/bpf: Test __nullable pointer members
  2026-10-12 13:37 ` [PATCH bpf-next v1 04/10] selftests/bpf: Test __nullable pointer members Kumar Kartikeya Dwivedi
@ 2026-10-12 14:27   ` bot+bpf-ci
  0 siblings, 0 replies; 20+ messages in thread
From: bot+bpf-ci @ 2026-10-12 14:27 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: 3413 bytes --]

> selftests/bpf: Test __nullable pointer members

TL;DR: Test issue: on GCC-built kernels the "nullable" BTF tag is missing, so
the unchecked-pointer negative test struct_ops_nullable_fail would load and
fail instead of skipping; it should skip when the tag is absent.

> 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..fc52870782b7
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_nullable.c

[ ... ]

> +/* Serialized because it attaches the singleton bpf_testmod_ops3. */
> +void serial_test_struct_ops_nullable(void)
> +{
> +	if (test__start_subtest("nullable_member"))
> +		nullable_member();
> +	RUN_TESTS(struct_ops_nullable_fail);
> +}

Will struct_ops_nullable_fail fail instead of skip on kernels built with
GCC?

The "nullable" BTF type tag only reaches the bpf_testmod BTF when the
kernel is configured with CONFIG_PAHOLE_HAS_BTF_TAG, which depends on
CC_IS_CLANG:

    include/linux/compiler_types.h:
    #if defined(CONFIG_DEBUG_INFO_BTF) && defined(CONFIG_PAHOLE_HAS_BTF_TAG) && \
            __has_attribute(btf_type_tag) && !defined(__BINDGEN__)
    # define BTF_TYPE_TAG(value) __attribute__((btf_type_tag(#value)))
    #else
    # define BTF_TYPE_TAG(value) /* nothing */
    #endif
    ...
    # define __nullable	BTF_TYPE_TAG(nullable)

    lib/Kconfig.debug:
    config PAHOLE_HAS_BTF_TAG
            def_bool PAHOLE_VERSION >= 123
            depends on CC_IS_CLANG

On a GCC-configured kernel, "struct task_struct __nullable *task_or_null"
in bpf_testmod.h is then a plain pointer in the module BTF, so
btf_struct_walk() never sets PTR_MAYBE_NULL for it.

check_ptr_to_btf_access() treats args->task_or_null as a plain PTR_TRUSTED
because bpf_testmod_nullable_args__safe_trusted lists the member, and the
unchecked bpf_task_acquire() in unchecked_nullable() passes the verifier:

    t = bpf_task_acquire(args->task_or_null);

The program loads, so the __failure __msg("Possibly NULL pointer passed to
trusted") expectation reports a failure.

The nullable_member subtest still passes in that setup, since the callback
checks the pointer either way, so only the negative test breaks.
.github/scripts/matrix.py defaults kernel_compiler to Compiler.GCC, and the
x86_64, aarch64 and s390x configs there build with GCC and run test_progs
and test_progs_no_alu32.

Other tests that depend on module BTF type tags skip when the tag is
missing.  For example, load_btfs() in prog_tests/btf_tag.c does:

    /* skip the test if the module does not have __user tags */
    type_id = btf__find_by_name_kind(*module_btf, "user", BTF_KIND_TYPE_TAG);
    if (type_id <= 0) {
            printf("%s:SKIP: btf_type_tag attribute not in %s", __func__,
                   module_name);
            test__skip();

Should serial_test_struct_ops_nullable() look up a "nullable"
BTF_KIND_TYPE_TAG in the bpf_testmod BTF the same way, and skip
struct_ops_nullable_fail when it isn't there?

No later commit in the series (1ae1456b867d..00e951183327) adds a skip or a
DENYLIST entry for this test.


---
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/38258617362

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

* Re: [PATCH bpf-next v1 06/10] libbpf: Bind the arena of an object to every program in it
  2026-10-12 13:37 ` [PATCH bpf-next v1 06/10] libbpf: Bind the arena of an object to every program in it Kumar Kartikeya Dwivedi
@ 2026-10-12 14:27   ` bot+bpf-ci
  0 siblings, 0 replies; 20+ messages in thread
From: bot+bpf-ci @ 2026-10-12 14:27 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: 5052 bytes --]

> libbpf: Bind the arena of an object to every program in it

TL;DR:
- libbpf arena binding may make every program in an object fail to load
  (-EINVAL/-EACCES) when the arena isn't autocreated or is exclusive
- bpf_program__clone() may fail on loaded arena objects

> 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_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];
> +	if (map->fd < 0)
> +		return 0;

Is this check able to trigger?  bpf_object__add_map() always sets
map->fd = create_placeholder_fd(), and bpf_object__create_maps() skips maps
with !map->autocreate without closing or resetting that placeholder, so
map->fd is never negative here.

If the arena has autocreate set to false, the placeholder memfd ends up in
fd_array[0] with fd_array_cnt = 1.  On kernels with FEAT_FD_ARRAY_CNT,
process_fd_array_continuous() -> add_fd_from_fd_array() then rejects it:

    verbose(env, "fd %d is not pointing to valid bpf_map or btf\n", fd);
    return PTR_ERR(map);

and every program in the object fails to load with -EINVAL.  Before this
patch those programs loaded fine as long as they did not touch the arena.

This looks reachable from the in-tree selftests.  In unpriv mode,
test_loader.c calls:

    bpf_map__set_autocreate(map, !unpriv || is_unpriv_capable_map(map))

and is_unpriv_capable_map() returns false for BPF_MAP_TYPE_ARENA.
progs/verifier_ldsx.c declares an arena map next to SEC("socket") programs
marked __success_unpriv, so their unpriv runs would now fail when the object
is loaded.  Users who turn off the arena and the programs that use it, for
example on architectures whose JIT does not support arenas, would hit the
same failure.

Should the check be "if (!map->autocreate)", matching what
bpf_object__relocate_data() does when it poisons ldimm64 insns that refer to
a map that was not created?

> +
> +	if (!obj->fd_array_cnt) {
> +		err = libbpf_ensure_mem((void **)&obj->fd_array, &obj->fd_array_cap, sizeof(int), 1);
> +		if (err)
> +			return err;
> +		obj->fd_array_cnt = 1;
> +	}

After this, obj->fd_array_cnt is 1 for every object that has an arena.
bpf_object_post_load_cleanup() frees the array with zfree(&obj->fd_array)
but leaves obj->fd_array_cnt at 1.

bpf_program__clone() accepts objects in the OBJ_LOADED state and fills the
attributes from the object:

    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;

With obj->fd_array NULL and obj->fd_array_cnt 1, the kernel's
process_fd_array() rejects this with "fd_array_cnt %u without fd_array is
invalid" and returns -EINVAL.  Before this patch, an arena object without
module kfuncs kept fd_array_cnt at 0, so cloning one of its programs after
bpf_object__load() worked.

Can cloning a program from a loaded arena object still succeed on kernels
that support fd_array_cnt?

Related to this, bpf_program__clone() on a prepared object, which is how
veristat uses it, does not call bpf_object__bind_arena() at all.  Programs
that now load through bpf_object__load(), such as a struct_ops callback that
only touches __arena arguments, would still fail when loaded through clone.
Is that intended?

> +	obj->fd_array[0] = map->fd;
> +	load_attr->fd_array = obj->fd_array;
> +	load_attr->fd_array_cnt = obj->fd_array_cnt;

This binds the arena to every program, including programs that cannot accept
it.  One case libbpf can see for itself is an exclusive arena.  If the user
calls bpf_map__set_exclusive_program(arena, progA), bpf_object__create_map()
creates the arena with excl_prog_hash set to progA's hash.

Binding that arena to any other program reaches
__add_used_map() -> check_map_prog_compatibility(), which does:

    if (map->excl_prog_sha &&
        memcmp(map->excl_prog_sha, prog->digest, SHA256_DIGEST_SIZE)) {
            verbose(env, "program's hash doesn't match map's excl_prog_hash\n");
            return -EACCES;
    }

So every other program in the object, and with it bpf_object__load(), would
now fail with -EACCES, where programs that did not refer to the arena loaded
normally before.

check_map_prog_compatibility() also rejects hardware-offloaded programs
(prog_ifindex set) with "offload device mismatch between prog and map", and
binding the arena makes programs that never touch it require CAP_PERFMON and
an enabled JIT.

Should bpf_object__bind_arena() skip the binding when
"map->excl_prog && map->excl_prog != prog"?  It may need to skip it for
offloaded programs as well.


---
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/38258617362

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

* Re: [PATCH bpf-next v1 07/10] selftests/bpf: Test arena binding by libbpf
  2026-10-12 13:37 ` [PATCH bpf-next v1 07/10] selftests/bpf: Test arena binding by libbpf Kumar Kartikeya Dwivedi
  2026-10-12 13:44   ` sashiko-bot
@ 2026-10-12 14:27   ` bot+bpf-ci
  1 sibling, 0 replies; 20+ messages in thread
From: bot+bpf-ci @ 2026-10-12 14:27 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: 3661 bytes --]

> selftests/bpf: Test arena binding by libbpf

TL;DR: libbpf's arena binding may pass a placeholder memfd in fd_array when
the arena map has autocreate off, so loads could fail with -EINVAL;
verifier_ldsx @unpriv tests look affected but aren't in the changelog.

>     selftests/bpf: Test arena binding by libbpf
>
>     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>

The changelog lists two existing programs that stop failing once libbpf binds
the arena to every program.  Is that list complete?

At least one more existing test looks affected: the @unpriv variants in
progs/verifier_ldsx.c.  That object declares an arena map (under
__BPF_FEATURE_ADDR_SPACE_CAST) in the same object as socket programs marked
__success __success_unpriv (ldsx_s8, ldsx_s16, ldsx_s32, ldsx_s16_range,
ldsx_s32_range).

In unprivileged mode, test_loader turns off creation of maps that are not
unpriv-capable, and the arena is one of them:

    bpf_object__for_each_map(map, tobj)
        bpf_map__set_autocreate(map, !unpriv || is_unpriv_capable_map(map));

The arena map still holds the memfd placeholder that libbpf set up at open
time (bpf_object__add_map() does map->fd = create_placeholder_fd()), so
bpf_object__bind_arena() sees map->fd >= 0 and never looks at
map->autocreate:

    map = &obj->maps[obj->arena_map_idx];
    if (map->fd < 0)
        return 0;
    ...
    obj->fd_array[0] = map->fd;
    load_attr->fd_array = obj->fd_array;
    load_attr->fd_array_cnt = obj->fd_array_cnt;

Doesn't this hand the placeholder memfd to the kernel as a continuous
fd_array entry?

The kernel resolves every entry up front in
process_fd_array_continuous() -> add_fd_from_fd_array().  A memfd is neither
a map nor a BTF, so the load would fail with "fd %d is not pointing to valid
bpf_map or btf" (-EINVAL), while these tests expect success.

The selftests config leaves CONFIG_BPF_UNPRIV_DEFAULT_OFF unset, so subtests
such as "verifier_ldsx/LDSX, S8 @unpriv" run and would now fail.

The same applies to real libbpf users.  Any object whose arena map has
bpf_map__set_autocreate(map, false) could no longer load any of its
programs.

Should bpf_object__bind_arena() skip the arena when !map->autocreate, rather
than leaving this to the tests?  Should the changelog's list of affected
tests be extended, or should the libbpf side be fixed before this test
commit?  None of the later patches in the series (b94d4c47e39b,
a34d46a6be25, 00e951183327) appear to touch bpf_object__bind_arena().

A related point: verifier_ldsx.c is also built for __TARGET_ARCH_arm, whose
jit has no arena support.  With clang 18 or newer, its LDSX
socket/xdp/tc programs now get the arena bound in privileged mode too, so
would they fail with "JIT doesn't support arena"?  The same programs would
fail with "JIT is required to use arena" when the jit is disabled.


---
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/38258617362

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

* Re: [PATCH bpf-next v1 05/10] selftests/bpf: Test __arena pointer members
  2026-10-12 13:37 ` [PATCH bpf-next v1 05/10] selftests/bpf: Test __arena " Kumar Kartikeya Dwivedi
@ 2026-10-12 14:27   ` bot+bpf-ci
  0 siblings, 0 replies; 20+ messages in thread
From: bot+bpf-ci @ 2026-10-12 14:27 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: 2592 bytes --]

> selftests/bpf: Test __arena pointer members

TL;DR: Commit message only: it says a failure test covers "a store through the
member", but the test stores to the member itself; should it say "a store to
the member"?

> selftests/bpf: Test __arena pointer members
>
> 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 through 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>

The failure cases are described as covering "a store through the member", but
the test for it, struct_ops_arena_member_fail__write.c, stores to the member
itself:

    /* 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)
    {
        ...
        args->ptr = NULL;

A store through the member would be *args->ptr = v, which writes arena memory.
That is allowed, and the success callback in struct_ops_arena_member.c does
it:

    u64 __arena *ptr = args->ptr;
    ...
    *ptr += 1;

The earlier paragraph also says "Both increment the value through each
pointer".

As written, the failure-case sentence reads as if stores through the arena
pointer are rejected.  Should it say "a store to the member" instead?


---
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/38258617362

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

* Re: [PATCH bpf-next v1 03/10] bpf: Turn loads of __arena pointer members into arena pointers
  2026-10-12 13:37 ` [PATCH bpf-next v1 03/10] bpf: Turn loads of __arena pointer members into arena pointers Kumar Kartikeya Dwivedi
  2026-10-12 14:00   ` sashiko-bot
@ 2026-10-12 14:27   ` bot+bpf-ci
  1 sibling, 0 replies; 20+ messages in thread
From: bot+bpf-ci @ 2026-10-12 14:27 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: 3649 bytes --]

> bpf: Turn loads of __arena pointer members into arena pointers

TL;DR: The check meant to reject an insn loading an arena member on one path
and another member on a PTR_TO_MEM path may miss it, so path B could silently
load a wrong value (memory safety holds).

> diff --git a/kernel/bpf/fixups.c b/kernel/bpf/fixups.c
> index b2fa7e8bedf3..bbb653df6b6f 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,

[ ... ]

> +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_member) {
> +		verbose(env, "an insn loads an arena pointer member on one path and a different member on another\n");
> +		return -EINVAL;
> +	}

Does this check catch every path that loads a different member?

non_arena_member is only set from check_ptr_to_btf_access(), after the walk
returns something other than PTR_TO_ARENA:

    if (ret == PTR_TO_ARENA)
        return check_arena_member_load(env, regs, value_regno, flag, tname, field_name);
    if (atype == BPF_READ && value_regno >= 0)
        env->insn_aux_data[env->insn_idx].non_arena_member = true;

A plain BPF_LDX is verified with allow_trust_mismatch set, and
save_aux_ptr_type() merges PTR_TO_BTF_ID on one path with PTR_TO_MEM on
another:

    if (allow_trust_mismatch &&
        bpf_is_ptr_to_mem_or_btf_id(type) &&
        bpf_is_ptr_to_mem_or_btf_id(*prev_type)) {
        *prev_type = merge_ptr_types(type, *prev_type);

The PTR_TO_MEM path is handled in check_mem_access() by the
base_type(reg->type) == PTR_TO_MEM branch (check_mem_region_access() +
mark_reg_unknown()) and never reaches check_ptr_to_btf_access(), so it never
sets non_arena_member.

Consider a struct_ops program with an arena, where r6 is the trusted args
pointer on one path and PTR_TO_MEM on the other.  The second case can come
from a bpf_ringbuf_reserve() sample, bpf_rdonly_cast(p, 0), or a load of a
non-struct pointer member (WALK_PTR_UNTRUSTED -> PTR_TO_MEM | MEM_RDONLY |
PTR_UNTRUSTED).  Both paths reach:

    r0 = *(u64 *)(r6 + 0)

Path A sets arena_member.  Path B is merged into ptr_type as PTR_TO_MEM
(possibly with MEM_RDONLY | PTR_UNTRUSTED).  In bpf_convert_ctx_accesses()
that ptr_type goes to the default case, and arena_member_fixup() then sees
arena_member && !non_arena_member, so it appends:

    w0 -= lo32(kern_vm_start)

(and the JEQ for a __nullable member) to the shared insn.

At runtime the value loaded on path B is silently replaced with
(u32)(val - lo32(kern_vm_start)).  The verifier still models it as an unknown
scalar, so memory safety holds, but the program reads a wrong value and is
accepted, rather than rejected as the commit message describes:

> an insn that loaded an arena pointer member on one path and a different
> member on another is rejected by the fixup pass

Would it work to reject in arena_member_fixup() when base_type(aux->ptr_type)
!= PTR_TO_BTF_ID, since a merge with PTR_TO_MEM leaves PTR_TO_MEM there?
Alternatively, non_arena_member could be set for every BPF_READ with
value_regno >= 0 that does not go through check_ptr_to_btf_access().

No later patch in the series changes this.


---
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/38258617362

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

* Re: [PATCH bpf-next v1 08/10] bpf: Set up the arena base only in functions that use it
  2026-10-12 13:37 ` [PATCH bpf-next v1 08/10] bpf: Set up the arena base only in functions that use it Kumar Kartikeya Dwivedi
@ 2026-10-12 14:27   ` bot+bpf-ci
  0 siblings, 0 replies; 20+ messages in thread
From: bot+bpf-ci @ 2026-10-12 14:27 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: 6236 bytes --]

> bpf: Set up the arena base only in functions that use it

TL;DR: Kfunc __arena arg use is possibly marked on subprog 0 rather than the
calling subprog, so JIT of that subprog may fail, rejecting programs that
loaded before (WARN, panic with panic_on_warn).

> 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

[ ... ]

> @@ -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 */

[ ... ]

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index a24a2bab802b..9e4aedab71ed 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;
> +}
> +

[ ... ]

> @@ -13848,6 +13862,8 @@ get_kfunc_arg_type(struct bpf_verifier_env *env, struct bpf_call_arg_meta *meta,
>  				reg_arg_name(env, argno));
>  			return -EINVAL;
>  		}
> +		/* the JIT rebases the argument on the arena base */
> +		mark_uses_arena_insn(env);
>  		if (reg_from_argno(argno) < 0) {
>  			verbose(env, "%s arena pointer cannot be a stack argument\n",
>  				reg_arg_name(env, argno));

Can this record the arena use against the wrong function?

get_kfunc_arg_type() is not run once per call site.  Its only caller is
gen_kfunc_arg_proto(), and the only caller of that is bpf_add_kfunc_call():

    add_kfuncs()
      bpf_add_kfunc_call()
        gen_kfunc_arg_proto()
          get_kfunc_arg_type()
            mark_uses_arena_insn()

bpf_add_kfunc_call() builds one descriptor per kfunc and returns early
through find_kfunc_desc() when the descriptor already exists.  add_kfuncs()
runs from bpf_check() after add_subprogs()/check_subprogs() and before
do_check_main(), so env->insn_idx is still 0 at that point (env comes from
kvzalloc_obj(), and insn_idx is only set inside do_check_main(),
do_check_subprogs() and the gotox handling).

mark_uses_arena_insn() then does:

    bpf_find_containing_subprog(env, env->insn_idx)->uses_arena_insns = true;

which always marks subprog_info[0], and never the subprog that contains the
call.  jit_subprogs() copies the per-subprog flag:

    func[i]->aux->uses_arena_insns = env->subprog_info[i].uses_arena_insns;

so func[i] for i >= 1 has the flag clear when its only arena related insn is
the kfunc call.

ARG_PTR_TO_ARENA accepts SCALAR_VALUE, so a subprog can forward a number it
received as an argument without any arena access, addr_space_cast or
arena-scalar access of its own:

    static __noinline int cap(u64 v)
    {
        return bpf_kfunc_arena_cap_test((u64 *)v);
    }

    SEC("syscall") int prog(void *ctx)
    {
        stash = (u64)bpf_arena_alloc_pages(&arena, NULL, 1, NUMA_NO_NODE, 0);
        return cap(stash);
    }

The same applies to a static or global wrapper around scx_bpf_sub_grant(),
scx_bpf_sub_caps() or scx_bpf_cid_override().

The function model still carries BTF_FMODEL_ARENA_ARG for the argument, so
when func[1] is JITed:

    x86 emit_kfunc_arena_args():
        if (WARN_ON_ONCE(!bpf_prog_uses_arena_insns(bpf_prog)))
            return -EINVAL;

    arm64 emit_kfunc_arena_args():
        if (WARN_ON_ONCE(!ctx->arena_vm_start))   /* now 0 for func[1] */
            return -EINVAL;

do_jit() propagates the error, func[1]->jited stays 0, and jit_subprogs()
returns -ENOTSUPP.  Because kfunc calls set prog->jit_required,
bpf_fixup_call_args() then rejects the program ("program requires BPF JIT
compiler but it is not available"), or returns the JIT error under
CONFIG_BPF_JIT_ALWAYS_ON.

Before this patch the check was !bpf_prog->aux->arena, and
func[i]->aux->arena is set for every subprog, so this program loaded and
ran.  The WARN_ON_ONCE can be triggered by any CAP_BPF loader, so with
panic_on_warn this becomes a panic.

The same mistake also marks subprog 0, and so sets up r12/x28, even when the
main function never uses the arena, which works against what the patch is
trying to do.  The add_kfunc_in_insns() path from bpf_convert_ctx_accesses()
also calls mark_uses_arena_insn() with a stale env->insn_idx left over from
do_check.

The existing selftests (arena_kfunc.c, arena_kfunc_jit.c,
struct_ops_arena_member.c) only call these kfuncs from the main function,
where marking subprog 0 happens to be right, so they do not catch this.

The commit message says the verifier records "a kfunc call with an __arena
argument" per function, the comment above mark_uses_arena_insn() says "The
insn being verified", and the comment above bpf_prog_uses_arena_insns() in
filter.h says the verifier "sets the flag for every function with an insn
that works with the base".  None of these hold for this path.

Should the kfunc case be marked at the call site during do_check, for
example in check_kfunc_args() when an ARG_PTR_TO_ARENA argument is
processed, or in check_kfunc_call(), instead of when the prototype is built?

No later commit in the series (a34d46a6be25 "libbpf: Render the arena type
tag with its address space", 00e951183327 "selftests/bpf: Test the address
space rendering of the arena type tag") addresses this.


---
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/38258617362

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

end of thread, other threads:[~2026-10-12 14:27 UTC | newest]

Thread overview: 20+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-12 13:36 [PATCH bpf-next v1 00/10] Fix some arena paper cuts Kumar Kartikeya Dwivedi
2026-10-12 13:36 ` [PATCH bpf-next v1 01/10] compiler_types: Add __arena and __nullable BTF type tag qualifiers Kumar Kartikeya Dwivedi
2026-10-12 13:36 ` [PATCH bpf-next v1 02/10] bpf: Recognize __nullable on pointer members of kernel structs Kumar Kartikeya Dwivedi
2026-10-12 13:54   ` sashiko-bot
2026-10-12 13:37 ` [PATCH bpf-next v1 03/10] bpf: Turn loads of __arena pointer members into arena pointers Kumar Kartikeya Dwivedi
2026-10-12 14:00   ` sashiko-bot
2026-10-12 14:27   ` bot+bpf-ci
2026-10-12 13:37 ` [PATCH bpf-next v1 04/10] selftests/bpf: Test __nullable pointer members Kumar Kartikeya Dwivedi
2026-10-12 14:27   ` bot+bpf-ci
2026-10-12 13:37 ` [PATCH bpf-next v1 05/10] selftests/bpf: Test __arena " Kumar Kartikeya Dwivedi
2026-10-12 14:27   ` bot+bpf-ci
2026-10-12 13:37 ` [PATCH bpf-next v1 06/10] libbpf: Bind the arena of an object to every program in it Kumar Kartikeya Dwivedi
2026-10-12 14:27   ` bot+bpf-ci
2026-10-12 13:37 ` [PATCH bpf-next v1 07/10] selftests/bpf: Test arena binding by libbpf Kumar Kartikeya Dwivedi
2026-10-12 13:44   ` sashiko-bot
2026-10-12 14:27   ` bot+bpf-ci
2026-10-12 13:37 ` [PATCH bpf-next v1 08/10] bpf: Set up the arena base only in functions that use it Kumar Kartikeya Dwivedi
2026-10-12 14:27   ` bot+bpf-ci
2026-10-12 13:37 ` [PATCH bpf-next v1 09/10] libbpf: Render the arena type tag with its address space Kumar Kartikeya Dwivedi
2026-10-12 13:37 ` [PATCH bpf-next v1 10/10] selftests/bpf: Test the address space rendering of the arena type tag Kumar Kartikeya Dwivedi

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