AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/10/13 08:44 flow patch-triage
14m Args:
null

Results:
{
  "EnableConfigs": [
    "BPF_SYSCALL",
    "BPF_JIT",
    "DEBUG_INFO_BTF"
  ],
  "FocusSymbols": [
    "arena_member_fixup",
    "check_arena_member_load",
    "btf_struct_access",
    "btf_type_is_arena_ptr",
    "emit_kfunc_arena_args"
  ],
  "KMSANReasoning": "The patch introduces support for `__arena` and `__nullable` BTF type tags on kernel struct members accessed by BPF programs and optimizes JIT compilers across architectures to only set up the arena base registers when `bpf_prog_uses_arena_insns(prog)` is true.\n\nFrom a memory safety perspective:\n1. All newly added struct fields (`uses_arena_insns` in `struct bpf_prog_aux` and `struct bpf_subprog_info`, and `arena_member`, `arena_member_nullable`, and `non_arena_load` in `struct bpf_insn_aux_data`) are zero-initialized upon allocation via `kzalloc_obj`, `kvzalloc_obj`, or `__GFP_ZERO`.\n2. In the JIT compilers, the newly conditioned variables (`arena_vm_start` and `user_vm_start` in x86 `do_jit()`) are explicitly initialized to 0, and the context structures in arm64, s390, loongarch, powerpc, and riscv are zeroed via `memset` or `kcalloc`/`kzalloc` before inspection.\n3. In `arena_member_fixup()`, patched BPF instructions are fully constructed before insertion into the instruction stream.\n4. No kernel structures copied to user space, netlink attributes, ioctl buffers, or infoleak vectors are modified or introduced.\n\nAny potential bugs resulting from these changes (e.g., miscompilation in JIT passes, incorrect verifier state tracking, or invalid instruction rewriting) would manifest as verifier rejections, invalid memory dereferences, or incorrect execution logic—issues covered by standard functional testing and KASAN. There is no risk of uninitialized memory reads or information leaks that would require a dedicated KMSAN fuzzing session.",
  "NeedsKMSAN": false,
  "Reasoning": "The patch introduces support for __arena and __nullable BTF type tags on kernel struct members accessed by BPF programs. This touches core BPF verification logic (BTF struct walking and member load checking), BPF instruction rewriting (arena_member_fixup), and x86 JIT compilation (arena base tracking and emit_kfunc_arena_args assertions). All modified code is reachable via the bpf() syscall in standard virtualized environments.",
  "WorthFuzzing": true
}

1/1 2026/10/13 08:44 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit 7aedf27ed55ba9cc9fbee2220c02ac479c9b901e\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Tue Oct 13 08:44:52 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/arch/arm64/net/bpf_jit_comp.c b/arch/arm64/net/bpf_jit_comp.c\nindex 506c4201fec85..1d5f4cf88d331 100644\n--- a/arch/arm64/net/bpf_jit_comp.c\n+++ b/arch/arm64/net/bpf_jit_comp.c\n@@ -2284,8 +2284,10 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr\n \tif (ctx.offset == NULL)\n \t\tgoto out_off;\n \n-\tctx.user_vm_start = bpf_arena_get_user_vm_start(prog-\u003eaux-\u003earena);\n-\tctx.arena_vm_start = bpf_arena_get_kern_vm_start(prog-\u003eaux-\u003earena);\n+\tif (bpf_prog_uses_arena_insns(prog)) {\n+\t\tctx.user_vm_start = bpf_arena_get_user_vm_start(prog-\u003eaux-\u003earena);\n+\t\tctx.arena_vm_start = bpf_arena_get_kern_vm_start(prog-\u003eaux-\u003earena);\n+\t}\n \n \tout_cnt = bpf_out_stack_arg_cnt(env, prog);\n \tif (out_cnt) {\ndiff --git a/arch/loongarch/net/bpf_jit.c b/arch/loongarch/net/bpf_jit.c\nindex e78eb582c3005..9393e4b0db8e1 100644\n--- a/arch/loongarch/net/bpf_jit.c\n+++ b/arch/loongarch/net/bpf_jit.c\n@@ -2245,8 +2245,10 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr\n \n \tmemset(\u0026ctx, 0, sizeof(ctx));\n \tctx.prog = prog;\n-\tctx.arena_vm_start = bpf_arena_get_kern_vm_start(prog-\u003eaux-\u003earena);\n-\tctx.user_vm_start = bpf_arena_get_user_vm_start(prog-\u003eaux-\u003earena);\n+\tif (bpf_prog_uses_arena_insns(prog)) {\n+\t\tctx.arena_vm_start = bpf_arena_get_kern_vm_start(prog-\u003eaux-\u003earena);\n+\t\tctx.user_vm_start = bpf_arena_get_user_vm_start(prog-\u003eaux-\u003earena);\n+\t}\n \n \tctx.offset = kvcalloc(prog-\u003elen + 1, sizeof(u32), GFP_KERNEL);\n \tif (ctx.offset == NULL)\ndiff --git a/arch/powerpc/net/bpf_jit_comp.c b/arch/powerpc/net/bpf_jit_comp.c\nindex 7a612688e7a20..0d6ea6df01065 100644\n--- a/arch/powerpc/net/bpf_jit_comp.c\n+++ b/arch/powerpc/net/bpf_jit_comp.c\n@@ -241,8 +241,10 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr\n \n \t/* Make sure that the stack is quadword aligned. */\n \tcgctx.stack_size = round_up(fp-\u003eaux-\u003estack_depth, 16);\n-\tcgctx.arena_vm_start = bpf_arena_get_kern_vm_start(fp-\u003eaux-\u003earena);\n-\tcgctx.user_vm_start = bpf_arena_get_user_vm_start(fp-\u003eaux-\u003earena);\n+\tif (bpf_prog_uses_arena_insns(fp)) {\n+\t\tcgctx.arena_vm_start = bpf_arena_get_kern_vm_start(fp-\u003eaux-\u003earena);\n+\t\tcgctx.user_vm_start = bpf_arena_get_user_vm_start(fp-\u003eaux-\u003earena);\n+\t}\n \tcgctx.is_subprog = bpf_is_subprog(fp);\n \tcgctx.exception_boundary = fp-\u003eaux-\u003eexception_boundary;\n \tcgctx.exception_cb = fp-\u003eaux-\u003eexception_cb;\ndiff --git a/arch/riscv/net/bpf_jit_core.c b/arch/riscv/net/bpf_jit_core.c\nindex 5265dd5bd39e1..f2497ec7d3788 100644\n--- a/arch/riscv/net/bpf_jit_core.c\n+++ b/arch/riscv/net/bpf_jit_core.c\n@@ -73,8 +73,10 @@ struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_pr\n \t\tgoto skip_init_ctx;\n \t}\n \n-\tctx-\u003earena_vm_start = bpf_arena_get_kern_vm_start(prog-\u003eaux-\u003earena);\n-\tctx-\u003euser_vm_start = bpf_arena_get_user_vm_start(prog-\u003eaux-\u003earena);\n+\tif (bpf_prog_uses_arena_insns(prog)) {\n+\t\tctx-\u003earena_vm_start = bpf_arena_get_kern_vm_start(prog-\u003eaux-\u003earena);\n+\t\tctx-\u003euser_vm_start = bpf_arena_get_user_vm_start(prog-\u003eaux-\u003earena);\n+\t}\n \n \tstack_arg_cnt = bpf_out_stack_arg_cnt(env, prog);\n \t/* First 3 stack args in regs, rest on stack */\ndiff --git a/arch/s390/net/bpf_jit_comp.c b/arch/s390/net/bpf_jit_comp.c\nindex 1b2566012b63a..0fe629f0b66fc 100644\n--- a/arch/s390/net/bpf_jit_comp.c\n+++ b/arch/s390/net/bpf_jit_comp.c\n@@ -2293,10 +2293,11 @@ static int bpf_jit_prog(struct bpf_jit *jit, struct bpf_prog *fp,\n \telse\n \t\tjit-\u003eframe_off = 0;\n \n-\tkern_arena = bpf_arena_get_kern_vm_start(fp-\u003eaux-\u003earena);\n-\tif (kern_arena)\n+\tif (bpf_prog_uses_arena_insns(fp)) {\n+\t\tkern_arena = bpf_arena_get_kern_vm_start(fp-\u003eaux-\u003earena);\n \t\tjit-\u003ekern_arena = _EMIT_CONST_U64(kern_arena);\n-\tjit-\u003euser_arena = bpf_arena_get_user_vm_start(fp-\u003eaux-\u003earena);\n+\t\tjit-\u003euser_arena = bpf_arena_get_user_vm_start(fp-\u003eaux-\u003earena);\n+\t}\n \n \tbpf_jit_prologue(jit, fp);\n \tif (bpf_set_addr(jit, 0) \u003c 0)\ndiff --git a/arch/x86/net/bpf_jit_comp.c b/arch/x86/net/bpf_jit_comp.c\nindex 083fcd6cf15b7..3130d9ecbb994 100644\n--- a/arch/x86/net/bpf_jit_comp.c\n+++ b/arch/x86/net/bpf_jit_comp.c\n@@ -878,7 +878,7 @@ static void emit_bpf_tail_call_indirect(struct bpf_prog *bpf_prog,\n \t\tpop_r12(\u0026prog);\n \t} else {\n \t\tpop_callee_regs(\u0026prog, callee_regs_used);\n-\t\tif (bpf_arena_get_kern_vm_start(bpf_prog-\u003eaux-\u003earena))\n+\t\tif (bpf_prog_uses_arena_insns(bpf_prog))\n \t\t\tpop_r12(\u0026prog);\n \t}\n \n@@ -945,7 +945,7 @@ static void emit_bpf_tail_call_direct(struct bpf_prog *bpf_prog,\n \t\tpop_r12(\u0026prog);\n \t} else {\n \t\tpop_callee_regs(\u0026prog, callee_regs_used);\n-\t\tif (bpf_arena_get_kern_vm_start(bpf_prog-\u003eaux-\u003earena))\n+\t\tif (bpf_prog_uses_arena_insns(bpf_prog))\n \t\t\tpop_r12(\u0026prog);\n \t}\n \n@@ -1980,7 +1980,7 @@ static int emit_kfunc_arena_args(struct bpf_prog *bpf_prog,\n \n \t\tif (!(flags \u0026 BTF_FMODEL_ARENA_ARG))\n \t\t\tcontinue;\n-\t\tif (WARN_ON_ONCE(!bpf_prog-\u003eaux-\u003earena))\n+\t\tif (WARN_ON_ONCE(!bpf_prog_uses_arena_insns(bpf_prog)))\n \t\t\treturn -EINVAL;\n \n \t\t/* mov eN, eN: truncate and clear the upper 32 bits */\n@@ -2011,7 +2011,7 @@ static int do_jit(struct bpf_verifier_env *env, struct bpf_prog *bpf_prog, int *\n \tu8 temp[BPF_MAX_INSN_SIZE + BPF_INSN_SAFETY];\n \tvoid __percpu *priv_frame_ptr = NULL;\n \tu16 out_stack_arg_cnt, outgoing_rsp;\n-\tu64 arena_vm_start, user_vm_start;\n+\tu64 arena_vm_start = 0, user_vm_start = 0;\n \tvoid __percpu *priv_stack_ptr;\n \tint i, excnt = 0;\n \tint ilen, proglen = 0;\n@@ -2051,8 +2051,10 @@ static int do_jit(struct bpf_verifier_env *env, struct bpf_prog *bpf_prog, int *\n \t *   [below]             callee-saved regs\n \t *   [below]             outgoing args 7+ (= rsp)\n \t */\n-\tarena_vm_start = bpf_arena_get_kern_vm_start(bpf_prog-\u003eaux-\u003earena);\n-\tuser_vm_start = bpf_arena_get_user_vm_start(bpf_prog-\u003eaux-\u003earena);\n+\tif (bpf_prog_uses_arena_insns(bpf_prog)) {\n+\t\tarena_vm_start = bpf_arena_get_kern_vm_start(bpf_prog-\u003eaux-\u003earena);\n+\t\tuser_vm_start = bpf_arena_get_user_vm_start(bpf_prog-\u003eaux-\u003earena);\n+\t}\n \n \tdetect_reg_usage(insn, insn_cnt, callee_regs_used);\n \ndiff --git a/include/linux/bpf.h b/include/linux/bpf.h\nindex 54144372281cb..b0a5b2d5b7560 100644\n--- a/include/linux/bpf.h\n+++ b/include/linux/bpf.h\n@@ -1883,6 +1883,8 @@ struct bpf_prog_aux {\n \tbool changes_pkt_data;\n \tbool might_sleep;\n \tbool kprobe_write_ctx;\n+\t/* has insns that work with the arena base, see bpf_prog_uses_arena_insns() */\n+\tbool uses_arena_insns;\n \tstruct {\n \t\ts32 keyring_serial;\n \t\tu8 keyring_type;\ndiff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h\nindex 54cb6d3d06535..948daef06c7d4 100644\n--- a/include/linux/bpf_verifier.h\n+++ b/include/linux/bpf_verifier.h\n@@ -791,6 +791,14 @@ struct bpf_insn_aux_data {\n \t */\n \tu8 fastcall_spills_num:3;\n \tu8 arg_prog:4;\n+\t/*\n+\t * What a load found, on any path: an __arena pointer member of a\n+\t * kernel struct, which arena_member_fixup() rewrites, one that is\n+\t * __nullable as well, or anything else through any pointer.\n+\t */\n+\tu8 arena_member:1;\n+\tu8 arena_member_nullable:1;\n+\tu8 non_arena_load:1;\n \n \t/* below fields are initialized once */\n \tunsigned int orig_idx; /* original instruction index */\n@@ -958,6 +966,8 @@ struct bpf_subprog_info {\n \tbool keep_fastcall_stack: 1;\n \tbool changes_pkt_data: 1;\n \tbool might_sleep: 1;\n+\t/* true if an insn works with the arena base, see mark_uses_arena_insn() */\n+\tbool uses_arena_insns: 1;\n \tu8 arg_slot_cnt:4;\n \n \tenum priv_stack_mode priv_stack_mode;\ndiff --git a/include/linux/compiler_types.h b/include/linux/compiler_types.h\nindex c5921f1390079..e97ee2d4d1dfb 100644\n--- a/include/linux/compiler_types.h\n+++ b/include/linux/compiler_types.h\n@@ -51,6 +51,8 @@\n # define __iomem\t__attribute__((noderef, address_space(__iomem)))\n # define __percpu\t__attribute__((noderef, address_space(__percpu)))\n # define __rcu\t\t__attribute__((noderef, address_space(__rcu)))\n+# define __arena\n+# define __nullable\n static inline void __chk_user_ptr(const volatile void __user *ptr) { }\n static inline void __chk_io_ptr(const volatile void __iomem *ptr) { }\n /* other */\n@@ -70,6 +72,14 @@ static inline void __chk_io_ptr(const volatile void __iomem *ptr) { }\n # define __iomem\n # define __percpu\t__percpu_qual BTF_TYPE_TAG(percpu)\n # define __rcu\t\tBTF_TYPE_TAG(rcu)\n+/*\n+ * Qualifiers for the BPF verifier rather than address spaces: __arena marks\n+ * a pointer into a BPF arena, __nullable a pointer that may be NULL. They\n+ * reach BPF programs as BTF type tags on kernel struct members; to kernel\n+ * code the pointers are ordinary.\n+ */\n+# define __arena\tBTF_TYPE_TAG(arena)\n+# define __nullable\tBTF_TYPE_TAG(nullable)\n \n # define __chk_user_ptr(x)\t(void)0\n # define __chk_io_ptr(x)\t(void)0\ndiff --git a/include/linux/filter.h b/include/linux/filter.h\nindex 9339c6131f8ff..f5518bae48556 100644\n--- a/include/linux/filter.h\n+++ b/include/linux/filter.h\n@@ -1110,6 +1110,17 @@ static inline unsigned int bpf_prog_size(unsigned int proglen)\n \t\t   offsetof(struct bpf_prog, insns[proglen]));\n }\n \n+/*\n+ * Whether a JIT has to set up the arena base for @prog. A program bound to\n+ * an arena need not touch it, libbpf binds the arena of an object to every\n+ * program in it. The verifier sets the flag for every function with an insn\n+ * that works with the base, and only in a program that has an arena.\n+ */\n+static inline bool bpf_prog_uses_arena_insns(const struct bpf_prog *prog)\n+{\n+\treturn prog-\u003eaux-\u003euses_arena_insns;\n+}\n+\n static inline bool bpf_prog_was_classic(const struct bpf_prog *prog)\n {\n \t/* When classic BPF programs have been loaded and the arch\ndiff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c\nindex b09c04ccdc6db..b992027b60b02 100644\n--- a/kernel/bpf/btf.c\n+++ b/kernel/bpf/btf.c\n@@ -3557,7 +3557,8 @@ static int btf_type_tag_walk(const struct btf *btf,\n \treturn 0;\n }\n \n-bool btf_type_is_arena_ptr(const struct btf *btf, const struct btf_type *t)\n+/* Whether the pointee of pointer type @t carries the type tag @name */\n+static bool btf_ptr_has_type_tag(const struct btf *btf, const struct btf_type *t, const char *name)\n {\n \tif (!btf_type_is_ptr(t))\n \t\treturn false;\n@@ -3566,13 +3567,18 @@ bool btf_type_is_arena_ptr(const struct btf *btf, const struct btf_type *t)\n \t     t = btf_type_by_id(btf, t-\u003etype)) {\n \t\tif (!btf_type_is_type_tag(t) || btf_type_kflag(t))\n \t\t\tcontinue;\n-\t\tif (!strcmp(__btf_name_by_offset(btf, t-\u003ename_off), \"arena\"))\n+\t\tif (!strcmp(__btf_name_by_offset(btf, t-\u003ename_off), name))\n \t\t\treturn true;\n \t}\n \n \treturn false;\n }\n \n+bool btf_type_is_arena_ptr(const struct btf *btf, const struct btf_type *t)\n+{\n+\treturn btf_ptr_has_type_tag(btf, t, \"arena\");\n+}\n+\n static int btf_find_kptr(const struct btf *btf, const struct btf_type *t,\n \t\t\t u32 off, int sz, struct btf_field_info *info, u32 field_mask)\n {\n@@ -7689,6 +7695,7 @@ enum bpf_struct_walk_result {\n \tWALK_SCALAR = 0,\n \tWALK_PTR,\n \tWALK_PTR_UNTRUSTED,\n+\tWALK_PTR_ARENA,\n \tWALK_STRUCT,\n };\n \n@@ -7961,6 +7968,38 @@ static int btf_struct_walk(struct bpf_verifier_log *log, const struct btf *btf,\n \t\t\tid = ctx.id;\n \t\t\tstype = ctx.t;\n \n+\t\t\t/*\n+\t\t\t * __nullable combines with any of the above: it only says\n+\t\t\t * that the pointer may be NULL. The caller decides whether\n+\t\t\t * that matters for the register type it derives.\n+\t\t\t */\n+\t\t\tif (btf_ptr_has_type_tag(btf, mtype, \"nullable\"))\n+\t\t\t\ttmp_flag |= PTR_MAYBE_NULL;\n+\n+\t\t\t/*\n+\t\t\t * An __arena member of a kernel struct holds the kernel\n+\t\t\t * address of arena memory, which the verifier turns into\n+\t\t\t * an arena pointer on load. Program BTF is not affected:\n+\t\t\t * there the tag is what the compiler emits for the arena\n+\t\t\t * address space. An offset into a union resolves to its\n+\t\t\t * first member, so a program reading another member there\n+\t\t\t * would get an arena pointer it did not ask for; the walk\n+\t\t\t * marked such a union untrusted.\n+\t\t\t */\n+\t\t\tif (btf_is_kernel(btf) \u0026\u0026 btf_type_is_arena_ptr(btf, mtype)) {\n+\t\t\t\tif (*flag \u0026 PTR_UNTRUSTED) {\n+\t\t\t\t\tbpf_log(log,\n+\t\t\t\t\t\t\"arena pointer member %s of %s is inside a union\\n\",\n+\t\t\t\t\t\tmname, tname);\n+\t\t\t\t\treturn -EACCES;\n+\t\t\t\t}\n+\t\t\t\t*next_btf_id = id;\n+\t\t\t\t*flag |= tmp_flag \u0026 PTR_MAYBE_NULL;\n+\t\t\t\tif (field_name)\n+\t\t\t\t\t*field_name = mname;\n+\t\t\t\treturn WALK_PTR_ARENA;\n+\t\t\t}\n+\n \t\t\tif (btf_type_is_struct(stype)) {\n \t\t\t\t*next_btf_id = id;\n \t\t\t\t*flag |= tmp_flag;\n@@ -8063,6 +8102,11 @@ int btf_struct_access(struct bpf_verifier_log *log,\n \t\t\t*flag = MEM_RDONLY | PTR_UNTRUSTED;\n \t\t\tret = PTR_TO_MEM;\n \t\t\tgoto check_variable_offset;\n+\t\tcase WALK_PTR_ARENA:\n+\t\t\t*next_btf_id = id;\n+\t\t\t*flag = tmp_flag;\n+\t\t\tret = PTR_TO_ARENA;\n+\t\t\tgoto check_variable_offset;\n \t\tcase WALK_SCALAR:\n \t\t\tret = SCALAR_VALUE;\n \t\t\tgoto check_variable_offset;\ndiff --git a/kernel/bpf/fixups.c b/kernel/bpf/fixups.c\nindex b2fa7e8bedf3a..a1f2cfefda76a 100644\n--- a/kernel/bpf/fixups.c\n+++ b/kernel/bpf/fixups.c\n@@ -870,6 +870,36 @@ int bpf_opt_subreg_zext_lo32_rnd_hi32(struct bpf_verifier_env *env,\n  *     struct __sk_buff    -\u003e struct sk_buff\n  *     struct bpf_sock_ops -\u003e struct sock\n  */\n+/*\n+ * A load through an __arena pointer member of a kernel struct reads the kernel\n+ * address of arena memory. Append the conversion to the arena pointer form,\n+ * (u32)(kaddr - kern_vm_start), that the struct_ops trampoline applies to\n+ * __arena arguments. A __nullable member keeps NULL as NULL, tested on the\n+ * full kernel address. Returns the number of insns in @buf, 0 when the insn\n+ * loads no such member, or an error when it does so on some paths only.\n+ */\n+static int arena_member_fixup(struct bpf_verifier_env *env, int idx, struct bpf_insn *insn,\n+\t\t\t      struct bpf_insn *buf)\n+{\n+\tstruct bpf_insn_aux_data *aux = \u0026env-\u003einsn_aux_data[idx];\n+\tint cnt = 0;\n+\tu32 base;\n+\n+\tif (!aux-\u003earena_member)\n+\t\treturn 0;\n+\tif (aux-\u003enon_arena_load) {\n+\t\tverbose(env, \"an insn loads an arena pointer member on one path and something else on another\\n\");\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tbase = bpf_arena_get_kern_vm_start(env-\u003eprog-\u003eaux-\u003earena);\n+\tbuf[cnt++] = *insn;\n+\tif (aux-\u003earena_member_nullable)\n+\t\tbuf[cnt++] = BPF_JMP_IMM(BPF_JEQ, insn-\u003edst_reg, 0, 1);\n+\tbuf[cnt++] = BPF_ALU32_IMM(BPF_SUB, insn-\u003edst_reg, (s32)base);\n+\treturn cnt;\n+}\n+\n int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)\n {\n \tstruct bpf_subprog_info *subprogs = env-\u003esubprog_info;\n@@ -1093,11 +1123,23 @@ int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)\n \t\t\t\t\tinsn-\u003ecode = BPF_LDX | BPF_PROBE_MEMSX |\n \t\t\t\t\t\t     BPF_SIZE(insn-\u003ecode);\n \t\t\t\tenv-\u003eprog-\u003eaux-\u003enum_exentries++;\n-\t\t\t\tcontinue;\n-\t\t\t}\n-\t\t\tif (verifier_bug_if(bpf_may_fault_on_deref(ptr_type), env,\n-\t\t\t\t\t    \"access to a fault prone pointer is not rewritten as a probed one\"))\n+\t\t\t} else if (verifier_bug_if(bpf_may_fault_on_deref(ptr_type), env,\n+\t\t\t\t\t\t   \"access to a fault prone pointer is not rewritten as a probed one\")) {\n \t\t\t\treturn -EFAULT;\n+\t\t\t}\n+\n+\t\t\tcnt = arena_member_fixup(env, i + delta, insn, insn_buf);\n+\t\t\tif (cnt \u003c 0)\n+\t\t\t\treturn cnt;\n+\t\t\tif (!cnt)\n+\t\t\t\tcontinue;\n+\t\t\tnew_prog = bpf_patch_insn_data(env, i + delta, insn_buf, cnt);\n+\t\t\tif (!new_prog)\n+\t\t\t\treturn -ENOMEM;\n+\n+\t\t\tdelta    += cnt - 1;\n+\t\t\tenv-\u003eprog = new_prog;\n+\t\t\tinsn      = new_prog-\u003einsnsi + i + delta;\n \t\t\tcontinue;\n \t\t}\n \n@@ -1366,6 +1408,7 @@ static int jit_subprogs(struct bpf_verifier_env *env)\n \t\tfunc[i]-\u003eaux-\u003ejited_linfo = prog-\u003eaux-\u003ejited_linfo;\n \t\tfunc[i]-\u003eaux-\u003elinfo_idx = env-\u003esubprog_info[i].linfo_idx;\n \t\tfunc[i]-\u003eaux-\u003earena = prog-\u003eaux-\u003earena;\n+\t\tfunc[i]-\u003eaux-\u003euses_arena_insns = env-\u003esubprog_info[i].uses_arena_insns;\n \t\tfunc[i]-\u003eaux-\u003eused_maps = env-\u003eused_maps;\n \t\tfunc[i]-\u003eaux-\u003eused_map_cnt = env-\u003eused_map_cnt;\n \t\tnum_exentries = 0;\ndiff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c\nindex 2f190abe34489..63dd5da0708ff 100644\n--- a/kernel/bpf/verifier.c\n+++ b/kernel/bpf/verifier.c\n@@ -5436,6 +5436,18 @@ static bool is_arena_scalar(struct bpf_verifier_env *env, int regno)\n \treturn reg-\u003etype == SCALAR_VALUE \u0026\u0026 env-\u003earena_scalar \u0026\u0026 env-\u003eprog-\u003eaux-\u003earena;\n }\n \n+/*\n+ * The insn being verified works with the arena base, which a JIT sets up in\n+ * the prologue of the function the insn belongs to, see\n+ * bpf_prog_uses_arena_insns(). Every caller checked that the program has an\n+ * arena, so uses_arena_insns implies one.\n+ */\n+static void mark_uses_arena_insn(struct bpf_verifier_env *env)\n+{\n+\tbpf_find_containing_subprog(env, env-\u003einsn_idx)-\u003euses_arena_insns = true;\n+\tenv-\u003eprog-\u003eaux-\u003euses_arena_insns = true;\n+}\n+\n static bool is_load_acq_unsafe(struct bpf_verifier_env *env, int regno,\n \t\t\t       struct bpf_insn *insn)\n {\n@@ -6552,6 +6564,42 @@ static bool type_is_trusted_or_null(struct bpf_verifier_env *env,\n \t\t\t\t\t  \"__safe_trusted_or_null\");\n }\n \n+/*\n+ * The member holds the kernel address of arena memory, which the rewritten\n+ * load turns into an arena pointer, see arena_member_fixup(). NULL needs no\n+ * tracking, PTR_TO_ARENA covers it.\n+ */\n+static int check_arena_member_load(struct bpf_verifier_env *env, struct bpf_reg_state *regs,\n+\t\t\t\t   int value_regno, enum bpf_type_flag flag, const char *tname,\n+\t\t\t\t   const char *field_name)\n+{\n+\tstruct bpf_insn_aux_data *aux = \u0026env-\u003einsn_aux_data[env-\u003einsn_idx];\n+\n+\t/* a helper reading the member as bytes needs no rewrite */\n+\tif (value_regno \u003c 0)\n+\t\treturn 0;\n+\tif (!env-\u003eprog-\u003eaux-\u003earena) {\n+\t\tverbose(env,\n+\t\t\t\"arena pointer member %s of struct %s requires a program with an associated arena\\n\",\n+\t\t\tfield_name, tname);\n+\t\treturn -EINVAL;\n+\t}\n+\t/* only a plain load is rewritten */\n+\tif (BPF_CLASS(env-\u003eprog-\u003einsnsi[env-\u003einsn_idx].code) != BPF_LDX) {\n+\t\tverbose(env,\n+\t\t\t\"arena pointer member %s of struct %s can only be read with a plain load\\n\",\n+\t\t\tfield_name, tname);\n+\t\treturn -EACCES;\n+\t}\n+\n+\taux-\u003earena_member = true;\n+\tif (flag \u0026 PTR_MAYBE_NULL)\n+\t\taux-\u003earena_member_nullable = true;\n+\tmark_reg_unknown(env, regs, value_regno);\n+\tregs[value_regno].type = PTR_TO_ARENA;\n+\treturn 0;\n+}\n+\n static int check_ptr_to_btf_access(struct bpf_verifier_env *env,\n \t\t\t\t   struct bpf_reg_state *regs, struct bpf_reg_state *reg,\n \t\t\t\t   argno_t argno, int off, int size,\n@@ -6661,6 +6709,9 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,\n \tif (ret \u003c 0)\n \t\treturn ret;\n \n+\tif (ret == PTR_TO_ARENA)\n+\t\treturn check_arena_member_load(env, regs, value_regno, flag, tname, field_name);\n+\n \tif (ret != PTR_TO_BTF_ID) {\n \t\t/* just mark; */\n \n@@ -6721,6 +6772,13 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,\n \t\tclear_trusted_flags(\u0026flag);\n \t}\n \n+\t/*\n+\t * A __nullable member only matters where NULL would otherwise be\n+\t * assumed away, on trusted and RCU pointers; the rest are probed.\n+\t */\n+\tif (!(flag \u0026 (PTR_TRUSTED | MEM_RCU)))\n+\t\tflag \u0026= ~PTR_MAYBE_NULL;\n+\n \tif (atype == BPF_READ \u0026\u0026 value_regno \u003e= 0) {\n \t\tret = mark_btf_ld_reg(env, regs, value_regno, ret, reg-\u003ebtf, btf_id, flag);\n \t\tif (ret \u003c 0)\n@@ -7330,6 +7388,7 @@ static int check_mem_access(struct bpf_verifier_env *env, int insn_idx, struct b\n \t\tif (!err \u0026\u0026 value_regno \u003e= 0 \u0026\u0026 (rdonly_mem || t == BPF_READ))\n \t\t\tmark_reg_unknown(env, regs, value_regno);\n \t} else if (reg-\u003etype == PTR_TO_ARENA) {\n+\t\tmark_uses_arena_insn(env);\n \t\tif (t == BPF_READ \u0026\u0026 value_regno \u003e= 0)\n \t\t\tmark_reg_unknown(env, regs, value_regno);\n \t} else {\n@@ -7389,6 +7448,7 @@ static struct bpf_reg_state *mem_access_reg(struct bpf_verifier_env *env, int re\n \tmemset(arena, 0, sizeof(*arena));\n \tarena-\u003etype = PTR_TO_ARENA;\n \tenv-\u003einsn_aux_data[env-\u003einsn_idx].arena_scalar = true;\n+\tmark_uses_arena_insn(env);\n \treturn arena;\n }\n \n@@ -7432,6 +7492,17 @@ static int check_load_mem(struct bpf_verifier_env *env, struct bpf_insn *insn,\n \terr = check_mem_access(env, env-\u003einsn_idx, src_reg, argno_from_reg(insn-\u003esrc_reg),\n \t\t\t       insn-\u003eoff, BPF_SIZE(insn-\u003ecode), BPF_READ, insn-\u003edst_reg,\n \t\t\t       strict_alignment_once, is_ldsx);\n+\t/*\n+\t * Paths through a kernel struct pointer and through other pointers\n+\t * may share the insn, see save_aux_ptr_type(). Record a load that did\n+\t * not produce an arena pointer from an __arena member, the only load\n+\t * through a kernel struct pointer that does, next to the one\n+\t * check_arena_member_load() records, so that arena_member_fixup() can\n+\t * tell when the paths disagree.\n+\t */\n+\tif (!err \u0026\u0026 !(base_type(src_reg_type) == PTR_TO_BTF_ID \u0026\u0026\n+\t\t      regs[insn-\u003edst_reg].type == PTR_TO_ARENA))\n+\t\tenv-\u003einsn_aux_data[env-\u003einsn_idx].non_arena_load = true;\n \terr = err ?: save_aux_ptr_type(env, src_reg_type,\n \t\t\t\t       allow_trust_mismatch);\n \terr = err ?: reg_bounds_sanity_check(env, \u0026regs[insn-\u003edst_reg], ctx);\n@@ -9867,6 +9938,9 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p\n \t\t}\n \t\tbreak;\n \tcase ARG_PTR_TO_ARENA:\n+\t\t/* the JIT rebases the __arena argument of a kfunc on the arena base */\n+\t\tif (is_kfunc(meta))\n+\t\t\tmark_uses_arena_insn(env);\n \t\tbreak;\n \tcase ARG_PTR_TO_ALLOC_BTF_ID:\n \t\tif (reg-\u003etype == (PTR_TO_BTF_ID | MEM_ALLOC)) {\n@@ -17478,6 +17552,7 @@ static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)\n \t\t\t\t\tverbose(env, \"addr_space_cast insn can only be used in a program that has an associated arena\\n\");\n \t\t\t\t\treturn -EINVAL;\n \t\t\t\t}\n+\t\t\t\tmark_uses_arena_insn(env);\n \t\t\t}\n \n \t\t\t/* check src operand */\ndiff --git a/tools/lib/bpf/btf_dump.c b/tools/lib/bpf/btf_dump.c\nindex 996e61b5f11f0..7dedd3975c395 100644\n--- a/tools/lib/bpf/btf_dump.c\n+++ b/tools/lib/bpf/btf_dump.c\n@@ -1595,6 +1595,9 @@ static void btf_dump_emit_type_chain(struct btf_dump *d,\n \t\t\tname = btf_name_of(d, t-\u003ename_off);\n \t\t\tif (btf_kflag(t))\n \t\t\t\tbtf_dump_printf(d, \" __attribute__((%s))\", name);\n+\t\t\telse if (strcmp(name, \"arena\") == 0)\n+\t\t\t\t/* the tag denotes a pointer into the BPF arena address space */\n+\t\t\t\tbtf_dump_printf(d, \" __attribute__((address_space(1))) __attribute__((btf_type_tag(\\\"arena\\\")))\");\n \t\t\telse\n \t\t\t\tbtf_dump_printf(d, \" __attribute__((btf_type_tag(\\\"%s\\\")))\", name);\n \t\t\tbreak;\ndiff --git a/tools/lib/bpf/features.c b/tools/lib/bpf/features.c\nindex 9f589980beef6..71c6a666afcb2 100644\n--- a/tools/lib/bpf/features.c\n+++ b/tools/lib/bpf/features.c\n@@ -77,6 +77,43 @@ static int probe_kern_global_data(int token_fd)\n \treturn probe_fd(ret);\n }\n \n+/*\n+ * A continuous fd_array binds every map in it to the program, whether an\n+ * insn refers to the map or not. Kernels without the attribute reject a\n+ * non-zero fd_array_cnt.\n+ */\n+static int probe_kern_fd_array_cnt(int token_fd)\n+{\n+\tstruct bpf_insn insns[] = {\n+\t\tBPF_MOV64_IMM(BPF_REG_0, 0),\n+\t\tBPF_EXIT_INSN(),\n+\t};\n+\tLIBBPF_OPTS(bpf_map_create_opts, map_opts,\n+\t\t.token_fd = token_fd,\n+\t\t.map_flags = token_fd ? BPF_F_TOKEN_FD : 0,\n+\t);\n+\tLIBBPF_OPTS(bpf_prog_load_opts, prog_opts,\n+\t\t.token_fd = token_fd,\n+\t\t.prog_flags = token_fd ? BPF_F_TOKEN_FD : 0,\n+\t\t.fd_array_cnt = 1,\n+\t);\n+\tint ret, map, insn_cnt = ARRAY_SIZE(insns);\n+\n+\tmap = bpf_map_create(BPF_MAP_TYPE_ARRAY, \"libbpf_det_bind\", sizeof(int), sizeof(int), 1,\n+\t\t\t     \u0026map_opts);\n+\tif (map \u003c 0) {\n+\t\tret = -errno;\n+\t\tpr_warn(\"Error in %s(): %s. Couldn't create simple array map.\\n\",\n+\t\t\t__func__, errstr(ret));\n+\t\treturn ret;\n+\t}\n+\n+\tprog_opts.fd_array = \u0026map;\n+\tret = bpf_prog_load(BPF_PROG_TYPE_SOCKET_FILTER, NULL, \"GPL\", insns, insn_cnt, \u0026prog_opts);\n+\tclose(map);\n+\treturn probe_fd(ret);\n+}\n+\n static int probe_kern_btf(int token_fd)\n {\n \tstatic const char strs[] = \"\\0int\";\n@@ -742,6 +779,9 @@ static struct kern_feature_desc {\n \t[FEAT_PERCPU_DATA] = {\n \t\t\"kernel supports percpu data\", probe_kern_percpu_data,\n \t},\n+\t[FEAT_FD_ARRAY_CNT] = {\n+\t\t\"continuous fd_array (fd_array_cnt)\", probe_kern_fd_array_cnt,\n+\t},\n };\n \n bool feat_supported(struct kern_feature_cache *cache, enum kern_feature_id feat_id)\ndiff --git a/tools/lib/bpf/libbpf.c b/tools/lib/bpf/libbpf.c\nindex cb09ded907732..0cd2c25a120d4 100644\n--- a/tools/lib/bpf/libbpf.c\n+++ b/tools/lib/bpf/libbpf.c\n@@ -794,7 +794,12 @@ struct bpf_object {\n \n \tint *fd_array;\n \tsize_t fd_array_cap;\n-\tsize_t fd_array_cnt;\n+\t/*\n+\t * Slots handed out so far, slot 0 included. A kfunc insn names a module\n+\t * BTF slot through its offset, slot 0 stands for vmlinux BTF and stays\n+\t * empty, so the array goes to the kernel without a count.\n+\t */\n+\tsize_t fd_array_used;\n \n \tstruct usdt_manager *usdt_man;\n \n@@ -8799,6 +8804,46 @@ static int libbpf_prepare_prog_load(struct bpf_program *prog,\n \n static void fixup_verifier_log(struct bpf_program *prog, char *buf, size_t buf_sz);\n \n+/*\n+ * Bind the arena of the object to the program about to be loaded. A program\n+ * has an arena only when an insn refers to the map, but the kernel can hand\n+ * it arena pointers all the same, as struct_ops arguments, through tagged\n+ * struct members or as kfunc return values, and the verifier rejects those\n+ * when the program has no arena to place them in. A continuous fd_array\n+ * binds every map in it, the arena takes slot 0, which is free: a kfunc insn\n+ * with offset 0 names vmlinux BTF rather than a slot.\n+ */\n+static int bpf_object__bind_arena(struct bpf_object *obj, struct bpf_program *prog,\n+\t\t\t\t  struct bpf_prog_load_opts *load_attr)\n+{\n+\tstruct bpf_map *map;\n+\tint err;\n+\n+\tif (obj-\u003earena_map_idx \u003c 0 || !kernel_supports(obj, FEAT_FD_ARRAY_CNT))\n+\t\treturn 0;\n+\tmap = \u0026obj-\u003emaps[obj-\u003earena_map_idx];\n+\t/*\n+\t * Nothing to bind when the arena is not created, when it is exclusive\n+\t * to another program, or for an offloaded program, which the kernel\n+\t * does not let near an arena.\n+\t */\n+\tif (!map-\u003eautocreate || (map-\u003eexcl_prog \u0026\u0026 map-\u003eexcl_prog != prog) ||\n+\t    load_attr-\u003eprog_ifindex)\n+\t\treturn 0;\n+\n+\tif (!obj-\u003efd_array_used) {\n+\t\terr = libbpf_ensure_mem((void **)\u0026obj-\u003efd_array, \u0026obj-\u003efd_array_cap, sizeof(int), 1);\n+\t\tif (err)\n+\t\t\treturn err;\n+\t\tobj-\u003efd_array_used = 1;\n+\t}\n+\tobj-\u003efd_array[0] = map-\u003efd;\n+\tload_attr-\u003efd_array = obj-\u003efd_array;\n+\t/* with slot 0 filled every slot handed out is valid, so the array can go with a count */\n+\tload_attr-\u003efd_array_cnt = obj-\u003efd_array_used;\n+\treturn 0;\n+}\n+\n static int bpf_object_load_prog(struct bpf_object *obj, struct bpf_program *prog,\n \t\t\t\tstruct bpf_insn *insns, int insns_cnt,\n \t\t\t\tconst char *license, __u32 kern_version, int *prog_fd)\n@@ -8862,6 +8907,11 @@ static int bpf_object_load_prog(struct bpf_object *obj, struct bpf_program *prog\n \tif (obj-\u003edata_in_arena)\n \t\tload_attr.prog_flags |= BPF_F_ARENA_SCALAR;\n \tload_attr.fd_array = obj-\u003efd_array;\n+\tif (!obj-\u003egen_loader) {\n+\t\terr = bpf_object__bind_arena(obj, prog, \u0026load_attr);\n+\t\tif (err)\n+\t\t\treturn err;\n+\t}\n \n \tload_attr.token_fd = obj-\u003etoken_fd;\n \tif (obj-\u003etoken_fd)\n@@ -9716,22 +9766,22 @@ static int bpf_object__resolve_ksym_func_btf_id(struct bpf_object *obj,\n \t/* set index for module BTF fd in fd_array, if unset */\n \tif (mod_btf \u0026\u0026 !mod_btf-\u003efd_array_idx) {\n \t\t/* insn-\u003eoff is s16 */\n-\t\tif (obj-\u003efd_array_cnt == INT16_MAX) {\n+\t\tif (obj-\u003efd_array_used == INT16_MAX) {\n \t\t\tpr_warn(\"extern (func ksym) '%s': module BTF fd index %d too big to fit in bpf_insn offset\\n\",\n \t\t\t\text-\u003ename, mod_btf-\u003efd_array_idx);\n \t\t\treturn -E2BIG;\n \t\t}\n \t\t/* Cannot use index 0 for module BTF fd */\n-\t\tif (!obj-\u003efd_array_cnt)\n-\t\t\tobj-\u003efd_array_cnt = 1;\n+\t\tif (!obj-\u003efd_array_used)\n+\t\t\tobj-\u003efd_array_used = 1;\n \n \t\tret = libbpf_ensure_mem((void **)\u0026obj-\u003efd_array, \u0026obj-\u003efd_array_cap, sizeof(int),\n-\t\t\t\t\tobj-\u003efd_array_cnt + 1);\n+\t\t\t\t\tobj-\u003efd_array_used + 1);\n \t\tif (ret)\n \t\t\treturn ret;\n-\t\tmod_btf-\u003efd_array_idx = obj-\u003efd_array_cnt;\n+\t\tmod_btf-\u003efd_array_idx = obj-\u003efd_array_used;\n \t\t/* we assume module BTF FD is always \u003e0 */\n-\t\tobj-\u003efd_array[obj-\u003efd_array_cnt++] = mod_btf-\u003efd;\n+\t\tobj-\u003efd_array[obj-\u003efd_array_used++] = mod_btf-\u003efd;\n \t}\n \n \text-\u003eis_set = true;\n@@ -9961,8 +10011,10 @@ static void bpf_object_cleanup_btf(struct bpf_object *obj)\n \n static void bpf_object_post_load_cleanup(struct bpf_object *obj)\n {\n-\t/* clean up fd_array */\n+\t/* clean up fd_array, and the slots handed out with it */\n \tzfree(\u0026obj-\u003efd_array);\n+\tobj-\u003efd_array_cap = 0;\n+\tobj-\u003efd_array_used = 0;\n \n \t/* clean up BTF */\n \tbpf_object_cleanup_btf(obj);\n@@ -10940,7 +10992,13 @@ int bpf_program__clone(struct bpf_program *prog, const struct bpf_prog_load_opts\n \tattr.prog_ifindex = OPTS_GET(opts, prog_ifindex, 0) ?: prog-\u003eprog_ifindex;\n \tattr.kern_version = OPTS_GET(opts, kern_version, 0) ?: obj-\u003ekern_version;\n \tattr.fd_array = OPTS_GET(opts, fd_array, NULL) ?: obj-\u003efd_array;\n-\tattr.fd_array_cnt = OPTS_GET(opts, fd_array_cnt, 0) ?: obj-\u003efd_array_cnt;\n+\tattr.fd_array_cnt = OPTS_GET(opts, fd_array_cnt, 0);\n+\t/* the arena of the object goes with the program, as in bpf_object__load() */\n+\tif (!obj-\u003egen_loader \u0026\u0026 !OPTS_GET(opts, fd_array, NULL)) {\n+\t\terr = bpf_object__bind_arena(obj, prog, \u0026attr);\n+\t\tif (err)\n+\t\t\treturn libbpf_err(err);\n+\t}\n \tattr.token_fd = OPTS_GET(opts, token_fd, 0) ?: obj-\u003etoken_fd;\n \tif (attr.token_fd)\n \t\tattr.prog_flags |= BPF_F_TOKEN_FD;\ndiff --git a/tools/lib/bpf/libbpf_internal.h b/tools/lib/bpf/libbpf_internal.h\nindex 4e45dfacb548a..22d9fe512ea1a 100644\n--- a/tools/lib/bpf/libbpf_internal.h\n+++ b/tools/lib/bpf/libbpf_internal.h\n@@ -404,6 +404,8 @@ enum kern_feature_id {\n \tFEAT_BPF_SYSCALL_COMMON_ATTRS,\n \t/* Kernel supports percpu data */\n \tFEAT_PERCPU_DATA,\n+\t/* Kernel binds every map in a continuous fd_array (fd_array_cnt) to the program */\n+\tFEAT_FD_ARRAY_CNT,\n \t__FEAT_CNT,\n };\n \ndiff --git a/tools/testing/selftests/bpf/prog_tests/btf_dump.c b/tools/testing/selftests/bpf/prog_tests/btf_dump.c\nindex 17fa44cb9c91f..35f214e254da8 100644\n--- a/tools/testing/selftests/bpf/prog_tests/btf_dump.c\n+++ b/tools/testing/selftests/bpf/prog_tests/btf_dump.c\n@@ -604,8 +604,10 @@ static void test_btf_dump_type_tags(void)\n \t * struct s {\n \t *   void __attribute__((btf_type_tag(\\\"void_tag\\\"))) *p1;\n \t *   void __attribute__((void_attr)) *p2;\n+\t *   void __arena *p3;\n \t * };\n \t *\n+\t * The arena tag is rendered with the address space it denotes.\n \t */\n \n \tid = btf__add_type_tag(btf, \"void_tag\", 0);\n@@ -618,17 +620,25 @@ static void test_btf_dump_type_tags(void)\n \tid = btf__add_ptr(btf, id);\n \tASSERT_EQ(id, 4, \"void_ptr_id2\");\n \n-\tid = btf__add_struct(btf, \"s\", 8);\n-\tASSERT_EQ(id, 5, \"struct_id\");\n+\tid = btf__add_type_tag(btf, \"arena\", 0);\n+\tASSERT_EQ(id, 5, \"arena_tag_id\");\n+\tid = btf__add_ptr(btf, id);\n+\tASSERT_EQ(id, 6, \"void_ptr_id3\");\n+\n+\tid = btf__add_struct(btf, \"s\", 24);\n+\tASSERT_EQ(id, 7, \"struct_id\");\n \terr = btf__add_field(btf, \"p1\", 2, 0, 0);\n \tASSERT_OK(err, \"field_ok1\");\n-\terr = btf__add_field(btf, \"p2\", 4, 0, 0);\n+\terr = btf__add_field(btf, \"p2\", 4, 64, 0);\n \tASSERT_OK(err, \"field_ok2\");\n+\terr = btf__add_field(btf, \"p3\", 6, 128, 0);\n+\tASSERT_OK(err, \"field_ok3\");\n \n \ttest_ctx__dump_and_compare(\u0026t,\n \"struct s {\\n\"\n \"\tvoid __attribute__((btf_type_tag(\\\"void_tag\\\"))) *p1;\\n\"\n \"\tvoid __attribute__((void_attr)) *p2;\\n\"\n+\"\tvoid __attribute__((address_space(1))) __attribute__((btf_type_tag(\\\"arena\\\"))) *p3;\\n\"\n \"};\\n\\n\", \"dump_and_compare\");\n \n \ttest_ctx__free(\u0026t);\ndiff --git a/tools/testing/selftests/bpf/prog_tests/clone_module_kfunc.c b/tools/testing/selftests/bpf/prog_tests/clone_module_kfunc.c\nnew file mode 100644\nindex 0000000000000..fa6ccbaa7fd98\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/prog_tests/clone_module_kfunc.c\n@@ -0,0 +1,30 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+#include \u003ctest_progs.h\u003e\n+#include \"kfunc_call_test.skel.h\"\n+\n+/*\n+ * bpf_program__clone() loads a program from a prepared object with the\n+ * fd_array of the object, where module BTF fds start at slot 1 and slot 0 is\n+ * empty. It used to pass the fill index of that array as fd_array_cnt, which\n+ * made the kernel resolve slot 0 and fail the load.\n+ */\n+void test_clone_module_kfunc(void)\n+{\n+\tstruct kfunc_call_test *skel;\n+\tint err, fd;\n+\n+\tskel = kfunc_call_test__open();\n+\tif (!ASSERT_OK_PTR(skel, \"skel_open\"))\n+\t\treturn;\n+\n+\terr = bpf_object__prepare(skel-\u003eobj);\n+\tif (!ASSERT_OK(err, \"prepare\"))\n+\t\tgoto out;\n+\n+\tfd = bpf_program__clone(skel-\u003eprogs.kfunc_call_test1, NULL);\n+\tif (ASSERT_GE(fd, 0, \"clone\"))\n+\t\tclose(fd);\n+out:\n+\tkfunc_call_test__destroy(skel);\n+}\ndiff --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\nindex 7f9f54ba3fbe5..149b130f8cdb7 100644\n--- a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c\n+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c\n@@ -1,10 +1,20 @@\n // SPDX-License-Identifier: GPL-2.0\n /* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n #include \u003ctest_progs.h\u003e\n+#include \u003cbpf/btf.h\u003e\n \n #include \"struct_ops_arena.skel.h\"\n #include \"struct_ops_arena_attach.skel.h\"\n #include \"struct_ops_arena_fail.skel.h\"\n+#include \"struct_ops_arena_member.skel.h\"\n+#include \"struct_ops_arena_member_render.skel.h\"\n+#include \"struct_ops_arena_member_fail__union.skel.h\"\n+#include \"struct_ops_arena_member_fail__load_acq.skel.h\"\n+#include \"struct_ops_arena_member_fail__mismatch.skel.h\"\n+#include \"struct_ops_arena_member_fail__mismatch_mem.skel.h\"\n+#include \"struct_ops_arena_member_fail__write.skel.h\"\n+#include \"struct_ops_arena_member_fail__no_arena.skel.h\"\n+#include \"struct_ops_arena_autobind.skel.h\"\n \n #if defined(__x86_64__) || defined(__aarch64__)\n /*\n@@ -101,6 +111,121 @@ static void arena_arg_attach(void)\n \n \tstruct_ops_arena__destroy(skel);\n }\n+\n+/* Attach @st_ops and run @trigger once, it returns 0 when everything checked out. */\n+static void attach_and_trigger(struct bpf_map *st_ops, struct bpf_program *trigger)\n+{\n+\tLIBBPF_OPTS(bpf_test_run_opts, topts);\n+\tstruct bpf_link *link;\n+\tint err;\n+\n+\tlink = bpf_map__attach_struct_ops(st_ops);\n+\tif (!ASSERT_OK_PTR(link, \"attach_struct_ops\"))\n+\t\treturn;\n+\n+\terr = bpf_prog_test_run_opts(bpf_program__fd(trigger), \u0026topts);\n+\tASSERT_OK(err, \"test_run\");\n+\tASSERT_EQ(topts.retval, 0, \"trigger_retval\");\n+\n+\tbpf_link__destroy(link);\n+}\n+\n+/*\n+ * The tags on the members of struct bpf_testmod_arena_member_args reach the\n+ * module BTF only from a toolchain with btf_type_tag support.\n+ */\n+static bool testmod_has_type_tag(const char *tag)\n+{\n+\tstruct btf *vmlinux_btf, *module_btf;\n+\tbool found = false;\n+\n+\tvmlinux_btf = btf__load_vmlinux_btf();\n+\tif (!ASSERT_OK_PTR(vmlinux_btf, \"vmlinux_btf\"))\n+\t\treturn false;\n+\tmodule_btf = btf__load_module_btf(\"bpf_testmod\", vmlinux_btf);\n+\tif (ASSERT_OK_PTR(module_btf, \"module_btf\"))\n+\t\tfound = btf__find_by_name_kind(module_btf, tag, BTF_KIND_TYPE_TAG) \u003e 0;\n+\tif (!found)\n+\t\tprintf(\"%s:SKIP: no %s type tag in the bpf_testmod BTF\\n\", __func__, tag);\n+\tbtf__free(module_btf);\n+\tbtf__free(vmlinux_btf);\n+\treturn found;\n+}\n+\n+/*\n+ * Attach a callback that reads arena pointers out of its argument struct.\n+ * The header is shared with the module, so the members are arena pointers\n+ * on the program side as well.\n+ */\n+static void arena_member(void)\n+{\n+\tstruct struct_ops_arena_member *skel;\n+\n+\tskel = struct_ops_arena_member__open_and_load();\n+\tif (!ASSERT_OK_PTR(skel, \"struct_ops_arena_member__open_and_load\"))\n+\t\treturn;\n+\n+\tattach_and_trigger(skel-\u003emaps.testmod_arena_member, skel-\u003eprogs.trigger);\n+\tstruct_ops_arena_member__destroy(skel);\n+}\n+\n+/* @prog_fd lists exactly one map, the one with @map_id. */\n+static void assert_prog_map(int prog_fd, __u32 map_id, const char *name)\n+{\n+\t__u32 map_ids[2] = {}, len = sizeof(struct bpf_prog_info);\n+\tstruct bpf_prog_info info = {\n+\t\t.nr_map_ids = ARRAY_SIZE(map_ids),\n+\t\t.map_ids = ptr_to_u64(map_ids),\n+\t};\n+\tint err;\n+\n+\terr = bpf_prog_get_info_by_fd(prog_fd, \u0026info, \u0026len);\n+\tif (!ASSERT_OK(err, name))\n+\t\treturn;\n+\tif (!ASSERT_EQ(info.nr_map_ids, 1, name))\n+\t\treturn;\n+\tASSERT_EQ(map_ids[0], map_id, name);\n+}\n+\n+/*\n+ * libbpf binds the arena of an object to every program in it: a callback\n+ * with an __arena argument loads without referring to the arena, and an\n+ * unrelated program gets the arena as well.\n+ */\n+static void arena_autobind(void)\n+{\n+\tstruct struct_ops_arena_autobind *skel;\n+\tstruct bpf_map_info info = {};\n+\t__u32 len = sizeof(info);\n+\tint err;\n+\n+\tskel = struct_ops_arena_autobind__open_and_load();\n+\tif (!ASSERT_OK_PTR(skel, \"struct_ops_arena_autobind__open_and_load\"))\n+\t\treturn;\n+\n+\terr = bpf_map_get_info_by_fd(bpf_map__fd(skel-\u003emaps.arena), \u0026info, \u0026len);\n+\tif (!ASSERT_OK(err, \"arena_info\"))\n+\t\tgoto out;\n+\tassert_prog_map(bpf_program__fd(skel-\u003eprogs.test_arena_cb), info.id, \"test_arena_cb\");\n+\tassert_prog_map(bpf_program__fd(skel-\u003eprogs.unrelated), info.id, \"unrelated\");\n+\n+\tattach_and_trigger(skel-\u003emaps.testmod_arena_autobind, skel-\u003eprogs.trigger);\n+out:\n+\tstruct_ops_arena_autobind__destroy(skel);\n+}\n+\n+/* The same through the vmlinux.h rendering of the members, which takes casts. */\n+static void arena_member_render(void)\n+{\n+\tstruct struct_ops_arena_member_render *skel;\n+\n+\tskel = struct_ops_arena_member_render__open_and_load();\n+\tif (!ASSERT_OK_PTR(skel, \"struct_ops_arena_member_render__open_and_load\"))\n+\t\treturn;\n+\n+\tattach_and_trigger(skel-\u003emaps.testmod_arena_member_render, skel-\u003eprogs.trigger);\n+\tstruct_ops_arena_member_render__destroy(skel);\n+}\n #endif\n \n /*\n@@ -122,6 +247,20 @@ void serial_test_struct_ops_arena(void)\n \t\tarena_arg_fail();\n \tif (test__start_subtest(\"arena_arg_attach\"))\n \t\tarena_arg_attach();\n+\tif (testmod_has_type_tag(\"arena\")) {\n+\t\tif (test__start_subtest(\"arena_member\"))\n+\t\t\tarena_member();\n+\t\tif (test__start_subtest(\"arena_member_render\"))\n+\t\t\tarena_member_render();\n+\t\tRUN_TESTS(struct_ops_arena_member_fail__union);\n+\t\tRUN_TESTS(struct_ops_arena_member_fail__load_acq);\n+\t\tRUN_TESTS(struct_ops_arena_member_fail__mismatch);\n+\t\tRUN_TESTS(struct_ops_arena_member_fail__mismatch_mem);\n+\t\tRUN_TESTS(struct_ops_arena_member_fail__write);\n+\t\tRUN_TESTS(struct_ops_arena_member_fail__no_arena);\n+\t}\n+\tif (test__start_subtest(\"arena_autobind\"))\n+\t\tarena_autobind();\n #else\n \ttest__skip();\n #endif\ndiff --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\nnew file mode 100644\nindex 0000000000000..17d5de78273ae\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_nullable.c\n@@ -0,0 +1,68 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+#include \u003ctest_progs.h\u003e\n+#include \u003cbpf/btf.h\u003e\n+\n+#include \"struct_ops_nullable.skel.h\"\n+#include \"struct_ops_nullable_fail.skel.h\"\n+\n+/*\n+ * Attach a callback that gets a trusted and a trusted __nullable task pointer\n+ * through its argument struct and drive it through the\n+ * bpf_testmod_ops3_call_test_nullable() kfunc.\n+ */\n+static void nullable_member(void)\n+{\n+\tLIBBPF_OPTS(bpf_test_run_opts, topts);\n+\tstruct struct_ops_nullable *skel;\n+\tstruct bpf_link *link;\n+\tint err;\n+\n+\tskel = struct_ops_nullable__open_and_load();\n+\tif (!ASSERT_OK_PTR(skel, \"struct_ops_nullable__open_and_load\"))\n+\t\treturn;\n+\n+\tlink = bpf_map__attach_struct_ops(skel-\u003emaps.testmod_nullable);\n+\tif (!ASSERT_OK_PTR(link, \"attach_struct_ops\"))\n+\t\tgoto out;\n+\n+\terr = bpf_prog_test_run_opts(bpf_program__fd(skel-\u003eprogs.trigger), \u0026topts);\n+\tASSERT_OK(err, \"test_run\");\n+\tASSERT_EQ(topts.retval, 0, \"trigger_retval\");\n+\n+\tbpf_link__destroy(link);\n+out:\n+\tstruct_ops_nullable__destroy(skel);\n+}\n+\n+/* Serialized because it attaches the singleton bpf_testmod_ops3. */\n+/*\n+ * The tag on the member of struct bpf_testmod_nullable_args reaches the module\n+ * BTF only from a toolchain with btf_type_tag support.\n+ */\n+static bool testmod_has_nullable_tag(void)\n+{\n+\tstruct btf *vmlinux_btf, *module_btf;\n+\tbool found = false;\n+\n+\tvmlinux_btf = btf__load_vmlinux_btf();\n+\tif (!ASSERT_OK_PTR(vmlinux_btf, \"vmlinux_btf\"))\n+\t\treturn false;\n+\tmodule_btf = btf__load_module_btf(\"bpf_testmod\", vmlinux_btf);\n+\tif (ASSERT_OK_PTR(module_btf, \"module_btf\"))\n+\t\tfound = btf__find_by_name_kind(module_btf, \"nullable\", BTF_KIND_TYPE_TAG) \u003e 0;\n+\tbtf__free(module_btf);\n+\tbtf__free(vmlinux_btf);\n+\treturn found;\n+}\n+\n+void serial_test_struct_ops_nullable(void)\n+{\n+\tif (!testmod_has_nullable_tag()) {\n+\t\ttest__skip();\n+\t\treturn;\n+\t}\n+\tif (test__start_subtest(\"nullable_member\"))\n+\t\tnullable_member();\n+\tRUN_TESTS(struct_ops_nullable_fail);\n+}\ndiff --git a/tools/testing/selftests/bpf/prog_tests/verifier.c b/tools/testing/selftests/bpf/prog_tests/verifier.c\nindex c69c0f7e9cc89..6df440d826750 100644\n--- a/tools/testing/selftests/bpf/prog_tests/verifier.c\n+++ b/tools/testing/selftests/bpf/prog_tests/verifier.c\n@@ -3,6 +3,7 @@\n #include \u003ctest_progs.h\u003e\n \n #include \"arena_kfunc.skel.h\"\n+#include \"arena_noarena.skel.h\"\n #include \"arena_kfunc_jit.skel.h\"\n #include \"cap_helpers.h\"\n #include \"cb_refs.skel.h\"\n@@ -193,6 +194,7 @@ static void run_tests_aux(const char *skel_name,\n } while (0)\n \n void test_arena_kfunc(void)                   { RUN_TESTS(arena_kfunc); }\n+void test_arena_noarena(void)                 { RUN_TESTS(arena_noarena); }\n \n void test_arena_kfunc_jit(void)               { RUN_TESTS(arena_kfunc_jit); }\n \ndiff --git a/tools/testing/selftests/bpf/progs/arena_kfunc.c b/tools/testing/selftests/bpf/progs/arena_kfunc.c\nindex 96f7aacf4d9f2..168ae8d7e303e 100644\n--- a/tools/testing/selftests/bpf/progs/arena_kfunc.c\n+++ b/tools/testing/selftests/bpf/progs/arena_kfunc.c\n@@ -192,14 +192,41 @@ int arena_arg_unpopulated(void *ctx)\n \treturn 0;\n }\n \n+/*\n+ * A subprogram whose only arena use is the argument of a kfunc needs the\n+ * arena base too, the JIT rebases the argument on it. The address arrives\n+ * as a number, so no cast marks the subprogram either.\n+ */\n+__noinline int arena_arg_from_subprog(unsigned long addr)\n+{\n+\treturn bpf_kfunc_arena_arg_test((u64 *)addr) != 41;\n+}\n+\n SEC(\"syscall\")\n __arch_x86_64\n __arch_arm64\n-__failure __msg(\"arena pointer requires a program with an associated arena\")\n-int arena_arg_no_arena(void *ctx)\n+__success __retval(0)\n+int arena_arg_subprog(void *ctx)\n {\n-\tbpf_kfunc_arena_arg_test((u64 *)1);\n+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST)\n+\tu64 __arena *val;\n+\tint ret;\n+\n+\tval = bpf_arena_alloc_pages(\u0026arena, NULL, 1, NUMA_NO_NODE, 0);\n+\tif (!val)\n+\t\treturn 1;\n+\n+\t*val = 41;\n+\tif (arena_arg_from_subprog((unsigned long)val))\n+\t\tret = 2;\n+\telse\n+\t\tret = *val != 42 ? 3 : 0;\n+\n+\tbpf_arena_free_pages(\u0026arena, (void __arena *)val, 1);\n+\treturn ret;\n+#else\n \treturn 0;\n+#endif\n }\n \n SEC(\"syscall\")\ndiff --git a/tools/testing/selftests/bpf/progs/arena_noarena.c b/tools/testing/selftests/bpf/progs/arena_noarena.c\nnew file mode 100644\nindex 0000000000000..edbd40cd5be8a\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/arena_noarena.c\n@@ -0,0 +1,39 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+\n+#include \u003cvmlinux.h\u003e\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \"bpf_misc.h\"\n+#include \"../test_kmods/bpf_testmod_kfunc.h\"\n+\n+/*\n+ * Programs without an arena. They cannot share an object with an arena map,\n+ * libbpf binds the arena of an object to every program in it.\n+ */\n+\n+SEC(\"syscall\")\n+__arch_x86_64\n+__arch_arm64\n+__failure __msg(\"arena pointer requires a program with an associated arena\")\n+int arena_arg_no_arena(void *ctx)\n+{\n+\tbpf_kfunc_arena_arg_test((u64 *)1);\n+\treturn 0;\n+}\n+\n+SEC(\"syscall\")\n+__flag(BPF_F_ARENA_SCALAR)\n+__arch_x86_64\n+__arch_arm64\n+__description(\"arena_scalar: no arena, no access through a number\")\n+__failure __msg(\"R6 invalid mem access 'scalar'\")\n+__naked void no_arena(void)\n+{\n+\tasm volatile (\"\t\t\t\t\t\\\n+\tr6 = 0x100000000000 ll;\t\t\t\t\\\n+\tr0 = *(u64 *)(r6 + 0);\t\t\t\t\\\n+\texit;\t\t\t\t\t\t\\\n+\"\t::: __clobber_all);\n+}\n+\n+char _license[] SEC(\"license\") = \"GPL\";\ndiff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_autobind.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_autobind.c\nnew file mode 100644\nindex 0000000000000..495faa0f4a8b8\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_autobind.c\n@@ -0,0 +1,64 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+\n+#define BPF_NO_KFUNC_PROTOTYPES\n+#include \u003cvmlinux.h\u003e\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \"bpf_experimental.h\"\n+#include \u003cbpf_arena_common.h\u003e\n+#include \"../test_kmods/bpf_testmod.h\"\n+#include \"../test_kmods/bpf_testmod_kfunc.h\"\n+\n+char _license[] SEC(\"license\") = \"GPL\";\n+\n+struct {\n+\t__uint(type, BPF_MAP_TYPE_ARENA);\n+\t__uint(map_flags, BPF_F_MMAPABLE);\n+\t__uint(max_entries, 1);\n+} arena SEC(\".maps\");\n+\n+/*\n+ * Neither the callback nor the unrelated program refers to the arena. libbpf\n+ * binds it to them all the same, so the callback can use the __arena argument\n+ * the kernel hands it.\n+ */\n+SEC(\"struct_ops/test_arena\")\n+int test_arena_cb(unsigned long long *ctx)\n+{\n+\tu64 __arena *ptr = (u64 __arena *)ctx[0];\n+\n+\t*ptr += 1;\n+\treturn 0;\n+}\n+\n+SEC(\".struct_ops.link\")\n+struct bpf_testmod_ops3 testmod_arena_autobind = {\n+\t.test_arena = (void *)test_arena_cb,\n+};\n+\n+SEC(\"syscall\")\n+int unrelated(void *ctx)\n+{\n+\treturn 0;\n+}\n+\n+SEC(\"syscall\")\n+int trigger(void *ctx)\n+{\n+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST)\n+\tu64 __arena *val;\n+\n+\tval = bpf_arena_alloc_pages(\u0026arena, NULL, 1, NUMA_NO_NODE, 0);\n+\tif (!val)\n+\t\treturn 1;\n+\n+\t*val = 41;\n+\tif (bpf_testmod_ops3_call_test_arena((u64 *)val))\n+\t\treturn 2;\n+\tif (*val != 42)\n+\t\treturn 3;\n+\n+\tbpf_arena_free_pages(\u0026arena, (void __arena *)val, 1);\n+#endif\n+\treturn 0;\n+}\ndiff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member.c\nnew file mode 100644\nindex 0000000000000..469e62e383f2f\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member.c\n@@ -0,0 +1,92 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+\n+#define BPF_NO_KFUNC_PROTOTYPES\n+#include \u003cvmlinux.h\u003e\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_tracing.h\u003e\n+#include \"bpf_experimental.h\"\n+#include \u003cbpf_arena_common.h\u003e\n+#include \"../test_kmods/bpf_testmod.h\"\n+#include \"../test_kmods/bpf_testmod_kfunc.h\"\n+\n+char _license[] SEC(\"license\") = \"GPL\";\n+\n+struct {\n+\t__uint(type, BPF_MAP_TYPE_ARENA);\n+\t__uint(map_flags, BPF_F_MMAPABLE);\n+\t/* page 0 hosts the arena globals, page 1 is for allocations */\n+\t__uint(max_entries, 2);\n+} arena SEC(\".maps\");\n+\n+/* also associates the callback with the arena */\n+u64 __arena arena_touch;\n+/* the register value a load of args-\u003eptr produced in the callback */\n+u64 __arena member_ptr_val;\n+\n+/*\n+ * The header is shared with the module, so the members are u64 __arena * on\n+ * this side too and the loads need no cast. The verifier derives the type of\n+ * the loaded value from the module BTF, where the members carry the tag.\n+ */\n+SEC(\"struct_ops/test_arena_member\")\n+int BPF_PROG(test_arena_member_cb, struct bpf_testmod_arena_member_args *args)\n+{\n+\tu64 __arena *ptr = args-\u003eptr;\n+\tu64 raw;\n+\n+\tarena_touch++;\n+\n+\t/* the register right after the load, untouched by the compiler */\n+\tasm volatile(\"%[raw] = *(u64 *)(%[args] + %[off])\"\n+\t\t     : [raw]\"=r\"(raw)\n+\t\t     : [args]\"r\"(args),\n+\t\t       [off]\"i\"(offsetof(struct bpf_testmod_arena_member_args, ptr)));\n+\tmember_ptr_val = raw;\n+\n+\t*ptr += 1;\n+\tif (args-\u003enull_ptr)\n+\t\treturn 0xbad;\n+\t*args-\u003enested.ptr += 1;\n+\tif (args-\u003ecookie != 0xc00c1e)\n+\t\treturn 0xbad;\n+\t/* an arena pointer to kfuncs as well, the kfunc adds one */\n+\tbpf_kfunc_arena_arg_test((u64 *)args-\u003eptr);\n+\treturn 0;\n+}\n+\n+SEC(\".struct_ops.link\")\n+struct bpf_testmod_ops3 testmod_arena_member = {\n+\t.test_arena_member = (void *)test_arena_member_cb,\n+};\n+\n+SEC(\"syscall\")\n+int trigger(void *ctx)\n+{\n+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST)\n+\tu64 __arena *val;\n+\tint ret;\n+\n+\tval = bpf_arena_alloc_pages(\u0026arena, NULL, 1, NUMA_NO_NODE, 0);\n+\tif (!val)\n+\t\treturn 1;\n+\n+\t*val = 41;\n+\tret = bpf_testmod_ops3_call_test_arena_member((u64 *)val);\n+\tif (ret)\n+\t\treturn 2;\n+\t/* ptr, nested.ptr and the kfunc each added one */\n+\tif (*val != 44)\n+\t\treturn 3;\n+\n+\t/*\n+\t * The callback saw (u32)(kaddr - kern_vm_start), the arena offset of\n+\t * val with the upper 32 bits clear, the same as an __arena argument.\n+\t */\n+\tif (member_ptr_val != (u32)(u64)val)\n+\t\treturn 4;\n+\n+\tbpf_arena_free_pages(\u0026arena, (void __arena *)val, 1);\n+#endif\n+\treturn 0;\n+}\ndiff --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\nnew file mode 100644\nindex 0000000000000..9d7972ae5c558\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__load_acq.c\n@@ -0,0 +1,38 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+\n+#include \u003cvmlinux.h\u003e\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf_arena_common.h\u003e\n+#include \"bpf_misc.h\"\n+#include \"../test_kmods/bpf_testmod.h\"\n+\n+char _license[] SEC(\"license\") = \"GPL\";\n+\n+struct {\n+\t__uint(type, BPF_MAP_TYPE_ARENA);\n+\t__uint(map_flags, BPF_F_MMAPABLE);\n+\t__uint(max_entries, 1);\n+} arena SEC(\".maps\");\n+\n+/* Only a plain load is rewritten to translate the member. */\n+SEC(\"struct_ops/test_arena_member\")\n+__failure __msg(\"arena pointer member ptr of struct bpf_testmod_arena_member_args can only be read with a plain load\")\n+__naked int load_acq_member(void)\n+{\n+\tasm volatile (\n+\t\"r1 = *(u64 *)(r1 + 0);\"\n+\t\"r2 = %[arena] ll;\"\n+\t/* r0 = load_acquire((u64 *)(r1 + 0)): BPF_STX | BPF_ATOMIC | BPF_DW, imm BPF_LOAD_ACQ */\n+\t\".8byte 0x00000100000010db;\"\n+\t\"r0 = 0;\"\n+\t\"exit;\"\n+\t:\n+\t: __imm_addr(arena)\n+\t: __clobber_all);\n+}\n+\n+SEC(\".struct_ops.link\")\n+struct bpf_testmod_ops3 testmod_arena_member_fail = {\n+\t.test_arena_member = (void *)load_acq_member,\n+};\ndiff --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\nnew file mode 100644\nindex 0000000000000..0b4147894122c\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch.c\n@@ -0,0 +1,45 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+\n+#include \u003cvmlinux.h\u003e\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf_arena_common.h\u003e\n+#include \"bpf_misc.h\"\n+#include \"../test_kmods/bpf_testmod.h\"\n+\n+char _license[] SEC(\"license\") = \"GPL\";\n+\n+struct {\n+\t__uint(type, BPF_MAP_TYPE_ARENA);\n+\t__uint(map_flags, BPF_F_MMAPABLE);\n+\t__uint(max_entries, 1);\n+} arena SEC(\".maps\");\n+\n+/*\n+ * The same load reaches args-\u003eptr on one path and args-\u003ecookie on the other,\n+ * the branch on the unknown cookie value makes the verifier walk both. The\n+ * rewrite is per insn, so the paths have to agree on what they load.\n+ */\n+SEC(\"struct_ops/test_arena_member\")\n+__failure __msg(\"loads an arena pointer member on one path and something else on another\")\n+__naked int mismatch_member(void)\n+{\n+\tasm volatile (\n+\t\"r6 = *(u64 *)(r1 + 0);\"\n+\t\"r2 = %[arena] ll;\"\n+\t\"r0 = *(u64 *)(r6 + %[cookie_off]);\"\n+\t\"if r0 == 0 goto 1f;\"\n+\t\"r6 += %[cookie_off];\"\n+\"1:\tr0 = *(u64 *)(r6 + 0);\"\n+\t\"r0 = 0;\"\n+\t\"exit;\"\n+\t:\n+\t: __imm_addr(arena),\n+\t  __imm_const(cookie_off, offsetof(struct bpf_testmod_arena_member_args, cookie))\n+\t: __clobber_all);\n+}\n+\n+SEC(\".struct_ops.link\")\n+struct bpf_testmod_ops3 testmod_arena_member_fail = {\n+\t.test_arena_member = (void *)mismatch_member,\n+};\ndiff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c\nnew file mode 100644\nindex 0000000000000..ff6d4db47cfdf\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c\n@@ -0,0 +1,54 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+\n+#include \u003cvmlinux.h\u003e\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_tracing.h\u003e\n+#include \u003cbpf_arena_common.h\u003e\n+#include \"bpf_misc.h\"\n+#include \"../test_kmods/bpf_testmod.h\"\n+\n+char _license[] SEC(\"license\") = \"GPL\";\n+\n+struct {\n+\t__uint(type, BPF_MAP_TYPE_ARENA);\n+\t__uint(map_flags, BPF_F_MMAPABLE);\n+\t__uint(max_entries, 1);\n+} arena SEC(\".maps\");\n+\n+/*\n+ * The same load reads args-\u003eptr on one path and the memory argument on the\n+ * other. The verifier lets a load insn serve a kernel struct pointer and a\n+ * memory pointer, see save_aux_ptr_type(), but the rewrite of the arena\n+ * member would corrupt the value the other path loads. A global function\n+ * is the only place a struct_ops program of this type gets a memory pointer.\n+ * The loaded value goes nowhere: an ALU insn on it would be an arena\n+ * pointer on one path and a scalar on the other, which the verifier rejects\n+ * on its own before the fixup pass gets to the load.\n+ */\n+__noinline int load_either(unsigned long long *ctx __arg_ctx, u64 *mem __arg_nonnull)\n+{\n+\tstruct bpf_testmod_arena_member_args *args = (void *)ctx[0];\n+\tvoid *p = args;\n+\n+\tif (args-\u003ecookie == 0)\n+\t\tp = mem;\n+\t/* one load insn, reached with both pointers */\n+\tasm volatile(\"r0 = *(u64 *)(%[p] + 0)\" :: [p]\"r\"(p) : \"r0\");\n+\treturn 0;\n+}\n+\n+SEC(\"struct_ops/test_arena_member\")\n+__failure __msg(\"loads an arena pointer member on one path and something else on another\")\n+int BPF_PROG(mismatch_mem, struct bpf_testmod_arena_member_args *args)\n+{\n+\tu64 local = 0;\n+\n+\tasm volatile(\"\" :: \"r\"(\u0026arena));\n+\treturn load_either(ctx, \u0026local);\n+}\n+\n+SEC(\".struct_ops.link\")\n+struct bpf_testmod_ops3 testmod_arena_member_fail = {\n+\t.test_arena_member = (void *)mismatch_mem,\n+};\ndiff --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\nnew file mode 100644\nindex 0000000000000..3cf76e6615839\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__no_arena.c\n@@ -0,0 +1,23 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+\n+#include \u003cvmlinux.h\u003e\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_tracing.h\u003e\n+#include \"bpf_misc.h\"\n+#include \"../test_kmods/bpf_testmod.h\"\n+\n+char _license[] SEC(\"license\") = \"GPL\";\n+\n+/* No arena in the object: the member cannot be translated into anything. */\n+SEC(\"struct_ops/test_arena_member\")\n+__failure __msg(\"arena pointer member ptr of struct bpf_testmod_arena_member_args requires a program with an associated arena\")\n+int BPF_PROG(no_arena_member, struct bpf_testmod_arena_member_args *args)\n+{\n+\treturn args-\u003eptr != NULL;\n+}\n+\n+SEC(\".struct_ops.link\")\n+struct bpf_testmod_ops3 testmod_arena_member_fail = {\n+\t.test_arena_member = (void *)no_arena_member,\n+};\ndiff --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\nnew file mode 100644\nindex 0000000000000..d45666fd482c5\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__union.c\n@@ -0,0 +1,34 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+\n+#include \u003cvmlinux.h\u003e\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_tracing.h\u003e\n+#include \u003cbpf_arena_common.h\u003e\n+#include \"bpf_misc.h\"\n+#include \"../test_kmods/bpf_testmod.h\"\n+\n+char _license[] SEC(\"license\") = \"GPL\";\n+\n+struct {\n+\t__uint(type, BPF_MAP_TYPE_ARENA);\n+\t__uint(map_flags, BPF_F_MMAPABLE);\n+\t__uint(max_entries, 1);\n+} arena SEC(\".maps\");\n+\n+/*\n+ * An offset into a union resolves to its first member, so a program reading\n+ * raw there would get an arena pointer it did not ask for.\n+ */\n+SEC(\"struct_ops/test_arena_member\")\n+__failure __msg(\"arena pointer member ptr of bpf_testmod_arena_union is inside a union\")\n+int BPF_PROG(union_member, struct bpf_testmod_arena_member_args *args)\n+{\n+\tasm volatile(\"\" :: \"r\"(\u0026arena));\n+\treturn *args-\u003eu.ptr != 0;\n+}\n+\n+SEC(\".struct_ops.link\")\n+struct bpf_testmod_ops3 testmod_arena_member_fail = {\n+\t.test_arena_member = (void *)union_member,\n+};\ndiff --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\nnew file mode 100644\nindex 0000000000000..bcf1f6586e196\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__write.c\n@@ -0,0 +1,32 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+\n+#include \u003cvmlinux.h\u003e\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_tracing.h\u003e\n+#include \u003cbpf_arena_common.h\u003e\n+#include \"bpf_misc.h\"\n+#include \"../test_kmods/bpf_testmod.h\"\n+\n+char _license[] SEC(\"license\") = \"GPL\";\n+\n+struct {\n+\t__uint(type, BPF_MAP_TYPE_ARENA);\n+\t__uint(map_flags, BPF_F_MMAPABLE);\n+\t__uint(max_entries, 1);\n+} arena SEC(\".maps\");\n+\n+/* The kernel owns the member, a program only reads it. */\n+SEC(\"struct_ops/test_arena_member\")\n+__failure __msg(\"only read is supported\")\n+int BPF_PROG(write_member, struct bpf_testmod_arena_member_args *args)\n+{\n+\tasm volatile(\"\" :: \"r\"(\u0026arena));\n+\targs-\u003eptr = NULL;\n+\treturn 0;\n+}\n+\n+SEC(\".struct_ops.link\")\n+struct bpf_testmod_ops3 testmod_arena_member_fail = {\n+\t.test_arena_member = (void *)write_member,\n+};\ndiff --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\nnew file mode 100644\nindex 0000000000000..58516fdc13511\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_render.c\n@@ -0,0 +1,82 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+\n+#define BPF_NO_KFUNC_PROTOTYPES\n+#include \u003cvmlinux.h\u003e\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_tracing.h\u003e\n+#include \"bpf_experimental.h\"\n+#include \u003cbpf_arena_common.h\u003e\n+\n+char _license[] SEC(\"license\") = \"GPL\";\n+\n+/*\n+ * The module types as a vmlinux.h generated by a libbpf that does not render\n+ * the arena tag with its address space shows them: an attribute on a pointer\n+ * in the kernel address space, so the program has to cast the member to the\n+ * arena address space before using it.\n+ */\n+struct bpf_testmod_arena_member_args {\n+\t__u64 __attribute__((btf_type_tag(\"arena\"))) *ptr;\n+\t__u64 __attribute__((btf_type_tag(\"arena\"))) __attribute__((btf_type_tag(\"nullable\"))) *null_ptr;\n+\tstruct {\n+\t\t__u64 __attribute__((btf_type_tag(\"arena\"))) *ptr;\n+\t} nested;\n+} __attribute__((preserve_access_index));\n+\n+struct bpf_testmod_ops3 {\n+\tint (*test_arena_member)(struct bpf_testmod_arena_member_args *args);\n+};\n+\n+int bpf_testmod_ops3_call_test_arena_member(__u64 *ptr__arena) __ksym;\n+\n+struct {\n+\t__uint(type, BPF_MAP_TYPE_ARENA);\n+\t__uint(map_flags, BPF_F_MMAPABLE);\n+\t__uint(max_entries, 2);\n+} arena SEC(\".maps\");\n+\n+/* also associates the callback with the arena */\n+u64 __arena arena_touch;\n+\n+SEC(\"struct_ops/test_arena_member\")\n+int BPF_PROG(test_arena_member_render_cb, struct bpf_testmod_arena_member_args *args)\n+{\n+\tu64 __arena *ptr = (u64 __arena *)args-\u003eptr;\n+\n+\tarena_touch++;\n+\t*ptr += 1;\n+\tif (args-\u003enull_ptr)\n+\t\treturn 0xbad;\n+\t*(u64 __arena *)args-\u003enested.ptr += 1;\n+\treturn 0;\n+}\n+\n+SEC(\".struct_ops.link\")\n+struct bpf_testmod_ops3 testmod_arena_member_render = {\n+\t.test_arena_member = (void *)test_arena_member_render_cb,\n+};\n+\n+SEC(\"syscall\")\n+int trigger(void *ctx)\n+{\n+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST)\n+\tu64 __arena *val;\n+\tint ret;\n+\n+\tval = bpf_arena_alloc_pages(\u0026arena, NULL, 1, NUMA_NO_NODE, 0);\n+\tif (!val)\n+\t\treturn 1;\n+\n+\t*val = 41;\n+\tret = bpf_testmod_ops3_call_test_arena_member((u64 *)val);\n+\tif (ret)\n+\t\treturn 2;\n+\t/* ptr and nested.ptr each added one */\n+\tif (*val != 43)\n+\t\treturn 3;\n+\n+\tbpf_arena_free_pages(\u0026arena, (void __arena *)val, 1);\n+#endif\n+\treturn 0;\n+}\ndiff --git a/tools/testing/selftests/bpf/progs/struct_ops_nullable.c b/tools/testing/selftests/bpf/progs/struct_ops_nullable.c\nnew file mode 100644\nindex 0000000000000..387792924c09e\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/struct_ops_nullable.c\n@@ -0,0 +1,45 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+\n+#include \u003cvmlinux.h\u003e\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_tracing.h\u003e\n+#include \"../test_kmods/bpf_testmod.h\"\n+#include \"../test_kmods/bpf_testmod_kfunc.h\"\n+\n+char _license[] SEC(\"license\") = \"GPL\";\n+\n+SEC(\"struct_ops/test_nullable\")\n+int BPF_PROG(test_nullable_cb, struct bpf_testmod_nullable_args *args)\n+{\n+\tstruct task_struct *t;\n+\tint ret = 0;\n+\n+\t/* trusted, so good for a kfunc as is */\n+\tt = bpf_task_acquire(args-\u003etask);\n+\tif (!t)\n+\t\treturn -1;\n+\tbpf_task_release(t);\n+\n+\t/* trusted but __nullable, so a check has to come first */\n+\tif (args-\u003etask_or_null) {\n+\t\tt = bpf_task_acquire(args-\u003etask_or_null);\n+\t\tif (!t)\n+\t\t\treturn -1;\n+\t\tbpf_task_release(t);\n+\t\tret = 1;\n+\t}\n+\treturn ret;\n+}\n+\n+SEC(\".struct_ops.link\")\n+struct bpf_testmod_ops3 testmod_nullable = {\n+\t.test_nullable = (void *)test_nullable_cb,\n+};\n+\n+SEC(\"syscall\")\n+int trigger(void *ctx)\n+{\n+\t/* one call without task_or_null, one with */\n+\treturn bpf_testmod_ops3_call_test_nullable() != 2;\n+}\ndiff --git a/tools/testing/selftests/bpf/progs/struct_ops_nullable_fail.c b/tools/testing/selftests/bpf/progs/struct_ops_nullable_fail.c\nnew file mode 100644\nindex 0000000000000..c15b3a8c8d39f\n--- /dev/null\n+++ b/tools/testing/selftests/bpf/progs/struct_ops_nullable_fail.c\n@@ -0,0 +1,28 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */\n+\n+#include \u003cvmlinux.h\u003e\n+#include \u003cbpf/bpf_helpers.h\u003e\n+#include \u003cbpf/bpf_tracing.h\u003e\n+#include \"bpf_misc.h\"\n+#include \"../test_kmods/bpf_testmod.h\"\n+\n+char _license[] SEC(\"license\") = \"GPL\";\n+\n+/* The member is trusted, but __nullable, so it needs a check before the kfunc. */\n+SEC(\"struct_ops/test_nullable\")\n+__failure __msg(\"Possibly NULL pointer passed to trusted\")\n+int BPF_PROG(unchecked_nullable, struct bpf_testmod_nullable_args *args)\n+{\n+\tstruct task_struct *t;\n+\n+\tt = bpf_task_acquire(args-\u003etask_or_null);\n+\tif (t)\n+\t\tbpf_task_release(t);\n+\treturn 0;\n+}\n+\n+SEC(\".struct_ops.link\")\n+struct bpf_testmod_ops3 testmod_nullable_fail = {\n+\t.test_nullable = (void *)unchecked_nullable,\n+};\ndiff --git a/tools/testing/selftests/bpf/progs/verifier_arena_scalar.c b/tools/testing/selftests/bpf/progs/verifier_arena_scalar.c\nindex bebc37f501b7e..a4f15eb21809d 100644\n--- a/tools/testing/selftests/bpf/progs/verifier_arena_scalar.c\n+++ b/tools/testing/selftests/bpf/progs/verifier_arena_scalar.c\n@@ -735,19 +735,6 @@ __naked void no_flag(void)\n \t: __clobber_all);\n }\n \n-SEC(\"syscall\")\n-__arena_scalar\n-__description(\"arena_scalar: no arena, no access through a number\")\n-__failure __msg(\"R6 invalid mem access 'scalar'\")\n-__naked void no_arena(void)\n-{\n-\tasm volatile (\"\t\t\t\t\t\\\n-\tr6 = 0x100000000000 ll;\t\t\t\t\\\n-\tr0 = *(u64 *)(r6 + 0);\t\t\t\t\\\n-\texit;\t\t\t\t\t\t\\\n-\"\t::: __clobber_all);\n-}\n-\n SEC(\"syscall\")\n __arena_scalar\n __description(\"arena_scalar: pointer that is NULL is an address in arena\")\ndiff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c\nindex b4f0af2aeca45..5a0556d4ce311 100644\n--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c\n+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c\n@@ -460,6 +460,16 @@ static int bpf_testmod_ops3__test_arena_multislot(struct bpf_testmod_arena_pair\n \treturn 0;\n }\n \n+static int bpf_testmod_ops3__test_arena_member(struct bpf_testmod_arena_member_args *args)\n+{\n+\treturn 0;\n+}\n+\n+static int bpf_testmod_ops3__test_nullable(struct bpf_testmod_nullable_args *args)\n+{\n+\treturn 0;\n+}\n+\n static struct bpf_testmod_ops3 __bpf_testmod_ops3 = {\n \t.test_1 = bpf_testmod_test_3,\n \t.test_2 = bpf_testmod_test_4,\n@@ -467,6 +477,8 @@ static struct bpf_testmod_ops3 __bpf_testmod_ops3 = {\n \t.test_arena_nullable = bpf_testmod_ops3__test_arena_nullable,\n \t.test_arena_stack = bpf_testmod_ops3__test_arena_stack,\n \t.test_arena_multislot = bpf_testmod_ops3__test_arena_multislot,\n+\t.test_arena_member = bpf_testmod_ops3__test_arena_member,\n+\t.test_nullable = bpf_testmod_ops3__test_nullable,\n };\n \n static void bpf_testmod_test_struct_ops3(void)\n@@ -507,6 +519,45 @@ __bpf_kfunc int bpf_testmod_ops3_call_test_arena_multislot(u64 *ptr__arena)\n \treturn st_ops3-\u003etest_arena_multislot(p, ptr__arena);\n }\n \n+/* The callback gets the kernel address of @ptr__arena behind each __arena member. */\n+__bpf_kfunc int bpf_testmod_ops3_call_test_arena_member(u64 *ptr__arena)\n+{\n+\tstruct bpf_testmod_arena_member_args args = {\n+\t\t.ptr = ptr__arena,\n+\t\t.nested.ptr = ptr__arena,\n+\t\t.u.ptr = ptr__arena,\n+\t\t.cookie = 0xc00c1e,\n+\t};\n+\n+\treturn st_ops3-\u003etest_arena_member(\u0026args);\n+}\n+\n+/*\n+ * The members of struct bpf_testmod_nullable_args are trusted the way the\n+ * verifier's BTF_TYPE_SAFE_TRUSTED() lists vmlinux members: through a struct\n+ * of the same name with the __safe_trusted suffix in the BTF of the module.\n+ */\n+struct bpf_testmod_nullable_args__safe_trusted {\n+\tstruct task_struct *task;\n+\tstruct task_struct *task_or_null;\n+};\n+\n+/*\n+ * Call test_nullable() with @task_or_null unset, then set. The callback\n+ * returns 1 when it found a task there, so the expected result is 2.\n+ */\n+__bpf_kfunc int bpf_testmod_ops3_call_test_nullable(void)\n+{\n+\tstruct bpf_testmod_nullable_args args = { .task = current };\n+\tint ret;\n+\n+\tBTF_TYPE_EMIT(struct bpf_testmod_nullable_args__safe_trusted);\n+\n+\tret = st_ops3-\u003etest_nullable(\u0026args);\n+\targs.task_or_null = current;\n+\treturn ret | st_ops3-\u003etest_nullable(\u0026args) \u003c\u003c 1;\n+}\n+\n struct bpf_testmod_btf_type_tag_1 {\n \tint a;\n };\n@@ -926,6 +977,8 @@ BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena)\n BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_nullable)\n BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_stack)\n BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_multislot)\n+BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_arena_member)\n+BTF_ID_FLAGS(func, bpf_testmod_ops3_call_test_nullable)\n BTF_ID_FLAGS(func, bpf_kfunc_get_default_trusted_ptr_test);\n BTF_ID_FLAGS(func, bpf_kfunc_put_default_trusted_ptr_test);\n BTF_ID_FLAGS(func, bpf_kfunc_kasan_poison)\ndiff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h\nindex 2b12f81bf7487..e92f9fbfe6a67 100644\n--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h\n+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h\n@@ -114,6 +114,48 @@ struct bpf_testmod_arena_pair {\n \tu64 b;\n };\n \n+/*\n+ * The kernel defines the BPF pointer qualifiers, a BPF program gets __arena\n+ * from bpf_arena_common.h and user space needs neither.\n+ */\n+#ifndef __arena\n+#define __arena\n+#endif\n+#ifndef __nullable\n+#define __nullable\n+#endif\n+\n+struct bpf_testmod_arena_nested {\n+\tu64 __arena *ptr;\n+};\n+\n+union bpf_testmod_arena_union {\n+\tu64 __arena *ptr;\n+\tu64 raw;\n+};\n+\n+/* Arguments of test_arena_member(), the pointers hold kernel addresses of arena memory */\n+struct bpf_testmod_arena_member_args {\n+\t/* the value the caller passed, never NULL */\n+\tu64 __arena *ptr;\n+\t/* always NULL */\n+\tu64 __arena __nullable *null_ptr;\n+\t/* the same value again, behind a nested struct */\n+\tstruct bpf_testmod_arena_nested nested;\n+\t/* the same value again, but a load from a union is rejected */\n+\tunion bpf_testmod_arena_union u;\n+\t/* a scalar, loaded by the same insn as @ptr in a verifier test */\n+\tu64 cookie;\n+};\n+\n+/* Arguments of test_nullable(), trusted through the __safe_trusted alias in the module */\n+struct bpf_testmod_nullable_args {\n+\t/* never NULL */\n+\tstruct task_struct *task;\n+\t/* NULL on the first call, set on the second */\n+\tstruct task_struct __nullable *task_or_null;\n+};\n+\n struct bpf_testmod_ops3 {\n \tint (*test_1)(void);\n \tint (*test_2)(void);\n@@ -125,6 +167,10 @@ struct bpf_testmod_ops3 {\n \t\t\t\tu64 g, u64 h, u64 *ptr);\n \t/* a multi-slot leading arg, so @ptr is not at the slot its arg index suggests */\n \tint (*test_arena_multislot)(struct bpf_testmod_arena_pair p, u64 *ptr);\n+\t/* Used to test __arena pointer members. */\n+\tint (*test_arena_member)(struct bpf_testmod_arena_member_args *args);\n+\t/* Used to test __nullable pointer members. */\n+\tint (*test_nullable)(struct bpf_testmod_nullable_args *args);\n };\n \n struct st_ops_args {\ndiff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h b/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h\nindex 67c02a421d133..8b85b2d73e1ca 100644\n--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h\n+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h\n@@ -218,6 +218,8 @@ int bpf_testmod_ops3_call_test_arena(__u64 *ptr__arena) __ksym;\n int bpf_testmod_ops3_call_test_arena_nullable(__u64 *ptr__arena__nullable) __ksym;\n int bpf_testmod_ops3_call_test_arena_stack(__u64 *ptr__arena) __ksym;\n int bpf_testmod_ops3_call_test_arena_multislot(__u64 *ptr__arena) __ksym;\n+int bpf_testmod_ops3_call_test_arena_member(__u64 *ptr__arena) __ksym;\n+int bpf_testmod_ops3_call_test_nullable(void) __ksym;\n \n __u64 bpf_kfunc_call_test1(struct sock *sk, __u32 a, __u64 b,\n \t\t\t\t__u32 c, __u64 d) __ksym;\n"
}

2/1 2026/10/13 08:44 agent patch-evaluator
5m Model:
core

Args:
null

Results:
{
  "EnableConfigs": [
    "BPF_SYSCALL",
    "BPF_JIT",
    "DEBUG_INFO_BTF"
  ],
  "FocusSymbols": [
    "arena_member_fixup",
    "check_arena_member_load",
    "btf_struct_access",
    "btf_type_is_arena_ptr",
    "emit_kfunc_arena_args"
  ],
  "Reasoning": "The patch introduces support for __arena and __nullable BTF type tags on kernel struct members accessed by BPF programs. This touches core BPF verification logic (BTF struct walking and member load checking), BPF instruction rewriting (arena_member_fixup), and x86 JIT compilation (arena base tracking and emit_kfunc_arena_args assertions). All modified code is reachable via the bpf() syscall in standard virtualized environments.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer and security engineer.
Your job is to review a provided patch series and evaluate whether it warrants fuzzing with syzkaller.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on internal assumptions. You must actively use your code access
tools to inspect the actual source code, callers, and surrounding context.

================================================================================
1. CORE TRIAGE PHILOSOPHY
================================================================================
The goal of patch fuzzing is to discover crashes, regressions, exposed latent bugs,
and newly triggered assertions introduced by the patch series.

- REACHABILITY IS THE PRIMARY GATE:
  Fuzzing can only discover bugs in code that can actually execute in standard virtualized
  environments (GCE or QEMU, utilizing software-emulated devices like USB gadgets, netdev, tun/tap).
  If the modified code is structurally unreachable (see Section 2), it MUST NOT be fuzzed,
  regardless of whether it adds assertions or complex logic.

- DO NOT BLINDLY TRUST "NO FUNCTIONAL CHANGE" (NFCI) OR "REFACTORING" CLAIMS:
  Patch authors routinely label changes as "cleanups", "refactorings", or state
  "No functional change intended". Do NOT take these claims at face value.
  Code refactorings that rearrange logic, introduce helper functions, or alter state management
  in core subsystems frequently introduce subtle semantic shifts or uncover latent kernel bugs.
  If reachable executable code is modified or refactored, it MUST be fuzzed.

- NEW OR MODIFIED ASSERTIONS IN REACHABLE CODE MUST BE FUZZED:
  When a patch introduces or modifies runtime checks or assertions (e.g., WARN_ON*, VM_WARN_ON*,
  BUG_ON*, lockdep_assert*) in reachable code paths, it enforces new or stricter invariants.
  Even if the author believes the invariant always holds, fuzzing is essential to verify whether
  an unusual sequence of operations can violate it.

================================================================================
2. WHEN TO RETURN WorthFuzzing=false (NEGATIVE CRITERIA)
================================================================================
Return WorthFuzzing=false ONLY IF all modified code falls strictly into one or more of these categories:

- Non-kernel and non-executable changes:
  * Modifications to Documentation/, comments, or spelling fixes.
  * User-space directories, self-tests, samples, or scripts (e.g., tools/, samples/, scripts/, usr/)
    that do not affect the compiled kernel image (vmlinux) or kernel modules.
  * Purely decorative logging (e.g., message strings in pr_err, printk, dev_info) or tracepoints
    that do not alter control flow or data structures.
  * Build system or Kconfig changes that do not alter compiled C logic.
- Structurally unreachable hardware:
  * Vendor-specific PCIe switches, SmartNICs, or GPU drivers (e.g., mlxsw, pds_core, qed,
    ionic, amdgpu) requiring physical ASIC/PCIe cards not emulated in standard QEMU.
- Unreachable execution paths:
  * Driver teardown callbacks (.remove, .shutdown, pci_unregister_driver) executed only during
    physical PCI hot-unplug or manual sysfs driver unbinding.
  * Code paths exclusive to architectures other than the target architecture.

================================================================================
3. WHEN TO RETURN WorthFuzzing=true (POSITIVE CRITERIA)
================================================================================
Return WorthFuzzing=true whenever the patch touches reachable executable code, including:
- Core Subsystems:
  * Any logic modifications in memory management (mm/), synchronization/locking (kernel/locking/),
    BPF, scheduler, core networking, VFS, or syscall handling.
- Refactorings and Code Cleanups:
  * Any restructuring of reachable data structures, helper abstractions, or algorithm flows.
- Runtime Assertions and Defensive Checks:
  * Any introduction or alteration of assertions (WARN_ON*, VM_WARN_ON*, BUG_ON*, etc.) in reachable paths.
- Reachable Drivers and Protocols:
  * Drivers accessible via virtual buses (virtio, USB gadget, loopback, netlink, binder, sockets, etc.).

================================================================================
4. EXTRACTING FocusSymbols (PREVENTING DILUTION)
================================================================================
When WorthFuzzing=true, you must extract specific kernel functions into FocusSymbols to guide the fuzzer:

- AVOID UBIQUITOUS LIFECYCLE HOT-PATHS:
  Do NOT list generic, ubiquitous functions called by almost every program in the corpus
  (including, but not limited to: general memory allocators and deallocators, page fault
  and trap handlers, or core synchronization primitives; this is not an exhaustive list).
  Listing ubiquitous functions causes the fuzzer to classify thousands of unrelated tests as "focused",
  which severely dilutes fuzzing effort away from the actual changes.

- TARGET SPECIFIC FEATURE LOGIC AND ENTRYPOINTS:
  List functions that specifically implement the logic being added or altered, or direct API entrypoints
  for the subsystem feature under review.

- HANDLING STATIC INLINE FUNCTIONS IN HEADERS (.h):
  Compiler-inlined static functions (such as static inlines in mm/*.h or include/linux/*.h) lack
  distinct symbol addresses in vmlinux and cannot be targeted directly by symbol coverage filters.
  If the changes are primarily in static inline helpers, identify non-static, feature-specific caller
  functions in .c files that exercise them (avoiding ubiquitous lifecycle wrappers).

================================================================================
5. IDENTIFYING EnableConfigs
================================================================================
Identify any specific CONFIG_ options required to properly compile and reach the modified code:
- Inspect Kconfig files and #ifdef guards; do not make assumptions.
- Check "depends on" lines in Kconfig to include any non-standard parent subsystem configs needed.
- Strip any 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
Target architecture: amd64

For your convenience, here is the diff of the changes:
commit 7aedf27ed55ba9cc9fbee2220c02ac479c9b901e
Author: syz-cluster <triage@syzkaller.com>
Date:   Tue Oct 13 08:44:52 2026 +0000

    syz-cluster: applied patch under review

diff --git a/arch/arm64/net/bpf_jit_comp.c b/arch/arm64/net/bpf_jit_comp.c
index 506c4201fec85..1d5f4cf88d331 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 e78eb582c3005..9393e4b0db8e1 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 7a612688e7a20..0d6ea6df01065 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 5265dd5bd39e1..f2497ec7d3788 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 1b2566012b63a..0fe629f0b66fc 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 083fcd6cf15b7..3130d9ecbb994 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 54144372281cb..b0a5b2d5b7560 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 54cb6d3d06535..948daef06c7d4 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -791,6 +791,14 @@ struct bpf_insn_aux_data {
 	 */
 	u8 fastcall_spills_num:3;
 	u8 arg_prog:4;
+	/*
+	 * What a load found, on any path: an __arena pointer member of a
+	 * kernel struct, which arena_member_fixup() rewrites, one that is
+	 * __nullable as well, or anything else through any pointer.
+	 */
+	u8 arena_member:1;
+	u8 arena_member_nullable:1;
+	u8 non_arena_load:1;
 
 	/* below fields are initialized once */
 	unsigned int orig_idx; /* original instruction index */
@@ -958,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/compiler_types.h b/include/linux/compiler_types.h
index c5921f1390079..e97ee2d4d1dfb 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
diff --git a/include/linux/filter.h b/include/linux/filter.h
index 9339c6131f8ff..f5518bae48556 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/btf.c b/kernel/bpf/btf.c
index b09c04ccdc6db..b992027b60b02 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)
 {
@@ -7689,6 +7695,7 @@ enum bpf_struct_walk_result {
 	WALK_SCALAR = 0,
 	WALK_PTR,
 	WALK_PTR_UNTRUSTED,
+	WALK_PTR_ARENA,
 	WALK_STRUCT,
 };
 
@@ -7961,6 +7968,38 @@ 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;
+
+			/*
+			 * 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;
@@ -8063,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 b2fa7e8bedf3a..a1f2cfefda76a 100644
--- a/kernel/bpf/fixups.c
+++ b/kernel/bpf/fixups.c
@@ -870,6 +870,36 @@ int bpf_opt_subreg_zext_lo32_rnd_hi32(struct bpf_verifier_env *env,
  *     struct __sk_buff    -> struct sk_buff
  *     struct bpf_sock_ops -> struct sock
  */
+/*
+ * A load through an __arena pointer member of a kernel struct reads the kernel
+ * address of arena memory. Append the conversion to the arena pointer form,
+ * (u32)(kaddr - kern_vm_start), that the struct_ops trampoline applies to
+ * __arena arguments. A __nullable member keeps NULL as NULL, tested on the
+ * full kernel address. Returns the number of insns in @buf, 0 when the insn
+ * loads no such member, or an error when it does so on some paths only.
+ */
+static int arena_member_fixup(struct bpf_verifier_env *env, int idx, struct bpf_insn *insn,
+			      struct bpf_insn *buf)
+{
+	struct bpf_insn_aux_data *aux = &env->insn_aux_data[idx];
+	int cnt = 0;
+	u32 base;
+
+	if (!aux->arena_member)
+		return 0;
+	if (aux->non_arena_load) {
+		verbose(env, "an insn loads an arena pointer member on one path and something else on another\n");
+		return -EINVAL;
+	}
+
+	base = bpf_arena_get_kern_vm_start(env->prog->aux->arena);
+	buf[cnt++] = *insn;
+	if (aux->arena_member_nullable)
+		buf[cnt++] = BPF_JMP_IMM(BPF_JEQ, insn->dst_reg, 0, 1);
+	buf[cnt++] = BPF_ALU32_IMM(BPF_SUB, insn->dst_reg, (s32)base);
+	return cnt;
+}
+
 int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)
 {
 	struct bpf_subprog_info *subprogs = env->subprog_info;
@@ -1093,11 +1123,23 @@ int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)
 					insn->code = BPF_LDX | BPF_PROBE_MEMSX |
 						     BPF_SIZE(insn->code);
 				env->prog->aux->num_exentries++;
-				continue;
-			}
-			if (verifier_bug_if(bpf_may_fault_on_deref(ptr_type), env,
-					    "access to a fault prone pointer is not rewritten as a probed one"))
+			} else if (verifier_bug_if(bpf_may_fault_on_deref(ptr_type), env,
+						   "access to a fault prone pointer is not rewritten as a probed one")) {
 				return -EFAULT;
+			}
+
+			cnt = arena_member_fixup(env, i + delta, insn, insn_buf);
+			if (cnt < 0)
+				return cnt;
+			if (!cnt)
+				continue;
+			new_prog = bpf_patch_insn_data(env, i + delta, insn_buf, cnt);
+			if (!new_prog)
+				return -ENOMEM;
+
+			delta    += cnt - 1;
+			env->prog = new_prog;
+			insn      = new_prog->insnsi + i + delta;
 			continue;
 		}
 
@@ -1366,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 2f190abe34489..63dd5da0708ff 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)
 {
@@ -6552,6 +6564,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 +6709,9 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
 	if (ret < 0)
 		return ret;
 
+	if (ret == PTR_TO_ARENA)
+		return check_arena_member_load(env, regs, value_regno, flag, tname, field_name);
+
 	if (ret != PTR_TO_BTF_ID) {
 		/* just mark; */
 
@@ -6721,6 +6772,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)
@@ -7330,6 +7388,7 @@ static int check_mem_access(struct bpf_verifier_env *env, int insn_idx, struct b
 		if (!err && value_regno >= 0 && (rdonly_mem || t == BPF_READ))
 			mark_reg_unknown(env, regs, value_regno);
 	} else if (reg->type == PTR_TO_ARENA) {
+		mark_uses_arena_insn(env);
 		if (t == BPF_READ && value_regno >= 0)
 			mark_reg_unknown(env, regs, value_regno);
 	} else {
@@ -7389,6 +7448,7 @@ static struct bpf_reg_state *mem_access_reg(struct bpf_verifier_env *env, int re
 	memset(arena, 0, sizeof(*arena));
 	arena->type = PTR_TO_ARENA;
 	env->insn_aux_data[env->insn_idx].arena_scalar = true;
+	mark_uses_arena_insn(env);
 	return arena;
 }
 
@@ -7432,6 +7492,17 @@ static int check_load_mem(struct bpf_verifier_env *env, struct bpf_insn *insn,
 	err = check_mem_access(env, env->insn_idx, src_reg, argno_from_reg(insn->src_reg),
 			       insn->off, BPF_SIZE(insn->code), BPF_READ, insn->dst_reg,
 			       strict_alignment_once, is_ldsx);
+	/*
+	 * Paths through a kernel struct pointer and through other pointers
+	 * may share the insn, see save_aux_ptr_type(). Record a load that did
+	 * not produce an arena pointer from an __arena member, the only load
+	 * through a kernel struct pointer that does, next to the one
+	 * check_arena_member_load() records, so that arena_member_fixup() can
+	 * tell when the paths disagree.
+	 */
+	if (!err && !(base_type(src_reg_type) == PTR_TO_BTF_ID &&
+		      regs[insn->dst_reg].type == PTR_TO_ARENA))
+		env->insn_aux_data[env->insn_idx].non_arena_load = true;
 	err = err ?: save_aux_ptr_type(env, src_reg_type,
 				       allow_trust_mismatch);
 	err = err ?: reg_bounds_sanity_check(env, &regs[insn->dst_reg], ctx);
@@ -9867,6 +9938,9 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
 		}
 		break;
 	case ARG_PTR_TO_ARENA:
+		/* the JIT rebases the __arena argument of a kfunc on the arena base */
+		if (is_kfunc(meta))
+			mark_uses_arena_insn(env);
 		break;
 	case ARG_PTR_TO_ALLOC_BTF_ID:
 		if (reg->type == (PTR_TO_BTF_ID | MEM_ALLOC)) {
@@ -17478,6 +17552,7 @@ static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
 					verbose(env, "addr_space_cast insn can only be used in a program that has an associated arena\n");
 					return -EINVAL;
 				}
+				mark_uses_arena_insn(env);
 			}
 
 			/* check src operand */
diff --git a/tools/lib/bpf/btf_dump.c b/tools/lib/bpf/btf_dump.c
index 996e61b5f11f0..7dedd3975c395 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;
diff --git a/tools/lib/bpf/features.c b/tools/lib/bpf/features.c
index 9f589980beef6..71c6a666afcb2 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 cb09ded907732..0cd2c25a120d4 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -794,7 +794,12 @@ struct bpf_object {
 
 	int *fd_array;
 	size_t fd_array_cap;
-	size_t fd_array_cnt;
+	/*
+	 * Slots handed out so far, slot 0 included. A kfunc insn names a module
+	 * BTF slot through its offset, slot 0 stands for vmlinux BTF and stays
+	 * empty, so the array goes to the kernel without a count.
+	 */
+	size_t fd_array_used;
 
 	struct usdt_manager *usdt_man;
 
@@ -8799,6 +8804,46 @@ static int libbpf_prepare_prog_load(struct bpf_program *prog,
 
 static void fixup_verifier_log(struct bpf_program *prog, char *buf, size_t buf_sz);
 
+/*
+ * Bind the arena of the object to the program about to be loaded. A program
+ * has an arena only when an insn refers to the map, but the kernel can hand
+ * it arena pointers all the same, as struct_ops arguments, through tagged
+ * struct members or as kfunc return values, and the verifier rejects those
+ * when the program has no arena to place them in. A continuous fd_array
+ * binds every map in it, the arena takes slot 0, which is free: a kfunc insn
+ * with offset 0 names vmlinux BTF rather than a slot.
+ */
+static int bpf_object__bind_arena(struct bpf_object *obj, struct bpf_program *prog,
+				  struct bpf_prog_load_opts *load_attr)
+{
+	struct bpf_map *map;
+	int err;
+
+	if (obj->arena_map_idx < 0 || !kernel_supports(obj, FEAT_FD_ARRAY_CNT))
+		return 0;
+	map = &obj->maps[obj->arena_map_idx];
+	/*
+	 * Nothing to bind when the arena is not created, when it is exclusive
+	 * to another program, or for an offloaded program, which the kernel
+	 * does not let near an arena.
+	 */
+	if (!map->autocreate || (map->excl_prog && map->excl_prog != prog) ||
+	    load_attr->prog_ifindex)
+		return 0;
+
+	if (!obj->fd_array_used) {
+		err = libbpf_ensure_mem((void **)&obj->fd_array, &obj->fd_array_cap, sizeof(int), 1);
+		if (err)
+			return err;
+		obj->fd_array_used = 1;
+	}
+	obj->fd_array[0] = map->fd;
+	load_attr->fd_array = obj->fd_array;
+	/* with slot 0 filled every slot handed out is valid, so the array can go with a count */
+	load_attr->fd_array_cnt = obj->fd_array_used;
+	return 0;
+}
+
 static int bpf_object_load_prog(struct bpf_object *obj, struct bpf_program *prog,
 				struct bpf_insn *insns, int insns_cnt,
 				const char *license, __u32 kern_version, int *prog_fd)
@@ -8862,6 +8907,11 @@ static int bpf_object_load_prog(struct bpf_object *obj, struct bpf_program *prog
 	if (obj->data_in_arena)
 		load_attr.prog_flags |= BPF_F_ARENA_SCALAR;
 	load_attr.fd_array = obj->fd_array;
+	if (!obj->gen_loader) {
+		err = bpf_object__bind_arena(obj, prog, &load_attr);
+		if (err)
+			return err;
+	}
 
 	load_attr.token_fd = obj->token_fd;
 	if (obj->token_fd)
@@ -9716,22 +9766,22 @@ static int bpf_object__resolve_ksym_func_btf_id(struct bpf_object *obj,
 	/* set index for module BTF fd in fd_array, if unset */
 	if (mod_btf && !mod_btf->fd_array_idx) {
 		/* insn->off is s16 */
-		if (obj->fd_array_cnt == INT16_MAX) {
+		if (obj->fd_array_used == INT16_MAX) {
 			pr_warn("extern (func ksym) '%s': module BTF fd index %d too big to fit in bpf_insn offset\n",
 				ext->name, mod_btf->fd_array_idx);
 			return -E2BIG;
 		}
 		/* Cannot use index 0 for module BTF fd */
-		if (!obj->fd_array_cnt)
-			obj->fd_array_cnt = 1;
+		if (!obj->fd_array_used)
+			obj->fd_array_used = 1;
 
 		ret = libbpf_ensure_mem((void **)&obj->fd_array, &obj->fd_array_cap, sizeof(int),
-					obj->fd_array_cnt + 1);
+					obj->fd_array_used + 1);
 		if (ret)
 			return ret;
-		mod_btf->fd_array_idx = obj->fd_array_cnt;
+		mod_btf->fd_array_idx = obj->fd_array_used;
 		/* we assume module BTF FD is always >0 */
-		obj->fd_array[obj->fd_array_cnt++] = mod_btf->fd;
+		obj->fd_array[obj->fd_array_used++] = mod_btf->fd;
 	}
 
 	ext->is_set = true;
@@ -9961,8 +10011,10 @@ static void bpf_object_cleanup_btf(struct bpf_object *obj)
 
 static void bpf_object_post_load_cleanup(struct bpf_object *obj)
 {
-	/* clean up fd_array */
+	/* clean up fd_array, and the slots handed out with it */
 	zfree(&obj->fd_array);
+	obj->fd_array_cap = 0;
+	obj->fd_array_used = 0;
 
 	/* clean up BTF */
 	bpf_object_cleanup_btf(obj);
@@ -10940,7 +10992,13 @@ int bpf_program__clone(struct bpf_program *prog, const struct bpf_prog_load_opts
 	attr.prog_ifindex = OPTS_GET(opts, prog_ifindex, 0) ?: prog->prog_ifindex;
 	attr.kern_version = OPTS_GET(opts, kern_version, 0) ?: obj->kern_version;
 	attr.fd_array = OPTS_GET(opts, fd_array, NULL) ?: obj->fd_array;
-	attr.fd_array_cnt = OPTS_GET(opts, fd_array_cnt, 0) ?: obj->fd_array_cnt;
+	attr.fd_array_cnt = OPTS_GET(opts, fd_array_cnt, 0);
+	/* the arena of the object goes with the program, as in bpf_object__load() */
+	if (!obj->gen_loader && !OPTS_GET(opts, fd_array, NULL)) {
+		err = bpf_object__bind_arena(obj, prog, &attr);
+		if (err)
+			return libbpf_err(err);
+	}
 	attr.token_fd = OPTS_GET(opts, token_fd, 0) ?: obj->token_fd;
 	if (attr.token_fd)
 		attr.prog_flags |= BPF_F_TOKEN_FD;
diff --git a/tools/lib/bpf/libbpf_internal.h b/tools/lib/bpf/libbpf_internal.h
index 4e45dfacb548a..22d9fe512ea1a 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,
 };
 
diff --git a/tools/testing/selftests/bpf/prog_tests/btf_dump.c b/tools/testing/selftests/bpf/prog_tests/btf_dump.c
index 17fa44cb9c91f..35f214e254da8 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/prog_tests/clone_module_kfunc.c b/tools/testing/selftests/bpf/prog_tests/clone_module_kfunc.c
new file mode 100644
index 0000000000000..fa6ccbaa7fd98
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/clone_module_kfunc.c
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <test_progs.h>
+#include "kfunc_call_test.skel.h"
+
+/*
+ * bpf_program__clone() loads a program from a prepared object with the
+ * fd_array of the object, where module BTF fds start at slot 1 and slot 0 is
+ * empty. It used to pass the fill index of that array as fd_array_cnt, which
+ * made the kernel resolve slot 0 and fail the load.
+ */
+void test_clone_module_kfunc(void)
+{
+	struct kfunc_call_test *skel;
+	int err, fd;
+
+	skel = kfunc_call_test__open();
+	if (!ASSERT_OK_PTR(skel, "skel_open"))
+		return;
+
+	err = bpf_object__prepare(skel->obj);
+	if (!ASSERT_OK(err, "prepare"))
+		goto out;
+
+	fd = bpf_program__clone(skel->progs.kfunc_call_test1, NULL);
+	if (ASSERT_GE(fd, 0, "clone"))
+		close(fd);
+out:
+	kfunc_call_test__destroy(skel);
+}
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 7f9f54ba3fbe5..149b130f8cdb7 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
@@ -1,10 +1,20 @@
 // SPDX-License-Identifier: GPL-2.0
 /* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
 #include <test_progs.h>
+#include <bpf/btf.h>
 
 #include "struct_ops_arena.skel.h"
 #include "struct_ops_arena_attach.skel.h"
 #include "struct_ops_arena_fail.skel.h"
+#include "struct_ops_arena_member.skel.h"
+#include "struct_ops_arena_member_render.skel.h"
+#include "struct_ops_arena_member_fail__union.skel.h"
+#include "struct_ops_arena_member_fail__load_acq.skel.h"
+#include "struct_ops_arena_member_fail__mismatch.skel.h"
+#include "struct_ops_arena_member_fail__mismatch_mem.skel.h"
+#include "struct_ops_arena_member_fail__write.skel.h"
+#include "struct_ops_arena_member_fail__no_arena.skel.h"
+#include "struct_ops_arena_autobind.skel.h"
 
 #if defined(__x86_64__) || defined(__aarch64__)
 /*
@@ -101,6 +111,121 @@ static void arena_arg_attach(void)
 
 	struct_ops_arena__destroy(skel);
 }
+
+/* Attach @st_ops and run @trigger once, it returns 0 when everything checked out. */
+static void attach_and_trigger(struct bpf_map *st_ops, struct bpf_program *trigger)
+{
+	LIBBPF_OPTS(bpf_test_run_opts, topts);
+	struct bpf_link *link;
+	int err;
+
+	link = bpf_map__attach_struct_ops(st_ops);
+	if (!ASSERT_OK_PTR(link, "attach_struct_ops"))
+		return;
+
+	err = bpf_prog_test_run_opts(bpf_program__fd(trigger), &topts);
+	ASSERT_OK(err, "test_run");
+	ASSERT_EQ(topts.retval, 0, "trigger_retval");
+
+	bpf_link__destroy(link);
+}
+
+/*
+ * The tags on the members of struct bpf_testmod_arena_member_args reach the
+ * module BTF only from a toolchain with btf_type_tag support.
+ */
+static bool testmod_has_type_tag(const char *tag)
+{
+	struct btf *vmlinux_btf, *module_btf;
+	bool found = false;
+
+	vmlinux_btf = btf__load_vmlinux_btf();
+	if (!ASSERT_OK_PTR(vmlinux_btf, "vmlinux_btf"))
+		return false;
+	module_btf = btf__load_module_btf("bpf_testmod", vmlinux_btf);
+	if (ASSERT_OK_PTR(module_btf, "module_btf"))
+		found = btf__find_by_name_kind(module_btf, tag, BTF_KIND_TYPE_TAG) > 0;
+	if (!found)
+		printf("%s:SKIP: no %s type tag in the bpf_testmod BTF\n", __func__, tag);
+	btf__free(module_btf);
+	btf__free(vmlinux_btf);
+	return found;
+}
+
+/*
+ * Attach a callback that reads arena pointers out of its argument struct.
+ * The header is shared with the module, so the members are arena pointers
+ * on the program side as well.
+ */
+static void arena_member(void)
+{
+	struct struct_ops_arena_member *skel;
+
+	skel = struct_ops_arena_member__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "struct_ops_arena_member__open_and_load"))
+		return;
+
+	attach_and_trigger(skel->maps.testmod_arena_member, skel->progs.trigger);
+	struct_ops_arena_member__destroy(skel);
+}
+
+/* @prog_fd lists exactly one map, the one with @map_id. */
+static void assert_prog_map(int prog_fd, __u32 map_id, const char *name)
+{
+	__u32 map_ids[2] = {}, len = sizeof(struct bpf_prog_info);
+	struct bpf_prog_info info = {
+		.nr_map_ids = ARRAY_SIZE(map_ids),
+		.map_ids = ptr_to_u64(map_ids),
+	};
+	int err;
+
+	err = bpf_prog_get_info_by_fd(prog_fd, &info, &len);
+	if (!ASSERT_OK(err, name))
+		return;
+	if (!ASSERT_EQ(info.nr_map_ids, 1, name))
+		return;
+	ASSERT_EQ(map_ids[0], map_id, name);
+}
+
+/*
+ * libbpf binds the arena of an object to every program in it: a callback
+ * with an __arena argument loads without referring to the arena, and an
+ * unrelated program gets the arena as well.
+ */
+static void arena_autobind(void)
+{
+	struct struct_ops_arena_autobind *skel;
+	struct bpf_map_info info = {};
+	__u32 len = sizeof(info);
+	int err;
+
+	skel = struct_ops_arena_autobind__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "struct_ops_arena_autobind__open_and_load"))
+		return;
+
+	err = bpf_map_get_info_by_fd(bpf_map__fd(skel->maps.arena), &info, &len);
+	if (!ASSERT_OK(err, "arena_info"))
+		goto out;
+	assert_prog_map(bpf_program__fd(skel->progs.test_arena_cb), info.id, "test_arena_cb");
+	assert_prog_map(bpf_program__fd(skel->progs.unrelated), info.id, "unrelated");
+
+	attach_and_trigger(skel->maps.testmod_arena_autobind, skel->progs.trigger);
+out:
+	struct_ops_arena_autobind__destroy(skel);
+}
+
+/* The same through the vmlinux.h rendering of the members, which takes casts. */
+static void arena_member_render(void)
+{
+	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 +247,20 @@ void serial_test_struct_ops_arena(void)
 		arena_arg_fail();
 	if (test__start_subtest("arena_arg_attach"))
 		arena_arg_attach();
+	if (testmod_has_type_tag("arena")) {
+		if (test__start_subtest("arena_member"))
+			arena_member();
+		if (test__start_subtest("arena_member_render"))
+			arena_member_render();
+		RUN_TESTS(struct_ops_arena_member_fail__union);
+		RUN_TESTS(struct_ops_arena_member_fail__load_acq);
+		RUN_TESTS(struct_ops_arena_member_fail__mismatch);
+		RUN_TESTS(struct_ops_arena_member_fail__mismatch_mem);
+		RUN_TESTS(struct_ops_arena_member_fail__write);
+		RUN_TESTS(struct_ops_arena_member_fail__no_arena);
+	}
+	if (test__start_subtest("arena_autobind"))
+		arena_autobind();
 #else
 	test__skip();
 #endif
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 0000000000000..17d5de78273ae
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_nullable.c
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <test_progs.h>
+#include <bpf/btf.h>
+
+#include "struct_ops_nullable.skel.h"
+#include "struct_ops_nullable_fail.skel.h"
+
+/*
+ * Attach a callback that gets a trusted and a trusted __nullable task pointer
+ * through its argument struct and drive it through the
+ * bpf_testmod_ops3_call_test_nullable() kfunc.
+ */
+static void nullable_member(void)
+{
+	LIBBPF_OPTS(bpf_test_run_opts, topts);
+	struct struct_ops_nullable *skel;
+	struct bpf_link *link;
+	int err;
+
+	skel = struct_ops_nullable__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "struct_ops_nullable__open_and_load"))
+		return;
+
+	link = bpf_map__attach_struct_ops(skel->maps.testmod_nullable);
+	if (!ASSERT_OK_PTR(link, "attach_struct_ops"))
+		goto out;
+
+	err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.trigger), &topts);
+	ASSERT_OK(err, "test_run");
+	ASSERT_EQ(topts.retval, 0, "trigger_retval");
+
+	bpf_link__destroy(link);
+out:
+	struct_ops_nullable__destroy(skel);
+}
+
+/* Serialized because it attaches the singleton bpf_testmod_ops3. */
+/*
+ * The tag on the member of struct bpf_testmod_nullable_args reaches the module
+ * BTF only from a toolchain with btf_type_tag support.
+ */
+static bool testmod_has_nullable_tag(void)
+{
+	struct btf *vmlinux_btf, *module_btf;
+	bool found = false;
+
+	vmlinux_btf = btf__load_vmlinux_btf();
+	if (!ASSERT_OK_PTR(vmlinux_btf, "vmlinux_btf"))
+		return false;
+	module_btf = btf__load_module_btf("bpf_testmod", vmlinux_btf);
+	if (ASSERT_OK_PTR(module_btf, "module_btf"))
+		found = btf__find_by_name_kind(module_btf, "nullable", BTF_KIND_TYPE_TAG) > 0;
+	btf__free(module_btf);
+	btf__free(vmlinux_btf);
+	return found;
+}
+
+void serial_test_struct_ops_nullable(void)
+{
+	if (!testmod_has_nullable_tag()) {
+		test__skip();
+		return;
+	}
+	if (test__start_subtest("nullable_member"))
+		nullable_member();
+	RUN_TESTS(struct_ops_nullable_fail);
+}
diff --git a/tools/testing/selftests/bpf/prog_tests/verifier.c b/tools/testing/selftests/bpf/prog_tests/verifier.c
index c69c0f7e9cc89..6df440d826750 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 96f7aacf4d9f2..168ae8d7e303e 100644
--- a/tools/testing/selftests/bpf/progs/arena_kfunc.c
+++ b/tools/testing/selftests/bpf/progs/arena_kfunc.c
@@ -192,14 +192,41 @@ int arena_arg_unpopulated(void *ctx)
 	return 0;
 }
 
+/*
+ * A subprogram whose only arena use is the argument of a kfunc needs the
+ * arena base too, the JIT rebases the argument on it. The address arrives
+ * as a number, so no cast marks the subprogram either.
+ */
+__noinline int arena_arg_from_subprog(unsigned long addr)
+{
+	return bpf_kfunc_arena_arg_test((u64 *)addr) != 41;
+}
+
 SEC("syscall")
 __arch_x86_64
 __arch_arm64
-__failure __msg("arena pointer requires a program with an associated arena")
-int arena_arg_no_arena(void *ctx)
+__success __retval(0)
+int arena_arg_subprog(void *ctx)
 {
-	bpf_kfunc_arena_arg_test((u64 *)1);
+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST)
+	u64 __arena *val;
+	int ret;
+
+	val = bpf_arena_alloc_pages(&arena, NULL, 1, NUMA_NO_NODE, 0);
+	if (!val)
+		return 1;
+
+	*val = 41;
+	if (arena_arg_from_subprog((unsigned long)val))
+		ret = 2;
+	else
+		ret = *val != 42 ? 3 : 0;
+
+	bpf_arena_free_pages(&arena, (void __arena *)val, 1);
+	return ret;
+#else
 	return 0;
+#endif
 }
 
 SEC("syscall")
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 0000000000000..edbd40cd5be8a
--- /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 0000000000000..495faa0f4a8b8
--- /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/struct_ops_arena_member.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member.c
new file mode 100644
index 0000000000000..469e62e383f2f
--- /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 0000000000000..9d7972ae5c558
--- /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 0000000000000..0b4147894122c
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch.c
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf_arena_common.h>
+#include "bpf_misc.h"
+#include "../test_kmods/bpf_testmod.h"
+
+char _license[] SEC("license") = "GPL";
+
+struct {
+	__uint(type, BPF_MAP_TYPE_ARENA);
+	__uint(map_flags, BPF_F_MMAPABLE);
+	__uint(max_entries, 1);
+} arena SEC(".maps");
+
+/*
+ * The same load reaches args->ptr on one path and args->cookie on the other,
+ * the branch on the unknown cookie value makes the verifier walk both. The
+ * rewrite is per insn, so the paths have to agree on what they load.
+ */
+SEC("struct_ops/test_arena_member")
+__failure __msg("loads an arena pointer member on one path and something else on another")
+__naked int mismatch_member(void)
+{
+	asm volatile (
+	"r6 = *(u64 *)(r1 + 0);"
+	"r2 = %[arena] ll;"
+	"r0 = *(u64 *)(r6 + %[cookie_off]);"
+	"if r0 == 0 goto 1f;"
+	"r6 += %[cookie_off];"
+"1:	r0 = *(u64 *)(r6 + 0);"
+	"r0 = 0;"
+	"exit;"
+	:
+	: __imm_addr(arena),
+	  __imm_const(cookie_off, offsetof(struct bpf_testmod_arena_member_args, cookie))
+	: __clobber_all);
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_arena_member_fail = {
+	.test_arena_member = (void *)mismatch_member,
+};
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c
new file mode 100644
index 0000000000000..ff6d4db47cfdf
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include <bpf_arena_common.h>
+#include "bpf_misc.h"
+#include "../test_kmods/bpf_testmod.h"
+
+char _license[] SEC("license") = "GPL";
+
+struct {
+	__uint(type, BPF_MAP_TYPE_ARENA);
+	__uint(map_flags, BPF_F_MMAPABLE);
+	__uint(max_entries, 1);
+} arena SEC(".maps");
+
+/*
+ * The same load reads args->ptr on one path and the memory argument on the
+ * other. The verifier lets a load insn serve a kernel struct pointer and a
+ * memory pointer, see save_aux_ptr_type(), but the rewrite of the arena
+ * member would corrupt the value the other path loads. A global function
+ * is the only place a struct_ops program of this type gets a memory pointer.
+ * The loaded value goes nowhere: an ALU insn on it would be an arena
+ * pointer on one path and a scalar on the other, which the verifier rejects
+ * on its own before the fixup pass gets to the load.
+ */
+__noinline int load_either(unsigned long long *ctx __arg_ctx, u64 *mem __arg_nonnull)
+{
+	struct bpf_testmod_arena_member_args *args = (void *)ctx[0];
+	void *p = args;
+
+	if (args->cookie == 0)
+		p = mem;
+	/* one load insn, reached with both pointers */
+	asm volatile("r0 = *(u64 *)(%[p] + 0)" :: [p]"r"(p) : "r0");
+	return 0;
+}
+
+SEC("struct_ops/test_arena_member")
+__failure __msg("loads an arena pointer member on one path and something else on another")
+int BPF_PROG(mismatch_mem, struct bpf_testmod_arena_member_args *args)
+{
+	u64 local = 0;
+
+	asm volatile("" :: "r"(&arena));
+	return load_either(ctx, &local);
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_arena_member_fail = {
+	.test_arena_member = (void *)mismatch_mem,
+};
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__no_arena.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__no_arena.c
new file mode 100644
index 0000000000000..3cf76e6615839
--- /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 0000000000000..d45666fd482c5
--- /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 0000000000000..bcf1f6586e196
--- /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 0000000000000..58516fdc13511
--- /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 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;
+	__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/progs/struct_ops_nullable.c b/tools/testing/selftests/bpf/progs/struct_ops_nullable.c
new file mode 100644
index 0000000000000..387792924c09e
--- /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 0000000000000..c15b3a8c8d39f
--- /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/progs/verifier_arena_scalar.c b/tools/testing/selftests/bpf/progs/verifier_arena_scalar.c
index bebc37f501b7e..a4f15eb21809d 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")
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
index b4f0af2aeca45..5a0556d4ce311 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
@@ -460,6 +460,16 @@ 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;
+}
+
 static struct bpf_testmod_ops3 __bpf_testmod_ops3 = {
 	.test_1 = bpf_testmod_test_3,
 	.test_2 = bpf_testmod_test_4,
@@ -467,6 +477,8 @@ 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,
 };
 
 static void bpf_testmod_test_struct_ops3(void)
@@ -507,6 +519,45 @@ __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
+ * 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 +977,8 @@ 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);
 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 2b12f81bf7487..e92f9fbfe6a67 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h
@@ -114,6 +114,48 @@ struct bpf_testmod_arena_pair {
 	u64 b;
 };
 
+/*
+ * 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 */
+	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 +167,10 @@ 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);
 };
 
 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 67c02a421d133..8b85b2d73e1ca 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,8 @@ 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,
 				__u32 c, __u64 d) __ksym;

3/2 2026/10/13 08:44 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 31670
							output: 36
							thoughts: 1767
						

Thoughts:
**BPF Subsystem Modifications and Fuzzing Strategy**

Okay, here's what I'm thinking, given this patch series. First, it's clear this touches the BPF core. I see changes across several architectures, but the primary target seems to be x86_64. The modifications are hitting the verifier, JIT compilation (specifically for x86), and introducing type tags for BPF arena handling, including `__arena` and `__nullable` pointers, and a struct member dereference fixup (`arena_member_fixup`).

My immediate thought? **WorthFuzzing=true**, absolutely. BPF is reachable via `bpf` syscalls and accessible in virtualized environments – the primary gate is open. This touches core subsystems: the verifier, the JIT, type handling, and struct access! This screams for fuzzing.

My strategy is to identify the crucial `FocusSymbols` and `EnableConfigs`. Let's dig in:

I'm seeing changes in x86 JIT compilation (`arch/x86/net/bpf_jit_comp.c`). The JIT now seems to be arena-aware. The `bpf_prog_uses_arena_insns` check is all over the place here, influencing the emitted code.

Then, there are the `kernel/bpf/btf.c` changes.  `btf_ptr_has_type_tag` is new.  `btf_type_is_arena_ptr` is modified. And the big one: `btf_struct_walk` now handles `nullable` and `arena` pointers.  `btf_struct_access` is also updated to handle the `WALK_PTR_ARENA` case. I'll need to understand how the BTF structures are built.

`kernel/bpf/fixups.c` introduces `arena_member_fixup`, which is called within `bpf_convert_ctx_accesses`.  I am also noting `jit_subprogs`.

Finally, the `kernel/bpf/verifier.c` changes are significant.  New helper: `check_arena_member_load`.  `check_ptr_to_btf_access` calls `check_arena_member_load` when `ret == PTR_TO_ARENA`, also handles `PTR_MAYBE_NULL` for nullable pointers. I'm also seeing a `mark_uses_arena_insn`, called extensively.  There are changes in memory access checks (`check_mem_access`, `check_load_mem`, `mem_access_reg`) and kfunc argument handling (`check_func_arg`) as well.

Now, about those functions and their statics.  I'm keeping Rule 4 in mind: I need to identify the specific logic being added. Specifically, it talks about handling static functions, whether they are inlined or not.  So, `arena_member_fixup`, `check_arena_member_load`, are those static functions important, or should I target the non-static ones?

I need to quickly check the kernel symbols to see if the static functions are available! I can grepper or read-file to check if the static functions `arena_member_fixup`, `check_arena_member_load`, etc. or the non-static symbols are available. The static in `.c` files *should* be available unless they are inlined. I am thinking, I should check kernel symbols in kernel/bpf/fixups.c and kernel/bpf/verifier.c to determine which approach is better to take.



4/2 2026/10/13 08:45 tool grepper
0m Args:
{
  "Expression": "arena_member_fixup",
  "PathPrefixes": [
    "kernel/bpf/fixups.c"
  ]
}

Results:
{
  "Output": "kernel/bpf/fixups.c=771=int bpf_opt_subreg_zext_lo32_rnd_hi32(struct bpf_verifier_env *env,\n--\nkernel/bpf/fixups.c-880- */\nkernel/bpf/fixups.c:881:static int arena_member_fixup(struct bpf_verifier_env *env, int idx, struct bpf_insn *insn,\nkernel/bpf/fixups.c-882-\t\t\t      struct bpf_insn *buf)\n--\nkernel/bpf/fixups.c=903=int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)\n--\nkernel/bpf/fixups.c-1130-\nkernel/bpf/fixups.c:1131:\t\t\tcnt = arena_member_fixup(env, i + delta, insn, insn_buf);\nkernel/bpf/fixups.c-1132-\t\t\tif (cnt \u003c 0)\n"
}

5/2 2026/10/13 08:45 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 34525
							output: 35
							thoughts: 111
						

Thoughts:
Okay, let's dive into this. Here's what I'm thinking:

**Examining BPF Arena and Struct Operations**

Alright, I need to thoroughly audit the BPF arena and struct_ops functionality. My initial focus is on the core code paths. I'll need to scrutinize functions like `bpf_convert_ctx_accesses`, `check_arena_member_load`, `btf_struct_access`, `check_ptr_to_btf_access`, and `emit_kfunc_arena_args`. Make sure they are correct.

Beyond that, I also need to verify that all the necessary Kconfig options are correctly set for BPF arena and struct_ops, as well as the BPF verifier and JIT. That's crucial for the features to function as intended.

To do this efficiently, I'll start by grepping the Kconfig files for both `BPF_MAP_TYPE_ARENA` and the keyword "arena" itself. This should reveal the relevant configurations and their dependencies. This is essential to guarantee it works as I'd expect.



6/2 2026/10/13 08:45 tool grepper
0m Args:
{
  "Expression": "BPF_MAP_TYPE_ARENA",
  "PathPrefixes": [
    "kernel/bpf/"
  ]
}

Results:
{
  "Output": "kernel/bpf/arena.c=86=u64 bpf_arena_get_user_vm_start(struct bpf_arena *arena)\n--\nkernel/bpf/arena.c-92- * bpf_arena_map_kern_vm_start - kern_vm_start lookup by struct bpf_map *\nkernel/bpf/arena.c:93: * @map: a BPF_MAP_TYPE_ARENA map\nkernel/bpf/arena.c-94- *\n--\nkernel/bpf/arena.c=1096=__bpf_kfunc void *bpf_arena_alloc_pages(void *p__map, void *addr__ign, u32 page_cnt,\n--\nkernel/bpf/arena.c-1101-\nkernel/bpf/arena.c:1102:\tif (map-\u003emap_type != BPF_MAP_TYPE_ARENA || flags || !page_cnt)\nkernel/bpf/arena.c-1103-\t\treturn NULL;\n--\nkernel/bpf/arena.c=1108=void *bpf_arena_alloc_pages_non_sleepable(void *p__map, void *addr__ign, u32 page_cnt,\n--\nkernel/bpf/arena.c-1113-\nkernel/bpf/arena.c:1114:\tif (map-\u003emap_type != BPF_MAP_TYPE_ARENA || flags || !page_cnt)\nkernel/bpf/arena.c-1115-\t\treturn NULL;\n--\nkernel/bpf/arena.c=1120=void *bpf_arena_alloc_pages_sleepable(void *p__map, void *addr__ign, u32 page_cnt,\n--\nkernel/bpf/arena.c-1125-\nkernel/bpf/arena.c:1126:\tif (map-\u003emap_type != BPF_MAP_TYPE_ARENA || flags || !page_cnt)\nkernel/bpf/arena.c-1127-\t\treturn NULL;\n--\nkernel/bpf/arena.c=1132=__bpf_kfunc void bpf_arena_free_pages(void *p__map, void *ptr__ign, u32 page_cnt)\n--\nkernel/bpf/arena.c-1136-\nkernel/bpf/arena.c:1137:\tif (map-\u003emap_type != BPF_MAP_TYPE_ARENA || !page_cnt || !ptr__ign)\nkernel/bpf/arena.c-1138-\t\treturn;\n--\nkernel/bpf/arena.c=1142=void bpf_arena_free_pages_non_sleepable(void *p__map, void *ptr__ign, u32 page_cnt)\n--\nkernel/bpf/arena.c-1146-\nkernel/bpf/arena.c:1147:\tif (map-\u003emap_type != BPF_MAP_TYPE_ARENA || !page_cnt || !ptr__ign)\nkernel/bpf/arena.c-1148-\t\treturn;\n--\nkernel/bpf/arena.c=1152=__bpf_kfunc int bpf_arena_reserve_pages(void *p__map, void *ptr__ign, u32 page_cnt)\n--\nkernel/bpf/arena.c-1156-\nkernel/bpf/arena.c:1157:\tif (map-\u003emap_type != BPF_MAP_TYPE_ARENA)\nkernel/bpf/arena.c-1158-\t\treturn -EINVAL;\n--\nkernel/bpf/syscall.c=250=static int bpf_map_update_value(struct bpf_map *map, struct file *map_file,\n--\nkernel/bpf/syscall.c-258-\t} else if (map-\u003emap_type == BPF_MAP_TYPE_CPUMAP ||\nkernel/bpf/syscall.c:259:\t\t   map-\u003emap_type == BPF_MAP_TYPE_ARENA ||\nkernel/bpf/syscall.c-260-\t\t   map-\u003emap_type == BPF_MAP_TYPE_STRUCT_OPS) {\n--\nkernel/bpf/syscall.c=1401=static int map_create_alloc(union bpf_attr *attr, bpfptr_t uattr, struct bpf_verifier_log *log,\n--\nkernel/bpf/syscall.c-1438-\tif (attr-\u003emap_type != BPF_MAP_TYPE_BLOOM_FILTER \u0026\u0026\nkernel/bpf/syscall.c:1439:\t    attr-\u003emap_type != BPF_MAP_TYPE_ARENA \u0026\u0026\nkernel/bpf/syscall.c-1440-\t    attr-\u003emap_type != BPF_MAP_TYPE_RHASH \u0026\u0026\n--\nkernel/bpf/syscall.c-1534-\tcase BPF_MAP_TYPE_CPUMAP:\nkernel/bpf/syscall.c:1535:\tcase BPF_MAP_TYPE_ARENA:\nkernel/bpf/syscall.c-1536-\tcase BPF_MAP_TYPE_INSN_ARRAY:\n--\nkernel/bpf/verifier.c=18806=static int check_ld_imm(struct bpf_verifier_env *env, struct bpf_insn *insn)\n--\nkernel/bpf/verifier.c-18890-\t    insn-\u003esrc_reg == BPF_PSEUDO_MAP_IDX_VALUE) {\nkernel/bpf/verifier.c:18891:\t\tif (map-\u003emap_type == BPF_MAP_TYPE_ARENA) {\nkernel/bpf/verifier.c-18892-\t\t\t__mark_reg_unknown(env, dst_reg);\n--\nkernel/bpf/verifier.c=20661=static int check_map_prog_compatibility(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-20726-\t\tcase BPF_MAP_TYPE_STACK:\nkernel/bpf/verifier.c:20727:\t\tcase BPF_MAP_TYPE_ARENA:\nkernel/bpf/verifier.c-20728-\t\tcase BPF_MAP_TYPE_INSN_ARRAY:\n--\nkernel/bpf/verifier.c-20742-\nkernel/bpf/verifier.c:20743:\tif (map-\u003emap_type == BPF_MAP_TYPE_ARENA) {\nkernel/bpf/verifier.c-20744-\t\tif (env-\u003eprog-\u003eaux-\u003earena) {\n"
}

7/2 2026/10/13 08:45 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 36559
							output: 39
							thoughts: 38
						

Thoughts:
Here's my take on the Makefile in `kernel/bpf/`...

**Examining the BPF Compilation Process**

Alright, let's dive into this. My initial instinct is to go straight to `kernel/bpf/Makefile` to understand the low-level build process for the BPF infrastructure. Specifically, I want to see how the compiler handles core components like `arena.c`, `verifier.c`, and `btf.c`. I need to understand the dependencies and compilation flags involved. Knowing this will give me insights into how the kernel is actually integrating and optimizing BPF code. I’m particularly interested in seeing any non-standard or optimized compilation flags. I'll be looking for nuances that might inform my own projects.



8/2 2026/10/13 08:46 tool grepper
0m Args:
{
  "Expression": "arena\\.o|verifier\\.o|btf\\.o",
  "PathPrefixes": [
    "kernel/bpf/Makefile"
  ]
}

Results:
{
  "Output": "kernel/bpf/Makefile=7=CFLAGS_core.o += -Wno-override-init $(cflags-nogcse-yy)\nkernel/bpf/Makefile-8-\nkernel/bpf/Makefile:9:obj-$(CONFIG_BPF_SYSCALL) += syscall.o verifier.o inode.o helpers.o tnum.o cnum.o log.o token.o liveness.o const_fold.o diagnostics.o loops.o scev.o\nkernel/bpf/Makefile-10-obj-$(CONFIG_BPF_SYSCALL) += bpf_iter.o map_iter.o task_iter.o prog_iter.o link_iter.o\n--\nkernel/bpf/Makefile=13=obj-$(CONFIG_BPF_SYSCALL) += bpf_local_storage.o bpf_task_storage.o\nkernel/bpf/Makefile:14:obj-$(CONFIG_BPF_SYSCALL) += fixups.o cfg.o states.o backtrack.o check_btf.o\nkernel/bpf/Makefile-15-obj-${CONFIG_BPF_LSM}\t  += bpf_inode_storage.o\n--\nkernel/bpf/Makefile=17=obj-$(CONFIG_BPF_JIT) += trampoline.o\nkernel/bpf/Makefile:18:obj-$(CONFIG_BPF_SYSCALL) += btf.o memalloc.o rqspinlock.o stream.o\nkernel/bpf/Makefile-19-ifeq ($(CONFIG_MMU)$(CONFIG_64BIT),yy)\nkernel/bpf/Makefile:20:obj-$(CONFIG_BPF_SYSCALL) += arena.o range_tree.o\nkernel/bpf/Makefile-21-endif\n--\nkernel/bpf/Makefile=43=ifeq ($(CONFIG_SYSFS),y)\nkernel/bpf/Makefile:44:obj-$(CONFIG_DEBUG_INFO_BTF) += sysfs_btf.o\nkernel/bpf/Makefile-45-obj-$(CONFIG_DEBUG_INFO_BTF_INLINE) += btf_vmlinux_inline.o\n"
}

9/2 2026/10/13 08:46 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 37314
							output: 38
							thoughts: 115
						

Thoughts:
Okay, let's break this down.

**Examining the BPF Syscall Compilation Logic**

Hmm, let's see. First, I spot this line: `obj-$(CONFIG_BPF_SYSCALL) += syscall.o verifier.o ... fixups.o ... btf.o`. Right, so this means that if `CONFIG_BPF_SYSCALL` is enabled, those object files, including the core syscall handler, the verifier, and the BTF components, are being compiled into `obj-$(CONFIG_BPF_SYSCALL)`. Makes sense. Standard way to include modules conditionally based on a config option.

Then, there's a conditional inclusion: `ifeq ($(CONFIG_MMU)$(CONFIG_64BIT),yy)`. Ah, this is interesting. They are adding `arena.o` and `range_tree.o` to the build only if both the Memory Management Unit (MMU) *and* 64-bit support are enabled. This hints that these specific components, likely related to memory management, are only needed in environments that have both the MMU and a 64-bit architecture. That aligns with my expectations of how those might interact, as 64-bit systems are designed for use with MMU's to manage large address spaces.

Now, I see the file `arch/x86/net/bpf_jit_comp.c`. That tells me we are looking at the Just-In-Time (JIT) compiler for BPF bytecode within the x86 architecture, specifically in the network subsystem. So to understand this, I should probably check the associated `Makefile`. Let's take a look there to see how this fits into the broader build process of the x86 network BPF components.



10/2 2026/10/13 08:46 tool grepper
0m Args:
{
  "Expression": "bpf_jit_comp\\.o",
  "PathPrefixes": [
    "arch/x86/net/Makefile"
  ]
}

Results:
{
  "Output": "arch/x86/net/Makefile=8=else\narch/x86/net/Makefile:9:        obj-$(CONFIG_BPF_JIT) += bpf_jit_comp.o bpf_timed_may_goto.o\narch/x86/net/Makefile-10-endif\n"
}

11/2 2026/10/13 08:46 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 37938
							output: 53
							thoughts: 160
						

Thoughts:
Here's my thought process, as it stands:

**Examining BPF Configuration Dependencies**

Okay, so I'm looking at `CONFIG_BPF_SYSCALL` and `CONFIG_BPF_JIT`. My immediate question is, how do `BPF_JIT` itself and `BPF_SYSCALL` interrelate? I need to get a clear picture of the dependencies here.

Next, I'm thinking about whether `DEBUG_INFO_BTF` is a hard requirement for BTF tags. Based on this patch, it looks like `btf_struct_walk` relies on `btf_ptr_has_type_tag`, and that uses `btf_is_kernel(btf) && btf_type_is_arena_ptr(btf, mtype)`. And from context, `CONFIG_DEBUG_INFO_BTF` compiles BTF information for both the vmlinux and module builds. I'll need to confirm that relationship.

Finally, I should take a step back and examine the wider configuration landscape. I should check if `CONFIG_BPF_JIT` has any direct dependencies, or if there are other configuration options that are relevant to this area that I haven't considered yet.



12/2 2026/10/13 08:46 tool grepper
0m Args:
{
  "Expression": "config BPF_JIT\\b|config BPF_SYSCALL\\b",
  "PathPrefixes": [
    "kernel/bpf/Kconfig",
    "init/Kconfig"
  ]
}

Results:
{
  "Output": "kernel/bpf/Kconfig=29=menu \"BPF subsystem\"\nkernel/bpf/Kconfig-30-\nkernel/bpf/Kconfig:31:config BPF_SYSCALL\nkernel/bpf/Kconfig-32-\tbool \"Enable bpf() system call\"\n--\nkernel/bpf/Kconfig-45-\nkernel/bpf/Kconfig:46:config BPF_JIT\nkernel/bpf/Kconfig-47-\tbool \"Enable BPF Just In Time compiler\"\n"
}

13/2 2026/10/13 08:46 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 38560
							output: 34
							thoughts: 22
						

Thoughts:
Here's a summary of my thought process:

**Diving into the BPF Configuration Landscape**

Okay, so I need to understand something about the BPF configuration within the kernel. My immediate thought is to zero in on `kernel/bpf/Kconfig`. That's the logical place to start – it's where the configuration options for BPF, a core subsystem, should reside. Specifically, I'm going to look around lines 31-60. I don't have a specific thing I am looking for yet, but that seems like a reasonable range to begin my investigation to understand the configuration options, dependencies, or any conditional logic related to the BPF subsystem. I'm hoping to quickly grasp the relevant parameters and their relationships. Let's see what's there.



14/2 2026/10/13 08:47 tool read-file
0m Args:
{
  "File": "kernel/bpf/Kconfig",
  "FirstLine": 31,
  "LineCount": 40
}

Results:
{
  "Contents": "  31:\tconfig BPF_SYSCALL\n  32:\t\tbool \"Enable bpf() system call\"\n  33:\t\tselect BPF\n  34:\t\tselect IRQ_WORK\n  35:\t\tselect NEED_TASKS_RCU\n  36:\t\tselect TASKS_TRACE_RCU\n  37:\t\tselect BINARY_PRINTF\n  38:\t\tselect NET_SOCK_MSG if NET\n  39:\t\tselect NET_XGRESS if NET\n  40:\t\tselect PAGE_POOL if NET\n  41:\t\tdefault n\n  42:\t\thelp\n  43:\t\t  Enable the bpf() system call that allows to manipulate BPF programs\n  44:\t\t  and maps via file descriptors.\n  45:\t\n  46:\tconfig BPF_JIT\n  47:\t\tbool \"Enable BPF Just In Time compiler\"\n  48:\t\tdepends on BPF\n  49:\t\tdepends on HAVE_CBPF_JIT || HAVE_EBPF_JIT\n  50:\t\tselect EXECMEM\n  51:\t\thelp\n  52:\t\t  BPF programs are normally handled by a BPF interpreter. This option\n  53:\t\t  allows the kernel to generate native code when a program is loaded\n  54:\t\t  into the kernel. This will significantly speed-up processing of BPF\n  55:\t\t  programs.\n  56:\t\n  57:\t\t  Note, an admin should enable this feature changing:\n  58:\t\t  /proc/sys/net/core/bpf_jit_enable\n  59:\t\t  /proc/sys/net/core/bpf_jit_harden   (optional)\n  60:\t\t  /proc/sys/net/core/bpf_jit_kallsyms (optional)\n  61:\t\n  62:\tconfig BPF_JIT_ALWAYS_ON\n  63:\t\tbool \"Permanently enable BPF JIT and remove BPF interpreter\"\n  64:\t\tdepends on BPF_SYSCALL \u0026\u0026 HAVE_EBPF_JIT \u0026\u0026 BPF_JIT\n  65:\t\thelp\n  66:\t\t  Enables BPF JIT and removes BPF interpreter to avoid speculative\n  67:\t\t  execution of BPF instructions by the interpreter.\n  68:\t\n  69:\t\t  When CONFIG_BPF_JIT_ALWAYS_ON is enabled, /proc/sys/net/core/bpf_jit_enable\n  70:\t\t  is permanently set to 1 and setting any other value than that will\n"
}

15/2 2026/10/13 08:47 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 39408
							output: 36
							thoughts: 12
						

Thoughts:
Okay, here's my take on those thoughts, tailored for an expert audience, presented in the first person:

**Navigating the Labyrinth of Debug Information**

Alright, time to dive in. My immediate thought, given the situation, is to zero in on `DEBUG_INFO_BTF`. I need to understand what's happening at the level of the *debug information itself*, particularly focusing on BTF (BPF Type Format). That's my starting point for this investigation. This usually means I'm troubleshooting some tricky issue related to symbol resolution, BPF program interaction, or maybe even kernel module debugging. The fact that `DEBUG_INFO_BTF` is top of mind indicates that I'm suspecting potential issues in the representation and availability of type information. I'll need to verify its integrity and how effectively it's being used by whatever tool or process is involved. The first check will involve confirming that BTF data is available, loadable, and correctly representing the relevant types. Then I'll have to investigate if the debugging tools I'm using, are effectively interpreting this information.



16/2 2026/10/13 08:47 tool grepper
0m Args:
{
  "Expression": "config DEBUG_INFO_BTF\\b",
  "PathPrefixes": [
    "lib/Kconfig.debug"
  ]
}

Results:
{
  "Output": "lib/Kconfig.debug=378=config DEBUG_INFO_SPLIT\n--\nlib/Kconfig.debug-397-\nlib/Kconfig.debug:398:config DEBUG_INFO_BTF\nlib/Kconfig.debug-399-\tbool \"Generate BTF type information\"\n"
}

17/2 2026/10/13 08:47 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 39773
							output: 35
							thoughts: 2116
						

Thoughts:
**Analysis of BPF Arena and Nullable Pointer Support**

Okay, let's break down this patch. My initial thought is that this is introducing significant changes around how BPF programs interact with and verify memory access to structures, specifically those utilizing an "arena" and/or nullable pointers. The core objective appears to be enhancing security and flexibility when BPF programs access kernel data.

My first impression is that the bulk of the work focuses on adding support for `__arena` and `__nullable` type tags, primarily to struct members accessed via BPF programs. The changes are spread across several files, so I'll go section by section to identify critical elements.

1.  `arch/*/net/bpf_jit_comp.c`: The JIT compilation process now conditionally sets up the arena base using `bpf_prog_uses_arena_insns()`, avoiding unnecessary setup if the program doesn't utilize arena instructions.  Also, it appears `WARN_ON_ONCE` has been added to `emit_kfunc_arena_args`, which immediately signals a potential point of interest.

2.  `kernel/bpf/btf.c`: This file introduces new helper functions, like `btf_ptr_has_type_tag`, to check for BTF type tags such as "arena" and "nullable."  `btf_struct_walk` is modified to handle these tags, checking if a pointer has the "nullable" tag. If so, a flag is set. If the kernel pointer has the "arena" tag, a special return value, `WALK_PTR_ARENA`, is returned, *unless* inside a union, where it will return `-EACCES`. Furthermore, in `btf_struct_access`, `PTR_TO_ARENA` is set when an arena pointer is detected. This shows that the BPF verifier interacts closely with BTF (BPF Type Format) data structures.

3.  `kernel/bpf/verifier.c`: This is where things get really interesting. The verifier is getting extensive updates to handle arena pointers. Functions like `mark_uses_arena_insn` and `check_arena_member_load` are introduced. Specifically, `check_arena_member_load` verifies that the current program *actually* uses an arena and throws an error if not. Based on my expertise, I'm noting that the verifier is now checking a lot more aspects of memory access. It also now propagates this information about `uses_arena_insns` to subprograms. Instruction checking is also added with `check_mem_access`, `mem_access_reg` and `check_load_mem`, as well as `check_func_arg` and `check_alu_op`, which all now call `mark_uses_arena_insn`.

4.  `kernel/bpf/fixups.c`: This file introduces a new instruction rewriting pass via `arena_member_fixup`, which is called by `bpf_convert_ctx_accesses`.  This fixup pass calculates the arena base and rewrites load instructions based on the presence of the `__nullable` tag. The program checks `aux->non_arena_load` which should lead to more robust behavior.

5.  `include/linux/compiler_types.h`: This simply defines the `__arena` and `__nullable` type tags, which are crucial for this whole process.

**Focus Symbol Selection**

Now, let's choose some targeted symbols for fuzzing. The goal is to focus on entry points and specific new logic. Based on the guidance, I want to avoid broad calls, so I am going to look for non-static function definitions or functions that call static inlines.

I have come to this list of candidates:

*   `kernel/bpf/btf.c`:
    *   `btf_struct_access`: It directly interfaces with the BTF data and new tag checks.
    *   `btf_type_is_arena_ptr`: It interfaces with arena type pointers, which has a distinct purpose.

*   `kernel/bpf/fixups.c`:
    *   `bpf_convert_ctx_accesses`: This function calls `arena_member_fixup`, performing instruction rewriting, which is core to the patch's changes.

*   `arch/x86/net/bpf_jit_comp.c`:
    *   `bpf_int_jit_compile`: JIT compilation is important.

These candidates represent either new entry points or the application of the new logic, thus they are very specific and focused on the change's new functionality.



18/2 2026/10/13 08:47 tool grepper
0m Args:
{
  "Expression": "bpf_convert_ctx_accesses",
  "PathPrefixes": [
    "kernel/bpf/"
  ]
}

Results:
{
  "Output": "kernel/bpf/fixups.c-15-/*\nkernel/bpf/fixups.c:16: * Matches BPF_PROBE_ATOMIC too: bpf_convert_ctx_accesses() rewrites arena\nkernel/bpf/fixups.c-17- * atomics before bpf_opt_subreg_zext_lo32_rnd_hi32() runs.\n--\nkernel/bpf/fixups.c=881=static int arena_member_fixup(struct bpf_verifier_env *env, int idx, struct bpf_insn *insn,\n--\nkernel/bpf/fixups.c-902-\nkernel/bpf/fixups.c:903:int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)\nkernel/bpf/fixups.c-904-{\n--\nkernel/bpf/verifier.c=23332=int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,\n--\nkernel/bpf/verifier.c-23604-\t\t/* program is valid, convert *(u32*)(ctx + off) accesses */\nkernel/bpf/verifier.c:23605:\t\tret = bpf_convert_ctx_accesses(env);\nkernel/bpf/verifier.c-23606-\n"
}

19/2 2026/10/13 08:47 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 43283
							output: 43
							thoughts: 315
						

Thoughts:
**Analyzing BPF Verification and Static Functions**

Okay, so I'm digging into this `bpf_check` function, and I see that `bpf_convert_ctx_accesses` is calling `bpf_convert_ctx_accesses`. Wait a second, is this `bpf_convert_ctx_accesses` call a very common operation for BPF programs in general, or is this `arena_member_fixup` thing more specifically tied to a certain arena or context? Hmm...

Alright, let me shift gears here. Thinking about how the kernel is compiled... in `vmlinux`, are static functions like `arena_member_fixup` or `check_arena_member_load` always compiled into symbols, or could the compiler optimize them away, perhaps inlining them? I need to check how the compiler handles static functions in this context. I need to refresh my knowledge of gcc/clang's behavior here - will the compiler always emit symbols unless inlined, or might it inline them?

Okay, let's look closer at `arena_member_fixup`. Let's see its definition:

```c
static int arena_member_fixup(struct bpf_verifier_env *env, int idx, struct bpf_insn *insn, struct bpf_insn *buf)
```

It's a relatively short function, maybe twenty lines at most, and it looks like it's only called in one place: within `bpf_convert_ctx_accesses`. That's a strong hint! The compiler could easily inline it for a performance optimization.

But, `check_arena_member_load` is also only called within `check_ptr_to_btf_access`. So I'll need to figure out what functions are unique to this logic.

Now, which functions specifically target the logic that I'm interested in? Let's check:

-   `btf_type_is_arena_ptr` (an exported symbol, declared in `include/linux/btf.h` and defined in `kernel/bpf/btf.c`)

Now, let's examine `btf_type_is_arena_ptr`.



20/2 2026/10/13 08:47 tool grepper
0m Args:
{
  "Expression": "btf_type_is_arena_ptr",
  "PathPrefixes": [
    "kernel/bpf/",
    "include/linux/"
  ]
}

Results:
{
  "Output": "include/linux/btf.h=252=bool btf_type_is_void(const struct btf_type *t);\ninclude/linux/btf.h:253:bool btf_type_is_arena_ptr(const struct btf *btf, const struct btf_type *t);\ninclude/linux/btf.h-254-s32 btf_find_by_name_kind(const struct btf *btf, const char *name, u8 kind);\n--\nkernel/bpf/btf.c=3561=static bool btf_ptr_has_type_tag(const struct btf *btf, const struct btf_type *t, const char *name)\n--\nkernel/bpf/btf.c-3576-\nkernel/bpf/btf.c:3577:bool btf_type_is_arena_ptr(const struct btf *btf, const struct btf_type *t)\nkernel/bpf/btf.c-3578-{\n--\nkernel/bpf/btf.c=7734=static int btf_struct_walk(struct bpf_verifier_log *log, const struct btf *btf,\n--\nkernel/bpf/btf.c-7988-\t\t\t */\nkernel/bpf/btf.c:7989:\t\t\tif (btf_is_kernel(btf) \u0026\u0026 btf_type_is_arena_ptr(btf, mtype)) {\nkernel/bpf/btf.c-7990-\t\t\t\tif (*flag \u0026 PTR_UNTRUSTED) {\n--\nkernel/bpf/btf.c=8666=static void btf_scan_type_tags(const struct btf *btf, u32 type_id, u32 *tags)\n--\nkernel/bpf/btf.c-8670-\nkernel/bpf/btf.c:8671:\tif (btf_type_is_arena_ptr(btf, t))\nkernel/bpf/btf.c-8672-\t\t*tags |= ARG_TAG_ARENA;\n--\nkernel/bpf/btf.c=8676=static int btf_validate_return_type(struct bpf_verifier_env *env, struct btf *btf,\n--\nkernel/bpf/btf.c-8685-\t */\nkernel/bpf/btf.c:8686:\tif (subprog \u0026\u0026 btf_type_is_arena_ptr(btf, t))\nkernel/bpf/btf.c-8687-\t\treturn 0;\n--\nkernel/bpf/verifier.c=13241=static bool btf_member_kind_allowed(const struct btf *btf, const struct btf_type *t,\n--\nkernel/bpf/verifier.c-13245-\t\treturn true;\nkernel/bpf/verifier.c:13246:\tif ((member_kinds \u0026 BTF_MEMBER_ARENA_PTR) \u0026\u0026 btf_type_is_arena_ptr(btf, t))\nkernel/bpf/verifier.c-13247-\t\treturn true;\n"
}

21/2 2026/10/13 08:47 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 44918
							output: 37
							thoughts: 108
						

Thoughts:
**Investigating Non-Static Functions in BPF/BTF**

Okay, so I'm looking at `btf_type_is_arena_ptr`. Right, it's non-static, I knew that. It's defined in `kernel/bpf/btf.c` and declared in `include/linux/btf.h`. No surprises there.

Now, let's move on to `btf_struct_access`. Hmmm, looks like it's also non-static. Defined in `kernel/bpf/btf.c` as well, but declared in either `include/linux/bpf_verifier.h` or `include/linux/btf.h`. I need to make a note to check both headers if I need to use it.

Right, I just wanted to verify my assumptions, so I'll make sure all the non-static assumptions I have about these functions are correct.



22/2 2026/10/13 08:48 tool grepper
0m Args:
{
  "Expression": "btf_struct_access",
  "PathPrefixes": [
    "kernel/bpf/",
    "include/"
  ]
}

Results:
{
  "Output": "include/linux/bpf.h=1193=struct bpf_verifier_ops {\n--\ninclude/linux/bpf.h-1214-\t\t\t\t  struct bpf_prog *prog, u32 *target_size);\ninclude/linux/bpf.h:1215:\tint (*btf_struct_access)(struct bpf_verifier_log *log,\ninclude/linux/bpf.h-1216-\t\t\t\t const struct bpf_reg_state *reg,\n--\ninclude/linux/bpf.h=3298=static inline bool bpf_tracing_btf_ctx_access(int off, int size,\n--\ninclude/linux/bpf.h-3307-\ninclude/linux/bpf.h:3308:int btf_struct_access(struct bpf_verifier_log *log,\ninclude/linux/bpf.h-3309-\t\t      const struct bpf_reg_state *reg,\n--\ninclude/linux/bpf.h=3653=static inline struct bpf_prog *bpf_prog_by_id(u32 id)\n--\ninclude/linux/bpf.h-3657-\ninclude/linux/bpf.h:3658:static inline int btf_struct_access(struct bpf_verifier_log *log,\ninclude/linux/bpf.h-3659-\t\t\t\t    const struct bpf_reg_state *reg,\n--\ninclude/linux/filter.h=751=extern struct mutex nf_conn_btf_access_lock;\ninclude/linux/filter.h:752:extern int (*nfct_btf_struct_access)(struct bpf_verifier_log *log,\ninclude/linux/filter.h-753-\t\t\t\t     const struct bpf_reg_state *reg,\n--\nkernel/bpf/btf.c=7734=static int btf_struct_walk(struct bpf_verifier_log *log, const struct btf *btf,\n--\nkernel/bpf/btf.c-8032-\nkernel/bpf/btf.c:8033:int btf_struct_access(struct bpf_verifier_log *log,\nkernel/bpf/btf.c-8034-\t\t      const struct bpf_reg_state *reg,\n--\nkernel/bpf/verifier.c=6603=static int check_ptr_to_btf_access(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-6662-\nkernel/bpf/verifier.c:6663:\tif (env-\u003eops-\u003ebtf_struct_access \u0026\u0026 !type_is_alloc(reg-\u003etype) \u0026\u0026 atype == BPF_WRITE) {\nkernel/bpf/verifier.c-6664-\t\tif (!tnum_is_const(reg-\u003evar_off)) {\n--\nkernel/bpf/verifier.c-6679-\t\t}\nkernel/bpf/verifier.c:6680:\t\tret = env-\u003eops-\u003ebtf_struct_access(\u0026env-\u003elog, reg, off, size);\nkernel/bpf/verifier.c-6681-\t\tif (ret \u003c 0)\n--\nkernel/bpf/verifier.c-6685-\t} else {\nkernel/bpf/verifier.c:6686:\t\t/* Writes are permitted with default btf_struct_access for\nkernel/bpf/verifier.c-6687-\t\t * program allocated objects (which always have id \u003e 0).\n--\nkernel/bpf/verifier.c-6705-\nkernel/bpf/verifier.c:6706:\t\tret = btf_struct_access(\u0026env-\u003elog, reg, off, size, atype, \u0026btf_id, \u0026flag, \u0026field_name);\nkernel/bpf/verifier.c-6707-\t}\n--\nkernel/bpf/verifier.c=6791=static int check_ptr_to_map_access(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-6842-\t\treturn ret;\nkernel/bpf/verifier.c:6843:\tret = btf_struct_access(\u0026env-\u003elog, \u0026map_reg, off, size, atype, \u0026btf_id, \u0026flag, NULL);\nkernel/bpf/verifier.c-6844-\tif (ret \u003c 0)\n"
}

23/2 2026/10/13 08:48 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 46370
							output: 45
							thoughts: 281
						

Thoughts:
**Analyzing the BTF Structure Access Patch**

Okay, so this patch is clearly messing with how we handle struct member access within the BTF framework. My initial assessment is that it targets critical functions: `btf_struct_access` and `btf_struct_walk`. These are central to how we interpret and traverse structures described by BTF data. The fact that the patch touches these directly is noteworthy.

Let me break down the affected areas and their implications. I see modifications in `kernel/bpf/btf.c`. The fact that it has changes to `btf_struct_access`, `btf_struct_walk`, `btf_type_is_arena_ptr`, and `btf_ptr_has_type_tag` suggests the core BTF structure access logic is being refined. It appears to incorporate handling for arena pointers, which would have implications for memory management and allocation.

Now, shifting gears to `kernel/bpf/verifier.c`, I notice this patch deals with: `check_arena_member_load`, `mark_uses_arena_insn`, and `check_ptr_to_btf_access`. This tells me the verifier is getting updated to correctly validate instructions and data accesses that involve arena members and BTF-derived pointer types. This is essential for preventing security vulnerabilities.

The inclusion of `arena_member_fixup` and `bpf_convert_ctx_accesses` in `kernel/bpf/fixups.c` indicates post-verification fixes and context access conversions will be taking place. This is where we likely handle any potential issues flagged by the verifier and ensure the code is optimized to work correctly within the kernel context.

Finally, the changes within `arch/x86/net/bpf_jit_comp.c`, specifically `emit_kfunc_arena_args`, `emit_bpf_tail_call_indirect`, `emit_bpf_tail_call_direct`, and `do_jit`, show the JIT compiler is adapting to these BTF changes. This means the JIT-compiled BPF code is going to correctly handle arena arguments within kernel functions, and also be aware of any adjustments that were made to `tail_call`. This is crucial for performance and ensuring correct code generation. I should probably focus on `emit_kfunc_arena_args` there to start. I'll need to dig into the x86 JIT compilation to understand those implications, because that's where the rubber meets the road.



24/2 2026/10/13 08:48 tool grepper
0m Args:
{
  "Expression": "emit_kfunc_arena_args",
  "PathPrefixes": [
    "arch/x86/net/bpf_jit_comp.c"
  ]
}

Results:
{
  "Output": "arch/x86/net/bpf_jit_comp.c=1914=static int emit_kfunc_arg_moves(const struct btf_func_model *fm, s32 stack_base, u8 **pprog)\n--\narch/x86/net/bpf_jit_comp.c-1963- */\narch/x86/net/bpf_jit_comp.c:1964:static int emit_kfunc_arena_args(struct bpf_prog *bpf_prog,\narch/x86/net/bpf_jit_comp.c-1965-\t\t\t\t const struct btf_func_model *fm, u8 **pprog)\n--\narch/x86/net/bpf_jit_comp.c=2003=static int do_jit(struct bpf_verifier_env *env, struct bpf_prog *bpf_prog, int *addrs, u8 *image,\n--\narch/x86/net/bpf_jit_comp.c-2959-\t\t\t\t\treturn -EINVAL;\narch/x86/net/bpf_jit_comp.c:2960:\t\t\t\terr = emit_kfunc_arena_args(bpf_prog, fm, \u0026prog);\narch/x86/net/bpf_jit_comp.c-2961-\t\t\t\tif (err \u003c 0)\n"
}

25/2 2026/10/13 08:48 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 47583
							output: 41
							thoughts: 1695
						

Thoughts:
**Focusing BPF Symbol Coverage for Arena Instruction Tracking**

Alright, let's break this down. We have a potential issue where the `WARN_ON_ONCE` in `emit_kfunc_arena_args` might be triggered because the verifier fails to mark that a BPF program uses arena instructions via `mark_uses_arena_insn`. This arises when a kfunc using `BTF_FMODEL_ARENA_ARG` is called, but the program's `uses_arena_insns` flag is not set. The code path that determines `uses_arena_insns` is what we need to focus on. The goal is to maximize the effectiveness of syzkaller KCOV symbol coverage for finding potential bugs related to the arena feature.

Specifically, the problem is that we are looking at getting KCOV symbol coverage on `bpf_prog_uses_arena_insns`. But it's an inline function, so it won't work that way.

Now, let's get specific on what we *can* target for symbol coverage. We should focus on the *callers* or *implementations* of this feature rather than the helper functions themselves.

So, let's identify the likely candidates:

*   `arena_member_fixup` (or `bpf_convert_ctx_accesses`)
*   `check_arena_member_load`
*   `btf_struct_access`
*   `emit_kfunc_arena_args`
*   `btf_type_is_arena_ptr`
*   `check_ptr_to_btf_access`

Now, let's consider their static/non-static status and if static functions are appropriate targets.

Okay, let's confirm the static/non-static state of these functions and whether they are suitable for symbol coverage. `bpf_convert_ctx_accesses`, `btf_struct_access`, and `btf_type_is_arena_ptr` are non-static. The functions `arena_member_fixup`, `check_arena_member_load`, `emit_kfunc_arena_args` are static.

Since `bpf_convert_ctx_accesses` is called on *every* BPF program that passes verification, it might be too ubiquitous to be a good target for focused coverage. The goal is to include functions that are specific to arena features. So, it's better to exclude it.

But `arena_member_fixup`, `check_arena_member_load`, `btf_struct_access`, `btf_type_is_arena_ptr` and `emit_kfunc_arena_args` are *much* more specific to arena members and pointers. When an arena member is accessed on a BTF struct, `check_ptr_to_btf_access` calls `check_arena_member_load`. And `arena_member_fixup` is called from `bpf_convert_ctx_accesses`. Also `mark_uses_arena_insn` and `check_arena_member_load` will need to be included.

In summary, we *should* include these functions in FocusSymbols:

*   `arena_member_fixup`
*   `check_arena_member_load`
*   `btf_struct_access`
*   `btf_type_is_arena_ptr`
*   `emit_kfunc_arena_args`

Now, let's confirm the needed configurations. We'll need `BPF_SYSCALL` (for the BPF syscall and verifier), `BPF_JIT` (for JIT compilation), and `DEBUG_INFO_BTF` (for BTF generation). Now let's double check if there's any `BPF_ARENA` Kconfig.



26/2 2026/10/13 08:48 tool grepper
0m Error:
no matches

Args:
{
  "Expression": "ARENA",
  "PathPrefixes": [
    "kernel/bpf/Kconfig",
    "arch/x86/Kconfig"
  ]
}

Results:
{
  "error": "no matches"
}

27/2 2026/10/13 08:48 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 50175
							output: 36
							thoughts: 207
						

Thoughts:
**Investigating BPF Arena and JIT Compilation Dependencies**

Okay, so I'm digging into the BPF configuration landscape. First, I see there's no direct `CONFIG_BPF_ARENA` option. It's conditionally compiled based on `CONFIG_MMU` and `CONFIG_64BIT`. Specifically, it appears the arena support, which includes `arena.o` and `range_tree.o`, is only included if both are set to 'y'. Right, and on x86_64, which is what I'm looking at, that's almost always the case. So, arena support is largely baked in on amd64.

Next, I'm thinking about the `BPF_JIT`. The makefiles for both `kernel/bpf/` and `arch/x86/net/` tell the story, and the `obj-$(CONFIG_BPF_JIT) +=` lines are critical. The `trampoline.o` is going to be built, and then in the x86 net directory the `bpf_jit_comp.o` object is also going to be built.

Finally, I want to confirm whether anything relies on `BPF_STRUCT_OPS`, so I'll check the Kconfig files for that or something similar.



28/2 2026/10/13 08:49 tool grepper
0m Error:
no matches

Args:
{
  "Expression": "config BPF_.*STRUCT_OPS",
  "PathPrefixes": [
    "kernel/bpf/Kconfig"
  ]
}

Results:
{
  "error": "no matches"
}

29/2 2026/10/13 08:49 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 50714
							output: 33
							thoughts: 22
						

Thoughts:
Here's my thought process, as requested:

**Investigating BPF Structure Operations**

Okay, so I need to dig into the BPF (Berkeley Packet Filter) code. My initial thought is to find out where the structure operations are defined – the data structures associated with BPF operations. A good starting point, as always, is the `Makefile` within the kernel's BPF directory. It's a central control point and likely has information about the important source files. I'm going to search for the specific identifier I'm interested in, `bpf_struct_ops`, within the `kernel/bpf/Makefile` file. This should point me to the relevant source files or include statements. This will tell me where the structure operations are handled. I expect the `Makefile` to link to the corresponding files.



30/2 2026/10/13 08:49 tool grepper
0m Args:
{
  "Expression": "bpf_struct_ops",
  "PathPrefixes": [
    "kernel/bpf/Makefile"
  ]
}

Results:
{
  "Output": "kernel/bpf/Makefile=47=ifeq ($(CONFIG_BPF_JIT),y)\nkernel/bpf/Makefile:48:obj-$(CONFIG_BPF_SYSCALL) += bpf_struct_ops.o\nkernel/bpf/Makefile-49-obj-$(CONFIG_BPF_SYSCALL) += cpumask.o\n"
}

31/2 2026/10/13 08:49 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 51040
							output: 36
							thoughts: 149
						

Thoughts:
**My Analysis of Kernel Configuration Dependencies**

Okay, so it looks like `bpf_struct_ops.o` only gets compiled if both `CONFIG_BPF_JIT` and `CONFIG_BPF_SYSCALL` are enabled, that's straightforward enough. Now, I'm wondering about the interaction with `DEBUG_INFO_BTF`. My immediate thought is, is `DEBUG_INFO_BTF` *required* for this? Given my understanding of BTF, my hunch is that it might be, since BTF type tags like `arena` and `nullable` for kernel structures are derived from the BTF information itself. If `DEBUG_INFO_BTF` is disabled, the system likely wouldn't populate the kernel struct BTF with these type tags. So, to confirm this, I'll dive into `lib/Kconfig.debug` to examine the dependencies of `DEBUG_INFO_BTF`. That should give me a definitive answer.



32/2 2026/10/13 08:49 tool grepper
0m Args:
{
  "Expression": "config DEBUG_INFO_BTF\\b",
  "PathPrefixes": [
    "lib/Kconfig.debug"
  ]
}

Results:
{
  "Output": "lib/Kconfig.debug=378=config DEBUG_INFO_SPLIT\n--\nlib/Kconfig.debug-397-\nlib/Kconfig.debug:398:config DEBUG_INFO_BTF\nlib/Kconfig.debug-399-\tbool \"Generate BTF type information\"\n"
}

33/2 2026/10/13 08:49 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 51517
							output: 34
							thoughts: 19
						

Thoughts:
Here's my take on the thought process, tailored for an expert audience:

**Diving into Debugging Configuration**

Okay, so we're talking debugging, and I need to get to the heart of the matter. Let's start with `lib/Kconfig.debug`. It's usually the go-to place for debug-related options in the kernel build system. The logical approach is to trace the relevant configurations, and since `Kconfig` files are organized with structure, let's zero in around line 398. That's a good starting point to hunt for the core debug options. I expect to find things like trace buffer sizes, debug symbols, or printk control there, or perhaps some branching logic that relates to this feature. It's a quick and efficient way to explore the dependencies and how they interact to achieve debugging functionalities, by starting with the most typical files in the relevant directory.



34/2 2026/10/13 08:49 tool read-file
0m Args:
{
  "File": "lib/Kconfig.debug",
  "FirstLine": 398,
  "LineCount": 20
}

Results:
{
  "Contents": " 398:\tconfig DEBUG_INFO_BTF\n 399:\t\tbool \"Generate BTF type information\"\n 400:\t\tdepends on !DEBUG_INFO_SPLIT \u0026\u0026 !DEBUG_INFO_REDUCED\n 401:\t\tdepends on !GCC_PLUGIN_RANDSTRUCT || COMPILE_TEST\n 402:\t\tdepends on BPF_SYSCALL\n 403:\t\tdepends on PAHOLE_VERSION \u003e= 122\n 404:\t\t# pahole uses elfutils, which does not have support for Hexagon relocations\n 405:\t\tdepends on !HEXAGON\n 406:\t\thelp\n 407:\t\t  Generate deduplicated BTF type information from DWARF debug info.\n 408:\t\t  Turning this on requires pahole v1.22 or later, which will convert\n 409:\t\t  DWARF type info into equivalent deduplicated BTF type info.\n 410:\t\n 411:\tconfig PAHOLE_HAS_BTF_TAG\n 412:\t\tdef_bool PAHOLE_VERSION \u003e= 123\n 413:\t\tdepends on CC_IS_CLANG\n 414:\t\thelp\n 415:\t\t  Decide whether pahole emits btf_tag attributes (btf_type_tag and\n 416:\t\t  btf_decl_tag) or not. Currently only clang compiler implements\n 417:\t\t  these attributes, so make the config depend on CC_IS_CLANG.\n"
}

35/2 2026/10/13 08:49 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 52136
							output: 199
							thoughts: 1003
						

Thoughts:
**Setting Up the Fuzzing Configuration**

Okay, so we're diving into configuring this fuzzing run. First things first, we *definitely* need `DEBUG_INFO_BTF`. And, of course, `BPF_SYSCALL` and `BPF_JIT`. Let's just make sure there aren't any other obvious configuration options we're missing. Alright, looks like `BPF_SYSCALL` selects `BPF` in `kernel/bpf/Kconfig`, so those three are the key ones: `BPF_SYSCALL`, `BPF_JIT`, and `DEBUG_INFO_BTF`.

Now, we need to nail down the `FocusSymbols` list. I need to pick the most appropriate functions. Let's start by reviewing what functions are actually touched by the changes introduced by this patch.

I see the following functions affected:

*   In `kernel/bpf/verifier.c`: `check_arena_member_load`, `mark_uses_arena_insn`, `check_ptr_to_btf_access`.
*   In `kernel/bpf/btf.c`: `btf_struct_access`, `btf_type_is_arena_ptr`, `btf_struct_walk`.
*   In `kernel/bpf/fixups.c`: `arena_member_fixup`, `bpf_convert_ctx_accesses`.
*   In `arch/x86/net/bpf_jit_comp.c`: `emit_kfunc_arena_args`, `do_jit`.

Let's carefully assess which of these are the *most* relevant. I have to avoid including functions that are *too* broadly applicable or that get hit in almost every BPF program run.

`bpf_convert_ctx_accesses`: This one runs on *every* BPF program that passes the verifier. Could be alright if we are focused on all bpf programs. However, it's broader than necessary. `arena_member_fixup` seems more targeted.

`btf_struct_access`: This is better, it runs only when BPF programs access struct members through BTF. That's more focused.

`check_arena_member_load`: Seems very specific, only when an arena member of a struct is being loaded.

`arena_member_fixup`: This one is really good; only runs when fixing up arena member loads in BPF instructions.

`emit_kfunc_arena_args`: This one seems ideal; only runs when emitting JIT code for kfuncs with arena arguments, and it contains a `WARN_ON_ONCE`.

`btf_type_is_arena_ptr`: This is fairly good too, it checks if a BTF pointer has the arena type tag.

`check_ptr_to_btf_access`: It also verifies access to BTF pointers.

So, can we include `arena_member_fixup`, `check_arena_member_load`, `btf_struct_access`, `btf_type_is_arena_ptr`, and `emit_kfunc_arena_args` in `FocusSymbols`? Yes, these represent the specific feature logic being modified:

1.  `arena_member_fixup` (in `kernel/bpf/fixups.c`)
2.  `check_arena_member_load` (in `kernel/bpf/verifier.c`)
3.  `btf_struct_access` (in `kernel/bpf/btf.c`)
4.  `btf_type_is_arena_ptr` (in `kernel/bpf/btf.c`)
5.  `emit_kfunc_arena_args` (in `arch/x86/net/bpf_jit_comp.c`)

Great, we have the right parameters to set.

Let's review the required parameters for `set-results`:

*   `WorthFuzzing`: `true`
*   `Reasoning`: Concise explanation of why fuzzing is needed: This patch touches core BPF verifier, instruction fixups, and x86 JIT compilation for `__arena` and `__nullable` BTF type tags on struct members; it introduces new instruction rewriting logic and runtime assertions.
*   `FocusSymbols`: `["arena_member_fixup", "check_arena_member_load", "btf_struct_access", "btf_type_is_arena_ptr", "emit_kfunc_arena_args"]`
*   `EnableConfigs`: `["BPF_SYSCALL", "BPF_


36/2 2026/10/13 08:50 tool set-results
0m Args:
{
  "EnableConfigs": [
    "BPF_SYSCALL",
    "BPF_JIT",
    "DEBUG_INFO_BTF"
  ],
  "FocusSymbols": [
    "arena_member_fixup",
    "check_arena_member_load",
    "btf_struct_access",
    "btf_type_is_arena_ptr",
    "emit_kfunc_arena_args"
  ],
  "Reasoning": "The patch introduces support for __arena and __nullable BTF type tags on kernel struct members accessed by BPF programs. This touches core BPF verification logic (BTF struct walking and member load checking), BPF instruction rewriting (arena_member_fixup), and x86 JIT compilation (arena base tracking and emit_kfunc_arena_args assertions). All modified code is reachable via the bpf() syscall in standard virtualized environments.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "BPF_SYSCALL",
    "BPF_JIT",
    "DEBUG_INFO_BTF"
  ],
  "FocusSymbols": [
    "arena_member_fixup",
    "check_arena_member_load",
    "btf_struct_access",
    "btf_type_is_arena_ptr",
    "emit_kfunc_arena_args"
  ],
  "Reasoning": "The patch introduces support for __arena and __nullable BTF type tags on kernel struct members accessed by BPF programs. This touches core BPF verification logic (BTF struct walking and member load checking), BPF instruction rewriting (arena_member_fixup), and x86 JIT compilation (arena base tracking and emit_kfunc_arena_args assertions). All modified code is reachable via the bpf() syscall in standard virtualized environments.",
  "WorthFuzzing": true
}

37/1 2026/10/13 08:50 action If
8m Args:
{
  "WorthFuzzing": true
}

Results:
null

38/2 2026/10/13 08:50 agent kmsan-evaluator
8m Model:
core

Args:
null

Results:
{
  "KMSANReasoning": "The patch introduces support for `__arena` and `__nullable` BTF type tags on kernel struct members accessed by BPF programs and optimizes JIT compilers across architectures to only set up the arena base registers when `bpf_prog_uses_arena_insns(prog)` is true.\n\nFrom a memory safety perspective:\n1. All newly added struct fields (`uses_arena_insns` in `struct bpf_prog_aux` and `struct bpf_subprog_info`, and `arena_member`, `arena_member_nullable`, and `non_arena_load` in `struct bpf_insn_aux_data`) are zero-initialized upon allocation via `kzalloc_obj`, `kvzalloc_obj`, or `__GFP_ZERO`.\n2. In the JIT compilers, the newly conditioned variables (`arena_vm_start` and `user_vm_start` in x86 `do_jit()`) are explicitly initialized to 0, and the context structures in arm64, s390, loongarch, powerpc, and riscv are zeroed via `memset` or `kcalloc`/`kzalloc` before inspection.\n3. In `arena_member_fixup()`, patched BPF instructions are fully constructed before insertion into the instruction stream.\n4. No kernel structures copied to user space, netlink attributes, ioctl buffers, or infoleak vectors are modified or introduced.\n\nAny potential bugs resulting from these changes (e.g., miscompilation in JIT passes, incorrect verifier state tracking, or invalid instruction rewriting) would manifest as verifier rejections, invalid memory dereferences, or incorrect execution logic—issues covered by standard functional testing and KASAN. There is no risk of uninitialized memory reads or information leaks that would require a dedicated KMSAN fuzzing session.",
  "NeedsKMSAN": false
}

Instruction:
You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.

CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
  a comprehensive suite of debugging tools and sanitizers, including KASAN
  (out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
  (locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
  or page allocations) and kernel-to-user memory info-leaks.

Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.

Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.

Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
   attributes, ioctl output arguments, socket options, or BPF buffers) where fields
   or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
   or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
   could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
   uninitialized bytes of existing buffers.

Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
  dereferences, locking deadlocks, or use-after-free bugs (these are already caught
  by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
  or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.

Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
Target architecture: amd64

For your convenience, here is the diff of the changes:
commit 7aedf27ed55ba9cc9fbee2220c02ac479c9b901e
Author: syz-cluster <triage@syzkaller.com>
Date:   Tue Oct 13 08:44:52 2026 +0000

    syz-cluster: applied patch under review

diff --git a/arch/arm64/net/bpf_jit_comp.c b/arch/arm64/net/bpf_jit_comp.c
index 506c4201fec85..1d5f4cf88d331 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 e78eb582c3005..9393e4b0db8e1 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 7a612688e7a20..0d6ea6df01065 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 5265dd5bd39e1..f2497ec7d3788 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 1b2566012b63a..0fe629f0b66fc 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 083fcd6cf15b7..3130d9ecbb994 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 54144372281cb..b0a5b2d5b7560 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 54cb6d3d06535..948daef06c7d4 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -791,6 +791,14 @@ struct bpf_insn_aux_data {
 	 */
 	u8 fastcall_spills_num:3;
 	u8 arg_prog:4;
+	/*
+	 * What a load found, on any path: an __arena pointer member of a
+	 * kernel struct, which arena_member_fixup() rewrites, one that is
+	 * __nullable as well, or anything else through any pointer.
+	 */
+	u8 arena_member:1;
+	u8 arena_member_nullable:1;
+	u8 non_arena_load:1;
 
 	/* below fields are initialized once */
 	unsigned int orig_idx; /* original instruction index */
@@ -958,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/compiler_types.h b/include/linux/compiler_types.h
index c5921f1390079..e97ee2d4d1dfb 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
diff --git a/include/linux/filter.h b/include/linux/filter.h
index 9339c6131f8ff..f5518bae48556 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/btf.c b/kernel/bpf/btf.c
index b09c04ccdc6db..b992027b60b02 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)
 {
@@ -7689,6 +7695,7 @@ enum bpf_struct_walk_result {
 	WALK_SCALAR = 0,
 	WALK_PTR,
 	WALK_PTR_UNTRUSTED,
+	WALK_PTR_ARENA,
 	WALK_STRUCT,
 };
 
@@ -7961,6 +7968,38 @@ 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;
+
+			/*
+			 * 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;
@@ -8063,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 b2fa7e8bedf3a..a1f2cfefda76a 100644
--- a/kernel/bpf/fixups.c
+++ b/kernel/bpf/fixups.c
@@ -870,6 +870,36 @@ int bpf_opt_subreg_zext_lo32_rnd_hi32(struct bpf_verifier_env *env,
  *     struct __sk_buff    -> struct sk_buff
  *     struct bpf_sock_ops -> struct sock
  */
+/*
+ * A load through an __arena pointer member of a kernel struct reads the kernel
+ * address of arena memory. Append the conversion to the arena pointer form,
+ * (u32)(kaddr - kern_vm_start), that the struct_ops trampoline applies to
+ * __arena arguments. A __nullable member keeps NULL as NULL, tested on the
+ * full kernel address. Returns the number of insns in @buf, 0 when the insn
+ * loads no such member, or an error when it does so on some paths only.
+ */
+static int arena_member_fixup(struct bpf_verifier_env *env, int idx, struct bpf_insn *insn,
+			      struct bpf_insn *buf)
+{
+	struct bpf_insn_aux_data *aux = &env->insn_aux_data[idx];
+	int cnt = 0;
+	u32 base;
+
+	if (!aux->arena_member)
+		return 0;
+	if (aux->non_arena_load) {
+		verbose(env, "an insn loads an arena pointer member on one path and something else on another\n");
+		return -EINVAL;
+	}
+
+	base = bpf_arena_get_kern_vm_start(env->prog->aux->arena);
+	buf[cnt++] = *insn;
+	if (aux->arena_member_nullable)
+		buf[cnt++] = BPF_JMP_IMM(BPF_JEQ, insn->dst_reg, 0, 1);
+	buf[cnt++] = BPF_ALU32_IMM(BPF_SUB, insn->dst_reg, (s32)base);
+	return cnt;
+}
+
 int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)
 {
 	struct bpf_subprog_info *subprogs = env->subprog_info;
@@ -1093,11 +1123,23 @@ int bpf_convert_ctx_accesses(struct bpf_verifier_env *env)
 					insn->code = BPF_LDX | BPF_PROBE_MEMSX |
 						     BPF_SIZE(insn->code);
 				env->prog->aux->num_exentries++;
-				continue;
-			}
-			if (verifier_bug_if(bpf_may_fault_on_deref(ptr_type), env,
-					    "access to a fault prone pointer is not rewritten as a probed one"))
+			} else if (verifier_bug_if(bpf_may_fault_on_deref(ptr_type), env,
+						   "access to a fault prone pointer is not rewritten as a probed one")) {
 				return -EFAULT;
+			}
+
+			cnt = arena_member_fixup(env, i + delta, insn, insn_buf);
+			if (cnt < 0)
+				return cnt;
+			if (!cnt)
+				continue;
+			new_prog = bpf_patch_insn_data(env, i + delta, insn_buf, cnt);
+			if (!new_prog)
+				return -ENOMEM;
+
+			delta    += cnt - 1;
+			env->prog = new_prog;
+			insn      = new_prog->insnsi + i + delta;
 			continue;
 		}
 
@@ -1366,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 2f190abe34489..63dd5da0708ff 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)
 {
@@ -6552,6 +6564,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 +6709,9 @@ static int check_ptr_to_btf_access(struct bpf_verifier_env *env,
 	if (ret < 0)
 		return ret;
 
+	if (ret == PTR_TO_ARENA)
+		return check_arena_member_load(env, regs, value_regno, flag, tname, field_name);
+
 	if (ret != PTR_TO_BTF_ID) {
 		/* just mark; */
 
@@ -6721,6 +6772,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)
@@ -7330,6 +7388,7 @@ static int check_mem_access(struct bpf_verifier_env *env, int insn_idx, struct b
 		if (!err && value_regno >= 0 && (rdonly_mem || t == BPF_READ))
 			mark_reg_unknown(env, regs, value_regno);
 	} else if (reg->type == PTR_TO_ARENA) {
+		mark_uses_arena_insn(env);
 		if (t == BPF_READ && value_regno >= 0)
 			mark_reg_unknown(env, regs, value_regno);
 	} else {
@@ -7389,6 +7448,7 @@ static struct bpf_reg_state *mem_access_reg(struct bpf_verifier_env *env, int re
 	memset(arena, 0, sizeof(*arena));
 	arena->type = PTR_TO_ARENA;
 	env->insn_aux_data[env->insn_idx].arena_scalar = true;
+	mark_uses_arena_insn(env);
 	return arena;
 }
 
@@ -7432,6 +7492,17 @@ static int check_load_mem(struct bpf_verifier_env *env, struct bpf_insn *insn,
 	err = check_mem_access(env, env->insn_idx, src_reg, argno_from_reg(insn->src_reg),
 			       insn->off, BPF_SIZE(insn->code), BPF_READ, insn->dst_reg,
 			       strict_alignment_once, is_ldsx);
+	/*
+	 * Paths through a kernel struct pointer and through other pointers
+	 * may share the insn, see save_aux_ptr_type(). Record a load that did
+	 * not produce an arena pointer from an __arena member, the only load
+	 * through a kernel struct pointer that does, next to the one
+	 * check_arena_member_load() records, so that arena_member_fixup() can
+	 * tell when the paths disagree.
+	 */
+	if (!err && !(base_type(src_reg_type) == PTR_TO_BTF_ID &&
+		      regs[insn->dst_reg].type == PTR_TO_ARENA))
+		env->insn_aux_data[env->insn_idx].non_arena_load = true;
 	err = err ?: save_aux_ptr_type(env, src_reg_type,
 				       allow_trust_mismatch);
 	err = err ?: reg_bounds_sanity_check(env, &regs[insn->dst_reg], ctx);
@@ -9867,6 +9938,9 @@ static int check_func_arg(struct bpf_verifier_env *env, u32 arg, u32 slot, u32 p
 		}
 		break;
 	case ARG_PTR_TO_ARENA:
+		/* the JIT rebases the __arena argument of a kfunc on the arena base */
+		if (is_kfunc(meta))
+			mark_uses_arena_insn(env);
 		break;
 	case ARG_PTR_TO_ALLOC_BTF_ID:
 		if (reg->type == (PTR_TO_BTF_ID | MEM_ALLOC)) {
@@ -17478,6 +17552,7 @@ static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
 					verbose(env, "addr_space_cast insn can only be used in a program that has an associated arena\n");
 					return -EINVAL;
 				}
+				mark_uses_arena_insn(env);
 			}
 
 			/* check src operand */
diff --git a/tools/lib/bpf/btf_dump.c b/tools/lib/bpf/btf_dump.c
index 996e61b5f11f0..7dedd3975c395 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;
diff --git a/tools/lib/bpf/features.c b/tools/lib/bpf/features.c
index 9f589980beef6..71c6a666afcb2 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 cb09ded907732..0cd2c25a120d4 100644
--- a/tools/lib/bpf/libbpf.c
+++ b/tools/lib/bpf/libbpf.c
@@ -794,7 +794,12 @@ struct bpf_object {
 
 	int *fd_array;
 	size_t fd_array_cap;
-	size_t fd_array_cnt;
+	/*
+	 * Slots handed out so far, slot 0 included. A kfunc insn names a module
+	 * BTF slot through its offset, slot 0 stands for vmlinux BTF and stays
+	 * empty, so the array goes to the kernel without a count.
+	 */
+	size_t fd_array_used;
 
 	struct usdt_manager *usdt_man;
 
@@ -8799,6 +8804,46 @@ static int libbpf_prepare_prog_load(struct bpf_program *prog,
 
 static void fixup_verifier_log(struct bpf_program *prog, char *buf, size_t buf_sz);
 
+/*
+ * Bind the arena of the object to the program about to be loaded. A program
+ * has an arena only when an insn refers to the map, but the kernel can hand
+ * it arena pointers all the same, as struct_ops arguments, through tagged
+ * struct members or as kfunc return values, and the verifier rejects those
+ * when the program has no arena to place them in. A continuous fd_array
+ * binds every map in it, the arena takes slot 0, which is free: a kfunc insn
+ * with offset 0 names vmlinux BTF rather than a slot.
+ */
+static int bpf_object__bind_arena(struct bpf_object *obj, struct bpf_program *prog,
+				  struct bpf_prog_load_opts *load_attr)
+{
+	struct bpf_map *map;
+	int err;
+
+	if (obj->arena_map_idx < 0 || !kernel_supports(obj, FEAT_FD_ARRAY_CNT))
+		return 0;
+	map = &obj->maps[obj->arena_map_idx];
+	/*
+	 * Nothing to bind when the arena is not created, when it is exclusive
+	 * to another program, or for an offloaded program, which the kernel
+	 * does not let near an arena.
+	 */
+	if (!map->autocreate || (map->excl_prog && map->excl_prog != prog) ||
+	    load_attr->prog_ifindex)
+		return 0;
+
+	if (!obj->fd_array_used) {
+		err = libbpf_ensure_mem((void **)&obj->fd_array, &obj->fd_array_cap, sizeof(int), 1);
+		if (err)
+			return err;
+		obj->fd_array_used = 1;
+	}
+	obj->fd_array[0] = map->fd;
+	load_attr->fd_array = obj->fd_array;
+	/* with slot 0 filled every slot handed out is valid, so the array can go with a count */
+	load_attr->fd_array_cnt = obj->fd_array_used;
+	return 0;
+}
+
 static int bpf_object_load_prog(struct bpf_object *obj, struct bpf_program *prog,
 				struct bpf_insn *insns, int insns_cnt,
 				const char *license, __u32 kern_version, int *prog_fd)
@@ -8862,6 +8907,11 @@ static int bpf_object_load_prog(struct bpf_object *obj, struct bpf_program *prog
 	if (obj->data_in_arena)
 		load_attr.prog_flags |= BPF_F_ARENA_SCALAR;
 	load_attr.fd_array = obj->fd_array;
+	if (!obj->gen_loader) {
+		err = bpf_object__bind_arena(obj, prog, &load_attr);
+		if (err)
+			return err;
+	}
 
 	load_attr.token_fd = obj->token_fd;
 	if (obj->token_fd)
@@ -9716,22 +9766,22 @@ static int bpf_object__resolve_ksym_func_btf_id(struct bpf_object *obj,
 	/* set index for module BTF fd in fd_array, if unset */
 	if (mod_btf && !mod_btf->fd_array_idx) {
 		/* insn->off is s16 */
-		if (obj->fd_array_cnt == INT16_MAX) {
+		if (obj->fd_array_used == INT16_MAX) {
 			pr_warn("extern (func ksym) '%s': module BTF fd index %d too big to fit in bpf_insn offset\n",
 				ext->name, mod_btf->fd_array_idx);
 			return -E2BIG;
 		}
 		/* Cannot use index 0 for module BTF fd */
-		if (!obj->fd_array_cnt)
-			obj->fd_array_cnt = 1;
+		if (!obj->fd_array_used)
+			obj->fd_array_used = 1;
 
 		ret = libbpf_ensure_mem((void **)&obj->fd_array, &obj->fd_array_cap, sizeof(int),
-					obj->fd_array_cnt + 1);
+					obj->fd_array_used + 1);
 		if (ret)
 			return ret;
-		mod_btf->fd_array_idx = obj->fd_array_cnt;
+		mod_btf->fd_array_idx = obj->fd_array_used;
 		/* we assume module BTF FD is always >0 */
-		obj->fd_array[obj->fd_array_cnt++] = mod_btf->fd;
+		obj->fd_array[obj->fd_array_used++] = mod_btf->fd;
 	}
 
 	ext->is_set = true;
@@ -9961,8 +10011,10 @@ static void bpf_object_cleanup_btf(struct bpf_object *obj)
 
 static void bpf_object_post_load_cleanup(struct bpf_object *obj)
 {
-	/* clean up fd_array */
+	/* clean up fd_array, and the slots handed out with it */
 	zfree(&obj->fd_array);
+	obj->fd_array_cap = 0;
+	obj->fd_array_used = 0;
 
 	/* clean up BTF */
 	bpf_object_cleanup_btf(obj);
@@ -10940,7 +10992,13 @@ int bpf_program__clone(struct bpf_program *prog, const struct bpf_prog_load_opts
 	attr.prog_ifindex = OPTS_GET(opts, prog_ifindex, 0) ?: prog->prog_ifindex;
 	attr.kern_version = OPTS_GET(opts, kern_version, 0) ?: obj->kern_version;
 	attr.fd_array = OPTS_GET(opts, fd_array, NULL) ?: obj->fd_array;
-	attr.fd_array_cnt = OPTS_GET(opts, fd_array_cnt, 0) ?: obj->fd_array_cnt;
+	attr.fd_array_cnt = OPTS_GET(opts, fd_array_cnt, 0);
+	/* the arena of the object goes with the program, as in bpf_object__load() */
+	if (!obj->gen_loader && !OPTS_GET(opts, fd_array, NULL)) {
+		err = bpf_object__bind_arena(obj, prog, &attr);
+		if (err)
+			return libbpf_err(err);
+	}
 	attr.token_fd = OPTS_GET(opts, token_fd, 0) ?: obj->token_fd;
 	if (attr.token_fd)
 		attr.prog_flags |= BPF_F_TOKEN_FD;
diff --git a/tools/lib/bpf/libbpf_internal.h b/tools/lib/bpf/libbpf_internal.h
index 4e45dfacb548a..22d9fe512ea1a 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,
 };
 
diff --git a/tools/testing/selftests/bpf/prog_tests/btf_dump.c b/tools/testing/selftests/bpf/prog_tests/btf_dump.c
index 17fa44cb9c91f..35f214e254da8 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/prog_tests/clone_module_kfunc.c b/tools/testing/selftests/bpf/prog_tests/clone_module_kfunc.c
new file mode 100644
index 0000000000000..fa6ccbaa7fd98
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/clone_module_kfunc.c
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <test_progs.h>
+#include "kfunc_call_test.skel.h"
+
+/*
+ * bpf_program__clone() loads a program from a prepared object with the
+ * fd_array of the object, where module BTF fds start at slot 1 and slot 0 is
+ * empty. It used to pass the fill index of that array as fd_array_cnt, which
+ * made the kernel resolve slot 0 and fail the load.
+ */
+void test_clone_module_kfunc(void)
+{
+	struct kfunc_call_test *skel;
+	int err, fd;
+
+	skel = kfunc_call_test__open();
+	if (!ASSERT_OK_PTR(skel, "skel_open"))
+		return;
+
+	err = bpf_object__prepare(skel->obj);
+	if (!ASSERT_OK(err, "prepare"))
+		goto out;
+
+	fd = bpf_program__clone(skel->progs.kfunc_call_test1, NULL);
+	if (ASSERT_GE(fd, 0, "clone"))
+		close(fd);
+out:
+	kfunc_call_test__destroy(skel);
+}
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 7f9f54ba3fbe5..149b130f8cdb7 100644
--- a/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_arena.c
@@ -1,10 +1,20 @@
 // SPDX-License-Identifier: GPL-2.0
 /* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
 #include <test_progs.h>
+#include <bpf/btf.h>
 
 #include "struct_ops_arena.skel.h"
 #include "struct_ops_arena_attach.skel.h"
 #include "struct_ops_arena_fail.skel.h"
+#include "struct_ops_arena_member.skel.h"
+#include "struct_ops_arena_member_render.skel.h"
+#include "struct_ops_arena_member_fail__union.skel.h"
+#include "struct_ops_arena_member_fail__load_acq.skel.h"
+#include "struct_ops_arena_member_fail__mismatch.skel.h"
+#include "struct_ops_arena_member_fail__mismatch_mem.skel.h"
+#include "struct_ops_arena_member_fail__write.skel.h"
+#include "struct_ops_arena_member_fail__no_arena.skel.h"
+#include "struct_ops_arena_autobind.skel.h"
 
 #if defined(__x86_64__) || defined(__aarch64__)
 /*
@@ -101,6 +111,121 @@ static void arena_arg_attach(void)
 
 	struct_ops_arena__destroy(skel);
 }
+
+/* Attach @st_ops and run @trigger once, it returns 0 when everything checked out. */
+static void attach_and_trigger(struct bpf_map *st_ops, struct bpf_program *trigger)
+{
+	LIBBPF_OPTS(bpf_test_run_opts, topts);
+	struct bpf_link *link;
+	int err;
+
+	link = bpf_map__attach_struct_ops(st_ops);
+	if (!ASSERT_OK_PTR(link, "attach_struct_ops"))
+		return;
+
+	err = bpf_prog_test_run_opts(bpf_program__fd(trigger), &topts);
+	ASSERT_OK(err, "test_run");
+	ASSERT_EQ(topts.retval, 0, "trigger_retval");
+
+	bpf_link__destroy(link);
+}
+
+/*
+ * The tags on the members of struct bpf_testmod_arena_member_args reach the
+ * module BTF only from a toolchain with btf_type_tag support.
+ */
+static bool testmod_has_type_tag(const char *tag)
+{
+	struct btf *vmlinux_btf, *module_btf;
+	bool found = false;
+
+	vmlinux_btf = btf__load_vmlinux_btf();
+	if (!ASSERT_OK_PTR(vmlinux_btf, "vmlinux_btf"))
+		return false;
+	module_btf = btf__load_module_btf("bpf_testmod", vmlinux_btf);
+	if (ASSERT_OK_PTR(module_btf, "module_btf"))
+		found = btf__find_by_name_kind(module_btf, tag, BTF_KIND_TYPE_TAG) > 0;
+	if (!found)
+		printf("%s:SKIP: no %s type tag in the bpf_testmod BTF\n", __func__, tag);
+	btf__free(module_btf);
+	btf__free(vmlinux_btf);
+	return found;
+}
+
+/*
+ * Attach a callback that reads arena pointers out of its argument struct.
+ * The header is shared with the module, so the members are arena pointers
+ * on the program side as well.
+ */
+static void arena_member(void)
+{
+	struct struct_ops_arena_member *skel;
+
+	skel = struct_ops_arena_member__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "struct_ops_arena_member__open_and_load"))
+		return;
+
+	attach_and_trigger(skel->maps.testmod_arena_member, skel->progs.trigger);
+	struct_ops_arena_member__destroy(skel);
+}
+
+/* @prog_fd lists exactly one map, the one with @map_id. */
+static void assert_prog_map(int prog_fd, __u32 map_id, const char *name)
+{
+	__u32 map_ids[2] = {}, len = sizeof(struct bpf_prog_info);
+	struct bpf_prog_info info = {
+		.nr_map_ids = ARRAY_SIZE(map_ids),
+		.map_ids = ptr_to_u64(map_ids),
+	};
+	int err;
+
+	err = bpf_prog_get_info_by_fd(prog_fd, &info, &len);
+	if (!ASSERT_OK(err, name))
+		return;
+	if (!ASSERT_EQ(info.nr_map_ids, 1, name))
+		return;
+	ASSERT_EQ(map_ids[0], map_id, name);
+}
+
+/*
+ * libbpf binds the arena of an object to every program in it: a callback
+ * with an __arena argument loads without referring to the arena, and an
+ * unrelated program gets the arena as well.
+ */
+static void arena_autobind(void)
+{
+	struct struct_ops_arena_autobind *skel;
+	struct bpf_map_info info = {};
+	__u32 len = sizeof(info);
+	int err;
+
+	skel = struct_ops_arena_autobind__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "struct_ops_arena_autobind__open_and_load"))
+		return;
+
+	err = bpf_map_get_info_by_fd(bpf_map__fd(skel->maps.arena), &info, &len);
+	if (!ASSERT_OK(err, "arena_info"))
+		goto out;
+	assert_prog_map(bpf_program__fd(skel->progs.test_arena_cb), info.id, "test_arena_cb");
+	assert_prog_map(bpf_program__fd(skel->progs.unrelated), info.id, "unrelated");
+
+	attach_and_trigger(skel->maps.testmod_arena_autobind, skel->progs.trigger);
+out:
+	struct_ops_arena_autobind__destroy(skel);
+}
+
+/* The same through the vmlinux.h rendering of the members, which takes casts. */
+static void arena_member_render(void)
+{
+	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 +247,20 @@ void serial_test_struct_ops_arena(void)
 		arena_arg_fail();
 	if (test__start_subtest("arena_arg_attach"))
 		arena_arg_attach();
+	if (testmod_has_type_tag("arena")) {
+		if (test__start_subtest("arena_member"))
+			arena_member();
+		if (test__start_subtest("arena_member_render"))
+			arena_member_render();
+		RUN_TESTS(struct_ops_arena_member_fail__union);
+		RUN_TESTS(struct_ops_arena_member_fail__load_acq);
+		RUN_TESTS(struct_ops_arena_member_fail__mismatch);
+		RUN_TESTS(struct_ops_arena_member_fail__mismatch_mem);
+		RUN_TESTS(struct_ops_arena_member_fail__write);
+		RUN_TESTS(struct_ops_arena_member_fail__no_arena);
+	}
+	if (test__start_subtest("arena_autobind"))
+		arena_autobind();
 #else
 	test__skip();
 #endif
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 0000000000000..17d5de78273ae
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/test_struct_ops_nullable.c
@@ -0,0 +1,68 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+#include <test_progs.h>
+#include <bpf/btf.h>
+
+#include "struct_ops_nullable.skel.h"
+#include "struct_ops_nullable_fail.skel.h"
+
+/*
+ * Attach a callback that gets a trusted and a trusted __nullable task pointer
+ * through its argument struct and drive it through the
+ * bpf_testmod_ops3_call_test_nullable() kfunc.
+ */
+static void nullable_member(void)
+{
+	LIBBPF_OPTS(bpf_test_run_opts, topts);
+	struct struct_ops_nullable *skel;
+	struct bpf_link *link;
+	int err;
+
+	skel = struct_ops_nullable__open_and_load();
+	if (!ASSERT_OK_PTR(skel, "struct_ops_nullable__open_and_load"))
+		return;
+
+	link = bpf_map__attach_struct_ops(skel->maps.testmod_nullable);
+	if (!ASSERT_OK_PTR(link, "attach_struct_ops"))
+		goto out;
+
+	err = bpf_prog_test_run_opts(bpf_program__fd(skel->progs.trigger), &topts);
+	ASSERT_OK(err, "test_run");
+	ASSERT_EQ(topts.retval, 0, "trigger_retval");
+
+	bpf_link__destroy(link);
+out:
+	struct_ops_nullable__destroy(skel);
+}
+
+/* Serialized because it attaches the singleton bpf_testmod_ops3. */
+/*
+ * The tag on the member of struct bpf_testmod_nullable_args reaches the module
+ * BTF only from a toolchain with btf_type_tag support.
+ */
+static bool testmod_has_nullable_tag(void)
+{
+	struct btf *vmlinux_btf, *module_btf;
+	bool found = false;
+
+	vmlinux_btf = btf__load_vmlinux_btf();
+	if (!ASSERT_OK_PTR(vmlinux_btf, "vmlinux_btf"))
+		return false;
+	module_btf = btf__load_module_btf("bpf_testmod", vmlinux_btf);
+	if (ASSERT_OK_PTR(module_btf, "module_btf"))
+		found = btf__find_by_name_kind(module_btf, "nullable", BTF_KIND_TYPE_TAG) > 0;
+	btf__free(module_btf);
+	btf__free(vmlinux_btf);
+	return found;
+}
+
+void serial_test_struct_ops_nullable(void)
+{
+	if (!testmod_has_nullable_tag()) {
+		test__skip();
+		return;
+	}
+	if (test__start_subtest("nullable_member"))
+		nullable_member();
+	RUN_TESTS(struct_ops_nullable_fail);
+}
diff --git a/tools/testing/selftests/bpf/prog_tests/verifier.c b/tools/testing/selftests/bpf/prog_tests/verifier.c
index c69c0f7e9cc89..6df440d826750 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 96f7aacf4d9f2..168ae8d7e303e 100644
--- a/tools/testing/selftests/bpf/progs/arena_kfunc.c
+++ b/tools/testing/selftests/bpf/progs/arena_kfunc.c
@@ -192,14 +192,41 @@ int arena_arg_unpopulated(void *ctx)
 	return 0;
 }
 
+/*
+ * A subprogram whose only arena use is the argument of a kfunc needs the
+ * arena base too, the JIT rebases the argument on it. The address arrives
+ * as a number, so no cast marks the subprogram either.
+ */
+__noinline int arena_arg_from_subprog(unsigned long addr)
+{
+	return bpf_kfunc_arena_arg_test((u64 *)addr) != 41;
+}
+
 SEC("syscall")
 __arch_x86_64
 __arch_arm64
-__failure __msg("arena pointer requires a program with an associated arena")
-int arena_arg_no_arena(void *ctx)
+__success __retval(0)
+int arena_arg_subprog(void *ctx)
 {
-	bpf_kfunc_arena_arg_test((u64 *)1);
+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST)
+	u64 __arena *val;
+	int ret;
+
+	val = bpf_arena_alloc_pages(&arena, NULL, 1, NUMA_NO_NODE, 0);
+	if (!val)
+		return 1;
+
+	*val = 41;
+	if (arena_arg_from_subprog((unsigned long)val))
+		ret = 2;
+	else
+		ret = *val != 42 ? 3 : 0;
+
+	bpf_arena_free_pages(&arena, (void __arena *)val, 1);
+	return ret;
+#else
 	return 0;
+#endif
 }
 
 SEC("syscall")
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 0000000000000..edbd40cd5be8a
--- /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 0000000000000..495faa0f4a8b8
--- /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/struct_ops_arena_member.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member.c
new file mode 100644
index 0000000000000..469e62e383f2f
--- /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 0000000000000..9d7972ae5c558
--- /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 0000000000000..0b4147894122c
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch.c
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf_arena_common.h>
+#include "bpf_misc.h"
+#include "../test_kmods/bpf_testmod.h"
+
+char _license[] SEC("license") = "GPL";
+
+struct {
+	__uint(type, BPF_MAP_TYPE_ARENA);
+	__uint(map_flags, BPF_F_MMAPABLE);
+	__uint(max_entries, 1);
+} arena SEC(".maps");
+
+/*
+ * The same load reaches args->ptr on one path and args->cookie on the other,
+ * the branch on the unknown cookie value makes the verifier walk both. The
+ * rewrite is per insn, so the paths have to agree on what they load.
+ */
+SEC("struct_ops/test_arena_member")
+__failure __msg("loads an arena pointer member on one path and something else on another")
+__naked int mismatch_member(void)
+{
+	asm volatile (
+	"r6 = *(u64 *)(r1 + 0);"
+	"r2 = %[arena] ll;"
+	"r0 = *(u64 *)(r6 + %[cookie_off]);"
+	"if r0 == 0 goto 1f;"
+	"r6 += %[cookie_off];"
+"1:	r0 = *(u64 *)(r6 + 0);"
+	"r0 = 0;"
+	"exit;"
+	:
+	: __imm_addr(arena),
+	  __imm_const(cookie_off, offsetof(struct bpf_testmod_arena_member_args, cookie))
+	: __clobber_all);
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_arena_member_fail = {
+	.test_arena_member = (void *)mismatch_member,
+};
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c
new file mode 100644
index 0000000000000..ff6d4db47cfdf
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__mismatch_mem.c
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include <bpf_arena_common.h>
+#include "bpf_misc.h"
+#include "../test_kmods/bpf_testmod.h"
+
+char _license[] SEC("license") = "GPL";
+
+struct {
+	__uint(type, BPF_MAP_TYPE_ARENA);
+	__uint(map_flags, BPF_F_MMAPABLE);
+	__uint(max_entries, 1);
+} arena SEC(".maps");
+
+/*
+ * The same load reads args->ptr on one path and the memory argument on the
+ * other. The verifier lets a load insn serve a kernel struct pointer and a
+ * memory pointer, see save_aux_ptr_type(), but the rewrite of the arena
+ * member would corrupt the value the other path loads. A global function
+ * is the only place a struct_ops program of this type gets a memory pointer.
+ * The loaded value goes nowhere: an ALU insn on it would be an arena
+ * pointer on one path and a scalar on the other, which the verifier rejects
+ * on its own before the fixup pass gets to the load.
+ */
+__noinline int load_either(unsigned long long *ctx __arg_ctx, u64 *mem __arg_nonnull)
+{
+	struct bpf_testmod_arena_member_args *args = (void *)ctx[0];
+	void *p = args;
+
+	if (args->cookie == 0)
+		p = mem;
+	/* one load insn, reached with both pointers */
+	asm volatile("r0 = *(u64 *)(%[p] + 0)" :: [p]"r"(p) : "r0");
+	return 0;
+}
+
+SEC("struct_ops/test_arena_member")
+__failure __msg("loads an arena pointer member on one path and something else on another")
+int BPF_PROG(mismatch_mem, struct bpf_testmod_arena_member_args *args)
+{
+	u64 local = 0;
+
+	asm volatile("" :: "r"(&arena));
+	return load_either(ctx, &local);
+}
+
+SEC(".struct_ops.link")
+struct bpf_testmod_ops3 testmod_arena_member_fail = {
+	.test_arena_member = (void *)mismatch_mem,
+};
diff --git a/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__no_arena.c b/tools/testing/selftests/bpf/progs/struct_ops_arena_member_fail__no_arena.c
new file mode 100644
index 0000000000000..3cf76e6615839
--- /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 0000000000000..d45666fd482c5
--- /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 0000000000000..bcf1f6586e196
--- /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 0000000000000..58516fdc13511
--- /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 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;
+	__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/progs/struct_ops_nullable.c b/tools/testing/selftests/bpf/progs/struct_ops_nullable.c
new file mode 100644
index 0000000000000..387792924c09e
--- /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 0000000000000..c15b3a8c8d39f
--- /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/progs/verifier_arena_scalar.c b/tools/testing/selftests/bpf/progs/verifier_arena_scalar.c
index bebc37f501b7e..a4f15eb21809d 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")
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
index b4f0af2aeca45..5a0556d4ce311 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
@@ -460,6 +460,16 @@ 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;
+}
+
 static struct bpf_testmod_ops3 __bpf_testmod_ops3 = {
 	.test_1 = bpf_testmod_test_3,
 	.test_2 = bpf_testmod_test_4,
@@ -467,6 +477,8 @@ 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,
 };
 
 static void bpf_testmod_test_struct_ops3(void)
@@ -507,6 +519,45 @@ __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
+ * 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 +977,8 @@ 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);
 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 2b12f81bf7487..e92f9fbfe6a67 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.h
@@ -114,6 +114,48 @@ struct bpf_testmod_arena_pair {
 	u64 b;
 };
 
+/*
+ * 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 */
+	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 +167,10 @@ 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);
 };
 
 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 67c02a421d133..8b85b2d73e1ca 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,8 @@ 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,
 				__u32 c, __u64 d) __ksym;

39/3 2026/10/13 08:50 llm kmsan-evaluator
1m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 30882
							output: 45
							thoughts: 1455
						

Thoughts:
**KMSAN Fuzzing Session Justification: BPF Arena Instruction Handling**

Alright, let's assess if this patch series warrants a dedicated KMSAN fuzzing session, specifically targeting the amd64 architecture, although broader kernel impacts are always important to consider. My initial thought is that this touches BPF arena instruction handling, which can be complex, especially with cross-architecture changes.

First, I need to understand exactly what the patch modifies. It's touching several architectures: arm64, loongarch, powerpc, riscv, s390, and x86, along with general BPF infrastructure. The common theme across those architectures is conditional assignment of `ctx.user_vm_start` and `ctx.arena_vm_start`, based on `bpf_prog_uses_arena_insns(prog)`. Okay, that's a hint. The x86 changes seem a bit more involved, with some tail call helper modifications and additions in `emit_kfunc_arena_args`. I see a `WARN_ON_ONCE` there, which is a red flag. Also, `do_jit` is changed; `arena_vm_start` and `user_vm_start` are explicitly initialized to zero, and the conditional assignment is used if the program uses arena instructions.  This changes from the original initialization from `bpf_arena_get_*`. That's a potential area for concern.

I see new additions to `bpf.h`, `bpf_verifier.h`, and `compiler_types.h`, that are all centered around arena BPF instructions.  It defines `__arena` and `__nullable` BTF type tags. These additions look like they are to extend the BPF verifier to deal with new type tags. This means it may be able to read and write from a pointer.

Digging deeper, `btf.c` and `fixups.c` handle these new tags and the new `uses_arena_insns` flag. The fixup process for arena members and propagating the `uses_arena_insns` flag are key. The verifier now includes checks for loads of arena pointers. This is crucial as any unintended memory accesses would be problematic.

Now, focusing on the changes in x86's `do_jit`, I see that the original patch sets `arena_vm_start` and `user_vm_start` to 0. It seems the code uses the return value of `bpf_prog_uses_arena_insns(bpf_prog)` to determine whether to set the correct values.  I need to check how the `ctx` struct is initialized across different architectures, since the patch introduces conditional logic to set VM start addresses. I'll need to check the arm64 implementation, and compare this to amd64. Let me verify the initialization of `ctx` in arm64's `bpf_int_jit_compile`, using `grepper` or `read-file` to check.

The changes in x86 look like a potential for a regression. The previous approach assumed the VM starts are initialized, while the new approach may have uninitialized values if it doesn't meet the correct criteria.

Therefore, given the potential for memory access issues related to the BPF arena and the conditional assignments of VM start addresses, **a KMSAN fuzzing session for this patch series on amd64 is definitely warranted.** We need to be especially mindful of how the verifier handles the new instruction set.



40/3 2026/10/13 08:51 tool read-file
0m Args:
{
  "File": "arch/arm64/net/bpf_jit_comp.c",
  "FirstLine": 2270,
  "LineCount": 35
}

Results:
{
  "Contents": "2270:\t\t\tctx = jit_data-\u003ectx;\n2271:\t\t\tro_image_ptr = jit_data-\u003ero_image;\n2272:\t\t\tro_header = jit_data-\u003ero_header;\n2273:\t\t\theader = jit_data-\u003eheader;\n2274:\t\t\timage_ptr = (void *)header + ((void *)ro_image_ptr\n2275:\t\t\t\t\t\t\t - (void *)ro_header);\n2276:\t\t\textra_pass = true;\n2277:\t\t\tprog_size = sizeof(u32) * ctx.idx;\n2278:\t\t\tgoto skip_init_ctx;\n2279:\t\t}\n2280:\t\tmemset(\u0026ctx, 0, sizeof(ctx));\n2281:\t\tctx.prog = prog;\n2282:\t\n2283:\t\tctx.offset = kvzalloc_objs(int, prog-\u003elen + 1);\n2284:\t\tif (ctx.offset == NULL)\n2285:\t\t\tgoto out_off;\n2286:\t\n2287:\t\tif (bpf_prog_uses_arena_insns(prog)) {\n2288:\t\t\tctx.user_vm_start = bpf_arena_get_user_vm_start(prog-\u003eaux-\u003earena);\n2289:\t\t\tctx.arena_vm_start = bpf_arena_get_kern_vm_start(prog-\u003eaux-\u003earena);\n2290:\t\t}\n2291:\t\n2292:\t\tout_cnt = bpf_out_stack_arg_cnt(env, prog);\n2293:\t\tif (out_cnt) {\n2294:\t\t\tint nr_on_stack = out_cnt - NR_STACK_ARG_REGS;\n2295:\t\n2296:\t\t\tif (nr_on_stack \u003e 0)\n2297:\t\t\t\tctx.stack_arg_size = round_up(nr_on_stack * sizeof(u64), 16);\n2298:\t\t}\n2299:\t\n2300:\t\tif (priv_stack_ptr)\n2301:\t\t\tctx.priv_sp_used = true;\n2302:\t\n2303:\t\t/* Pass 1: Estimate the maximum image size.\n2304:\t\t *\n"
}

41/3 2026/10/13 08:51 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 33722
							output: 43
							thoughts: 77
						

Thoughts:
Here's my thought process, as it stands:

**Checking Architecture-Specific Initialization of Contexts in BPF JIT Compilation**

Okay, so I'm thinking about BPF JIT compilation and the context initialization. I see in arm64, they're using `memset(&ctx, 0, sizeof(ctx));` before things get started. That makes sense; it's a common and generally safe way to ensure the context starts in a predictable, zeroed-out state.

But now I'm curious about other architectures. I know that's not always handled the same way across the board. So, I need to check how `arch/loongarch` handles this. I need to make sure this consistent practice is adopted across other architectures.

And of course, powerpc is always an interesting case to look at. So, I'll need to dig into the source code and find out. Let me take a look at `arch/powerpc/net/bpf_jit_comp.c` to see if there's any similar initialization there.



42/3 2026/10/13 08:51 tool read-file
0m Args:
{
  "File": "arch/powerpc/net/bpf_jit_comp.c",
  "FirstLine": 230,
  "LineCount": 25
}

Results:
{
  "Contents": " 230:\t\t\t/* During extra pass, ensure index is reset before repopulating extable entries */\n 231:\t\t\tcgctx.exentry_idx = 0;\n 232:\t\t\tgoto skip_init_ctx;\n 233:\t\t}\n 234:\t\n 235:\t\taddrs = kcalloc(flen + 1, sizeof(*addrs), GFP_KERNEL);\n 236:\t\tif (addrs == NULL)\n 237:\t\t\tgoto out_err;\n 238:\t\n 239:\t\tmemset(\u0026cgctx, 0, sizeof(struct codegen_context));\n 240:\t\tbpf_jit_init_reg_mapping(\u0026cgctx);\n 241:\t\n 242:\t\t/* Make sure that the stack is quadword aligned. */\n 243:\t\tcgctx.stack_size = round_up(fp-\u003eaux-\u003estack_depth, 16);\n 244:\t\tif (bpf_prog_uses_arena_insns(fp)) {\n 245:\t\t\tcgctx.arena_vm_start = bpf_arena_get_kern_vm_start(fp-\u003eaux-\u003earena);\n 246:\t\t\tcgctx.user_vm_start = bpf_arena_get_user_vm_start(fp-\u003eaux-\u003earena);\n 247:\t\t}\n 248:\t\tcgctx.is_subprog = bpf_is_subprog(fp);\n 249:\t\tcgctx.exception_boundary = fp-\u003eaux-\u003eexception_boundary;\n 250:\t\tcgctx.exception_cb = fp-\u003eaux-\u003eexception_cb;\n 251:\t\tcgctx.priv_sp = priv_stack_ptr;\n 252:\t\tcgctx.priv_stack_size = 0;\n 253:\t\tif (priv_stack_ptr) {\n 254:\t\t\t/*\n"
}

43/3 2026/10/13 08:51 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 34550
							output: 42
							thoughts: 54
						

Thoughts:
Here's my analysis of the provided snippet, tailored for an expert audience like yourself:

**Investigating Potential JIT Initialization Issues**

Okay, so I see a `memset` initializing a `cgctx` structure in `arch/powerpc` before its use. That's a common practice to ensure a clean state before passing it to subsequent JIT compilation steps. I'm wondering if this initialization strategy is consistently applied across other architectures, specifically RISC-V, which is relevant to my current work. I need to make sure this isn't a spot for potentially introducing a JIT bug.

Let's quickly check `arch/riscv`. Specifically, I'll need to look at `arch/riscv/net/bpf_jit_core.c`. I expect to find similar initialization patterns there, and if not, I'll need to dig deeper to understand the rationale behind the difference or evaluate if that code needs a security review. A missing initialization could mean that uninitialized memory leads to unexpected behavior in our compiled code, introducing vulnerabilities. I need to get to the bottom of this.



44/3 2026/10/13 08:51 tool read-file
0m Args:
{
  "File": "arch/riscv/net/bpf_jit_core.c",
  "FirstLine": 60,
  "LineCount": 25
}

Results:
{
  "Contents": "  60:\t\tif (!jit_data) {\n  61:\t\t\tjit_data = kzalloc_obj(*jit_data);\n  62:\t\t\tif (!jit_data) {\n  63:\t\t\t\treturn prog;\n  64:\t\t\t}\n  65:\t\t\tprog-\u003eaux-\u003ejit_data = jit_data;\n  66:\t\t}\n  67:\t\n  68:\t\tctx = \u0026jit_data-\u003ectx;\n  69:\t\n  70:\t\tif (ctx-\u003eoffset) {\n  71:\t\t\textra_pass = true;\n  72:\t\t\tprog_size = sizeof(*ctx-\u003einsns) * ctx-\u003eninsns;\n  73:\t\t\tgoto skip_init_ctx;\n  74:\t\t}\n  75:\t\n  76:\t\tif (bpf_prog_uses_arena_insns(prog)) {\n  77:\t\t\tctx-\u003earena_vm_start = bpf_arena_get_kern_vm_start(prog-\u003eaux-\u003earena);\n  78:\t\t\tctx-\u003euser_vm_start = bpf_arena_get_user_vm_start(prog-\u003eaux-\u003earena);\n  79:\t\t}\n  80:\t\n  81:\t\tstack_arg_cnt = bpf_out_stack_arg_cnt(env, prog);\n  82:\t\t/* First 3 stack args in regs, rest on stack */\n  83:\t\tctx-\u003estack_arg_sz = stack_arg_cnt \u003e RV_EXTRA_STK_ARGS ?\n  84:\t\t\t\t    round_up((stack_arg_cnt - RV_EXTRA_STK_ARGS) * 8, STACK_ALIGN) : 0;\n"
}

45/3 2026/10/13 08:51 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 35295
							output: 46
							thoughts: 54
						

Thoughts:
Here's my summary, as the expert:

**Investigating JIT Data Initialization**

Okay, so I see a `kzalloc_obj()` call on `jit_data`. This immediately zeroes out the memory allocated for the `jit_data` structure. That’s good, standard practice for avoiding uninitialized memory issues. Now, I need to make sure this is consistent across all relevant architectures, and how it is being used. Let's specifically check how this is handled in `arch/s390`, it's always good to be thorough and aware of architecture-specific implementations. I'll take a look at `arch/s390/net/bpf_jit_comp.c` to see if there are any specific considerations there.



46/3 2026/10/13 08:51 tool read-file
0m Args:
{
  "File": "arch/s390/net/bpf_jit_comp.c",
  "FirstLine": 2285,
  "LineCount": 25
}

Results:
{
  "Contents": "2285:\t\tjit-\u003elit32 = jit-\u003elit32_start;\n2286:\t\tjit-\u003elit64 = jit-\u003elit64_start;\n2287:\t\tjit-\u003eprg = 0;\n2288:\t\tjit-\u003eexcnt = 0;\n2289:\t\tif (is_first_pass(jit) || (jit-\u003eseen \u0026 SEEN_STACK))\n2290:\t\t\tjit-\u003eframe_off = sizeof(struct prog_frame) -\n2291:\t\t\t\t\t offsetofend(struct prog_frame, unused) +\n2292:\t\t\t\t\t round_up(fp-\u003eaux-\u003estack_depth, 8);\n2293:\t\telse\n2294:\t\t\tjit-\u003eframe_off = 0;\n2295:\t\n2296:\t\tif (bpf_prog_uses_arena_insns(fp)) {\n2297:\t\t\tkern_arena = bpf_arena_get_kern_vm_start(fp-\u003eaux-\u003earena);\n2298:\t\t\tjit-\u003ekern_arena = _EMIT_CONST_U64(kern_arena);\n2299:\t\t\tjit-\u003euser_arena = bpf_arena_get_user_vm_start(fp-\u003eaux-\u003earena);\n2300:\t\t}\n2301:\t\n2302:\t\tbpf_jit_prologue(jit, fp);\n2303:\t\tif (bpf_set_addr(jit, 0) \u003c 0)\n2304:\t\t\treturn -1;\n2305:\t\tfor (i = 0; i \u003c fp-\u003elen; i += insn_count) {\n2306:\t\t\tinsn_count = bpf_jit_insn(jit, fp, i, extra_pass);\n2307:\t\t\tif (insn_count \u003c 0)\n2308:\t\t\t\treturn -1;\n2309:\t\t\t/* Next instruction address */\n"
}

47/3 2026/10/13 08:51 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 36045
							output: 44
							thoughts: 24
						

Thoughts:
Here's a summary of my thinking, presented as requested:

**Investigating JIT Behavior on s390 Architecture**

Alright, let's dive into this. My primary focus is on how the Just-In-Time (JIT) compilation process, specifically related to the `bpf_jit_prog` structure, behaves on the s390 architecture. I'm essentially tracing the interaction between `jit` and this broader structure. I need to understand what specific transformations, optimizations, or limitations might arise when compiling BPF (Berkeley Packet Filter) code on this hardware. The s390 architecture has its own unique characteristics, its own registers, instruction sets, and memory access patterns. So, I need to know how these idiosyncrasies influence `jit`'s behavior and performance. I need to find out how it deals with the s390 instruction set and memory model, and if there are specific workarounds or optimizations that have been implemented to ensure efficient JIT compilation. Essentially, I'm trying to determine the impact on `jit` specifically before the whole `bpf_jit_prog` structure takes over.



48/3 2026/10/13 08:52 tool grepper
0m Args:
{
  "Expression": "bpf_jit_prog",
  "PathPrefixes": [
    "arch/s390/net/bpf_jit_comp.c"
  ]
}

Results:
{
  "Output": "arch/s390/net/bpf_jit_comp.c=2261=static int bpf_set_addr(struct bpf_jit *jit, int i)\n--\narch/s390/net/bpf_jit_comp.c-2278- */\narch/s390/net/bpf_jit_comp.c:2279:static int bpf_jit_prog(struct bpf_jit *jit, struct bpf_prog *fp,\narch/s390/net/bpf_jit_comp.c-2280-\t\t\tbool extra_pass)\n--\narch/s390/net/bpf_jit_comp.c=2387=struct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_prog *fp)\n--\narch/s390/net/bpf_jit_comp.c-2422-\tfor (pass = 1; pass \u003c= 3; pass++) {\narch/s390/net/bpf_jit_comp.c:2423:\t\tif (bpf_jit_prog(\u0026jit, fp, extra_pass))\narch/s390/net/bpf_jit_comp.c-2424-\t\t\tgoto out_err;\n--\narch/s390/net/bpf_jit_comp.c-2432-skip_init_ctx:\narch/s390/net/bpf_jit_comp.c:2433:\tif (bpf_jit_prog(\u0026jit, fp, extra_pass)) {\narch/s390/net/bpf_jit_comp.c-2434-\t\tbpf_jit_binary_free(header);\n"
}

49/3 2026/10/13 08:52 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 36798
							output: 46
							thoughts: 28
						

Thoughts:
Okay, here's my interpretation of that thought process, tailored for an expert audience, and written in the first person:

**Diving into the BPF JIT Compiler for s390**

Alright, time to dig in. I need to understand what's happening with the BPF JIT compiler on s390. Specifically, I need to check out what's going on at `arch/s390/net/bpf_jit_comp.c:2387`. That line number likely represents a crucial point in the code. I'm guessing it's either an important stage in the compilation process, perhaps a key code generation step, maybe an optimization hook, or even a critical error handling path. I need to understand the logic in that section of the code and how it interacts with the rest of the compilation process for BPF programs on this architecture. I'll need to consider how this function interacts with the register allocation, instruction selection, and code emission phases. I will likely start by checking the surrounding context to understand which function this line of code is within.



50/3 2026/10/13 08:52 tool read-file
0m Args:
{
  "File": "arch/s390/net/bpf_jit_comp.c",
  "FirstLine": 2387,
  "LineCount": 40
}

Results:
{
  "Contents": "2387:\tstruct bpf_prog *bpf_int_jit_compile(struct bpf_verifier_env *env, struct bpf_prog *fp)\n2388:\t{\n2389:\t\tstruct bpf_binary_header *header;\n2390:\t\tstruct s390_jit_data *jit_data;\n2391:\t\tbool extra_pass = false;\n2392:\t\tstruct bpf_jit jit;\n2393:\t\tint pass;\n2394:\t\n2395:\t\tif (!fp-\u003ejit_requested)\n2396:\t\t\treturn fp;\n2397:\t\n2398:\t\tjit_data = fp-\u003eaux-\u003ejit_data;\n2399:\t\tif (!jit_data) {\n2400:\t\t\tjit_data = kzalloc_obj(*jit_data);\n2401:\t\t\tif (!jit_data)\n2402:\t\t\t\treturn fp;\n2403:\t\t\tfp-\u003eaux-\u003ejit_data = jit_data;\n2404:\t\t}\n2405:\t\tif (jit_data-\u003ectx.addrs) {\n2406:\t\t\tjit = jit_data-\u003ectx;\n2407:\t\t\theader = jit_data-\u003eheader;\n2408:\t\t\textra_pass = true;\n2409:\t\t\tpass = jit_data-\u003epass + 1;\n2410:\t\t\tgoto skip_init_ctx;\n2411:\t\t}\n2412:\t\n2413:\t\tmemset(\u0026jit, 0, sizeof(jit));\n2414:\t\tjit.addrs = kvcalloc(fp-\u003elen + 1, sizeof(*jit.addrs), GFP_KERNEL);\n2415:\t\tif (jit.addrs == NULL)\n2416:\t\t\tgoto out_err;\n2417:\t\t/*\n2418:\t\t * Three initial passes:\n2419:\t\t *   - 1/2: Determine clobbered registers\n2420:\t\t *   - 3:   Calculate program size and addrs array\n2421:\t\t */\n2422:\t\tfor (pass = 1; pass \u003c= 3; pass++) {\n2423:\t\t\tif (bpf_jit_prog(\u0026jit, fp, extra_pass))\n2424:\t\t\t\tgoto out_err;\n2425:\t\t}\n2426:\t\t/*\n"
}

51/3 2026/10/13 08:52 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 37714
							output: 50
							thoughts: 1261
						

Thoughts:
**Analyzing the BTF Arena and Nullable Pointer Support Patch**

Okay, so first things first, `memset` is called on `jit` to zero-initialize the context before `bpf_jit_prog`. Always a good practice, ensuring consistent starting conditions across architectures.

Right, this patch is about supporting `__arena` and `__nullable` tags for BPF type format (BTF) within kernel structures. The goal is also optimization of the arena initialization for the JIT compiler.

The main idea is this: libbpf binds an arena to every program in the object.  Previously, if a program *had* an arena, the JIT would blindly set up the arena base registers (like r12 on x86).  But now, a program might have an arena associated with it but not actually *use* any arena instructions.  This patch aims to optimize that.

The key addition here is support for `__arena` and `__nullable` pointers as struct members in the kernel, particularly in contexts like `struct_ops` callbacks. When a BPF program accesses an `__arena` member, the kernel virtual address is stored in that member. The verifier will perform a rewrite, converting loads of these `kaddr` members into arena pointer form using `arena_member_fixup()`.  It subtracts `kern_vm_start` from `kaddr`. If `__nullable` is specified, it will first test if the pointer is null.

The code now tracks arena usage more closely with `mark_uses_arena_insn()` in several verifier functions.  If a register type is `PTR_TO_ARENA` or if it's a function argument of type `ARG_PTR_TO_ARENA` and the meta function uses `is_kfunc()`, it is marked as arena using. Also, `check_alu_op` will check to see if an ALU op is arena using.

The JIT will only bother setting up `arena_vm_start` and `user_vm_start` and saving/restoring the arena register (like r12 on x86) *if* `bpf_prog_uses_arena_insns(prog)` returns true. That's the optimization.

Now, let's get down to brass tacks:  the KMSAN/KASAN question. We *only* need KMSAN if there's a risk of *uninitialized* memory that KASAN, LOCKDEP, or other tools won't catch.  The rules are very specific: kernel structs sent to userspace, conditional logic using uninitialized variables, allocation of complex data structures with uninitialized fields, or vulnerabilities caused by bad bounds checking.

Let's go through the code changes point by point. We need to check for anything that could introduce a risk of uninitialized memory usage or info leaks.

First, looking at `arch/x86/net/bpf_jit_comp.c`:  The variables `arena_vm_start` and `user_vm_start` are initialized to 0. The JIT setup of arena parameters *only* occurs if the program uses arena instructions.  In the original code these variables are initialized using the `bpf_arena_get_*` functions. Now it is only called if `bpf_prog_uses_arena_insns(bpf_prog)` is true.

Wait, where are those variables `arena_vm_start` and `user_vm_start` used within `do_jit()`?  That's where the focus needs to be to prevent any uninitialized memory. Let's dig in a bit more.



52/3 2026/10/13 08:52 tool grepper
0m Args:
{
  "Expression": "(arena_vm_start|user_vm_start)",
  "PathPrefixes": [
    "arch/x86/net/bpf_jit_comp.c"
  ]
}

Results:
{
  "Output": "arch/x86/net/bpf_jit_comp.c=2003=static int do_jit(struct bpf_verifier_env *env, struct bpf_prog *bpf_prog, int *addrs, u8 *image,\n--\narch/x86/net/bpf_jit_comp.c-2013-\tu16 out_stack_arg_cnt, outgoing_rsp;\narch/x86/net/bpf_jit_comp.c:2014:\tu64 arena_vm_start = 0, user_vm_start = 0;\narch/x86/net/bpf_jit_comp.c-2015-\tvoid __percpu *priv_stack_ptr;\n--\narch/x86/net/bpf_jit_comp.c-2054-\tif (bpf_prog_uses_arena_insns(bpf_prog)) {\narch/x86/net/bpf_jit_comp.c:2055:\t\tarena_vm_start = bpf_arena_get_kern_vm_start(bpf_prog-\u003eaux-\u003earena);\narch/x86/net/bpf_jit_comp.c:2056:\t\tuser_vm_start = bpf_arena_get_user_vm_start(bpf_prog-\u003eaux-\u003earena);\narch/x86/net/bpf_jit_comp.c-2057-\t}\n--\narch/x86/net/bpf_jit_comp.c-2077-\t} else {\narch/x86/net/bpf_jit_comp.c:2078:\t\tif (arena_vm_start)\narch/x86/net/bpf_jit_comp.c-2079-\t\t\tpush_r12(\u0026prog);\n--\narch/x86/net/bpf_jit_comp.c-2084-\tcallee_saved_size = 0;\narch/x86/net/bpf_jit_comp.c:2085:\tif (bpf_prog-\u003eaux-\u003eexception_boundary || arena_vm_start)\narch/x86/net/bpf_jit_comp.c-2086-\t\tcallee_saved_size += 8; /* r12 */\n--\narch/x86/net/bpf_jit_comp.c-2119-\narch/x86/net/bpf_jit_comp.c:2120:\tif (arena_vm_start)\narch/x86/net/bpf_jit_comp.c-2121-\t\temit_mov_imm64(\u0026prog, X86_REG_R12,\narch/x86/net/bpf_jit_comp.c:2122:\t\t\t       arena_vm_start \u003e\u003e 32, (u32) arena_vm_start);\narch/x86/net/bpf_jit_comp.c-2123-\n--\narch/x86/net/bpf_jit_comp.c-2191-\narch/x86/net/bpf_jit_comp.c:2192:\t\t\t\t/* or dst_reg, user_vm_start */\narch/x86/net/bpf_jit_comp.c-2193-\t\t\t\tmaybe_emit_1mod(\u0026prog, dst_reg, true);\narch/x86/net/bpf_jit_comp.c-2194-\t\t\t\tif (is_axreg(dst_reg))\narch/x86/net/bpf_jit_comp.c:2195:\t\t\t\t\tEMIT1_off32(0x0D,  user_vm_start \u003e\u003e 32);\narch/x86/net/bpf_jit_comp.c-2196-\t\t\t\telse\narch/x86/net/bpf_jit_comp.c:2197:\t\t\t\t\tEMIT2_off32(0x81, add_1reg(0xC8, dst_reg),  user_vm_start \u003e\u003e 32);\narch/x86/net/bpf_jit_comp.c-2198-\n--\narch/x86/net/bpf_jit_comp.c-2656-\t\t\t\t * 0x0, 0x1) instruction. This address is adjusted with the addition\narch/x86/net/bpf_jit_comp.c:2657:\t\t\t\t * of arena_vm_start (see the implementation of BPF_PROBE_MEM32 and\narch/x86/net/bpf_jit_comp.c-2658-\t\t\t\t * BPF_PROBE_ATOMIC) before being used for the memory access. Pass\n--\narch/x86/net/bpf_jit_comp.c-3295-\t\t\t\tpop_callee_regs(\u0026prog, callee_regs_used);\narch/x86/net/bpf_jit_comp.c:3296:\t\t\t\tif (arena_vm_start)\narch/x86/net/bpf_jit_comp.c-3297-\t\t\t\t\tpop_r12(\u0026prog);\n"
}

53/3 2026/10/13 08:52 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 41088
							output: 26
							thoughts: 360
						

Thoughts:
**Analyzing Arena VM Initialization and Usage**

Okay, let's break this down. In `do_jit`, I'm seeing that `arena_vm_start` and `user_vm_start` are *initially* set to zero. That makes sense as a default. Now, if the BPF program doesn't utilize arena instructions, indicated by `bpf_prog_uses_arena_insns(bpf_prog)` being false, those variables *stay* at zero. Makes sense for optimization and to avoid doing unnecessary work.

Now, looking at the subsequent lines, there are a number of conditional blocks dependent on `arena_vm_start`. Specifically, lines 2078, 2085, 2120, and 3296 are checking this. If `arena_vm_start` is set, we're pushing `r12`, potentially increasing `callee_saved_size`, and emitting `mov_imm64` instructions. On line 3296, we pop r12. Okay, so that suggests `arena_vm_start` is being used to determine if we're dealing with arena-specific runtime requirements, like saving and restoring registers.

Then, there's `user_vm_start` on line 2192, which is being used in `addr_space_cast`. But here's the kicker: `addr_space_cast` *itself* calls `mark_uses_arena_insn(env)` during verifier checks! Thus, if `addr_space_cast` *is* present in the program, `bpf_prog_uses_arena_insns` will be set to *true*, and `user_vm_start` will get initialized. In fact, even if it wasn't there, both are initialized to zero.

So, the dependency is clear: `addr_space_cast` triggers the "arena" mode.

Now, I need to understand the allocation and lifecycle of the `bpf_prog_aux` structure. Let's go check how `prog->aux` is allocated. I need to understand how the allocation of `bpf_prog_aux` is done to build a mental picture of the underlying context.



54/3 2026/10/13 08:52 tool grepper
0m Error:
no matches

Args:
{
  "Expression": "kzalloc.*sizeof\\(struct bpf_prog_aux"
}

Results:
{
  "error": "no matches"
}

55/3 2026/10/13 08:52 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 41974
							output: 18
							thoughts: 13
						

Thoughts:
Here's a summary of my thought process:

**Where Did That Allocation Happen?**

Okay, I need to figure out the allocation point for `prog->aux`. This means I'm going to have to dive into the code and trace where that memory is being managed. My first move will be to search the codebase for where `prog->aux` itself is allocated. I need to understand the lifecycle of that particular memory region. This search will be crucial to understanding any potential memory-related issues, or even simply for debugging purposes.



56/3 2026/10/13 08:52 tool grepper
0m Error:
no matches

Args:
{
  "Expression": "prog-\u003eaux ="
}

Results:
{
  "error": "no matches"
}

57/3 2026/10/13 08:52 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 42131
							output: 34
							thoughts: 13
						

Thoughts:
Okay, here's my interpretation of that thought process, tailored for an expert audience, and written in the first person:

**Delving into the BPF Landscape**

Alright, so the task at hand requires exploring the world of BPF – specifically, I need to understand how programs are allocated. My initial instinct, based on experience, is to zero in on the function responsible for that. So, let's start with a direct search. My search query is going to be `bpf_prog_alloc`. That seems like the most likely starting point, right? That's probably where the meat is. I should be able to get a quick handle on what's going on from there.



58/3 2026/10/13 08:52 tool grepper
0m Args:
{
  "Expression": "bpf_prog_alloc.*\\(",
  "PathPrefixes": [
    "kernel/bpf/"
  ]
}

Results:
{
  "Output": "kernel/bpf/core.c=98=enum page_size_enum {\n--\nkernel/bpf/core.c-101-\nkernel/bpf/core.c:102:struct bpf_prog *bpf_prog_alloc_no_stats(unsigned int size, gfp_t gfp_extra_flags)\nkernel/bpf/core.c-103-{\n--\nkernel/bpf/core.c-148-\nkernel/bpf/core.c:149:struct bpf_prog *bpf_prog_alloc(unsigned int size, gfp_t gfp_extra_flags)\nkernel/bpf/core.c-150-{\n--\nkernel/bpf/core.c-154-\nkernel/bpf/core.c:155:\tprog = bpf_prog_alloc_no_stats(size, gfp_extra_flags);\nkernel/bpf/core.c-156-\tif (!prog)\n--\nkernel/bpf/core.c=175=EXPORT_SYMBOL_GPL(bpf_prog_alloc);\nkernel/bpf/core.c-176-\nkernel/bpf/core.c:177:int bpf_prog_alloc_jited_linfo(struct bpf_prog *prog)\nkernel/bpf/core.c-178-{\n--\nkernel/bpf/core.c=2676=struct bpf_prog *__bpf_prog_select_runtime(struct bpf_verifier_env *env, struct bpf_prog *fp,\n--\nkernel/bpf/core.c-2698-\tif (!bpf_prog_is_offloaded(fp-\u003eaux)) {\nkernel/bpf/core.c:2699:\t\t*err = bpf_prog_alloc_jited_linfo(fp);\nkernel/bpf/core.c-2700-\t\tif (*err)\n--\nkernel/bpf/fixups.c=1301=static int jit_subprogs(struct bpf_verifier_env *env)\n--\nkernel/bpf/fixups.c-1346-\nkernel/bpf/fixups.c:1347:\terr = bpf_prog_alloc_jited_linfo(prog);\nkernel/bpf/fixups.c-1348-\tif (err)\n--\nkernel/bpf/fixups.c-1365-\t\t */\nkernel/bpf/fixups.c:1366:\t\tfunc[i] = bpf_prog_alloc_no_stats(bpf_prog_size(len), GFP_USER);\nkernel/bpf/fixups.c-1367-\t\tif (!func[i])\n--\nkernel/bpf/syscall.c=2403=static void bpf_audit_prog(const struct bpf_prog *prog, unsigned int op)\n--\nkernel/bpf/syscall.c-2421-\nkernel/bpf/syscall.c:2422:static int bpf_prog_alloc_id(struct bpf_prog *prog)\nkernel/bpf/syscall.c-2423-{\n--\nkernel/bpf/syscall.c=2953=static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr, struct bpf_log_attr *attr_log)\n--\nkernel/bpf/syscall.c-3081-\t/* plain bpf_prog allocation */\nkernel/bpf/syscall.c:3082:\tprog = bpf_prog_alloc(bpf_prog_size(attr-\u003einsn_cnt), GFP_USER);\nkernel/bpf/syscall.c-3083-\tif (!prog) {\n--\nkernel/bpf/syscall.c-3184-\nkernel/bpf/syscall.c:3185:\terr = bpf_prog_alloc_id(prog);\nkernel/bpf/syscall.c-3186-\tif (err)\n--\nkernel/bpf/syscall.c-3188-\nkernel/bpf/syscall.c:3189:\t/* Upon success of bpf_prog_alloc_id(), the BPF prog is\nkernel/bpf/syscall.c-3190-\t * effectively publicly exposed. However, retrieving via\n--\nkernel/bpf/trampoline.c=1129=static struct bpf_shim_tramp_link *cgroup_shim_alloc(const struct bpf_prog *prog,\n--\nkernel/bpf/trampoline.c-1140-\nkernel/bpf/trampoline.c:1141:\tp = bpf_prog_alloc(1, 0);\nkernel/bpf/trampoline.c-1142-\tif (!p) {\n"
}

59/3 2026/10/13 08:52 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 43516
							output: 36
							thoughts: 16
						

Thoughts:
Okay, let's dive into this `bpf_prog_alloc_no_stats` function. I need to understand its role within the BPF system. From the name, I'm inferring it's related to program allocation, and the "no_stats" part suggests it might be a variation that bypasses some performance metric tracking, maybe for efficiency or specific use cases. I need to carefully consider the implications, as this suggests a trade-off. Is this for a high-frequency program? A debugging tool? What's the design rationale behind providing an allocation path *without* statistics gathering? This seems like a potential optimization or a specific niche use-case, I will have to investigate what its function is. It is important I understand the implications before I continue to use it, to be sure I am working with all the correct information.



60/3 2026/10/13 08:52 tool read-file
0m Args:
{
  "File": "kernel/bpf/core.c",
  "FirstLine": 102,
  "LineCount": 40
}

Results:
{
  "Contents": " 102:\tstruct bpf_prog *bpf_prog_alloc_no_stats(unsigned int size, gfp_t gfp_extra_flags)\n 103:\t{\n 104:\t\tgfp_t gfp_flags = bpf_memcg_flags(GFP_KERNEL | __GFP_ZERO | gfp_extra_flags);\n 105:\t\tstruct bpf_prog_aux *aux;\n 106:\t\tstruct bpf_prog *fp;\n 107:\t\n 108:\t\tsize = round_up(size, __PAGE_SIZE);\n 109:\t\tfp = __vmalloc(size, gfp_flags);\n 110:\t\tif (fp == NULL)\n 111:\t\t\treturn NULL;\n 112:\t\n 113:\t\taux = kzalloc_obj(*aux, bpf_memcg_flags(GFP_KERNEL | gfp_extra_flags));\n 114:\t\tif (aux == NULL) {\n 115:\t\t\tvfree(fp);\n 116:\t\t\treturn NULL;\n 117:\t\t}\n 118:\t\tfp-\u003eactive = __alloc_percpu_gfp(sizeof(u8[BPF_NR_CONTEXTS]), 4,\n 119:\t\t\t\t\t\tbpf_memcg_flags(GFP_KERNEL | gfp_extra_flags));\n 120:\t\tif (!fp-\u003eactive) {\n 121:\t\t\tvfree(fp);\n 122:\t\t\tkfree(aux);\n 123:\t\t\treturn NULL;\n 124:\t\t}\n 125:\t\n 126:\t\tfp-\u003epages = size / PAGE_SIZE;\n 127:\t\tfp-\u003eaux = aux;\n 128:\t\tfp-\u003eaux-\u003emain_prog_aux = aux;\n 129:\t\tfp-\u003eaux-\u003eprog = fp;\n 130:\t\tfp-\u003ejit_requested = ebpf_jit_enabled();\n 131:\t\tfp-\u003ejit_required = IS_ENABLED(CONFIG_BPF_JIT_ALWAYS_ON);\n 132:\t\tfp-\u003eblinding_requested = bpf_jit_blinding_enabled(fp);\n 133:\t#ifdef CONFIG_CGROUP_BPF\n 134:\t\taux-\u003ecgroup_atype = CGROUP_BPF_ATTACH_TYPE_INVALID;\n 135:\t#endif\n 136:\t\n 137:\t\tINIT_LIST_HEAD_RCU(\u0026fp-\u003eaux-\u003eksym.lnode);\n 138:\t#ifdef CONFIG_FINEIBT\n 139:\t\tINIT_LIST_HEAD_RCU(\u0026fp-\u003eaux-\u003eksym_prefix.lnode);\n 140:\t#endif\n 141:\t\tmutex_init(\u0026fp-\u003eaux-\u003eused_maps_mutex);\n"
}

61/3 2026/10/13 08:52 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 44456
							output: 35
							thoughts: 48
						

Thoughts:
Okay, here's my take on those thoughts, framed as my own:

**Analyzing Memory Allocation in the eBPF Verifier**

Alright, the code's using `kzalloc_obj()` - that immediately flags my mental checklist. Good, it's zero-initialized. Always a plus, less chance of picking up garbage data and introducing subtle, hard-to-debug bugs. That’s standard operating procedure when dealing with kernel memory.

Now, my focus shifts to the verifier, specifically `insn_aux_data`. I need to understand how it's being allocated. Are we getting the zero-initialization assurance for that struct as well? I have to trace back through the code to see where that `insn_aux_data` member is actually getting its memory. Is it `kzalloc()`? Or perhaps `kmalloc()`? Or something else entirely? A simple memory allocation trace should tell the story.



62/3 2026/10/13 08:52 tool grepper
0m Args:
{
  "Expression": "insn_aux_data",
  "PathPrefixes": [
    "kernel/bpf/verifier.c"
  ]
}

Results:
{
  "Output": "\nFull output is too long, showing 200 out of 325 lines.\nUse more precise expression or PathPrefixes if possible.\n\nNumber of matching lines per file (1 files in total):\nkernel/bpf/verifier.c:73\n\nkernel/bpf/verifier.c=224=static void scalar_min_max_add(struct bpf_reg_state *dst_reg, struct bpf_reg_state *src_reg);\nkernel/bpf/verifier.c-225-\nkernel/bpf/verifier.c:226:static void bpf_map_ptr_store(struct bpf_insn_aux_data *aux,\nkernel/bpf/verifier.c-227-\t\t\t      struct bpf_map *map,\n--\nkernel/bpf/verifier.c-235-\nkernel/bpf/verifier.c:236:static void bpf_map_key_store(struct bpf_insn_aux_data *aux, u64 state)\nkernel/bpf/verifier.c-237-{\n--\nkernel/bpf/verifier.c=3571=static void mark_indirect_target(struct bpf_verifier_env *env, int idx)\nkernel/bpf/verifier.c-3572-{\nkernel/bpf/verifier.c:3573:\tenv-\u003einsn_aux_data[idx].indirect_target = true;\nkernel/bpf/verifier.c-3574-}\n--\nkernel/bpf/verifier.c=3576=static void mark_non_stack_access(struct bpf_verifier_env *env, int idx)\nkernel/bpf/verifier.c-3577-{\nkernel/bpf/verifier.c:3578:\tenv-\u003einsn_aux_data[idx].non_stack_access = true;\nkernel/bpf/verifier.c-3579-}\n--\nkernel/bpf/verifier.c=3814=static void check_fastcall_stack_contract(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-3817-\tstruct bpf_subprog_info *subprog = \u0026env-\u003esubprog_info[state-\u003esubprogno];\nkernel/bpf/verifier.c:3818:\tstruct bpf_insn_aux_data *aux = env-\u003einsn_aux_data;\nkernel/bpf/verifier.c-3819-\tint i;\n--\nkernel/bpf/verifier.c=3854=static int check_stack_write_fixed_off(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-3905-\t\tif (sanitize)\nkernel/bpf/verifier.c:3906:\t\t\tenv-\u003einsn_aux_data[insn_idx].nospec_result = true;\nkernel/bpf/verifier.c-3907-\t}\n--\nkernel/bpf/verifier.c=5261=static int __check_ctx_access(struct bpf_verifier_env *env, int insn_idx, int off, int size,\n--\nkernel/bpf/verifier.c-5280-\t\t} else {\nkernel/bpf/verifier.c:5281:\t\t\tenv-\u003einsn_aux_data[insn_idx].ctx_field_size = info-\u003ectx_field_size;\nkernel/bpf/verifier.c-5282-\t\t}\n--\nkernel/bpf/verifier.c=5343=static int check_sock_access(struct bpf_verifier_env *env, int insn_idx,\n--\nkernel/bpf/verifier.c-5373-\tif (valid) {\nkernel/bpf/verifier.c:5374:\t\tenv-\u003einsn_aux_data[insn_idx].ctx_field_size =\nkernel/bpf/verifier.c-5375-\t\t\tinfo.ctx_field_size;\n--\nkernel/bpf/verifier.c=6572=static int check_arena_member_load(struct bpf_verifier_env *env, struct bpf_reg_state *regs,\n--\nkernel/bpf/verifier.c-6575-{\nkernel/bpf/verifier.c:6576:\tstruct bpf_insn_aux_data *aux = \u0026env-\u003einsn_aux_data[env-\u003einsn_idx];\nkernel/bpf/verifier.c-6577-\n--\nkernel/bpf/verifier.c=7442=static struct bpf_reg_state *mem_access_reg(struct bpf_verifier_env *env, int regno,\n--\nkernel/bpf/verifier.c-7449-\tarena-\u003etype = PTR_TO_ARENA;\nkernel/bpf/verifier.c:7450:\tenv-\u003einsn_aux_data[env-\u003einsn_idx].arena_scalar = true;\nkernel/bpf/verifier.c-7451-\tmark_uses_arena_insn(env);\n--\nkernel/bpf/verifier.c=7455=static int check_load_mem(struct bpf_verifier_env *env, struct bpf_insn *insn,\n--\nkernel/bpf/verifier.c-7504-\t\t      regs[insn-\u003edst_reg].type == PTR_TO_ARENA))\nkernel/bpf/verifier.c:7505:\t\tenv-\u003einsn_aux_data[env-\u003einsn_idx].non_arena_load = true;\nkernel/bpf/verifier.c-7506-\terr = err ?: save_aux_ptr_type(env, src_reg_type,\n--\nkernel/bpf/verifier.c=9033=static int get_bpf_res_spin_lock_kfunc_flags(const struct bpf_call_arg_meta *meta);\nkernel/bpf/verifier.c:9034:static struct bpf_insn_aux_data *cur_aux(const struct bpf_verifier_env *env);\nkernel/bpf/verifier.c-9035-static int process_irq_flag(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c=11586=static int set_map_elem_callback_state(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-11590-{\nkernel/bpf/verifier.c:11591:\tstruct bpf_insn_aux_data *insn_aux = \u0026env-\u003einsn_aux_data[insn_idx];\nkernel/bpf/verifier.c-11592-\tstruct bpf_map *map;\n--\nkernel/bpf/verifier.c=12062=record_func_map(struct bpf_verifier_env *env, struct bpf_call_arg_meta *meta,\n--\nkernel/bpf/verifier.c-12064-{\nkernel/bpf/verifier.c:12065:\tstruct bpf_insn_aux_data *aux = \u0026env-\u003einsn_aux_data[insn_idx];\nkernel/bpf/verifier.c-12066-\tstruct bpf_map *map = meta-\u003emap.ptr;\n--\nkernel/bpf/verifier.c=12108=record_func_key(struct bpf_verifier_env *env, struct bpf_call_arg_meta *meta,\n--\nkernel/bpf/verifier.c-12110-{\nkernel/bpf/verifier.c:12111:\tstruct bpf_insn_aux_data *aux = \u0026env-\u003einsn_aux_data[insn_idx];\nkernel/bpf/verifier.c-12112-\tstruct bpf_reg_state *reg;\n--\nkernel/bpf/verifier.c=12252=static int check_get_func_ip(struct bpf_verifier_env *env)\n--\nkernel/bpf/verifier.c-12272-\nkernel/bpf/verifier.c:12273:static struct bpf_insn_aux_data *cur_aux(const struct bpf_verifier_env *env)\nkernel/bpf/verifier.c-12274-{\nkernel/bpf/verifier.c:12275:\treturn \u0026env-\u003einsn_aux_data[env-\u003einsn_idx];\nkernel/bpf/verifier.c-12276-}\n--\nkernel/bpf/verifier.c=12389=static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn,\n--\nkernel/bpf/verifier.c-12474-\tif (!in_sleepable_context(env))\nkernel/bpf/verifier.c:12475:\t\tenv-\u003einsn_aux_data[insn_idx].non_sleepable = true;\nkernel/bpf/verifier.c-12476-\n--\nkernel/bpf/verifier.c-12638-\t\t\treturns_cpu_specific_alloc_ptr = true;\nkernel/bpf/verifier.c:12639:\t\t\tenv-\u003einsn_aux_data[insn_idx].call_with_percpu_alloc_ptr = true;\nkernel/bpf/verifier.c-12640-\t\t}\n--\nkernel/bpf/verifier.c=14684=bpf_helper_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn *insn,\n--\nkernel/bpf/verifier.c-14686-{\nkernel/bpf/verifier.c:14687:\tstruct bpf_insn_aux_data *aux = \u0026env-\u003einsn_aux_data[insn_idx];\nkernel/bpf/verifier.c-14688-\tstruct arg_access_info info = {\n--\nkernel/bpf/verifier.c=14812=bpf_kfunc_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn *insn,\n--\nkernel/bpf/verifier.c-14814-{\nkernel/bpf/verifier.c:14815:\tstruct bpf_insn_aux_data *aux = \u0026env-\u003einsn_aux_data[insn_idx];\nkernel/bpf/verifier.c-14816-\tstruct arg_access_info info = {\n--\nkernel/bpf/verifier.c=14974=static int check_special_kfunc(struct bpf_verifier_env *env, struct bpf_call_arg_meta *meta,\nkernel/bpf/verifier.c:14975:\t\t\t       struct bpf_reg_state *regs, struct bpf_insn_aux_data *insn_aux,\nkernel/bpf/verifier.c-14976-\t\t\t       const struct btf_type *ptr_type, struct btf *desc_btf)\n--\nkernel/bpf/verifier.c=15145=static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,\n--\nkernel/bpf/verifier.c-15153-\tstruct bpf_call_arg_meta meta;\nkernel/bpf/verifier.c:15154:\tstruct bpf_insn_aux_data *insn_aux;\nkernel/bpf/verifier.c-15155-\tconst char *operation;\n--\nkernel/bpf/verifier.c-15177-\tfunc_name = meta.func_name;\nkernel/bpf/verifier.c:15178:\tinsn_aux = \u0026env-\u003einsn_aux_data[insn_idx];\nkernel/bpf/verifier.c-15179-\n--\nkernel/bpf/verifier.c=15729=static bool can_skip_alu_sanitation(const struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-15736-\nkernel/bpf/verifier.c:15737:static int update_alu_sanitation_state(struct bpf_insn_aux_data *aux,\nkernel/bpf/verifier.c-15738-\t\t\t\t       u32 alu_state, u32 alu_limit)\n--\nkernel/bpf/verifier.c=15754=static int sanitize_val_alu(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-15756-{\nkernel/bpf/verifier.c:15757:\tstruct bpf_insn_aux_data *aux = cur_aux(env);\nkernel/bpf/verifier.c-15758-\n--\nkernel/bpf/verifier.c=15770=struct bpf_sanitize_info {\nkernel/bpf/verifier.c:15771:\tstruct bpf_insn_aux_data aux;\nkernel/bpf/verifier.c-15772-\tbool mask_to_left;\n--\nkernel/bpf/verifier.c=15795=static int sanitize_ptr_alu(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-15802-{\nkernel/bpf/verifier.c:15803:\tstruct bpf_insn_aux_data *aux = commit_window ? cur_aux(env) : \u0026info-\u003eaux;\nkernel/bpf/verifier.c-15804-\tstruct bpf_verifier_state *vstate = env-\u003ecur_state;\n--\nkernel/bpf/verifier.c=15891=static void sanitize_mark_insn_seen(struct bpf_verifier_env *env)\n--\nkernel/bpf/verifier.c-15900-\tif (!vstate-\u003especulative)\nkernel/bpf/verifier.c:15901:\t\tenv-\u003einsn_aux_data[env-\u003einsn_idx].seen = env-\u003epass_cnt;\nkernel/bpf/verifier.c-15902-}\n--\nkernel/bpf/verifier.c=17220=static int adjust_reg_min_max_vals(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-17227-\tbool alu32 = (BPF_CLASS(insn-\u003ecode) != BPF_ALU64);\nkernel/bpf/verifier.c:17228:\tstruct bpf_insn_aux_data *aux = cur_aux(env);\nkernel/bpf/verifier.c-17229-\tu8 opcode = BPF_OP(insn-\u003ecode);\n--\nkernel/bpf/verifier.c=18457=static void collect_linked_regs(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-18461-{\nkernel/bpf/verifier.c:18462:\tstruct bpf_insn_aux_data *aux = env-\u003einsn_aux_data;\nkernel/bpf/verifier.c-18463-\tstruct bpf_func_state *func;\n--\nkernel/bpf/verifier.c=18806=static int check_ld_imm(struct bpf_verifier_env *env, struct bpf_insn *insn)\nkernel/bpf/verifier.c-18807-{\nkernel/bpf/verifier.c:18808:\tstruct bpf_insn_aux_data *aux = cur_aux(env);\nkernel/bpf/verifier.c-18809-\tstruct bpf_reg_state *regs = cur_regs(env);\n--\nkernel/bpf/verifier.c=19334=bool bpf_get_call_summary(struct bpf_verifier_env *env, struct bpf_insn *call,\n--\nkernel/bpf/verifier.c-19421- * - look for such patterns;\nkernel/bpf/verifier.c:19422: * - mark spill and fill instructions in env-\u003einsn_aux_data[*].fastcall_pattern;\nkernel/bpf/verifier.c:19423: * - mark set env-\u003einsn_aux_data[*].fastcall_spills_num for call instruction;\nkernel/bpf/verifier.c-19424- * - update env-\u003esubprog_info[*]-\u003efastcall_stack_off to find an offset\n--\nkernel/bpf/verifier.c=19452=static void mark_fastcall_pattern_for_call(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-19513-\t\texpected_regs_mask \u0026= ~BIT(stx-\u003esrc_reg);\nkernel/bpf/verifier.c:19514:\t\tenv-\u003einsn_aux_data[insn_idx - i].fastcall_pattern = 1;\nkernel/bpf/verifier.c:19515:\t\tenv-\u003einsn_aux_data[insn_idx + i].fastcall_pattern = 1;\nkernel/bpf/verifier.c-19516-\t}\n--\nkernel/bpf/verifier.c-19536-\tif (cs.fastcall)\nkernel/bpf/verifier.c:19537:\t\tenv-\u003einsn_aux_data[insn_idx].fastcall_spills_num = i - 1;\nkernel/bpf/verifier.c-19538-\telse\n--\nkernel/bpf/verifier.c=19703=static int save_aux_ptr_type(struct bpf_verifier_env *env, enum bpf_reg_type type,\n--\nkernel/bpf/verifier.c-19705-{\nkernel/bpf/verifier.c:19706:\tenum bpf_reg_type *prev_type = \u0026env-\u003einsn_aux_data[env-\u003einsn_idx].ptr_type;\nkernel/bpf/verifier.c-19707-\n--\nkernel/bpf/verifier.c=20067=static int loop_stack_push(struct bpf_verifier_env *env, bool *pushed)\nkernel/bpf/verifier.c-20068-{\nkernel/bpf/verifier.c:20069:\tstruct bpf_insn_aux_data *aux = env-\u003einsn_aux_data;\nkernel/bpf/verifier.c-20070-\tstruct bpf_func_state *frame = cur_func(env);\n\n"
}

63/3 2026/10/13 08:52 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 49371
							output: 36
							thoughts: 42
						

Thoughts:
Okay, let's dive into this. Here's my thought process on dissecting where `env->insn_aux_data` is allocated within the BPF verifier code:

**Locating the Allocation of `env->insn_aux_data` in the BPF Verifier**

Alright, I'm thinking, "Where *exactly* is `env->insn_aux_data` being brought into existence within the kernel's BPF verifier?". My first impulse, given the structure of the kernel, is to jump straight into `kernel/bpf/verifier.c`. This is where the core logic of the BPF verifier usually resides.

So, let's use a grep search to zero in on the allocation. I'm going to look for the assignment. I'm going to search for instances where `insn_aux_data =` shows up. That should give me the points of allocation. Let's see what we come up with. That should be a good starting point to trace the lifetime of this data structure.



64/3 2026/10/13 08:52 tool grepper
0m Args:
{
  "Expression": "insn_aux_data =",
  "PathPrefixes": [
    "kernel/bpf/verifier.c"
  ]
}

Results:
{
  "Output": "kernel/bpf/verifier.c=23332=int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,\n--\nkernel/bpf/verifier.c-23408-\tlen = env-\u003einsn_aux_data_len = env-\u003eprog-\u003elen;\nkernel/bpf/verifier.c:23409:\tenv-\u003einsn_aux_data =\nkernel/bpf/verifier.c-23410-\t\t__vmalloc(array_size(sizeof(struct bpf_insn_aux_data), len),\n"
}

65/3 2026/10/13 08:52 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 49837
							output: 38
							thoughts: 28
						

Thoughts:
Okay, here's my interpretation of what's going through my mind, rendered in first person, keeping in mind I'm dealing with someone who understands this subject matter deeply:

**Diving Deep into the BPF Verifier**

Alright, time to get my hands dirty. Let's get right to it. My initial thought is to zero in on lines 23405 through 23420 of `kernel/bpf/verifier.c`. No need to beat around the bush; I'm going to jump right into those lines and see what's happening. I have a hunch about a potential issue related to [insert your field's potential problems related to this part of the code]. I need to see the context of this specific section to confirm my initial hypothesis. This should shed some light.



66/3 2026/10/13 08:52 tool read-file
0m Args:
{
  "File": "kernel/bpf/verifier.c",
  "FirstLine": 23405,
  "LineCount": 20
}

Results:
{
  "Contents": "23405:\t\tif (!is_priv)\n23406:\t\t\tmutex_lock(\u0026bpf_verifier_lock);\n23407:\t\n23408:\t\tlen = env-\u003einsn_aux_data_len = env-\u003eprog-\u003elen;\n23409:\t\tenv-\u003einsn_aux_data =\n23410:\t\t\t__vmalloc(array_size(sizeof(struct bpf_insn_aux_data), len),\n23411:\t\t\t\t  GFP_KERNEL_ACCOUNT | __GFP_ZERO);\n23412:\t\tret = -ENOMEM;\n23413:\t\tif (!env-\u003einsn_aux_data)\n23414:\t\t\tgoto skip_full_check;\n23415:\t\tfor (i = 0; i \u003c len; i++)\n23416:\t\t\tenv-\u003einsn_aux_data[i].orig_idx = i;\n23417:\t\tenv-\u003esucc = bpf_iarray_realloc(NULL, 2);\n23418:\t\tif (!env-\u003esucc)\n23419:\t\t\tgoto skip_full_check;\n23420:\t\n23421:\t\tmark_verifier_state_clean(env);\n23422:\t\n23423:\t\tif (IS_ERR(btf_vmlinux)) {\n23424:\t\t\t/* Either gcc or pahole or kernel are broken. */\n"
}

67/3 2026/10/13 08:52 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 50452
							output: 33
							thoughts: 45
						

Thoughts:
Here's my take on the thought process, as if it were my own:

**Memory Considerations in Subprogram Context**

Okay, so I'm looking at this `__vmalloc` call, and it's being used with `GFP_KERNEL_ACCOUNT | __GFP_ZERO`. That's good; it means the memory is zero-initialized right off the bat. That's a solid start. Makes things a lot cleaner to begin with. But, now the question is: what about the `env->subprog_info` structure? I need to be sure the relevant pointer is pointing to valid memory, but where is this thing actually allocated or, critically, initialized? I need to track down where `subprog_info` first gets its memory. Is it dynamically allocated somewhere else, maybe on the stack? Is it a statically defined structure? Or is it allocated on the heap too? I need to figure out the lifecycle of this structure. I need to be sure that it is safe to dereference.



68/3 2026/10/13 08:53 tool grepper
0m Args:
{
  "Expression": "subprog_info",
  "PathPrefixes": [
    "kernel/bpf/verifier.c"
  ]
}

Results:
{
  "Output": "\nFull output is too long, showing 200 out of 282 lines.\nUse more precise expression or PathPrefixes if possible.\n\nNumber of matching lines per file (1 files in total):\nkernel/bpf/verifier.c:62\n\nkernel/bpf/verifier.c=439=static int bpf_compute_subprog_ret_regs(struct bpf_verifier_env *env)\n--\nkernel/bpf/verifier.c-462-\t\tif (ret_regs_cnt(size) \u003e 1) {\nkernel/bpf/verifier.c:463:\t\t\tsubprog_info(env, subprog)-\u003eret_reg_pair = true;\nkernel/bpf/verifier.c-464-\t\t\tenv-\u003eprog-\u003ejit_required = 1;\n--\nkernel/bpf/verifier.c=482=void bpf_mark_subprog_exc_cb(struct bpf_verifier_env *env, int subprog)\nkernel/bpf/verifier.c-483-{\nkernel/bpf/verifier.c:484:\tstruct bpf_subprog_info *info = subprog_info(env, subprog);\nkernel/bpf/verifier.c-485-\n--\nkernel/bpf/verifier.c=491=static bool subprog_is_exc_cb(struct bpf_verifier_env *env, int subprog)\nkernel/bpf/verifier.c-492-{\nkernel/bpf/verifier.c:493:\treturn subprog_info(env, subprog)-\u003eis_exception_cb;\nkernel/bpf/verifier.c-494-}\n--\nkernel/bpf/verifier.c=1447=static int resize_reference_state(struct bpf_verifier_state *state, size_t n)\n--\nkernel/bpf/verifier.c-1458-/* Possibly update state-\u003eallocated_stack to be at least size bytes. Also\nkernel/bpf/verifier.c:1459: * possibly update the function's high-water mark in its bpf_subprog_info.\nkernel/bpf/verifier.c-1460- */\nkernel/bpf/verifier.c=1461=int bpf_grow_stack_state(struct bpf_verifier_env *env, struct bpf_func_state *state, int size)\n--\nkernel/bpf/verifier.c-1478-\t/* update known max for given subprogram */\nkernel/bpf/verifier.c:1479:\tif (env-\u003esubprog_info[state-\u003esubprogno].stack_depth \u003c size)\nkernel/bpf/verifier.c:1480:\t\tenv-\u003esubprog_info[state-\u003esubprogno].stack_depth = size;\nkernel/bpf/verifier.c-1481-\n--\nkernel/bpf/verifier.c=2588=static int cmp_subprogs(const void *a, const void *b)\nkernel/bpf/verifier.c-2589-{\nkernel/bpf/verifier.c:2590:\treturn ((struct bpf_subprog_info *)a)-\u003estart -\nkernel/bpf/verifier.c:2591:\t       ((struct bpf_subprog_info *)b)-\u003estart;\nkernel/bpf/verifier.c-2592-}\n--\nkernel/bpf/verifier.c-2594-/* Find subprogram that contains instruction at 'off' */\nkernel/bpf/verifier.c:2595:struct bpf_subprog_info *bpf_find_containing_subprog(struct bpf_verifier_env *env, int off)\nkernel/bpf/verifier.c-2596-{\nkernel/bpf/verifier.c:2597:\tstruct bpf_subprog_info *vals = env-\u003esubprog_info;\nkernel/bpf/verifier.c-2598-\tint l, r, m;\n--\nkernel/bpf/verifier.c=2616=int bpf_find_subprog(struct bpf_verifier_env *env, int off)\nkernel/bpf/verifier.c-2617-{\nkernel/bpf/verifier.c:2618:\tstruct bpf_subprog_info *p;\nkernel/bpf/verifier.c-2619-\n--\nkernel/bpf/verifier.c-2622-\t\treturn -ENOENT;\nkernel/bpf/verifier.c:2623:\treturn p - env-\u003esubprog_info;\nkernel/bpf/verifier.c-2624-}\n--\nkernel/bpf/verifier.c=2626=static int add_subprog(struct bpf_verifier_env *env, int off)\n--\nkernel/bpf/verifier.c-2642-\t/* determine subprog starts. The end is one before the next starts */\nkernel/bpf/verifier.c:2643:\tenv-\u003esubprog_info[env-\u003esubprog_cnt].exit_idx = U32_MAX;\nkernel/bpf/verifier.c:2644:\tenv-\u003esubprog_info[env-\u003esubprog_cnt++].start = off;\nkernel/bpf/verifier.c:2645:\tsort(env-\u003esubprog_info, env-\u003esubprog_cnt,\nkernel/bpf/verifier.c:2646:\t     sizeof(env-\u003esubprog_info[0]), cmp_subprogs, NULL);\nkernel/bpf/verifier.c-2647-\treturn env-\u003esubprog_cnt - 1;\n--\nkernel/bpf/verifier.c=3178=static int add_subprogs(struct bpf_verifier_env *env)\nkernel/bpf/verifier.c-3179-{\nkernel/bpf/verifier.c:3180:\tstruct bpf_subprog_info *subprog = env-\u003esubprog_info;\nkernel/bpf/verifier.c-3181-\tint i, ret, insn_cnt = env-\u003eprog-\u003elen, ex_cb_insn;\n--\nkernel/bpf/verifier.c-3243-\t\tfor (i = 1; i \u003c env-\u003esubprog_cnt; i++) {\nkernel/bpf/verifier.c:3244:\t\t\tif (env-\u003esubprog_info[i].start != ex_cb_insn)\nkernel/bpf/verifier.c-3245-\t\t\t\tcontinue;\n--\nkernel/bpf/verifier.c=3290=static void find_subprog_properties(struct bpf_verifier_env *env)\nkernel/bpf/verifier.c-3291-{\nkernel/bpf/verifier.c:3292:\tstruct bpf_subprog_info *subprog = env-\u003esubprog_info;\nkernel/bpf/verifier.c-3293-\tstruct bpf_insn *insn = env-\u003eprog-\u003einsnsi;\n--\nkernel/bpf/verifier.c=3318=static int check_subprogs(struct bpf_verifier_env *env)\n--\nkernel/bpf/verifier.c-3320-\tint i, subprog_start, subprog_end, off, cur_subprog = 0;\nkernel/bpf/verifier.c:3321:\tstruct bpf_subprog_info *subprog = env-\u003esubprog_info;\nkernel/bpf/verifier.c-3322-\tstruct bpf_insn *insn = env-\u003eprog-\u003einsnsi;\n--\nkernel/bpf/verifier.c=3425=static int sort_subprogs_topo(struct bpf_verifier_env *env)\nkernel/bpf/verifier.c-3426-{\nkernel/bpf/verifier.c:3427:\tstruct bpf_subprog_info *si = env-\u003esubprog_info;\nkernel/bpf/verifier.c-3428-\tint *insn_postorder = env-\u003ecfg.insn_postorder;\n--\nkernel/bpf/verifier.c=3814=static void check_fastcall_stack_contract(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-3816-{\nkernel/bpf/verifier.c:3817:\tstruct bpf_subprog_info *subprog = \u0026env-\u003esubprog_info[state-\u003esubprogno];\nkernel/bpf/verifier.c-3818-\tstruct bpf_insn_aux_data *aux = env-\u003einsn_aux_data;\n--\nkernel/bpf/verifier.c=4533=static int check_stack_arg_write(struct bpf_verifier_env *env, struct bpf_func_state *state,\n--\nkernel/bpf/verifier.c-4536-\tint max_stack_arg_regs = MAX_BPF_FUNC_ARGS - MAX_BPF_FUNC_REG_ARGS;\nkernel/bpf/verifier.c:4537:\tstruct bpf_subprog_info *subprog = \u0026env-\u003esubprog_info[state-\u003esubprogno];\nkernel/bpf/verifier.c-4538-\tint spi = -off / BPF_REG_SIZE - 1;\n--\nkernel/bpf/verifier.c=4576=static int check_stack_arg_read(struct bpf_verifier_env *env, struct bpf_func_state *state,\n--\nkernel/bpf/verifier.c-4578-{\nkernel/bpf/verifier.c:4579:\tstruct bpf_subprog_info *subprog = \u0026env-\u003esubprog_info[state-\u003esubprogno];\nkernel/bpf/verifier.c-4580-\tstruct bpf_verifier_state *vstate = env-\u003ecur_state;\n--\nkernel/bpf/verifier.c=5700=static int check_max_stack_depth_subprog(struct bpf_verifier_env *env, int idx,\n--\nkernel/bpf/verifier.c-5703-{\nkernel/bpf/verifier.c:5704:\tstruct bpf_subprog_info *subprog = env-\u003esubprog_info;\nkernel/bpf/verifier.c-5705-\tstruct bpf_insn *insn = env-\u003eprog-\u003einsnsi;\n--\nkernel/bpf/verifier.c=5950=static int check_max_stack_depth(struct bpf_verifier_env *env)\n--\nkernel/bpf/verifier.c-5953-\tstruct bpf_subprog_call_depth_info *dinfo;\nkernel/bpf/verifier.c:5954:\tstruct bpf_subprog_info *si = env-\u003esubprog_info;\nkernel/bpf/verifier.c-5955-\tbool priv_stack_supported;\n--\nkernel/bpf/verifier.c=6385=void bpf_adjust_func_ptrs_after_remove(struct bpf_verifier_env *env, u32 off, u32 len)\n--\nkernel/bpf/verifier.c-6399-\t\tsubprog = bpf_find_subprog(env, p-\u003exlated_off);\nkernel/bpf/verifier.c:6400:\t\tif (subprog \u003e 0 \u0026\u0026 env-\u003esubprog_info[subprog + 1].start \u003e off + len)\nkernel/bpf/verifier.c-6401-\t\t\tp-\u003exlated_off = off;\n--\nkernel/bpf/verifier.c=11086=static int setup_func_entry(struct bpf_verifier_env *env, int subprog, int callsite,\n--\nkernel/bpf/verifier.c-11133-\nkernel/bpf/verifier.c:11134:static void gen_subprog_arg_proto(const struct bpf_subprog_info *sub, const struct btf *btf,\nkernel/bpf/verifier.c-11135-\t\t\t\t  const struct btf_type *func_proto, struct bpf_func_proto *proto)\n--\nkernel/bpf/verifier.c=11166=static int btf_check_func_arg_match(struct bpf_verifier_env *env, int subprog, struct btf *btf)\nkernel/bpf/verifier.c-11167-{\nkernel/bpf/verifier.c:11168:\tstruct bpf_subprog_info *sub = subprog_info(env, subprog);\nkernel/bpf/verifier.c-11169-\tstruct bpf_func_state *caller = cur_func(env);\n--\nkernel/bpf/verifier.c=11248=static int push_callback_call(struct bpf_verifier_env *env, struct bpf_insn *insn,\n--\nkernel/bpf/verifier.c-11264-\t */\nkernel/bpf/verifier.c:11265:\tenv-\u003esubprog_info[subprog].is_cb = true;\nkernel/bpf/verifier.c-11266-\tif (bpf_pseudo_kfunc_call(insn) \u0026\u0026\n--\nkernel/bpf/verifier.c-11281-\t\t/* there is no real recursion here. timer and workqueue callbacks are async */\nkernel/bpf/verifier.c:11282:\t\tenv-\u003esubprog_info[subprog].is_async_cb = true;\nkernel/bpf/verifier.c:11283:\t\tasync_cb = push_async_cb(env, env-\u003esubprog_info[subprog].start,\nkernel/bpf/verifier.c-11284-\t\t\t\t\t insn_idx, subprog,\n--\nkernel/bpf/verifier.c-11310-\t */\nkernel/bpf/verifier.c:11311:\tcallback_state = push_stack(env, env-\u003esubprog_info[subprog].start, insn_idx, false);\nkernel/bpf/verifier.c-11312-\tif (IS_ERR(callback_state))\n--\nkernel/bpf/verifier.c=11334=static int check_static_func_call(struct bpf_verifier_env *env, int subprog,\n--\nkernel/bpf/verifier.c-11337-\tstruct bpf_verifier_state *state = env-\u003ecur_state;\nkernel/bpf/verifier.c:11338:\tstruct bpf_subprog_info *caller_info;\nkernel/bpf/verifier.c-11339-\tu16 callee_incoming, stack_arg_cnt;\n--\nkernel/bpf/verifier.c-11348-\t */\nkernel/bpf/verifier.c:11349:\tcaller_info = \u0026env-\u003esubprog_info[caller-\u003esubprogno];\nkernel/bpf/verifier.c:11350:\tcallee_incoming = bpf_in_stack_arg_cnt(\u0026env-\u003esubprog_info[subprog]);\nkernel/bpf/verifier.c-11351-\tstack_arg_cnt = bpf_in_stack_arg_cnt(caller_info) + callee_incoming;\n--\nkernel/bpf/verifier.c-11366-\t/* and go analyze first insn of the callee */\nkernel/bpf/verifier.c:11367:\t*insn_idx = env-\u003esubprog_info[subprog].start - 1;\nkernel/bpf/verifier.c-11368-\n--\nkernel/bpf/verifier.c=11379=static int check_func_call(struct bpf_verifier_env *env, struct bpf_insn *insn,\n--\nkernel/bpf/verifier.c-11411-\nkernel/bpf/verifier.c:11412:\t\tif (env-\u003esubprog_info[subprog].might_sleep \u0026\u0026 !in_sleepable_context(env)) {\nkernel/bpf/verifier.c-11413-\t\t\tconst char *suggestion;\n--\nkernel/bpf/verifier.c-11436-\t\treturns_void = subprog_returns_void(env, subprog);\nkernel/bpf/verifier.c:11437:\t\tif (env-\u003esubprog_info[subprog].changes_pkt_data)\nkernel/bpf/verifier.c-11438-\t\t\tclear_all_pkt_pointers(env);\n--\nkernel/bpf/verifier.c-11461-\nkernel/bpf/verifier.c:11462:\t\tif (env-\u003esubprog_info[subprog].might_throw) {\nkernel/bpf/verifier.c-11463-\t\t\tstruct bpf_verifier_state *branch;\n--\nkernel/bpf/verifier.c=11824=static void account_processed_insn(struct bpf_verifier_env *env)\n--\nkernel/bpf/verifier.c-11829-\tframe-\u003einsns_subtotal++;\nkernel/bpf/verifier.c:11830:\tenv-\u003esubprog_info[frame-\u003esubprogno].insns_self++;\nkernel/bpf/verifier.c-11831-}\n--\nkernel/bpf/verifier.c=11833=static void account_processed_insns(struct bpf_verifier_env *env,\n--\nkernel/bpf/verifier.c-11843-\nkernel/bpf/verifier.c:11844:\tenv-\u003esubprog_info[callee-\u003esubprogno].insns_total += insns;\nkernel/bpf/verifier.c-11845-\tif (caller)\n--\nkernel/bpf/verifier.c=14912=static void prepare_subprog_args(struct bpf_verifier_env *env)\n--\nkernel/bpf/verifier.c-14918-\t\t\t/* Keep all argument registers live if the prototype is unavailable. */\nkernel/bpf/verifier.c:14919:\t\t\tenv-\u003esubprog_info[i].arg_slot_cnt = MAX_BPF_FUNC_REG_ARGS;\nkernel/bpf/verifier.c-14920-\t}\n--\nkernel/bpf/verifier.c=14928=bpf_global_subprog_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn *insn,\n--\nkernel/bpf/verifier.c-14935-\t};\nkernel/bpf/verifier.c:14936:\tstruct bpf_subprog_info *sub;\nkernel/bpf/verifier.c-14937-\tint callee;\n--\nkernel/bpf/verifier.c-14941-\t\treturn info;\nkernel/bpf/verifier.c:14942:\tsub = subprog_info(env, callee);\n\n"
}

69/3 2026/10/13 08:53 llm kmsan-evaluator
1m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 55420
							output: 32
							thoughts: 68
						

Thoughts:
Here's my understanding, as if I were thinking it through:

**Deconstructing the BPF Verifier Environment**

Okay, so I'm looking at the `bpf_verifier_env` structure. My understanding is that this `env` instance holds some crucial information during the BPF verification process. Specifically, I see it contains an array, `subprog_info`, declared as `struct bpf_subprog_info subprog_info[BPF_MAX_SUBPROGS + 1]`. That suggests this array is meant to track information about subprograms within a larger BPF program, likely allowing for the tracking of context for function calls. And because the size is `BPF_MAX_SUBPROGS + 1`, I'd bet that the extra element might be a reserved spot or an accumulator for global program information, or even a 'null' entry. Now, the allocation itself happens using `kzalloc` within the `bpf_check` function. This is significant because `kzalloc` is memory safe, clearing the memory before handing it off, meaning any uninitialized data should be zeroed and making debugging a bit more straightforward. The next step is always the verification.



70/3 2026/10/13 08:54 tool grepper
0m Error:
no matches

Args:
{
  "Expression": "kzalloc.*verifier_env",
  "PathPrefixes": [
    "kernel/bpf/"
  ]
}

Results:
{
  "error": "no matches"
}

71/3 2026/10/13 08:54 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 55787
							output: 34
							thoughts: 25
						

Thoughts:
Here's my understanding of the thought process, summarized as you requested:

**Deconstructing the `bpf_check` Function's Environment Allocation**

Okay, so I need to understand how the `struct bpf_verifier_env *env` variable is being handled within the `bpf_check` function. The key is in how it's allocated. I need to trace its initialization and usage. I'm assuming that proper memory management and initialization of `env` is crucial to avoid bugs, especially around pointer dereferencing or double-free scenarios, as the verifier operates on kernel-level data. Is it on the stack, dynamically allocated, passed in as a parameter? The answers will dictate how the verifier functions. My analysis will focus on where and how this `env` structure is instantiated and if it is initialized, or if there is any potential for a use-before-init vulnerability. Let's dig in.



72/3 2026/10/13 08:54 tool grepper
0m Args:
{
  "Expression": "bpf_check\\(",
  "PathPrefixes": [
    "kernel/bpf/verifier.c"
  ]
}

Results:
{
  "Output": "kernel/bpf/verifier.c=61=static bool bpf_global_percpu_ma_set;\nkernel/bpf/verifier.c-62-\nkernel/bpf/verifier.c:63:/* bpf_check() is a static code analyzer that walks eBPF program\nkernel/bpf/verifier.c-64- * instruction by instruction and updates register/stack state.\n--\nkernel/bpf/verifier.c=23124=static enum bpf_sig_keyring bpf_classify_keyring(s32 keyring_id)\n--\nkernel/bpf/verifier.c-23140-/*\nkernel/bpf/verifier.c:23141: * Verify the PKCS#7 signature of a loaded program. Called from bpf_check()\nkernel/bpf/verifier.c-23142- * once the program's metadata maps have been resolved into used_maps, so\n--\nkernel/bpf/verifier.c=23294=static void log_program(struct bpf_verifier_env *env)\n--\nkernel/bpf/verifier.c-23331-\nkernel/bpf/verifier.c:23332:int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,\nkernel/bpf/verifier.c-23333-\t      struct bpf_log_attr *attr_log)\n--\nkernel/bpf/verifier.c-23346-\t/* 'struct bpf_verifier_env' can be global, but since it's not small,\nkernel/bpf/verifier.c:23347:\t * allocate/free it every time bpf_check() is called\nkernel/bpf/verifier.c-23348-\t */\n"
}

73/3 2026/10/13 08:54 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 56497
							output: 38
							thoughts: 28
						

Thoughts:
Okay, let's dive into this. Here's my take on those lines from `kernel/bpf/verifier.c`:

**Analyzing BPF Verifier Logic: Lines 23345-23360**

Right, so I need to get a handle on what's happening within those specific lines of the BPF verifier. Let's pull up lines 23345 through 23360. Based on my existing knowledge, given the context of the verifier, I'm expecting to see logic related to either: memory safety checks, register state management, potential instruction validity, or likely a combination of those. Given that the BPF verifier is so critical for security, I'm anticipating some complex branching, potentially involving data structures to track register content or memory regions. I'll be looking closely at the conditions; any potential vulnerabilities are a top concern. I’ll be on the lookout for any obvious control flow exploits as I can probably pick up an idea of the surrounding code that the lines are interacting with. I'm keen to understand how this snippet fits into the broader picture of the verifier's operation. Knowing the surrounding code, and perhaps the function signature, could provide invaluable insights. Alright, let's see what's actually there…



74/3 2026/10/13 08:55 tool read-file
0m Args:
{
  "File": "kernel/bpf/verifier.c",
  "FirstLine": 23345,
  "LineCount": 20
}

Results:
{
  "Contents": "23345:\t\n23346:\t\t/* 'struct bpf_verifier_env' can be global, but since it's not small,\n23347:\t\t * allocate/free it every time bpf_check() is called\n23348:\t\t */\n23349:\t\tenv = kvzalloc_obj(struct bpf_verifier_env, GFP_KERNEL_ACCOUNT);\n23350:\t\tif (!env)\n23351:\t\t\treturn -ENOMEM;\n23352:\t\n23353:\t\tenv-\u003ebt.env = env;\n23354:\t\tenv-\u003eprog = *prog;\n23355:\t\tenv-\u003eops = bpf_verifier_ops[env-\u003eprog-\u003etype];\n23356:\t\tenv-\u003estack_limit = bpf_prog_stack_limit(env-\u003eprog);\n23357:\t\tif (bpf_prog_reenters_datapath(env-\u003eprog))\n23358:\t\t\tenv-\u003estack_limit = MAX_BPF_STACK;\n23359:\t\n23360:\t\tenv-\u003eallow_ptr_leaks = bpf_allow_ptr_leaks(env-\u003eprog-\u003eaux-\u003etoken);\n23361:\t\tenv-\u003eallow_uninit_stack = bpf_allow_uninit_stack(env-\u003eprog-\u003eaux-\u003etoken);\n23362:\t\tenv-\u003ebypass_spec_v1 = bpf_bypass_spec_v1(env-\u003eprog-\u003eaux-\u003etoken);\n23363:\t\tenv-\u003ebypass_spec_v4 = bpf_bypass_spec_v4(env-\u003eprog-\u003eaux-\u003etoken);\n23364:\t\tenv-\u003ebpf_capable = is_priv = bpf_token_capable(env-\u003eprog-\u003eaux-\u003etoken, CAP_BPF);\n"
}

75/3 2026/10/13 08:55 llm kmsan-evaluator
3m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 57300
							output: 38
							thoughts: 388
						

Thoughts:
**My Analysis of BPF Context Access Conversion**

Okay, so I'm starting by allocating a `bpf_verifier_env` structure using `kvzalloc_obj`. The important thing here is that it's zero-initialized as well, which is good. Now, let's dive into what happens when instruction patching is going on, specifically within `bpf_convert_ctx_accesses`. I need to understand how context accesses are handled and transformed.

I'm looking at `kernel/bpf/fixups.c`, and I see the `arena_member_fixup` function. This seems key to understanding how we're dealing with arena members. The code checks `aux->arena_member`. If it's not an arena member, it just returns. However, if it *is*, then the code checks for `aux->non_arena_load` to prevent mixed member access. I see a `verbose` message that tells me that an instruction is loading from an arena pointer member on one path, and loading something else on another, which should be avoided.

If it *is* an arena member, the code is trying to get the kernel virtual memory start, which sounds like it's calculating an offset. Then the original instruction `*insn` is copied to the buffer. The function then checks if the arena member is nullable with `aux->arena_member_nullable`. If it is, then a jump instruction is added to skip the next instruction if the destination register (`insn->dst_reg`) is zero. The next instruction computes an offset of the current pointer from `base`, which is the start address of the arena. Finally, the function returns a count.

But wait a minute, I need to understand the role of `buf`. The question now is: where does `insn_buf` come from? I need to trace back to where this `buf` variable is defined and how it's being used. I need to understand the source of the buffer used in this function. I better go back and re-examine `bpf_convert_ctx_accesses` and see where this is coming from.



76/3 2026/10/13 08:58 tool read-file
0m Args:
{
  "File": "kernel/bpf/fixups.c",
  "FirstLine": 1085,
  "LineCount": 55
}

Results:
{
  "Contents": "1085:\t\t\tcase PTR_TO_SOCKET:\n1086:\t\t\tcase PTR_TO_SOCK_COMMON:\n1087:\t\t\t\tconvert_ctx_access = bpf_sock_convert_ctx_access;\n1088:\t\t\t\tbreak;\n1089:\t\t\tcase PTR_TO_TCP_SOCK:\n1090:\t\t\t\tconvert_ctx_access = bpf_tcp_sock_convert_ctx_access;\n1091:\t\t\t\tbreak;\n1092:\t\t\tcase PTR_TO_XDP_SOCK:\n1093:\t\t\t\tconvert_ctx_access = bpf_xdp_sock_convert_ctx_access;\n1094:\t\t\t\tbreak;\n1095:\t\t\tcase PTR_TO_ARENA:\n1096:\t\t\t\tif (BPF_MODE(insn-\u003ecode) == BPF_MEMSX) {\n1097:\t\t\t\t\tif (!bpf_jit_supports_insn(insn, true)) {\n1098:\t\t\t\t\t\tverbose(env, \"sign extending loads from arena are not supported yet\\n\");\n1099:\t\t\t\t\t\treturn -EOPNOTSUPP;\n1100:\t\t\t\t\t}\n1101:\t\t\t\t\tinsn-\u003ecode = BPF_CLASS(insn-\u003ecode) | BPF_PROBE_MEM32SX | BPF_SIZE(insn-\u003ecode);\n1102:\t\t\t\t} else {\n1103:\t\t\t\t\tinsn-\u003ecode = BPF_CLASS(insn-\u003ecode) | BPF_PROBE_MEM32 | BPF_SIZE(insn-\u003ecode);\n1104:\t\t\t\t}\n1105:\t\t\t\tenv-\u003eprog-\u003eaux-\u003enum_exentries++;\n1106:\t\t\t\tcontinue;\n1107:\t\t\tdefault:\n1108:\t\t\t\t/*\n1109:\t\t\t\t * A pointer which may fault on a dereference must not\n1110:\t\t\t\t * be loaded from without fault protection, hence turn\n1111:\t\t\t\t * the BPF_LDX into a BPF_PROBE_MEM one so that a bad\n1112:\t\t\t\t * address is handled rather than panicking the kernel.\n1113:\t\t\t\t * A store through one is rejected earlier, there is no\n1114:\t\t\t\t * probed counterpart to rewrite it into.\n1115:\t\t\t\t */\n1116:\t\t\t\tif (bpf_is_ptr_to_mem_or_btf_id(ptr_type) \u0026\u0026\n1117:\t\t\t\t    bpf_may_fault_on_deref(ptr_type) \u0026\u0026\n1118:\t\t\t\t    type == BPF_READ) {\n1119:\t\t\t\t\tif (BPF_MODE(insn-\u003ecode) == BPF_MEM)\n1120:\t\t\t\t\t\tinsn-\u003ecode = BPF_LDX | BPF_PROBE_MEM |\n1121:\t\t\t\t\t\t\t     BPF_SIZE(insn-\u003ecode);\n1122:\t\t\t\t\telse\n1123:\t\t\t\t\t\tinsn-\u003ecode = BPF_LDX | BPF_PROBE_MEMSX |\n1124:\t\t\t\t\t\t\t     BPF_SIZE(insn-\u003ecode);\n1125:\t\t\t\t\tenv-\u003eprog-\u003eaux-\u003enum_exentries++;\n1126:\t\t\t\t} else if (verifier_bug_if(bpf_may_fault_on_deref(ptr_type), env,\n1127:\t\t\t\t\t\t\t   \"access to a fault prone pointer is not rewritten as a probed one\")) {\n1128:\t\t\t\t\treturn -EFAULT;\n1129:\t\t\t\t}\n1130:\t\n1131:\t\t\t\tcnt = arena_member_fixup(env, i + delta, insn, insn_buf);\n1132:\t\t\t\tif (cnt \u003c 0)\n1133:\t\t\t\t\treturn cnt;\n1134:\t\t\t\tif (!cnt)\n1135:\t\t\t\t\tcontinue;\n1136:\t\t\t\tnew_prog = bpf_patch_insn_data(env, i + delta, insn_buf, cnt);\n1137:\t\t\t\tif (!new_prog)\n1138:\t\t\t\t\treturn -ENOMEM;\n1139:\t\n"
}

77/3 2026/10/13 08:58 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 59186
							output: 38
							thoughts: 296
						

Thoughts:
**Analyzing BPF Instruction Patching**

Okay, so I'm looking at this `bpf_convert_ctx_accesses` function and trying to understand how the instruction patching works. I see `insn_buf` declared as a fixed-size array of `bpf_insn` structs. Good, that's straightforward. Then, `arena_member_fixup` writes to this buffer, and it looks like it can write up to three instructions (`cnt`). Crucially, `bpf_patch_insn_data` only copies *`cnt`* instructions from `insn_buf`, which makes sense to avoid any potential buffer overflows.

The `BPF_JMP_IMM` and `BPF_ALU32_IMM` macros are also key here – they fully initialize *all* the fields of the `bpf_insn` struct. This is important: `code`, `dst_reg`, `src_reg`, `off`, and `imm` are all set, ensuring the instructions are well-defined. I understand that the macros handle the bitwise operations involved in constructing the instruction's code.

But now I'm starting to think about something related. Wait, what about BTF? I need to check how the BTF-related functions, specifically `btf_struct_walk` and `btf_struct_access` in `kernel/bpf/btf.c`, relate to this instruction manipulation. I'll need to double check how the BTF type information is used for offset calculations and how these offset calculations get baked into the instructions. Let's dig into lines 7961-8000 and 8060-8115 of `kernel/bpf/btf.c` right now, that's where I need to focus my attention.



78/3 2026/10/13 08:58 tool read-file
0m Args:
{
  "File": "kernel/bpf/btf.c",
  "FirstLine": 7965,
  "LineCount": 45
}

Results:
{
  "Contents": "7965:\t\t\t\t\treturn err;\n7966:\t\t\t\t}\n7967:\t\t\t\ttmp_flag = ctx.res;\n7968:\t\t\t\tid = ctx.id;\n7969:\t\t\t\tstype = ctx.t;\n7970:\t\n7971:\t\t\t\t/*\n7972:\t\t\t\t * __nullable combines with any of the above: it only says\n7973:\t\t\t\t * that the pointer may be NULL. The caller decides whether\n7974:\t\t\t\t * that matters for the register type it derives.\n7975:\t\t\t\t */\n7976:\t\t\t\tif (btf_ptr_has_type_tag(btf, mtype, \"nullable\"))\n7977:\t\t\t\t\ttmp_flag |= PTR_MAYBE_NULL;\n7978:\t\n7979:\t\t\t\t/*\n7980:\t\t\t\t * An __arena member of a kernel struct holds the kernel\n7981:\t\t\t\t * address of arena memory, which the verifier turns into\n7982:\t\t\t\t * an arena pointer on load. Program BTF is not affected:\n7983:\t\t\t\t * there the tag is what the compiler emits for the arena\n7984:\t\t\t\t * address space. An offset into a union resolves to its\n7985:\t\t\t\t * first member, so a program reading another member there\n7986:\t\t\t\t * would get an arena pointer it did not ask for; the walk\n7987:\t\t\t\t * marked such a union untrusted.\n7988:\t\t\t\t */\n7989:\t\t\t\tif (btf_is_kernel(btf) \u0026\u0026 btf_type_is_arena_ptr(btf, mtype)) {\n7990:\t\t\t\t\tif (*flag \u0026 PTR_UNTRUSTED) {\n7991:\t\t\t\t\t\tbpf_log(log,\n7992:\t\t\t\t\t\t\t\"arena pointer member %s of %s is inside a union\\n\",\n7993:\t\t\t\t\t\t\tmname, tname);\n7994:\t\t\t\t\t\treturn -EACCES;\n7995:\t\t\t\t\t}\n7996:\t\t\t\t\t*next_btf_id = id;\n7997:\t\t\t\t\t*flag |= tmp_flag \u0026 PTR_MAYBE_NULL;\n7998:\t\t\t\t\tif (field_name)\n7999:\t\t\t\t\t\t*field_name = mname;\n8000:\t\t\t\t\treturn WALK_PTR_ARENA;\n8001:\t\t\t\t}\n8002:\t\n8003:\t\t\t\tif (btf_type_is_struct(stype)) {\n8004:\t\t\t\t\t*next_btf_id = id;\n8005:\t\t\t\t\t*flag |= tmp_flag;\n8006:\t\t\t\t\tif (field_name)\n8007:\t\t\t\t\t\t*field_name = mname;\n8008:\t\t\t\t\treturn WALK_PTR;\n8009:\t\t\t\t}\n"
}

79/3 2026/10/13 08:58 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 60611
							output: 38
							thoughts: 28
						

Thoughts:
Here's my summary, as if I were thinking these thoughts:

**Examining BTF Handling in the Kernel**

Okay, so we're diving back into this BTF stuff. I need to make sure I'm crystal clear on how the kernel handles it, especially now that we're dealing with potentially complex BPF programs. I'm going to revisit the section on BTF within the kernel code. Specifically, let's take a look at lines 8095-8120 in `kernel/bpf/btf.c`. This section should reveal how the BTF type information is actually processed and potentially validate the issue that had come to our attention. I need to understand this implementation to see how the system is currently designed.



80/3 2026/10/13 08:58 tool read-file
0m Args:
{
  "File": "kernel/bpf/btf.c",
  "FirstLine": 8095,
  "LineCount": 25
}

Results:
{
  "Contents": "8095:\t\t\t\t * we're done.\n8096:\t\t\t\t */\n8097:\t\t\t\t*next_btf_id = id;\n8098:\t\t\t\t*flag = tmp_flag;\n8099:\t\t\t\tret = PTR_TO_BTF_ID;\n8100:\t\t\t\tgoto check_variable_offset;\n8101:\t\t\tcase WALK_PTR_UNTRUSTED:\n8102:\t\t\t\t*flag = MEM_RDONLY | PTR_UNTRUSTED;\n8103:\t\t\t\tret = PTR_TO_MEM;\n8104:\t\t\t\tgoto check_variable_offset;\n8105:\t\t\tcase WALK_PTR_ARENA:\n8106:\t\t\t\t*next_btf_id = id;\n8107:\t\t\t\t*flag = tmp_flag;\n8108:\t\t\t\tret = PTR_TO_ARENA;\n8109:\t\t\t\tgoto check_variable_offset;\n8110:\t\t\tcase WALK_SCALAR:\n8111:\t\t\t\tret = SCALAR_VALUE;\n8112:\t\t\t\tgoto check_variable_offset;\n8113:\t\t\tcase WALK_STRUCT:\n8114:\t\t\t\t/* We found nested struct, so continue the search\n8115:\t\t\t\t * by diving in it. At this point the offset is\n8116:\t\t\t\t * aligned with the new type, so set it to 0.\n8117:\t\t\t\t */\n8118:\t\t\t\tt = btf_type_by_id(btf, id);\n8119:\t\t\t\toff = 0;\n"
}

81/3 2026/10/13 08:58 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 61229
							output: 421
							thoughts: 1809
						

Thoughts:
**KMSAN Assessment: No Need for KMSAN in this Patch**

Okay, so I've dug into this patch regarding BPF arena pointers, BTF type tags, and JIT optimizations. My analysis indicates this patch doesn't warrant a KMSAN session. Here's the breakdown:

First, I examined `check_ptr_to_btf_access` and its related functions in the verifier. It appears that the patch introduces checks and data structures related to handling `__arena` and `__nullable` pointers.  The main goal seems to be to ensure correct address translation between kernel pointers and arena offsets within BPF programs.  Specifically, `check_arena_member_load` is central to this. I looked at `mark_reg_unknown`, which essentially zeroes the register state before setting the type.

I then went through the KMSAN criteria, specifically looking for uninitialized memory risks. I confirmed that no kernel structures are exposed to userspace via any of the usual suspects (`copy_to_user`, `put_user`, netlink, ioctl, etc.). Any new structures or fields introduced, such as `uses_arena_insns` in `bpf_prog_aux` and `bpf_subprog_info`, and `insn_aux_data`, are meticulously zero-initialized during allocation via `kzalloc`, `kvzalloc` or `__GFP_ZERO`. The local variables within the JIT compilers are similarly initialized.

I looked at the initialization of `arena_vm_start` and `user_vm_start` within `do_jit()` and confirmed they are initialized. I also verified that the JIT context structs in other architectures are `memset` before use.

I also considered the potential for uninitialized memory in the `arena_member_fixup` and other related instruction patching, but all generated `bpf_insn` structures are completely initialized. I then checked the memory initialization of structures used by `bpf_object__bind_arena`, `jit_subprogs`, and `bpf_prog_uses_arena_insns`. All these structs are zeroed at allocation.

My understanding is that this patch focuses on making sure the BPF verifier and JIT correctly handles and optimizes arena pointer instructions, which are memory mapped between userspace and kernel. The risks here are more related to incorrect behavior or memory safety. The introduced logic mainly affects the BPF verifier's ability to track and validate arena pointer types, and the JIT compiler's ability to emit the right code, and the correctness of the BPF bytecode execution. Any bugs here would manifest as incorrect byte code emission, verifier rejections, or out-of-bounds accesses. These issues would be picked up by KASAN/UBSan, not KMSAN.

Therefore, since all structures are initialized, no kernel-to-user memory copy is happening, and the patch deals primarily with correct logic and code generation, I'm confident that the answer is `NeedsKMSAN=false`.



82/3 2026/10/13 08:59 tool set-results
0m Args:
{
  "KMSANReasoning": "The patch introduces support for `__arena` and `__nullable` BTF type tags on kernel struct members accessed by BPF programs and optimizes JIT compilers across architectures to only set up the arena base registers when `bpf_prog_uses_arena_insns(prog)` is true.\n\nFrom a memory safety perspective:\n1. All newly added struct fields (`uses_arena_insns` in `struct bpf_prog_aux` and `struct bpf_subprog_info`, and `arena_member`, `arena_member_nullable`, and `non_arena_load` in `struct bpf_insn_aux_data`) are zero-initialized upon allocation via `kzalloc_obj`, `kvzalloc_obj`, or `__GFP_ZERO`.\n2. In the JIT compilers, the newly conditioned variables (`arena_vm_start` and `user_vm_start` in x86 `do_jit()`) are explicitly initialized to 0, and the context structures in arm64, s390, loongarch, powerpc, and riscv are zeroed via `memset` or `kcalloc`/`kzalloc` before inspection.\n3. In `arena_member_fixup()`, patched BPF instructions are fully constructed before insertion into the instruction stream.\n4. No kernel structures copied to user space, netlink attributes, ioctl buffers, or infoleak vectors are modified or introduced.\n\nAny potential bugs resulting from these changes (e.g., miscompilation in JIT passes, incorrect verifier state tracking, or invalid instruction rewriting) would manifest as verifier rejections, invalid memory dereferences, or incorrect execution logic—issues covered by standard functional testing and KASAN. There is no risk of uninitialized memory reads or information leaks that would require a dedicated KMSAN fuzzing session.",
  "NeedsKMSAN": false
}

Results:
{
  "KMSANReasoning": "The patch introduces support for `__arena` and `__nullable` BTF type tags on kernel struct members accessed by BPF programs and optimizes JIT compilers across architectures to only set up the arena base registers when `bpf_prog_uses_arena_insns(prog)` is true.\n\nFrom a memory safety perspective:\n1. All newly added struct fields (`uses_arena_insns` in `struct bpf_prog_aux` and `struct bpf_subprog_info`, and `arena_member`, `arena_member_nullable`, and `non_arena_load` in `struct bpf_insn_aux_data`) are zero-initialized upon allocation via `kzalloc_obj`, `kvzalloc_obj`, or `__GFP_ZERO`.\n2. In the JIT compilers, the newly conditioned variables (`arena_vm_start` and `user_vm_start` in x86 `do_jit()`) are explicitly initialized to 0, and the context structures in arm64, s390, loongarch, powerpc, and riscv are zeroed via `memset` or `kcalloc`/`kzalloc` before inspection.\n3. In `arena_member_fixup()`, patched BPF instructions are fully constructed before insertion into the instruction stream.\n4. No kernel structures copied to user space, netlink attributes, ioctl buffers, or infoleak vectors are modified or introduced.\n\nAny potential bugs resulting from these changes (e.g., miscompilation in JIT passes, incorrect verifier state tracking, or invalid instruction rewriting) would manifest as verifier rejections, invalid memory dereferences, or incorrect execution logic—issues covered by standard functional testing and KASAN. There is no risk of uninitialized memory reads or information leaks that would require a dedicated KMSAN fuzzing session.",
  "NeedsKMSAN": false
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)