* [PATCH bpf-next v4 01/12] bpf: Record each half of a paired return value in verifier diagnostics
2026-08-29 6:15 [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns Yonghong Song
@ 2026-08-29 6:15 ` Yonghong Song
2026-08-29 6:15 ` [PATCH bpf-next v4 02/12] bpf: Drop the recursion depth argument of btf_type_is_scalar_struct() Yonghong Song
` (11 subsequent siblings)
12 siblings, 0 replies; 18+ messages in thread
From: Yonghong Song @ 2026-08-29 6:15 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
A subprogram returning more than 8 bytes comes back in the R0:R2 register
pair. There are three places, check_func_call(), prepare_func_exit() and
check_kfunc_call(), where only R0 is tracked by the diagnostic
modification scope and R2 is missed.
Fix it by opening a diagnostic modification scope for each return register.
This way, both return registers are recorded.
Fixes: 0630ad00d96d ("bpf: Add verifier support for 16-byte returns in R0: R2")
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
kernel/bpf/verifier.c | 56 ++++++++++++++++++++++++++++++++-----------
1 file changed, 42 insertions(+), 14 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index e036ae20bf6b..5d8162e13c20 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -9995,9 +9995,14 @@ static int check_func_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
*/
if (!returns_void) {
nregs = bpf_ret_reg_pair(env, subprog) ? 2 : 1;
- for (i = 0; i < nregs; i++)
- mark_reg_unknown(env, caller->regs, ret_regs[i]);
+ mark_reg_unknown(env, caller->regs, ret_regs[0]);
bpf_diag_mod_end(env);
+ for (i = 1; i < nregs; i++) {
+ bpf_diag_mod_begin(env, &caller->regs[ret_regs[i]], NULL,
+ BPF_DIAG_MOD_WRITE);
+ mark_reg_unknown(env, caller->regs, ret_regs[i]);
+ bpf_diag_mod_end(env);
+ }
}
if (env->subprog_info[subprog].might_throw) {
@@ -10403,10 +10408,14 @@ static int prepare_func_exit(struct bpf_verifier_env *env, int *insn_idx)
* return to the caller whatever the callee had in the
* return register(s)
*/
- bpf_diag_mod_begin(env, &caller->regs[BPF_REG_0], r0, BPF_DIAG_MOD_WRITE);
- for (i = 0; i < nregs; i++)
- caller->regs[ret_regs[i]] = callee->regs[ret_regs[i]];
- bpf_diag_mod_end(env);
+ for (i = 0; i < nregs; i++) {
+ u32 regno = ret_regs[i];
+
+ bpf_diag_mod_begin(env, &caller->regs[regno], &callee->regs[regno],
+ BPF_DIAG_MOD_WRITE);
+ caller->regs[regno] = callee->regs[regno];
+ bpf_diag_mod_end(env);
+ }
}
/* for callbacks like bpf_loop or bpf_for_each_map_elem go back to callsite,
@@ -11342,15 +11351,33 @@ static int check_helper_call(struct bpf_verifier_env *env, struct bpf_insn *insn
/*
* Mark the register(s) holding a @size byte kfunc return value as unknown
- * scalars. Both halves of a register pair are treated the same way.
+ * scalars and return how many registers that took. Both halves of a register
+ * pair are treated the same way.
+ *
+ * R0 is written in the diagnostic modification scope the caller opened before
+ * the caller-saved scrub, since R0 gets no scrub record of its own and that
+ * scope is what carries its pre-call value into the history. A scope tracks a
+ * single register and does not nest, so close it before recording the rest of
+ * the return value. A value that fits in R0 leaves the caller's scope open for
+ * the caller to close, so a later write to R0 still lands in it.
*/
-static void mark_kfunc_ret_regs(struct bpf_verifier_env *env,
- struct bpf_reg_state *regs, u32 size)
+static u32 mark_kfunc_ret_regs(struct bpf_verifier_env *env,
+ struct bpf_reg_state *regs, u32 size)
{
u32 i, nregs = ret_regs_cnt(size);
- for (i = 0; i < nregs; i++)
+ mark_reg_unknown(env, regs, ret_regs[0]);
+ if (nregs == 1)
+ return nregs;
+
+ bpf_diag_mod_end(env);
+ for (i = 1; i < nregs; i++) {
+ bpf_diag_mod_begin(env, ®s[ret_regs[i]], NULL, BPF_DIAG_MOD_WRITE);
mark_reg_unknown(env, regs, ret_regs[i]);
+ bpf_diag_mod_end(env);
+ }
+
+ return nregs;
}
static bool is_kfunc_acquire(struct bpf_call_arg_meta *meta)
@@ -13767,7 +13794,7 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
struct bpf_insn_aux_data *insn_aux;
const char *operation;
int err, insn_idx = *insn_idx_p;
- u32 i, nargs, ptr_type_id;
+ u32 i, nargs, ptr_type_id, ret_nregs = 1;
struct bpf_kfunc_desc *desc;
struct btf *desc_btf;
int id;
@@ -14021,7 +14048,7 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
}
if (btf_type_is_scalar(t)) {
- mark_kfunc_ret_regs(env, regs, t->size);
+ ret_nregs = mark_kfunc_ret_regs(env, regs, t->size);
if (meta.btf == btf_vmlinux && (meta.func_id == special_kfunc_list[KF_bpf_res_spin_lock] ||
meta.func_id == special_kfunc_list[KF_bpf_res_spin_lock_irqsave]))
__mark_reg_const_zero(env, ®s[BPF_REG_0]);
@@ -14039,7 +14066,7 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
btf_name_by_offset(desc_btf, t->name_off));
return -EINVAL;
}
- mark_kfunc_ret_regs(env, regs, t->size);
+ ret_nregs = mark_kfunc_ret_regs(env, regs, t->size);
} else if (btf_type_is_ptr(t)) {
ptr_type = btf_type_skip_modifiers(desc_btf, t->type, &ptr_type_id);
err = check_special_kfunc(env, &meta, regs, insn_aux, ptr_type, desc_btf);
@@ -14170,7 +14197,8 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
* Record R0 before process_iter_next_call() snapshots the alternate
* iterator path's diagnostic position.
*/
- bpf_diag_mod_end(env);
+ if (ret_nregs == 1)
+ bpf_diag_mod_end(env);
if (bpf_is_iter_next_kfunc(&meta)) {
err = process_iter_next_call(env, insn_idx, &meta);
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH bpf-next v4 02/12] bpf: Drop the recursion depth argument of btf_type_is_scalar_struct()
2026-08-29 6:15 [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns Yonghong Song
2026-08-29 6:15 ` [PATCH bpf-next v4 01/12] bpf: Record each half of a paired return value in verifier diagnostics Yonghong Song
@ 2026-08-29 6:15 ` Yonghong Song
2026-08-29 6:15 ` [PATCH bpf-next v4 03/12] bpf: Add btf_type_is_arena_ptr() Yonghong Song
` (10 subsequent siblings)
12 siblings, 0 replies; 18+ messages in thread
From: Yonghong Song @ 2026-08-29 6:15 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
btf_type_is_scalar_struct() recurses into nested struct members and
carries the nesting depth in a @rec argument, so every caller has to
spell out the 0 that starts the walk.
Move the recursion into btf_struct_member_walk() and leave
btf_type_is_scalar_struct() as a thin wrapper over it. This is
groundwork for the later patches, which give the walk further state
that callers should not have to supply: the kinds of member it accepts,
and a record of the member that made it fail.
No functional change.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
include/linux/bpf_verifier.h | 2 +-
kernel/bpf/btf.c | 2 +-
kernel/bpf/verifier.c | 24 +++++++++++++++---------
3 files changed, 17 insertions(+), 11 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 004b06785521..3eb61edc8c5e 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -1489,7 +1489,7 @@ struct bpf_iarray *bpf_insn_successors(struct bpf_verifier_env *env, u32 idx);
void bpf_fmt_stack_mask(char *buf, ssize_t buf_sz, u64 stack_mask);
bool bpf_subprog_is_global(const struct bpf_verifier_env *env, int subprog);
bool btf_type_is_scalar_struct(struct bpf_verifier_env *env, const struct btf *btf,
- const struct btf_type *t, int rec);
+ const struct btf_type *t);
int bpf_find_subprog(struct bpf_verifier_env *env, int off);
bool bpf_is_throw_kfunc(struct bpf_insn *insn);
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 91b8ce77f699..47d43eb983a5 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -7995,7 +7995,7 @@ static int btf_validate_return_type(struct bpf_verifier_env *env, struct btf *bt
*/
bool local_func = subprog && !is_global;
- if (local_func || btf_type_is_scalar_struct(env, btf, t, 0))
+ if (local_func || btf_type_is_scalar_struct(env, btf, t))
return 0;
}
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 5d8162e13c20..ed077a5422af 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -11647,9 +11647,8 @@ static bool is_kfunc_arg_implicit(const struct bpf_call_arg_meta *meta, u32 arg_
}
/* Returns true if struct is composed of scalars, 4 levels of nesting allowed */
-bool btf_type_is_scalar_struct(struct bpf_verifier_env *env,
- const struct btf *btf,
- const struct btf_type *t, int rec)
+static bool btf_struct_member_walk(struct bpf_verifier_env *env, const struct btf *btf,
+ const struct btf_type *t, int rec)
{
const struct btf_type *member_type;
const struct btf_member *member;
@@ -11667,7 +11666,7 @@ bool btf_type_is_scalar_struct(struct bpf_verifier_env *env,
verbose(env, "max struct nesting depth exceeded\n");
return false;
}
- if (!btf_type_is_scalar_struct(env, btf, member_type, rec + 1))
+ if (!btf_struct_member_walk(env, btf, member_type, rec + 1))
return false;
continue;
}
@@ -11686,6 +11685,13 @@ bool btf_type_is_scalar_struct(struct bpf_verifier_env *env,
return true;
}
+bool btf_type_is_scalar_struct(struct bpf_verifier_env *env,
+ const struct btf *btf,
+ const struct btf_type *t)
+{
+ return btf_struct_member_walk(env, btf, t, 0);
+}
+
enum kfunc_ptr_arg_type {
KF_ARG_CONST_MEM_SIZE,
KF_ARG_MEM_SIZE,
@@ -12066,7 +12072,7 @@ get_kfunc_arg_type(struct bpf_verifier_env *env, struct bpf_call_arg_meta *meta,
(is_kfunc_arg_mem_size(meta->btf, &args[arg + 1]) ||
is_kfunc_arg_const_mem_size(meta->btf, &args[arg + 1]))) {
if (!btf_type_is_void(ref_t) && !btf_type_is_scalar(ref_t) &&
- !btf_type_is_scalar_struct(env, meta->btf, ref_t, 0)) {
+ !btf_type_is_scalar_struct(env, meta->btf, ref_t)) {
verbose(env, "%s pointer type %s %s must point to void, scalar, or struct with scalar\n",
reg_arg_name(env, argno), btf_type_str(ref_t), ref_tname);
return -EINVAL;
@@ -12082,7 +12088,7 @@ get_kfunc_arg_type(struct bpf_verifier_env *env, struct bpf_call_arg_meta *meta,
* scalars. The access size is derived from the pointed-to BTF type.
*/
if (!btf_type_is_scalar(ref_t) &&
- !btf_type_is_scalar_struct(env, meta->btf, ref_t, 0)) {
+ !btf_type_is_scalar_struct(env, meta->btf, ref_t)) {
verbose(env, "%s pointer type %s %s must point to scalar, or struct with scalar\n",
reg_arg_name(env, argno), btf_type_str(ref_t), ref_tname);
return -EINVAL;
@@ -13138,7 +13144,7 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_call_arg_me
break;
}
- if (!btf_type_is_scalar_struct(env, meta->btf, ref_t, 0)) {
+ if (!btf_type_is_scalar_struct(env, meta->btf, ref_t)) {
enum bpf_reg_type reg2btf_type = lookup_reg2btf_ids(ref_id);
const char *expected_type;
@@ -13680,7 +13686,7 @@ static int check_special_kfunc(struct bpf_verifier_env *env, struct bpf_call_arg
struct_meta = btf_find_struct_meta(ret_btf, ret_btf_id);
if (is_bpf_percpu_obj_new_kfunc(meta->func_id)) {
- if (!btf_type_is_scalar_struct(env, ret_btf, ret_t, 0)) {
+ if (!btf_type_is_scalar_struct(env, ret_btf, ret_t)) {
verbose(env, "bpf_percpu_obj_new type ID argument must be of a struct of scalars\n");
return -EINVAL;
}
@@ -14059,7 +14065,7 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
* otherwise a pointer field would be laundered into a scalar
* and escape provenance and reference tracking.
*/
- if (!btf_type_is_scalar_struct(env, desc_btf, t, 0)) {
+ if (!btf_type_is_scalar_struct(env, desc_btf, t)) {
verbose(env,
"kernel function %s returns %s %s that is not composed of scalars\n",
func_name, btf_type_str(t),
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH bpf-next v4 03/12] bpf: Add btf_type_is_arena_ptr()
2026-08-29 6:15 [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns Yonghong Song
2026-08-29 6:15 ` [PATCH bpf-next v4 01/12] bpf: Record each half of a paired return value in verifier diagnostics Yonghong Song
2026-08-29 6:15 ` [PATCH bpf-next v4 02/12] bpf: Drop the recursion depth argument of btf_type_is_scalar_struct() Yonghong Song
@ 2026-08-29 6:15 ` Yonghong Song
2026-08-29 6:15 ` [PATCH bpf-next v4 04/12] bpf: Let the by-value struct walk take the kinds of member it accepts Yonghong Song
` (9 subsequent siblings)
12 siblings, 0 replies; 18+ messages in thread
From: Yonghong Song @ 2026-08-29 6:15 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
Add btf_type_is_arena_ptr() to test whether a BTF type is a pointer
carrying the "arena" type tag, and use it to simplify both
btf_scan_type_tags() and btf_validate_return_type(). The helper has
external linkage because a later patch calls it from verifier.c to
decide whether a struct member returned by value may be an arena
pointer.
btf_scan_type_tags() drove btf_type_tag_walk() with a one-entry match
table, so the "multiple type tags" error it checked for could never
fire: btf_type_tag_walk() only fails when the walk sets more than one
flag. Dropping that error path, and with it the int return, is
therefore no functional change.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
include/linux/btf.h | 1 +
kernel/bpf/btf.c | 62 ++++++++++++++++-----------------------------
2 files changed, 23 insertions(+), 40 deletions(-)
diff --git a/include/linux/btf.h b/include/linux/btf.h
index 89d5a5c4f117..ddd0f4f32d24 100644
--- a/include/linux/btf.h
+++ b/include/linux/btf.h
@@ -235,6 +235,7 @@ struct btf_record *btf_parse_fields(const struct btf *btf, const struct btf_type
u32 field_mask, u32 value_size);
int btf_check_and_fixup_fields(const struct btf *btf, struct btf_record *rec);
bool btf_type_is_void(const struct btf_type *t);
+bool btf_type_is_arena_ptr(const struct btf *btf, const struct btf_type *t);
s32 btf_find_by_name_kind(const struct btf *btf, const char *name, u8 kind);
s32 bpf_find_btf_id(const char *name, u32 kind, struct btf **btf_p);
struct btf *btf_get_module_btf(const struct module *module);
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 47d43eb983a5..280530d25886 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -3523,6 +3523,22 @@ 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)
+{
+ if (!btf_type_is_ptr(t))
+ return false;
+
+ for (t = btf_type_by_id(btf, t->type); btf_type_is_modifier(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"))
+ return true;
+ }
+
+ return false;
+}
+
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)
{
@@ -7927,51 +7943,19 @@ static int btf_scan_decl_tags(struct bpf_verifier_env *env,
return 0;
}
-static int btf_scan_type_tags(struct bpf_verifier_env *env,
- const struct btf *btf, u32 type_id,
- u32 *tags)
+static void btf_scan_type_tags(const struct btf *btf, u32 type_id, u32 *tags)
{
- static const struct btf_type_tag_match func_type_tags[] = {
- { "arena", ARG_TAG_ARENA },
- };
- struct btf_type_tag_walk_ctx ctx;
- const struct btf_type *t;
- int err;
-
/* Find the first pointer type in the chain. */
- t = btf_type_skip_modifiers(btf, type_id, NULL);
+ const struct btf_type *t = btf_type_skip_modifiers(btf, type_id, NULL);
- /*
- * We currently reject type tags on non-pointer types,
- * which neither LLVM nor GCC support anyway.
- */
- if (!t || !btf_type_is_ptr(t))
- return 0;
-
- ctx.t = t;
- err = btf_type_tag_walk(btf, &ctx, func_type_tags,
- ARRAY_SIZE(func_type_tags));
- if (err) {
- bpf_log(&env->log,
- "function signature member has multiple type tags\n");
- return err;
- }
- *tags |= ctx.res;
-
- return 0;
+ if (btf_type_is_arena_ptr(btf, t))
+ *tags |= ARG_TAG_ARENA;
}
/* Check whether the type is a valid return type. */
static int btf_validate_return_type(struct bpf_verifier_env *env, struct btf *btf,
const struct btf_type *t, int subprog, bool is_global)
{
- u32 tags = 0;
- int err;
-
- err = btf_scan_type_tags(env, btf, t->type, &tags);
- if (err)
- return err;
-
t = btf_type_skip_modifiers(btf, t->type, NULL);
/*
@@ -7979,7 +7963,7 @@ static int btf_validate_return_type(struct bpf_verifier_env *env, struct btf *bt
* General arena variables are not allowed, since it makes no sense to return by value
* a variable that's on the heap in the first place.
*/
- if (subprog && (tags & ARG_TAG_ARENA) && btf_type_is_ptr(t))
+ if (subprog && btf_type_is_arena_ptr(btf, t))
return 0;
/* We always accept void or scalars. */
@@ -8106,9 +8090,7 @@ int btf_prepare_func_args(struct bpf_verifier_env *env, int subprog)
if (err)
return err;
- err = btf_scan_type_tags(env, btf, args[i].type, &tags);
- if (err)
- return err;
+ btf_scan_type_tags(btf, args[i].type, &tags);
t = btf_type_by_id(btf, args[i].type);
while (btf_type_is_modifier(t))
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH bpf-next v4 04/12] bpf: Let the by-value struct walk take the kinds of member it accepts
2026-08-29 6:15 [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns Yonghong Song
` (2 preceding siblings ...)
2026-08-29 6:15 ` [PATCH bpf-next v4 03/12] bpf: Add btf_type_is_arena_ptr() Yonghong Song
@ 2026-08-29 6:15 ` Yonghong Song
2026-08-29 6:15 ` [PATCH bpf-next v4 05/12] bpf: Let a by-value struct nest arrays and structs freely Yonghong Song
` (8 subsequent siblings)
12 siblings, 0 replies; 18+ messages in thread
From: Yonghong Song @ 2026-08-29 6:15 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
btf_struct_member_walk() hardcodes that every member of a struct returned
by value must be a scalar. A later patch needs the same walk to also
accept arena pointers, so give it a mask of the member kinds it allows.
The mask reaches callers through btf_struct_is_composed_of(), which
replaces btf_type_is_scalar_struct() as the entry point exported to
btf.c. btf_type_is_scalar_struct() stays as a verifier-local wrapper for
the call sites that only ever ask about scalars.
There is no functional change: btf_member_kind_allowed() is exactly
btf_type_is_scalar() when the mask is BTF_MEMBER_SCALAR alone, and the
shared check is the last statement of the loop body, so dropping the array
branch's continue neither skips a member nor checks one twice.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
include/linux/bpf_verifier.h | 11 +++++++++--
kernel/bpf/btf.c | 2 +-
kernel/bpf/verifier.c | 38 ++++++++++++++++++++++++++----------
3 files changed, 38 insertions(+), 13 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 3eb61edc8c5e..ae9f606539f4 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -1488,8 +1488,15 @@ int bpf_jmp_offset(struct bpf_insn *insn);
struct bpf_iarray *bpf_insn_successors(struct bpf_verifier_env *env, u32 idx);
void bpf_fmt_stack_mask(char *buf, ssize_t buf_sz, u64 stack_mask);
bool bpf_subprog_is_global(const struct bpf_verifier_env *env, int subprog);
-bool btf_type_is_scalar_struct(struct bpf_verifier_env *env, const struct btf *btf,
- const struct btf_type *t);
+
+/* Kinds of member a by-value struct or union may be composed of. */
+enum btf_member_kind {
+ BTF_MEMBER_SCALAR = BIT(0), /* an int or an enum */
+ BTF_MEMBER_ARENA_PTR = BIT(1), /* a pointer carrying the "arena" type tag */
+};
+
+bool btf_struct_is_composed_of(struct bpf_verifier_env *env, const struct btf *btf,
+ const struct btf_type *t, u32 member_kinds);
int bpf_find_subprog(struct bpf_verifier_env *env, int off);
bool bpf_is_throw_kfunc(struct bpf_insn *insn);
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index 280530d25886..b1f4ef614d4c 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -7979,7 +7979,7 @@ static int btf_validate_return_type(struct bpf_verifier_env *env, struct btf *bt
*/
bool local_func = subprog && !is_global;
- if (local_func || btf_type_is_scalar_struct(env, btf, t))
+ if (local_func || btf_struct_is_composed_of(env, btf, t, BTF_MEMBER_SCALAR))
return 0;
}
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index ed077a5422af..90139f1b78d1 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -11646,9 +11646,23 @@ static bool is_kfunc_arg_implicit(const struct bpf_call_arg_meta *meta, u32 arg_
return argn <= arg_idx;
}
-/* Returns true if struct is composed of scalars, 4 levels of nesting allowed */
+static bool btf_member_kind_allowed(const struct btf *btf, const struct btf_type *t,
+ u32 member_kinds)
+{
+ if ((member_kinds & BTF_MEMBER_SCALAR) && btf_type_is_scalar(t))
+ return true;
+ if ((member_kinds & BTF_MEMBER_ARENA_PTR) && btf_type_is_arena_ptr(btf, t))
+ return true;
+ return false;
+}
+
+/*
+ * Returns true if every member of struct @t is of a kind listed in
+ * @member_kinds, 4 levels of nesting allowed. An array member counts as its
+ * element type.
+ */
static bool btf_struct_member_walk(struct bpf_verifier_env *env, const struct btf *btf,
- const struct btf_type *t, int rec)
+ const struct btf_type *t, u32 member_kinds, int rec)
{
const struct btf_type *member_type;
const struct btf_member *member;
@@ -11666,7 +11680,7 @@ static bool btf_struct_member_walk(struct bpf_verifier_env *env, const struct bt
verbose(env, "max struct nesting depth exceeded\n");
return false;
}
- if (!btf_struct_member_walk(env, btf, member_type, rec + 1))
+ if (!btf_struct_member_walk(env, btf, member_type, member_kinds, rec + 1))
return false;
continue;
}
@@ -11675,21 +11689,25 @@ static bool btf_struct_member_walk(struct bpf_verifier_env *env, const struct bt
if (!array->nelems)
return false;
member_type = btf_type_skip_modifiers(btf, array->type, NULL);
- if (!btf_type_is_scalar(member_type))
- return false;
- continue;
}
- if (!btf_type_is_scalar(member_type))
+ if (!btf_member_kind_allowed(btf, member_type, member_kinds))
return false;
}
return true;
}
-bool btf_type_is_scalar_struct(struct bpf_verifier_env *env,
+bool btf_struct_is_composed_of(struct bpf_verifier_env *env,
const struct btf *btf,
- const struct btf_type *t)
+ const struct btf_type *t, u32 member_kinds)
+{
+ return btf_struct_member_walk(env, btf, t, member_kinds, 0);
+}
+
+static bool btf_type_is_scalar_struct(struct bpf_verifier_env *env,
+ const struct btf *btf,
+ const struct btf_type *t)
{
- return btf_struct_member_walk(env, btf, t, 0);
+ return btf_struct_is_composed_of(env, btf, t, BTF_MEMBER_SCALAR);
}
enum kfunc_ptr_arg_type {
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH bpf-next v4 05/12] bpf: Let a by-value struct nest arrays and structs freely
2026-08-29 6:15 [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns Yonghong Song
` (3 preceding siblings ...)
2026-08-29 6:15 ` [PATCH bpf-next v4 04/12] bpf: Let the by-value struct walk take the kinds of member it accepts Yonghong Song
@ 2026-08-29 6:15 ` Yonghong Song
2026-08-29 7:11 ` bot+bpf-ci
2026-08-29 6:15 ` [PATCH bpf-next v4 06/12] bpf: Report which member makes a kfunc return type unsupported Yonghong Song
` (7 subsequent siblings)
12 siblings, 1 reply; 18+ messages in thread
From: Yonghong Song @ 2026-08-29 6:15 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
Go through nested array types until it reaches a non-array type. Later
code will check such non-array type based on current logic. A
zero-length array is still rejected, at whichever dimension it appears.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
kernel/bpf/verifier.c | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 90139f1b78d1..a377b1995b51 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -11675,6 +11675,17 @@ static bool btf_struct_member_walk(struct bpf_verifier_env *env, const struct bt
const struct btf_array *array;
member_type = btf_type_skip_modifiers(btf, member->type, NULL);
+ /*
+ * Every element of an array is laid out in the value being
+ * returned, so an array counts as its element type however many
+ * dimensions deep that is.
+ */
+ while (btf_type_is_array(member_type)) {
+ array = btf_array(member_type);
+ if (!array->nelems)
+ return false;
+ member_type = btf_type_skip_modifiers(btf, array->type, NULL);
+ }
if (btf_type_is_struct(member_type)) {
if (rec >= 3) {
verbose(env, "max struct nesting depth exceeded\n");
@@ -11684,12 +11695,6 @@ static bool btf_struct_member_walk(struct bpf_verifier_env *env, const struct bt
return false;
continue;
}
- if (btf_type_is_array(member_type)) {
- array = btf_array(member_type);
- if (!array->nelems)
- return false;
- member_type = btf_type_skip_modifiers(btf, array->type, NULL);
- }
if (!btf_member_kind_allowed(btf, member_type, member_kinds))
return false;
}
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 18+ messages in thread* Re: [PATCH bpf-next v4 05/12] bpf: Let a by-value struct nest arrays and structs freely
2026-08-29 6:15 ` [PATCH bpf-next v4 05/12] bpf: Let a by-value struct nest arrays and structs freely Yonghong Song
@ 2026-08-29 7:11 ` bot+bpf-ci
0 siblings, 0 replies; 18+ messages in thread
From: bot+bpf-ci @ 2026-08-29 7:11 UTC (permalink / raw)
To: yonghong.song, bpf
Cc: ast, andrii, daniel, eddyz87, kernel-team, ast, andrii, daniel,
martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2565 bytes --]
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 90139f1b78d10..a377b1995b51f 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -11675,6 +11675,17 @@ static bool btf_struct_member_walk(struct bpf_verifier_env *env, const struct bt
> const struct btf_array *array;
>
> member_type = btf_type_skip_modifiers(btf, member->type, NULL);
> + /*
> + * Every element of an array is laid out in the value being
> + * returned, so an array counts as its element type however many
> + * dimensions deep that is.
> + */
This isn't a bug, but since btf_struct_member_walk() is also used for
kfunc pointer arguments (get_kfunc_arg_type(), check_kfunc_args()) and
the bpf_percpu_obj_new() type argument (check_special_kfunc()), would it
read better to phrase this in terms of the aggregate's layout rather than
"the value being returned"?
> + while (btf_type_is_array(member_type)) {
> + array = btf_array(member_type);
> + if (!array->nelems)
> + return false;
> + member_type = btf_type_skip_modifiers(btf, array->type, NULL);
> + }
> if (btf_type_is_struct(member_type)) {
> if (rec >= 3) {
> verbose(env, "max struct nesting depth exceeded\n");
> @@ -11684,12 +11695,6 @@ static bool btf_struct_member_walk(struct bpf_verifier_env *env, const struct bt
> return false;
> continue;
> }
> - if (btf_type_is_array(member_type)) {
> - array = btf_array(member_type);
> - if (!array->nelems)
> - return false;
> - member_type = btf_type_skip_modifiers(btf, array->type, NULL);
> - }
> if (!btf_member_kind_allowed(btf, member_type, member_kinds))
> return false;
> }
> bpf: Let a by-value struct nest arrays and structs freely
>
> Go through nested array types until it reaches a non-array type. Later
> code will check such non-array type based on current logic. A
> zero-length array is still rejected, at whichever dimension it appears.
This isn't a bug, but the changelog explains the array mechanics without
saying what this enables for users. Moving the unwrap ahead of the struct
test is what allows an array of structs (previously rejected the same way
a multi-dimensional array was).
Could the changelog mention both what this change allows (arrays of
structs, nested arrays) and why that matters?
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/33238287240
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH bpf-next v4 06/12] bpf: Report which member makes a kfunc return type unsupported
2026-08-29 6:15 [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns Yonghong Song
` (4 preceding siblings ...)
2026-08-29 6:15 ` [PATCH bpf-next v4 05/12] bpf: Let a by-value struct nest arrays and structs freely Yonghong Song
@ 2026-08-29 6:15 ` Yonghong Song
2026-08-29 7:11 ` bot+bpf-ci
2026-08-29 6:15 ` [PATCH bpf-next v4 07/12] bpf: Allow a global function to return arena pointers by value Yonghong Song
` (6 subsequent siblings)
12 siblings, 1 reply; 18+ messages in thread
From: Yonghong Song @ 2026-08-29 6:15 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
A kfunc that returns a struct by value may only return scalars, and the
message that rejects one names the type but not the member at fault:
kernel function bpf_kfunc_call_test_ret_ptr returns STRUCT
prog_test_ret_ptr that is not composed of scalars
Record the member that made the walk fail and name it. What is recorded
is a path, not a single member, because the walk descends up to 4 levels.
For
struct outer { struct inner { void *p; } in; __u64 tag; };
naming 'p' alone would send the reader looking for a member struct outer
does not have, so the actual message reads "member 'in.p' has type PTR".
For
struct s { void *p[2]; };
naming 'p' alone would blame the member, when it is the element type that
was rejected, so the actual message reads "member 'p[]' has type PTR". A
zero-length array has no element type to name and is rejected for its
length instead, so it reports "member 'p' is a zero-length array".
The detailed diagnostics for this failure:
Verification failed: Program Structure: Unsupported kernel function
return type
Reason:
bpf_kfunc_call_test_ret_ptr() returns STRUCT prog_test_ret_ptr by
value. Its member 'p' is PTR, not a scalar. Only kfuncs returning
scalar values, or structures composed of scalar values are
supported.
...
Suggestion:
Call a kernel function that returns only scalars by value.
A type nested deeper than the walk descends has no single member to
blame, so that case reports the depth instead:
Reason:
bpf_kfunc_call_test_ret_deep() returns STRUCT prog_test_ret_deep by
value. It nests structs more than 4 levels deep. ...
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
kernel/bpf/verifier.c | 103 ++++++++++++++++++++++++++++++++++++++----
1 file changed, 94 insertions(+), 9 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index a377b1995b51..745daf58b052 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -11646,6 +11646,17 @@ static bool is_kfunc_arg_implicit(const struct bpf_call_arg_meta *meta, u32 arg_
return argn <= arg_idx;
}
+#define BTF_MEMBER_MAX_DEPTH 4
+#define BTF_MEMBER_PATH_LEN 64
+
+struct btf_member_path {
+ const struct btf_member *member[BTF_MEMBER_MAX_DEPTH];
+ bool in_array[BTF_MEMBER_MAX_DEPTH];
+ const struct btf_type *bad_type;
+ int depth;
+ bool too_deep;
+};
+
static bool btf_member_kind_allowed(const struct btf *btf, const struct btf_type *t,
u32 member_kinds)
{
@@ -11662,10 +11673,12 @@ static bool btf_member_kind_allowed(const struct btf *btf, const struct btf_type
* element type.
*/
static bool btf_struct_member_walk(struct bpf_verifier_env *env, const struct btf *btf,
- const struct btf_type *t, u32 member_kinds, int rec)
+ const struct btf_type *t, u32 member_kinds, int rec,
+ struct btf_member_path *path)
{
const struct btf_type *member_type;
const struct btf_member *member;
+ bool in_array;
u32 i;
if (!btf_type_is_struct(t))
@@ -11674,6 +11687,7 @@ static bool btf_struct_member_walk(struct bpf_verifier_env *env, const struct bt
for_each_member(i, t, member) {
const struct btf_array *array;
+ in_array = false;
member_type = btf_type_skip_modifiers(btf, member->type, NULL);
/*
* Every element of an array is laid out in the value being
@@ -11682,30 +11696,48 @@ static bool btf_struct_member_walk(struct bpf_verifier_env *env, const struct bt
*/
while (btf_type_is_array(member_type)) {
array = btf_array(member_type);
- if (!array->nelems)
- return false;
+ if (!array->nelems) {
+ member_type = NULL;
+ goto bad_member;
+ }
member_type = btf_type_skip_modifiers(btf, array->type, NULL);
+ in_array = true;
}
if (btf_type_is_struct(member_type)) {
- if (rec >= 3) {
+ if (rec >= BTF_MEMBER_MAX_DEPTH - 1) {
verbose(env, "max struct nesting depth exceeded\n");
+ if (path)
+ path->too_deep = true;
return false;
}
- if (!btf_struct_member_walk(env, btf, member_type, member_kinds, rec + 1))
- return false;
+ if (!btf_struct_member_walk(env, btf, member_type, member_kinds,
+ rec + 1, path))
+ goto bad_path;
continue;
}
if (!btf_member_kind_allowed(btf, member_type, member_kinds))
- return false;
+ goto bad_member;
}
return true;
+
+bad_member:
+ if (path) {
+ path->depth = rec + 1;
+ path->bad_type = member_type;
+ }
+bad_path:
+ if (path && path->depth) {
+ path->member[rec] = member;
+ path->in_array[rec] = in_array;
+ }
+ return false;
}
bool btf_struct_is_composed_of(struct bpf_verifier_env *env,
const struct btf *btf,
const struct btf_type *t, u32 member_kinds)
{
- return btf_struct_member_walk(env, btf, t, member_kinds, 0);
+ return btf_struct_member_walk(env, btf, t, member_kinds, 0, NULL);
}
static bool btf_type_is_scalar_struct(struct bpf_verifier_env *env,
@@ -11715,6 +11747,25 @@ static bool btf_type_is_scalar_struct(struct bpf_verifier_env *env,
return btf_struct_is_composed_of(env, btf, t, BTF_MEMBER_SCALAR);
}
+static void btf_member_path_str(const struct btf *btf, const struct btf_member_path *path,
+ char *buf, size_t buf_sz)
+{
+ size_t len = 0;
+ int i;
+
+ buf[0] = '\0';
+ for (i = 0; i < path->depth; i++) {
+ const char *name = btf_name_by_offset(btf, path->member[i]->name_off);
+
+ /* An anonymous struct or union has no name to spell. */
+ if (!name || !name[0])
+ continue;
+ len += scnprintf(buf + len, buf_sz - len, "%s%s", len ? "." : "", name);
+ if (path->in_array[i])
+ len += scnprintf(buf + len, buf_sz - len, "[]");
+ }
+}
+
enum kfunc_ptr_arg_type {
KF_ARG_CONST_MEM_SIZE,
KF_ARG_MEM_SIZE,
@@ -14082,17 +14133,51 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
meta.func_id == special_kfunc_list[KF_bpf_res_spin_lock_irqsave]))
__mark_reg_const_zero(env, ®s[BPF_REG_0]);
} else if (btf_type_is_struct(t)) {
+ struct btf_member_path path = {};
+ const char *member_note = "";
+
/*
* The returned struct comes back as raw register bits modeled
* as an unknown scalar, so it must contain only scalars:
* otherwise a pointer field would be laundered into a scalar
* and escape provenance and reference tracking.
*/
- if (!btf_type_is_scalar_struct(env, desc_btf, t)) {
+ if (!btf_struct_member_walk(env, desc_btf, t, BTF_MEMBER_SCALAR, 0, &path)) {
verbose(env,
"kernel function %s returns %s %s that is not composed of scalars\n",
func_name, btf_type_str(t),
btf_name_by_offset(desc_btf, t->name_off));
+ if (path.too_deep) {
+ member_note = bpf_diag_fmt(
+ env, " It nests structs more than %d levels deep.",
+ BTF_MEMBER_MAX_DEPTH);
+ } else if (path.depth) {
+ char bad_name[BTF_MEMBER_PATH_LEN];
+
+ btf_member_path_str(desc_btf, &path, bad_name, sizeof(bad_name));
+ if (!path.bad_type) {
+ verbose(env, "member '%s' is a zero-length array\n",
+ bad_name);
+ member_note = bpf_diag_fmt(
+ env, " Its member '%s' is a zero-length array.",
+ bad_name);
+ } else {
+ verbose(env, "member '%s' has type %s\n", bad_name,
+ btf_type_str(path.bad_type));
+ member_note = bpf_diag_fmt(
+ env, " Its member '%s' is %s, not a scalar.",
+ bad_name, btf_type_str(path.bad_type));
+ }
+ }
+ bpf_diag_program_structure(
+ env, insn_idx, "unsupported kernel function return type",
+ "Call a kernel function that returns only scalars by value.",
+ "%s() returns %s %s by value.%s "
+ "Only kfuncs returning scalar values, or "
+ "structures composed of scalar values are "
+ "supported.",
+ func_name, btf_type_str(t),
+ btf_name_by_offset(desc_btf, t->name_off), member_note);
return -EINVAL;
}
ret_nregs = mark_kfunc_ret_regs(env, regs, t->size);
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 18+ messages in thread* Re: [PATCH bpf-next v4 06/12] bpf: Report which member makes a kfunc return type unsupported
2026-08-29 6:15 ` [PATCH bpf-next v4 06/12] bpf: Report which member makes a kfunc return type unsupported Yonghong Song
@ 2026-08-29 7:11 ` bot+bpf-ci
0 siblings, 0 replies; 18+ messages in thread
From: bot+bpf-ci @ 2026-08-29 7:11 UTC (permalink / raw)
To: yonghong.song, bpf
Cc: ast, andrii, daniel, eddyz87, kernel-team, ast, andrii, daniel,
martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2333 bytes --]
> bpf: Report which member makes a kfunc return type unsupported
>
> A kfunc that returns a struct by value may only return scalars, and the
> message that rejects one names the type but not the member at fault:
> kernel function bpf_kfunc_call_test_ret_ptr returns STRUCT
> prog_test_ret_ptr that is not composed of scalars
> Record the member that made the walk fail and name it. What is recorded
> is a path, not a single member, because the walk descends up to 4 levels.
> For
> struct outer { struct inner { void *p; } in; __u64 tag; };
> naming 'p' alone would send the reader looking for a member struct outer
> does not have, so the actual message reads "member 'in.p' has type PTR".
> For
> struct s { void *p[2]; };
> naming 'p' alone would blame the member, when it is the element type that
> was rejected, so the actual message reads "member 'p[]' has type PTR". A
> zero-length array has no element type to name and is rejected for its
> length instead, so it reports "member 'p' is a zero-length array".
> The detailed diagnostics for this failure:
> Verification failed: Program Structure: Unsupported kernel function
> return type
> Reason:
> bpf_kfunc_call_test_ret_ptr() returns STRUCT prog_test_ret_ptr by
> value. Its member 'p' is PTR, not a scalar. Only kfuncs returning
> scalar values, or structures composed of scalar values are
> supported.
> ...
> Suggestion:
> Call a kernel function that returns only scalars by value.
> A type nested deeper than the walk descends has no single member to
> blame, so that case reports the depth instead:
> Reason:
> bpf_kfunc_call_test_ret_deep() returns STRUCT prog_test_ret_deep by
> value. It nests structs more than 4 levels deep. ...
This isn't a bug, but the bpf_kfunc_call_test_ret_ptr() output above is
reproducible against the existing test kfunc, while bpf_kfunc_call_test_ret_deep()
and struct prog_test_ret_deep do not seem to exist in the tree. Would it
be worth marking that second block as a constructed example, or adding the
test kfunc so both can be reproduced?
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/33238287240
^ permalink raw reply [flat|nested] 18+ messages in thread
* [PATCH bpf-next v4 07/12] bpf: Allow a global function to return arena pointers by value
2026-08-29 6:15 [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns Yonghong Song
` (5 preceding siblings ...)
2026-08-29 6:15 ` [PATCH bpf-next v4 06/12] bpf: Report which member makes a kfunc return type unsupported Yonghong Song
@ 2026-08-29 6:15 ` Yonghong Song
2026-08-29 6:15 ` [PATCH bpf-next v4 08/12] bpf: Allow arena pointers in a by-value kfunc return Yonghong Song
` (5 subsequent siblings)
12 siblings, 0 replies; 18+ messages in thread
From: Yonghong Song @ 2026-08-29 6:15 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
A global function may already return an arena pointer on its own, and
check_global_ret_scalar_reg() accepts one in either half of the R0:R2
pair. Let the members of a by-value struct it returns be arena pointers
as well, rather than scalars only.
Only a subprogram gets this: the main program returns to the kernel,
which has no arena to cast the address back into, so
btf_validate_return_type() keeps rejecting an arena pointer there,
whether bare or inside a struct. The rejection message therefore has to
name what the function at hand may return, or it would offer the main
program an arena pointer it cannot have.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
kernel/bpf/btf.c | 28 +++++++++++++++----
.../selftests/bpf/progs/exceptions_fail.c | 2 +-
2 files changed, 23 insertions(+), 7 deletions(-)
diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
index b1f4ef614d4c..9c2cab08bb79 100644
--- a/kernel/bpf/btf.c
+++ b/kernel/bpf/btf.c
@@ -7973,13 +7973,23 @@ static int btf_validate_return_type(struct bpf_verifier_env *env, struct btf *bt
if (btf_type_is_struct(t) && t->size <= 16) {
/*
* A global function's caller models the return as an opaque
- * scalar pair, so it may only return scalars by value. A local
- * function is verified inline, so a pointer field stays tracked
- * and needs no such restriction.
+ * scalar pair, so a pointer member would be laundered into a
+ * scalar and escape provenance and reference tracking. Only
+ * scalars and arena pointers are allowed: an arena pointer has
+ * no provenance to lose, since a program may already derive one
+ * from any scalar with addr_space_cast(), which confines the
+ * result to the arena. The main program is the exception: it
+ * returns to the kernel, which has no arena to cast the address
+ * back into. A local function is verified inline, so its caller
+ * receives the real register state and any member is fine.
*/
bool local_func = subprog && !is_global;
+ u32 member_kinds = BTF_MEMBER_SCALAR;
- if (local_func || btf_struct_is_composed_of(env, btf, t, BTF_MEMBER_SCALAR))
+ if (subprog)
+ member_kinds |= BTF_MEMBER_ARENA_PTR;
+
+ if (local_func || btf_struct_is_composed_of(env, btf, t, member_kinds))
return 0;
}
@@ -8073,10 +8083,16 @@ int btf_prepare_func_args(struct bpf_verifier_env *env, int subprog)
err = btf_validate_return_type(env, btf, t, subprog, is_global);
if (err) {
if (is_global) {
+ /* Only a subprogram may return arena pointers. */
+ const char *supported = subprog ?
+ "void, scalar, arena pointer, or a struct/union of "
+ "scalars and arena pointers" :
+ "void, scalar, or a scalar-only struct/union";
+
bpf_log(log,
"Global function %s() has unsupported return type. "
- "Only void, scalar, or a scalar-only struct/union up to 16 bytes is supported.\n",
- tname);
+ "Only %s up to 16 bytes is supported.\n",
+ tname, supported);
}
return err;
}
diff --git a/tools/testing/selftests/bpf/progs/exceptions_fail.c b/tools/testing/selftests/bpf/progs/exceptions_fail.c
index 9708efb93683..22503cf62e9f 100644
--- a/tools/testing/selftests/bpf/progs/exceptions_fail.c
+++ b/tools/testing/selftests/bpf/progs/exceptions_fail.c
@@ -60,7 +60,7 @@ __noinline int exception_cb_ok_arg_small(int a)
SEC("?tc")
__exception_cb(exception_cb_bad_ret_type1)
-__failure __msg("Only void, scalar, or a scalar-only struct/union up to 16 bytes is supported.")
+__failure __msg("Only void, scalar, arena pointer, or a struct/union of scalars and arena pointers up to 16 bytes is supported.")
int reject_exception_cb_type_1(struct __sk_buff *ctx)
{
bpf_throw(0);
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH bpf-next v4 08/12] bpf: Allow arena pointers in a by-value kfunc return
2026-08-29 6:15 [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns Yonghong Song
` (6 preceding siblings ...)
2026-08-29 6:15 ` [PATCH bpf-next v4 07/12] bpf: Allow a global function to return arena pointers by value Yonghong Song
@ 2026-08-29 6:15 ` Yonghong Song
2026-08-29 6:16 ` [PATCH bpf-next v4 09/12] selftests/bpf: Check the member named for an unsupported kfunc return type Yonghong Song
` (4 subsequent siblings)
12 siblings, 0 replies; 18+ messages in thread
From: Yonghong Song @ 2026-08-29 6:15 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
A kfunc may already return an arena pointer on its own, which the program
casts back into the arena address space to use. Let the members of a
by-value struct it returns be arena pointers as well, rather than scalars
only.
Such a member carries the arena type tag but not the address space
qualifier, so the program has to cast it itself. Handing it back as a
scalar costs nothing: an arena address has no provenance to track, and the
cast is confined to the caller's arena.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
kernel/bpf/verifier.c | 22 +++++++++++--------
.../selftests/bpf/progs/aggregate_ret_kfunc.c | 2 +-
2 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 745daf58b052..a725e3917976 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -14138,13 +14138,16 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
/*
* The returned struct comes back as raw register bits modeled
- * as an unknown scalar, so it must contain only scalars:
- * otherwise a pointer field would be laundered into a scalar
- * and escape provenance and reference tracking.
+ * as an unknown scalar, so a pointer member would be laundered
+ * into a scalar and escape provenance and reference tracking.
+ * Only scalars and arena pointers are allowed; see
+ * btf_validate_return_type() in btf.c for why an arena pointer
+ * is safe here.
*/
- if (!btf_struct_member_walk(env, desc_btf, t, BTF_MEMBER_SCALAR, 0, &path)) {
+ if (!btf_struct_member_walk(env, desc_btf, t,
+ BTF_MEMBER_SCALAR | BTF_MEMBER_ARENA_PTR, 0, &path)) {
verbose(env,
- "kernel function %s returns %s %s that is not composed of scalars\n",
+ "kernel function %s returns %s %s that is not composed of scalars or arena pointers\n",
func_name, btf_type_str(t),
btf_name_by_offset(desc_btf, t->name_off));
if (path.too_deep) {
@@ -14165,16 +14168,17 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
verbose(env, "member '%s' has type %s\n", bad_name,
btf_type_str(path.bad_type));
member_note = bpf_diag_fmt(
- env, " Its member '%s' is %s, not a scalar.",
+ env,
+ " Its member '%s' is %s, not a scalar or an arena pointer.",
bad_name, btf_type_str(path.bad_type));
}
}
bpf_diag_program_structure(
env, insn_idx, "unsupported kernel function return type",
- "Call a kernel function that returns only scalars by value.",
+ "Call a kernel function that returns only scalars or arena pointers by value.",
"%s() returns %s %s by value.%s "
- "Only kfuncs returning scalar values, or "
- "structures composed of scalar values are "
+ "Only kfuncs returning scalar values or arena pointers, or "
+ "structures composed of scalar values or arena pointers are "
"supported.",
func_name, btf_type_str(t),
btf_name_by_offset(desc_btf, t->name_off), member_note);
diff --git a/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc.c b/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc.c
index d6b422ae9784..f10e5cf6fd89 100644
--- a/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc.c
+++ b/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc.c
@@ -71,7 +71,7 @@ __naked int aggregate_ret_kfunc_fastcall_fail(void)
SEC("tc")
__arch_x86_64 __arch_arm64
-__failure __msg("is not composed of scalars")
+__failure __msg("is not composed of scalars or arena pointers")
__naked int aggregate_ret_kfunc_ptr_fail(void)
{
asm volatile (
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH bpf-next v4 09/12] selftests/bpf: Check the member named for an unsupported kfunc return type
2026-08-29 6:15 [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns Yonghong Song
` (7 preceding siblings ...)
2026-08-29 6:15 ` [PATCH bpf-next v4 08/12] bpf: Allow arena pointers in a by-value kfunc return Yonghong Song
@ 2026-08-29 6:16 ` Yonghong Song
2026-08-29 6:16 ` [PATCH bpf-next v4 10/12] selftests/bpf: Test global functions returning arena pointers by value Yonghong Song
` (3 subsequent siblings)
12 siblings, 0 replies; 18+ messages in thread
From: Yonghong Song @ 2026-08-29 6:16 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
Add tests to cover cases where a kfunc return type is rejected with
proper messages including member names and array types.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
.../selftests/bpf/progs/aggregate_ret_kfunc.c | 82 +++++++++++++++++++
.../selftests/bpf/test_kmods/bpf_testmod.c | 40 +++++++++
.../bpf/test_kmods/bpf_testmod_kfunc.h | 42 ++++++++++
3 files changed, 164 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc.c b/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc.c
index f10e5cf6fd89..0361a7b42c9b 100644
--- a/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc.c
+++ b/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc.c
@@ -19,6 +19,11 @@ void __kfunc_btf_root(void)
"r"(&bpf_kfunc_call_test_ret_fastcall),
"r"(&bpf_kfunc_call_test_ret_ptr),
"r"(&bpf_kfunc_call_test_ret_ii),
+ "r"(&bpf_kfunc_call_test_ret_nested),
+ "r"(&bpf_kfunc_call_test_ret_ptr_arr),
+ "r"(&bpf_kfunc_call_test_ret_deep),
+ "r"(&bpf_kfunc_call_test_ret_arr_struct),
+ "r"(&bpf_kfunc_call_test_ret_arr2d),
"r"(&bpf_kfunc_call_test_ret_big));
}
@@ -72,6 +77,7 @@ __naked int aggregate_ret_kfunc_fastcall_fail(void)
SEC("tc")
__arch_x86_64 __arch_arm64
__failure __msg("is not composed of scalars or arena pointers")
+__msg("member 'p' has type PTR")
__naked int aggregate_ret_kfunc_ptr_fail(void)
{
asm volatile (
@@ -84,6 +90,82 @@ __naked int aggregate_ret_kfunc_ptr_fail(void)
: __clobber_all);
}
+SEC("tc")
+__arch_x86_64 __arch_arm64
+__failure __msg("is not composed of scalars or arena pointers")
+__msg("member 'in.p' has type PTR")
+__naked int aggregate_ret_kfunc_nested_ptr_fail(void)
+{
+ asm volatile (
+ "r1 = 0;"
+ "call %[bpf_kfunc_call_test_ret_nested];"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm(bpf_kfunc_call_test_ret_nested)
+ : __clobber_all);
+}
+
+SEC("tc")
+__arch_x86_64 __arch_arm64
+__failure __msg("is not composed of scalars or arena pointers")
+__msg("member 'p[]' has type PTR")
+__naked int aggregate_ret_kfunc_ptr_arr_fail(void)
+{
+ asm volatile (
+ "call %[bpf_kfunc_call_test_ret_ptr_arr];"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm(bpf_kfunc_call_test_ret_ptr_arr)
+ : __clobber_all);
+}
+
+SEC("tc")
+__arch_x86_64 __arch_arm64
+__failure __msg("is not composed of scalars or arena pointers")
+__msg("member 'in1[].in2.p' has type PTR")
+__naked int aggregate_ret_kfunc_arr_struct_fail(void)
+{
+ asm volatile (
+ "call %[bpf_kfunc_call_test_ret_arr_struct];"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm(bpf_kfunc_call_test_ret_arr_struct)
+ : __clobber_all);
+}
+
+SEC("tc")
+__arch_x86_64 __arch_arm64
+__failure __msg("is not composed of scalars or arena pointers")
+__msg("member 'a[].p' has type PTR")
+__naked int aggregate_ret_kfunc_arr2d_fail(void)
+{
+ asm volatile (
+ "call %[bpf_kfunc_call_test_ret_arr2d];"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm(bpf_kfunc_call_test_ret_arr2d)
+ : __clobber_all);
+}
+
+SEC("tc")
+__arch_x86_64 __arch_arm64
+__failure __msg("max struct nesting depth exceeded")
+__naked int aggregate_ret_kfunc_too_deep_fail(void)
+{
+ asm volatile (
+ "r1 = 0;"
+ "call %[bpf_kfunc_call_test_ret_deep];"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm(bpf_kfunc_call_test_ret_deep)
+ : __clobber_all);
+}
+
SEC("tc")
__arch_x86_64 __arch_arm64
__failure __msg("R2 !read_ok")
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
index 850cf4f830c4..dff1ad6f491f 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
@@ -981,6 +981,41 @@ __bpf_kfunc struct prog_test_ret_ptr bpf_kfunc_call_test_ret_ptr(u64 tag)
return r;
}
+__bpf_kfunc struct prog_test_ret_nested bpf_kfunc_call_test_ret_nested(u64 tag)
+{
+ struct prog_test_ret_nested r = { .in = { .p = NULL }, .tag = tag };
+
+ return r;
+}
+
+__bpf_kfunc struct prog_test_ret_ptr_arr bpf_kfunc_call_test_ret_ptr_arr(void)
+{
+ struct prog_test_ret_ptr_arr r = { .p = { NULL, NULL } };
+
+ return r;
+}
+
+__bpf_kfunc struct prog_test_ret_arr_struct bpf_kfunc_call_test_ret_arr_struct(void)
+{
+ struct prog_test_ret_arr_struct r = {};
+
+ return r;
+}
+
+__bpf_kfunc struct prog_test_ret_arr2d bpf_kfunc_call_test_ret_arr2d(void)
+{
+ struct prog_test_ret_arr2d r = {};
+
+ return r;
+}
+
+__bpf_kfunc struct prog_test_ret_deep bpf_kfunc_call_test_ret_deep(u64 v)
+{
+ struct prog_test_ret_deep r = { .l1 = { .l2 = { .l3 = { .l4 = { .v = v } } } } };
+
+ return r;
+}
+
__bpf_kfunc struct prog_test_ret_ii bpf_kfunc_call_test_ret_ii(int a, int b)
{
struct prog_test_ret_ii r = { .a = a, .b = b };
@@ -1539,6 +1574,11 @@ BTF_ID_FLAGS(func, bpf_kfunc_call_test_i128)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_pair)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_fastcall, KF_FASTCALL)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_ptr)
+BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_nested)
+BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_ptr_arr)
+BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_arr_struct)
+BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_arr2d)
+BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_deep)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_ii)
#endif
BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_big)
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 65e693ada736..9202c31ba6c9 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
@@ -70,6 +70,43 @@ struct prog_test_ret_ptr { /* 16 bytes: contains a pointer */
__u64 tag;
};
+struct prog_test_ret_nested { /* 16 bytes: the pointer hides one level down */
+ struct {
+ void *p;
+ } in;
+ __u64 tag;
+};
+
+struct prog_test_ret_ptr_arr { /* 16 bytes: an array of pointers */
+ void *p[2];
+};
+
+struct prog_test_ret_arr_struct { /* 16 bytes: the pointer is under an array of structs */
+ struct {
+ struct {
+ void *p;
+ } in2;
+ } in1[2];
+};
+
+struct prog_test_ret_arr2d { /* 16 bytes: a two dimensional array of structs */
+ struct {
+ void *p;
+ } a[1][2];
+};
+
+struct prog_test_ret_deep { /* 8 bytes, but nested past the 4-level walk limit */
+ struct {
+ struct {
+ struct {
+ struct {
+ __u64 v;
+ } l4;
+ } l3;
+ } l2;
+ } l1;
+};
+
struct prog_test_ret_big { /* 24 bytes: too large for R0:R2 */
__u64 a;
__u64 b;
@@ -159,6 +196,11 @@ struct prog_test_ret_pair bpf_kfunc_call_test_ret_pair(__u64 a, __u64 b) __ksym;
struct prog_test_ret_pair bpf_kfunc_call_test_ret_fastcall(__u64 a, __u64 b) __ksym;
struct prog_test_ret_ii bpf_kfunc_call_test_ret_ii(int a, int b) __ksym;
struct prog_test_ret_ptr bpf_kfunc_call_test_ret_ptr(__u64 tag) __ksym;
+struct prog_test_ret_nested bpf_kfunc_call_test_ret_nested(__u64 tag) __ksym;
+struct prog_test_ret_ptr_arr bpf_kfunc_call_test_ret_ptr_arr(void) __ksym;
+struct prog_test_ret_arr_struct bpf_kfunc_call_test_ret_arr_struct(void) __ksym;
+struct prog_test_ret_arr2d bpf_kfunc_call_test_ret_arr2d(void) __ksym;
+struct prog_test_ret_deep bpf_kfunc_call_test_ret_deep(__u64 v) __ksym;
struct prog_test_ret_big bpf_kfunc_call_test_ret_big(void) __ksym;
__u64 bpf_kfunc_call_stack_arg(__u64 a, __u64 b, __u64 c, __u64 d,
__u64 e, __u64 f, __u64 g, __u64 h,
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH bpf-next v4 10/12] selftests/bpf: Test global functions returning arena pointers by value
2026-08-29 6:15 [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns Yonghong Song
` (8 preceding siblings ...)
2026-08-29 6:16 ` [PATCH bpf-next v4 09/12] selftests/bpf: Check the member named for an unsupported kfunc return type Yonghong Song
@ 2026-08-29 6:16 ` Yonghong Song
2026-08-29 6:16 ` [PATCH bpf-next v4 11/12] selftests/bpf: Test kfuncs " Yonghong Song
` (2 subsequent siblings)
12 siblings, 0 replies; 18+ messages in thread
From: Yonghong Song @ 2026-08-29 6:16 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
Cover the by-value struct returns a global function may now make: two
arena pointers filling R0:R2, an arena pointer beside a scalar, an array
of them, and an eight byte struct returned in R0 alone. The existing
cases for a struct and a union carrying a plain pointer stay rejected.
check_arena_struct_ret() also stores through both halves of the returned
pair and reads them back, so the test covers the returned pointers still
being usable as arena pointers rather than only the program verifying.
A toolchain guard goes into each file, and they differ because the
constructs do.
aggregate_ret_func.c uses "#if defined(__clang__)". Those cases are
__naked, so the function body is the inline asm and nothing else: the
clang compiler never lowers the return, and the 16 byte return type
reaches the verifier only through BTF. The minimum checked is LLVM 21.
gcc is excluded because it returns a by-value struct through a hidden
pointer and emits the 'r0 = r1' returning it after the __naked body's
exit, leaving the subprogram falling through.
verifier_arena.c uses "#if defined(__clang_major__) && __clang_major__ >=
23". arena_word_pair() returns the struct from C, so the compiler lowers
the return itself, and a 16 byte return only lands in R0:R2 with the
LLVM 23 BPF ABI. An older clang does not fall back to anything here, it
rejects the function with "aggregate returns are not supported", so the
floor is what lets the file build at all.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
.../selftests/bpf/progs/aggregate_ret_func.c | 146 ++++++++++++++++++
.../selftests/bpf/progs/verifier_arena.c | 48 ++++++
2 files changed, 194 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/aggregate_ret_func.c b/tools/testing/selftests/bpf/progs/aggregate_ret_func.c
index 6f66fc822ced..f0077d951a25 100644
--- a/tools/testing/selftests/bpf/progs/aggregate_ret_func.c
+++ b/tools/testing/selftests/bpf/progs/aggregate_ret_func.c
@@ -2,6 +2,7 @@
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
+#include <bpf_arena_common.h>
#include "bpf_misc.h"
typedef unsigned __int128 u128;
@@ -234,4 +235,149 @@ __naked int aggregate_ret_global_union_ptr_fail(void)
#endif
+/*
+ * gcc returns a by-value struct through a hidden pointer, and emits the
+ * 'r0 = r1' returning it after the __naked body's exit, leaving the
+ * subprogram falling through. Build these with clang only.
+ */
+#if defined(__clang__)
+
+struct arena_pair {
+ void __arena *lo;
+ void __arena *hi;
+};
+
+struct arena_and_scalar {
+ void __arena *p;
+ __u64 x;
+};
+
+struct arena_array {
+ void __arena *p[2];
+};
+
+struct arena_single {
+ void __arena *p;
+};
+
+union arena_upair {
+ void __arena *p;
+ __u64 halves[2];
+};
+
+__naked struct arena_pair global_ret_arena_pair(void)
+{
+ asm volatile (
+ "r0 = 0;"
+ "r2 = 0;"
+ "exit;"
+ );
+}
+
+SEC("tc")
+__load_if_JITed()
+__success __retval(0)
+__naked int aggregate_ret_global_arena_pair(void)
+{
+ asm volatile (
+ "call %[global_ret_arena_pair];"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm(global_ret_arena_pair)
+ : __clobber_all);
+}
+
+__naked struct arena_and_scalar global_ret_arena_and_scalar(void)
+{
+ asm volatile (
+ "r0 = 0;"
+ "r2 = 0;"
+ "exit;"
+ );
+}
+
+SEC("tc")
+__load_if_JITed()
+__success __retval(0)
+__naked int aggregate_ret_global_arena_and_scalar(void)
+{
+ asm volatile (
+ "call %[global_ret_arena_and_scalar];"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm(global_ret_arena_and_scalar)
+ : __clobber_all);
+}
+
+__naked struct arena_array global_ret_arena_array(void)
+{
+ asm volatile (
+ "r0 = 0;"
+ "r2 = 0;"
+ "exit;"
+ );
+}
+
+SEC("tc")
+__load_if_JITed()
+__success __retval(0)
+__naked int aggregate_ret_global_arena_array(void)
+{
+ asm volatile (
+ "call %[global_ret_arena_array];"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm(global_ret_arena_array)
+ : __clobber_all);
+}
+
+__naked struct arena_single global_ret_arena_single(void)
+{
+ asm volatile (
+ "r0 = 0;"
+ "exit;"
+ );
+}
+
+SEC("tc")
+__success __retval(0)
+__naked int aggregate_ret_global_arena_single(void)
+{
+ asm volatile (
+ "call %[global_ret_arena_single];"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm(global_ret_arena_single)
+ : __clobber_all);
+}
+
+__naked union arena_upair global_ret_arena_union(void)
+{
+ asm volatile (
+ "r0 = 0;"
+ "r2 = 0;"
+ "exit;"
+ );
+}
+
+SEC("tc")
+__load_if_JITed()
+__success __retval(0)
+__naked int aggregate_ret_global_arena_union(void)
+{
+ asm volatile (
+ "call %[global_ret_arena_union];"
+ "r0 = 0;"
+ "exit;"
+ :
+ : __imm(global_ret_arena_union)
+ : __clobber_all);
+}
+
+#endif
+
char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/progs/verifier_arena.c b/tools/testing/selftests/bpf/progs/verifier_arena.c
index 815f342eb4b0..332322c9b54c 100644
--- a/tools/testing/selftests/bpf/progs/verifier_arena.c
+++ b/tools/testing/selftests/bpf/progs/verifier_arena.c
@@ -734,4 +734,52 @@ int check_arena_arg_ret(void *ctx)
return 0;
}
+#if defined(__clang_major__) && __clang_major__ >= 23
+
+struct arena_word_pair {
+ u32 __arena *first;
+ u32 __arena *second;
+};
+
+__weak struct arena_word_pair arena_word_pair(u32 __arena *page)
+{
+ struct arena_word_pair pair;
+
+ pair.first = page;
+ pair.second = page + 1;
+
+ return pair;
+}
+
+SEC("syscall")
+__load_if_JITed()
+__success __retval(0)
+int check_arena_struct_ret(void *ctx)
+{
+ u32 __arena *page = bpf_arena_alloc_pages(&arena, NULL, 1, NUMA_NO_NODE, 0);
+ u32 volatile __arena *first, *second;
+ struct arena_word_pair pair;
+
+ if (!page)
+ return 1;
+
+ pair = arena_word_pair(page);
+ if (!pair.first || !pair.second)
+ return 2;
+
+ /* Both halves of the pair must still be usable as arena pointers. */
+ first = pair.first;
+ second = pair.second;
+ *first = 1;
+ *second = 2;
+ if (*first != 1)
+ return 3;
+ if (*second != 2)
+ return 4;
+
+ return 0;
+}
+
+#endif
+
char _license[] SEC("license") = "GPL";
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH bpf-next v4 11/12] selftests/bpf: Test kfuncs returning arena pointers by value
2026-08-29 6:15 [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns Yonghong Song
` (9 preceding siblings ...)
2026-08-29 6:16 ` [PATCH bpf-next v4 10/12] selftests/bpf: Test global functions returning arena pointers by value Yonghong Song
@ 2026-08-29 6:16 ` Yonghong Song
2026-08-29 6:16 ` [PATCH bpf-next v4 12/12] docs/bpf: Document arena pointers in a by-value return Yonghong Song
2026-08-30 1:20 ` [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns patchwork-bot+netdevbpf
12 siblings, 0 replies; 18+ messages in thread
From: Yonghong Song @ 2026-08-29 6:16 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
Cover the by-value struct returns a kfunc may now make: two arena
pointers filling R0:R2, and an arena pointer beside a scalar. Two further
cases drop the tag from one member of a struct and one arm of a union,
and stay rejected naming that member, so what decides is the tag rather
than the member being a pointer. The existing cases for a struct and a
nested struct carrying a plain pointer stay rejected as well.
These cases call the kfuncs from C, so the compiler lowers the by-value
return itself, and a struct or union only lands in R0:R2 with the LLVM 23
BPF ABI. An older clang, and gcc, return it through a hidden pointer in
R1 instead, which shifts the arguments along and fails verification. The
file is therefore guarded on LLVM 23, falling back to a dummy test.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
.../selftests/bpf/prog_tests/aggregate_ret.c | 42 ++++++
.../bpf/progs/aggregate_ret_kfunc_arena.c | 121 ++++++++++++++++++
.../selftests/bpf/test_kmods/bpf_testmod.c | 32 +++++
.../bpf/test_kmods/bpf_testmod_kfunc.h | 30 +++++
4 files changed, 225 insertions(+)
create mode 100644 tools/testing/selftests/bpf/progs/aggregate_ret_kfunc_arena.c
diff --git a/tools/testing/selftests/bpf/prog_tests/aggregate_ret.c b/tools/testing/selftests/bpf/prog_tests/aggregate_ret.c
index e0b94ed10f94..07d9d6e1d6b8 100644
--- a/tools/testing/selftests/bpf/prog_tests/aggregate_ret.c
+++ b/tools/testing/selftests/bpf/prog_tests/aggregate_ret.c
@@ -1,11 +1,53 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */
#include <test_progs.h>
+#include <bpf/btf.h>
#include "aggregate_ret_func.skel.h"
#include "aggregate_ret_kfunc.skel.h"
+#include "aggregate_ret_kfunc_arena.skel.h"
+
+static bool testmod_has_arena_tagged_member(void)
+{
+ struct btf *vmlinux_btf, *module_btf = NULL;
+ const struct btf_type *t;
+ bool tagged = false;
+ __s32 id;
+
+ vmlinux_btf = btf__load_vmlinux_btf();
+ if (!vmlinux_btf)
+ return false;
+
+ module_btf = btf__load_module_btf("bpf_testmod", vmlinux_btf);
+ if (!module_btf)
+ goto out;
+
+ /* prog_test_ret_arena::a is 'void __arena_tag *': PTR -> TYPE_TAG -> void */
+ id = btf__find_by_name_kind(module_btf, "prog_test_ret_arena", BTF_KIND_STRUCT);
+ if (id <= 0)
+ goto out;
+
+ t = btf__type_by_id(module_btf, btf_members(btf__type_by_id(module_btf, id))[0].type);
+ if (!t || !btf_is_ptr(t))
+ goto out;
+
+ t = btf__type_by_id(module_btf, t->type);
+ tagged = t && btf_is_type_tag(t) &&
+ !strcmp(btf__name_by_offset(module_btf, t->name_off), "arena");
+
+out:
+ btf__free(module_btf);
+ btf__free(vmlinux_btf);
+
+ return tagged;
+}
void test_aggregate_ret(void)
{
RUN_TESTS(aggregate_ret_func);
RUN_TESTS(aggregate_ret_kfunc);
+
+ if (testmod_has_arena_tagged_member())
+ RUN_TESTS(aggregate_ret_kfunc_arena);
+ else
+ test__skip();
}
diff --git a/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc_arena.c b/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc_arena.c
new file mode 100644
index 000000000000..a0f263694a63
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/aggregate_ret_kfunc_arena.c
@@ -0,0 +1,121 @@
+// 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_kfunc.h"
+
+#if defined(__clang_major__) && __clang_major__ >= 23
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ __uint(max_entries, 2);
+} arena SEC(".maps");
+
+SEC("syscall")
+__arch_x86_64 __arch_arm64
+__load_if_JITed()
+__success __retval(0)
+int aggregate_ret_kfunc_arena(void *ctx)
+{
+ u32 volatile __arena *page = bpf_arena_alloc_pages(&arena, NULL, 1, NUMA_NO_NODE, 0);
+ u32 volatile __arena *a, *b;
+ struct prog_test_ret_arena r;
+
+ if (!page)
+ return 1;
+
+ /* Both halves come back in R0:R2, pointing at page and page + 4. */
+ r = bpf_kfunc_call_test_ret_arena((u64)page);
+ if (!r.a || !r.b)
+ return 2;
+
+ a = (u32 __arena *)r.a;
+ b = (u32 __arena *)r.b;
+ *a = 1;
+ *b = 2;
+ if (*a != 1)
+ return 3;
+ if (*b != 2)
+ return 4;
+
+ page[0] = 7;
+ if (*a != 7)
+ return 5;
+ page[1] = 9;
+ if (*b != 9)
+ return 6;
+
+ return 0;
+}
+
+SEC("syscall")
+__arch_x86_64 __arch_arm64
+__load_if_JITed()
+__success __retval(0)
+int aggregate_ret_kfunc_arena_mixed(void *ctx)
+{
+ u32 __arena *page = bpf_arena_alloc_pages(&arena, NULL, 1, NUMA_NO_NODE, 0);
+ struct prog_test_ret_arena_mixed r;
+ u32 volatile __arena *p;
+
+ if (!page)
+ return 1;
+
+ /* An arena pointer in R0 beside a scalar in R2. */
+ r = bpf_kfunc_call_test_ret_arena_mixed((u64)page);
+ if (!r.p)
+ return 2;
+ if (r.tag != 0xbeef)
+ return 3;
+
+ p = (u32 __arena *)r.p;
+ *p = 3;
+ if (*p != 3)
+ return 4;
+
+ return 0;
+}
+
+SEC("syscall")
+__arch_x86_64 __arch_arm64
+__failure __msg("is not composed of scalars or arena pointers")
+__msg("member 'b' has type PTR")
+int aggregate_ret_kfunc_arena_untagged_fail(void *ctx)
+{
+ struct prog_test_ret_arena_untagged r;
+
+ r = bpf_kfunc_call_test_ret_arena_untagged(0);
+
+ return r.a == r.b;
+}
+
+SEC("syscall")
+__arch_x86_64 __arch_arm64
+__failure __msg("is not composed of scalars or arena pointers")
+__msg("member 'b' has type PTR")
+int aggregate_ret_kfunc_arena_union_fail(void *ctx)
+{
+ union prog_test_ret_arena_union r;
+
+ r = bpf_kfunc_call_test_ret_arena_union(0);
+
+ return r.a == r.b;
+}
+
+#else
+
+SEC("socket")
+__description("aggregate_ret_kfunc_arena: needs LLVM 23, dummy test")
+__skip("needs LLVM 23")
+__success
+int dummy_test(void)
+{
+ return 0;
+}
+
+#endif
+
+char _license[] SEC("license") = "GPL";
diff --git a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
index dff1ad6f491f..2380b6cbdead 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
@@ -981,6 +981,34 @@ __bpf_kfunc struct prog_test_ret_ptr bpf_kfunc_call_test_ret_ptr(u64 tag)
return r;
}
+__bpf_kfunc struct prog_test_ret_arena bpf_kfunc_call_test_ret_arena(u64 addr)
+{
+ struct prog_test_ret_arena r = { .a = (void *)addr, .b = (void *)(addr + 4) };
+
+ return r;
+}
+
+__bpf_kfunc struct prog_test_ret_arena_mixed bpf_kfunc_call_test_ret_arena_mixed(u64 addr)
+{
+ struct prog_test_ret_arena_mixed r = { .p = (void *)addr, .tag = 0xbeef };
+
+ return r;
+}
+
+__bpf_kfunc struct prog_test_ret_arena_untagged bpf_kfunc_call_test_ret_arena_untagged(u64 addr)
+{
+ struct prog_test_ret_arena_untagged r = { .a = (void *)addr, .b = NULL };
+
+ return r;
+}
+
+__bpf_kfunc union prog_test_ret_arena_union bpf_kfunc_call_test_ret_arena_union(u64 addr)
+{
+ union prog_test_ret_arena_union r = { .a = (void *)addr };
+
+ return r;
+}
+
__bpf_kfunc struct prog_test_ret_nested bpf_kfunc_call_test_ret_nested(u64 tag)
{
struct prog_test_ret_nested r = { .in = { .p = NULL }, .tag = tag };
@@ -1574,6 +1602,10 @@ BTF_ID_FLAGS(func, bpf_kfunc_call_test_i128)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_pair)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_fastcall, KF_FASTCALL)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_ptr)
+BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_arena)
+BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_arena_mixed)
+BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_arena_untagged)
+BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_arena_union)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_nested)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_ptr_arr)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_ret_arr_struct)
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 9202c31ba6c9..b213ef14848b 100644
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
@@ -26,6 +26,12 @@ struct prog_test_ref_kfunc {
};
#endif
+#if __has_attribute(btf_type_tag)
+#define __arena_tag __attribute__((btf_type_tag("arena")))
+#else
+#define __arena_tag
+#endif
+
struct bpf_iter_testmod_seq;
struct prog_test_pass1 {
@@ -70,6 +76,26 @@ struct prog_test_ret_ptr { /* 16 bytes: contains a pointer */
__u64 tag;
};
+struct prog_test_ret_arena { /* 16 bytes: two arena pointers */
+ void __arena_tag *a;
+ void __arena_tag *b;
+};
+
+struct prog_test_ret_arena_mixed { /* 16 bytes: an arena pointer and a scalar */
+ void __arena_tag *p;
+ __u64 tag;
+};
+
+struct prog_test_ret_arena_untagged { /* 16 bytes: 'b' lacks the arena tag */
+ void __arena_tag *a;
+ void *b;
+};
+
+union prog_test_ret_arena_union { /* 8 bytes: 'b' lacks the arena tag */
+ void __arena_tag *a;
+ void *b;
+};
+
struct prog_test_ret_nested { /* 16 bytes: the pointer hides one level down */
struct {
void *p;
@@ -200,6 +226,10 @@ struct prog_test_ret_nested bpf_kfunc_call_test_ret_nested(__u64 tag) __ksym;
struct prog_test_ret_ptr_arr bpf_kfunc_call_test_ret_ptr_arr(void) __ksym;
struct prog_test_ret_arr_struct bpf_kfunc_call_test_ret_arr_struct(void) __ksym;
struct prog_test_ret_arr2d bpf_kfunc_call_test_ret_arr2d(void) __ksym;
+struct prog_test_ret_arena bpf_kfunc_call_test_ret_arena(__u64 addr) __ksym;
+struct prog_test_ret_arena_mixed bpf_kfunc_call_test_ret_arena_mixed(__u64 addr) __ksym;
+struct prog_test_ret_arena_untagged bpf_kfunc_call_test_ret_arena_untagged(__u64 addr) __ksym;
+union prog_test_ret_arena_union bpf_kfunc_call_test_ret_arena_union(__u64 addr) __ksym;
struct prog_test_ret_deep bpf_kfunc_call_test_ret_deep(__u64 v) __ksym;
struct prog_test_ret_big bpf_kfunc_call_test_ret_big(void) __ksym;
__u64 bpf_kfunc_call_stack_arg(__u64 a, __u64 b, __u64 c, __u64 d,
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 18+ messages in thread* [PATCH bpf-next v4 12/12] docs/bpf: Document arena pointers in a by-value return
2026-08-29 6:15 [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns Yonghong Song
` (10 preceding siblings ...)
2026-08-29 6:16 ` [PATCH bpf-next v4 11/12] selftests/bpf: Test kfuncs " Yonghong Song
@ 2026-08-29 6:16 ` Yonghong Song
2026-08-30 1:33 ` Kumar Kartikeya Dwivedi
2026-08-30 1:20 ` [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns patchwork-bot+netdevbpf
12 siblings, 1 reply; 18+ messages in thread
From: Yonghong Song @ 2026-08-29 6:16 UTC (permalink / raw)
To: bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
Section 2.9 describes the by-value return contract as scalars only, which
no longer holds: a kfunc and a global subprogram may now return a struct
or union whose members are scalars or arena pointers. Update it.
Note that an arena pointer member is handed back as a scalar like every
other member. That costs nothing: the program has to cast_kern() the value
before it can be used, and the verifier allows that cast on any scalar, so
the member gives the program no reach it did not already have.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
Documentation/bpf/kfuncs.rst | 39 ++++++++++++++++++++++--------------
1 file changed, 24 insertions(+), 15 deletions(-)
diff --git a/Documentation/bpf/kfuncs.rst b/Documentation/bpf/kfuncs.rst
index 89dea6b0b024..ebe37c1fe9d2 100644
--- a/Documentation/bpf/kfuncs.rst
+++ b/Documentation/bpf/kfuncs.rst
@@ -581,20 +581,29 @@ against the arena. Larger accesses must verify the range explicitly.
A kfunc may return a scalar, a pointer, or a small struct or union by
value. A scalar or pointer of up to 8 bytes is returned in R0, as usual.
-A struct or union returned by value must be composed only of scalars
-(recursively), where a scalar is an integer or an enum; arrays of scalars are
-allowed as members. Its bytes are handed back to the program as the raw
-contents of R0 (and R2), so a pointer field would be laundered into a scalar
-and escape the verifier's pointer provenance and reference tracking. A struct
-or union with a pointer member is therefore rejected at load time, and so is
-one with a floating-point member, which the ABI may not return in R0:R2 at
+A struct or union returned by value must be composed only of scalars and arena
+pointers, where a scalar is an integer or an enum and an arena pointer is one
+carrying the ``btf_type_tag("arena")`` attribute. Those may be nested in
+structs and unions and in arrays of any number of dimensions, in any
+combination, as long as what the nesting bottoms out in is a scalar or an arena
+pointer. Its bytes are handed back to the program as the raw contents of R0
+(and R2), so a member of any other pointer type would be laundered into a
+scalar and escape the verifier's pointer provenance and reference tracking. A
+struct or union with such a member is therefore rejected at load time, and so
+is one with a floating-point member, which the ABI may not return in R0:R2 at
all.
+An arena pointer member is handed back as a scalar too, but nothing is lost by
+that. The program must ``cast_kern()`` the value before it can be used, and the
+verifier allows that cast on any scalar, so a laundered arena address gives the
+program no reach it did not already have. The result is confined to the
+program's arena in either case.
+
A kfunc may also return a value larger than 8 bytes and up to 16 bytes -- a
-scalar-only struct or union, or an ``__int128``. Such a value is returned
-in the register pair R0:R2, matching the convention LLVM uses for the BPF
-target: the first 8 bytes in R0 and the second 8 bytes in R2. A struct or
-union of 8 bytes or less is returned in R0 alone.
+struct or union of scalars and arena pointers, or an ``__int128``. Such a
+value is returned in the register pair R0:R2, matching the convention LLVM
+uses for the BPF target: the first 8 bytes in R0 and the second 8 bytes in R2.
+A struct or union of 8 bytes or less is returned in R0 alone.
::
@@ -620,10 +629,10 @@ used when the program is JITed, since the interpreter propagates only R0 out of
a subprogram: without a JIT the return value stays in R0 alone, and a caller
reading R2 is rejected for reading an uninitialized register. A global
subprogram is verified in isolation, so its by-value struct or union return is
-restricted to scalars just like a kfunc's; a static subprogram is verified
-inline and has no such restriction. The main program is not covered: its return
-value is the program's exit code, read out of R0 alone, so a declared upper
-half is never looked at.
+restricted to scalars and arena pointers just like a kfunc's; a static
+subprogram is verified inline and has no such restriction. The main program is
+not covered: its return value is the program's exit code, read out of R0
+alone, so a declared upper half is never looked at.
A global subprogram must leave a scalar in *every* register of the pair, so
both halves of the returned value have to be assigned. Leaving the upper half
--
2.53.0-Meta
^ permalink raw reply related [flat|nested] 18+ messages in thread* Re: [PATCH bpf-next v4 12/12] docs/bpf: Document arena pointers in a by-value return
2026-08-29 6:16 ` [PATCH bpf-next v4 12/12] docs/bpf: Document arena pointers in a by-value return Yonghong Song
@ 2026-08-30 1:33 ` Kumar Kartikeya Dwivedi
2026-08-31 2:42 ` Yonghong Song
0 siblings, 1 reply; 18+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-08-30 1:33 UTC (permalink / raw)
To: Yonghong Song, bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
On Sat Aug 29, 2026 at 8:16 AM CEST, Yonghong Song wrote:
> Section 2.9 describes the by-value return contract as scalars only, which
> no longer holds: a kfunc and a global subprogram may now return a struct
> or union whose members are scalars or arena pointers. Update it.
>
> Note that an arena pointer member is handed back as a scalar like every
> other member. That costs nothing: the program has to cast_kern() the value
> before it can be used, and the verifier allows that cast on any scalar, so
> the member gives the program no reach it did not already have.
>
> Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
> ---
> Documentation/bpf/kfuncs.rst | 39 ++++++++++++++++++++++--------------
> 1 file changed, 24 insertions(+), 15 deletions(-)
>
> diff --git a/Documentation/bpf/kfuncs.rst b/Documentation/bpf/kfuncs.rst
> index 89dea6b0b024..ebe37c1fe9d2 100644
> --- a/Documentation/bpf/kfuncs.rst
> +++ b/Documentation/bpf/kfuncs.rst
> @@ -581,20 +581,29 @@ against the arena. Larger accesses must verify the range explicitly.
> A kfunc may return a scalar, a pointer, or a small struct or union by
> value. A scalar or pointer of up to 8 bytes is returned in R0, as usual.
>
> -A struct or union returned by value must be composed only of scalars
> -(recursively), where a scalar is an integer or an enum; arrays of scalars are
> -allowed as members. Its bytes are handed back to the program as the raw
> -contents of R0 (and R2), so a pointer field would be laundered into a scalar
> -and escape the verifier's pointer provenance and reference tracking. A struct
> -or union with a pointer member is therefore rejected at load time, and so is
> -one with a floating-point member, which the ABI may not return in R0:R2 at
> +A struct or union returned by value must be composed only of scalars and arena
> +pointers, where a scalar is an integer or an enum and an arena pointer is one
> +carrying the ``btf_type_tag("arena")`` attribute. Those may be nested in
> +structs and unions and in arrays of any number of dimensions, in any
> +combination, as long as what the nesting bottoms out in is a scalar or an arena
> +pointer. Its bytes are handed back to the program as the raw contents of R0
> +(and R2), so a member of any other pointer type would be laundered into a
> +scalar and escape the verifier's pointer provenance and reference tracking. A
> +struct or union with such a member is therefore rejected at load time, and so
> +is one with a floating-point member, which the ABI may not return in R0:R2 at
> all.
>
> +An arena pointer member is handed back as a scalar too, but nothing is lost by
> +that. The program must ``cast_kern()`` the value before it can be used, and the
> +verifier allows that cast on any scalar, so a laundered arena address gives the
> +program no reach it did not already have. The result is confined to the
> +program's arena in either case.
> +
I think it would make more sense to support translation for returned arena
pointers as well, i.e. KF_ARENA_RET + the case you describe above. Like, anyone
who would use this tag in a struct being returned to the user would probably we
working with the kernel pointer into the arena.
I think permitting them in the verifier is a good first step, but it isn't all
that useful unless the translation is supported as well. Otherwise, the kfunc
has to do it manually, which is a bit of a pain, esp. without access to the
program's arena's base addresses, which isn't always easily possible across
various program types.
I can look into doing this in case you don't have cycles, but overall should be
fairly simple to plumb support.
> [...]
^ permalink raw reply [flat|nested] 18+ messages in thread* Re: [PATCH bpf-next v4 12/12] docs/bpf: Document arena pointers in a by-value return
2026-08-30 1:33 ` Kumar Kartikeya Dwivedi
@ 2026-08-31 2:42 ` Yonghong Song
0 siblings, 0 replies; 18+ messages in thread
From: Yonghong Song @ 2026-08-31 2:42 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi, bpf
Cc: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, kernel-team
On 8/29/26 6:33 PM, Kumar Kartikeya Dwivedi wrote:
> On Sat Aug 29, 2026 at 8:16 AM CEST, Yonghong Song wrote:
>> Section 2.9 describes the by-value return contract as scalars only, which
>> no longer holds: a kfunc and a global subprogram may now return a struct
>> or union whose members are scalars or arena pointers. Update it.
>>
>> Note that an arena pointer member is handed back as a scalar like every
>> other member. That costs nothing: the program has to cast_kern() the value
>> before it can be used, and the verifier allows that cast on any scalar, so
>> the member gives the program no reach it did not already have.
>>
>> Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
>> ---
>> Documentation/bpf/kfuncs.rst | 39 ++++++++++++++++++++++--------------
>> 1 file changed, 24 insertions(+), 15 deletions(-)
>>
>> diff --git a/Documentation/bpf/kfuncs.rst b/Documentation/bpf/kfuncs.rst
>> index 89dea6b0b024..ebe37c1fe9d2 100644
>> --- a/Documentation/bpf/kfuncs.rst
>> +++ b/Documentation/bpf/kfuncs.rst
>> @@ -581,20 +581,29 @@ against the arena. Larger accesses must verify the range explicitly.
>> A kfunc may return a scalar, a pointer, or a small struct or union by
>> value. A scalar or pointer of up to 8 bytes is returned in R0, as usual.
>>
>> -A struct or union returned by value must be composed only of scalars
>> -(recursively), where a scalar is an integer or an enum; arrays of scalars are
>> -allowed as members. Its bytes are handed back to the program as the raw
>> -contents of R0 (and R2), so a pointer field would be laundered into a scalar
>> -and escape the verifier's pointer provenance and reference tracking. A struct
>> -or union with a pointer member is therefore rejected at load time, and so is
>> -one with a floating-point member, which the ABI may not return in R0:R2 at
>> +A struct or union returned by value must be composed only of scalars and arena
>> +pointers, where a scalar is an integer or an enum and an arena pointer is one
>> +carrying the ``btf_type_tag("arena")`` attribute. Those may be nested in
>> +structs and unions and in arrays of any number of dimensions, in any
>> +combination, as long as what the nesting bottoms out in is a scalar or an arena
>> +pointer. Its bytes are handed back to the program as the raw contents of R0
>> +(and R2), so a member of any other pointer type would be laundered into a
>> +scalar and escape the verifier's pointer provenance and reference tracking. A
>> +struct or union with such a member is therefore rejected at load time, and so
>> +is one with a floating-point member, which the ABI may not return in R0:R2 at
>> all.
>>
>> +An arena pointer member is handed back as a scalar too, but nothing is lost by
>> +that. The program must ``cast_kern()`` the value before it can be used, and the
>> +verifier allows that cast on any scalar, so a laundered arena address gives the
>> +program no reach it did not already have. The result is confined to the
>> +program's arena in either case.
>> +
> I think it would make more sense to support translation for returned arena
> pointers as well, i.e. KF_ARENA_RET + the case you describe above. Like, anyone
> who would use this tag in a struct being returned to the user would probably we
> working with the kernel pointer into the arena.
>
> I think permitting them in the verifier is a good first step, but it isn't all
> that useful unless the translation is supported as well. Otherwise, the kfunc
> has to do it manually, which is a bit of a pain, esp. without access to the
> program's arena's base addresses, which isn't always easily possible across
> various program types.
Probably you mean something like below for input argument 'addr'.
+__bpf_kfunc struct prog_test_ret_arena bpf_kfunc_call_test_ret_arena(u64 addr)
+{
+ struct prog_test_ret_arena r = { .a = (void *)addr, .b = (void *)(addr + 4) };
+
+ return r;
+}
Yes, if kfunc itself wants to do something (esp. dereference of the addr), it cannot
do it.
>
> I can look into doing this in case you don't have cycles, but overall should be
> fairly simple to plumb support.
Sure. Please do it. Thanks!
>
>> [...]
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns
2026-08-29 6:15 [PATCH bpf-next v4 00/12] bpf: Allow arena pointers in by-value returns Yonghong Song
` (11 preceding siblings ...)
2026-08-29 6:16 ` [PATCH bpf-next v4 12/12] docs/bpf: Document arena pointers in a by-value return Yonghong Song
@ 2026-08-30 1:20 ` patchwork-bot+netdevbpf
12 siblings, 0 replies; 18+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-08-30 1:20 UTC (permalink / raw)
To: Yonghong Song; +Cc: bpf, ast, andrii, daniel, eddyz87, kernel-team
Hello:
This series was applied to bpf/bpf-next.git (master)
by Alexei Starovoitov <ast@kernel.org>:
On Fri, 28 Aug 2026 23:15:14 -0700 you wrote:
> A function returning a struct by value may only return one whose members
> are all scalars. That is stricter than it needs to be: an arena pointer
> is safe to hand over as raw register bits, and both a global function
> and a kfunc can already return one on its own.
>
> This patch set allows returning arena pointer(s) (as member(s) of a
> struct) for global functions and kfuncs. Any other pointer member stays
> rejected, as it would be laundered into a scalar and escape provenance
> and reference tracking.
>
> [...]
Here is the summary with links:
- [bpf-next,v4,01/12] bpf: Record each half of a paired return value in verifier diagnostics
https://git.kernel.org/bpf/bpf-next/c/f62a9b82700c
- [bpf-next,v4,02/12] bpf: Drop the recursion depth argument of btf_type_is_scalar_struct()
https://git.kernel.org/bpf/bpf-next/c/77983371742e
- [bpf-next,v4,03/12] bpf: Add btf_type_is_arena_ptr()
https://git.kernel.org/bpf/bpf-next/c/3fc3e7331af6
- [bpf-next,v4,04/12] bpf: Let the by-value struct walk take the kinds of member it accepts
https://git.kernel.org/bpf/bpf-next/c/8d7ea1b752e5
- [bpf-next,v4,05/12] bpf: Let a by-value struct nest arrays and structs freely
https://git.kernel.org/bpf/bpf-next/c/9e0195cd1d31
- [bpf-next,v4,06/12] bpf: Report which member makes a kfunc return type unsupported
https://git.kernel.org/bpf/bpf-next/c/282d07dc6c74
- [bpf-next,v4,07/12] bpf: Allow a global function to return arena pointers by value
https://git.kernel.org/bpf/bpf-next/c/ff08e6b79ee1
- [bpf-next,v4,08/12] bpf: Allow arena pointers in a by-value kfunc return
https://git.kernel.org/bpf/bpf-next/c/9046109974dd
- [bpf-next,v4,09/12] selftests/bpf: Check the member named for an unsupported kfunc return type
https://git.kernel.org/bpf/bpf-next/c/cc03706d92c9
- [bpf-next,v4,10/12] selftests/bpf: Test global functions returning arena pointers by value
https://git.kernel.org/bpf/bpf-next/c/18bc61838842
- [bpf-next,v4,11/12] selftests/bpf: Test kfuncs returning arena pointers by value
https://git.kernel.org/bpf/bpf-next/c/f7213961e8e2
- [bpf-next,v4,12/12] docs/bpf: Document arena pointers in a by-value return
https://git.kernel.org/bpf/bpf-next/c/25b0d73a7742
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply [flat|nested] 18+ messages in thread