* [PATCH bpf-next v2 01/11] bpf: Set up callee state outside of setup_func_entry()
2026-10-09 20:27 [PATCH bpf-next v2 00/11] bpf: Scope callback arguments to their frame Ihor Solodrai
@ 2026-10-09 20:27 ` Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 02/11] bpf: Pass the owning verifier state to callback setters Ihor Solodrai
` (9 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ihor Solodrai @ 2026-10-09 20:27 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
Callback argument setup may need to attach bookkeeping to the verifier
state owning the callee frame. The frame should be current in that
state before the arguments are set up.
For sync callbacks, the setter runs inside setup_func_entry() before
the new frame becomes current. Async callback setters already run on a
complete state, directly from push_callback_call().
Move sync callback argument setup to push_callback_call() after the
frame is in place. Leave setup_func_entry() responsible for pushing the
frame, and copy static-call arguments directly.
No functional change.
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
kernel/bpf/verifier.c | 69 +++++++++++++------------------------------
1 file changed, 20 insertions(+), 49 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 353bde9ae227..a0310e093880 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -10821,32 +10821,25 @@ typedef int (*set_callee_state_fn)(struct bpf_verifier_env *env,
struct bpf_func_state *callee,
int insn_idx);
-static int set_callee_state(struct bpf_verifier_env *env,
- struct bpf_func_state *caller,
- struct bpf_func_state *callee, int insn_idx);
-
-static int setup_func_entry(struct bpf_verifier_env *env, int subprog, int callsite,
- set_callee_state_fn set_callee_state_cb,
- struct bpf_verifier_state *state)
+static struct bpf_func_state *setup_func_entry(struct bpf_verifier_env *env, int subprog,
+ int callsite, struct bpf_verifier_state *state)
{
- struct bpf_func_state *caller, *callee;
- int err;
+ struct bpf_func_state *callee;
if (state->curframe + 1 >= MAX_CALL_FRAMES) {
verbose(env, "the call stack of %d frames is too deep\n",
state->curframe + 2);
- return -E2BIG;
+ return ERR_PTR(-E2BIG);
}
if (state->frame[state->curframe + 1]) {
verifier_bug(env, "Frame %d already allocated", state->curframe + 1);
- return -EFAULT;
+ return ERR_PTR(-EFAULT);
}
- caller = state->frame[state->curframe];
callee = kzalloc_obj(*callee, GFP_KERNEL_ACCOUNT);
if (!callee)
- return -ENOMEM;
+ return ERR_PTR(-ENOMEM);
state->frame[state->curframe + 1] = callee;
/* callee cannot access r0, r6 - r9 for reading and has to write
@@ -10858,19 +10851,9 @@ static int setup_func_entry(struct bpf_verifier_env *env, int subprog, int calls
callsite,
state->curframe + 1 /* frameno within this callchain */,
subprog /* subprog number within this prog */);
- err = set_callee_state_cb(env, caller, callee, callsite);
- if (err)
- goto err_out;
-
- /* only increment it after check_reg_arg() finished */
state->curframe++;
- return 0;
-
-err_out:
- free_func_state(callee);
- state->frame[state->curframe + 1] = NULL;
- return err;
+ return callee;
}
static void gen_subprog_arg_proto(const struct bpf_subprog_info *sub, const struct btf *btf,
@@ -10990,10 +10973,6 @@ static int push_callback_call(struct bpf_verifier_env *env, struct bpf_insn *ins
if (err == -EFAULT)
return err;
- /* set_callee_state is used for direct subprog calls, but we are
- * interested in validating only BPF helpers that can call subprogs as
- * callbacks
- */
env->subprog_info[subprog].is_cb = true;
if (bpf_pseudo_kfunc_call(insn) &&
!is_callback_calling_kfunc(insn->imm)) {
@@ -11044,8 +11023,11 @@ static int push_callback_call(struct bpf_verifier_env *env, struct bpf_insn *ins
if (IS_ERR(callback_state))
return PTR_ERR(callback_state);
- err = setup_func_entry(env, subprog, insn_idx, set_callee_state_cb,
- callback_state);
+ callee = setup_func_entry(env, subprog, insn_idx, callback_state);
+ if (IS_ERR(callee))
+ return PTR_ERR(callee);
+
+ err = set_callee_state_cb(env, caller, callee, insn_idx);
if (err)
return err;
@@ -11069,8 +11051,8 @@ static int check_static_func_call(struct bpf_verifier_env *env, int subprog,
struct bpf_verifier_state *state = env->cur_state;
struct bpf_subprog_info *caller_info;
u16 callee_incoming, stack_arg_cnt;
- struct bpf_func_state *caller;
- int err;
+ struct bpf_func_state *caller, *callee;
+ int i;
caller = state->frame[state->curframe];
@@ -11088,9 +11070,12 @@ static int check_static_func_call(struct bpf_verifier_env *env, int subprog,
* For regular function entry setup new frame and continue
* from that frame.
*/
- err = setup_func_entry(env, subprog, *insn_idx, set_callee_state, state);
- if (err)
- return err;
+ callee = setup_func_entry(env, subprog, *insn_idx, state);
+ if (IS_ERR(callee))
+ return PTR_ERR(callee);
+
+ for (i = BPF_REG_1; i <= BPF_REG_5; i++)
+ callee->regs[i] = caller->regs[i];
bpf_diag_record_scrub(env, &caller->regs[BPF_REG_0], BPF_DIAG_MOD_CALLER_SAVED);
clear_caller_saved_regs(env, caller->regs);
@@ -11301,20 +11286,6 @@ int map_set_for_each_callback_args(struct bpf_verifier_env *env,
return 0;
}
-static int set_callee_state(struct bpf_verifier_env *env,
- struct bpf_func_state *caller,
- struct bpf_func_state *callee, int insn_idx)
-{
- int i;
-
- /* copy r1 - r5 args that callee can access. The copy includes parent
- * pointers, which connects us up to the liveness chain
- */
- for (i = BPF_REG_1; i <= BPF_REG_5; i++)
- callee->regs[i] = caller->regs[i];
- return 0;
-}
-
static int set_map_elem_callback_state(struct bpf_verifier_env *env,
struct bpf_func_state *caller,
struct bpf_func_state *callee,
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH bpf-next v2 02/11] bpf: Pass the owning verifier state to callback setters
2026-10-09 20:27 [PATCH bpf-next v2 00/11] bpf: Scope callback arguments to their frame Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 01/11] bpf: Set up callee state outside of setup_func_entry() Ihor Solodrai
@ 2026-10-09 20:27 ` Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 03/11] bpf: Append complete reference states Ihor Solodrai
` (8 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ihor Solodrai @ 2026-10-09 20:27 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
Callback setters receive the caller and callee frames, but not the
verifier state that owns the callee. Synchronous callbacks use a queued
state and asynchronous callbacks use a separate state, neither of which
is env->cur_state.
Callback-local bookkeeping needs to be recorded in that owning state.
Pass it through the callback setters and the map callback setup hook.
No functional change.
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
include/linux/bpf.h | 3 +++
kernel/bpf/verifier.c | 16 +++++++++++++---
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index 54144372281c..d5de9d49a168 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -39,6 +39,7 @@
#include <asm/rqspinlock.h>
struct bpf_verifier_env;
+struct bpf_verifier_state;
struct bpf_verifier_log;
struct perf_event;
struct bpf_prog;
@@ -178,6 +179,7 @@ struct bpf_map_ops {
int (*map_set_for_each_callback_args)(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *state,
struct bpf_func_state *caller,
struct bpf_func_state *callee);
long (*map_for_each_callback)(struct bpf_map *map,
@@ -3183,6 +3185,7 @@ int bpf_iter_map_fill_link_info(const struct bpf_iter_aux_info *aux,
struct bpf_link_info *info);
int map_set_for_each_callback_args(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *state,
struct bpf_func_state *caller,
struct bpf_func_state *callee);
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index a0310e093880..8d6812fee0c9 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -10817,6 +10817,7 @@ static void invalidate_outgoing_stack_args(struct bpf_verifier_env *env,
}
typedef int (*set_callee_state_fn)(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *state,
struct bpf_func_state *caller,
struct bpf_func_state *callee,
int insn_idx);
@@ -11000,7 +11001,7 @@ static int push_callback_call(struct bpf_verifier_env *env, struct bpf_insn *ins
callee->async_entry_cnt = state->frame[0]->async_entry_cnt + 1;
/* Convert bpf_timer_set_callback() args into timer callback args */
- err = set_callee_state_cb(env, caller, callee, insn_idx);
+ err = set_callee_state_cb(env, async_cb, caller, callee, insn_idx);
if (err)
return err;
@@ -11027,7 +11028,7 @@ static int push_callback_call(struct bpf_verifier_env *env, struct bpf_insn *ins
if (IS_ERR(callee))
return PTR_ERR(callee);
- err = set_callee_state_cb(env, caller, callee, insn_idx);
+ err = set_callee_state_cb(env, callback_state, caller, callee, insn_idx);
if (err)
return err;
@@ -11257,6 +11258,7 @@ static int check_func_callx(struct bpf_verifier_env *env, struct bpf_insn *insn,
}
int map_set_for_each_callback_args(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *state,
struct bpf_func_state *caller,
struct bpf_func_state *callee)
{
@@ -11287,6 +11289,7 @@ int map_set_for_each_callback_args(struct bpf_verifier_env *env,
}
static int set_map_elem_callback_state(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *state,
struct bpf_func_state *caller,
struct bpf_func_state *callee,
int insn_idx)
@@ -11303,7 +11306,7 @@ static int set_map_elem_callback_state(struct bpf_verifier_env *env,
return -ENOTSUPP;
}
- err = map->ops->map_set_for_each_callback_args(env, caller, callee);
+ err = map->ops->map_set_for_each_callback_args(env, state, caller, callee);
if (err)
return err;
@@ -11313,6 +11316,7 @@ static int set_map_elem_callback_state(struct bpf_verifier_env *env,
}
static int set_loop_callback_state(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *state,
struct bpf_func_state *caller,
struct bpf_func_state *callee,
int insn_idx)
@@ -11335,6 +11339,7 @@ static int set_loop_callback_state(struct bpf_verifier_env *env,
}
static int set_timer_callback_state(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *state,
struct bpf_func_state *caller,
struct bpf_func_state *callee,
int insn_idx)
@@ -11370,6 +11375,7 @@ static int set_timer_callback_state(struct bpf_verifier_env *env,
}
static int set_find_vma_callback_state(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *state,
struct bpf_func_state *caller,
struct bpf_func_state *callee,
int insn_idx)
@@ -11398,6 +11404,7 @@ static int set_find_vma_callback_state(struct bpf_verifier_env *env,
}
static int set_user_ringbuf_callback_state(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *state,
struct bpf_func_state *caller,
struct bpf_func_state *callee,
int insn_idx)
@@ -11421,6 +11428,7 @@ static int set_user_ringbuf_callback_state(struct bpf_verifier_env *env,
}
static int set_rbtree_add_callback_state(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *state,
struct bpf_func_state *caller,
struct bpf_func_state *callee,
int insn_idx)
@@ -11454,6 +11462,7 @@ static int set_rbtree_add_callback_state(struct bpf_verifier_env *env,
}
static int set_task_work_schedule_callback_state(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *state,
struct bpf_func_state *caller,
struct bpf_func_state *callee,
int insn_idx)
@@ -11489,6 +11498,7 @@ static int set_task_work_schedule_callback_state(struct bpf_verifier_env *env,
}
static int set_rcu_callback_state(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *state,
struct bpf_func_state *caller,
struct bpf_func_state *callee,
int insn_idx)
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH bpf-next v2 03/11] bpf: Append complete reference states
2026-10-09 20:27 [PATCH bpf-next v2 00/11] bpf: Scope callback arguments to their frame Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 01/11] bpf: Set up callee state outside of setup_func_entry() Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 02/11] bpf: Pass the owning verifier state to callback setters Ihor Solodrai
@ 2026-10-09 20:27 ` Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 04/11] bpf: Separate reference removal from descendant invalidation Ihor Solodrai
` (7 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ihor Solodrai @ 2026-10-09 20:27 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
Reference acquisition exposes an uninitialized entry for each caller to
fill after insertion.
Accept a complete reference entry and return its id after insertion. Keep
lock and IRQ accounting and acquisition diagnostics in their callers.
Take the target state directly so acquisition can also serve queued
callback states.
No functional change.
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
kernel/bpf/verifier.c | 73 +++++++++++++++++++++++--------------------
1 file changed, 39 insertions(+), 34 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 8d6812fee0c9..62b991f3c39a 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -1497,51 +1497,53 @@ static int grow_stack_arg_slots(struct bpf_verifier_env *env,
return 0;
}
-/* Acquire a pointer id from the env and update the state->refs to include
- * this new pointer reference.
- * On success, returns a valid pointer id to associate with the register
- * On failure, returns a negative errno.
+/*
+ * Append @ref to @state->refs. Return its id, or a negative errno.
*/
-static struct bpf_reference_state *acquire_reference_state(struct bpf_verifier_env *env, int insn_idx)
+static int acquire_reference_state(struct bpf_verifier_state *state,
+ const struct bpf_reference_state *ref)
{
- struct bpf_verifier_state *state = env->cur_state;
int new_ofs = state->acquired_refs;
int err;
err = resize_reference_state(state, state->acquired_refs + 1);
if (err)
- return NULL;
- state->refs[new_ofs].insn_idx = insn_idx;
-
- return &state->refs[new_ofs];
+ return err;
+ state->refs[new_ofs] = *ref;
+ return ref->id;
}
static int acquire_reference(struct bpf_verifier_env *env, int insn_idx, int parent_id)
{
- struct bpf_reference_state *s;
+ struct bpf_reference_state ref = {
+ .type = REF_TYPE_PTR,
+ .id = ++env->id_gen,
+ .insn_idx = insn_idx,
+ .parent_id = parent_id,
+ };
+ int id;
- s = acquire_reference_state(env, insn_idx);
- if (!s)
- return -ENOMEM;
- s->type = REF_TYPE_PTR;
- s->id = ++env->id_gen;
- s->parent_id = parent_id;
- bpf_diag_record_ref_acquire(env, insn_idx, s->id);
- return s->id;
+ id = acquire_reference_state(env->cur_state, &ref);
+ if (id >= 0)
+ bpf_diag_record_ref_acquire(env, insn_idx, id);
+ return id;
}
static int acquire_lock_state(struct bpf_verifier_env *env, int insn_idx, enum ref_state_type type,
int id, void *ptr)
{
struct bpf_verifier_state *state = env->cur_state;
- struct bpf_reference_state *s;
+ struct bpf_reference_state ref = {
+ .type = type,
+ .id = id,
+ .insn_idx = insn_idx,
+ .ptr = ptr,
+ };
+ int err;
- s = acquire_reference_state(env, insn_idx);
- if (!s)
- return -ENOMEM;
- s->type = type;
- s->id = id;
- s->ptr = ptr;
+ err = acquire_reference_state(state, &ref);
+ if (err < 0)
+ return err;
state->active_locks++;
state->active_lock_id = id;
@@ -1554,18 +1556,21 @@ static int acquire_lock_state(struct bpf_verifier_env *env, int insn_idx, enum r
static int acquire_irq_state(struct bpf_verifier_env *env, int insn_idx)
{
struct bpf_verifier_state *state = env->cur_state;
- struct bpf_reference_state *s;
+ struct bpf_reference_state ref = {
+ .type = REF_TYPE_IRQ,
+ .id = ++env->id_gen,
+ .insn_idx = insn_idx,
+ };
+ int id;
- s = acquire_reference_state(env, insn_idx);
- if (!s)
- return -ENOMEM;
- s->type = REF_TYPE_IRQ;
- s->id = ++env->id_gen;
+ id = acquire_reference_state(state, &ref);
+ if (id < 0)
+ return id;
- state->active_irq_id = s->id;
+ state->active_irq_id = id;
bpf_diag_record_context(env, insn_idx, BPF_DIAG_CONTEXT_IRQ, true,
bpf_diag_irq_depth(state));
- return s->id;
+ return id;
}
static void release_reference_state(struct bpf_verifier_state *state, int idx)
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH bpf-next v2 04/11] bpf: Separate reference removal from descendant invalidation
2026-10-09 20:27 [PATCH bpf-next v2 00/11] bpf: Scope callback arguments to their frame Ihor Solodrai
` (2 preceding siblings ...)
2026-10-09 20:27 ` [PATCH bpf-next v2 03/11] bpf: Append complete reference states Ihor Solodrai
@ 2026-10-09 20:27 ` Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 05/11] bpf: Share map-backed callback register setup Ihor Solodrai
` (6 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ihor Solodrai @ 2026-10-09 20:27 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
Reference release removes a program-owned reference and invalidates all
values derived from it in one operation.
Frame-owned values will need the same descendant walk without object
release semantics. Separate the walk from reference removal and pass its
diagnostic reason explicitly. Keep object-reference removal and the
existing release reason in release_reference().
No functional change.
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
kernel/bpf/verifier.c | 29 +++++++++++++++++------------
1 file changed, 17 insertions(+), 12 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 62b991f3c39a..88b86f658e4a 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -10668,8 +10668,9 @@ static int idstack_pop(struct bpf_idmap *idmap)
return idmap->map[--idmap->cnt].old;
}
-/* Release id and objects derived from it iteratively in a DFS manner */
-static int release_reference(struct bpf_verifier_env *env, int id)
+/* Invalidate id and values derived from it iteratively in a DFS manner. */
+static int invalidate_reference(struct bpf_verifier_env *env, int id,
+ enum bpf_diag_mod_reason reason)
{
u32 mask = (1 << STACK_SPILL) | (1 << STACK_DYNPTR);
struct bpf_verifier_state *vstate = env->cur_state;
@@ -10684,11 +10685,6 @@ static int release_reference(struct bpf_verifier_env *env, int id)
if (err)
return err;
- if (find_reference_state(vstate, id)) {
- err = release_reference_nomark(env, id);
- WARN_ON_ONCE(err);
- }
-
while ((id = idstack_pop(idstack))) {
/*
* Child references are inaccessible after parent is released,
@@ -10724,14 +10720,12 @@ static int release_reference(struct bpf_verifier_env *env, int id)
if (reg->dynptr.first_slot)
dyn_stack--;
- bpf_diag_record_scrub(env, &dyn_stack[0].spilled_ptr,
- BPF_DIAG_MOD_REF_RELEASE);
- bpf_diag_record_scrub(env, &dyn_stack[1].spilled_ptr,
- BPF_DIAG_MOD_REF_RELEASE);
+ bpf_diag_record_scrub(env, &dyn_stack[0].spilled_ptr, reason);
+ bpf_diag_record_scrub(env, &dyn_stack[1].spilled_ptr, reason);
invalidate_dynptr(env, dyn_stack);
continue;
}
- bpf_diag_record_scrub(env, reg, BPF_DIAG_MOD_REF_RELEASE);
+ bpf_diag_record_scrub(env, reg, reason);
if (!stack || stack->slot_type[BPF_REG_SIZE - 1] == STACK_SPILL)
mark_reg_invalid(env, reg);
}));
@@ -10740,6 +10734,17 @@ static int release_reference(struct bpf_verifier_env *env, int id)
return 0;
}
+static int release_reference(struct bpf_verifier_env *env, int id)
+{
+ int err;
+
+ if (find_reference_state(env->cur_state, id)) {
+ err = release_reference_nomark(env, id);
+ WARN_ON_ONCE(err);
+ }
+ return invalidate_reference(env, id, BPF_DIAG_MOD_REF_RELEASE);
+}
+
static void invalidate_non_owning_refs(struct bpf_verifier_env *env)
{
struct bpf_func_state *unused;
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH bpf-next v2 05/11] bpf: Share map-backed callback register setup
2026-10-09 20:27 [PATCH bpf-next v2 00/11] bpf: Scope callback arguments to their frame Ihor Solodrai
` (3 preceding siblings ...)
2026-10-09 20:27 ` [PATCH bpf-next v2 04/11] bpf: Separate reference removal from descendant invalidation Ihor Solodrai
@ 2026-10-09 20:27 ` Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 06/11] bpf: Rely on callback frame initialization defaults Ihor Solodrai
` (5 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ihor Solodrai @ 2026-10-09 20:27 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
Map iteration, timer, task-work and RCU callbacks repeat the same register
type, offset and map-metadata setup for their map, key and value arguments.
Use one initializer for map-backed callback registers. Leave argument
selection, value ids and callback-specific policy in each setter.
No functional change.
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
kernel/bpf/verifier.c | 81 +++++++++++--------------------------------
1 file changed, 21 insertions(+), 60 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 88b86f658e4a..8f2125c6e348 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -11267,6 +11267,16 @@ static int check_func_callx(struct bpf_verifier_env *env, struct bpf_insn *insn,
return check_static_func_call(env, subprog, insn_idx);
}
+static void mark_map_callback_reg(struct bpf_reg_state *reg,
+ const struct bpf_reg_state *map_reg,
+ enum bpf_reg_type type)
+{
+ reg->type = type;
+ __mark_reg_known_zero(reg);
+ reg->map_ptr = map_reg->map_ptr;
+ reg->map_uid = map_reg->map_uid;
+}
+
int map_set_for_each_callback_args(struct bpf_verifier_env *env,
struct bpf_verifier_state *state,
struct bpf_func_state *caller,
@@ -11279,15 +11289,8 @@ int map_set_for_each_callback_args(struct bpf_verifier_env *env,
*/
callee->regs[BPF_REG_1] = caller->regs[BPF_REG_1];
- callee->regs[BPF_REG_2].type = PTR_TO_MAP_KEY;
- __mark_reg_known_zero(&callee->regs[BPF_REG_2]);
- callee->regs[BPF_REG_2].map_ptr = caller->regs[BPF_REG_1].map_ptr;
- callee->regs[BPF_REG_2].map_uid = caller->regs[BPF_REG_1].map_uid;
-
- callee->regs[BPF_REG_3].type = PTR_TO_MAP_VALUE;
- __mark_reg_known_zero(&callee->regs[BPF_REG_3]);
- callee->regs[BPF_REG_3].map_ptr = caller->regs[BPF_REG_1].map_ptr;
- callee->regs[BPF_REG_3].map_uid = caller->regs[BPF_REG_1].map_uid;
+ mark_map_callback_reg(&callee->regs[BPF_REG_2], &caller->regs[BPF_REG_1], PTR_TO_MAP_KEY);
+ mark_map_callback_reg(&callee->regs[BPF_REG_3], &caller->regs[BPF_REG_1], PTR_TO_MAP_VALUE);
callee->regs[BPF_REG_3].id = ++env->id_gen;
/* pointer to stack or null */
@@ -11354,26 +11357,12 @@ static int set_timer_callback_state(struct bpf_verifier_env *env,
struct bpf_func_state *callee,
int insn_idx)
{
- struct bpf_map *map_ptr = caller->regs[BPF_REG_1].map_ptr;
- u32 map_uid = caller->regs[BPF_REG_1].map_uid;
-
/* bpf_timer_set_callback(struct bpf_timer *timer, void *callback_fn);
* callback_fn(struct bpf_map *map, void *key, void *value);
*/
- callee->regs[BPF_REG_1].type = CONST_PTR_TO_MAP;
- __mark_reg_known_zero(&callee->regs[BPF_REG_1]);
- callee->regs[BPF_REG_1].map_ptr = map_ptr;
- callee->regs[BPF_REG_1].map_uid = map_uid;
-
- callee->regs[BPF_REG_2].type = PTR_TO_MAP_KEY;
- __mark_reg_known_zero(&callee->regs[BPF_REG_2]);
- callee->regs[BPF_REG_2].map_ptr = map_ptr;
- callee->regs[BPF_REG_2].map_uid = map_uid;
-
- callee->regs[BPF_REG_3].type = PTR_TO_MAP_VALUE;
- __mark_reg_known_zero(&callee->regs[BPF_REG_3]);
- callee->regs[BPF_REG_3].map_ptr = map_ptr;
- callee->regs[BPF_REG_3].map_uid = map_uid;
+ mark_map_callback_reg(&callee->regs[BPF_REG_1], &caller->regs[BPF_REG_1], CONST_PTR_TO_MAP);
+ mark_map_callback_reg(&callee->regs[BPF_REG_2], &caller->regs[BPF_REG_1], PTR_TO_MAP_KEY);
+ mark_map_callback_reg(&callee->regs[BPF_REG_3], &caller->regs[BPF_REG_1], PTR_TO_MAP_VALUE);
callee->regs[BPF_REG_3].id = ++env->id_gen;
/* unused */
@@ -11477,26 +11466,12 @@ static int set_task_work_schedule_callback_state(struct bpf_verifier_env *env,
struct bpf_func_state *callee,
int insn_idx)
{
- struct bpf_map *map_ptr = caller->regs[BPF_REG_3].map_ptr;
- u32 map_uid = caller->regs[BPF_REG_3].map_uid;
-
/*
* callback_fn(struct bpf_map *map, void *key, void *value);
*/
- callee->regs[BPF_REG_1].type = CONST_PTR_TO_MAP;
- __mark_reg_known_zero(&callee->regs[BPF_REG_1]);
- callee->regs[BPF_REG_1].map_ptr = map_ptr;
- callee->regs[BPF_REG_1].map_uid = map_uid;
-
- callee->regs[BPF_REG_2].type = PTR_TO_MAP_KEY;
- __mark_reg_known_zero(&callee->regs[BPF_REG_2]);
- callee->regs[BPF_REG_2].map_ptr = map_ptr;
- callee->regs[BPF_REG_2].map_uid = map_uid;
-
- callee->regs[BPF_REG_3].type = PTR_TO_MAP_VALUE;
- __mark_reg_known_zero(&callee->regs[BPF_REG_3]);
- callee->regs[BPF_REG_3].map_ptr = map_ptr;
- callee->regs[BPF_REG_3].map_uid = map_uid;
+ mark_map_callback_reg(&callee->regs[BPF_REG_1], &caller->regs[BPF_REG_3], CONST_PTR_TO_MAP);
+ mark_map_callback_reg(&callee->regs[BPF_REG_2], &caller->regs[BPF_REG_3], PTR_TO_MAP_KEY);
+ mark_map_callback_reg(&callee->regs[BPF_REG_3], &caller->regs[BPF_REG_3], PTR_TO_MAP_VALUE);
callee->regs[BPF_REG_3].id = ++env->id_gen;
/* unused */
@@ -11513,26 +11488,12 @@ static int set_rcu_callback_state(struct bpf_verifier_env *env,
struct bpf_func_state *callee,
int insn_idx)
{
- struct bpf_map *map_ptr = caller->regs[BPF_REG_2].map_ptr;
- u32 map_uid = caller->regs[BPF_REG_2].map_uid;
-
/*
* callback_fn(struct bpf_map *map, void *key, void *value);
*/
- callee->regs[BPF_REG_1].type = CONST_PTR_TO_MAP;
- __mark_reg_known_zero(&callee->regs[BPF_REG_1]);
- callee->regs[BPF_REG_1].map_ptr = map_ptr;
- callee->regs[BPF_REG_1].map_uid = map_uid;
-
- callee->regs[BPF_REG_2].type = PTR_TO_MAP_KEY;
- __mark_reg_known_zero(&callee->regs[BPF_REG_2]);
- callee->regs[BPF_REG_2].map_ptr = map_ptr;
- callee->regs[BPF_REG_2].map_uid = map_uid;
-
- callee->regs[BPF_REG_3].type = PTR_TO_MAP_VALUE;
- __mark_reg_known_zero(&callee->regs[BPF_REG_3]);
- callee->regs[BPF_REG_3].map_ptr = map_ptr;
- callee->regs[BPF_REG_3].map_uid = map_uid;
+ mark_map_callback_reg(&callee->regs[BPF_REG_1], &caller->regs[BPF_REG_2], CONST_PTR_TO_MAP);
+ mark_map_callback_reg(&callee->regs[BPF_REG_2], &caller->regs[BPF_REG_2], PTR_TO_MAP_KEY);
+ mark_map_callback_reg(&callee->regs[BPF_REG_3], &caller->regs[BPF_REG_2], PTR_TO_MAP_VALUE);
/* unused */
bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_4]);
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH bpf-next v2 06/11] bpf: Rely on callback frame initialization defaults
2026-10-09 20:27 [PATCH bpf-next v2 00/11] bpf: Scope callback arguments to their frame Ihor Solodrai
` (4 preceding siblings ...)
2026-10-09 20:27 ` [PATCH bpf-next v2 05/11] bpf: Share map-backed callback register setup Ihor Solodrai
@ 2026-10-09 20:27 ` Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 07/11] bpf: Introduce REF_TYPE_FRAME in the verifier Ihor Solodrai
` (4 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ihor Solodrai @ 2026-10-09 20:27 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
Both callback entry paths call init_func_state(), which initializes all
registers to NOT_INIT and the callback return range to zero.
Callback setters repeat resets for unused registers, and the timer setter
repeats the default return range. Leave those defaults to frame
initialization and set only arguments and callback-specific state in the
setters.
No functional change.
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
kernel/bpf/verifier.c | 31 -------------------------------
1 file changed, 31 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 8f2125c6e348..67a61570a9ab 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -11295,9 +11295,6 @@ int map_set_for_each_callback_args(struct bpf_verifier_env *env,
/* pointer to stack or null */
callee->regs[BPF_REG_4] = caller->regs[BPF_REG_3];
-
- /* unused */
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_5]);
return 0;
}
@@ -11341,11 +11338,6 @@ static int set_loop_callback_state(struct bpf_verifier_env *env,
callee->regs[BPF_REG_1].type = SCALAR_VALUE;
callee->regs[BPF_REG_2] = caller->regs[BPF_REG_3];
- /* unused */
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_3]);
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_4]);
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_5]);
-
callee->in_callback_fn = true;
callee->callback_ret_range = retval_range(0, 1);
return 0;
@@ -11365,11 +11357,7 @@ static int set_timer_callback_state(struct bpf_verifier_env *env,
mark_map_callback_reg(&callee->regs[BPF_REG_3], &caller->regs[BPF_REG_1], PTR_TO_MAP_VALUE);
callee->regs[BPF_REG_3].id = ++env->id_gen;
- /* unused */
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_4]);
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_5]);
callee->in_async_callback_fn = true;
- callee->callback_ret_range = retval_range(0, 0);
return 0;
}
@@ -11393,10 +11381,6 @@ static int set_find_vma_callback_state(struct bpf_verifier_env *env,
/* pointer to stack or null */
callee->regs[BPF_REG_3] = caller->regs[BPF_REG_4];
-
- /* unused */
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_4]);
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_5]);
callee->in_callback_fn = true;
callee->callback_ret_range = retval_range(0, 1);
return 0;
@@ -11412,15 +11396,9 @@ static int set_user_ringbuf_callback_state(struct bpf_verifier_env *env,
* callback_ctx, u64 flags);
* callback_fn(const struct bpf_dynptr_t* dynptr, void *callback_ctx);
*/
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_0]);
mark_dynptr_cb_reg(env, &callee->regs[BPF_REG_1], BPF_DYNPTR_TYPE_LOCAL);
callee->regs[BPF_REG_2] = caller->regs[BPF_REG_3];
- /* unused */
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_3]);
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_4]);
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_5]);
-
callee->in_callback_fn = true;
callee->callback_ret_range = retval_range(0, 1);
return 0;
@@ -11452,9 +11430,6 @@ static int set_rbtree_add_callback_state(struct bpf_verifier_env *env,
mark_reg_graph_node(callee->regs, BPF_REG_2, &field->graph_root);
ref_set_non_owning(env, &callee->regs[BPF_REG_2]);
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_3]);
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_4]);
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_5]);
callee->in_callback_fn = true;
callee->callback_ret_range = retval_range(0, 1);
return 0;
@@ -11474,9 +11449,6 @@ static int set_task_work_schedule_callback_state(struct bpf_verifier_env *env,
mark_map_callback_reg(&callee->regs[BPF_REG_3], &caller->regs[BPF_REG_3], PTR_TO_MAP_VALUE);
callee->regs[BPF_REG_3].id = ++env->id_gen;
- /* unused */
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_4]);
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_5]);
callee->in_async_callback_fn = true;
callee->callback_ret_range = retval_range(S32_MIN, S32_MAX);
return 0;
@@ -11495,9 +11467,6 @@ static int set_rcu_callback_state(struct bpf_verifier_env *env,
mark_map_callback_reg(&callee->regs[BPF_REG_2], &caller->regs[BPF_REG_2], PTR_TO_MAP_KEY);
mark_map_callback_reg(&callee->regs[BPF_REG_3], &caller->regs[BPF_REG_2], PTR_TO_MAP_VALUE);
- /* unused */
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_4]);
- bpf_mark_reg_not_init(env, &callee->regs[BPF_REG_5]);
callee->in_async_callback_fn = true;
callee->callback_ret_range = retval_range(S32_MIN, S32_MAX);
return 0;
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH bpf-next v2 07/11] bpf: Introduce REF_TYPE_FRAME in the verifier
2026-10-09 20:27 [PATCH bpf-next v2 00/11] bpf: Scope callback arguments to their frame Ihor Solodrai
` (5 preceding siblings ...)
2026-10-09 20:27 ` [PATCH bpf-next v2 06/11] bpf: Rely on callback frame initialization defaults Ihor Solodrai
@ 2026-10-09 20:27 ` Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 08/11] bpf: Scope the bpf_user_ringbuf_drain() dynptr to its callback frame Ihor Solodrai
` (3 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ihor Solodrai @ 2026-10-09 20:27 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
A callback-calling helper can pass its callback a pointer that is only
valid for the duration of the callback invocation.
A callback can save such a value, or something derived from it, and
make it outlive the frame. The BPF program can then reuse it after the
helper returns.
The verifier tracks the argument's register type, but does not tie its
lifetime to the callback frame. Invalidating a value together with
everything derived from it is what invalidate_reference() already does,
walking reg->parent_id across every frame and stack slot. What is
missing is a type of reference that is not an object the program
acquired and releases.
Introduce REF_TYPE_FRAME: a reference owned by a callee frame.
A set_callee_state_fn can anchor an argument to such a reference with
mark_frame_scoped_arg(), and prepare_func_exit() drops it when the
frame is popped, invalidating the argument and everything derived from
it through the existing walk. The anchored register is its own parent,
so invalidate_reference() no longer queues a register as a descendant of
itself.
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
include/linux/bpf.h | 2 +
include/linux/bpf_verifier.h | 6 +--
kernel/bpf/diagnostics.c | 3 ++
kernel/bpf/diagnostics.h | 1 +
kernel/bpf/states.c | 4 ++
kernel/bpf/verifier.c | 80 +++++++++++++++++++++++++++++++++++-
6 files changed, 92 insertions(+), 4 deletions(-)
diff --git a/include/linux/bpf.h b/include/linux/bpf.h
index d5de9d49a168..e4498e6fa88a 100644
--- a/include/linux/bpf.h
+++ b/include/linux/bpf.h
@@ -3188,6 +3188,8 @@ int map_set_for_each_callback_args(struct bpf_verifier_env *env,
struct bpf_verifier_state *state,
struct bpf_func_state *caller,
struct bpf_func_state *callee);
+int mark_frame_scoped_arg(struct bpf_verifier_env *env, struct bpf_verifier_state *state,
+ struct bpf_func_state *callee, u32 regno);
int bpf_percpu_hash_copy(struct bpf_map *map, void *key, void *value, u64 flags);
int bpf_percpu_array_copy(struct bpf_map *map, void *key, void *value, u64 flags);
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index c51083c761cf..54a4f440c33b 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -272,15 +272,13 @@ struct bpf_stack_state {
};
struct bpf_reference_state {
- /* Each reference object has a type. Ensure REF_TYPE_PTR is zero to
- * default to pointer reference on zero initialization of a state.
- */
enum ref_state_type {
REF_TYPE_PTR = (1 << 1),
REF_TYPE_IRQ = (1 << 2),
REF_TYPE_LOCK = (1 << 3),
REF_TYPE_RES_LOCK = (1 << 4),
REF_TYPE_RES_LOCK_IRQ = (1 << 5),
+ REF_TYPE_FRAME = (1 << 6),
REF_TYPE_LOCK_MASK = REF_TYPE_LOCK | REF_TYPE_RES_LOCK | REF_TYPE_RES_LOCK_IRQ,
} type;
/* Track each reference created with a unique id, even if the same
@@ -298,6 +296,8 @@ struct bpf_reference_state {
* it matches on unlock.
*/
void *ptr;
+ /* For REF_TYPE_FRAME */
+ u32 frameno;
};
};
diff --git a/kernel/bpf/diagnostics.c b/kernel/bpf/diagnostics.c
index 857b668212fb..ba1a3f07d58e 100644
--- a/kernel/bpf/diagnostics.c
+++ b/kernel/bpf/diagnostics.c
@@ -2238,6 +2238,9 @@ static void diag_print_mod(struct bpf_verifier_env *env, const struct bpf_diag_h
"resource release invalidated "
"this value";
break;
+ case BPF_DIAG_MOD_FRAME_RELEASE:
+ reason = "the callback that owned this value returned";
+ break;
case BPF_DIAG_MOD_PKT_DATA_CHANGE:
reason = "packet data may have moved";
break;
diff --git a/kernel/bpf/diagnostics.h b/kernel/bpf/diagnostics.h
index a4102fb049ec..b5cab4d79c1a 100644
--- a/kernel/bpf/diagnostics.h
+++ b/kernel/bpf/diagnostics.h
@@ -22,6 +22,7 @@ enum bpf_diag_mod_reason {
BPF_DIAG_MOD_SPILL,
BPF_DIAG_MOD_VAR_WRITE,
BPF_DIAG_MOD_REF_RELEASE,
+ BPF_DIAG_MOD_FRAME_RELEASE,
BPF_DIAG_MOD_PKT_DATA_CHANGE,
BPF_DIAG_MOD_NON_OWN_REF,
BPF_DIAG_MOD_CALLER_SAVED,
diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c
index 18bf7b660c2f..1291824a3a7d 100644
--- a/kernel/bpf/states.c
+++ b/kernel/bpf/states.c
@@ -899,6 +899,10 @@ static bool refsafe(struct bpf_verifier_state *old, struct bpf_verifier_state *c
break;
case REF_TYPE_IRQ:
break;
+ case REF_TYPE_FRAME:
+ if (old->refs[i].frameno != cur->refs[i].frameno)
+ return false;
+ break;
case REF_TYPE_LOCK:
case REF_TYPE_RES_LOCK:
case REF_TYPE_RES_LOCK_IRQ:
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 67a61570a9ab..c969fef2d452 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -1529,6 +1529,49 @@ static int acquire_reference(struct bpf_verifier_env *env, int insn_idx, int par
return id;
}
+/* Acquire a reference owned by frame @frameno of @state */
+static int acquire_frame_reference(struct bpf_verifier_env *env, struct bpf_verifier_state *state,
+ int insn_idx, u32 frameno)
+{
+ struct bpf_reference_state ref = {
+ .type = REF_TYPE_FRAME,
+ .id = ++env->id_gen,
+ .insn_idx = insn_idx,
+ .frameno = frameno,
+ };
+
+ return acquire_reference_state(state, &ref);
+}
+
+/*
+ * Anchor @regno in @callee to a reference owned by the callee frame, so the
+ * value and everything derived from it is invalidated when the frame is
+ * popped. @state owns @callee; for a callback it is not env->cur_state.
+ */
+int mark_frame_scoped_arg(struct bpf_verifier_env *env, struct bpf_verifier_state *state,
+ struct bpf_func_state *callee, u32 regno)
+{
+ int id;
+
+ if (verifier_bug_if(!callee->frameno, env,
+ "cannot scope an argument to frame 0"))
+ return -EFAULT;
+
+ id = acquire_frame_reference(env, state, callee->callsite, callee->frameno);
+ if (id < 0)
+ return id;
+ /*
+ * The value is its own lifetime anchor: there is no associated
+ * object to borrow from, only the frame. parent_id = id here
+ * covers both possible derived references:
+ * - through the id (e.g. dynptr slice)
+ * - through parent_id (e.g. dynptr clone)
+ */
+ callee->regs[regno].id = id;
+ callee->regs[regno].parent_id = id;
+ return 0;
+}
+
static int acquire_lock_state(struct bpf_verifier_env *env, int insn_idx, enum ref_state_type type,
int id, void *ptr)
{
@@ -10705,7 +10748,7 @@ static int invalidate_reference(struct bpf_verifier_env *env, int id,
continue;
/* Free objects derived from the current object */
- if (reg->parent_id == id) {
+ if (reg->parent_id == id && reg->id != id) {
err = idstack_push(idstack, reg->id);
if (err)
return err;
@@ -10745,6 +10788,27 @@ static int release_reference(struct bpf_verifier_env *env, int id)
return invalidate_reference(env, id, BPF_DIAG_MOD_REF_RELEASE);
}
+static int release_frame_references(struct bpf_verifier_env *env, u32 frameno)
+{
+ struct bpf_verifier_state *state = env->cur_state;
+ int i, id, err;
+
+ for (i = 0; i < state->acquired_refs;) {
+ if (state->refs[i].type != REF_TYPE_FRAME ||
+ state->refs[i].frameno != frameno) {
+ i++;
+ continue;
+ }
+ id = state->refs[i].id;
+ release_reference_state(state, i);
+ err = invalidate_reference(env, id, BPF_DIAG_MOD_FRAME_RELEASE);
+ if (err)
+ return err;
+ }
+
+ return 0;
+}
+
static void invalidate_non_owning_refs(struct bpf_verifier_env *env)
{
struct bpf_func_state *unused;
@@ -11629,6 +11693,15 @@ static int prepare_func_exit(struct bpf_verifier_env *env, int *insn_idx)
verbose(env, "to caller at %d:\n", *insn_idx);
print_verifier_state(env, state, caller->frameno, true);
}
+
+ /*
+ * Values the caller only guaranteed for the duration of the call stop
+ * being valid here.
+ */
+ err = release_frame_references(env, callee->frameno);
+ if (err)
+ return err;
+
account_processed_insns(env, callee, caller);
/* clear everything in the callee. In case of exceptional exits using
* bpf_throw, this will be done by copy_verifier_state for extra frames. */
@@ -11806,6 +11879,11 @@ static int check_reference_leak(struct bpf_verifier_env *env, bool exception_exi
return 0;
for (i = 0; i < state->acquired_refs; i++) {
+ if (!exception_exit && state->refs[i].type == REF_TYPE_FRAME) {
+ verifier_bug(env, "frame %u reference id=%d alive at program exit",
+ state->refs[i].frameno, state->refs[i].id);
+ return -EFAULT;
+ }
if (state->refs[i].type != REF_TYPE_PTR)
continue;
/* Allow struct_ops programs to return a referenced kptr back to
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH bpf-next v2 08/11] bpf: Scope the bpf_user_ringbuf_drain() dynptr to its callback frame
2026-10-09 20:27 [PATCH bpf-next v2 00/11] bpf: Scope callback arguments to their frame Ihor Solodrai
` (6 preceding siblings ...)
2026-10-09 20:27 ` [PATCH bpf-next v2 07/11] bpf: Introduce REF_TYPE_FRAME in the verifier Ihor Solodrai
@ 2026-10-09 20:27 ` Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 09/11] selftests/bpf: Cover the user ringbuf callback dynptr lifetime Ihor Solodrai
` (2 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ihor Solodrai @ 2026-10-09 20:27 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
The dynptr argument to a bpf_user_ringbuf_drain() callback points to a
descriptor on the helper's stack. The descriptor and the sample it
describes are valid only during that callback invocation.
The callback can save the dynptr pointer, a slice, or a clone in the
caller's frame and use it after the drain returns. An escaped descriptor
pointer can allow arbitrary kernel read/write once the helper's stack
is reused.
The verifier assigns the dynptr a type and an id, but does not tie it
or its descendants to the callback's lifetime.
Anchor R1 to a frame reference so the argument and its descendants are
invalidated when the callback frame is popped.
Reported-by: Nicholas Carlini <npc@anthropic.com>
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
kernel/bpf/verifier.c | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index c969fef2d452..edc78e50dc22 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -713,11 +713,12 @@ static void mark_dynptr_stack_regs(struct bpf_verifier_env *env,
__mark_dynptr_reg(sreg2, type, false, id, parent_id);
}
-static void mark_dynptr_cb_reg(struct bpf_verifier_env *env,
- struct bpf_reg_state *reg,
- enum bpf_dynptr_type type)
+static int mark_dynptr_cb_reg(struct bpf_verifier_env *env, struct bpf_verifier_state *state,
+ struct bpf_func_state *callee, u32 regno,
+ enum bpf_dynptr_type type)
{
- __mark_dynptr_reg(reg, type, true, ++env->id_gen, 0);
+ __mark_dynptr_reg(&callee->regs[regno], type, true, 0, 0);
+ return mark_frame_scoped_arg(env, state, callee, regno);
}
static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env,
@@ -11456,11 +11457,15 @@ static int set_user_ringbuf_callback_state(struct bpf_verifier_env *env,
struct bpf_func_state *callee,
int insn_idx)
{
+ int err;
+
/* bpf_user_ringbuf_drain(struct bpf_map *map, void *callback_fn, void
* callback_ctx, u64 flags);
* callback_fn(const struct bpf_dynptr_t* dynptr, void *callback_ctx);
*/
- mark_dynptr_cb_reg(env, &callee->regs[BPF_REG_1], BPF_DYNPTR_TYPE_LOCAL);
+ err = mark_dynptr_cb_reg(env, state, callee, BPF_REG_1, BPF_DYNPTR_TYPE_LOCAL);
+ if (err)
+ return err;
callee->regs[BPF_REG_2] = caller->regs[BPF_REG_3];
callee->in_callback_fn = true;
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH bpf-next v2 09/11] selftests/bpf: Cover the user ringbuf callback dynptr lifetime
2026-10-09 20:27 [PATCH bpf-next v2 00/11] bpf: Scope callback arguments to their frame Ihor Solodrai
` (7 preceding siblings ...)
2026-10-09 20:27 ` [PATCH bpf-next v2 08/11] bpf: Scope the bpf_user_ringbuf_drain() dynptr to its callback frame Ihor Solodrai
@ 2026-10-09 20:27 ` Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 10/11] bpf: Scope the bpf_for_each_map_elem() array key to the callback frame Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 11/11] selftests/bpf: Cover callback-frame map key lifetime Ihor Solodrai
10 siblings, 0 replies; 12+ messages in thread
From: Ihor Solodrai @ 2026-10-09 20:27 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
The dynptr passed to a bpf_user_ringbuf_drain() callback, and values
derived from it, must not be used after that callback returns.
Exercise escapes through the callback context, slices returned by
bpf_dynptr_data() and bpf_dynptr_slice(), and clones stored in the
caller's frame. Also cover an inner callback's dynptr escaping into an
outer callback.
Require the callback-return diagnostic for escaped pointers and slices.
Check the full bpf_throw() callback rejection so a frame-reference leak
cannot satisfy the test accidentally.
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
.../selftests/bpf/progs/exceptions_fail.c | 20 +++
.../selftests/bpf/progs/user_ringbuf_fail.c | 135 ++++++++++++++++++
2 files changed, 155 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/exceptions_fail.c b/tools/testing/selftests/bpf/progs/exceptions_fail.c
index 22503cf62e9f..86a0667ba348 100644
--- a/tools/testing/selftests/bpf/progs/exceptions_fail.c
+++ b/tools/testing/selftests/bpf/progs/exceptions_fail.c
@@ -31,6 +31,11 @@ struct {
__type(value, struct hmap_elem);
} hmap SEC(".maps");
+struct {
+ __uint(type, BPF_MAP_TYPE_USER_RINGBUF);
+ __uint(max_entries, 4096);
+} user_ringbuf SEC(".maps");
+
private(A) struct bpf_spin_lock lock;
private(A) struct bpf_rb_root rbtree __contains(foo, node);
@@ -110,6 +115,21 @@ static int timer_cb(void *map, int *key, struct bpf_timer *timer)
return 0;
}
+static long drain_cb(struct bpf_dynptr *dynptr, void *context)
+{
+ bpf_throw(0);
+ return 0;
+}
+
+SEC("?tc")
+__failure
+__msg("bpf_throw kfunc (insn {{[0-9]+}}) cannot be called from callback subprog {{[0-9]+}}")
+int reject_user_ringbuf_callback_throw(struct __sk_buff *ctx)
+{
+ bpf_user_ringbuf_drain(&user_ringbuf, drain_cb, NULL, 0);
+ return 0;
+}
+
SEC("?tc")
__failure __msg("cannot be called from callback subprog")
int reject_async_callback_throw(struct __sk_buff *ctx)
diff --git a/tools/testing/selftests/bpf/progs/user_ringbuf_fail.c b/tools/testing/selftests/bpf/progs/user_ringbuf_fail.c
index c0d0422b8030..4e0f29c2d1f2 100644
--- a/tools/testing/selftests/bpf/progs/user_ringbuf_fail.c
+++ b/tools/testing/selftests/bpf/progs/user_ringbuf_fail.c
@@ -1,9 +1,11 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2022 Meta Platforms, Inc. and affiliates. */
+#include <stdbool.h>
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include "bpf_misc.h"
+#include "bpf_kfuncs.h"
char _license[] SEC("license") = "GPL";
@@ -243,3 +245,136 @@ int user_ringbuf_callback_const_ptr_to_dynptr_reg_off(void *ctx)
callback_adjust_bpf_dynptr_reg_off, NULL, 0);
return 0;
}
+
+/* The sample goes back to the producer as soon as the callback returns. */
+struct dynptr_ctx {
+ struct bpf_dynptr *saved;
+};
+
+static long callback_park_dynptr(struct bpf_dynptr *dynptr, void *context)
+{
+ struct dynptr_ctx *c = context;
+
+ c->saved = dynptr;
+ return 0;
+}
+
+SEC("?raw_tp")
+__failure __msg("the callback that owned this value returned")
+int user_ringbuf_callback_park_dynptr(void *ctx)
+{
+ struct dynptr_ctx c = {};
+ char buf[8] = {};
+
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_dynptr, &c, 0);
+ if (c.saved)
+ bpf_dynptr_read(buf, sizeof(buf), c.saved, 0, 0);
+ return buf[0];
+}
+
+struct slice_ctx {
+ char *p;
+};
+
+static long callback_park_data_slice(struct bpf_dynptr *dynptr, void *context)
+{
+ struct slice_ctx *c = context;
+
+ c->p = bpf_dynptr_data(dynptr, 0, 8);
+ return 0;
+}
+
+SEC("?raw_tp")
+__failure __msg("the callback that owned this value returned")
+int user_ringbuf_callback_park_data_slice(void *ctx)
+{
+ struct slice_ctx c = {};
+
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_data_slice, &c, 0);
+ if (c.p)
+ return c.p[0];
+ return 0;
+}
+
+static long callback_park_kfunc_slice(struct bpf_dynptr *dynptr, void *context)
+{
+ struct slice_ctx *c = context;
+
+ c->p = bpf_dynptr_slice(dynptr, 0, NULL, 8);
+ return 0;
+}
+
+SEC("?raw_tp")
+__failure __msg("the callback that owned this value returned")
+int user_ringbuf_callback_park_kfunc_slice(void *ctx)
+{
+ struct slice_ctx c = {};
+
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_kfunc_slice, &c, 0);
+ if (c.p)
+ return c.p[0];
+ return 0;
+}
+
+struct clone_ctx {
+ struct bpf_dynptr clone;
+ __u64 armed;
+};
+
+static long callback_park_clone(struct bpf_dynptr *dynptr, void *context)
+{
+ struct clone_ctx *c = context;
+
+ bpf_dynptr_clone(dynptr, &c->clone);
+ c->armed = 1;
+ return 0;
+}
+
+SEC("?raw_tp")
+__failure __msg("Expected an initialized dynptr as R3")
+int user_ringbuf_callback_park_clone(void *ctx)
+{
+ struct clone_ctx c = {};
+ char buf[8] = {};
+
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_clone, &c, 0);
+ if (c.armed)
+ bpf_dynptr_read(buf, sizeof(buf), &c.clone, 0, 0);
+ return buf[0];
+}
+
+SEC("?raw_tp")
+__failure __msg("Expected an initialized dynptr as R1")
+int user_ringbuf_callback_park_clone_then_slice(void *ctx)
+{
+ struct clone_ctx c = {};
+ char *p;
+
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_clone, &c, 0);
+ if (c.armed) {
+ p = bpf_dynptr_data(&c.clone, 0, 8);
+ if (p)
+ return p[0];
+ }
+ return 0;
+}
+
+/* An inner drain's dynptr must not escape into the outer callback either. */
+static long callback_park_outer(struct bpf_dynptr *dynptr, void *context)
+{
+ struct dynptr_ctx inner = {};
+ char buf[8] = {};
+
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_dynptr, &inner, 0);
+ if (inner.saved)
+ bpf_dynptr_read(buf, sizeof(buf), inner.saved, 0, 0);
+ return buf[0] ? 1 : 0;
+}
+
+SEC("?raw_tp")
+__failure __msg("the callback that owned this value returned")
+int user_ringbuf_callback_nested_park_inner(void *ctx)
+{
+ bpf_user_ringbuf_drain(&user_ringbuf, callback_park_outer, NULL, 0);
+ return 0;
+}
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH bpf-next v2 10/11] bpf: Scope the bpf_for_each_map_elem() array key to the callback frame
2026-10-09 20:27 [PATCH bpf-next v2 00/11] bpf: Scope callback arguments to their frame Ihor Solodrai
` (8 preceding siblings ...)
2026-10-09 20:27 ` [PATCH bpf-next v2 09/11] selftests/bpf: Cover the user ringbuf callback dynptr lifetime Ihor Solodrai
@ 2026-10-09 20:27 ` Ihor Solodrai
2026-10-09 20:27 ` [PATCH bpf-next v2 11/11] selftests/bpf: Cover callback-frame map key lifetime Ihor Solodrai
10 siblings, 0 replies; 12+ messages in thread
From: Ihor Solodrai @ 2026-10-09 20:27 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
bpf_for_each_map_elem() passes array and per-CPU array callbacks the
address of a key on the helper's stack.
A callback can save that pointer in its context and dereference it
after the helper returns. Loads through PTR_TO_MAP_KEY are not
fault-protected, allowing a four-byte read-only leak of reused kernel
stack.
The verifier tracks the pointer's type and key size, but does not tie
its lifetime to the callback invocation.
Anchor R2 to a frame reference in the array map callback setup so the
key and its copies are invalidated on callback return. Leave hash keys
and map values unchanged, since their storage is not callback-local.
Reported-by: Nicholas Carlini <npc@anthropic.com>
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
kernel/bpf/arraymap.c | 18 ++++++++++++++++--
1 file changed, 16 insertions(+), 2 deletions(-)
diff --git a/kernel/bpf/arraymap.c b/kernel/bpf/arraymap.c
index 0fe9afd4a591..5e8669e311b6 100644
--- a/kernel/bpf/arraymap.c
+++ b/kernel/bpf/arraymap.c
@@ -855,6 +855,20 @@ static u64 array_map_mem_usage(const struct bpf_map *map)
return usage;
}
+static int array_map_set_for_each_callback_args(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *state,
+ struct bpf_func_state *caller,
+ struct bpf_func_state *callee)
+{
+ int err;
+
+ err = map_set_for_each_callback_args(env, state, caller, callee);
+ if (err)
+ return err;
+
+ return mark_frame_scoped_arg(env, state, callee, BPF_REG_2);
+}
+
BTF_ID_LIST_SINGLE(array_map_btf_ids, struct, bpf_array)
const struct bpf_map_ops array_map_ops = {
.map_meta_equal = array_map_meta_equal,
@@ -875,7 +889,7 @@ const struct bpf_map_ops array_map_ops = {
.map_check_btf = array_map_check_btf,
.map_lookup_batch = generic_map_lookup_batch,
.map_update_batch = generic_map_update_batch,
- .map_set_for_each_callback_args = map_set_for_each_callback_args,
+ .map_set_for_each_callback_args = array_map_set_for_each_callback_args,
.map_for_each_callback = bpf_for_each_array_elem,
.map_mem_usage = array_map_mem_usage,
.map_btf_id = &array_map_btf_ids[0],
@@ -900,7 +914,7 @@ const struct bpf_map_ops percpu_array_map_ops = {
.map_check_btf = array_map_check_btf,
.map_lookup_batch = generic_map_lookup_batch,
.map_update_batch = generic_map_update_batch,
- .map_set_for_each_callback_args = map_set_for_each_callback_args,
+ .map_set_for_each_callback_args = array_map_set_for_each_callback_args,
.map_for_each_callback = bpf_for_each_array_elem,
.map_mem_usage = array_map_mem_usage,
.map_btf_id = &array_map_btf_ids[0],
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH bpf-next v2 11/11] selftests/bpf: Cover callback-frame map key lifetime
2026-10-09 20:27 [PATCH bpf-next v2 00/11] bpf: Scope callback arguments to their frame Ihor Solodrai
` (9 preceding siblings ...)
2026-10-09 20:27 ` [PATCH bpf-next v2 10/11] bpf: Scope the bpf_for_each_map_elem() array key to the callback frame Ihor Solodrai
@ 2026-10-09 20:27 ` Ihor Solodrai
10 siblings, 0 replies; 12+ messages in thread
From: Ihor Solodrai @ 2026-10-09 20:27 UTC (permalink / raw)
To: Alexei Starovoitov, Andrii Nakryiko, Daniel Borkmann,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Amery Hung, Emil Tsalapatis, Nicholas Carlini, bpf, linux-kernel,
kernel-team
Array and per-CPU array callbacks to bpf_for_each_map_elem() receive
a key on the helper's stack.
Check that saving this key in the callback context and dereferencing
it after iteration is rejected.
Also require the same pattern to keep verifying for a hash key and
an array value, whose storage outlives the callback. These controls
guard the array-only and key-only scope of the lifetime restriction.
Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
---
.../bpf/progs/verifier_iterating_callbacks.c | 91 +++++++++++++++++++
1 file changed, 91 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/verifier_iterating_callbacks.c b/tools/testing/selftests/bpf/progs/verifier_iterating_callbacks.c
index 1fbcc5228306..2e0c56888953 100644
--- a/tools/testing/selftests/bpf/progs/verifier_iterating_callbacks.c
+++ b/tools/testing/selftests/bpf/progs/verifier_iterating_callbacks.c
@@ -9,6 +9,20 @@ struct {
__type(value, __u64);
} map SEC(".maps");
+struct {
+ __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
+ __uint(max_entries, 8);
+ __type(key, __u32);
+ __type(value, __u64);
+} percpu_map SEC(".maps");
+
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, 8);
+ __type(key, __u32);
+ __type(value, __u64);
+} hash_map SEC(".maps");
+
struct {
__uint(type, BPF_MAP_TYPE_USER_RINGBUF);
__uint(max_entries, 8);
@@ -800,4 +814,81 @@ __naked void check_add_const_regsafe_off(void)
: __clobber_common);
}
+struct key_ctx {
+ __u32 *key;
+};
+
+static long park_key_cb(struct bpf_map *map, __u32 *key, __u64 *value,
+ void *context)
+{
+ struct key_ctx *c = context;
+
+ c->key = key;
+ return 0;
+}
+
+/* bpf_for_each_array_elem() passes a key from its own stack frame. */
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int array_park_map_key(void *ctx)
+{
+ struct key_ctx c = {};
+
+ bpf_for_each_map_elem(&map, park_key_cb, &c, 0);
+ if (c.key)
+ return *c.key;
+ return 0;
+}
+
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int percpu_array_park_map_key(void *ctx)
+{
+ struct key_ctx c = {};
+
+ bpf_for_each_map_elem(&percpu_map, park_key_cb, &c, 0);
+ if (c.key)
+ return *c.key;
+ return 0;
+}
+
+/* A hash key points into the element, which outlives the callback. */
+SEC("?raw_tp")
+__success
+int hash_park_map_key(void *ctx)
+{
+ struct key_ctx c = {};
+
+ bpf_for_each_map_elem(&hash_map, park_key_cb, &c, 0);
+ if (c.key)
+ return *c.key;
+ return 0;
+}
+
+struct value_ctx {
+ __u64 *value;
+};
+
+static long park_value_cb(struct bpf_map *map, __u32 *key, __u64 *value,
+ void *context)
+{
+ struct value_ctx *c = context;
+
+ c->value = value;
+ return 0;
+}
+
+/* Only the key is frame-scoped; the element lives until map teardown. */
+SEC("?raw_tp")
+__success
+int array_park_map_value(void *ctx)
+{
+ struct value_ctx c = {};
+
+ bpf_for_each_map_elem(&map, park_value_cb, &c, 0);
+ if (c.value)
+ return *c.value;
+ return 0;
+}
+
char _license[] SEC("license") = "GPL";
--
2.56.0
^ permalink raw reply [flat|nested] 12+ messages in thread