From: sashiko-bot@kernel.org
To: "Alexei Starovoitov" <alexei.starovoitov@gmail.com>, bpf@vger.kernel.org
Subject: Re: [PATCH bpf-next v2 21/35] bpf: Verify static subprogs on their own
Date: Thu, 05 Nov 2026 22:45:09 +0000 [thread overview]
Message-ID: <sashiko-outbox-200328@kernel.org> (raw)
In-Reply-To: <20261105204513.2230344-22-alexei.starovoitov@gmail.com>
Thank you for your contribution! Sashiko AI review found 3 potential issue(s) to consider:
- [Critical] bpf: stale reg->delta on scalar args in call_func_on_its_own()
- [High] bpf: bpf_flow_return() drops speculative state flag on callee exit
- [Medium] bpf: call_func_on_its_own() loses async callback RCU lock state
--
commit e8a6194ecde4a13fd7801b423df4fc8986d081f4
Author: Alexei Starovoitov <ast@kernel.org>
bpf: Verify static subprogs on their own
This commit verifies eligible static subprograms in a single-frame entry
state without their caller frames, mapping held references and locks to
positional inherited IDs and delivering exit states to all waiting callers.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 8080920378063..901f4c7620e9e 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -11794,6 +11797,175 @@ static void account_current_path(struct bpf_verifier_env *env);
> * BPF_INHERITED_ID + the position for the function, and put back at the return.
> */
[ ... ]
> +static int call_func_on_its_own(struct bpf_verifier_env *env, int subprog, int insn_idx)
> +{
> + u16 live = env->insn_aux_data[env->subprog_info[subprog].start].live_regs_before;
> + struct bpf_verifier_state *state = env->cur_state;
> + struct bpf_func_state *caller = state->frame[state->curframe], *callee;
> + struct bpf_verifier_state entry = {};
> + int i, err;
> +
> + callee = kzalloc_obj(*callee, GFP_KERNEL_ACCOUNT);
> + if (!callee)
> + return -ENOMEM;
> + entry.frame[0] = callee;
> + entry.in_sleepable = state->in_sleepable;
> + entry.callers_unknown = state->callers_unknown;
> + init_func_state(env, callee, BPF_MAIN_FUNC, 0, subprog);
> + /* the frame that says so is not there when the function is verified */
> + callee->in_rcu_callback_fn = in_rcu_callback(env);
[Severity: Medium]
Does setting callee->in_rcu_callback_fn = in_rcu_callback(env) lose the RCU
read lock state when called from a non-sleepable async callback within a
BPF_FUNC_CTX_NO_RCU_LOCK global subprogram?
When do_check_common() verifies a global subprogram with
env->no_rcu_read_lock = true and a non-sleepable async callback is queued via
push_async_cb(), the callback frame has frame[0]->in_async_callback_fn = true
and in_rcu_callback_fn = false. Inside that callback, has_rcu_read_lock()
returns true because of state->frame[0]->in_async_callback_fn:
static bool has_rcu_read_lock(struct bpf_verifier_env *env)
{
struct bpf_verifier_state *state = env->cur_state;
if (state->active_rcu_locks || in_rcu_callback(env))
return true;
if (in_sleepable(env))
return false;
return !env->no_rcu_read_lock || state->frame[0]->in_async_callback_fn;
}
When the async callback invokes a static subprogram via
call_func_on_its_own(), entry.frame[0] is replaced by callee where both
in_rcu_callback_fn and in_async_callback_fn are false while
env->no_rcu_read_lock remains true.
Wouldn't has_rcu_read_lock() then return false inside the static subprogram,
causing check_map_func_compatibility() to reject valid map helpers,
check_func_call() to record BPF_FUNC_CTX_NO_RCU_LOCK instead of
BPF_FUNC_CTX_NON_SLEEPABLE for global callees, and entry states with and
without RCU read lock protection to be merged?
> + for (i = BPF_REG_1; i <= BPF_REG_5; i++)
> + if (live & BIT(i))
> + callee->regs[i] = caller->regs[i];
> + err = grow_stack_arg_slots(env, callee, bpf_in_stack_arg_cnt(&env->subprog_info[subprog]));
> + if (err) {
> + bpf_free_verifier_state(&entry, false);
> + return err;
> + }
> + for (i = 0; i < callee->out_stack_arg_cnt; i++)
> + callee->stack_arg_regs[i] = caller->stack_arg_regs[i];
> +
> + err = copy_reference_state(&entry, state);
> + if (err) {
> + bpf_free_verifier_state(&entry, false);
> + return err;
> + }
> + for (i = 0; i < entry.acquired_refs; i++) {
> + /* the id of a lock is the id of the pointer to it, which may have none */
> + entry.refs[i].id = inherited_id(state, entry.refs[i].id);
> + if (entry.refs[i].type == REF_TYPE_PTR)
> + entry.refs[i].parent_id = inherited_id(state, entry.refs[i].parent_id);
> + }
> + entry.active_irq_id = inherited_id(state, entry.active_irq_id);
> + entry.active_lock_id = inherited_id(state, entry.active_lock_id);
> + callee->entry_refs = entry.acquired_refs;
> + callee->entry_rcu_locks = entry.active_rcu_locks;
> + callee->entry_preempt_locks = entry.active_preempt_locks;
> + for (i = BPF_REG_1; i <= BPF_REG_5; i++) {
> + callee->regs[i].id = inherited_id(state, callee->regs[i].id);
> + callee->regs[i].parent_id = inherited_id(state, callee->regs[i].parent_id);
> + callee->regs[i].map_uid = inherited_id(state, callee->regs[i].map_uid);
> + }
> + for (i = 0; i < callee->out_stack_arg_cnt; i++) {
> + callee->stack_arg_regs[i].id = inherited_id(state, callee->stack_arg_regs[i].id);
> + callee->stack_arg_regs[i].parent_id =
> + inherited_id(state, callee->stack_arg_regs[i].parent_id);
> + callee->stack_arg_regs[i].map_uid =
> + inherited_id(state, callee->stack_arg_regs[i].map_uid);
> + }
[Severity: Critical]
When clearing scalar argument IDs in call_func_on_its_own(), can leaving
reg->delta non-zero corrupt scalar bounds tracking in sync_linked_regs()?
If a caller passes a SCALAR_VALUE argument that was offset from a linked
scalar (for example r1 = r6; r1 += 10, which sets r1.id = base_id |
BPF_ADD_CONST64 and r1.delta = 10), inherited_id() returns 0 for r1.id,
leaving callee->regs[1].id == 0 and callee->regs[1].delta == 10 (and
similarly for callee->stack_arg_regs[i]).
In flow_arrive(), bpf_clear_singular_ids() skips registers where !reg->id.
When the callee later copies r1 to another register (r2 = r1),
assign_scalar_id_before_mov() only calls clear_scalar_id() if src_reg->id &
BPF_ADD_CONST:
if (src_reg->id & BPF_ADD_CONST)
clear_scalar_id(src_reg);
if (!src_reg->id && !tnum_is_const(src_reg->var_off))
src_reg->id = ++env->id_gen;
Because r1.id is 0, r1 and r2 receive a new base ID without BPF_ADD_CONST
while retaining delta == 10. If r2 is then offset (for example r2 += 4,
setting r2.id |= BPF_ADD_CONST64 and r2.delta = 4) and a conditional branch
refines r1 or r2, sync_linked_regs() assumes the register without
BPF_ADD_CONST has delta == 0 and computes (s64)reg->delta -
(s64)known_reg->delta using the stale delta of 10 (or if r2 += 10 so
reg->delta == known_reg->delta, copies *reg = *known_reg with no offset
adjustment).
In addition, reg_value_equal(), regs_exact(), and regsafe() (under
explore_alu_limits) memcmp up to offsetof(struct bpf_reg_state, id), which
includes s32 delta. Should call_func_on_its_own() also zero reg->delta (or
call clear_scalar_id()) when clearing IDs on arguments?
> +
> + account_current_path(env);
> + env->subprog_info[subprog].on_its_own = true;
> + err = bpf_flow_call(env, state, &entry, subprog, insn_idx);
> + bpf_free_verifier_state(&entry, false);
> + /* the call waits, there is nothing to go on with */
> + return err ?: PROCESS_BPF_EXIT;
> +}
> +
> +/* The current state is at exit of a function that is verified on its own. */
> +static int exit_func_on_its_own(struct bpf_verifier_env *env)
> +{
> + struct bpf_verifier_state *state = env->cur_state;
> + struct bpf_func_state *callee = state->frame[0];
> + u32 i, nregs;
> + int err;
> +
> + nregs = bpf_ret_reg_pair(env, callee->subprogno) ? 2 : 1;
> + for (i = 0; i < nregs; i++) {
> + if (callee->regs[ret_regs[i]].type != PTR_TO_STACK)
> + continue;
> + verbose(env, "cannot return stack pointer to the caller\n");
> + return -EINVAL;
> + }
> + state->unwound |= bpf_is_unwind_exit(&env->prog->insnsi[env->insn_idx]);
> +
> + /* Invalidate callee-local dynptrs and their slices before the frame goes away. */
> + err = destroy_dynptrs_in_stack_slots(env, callee, 0,
> + callee->allocated_stack / BPF_REG_SIZE - 1);
> + if (err)
> + return err;
> +
> + account_current_path(env);
> + err = bpf_flow_exit(env, state, env->insn_idx);
> + return err ?: PROCESS_BPF_EXIT;
> +}
[Severity: High]
What happens when a speculative path spawned inside a static subprogram
verified on its own reaches exit_func_on_its_own()?
A non-speculative caller passes func_is_on_its_own() and waits in
bpf_flow_call() with w->st.speculative == false. While verifying the callee,
check_cond_jmp_op(), sanitize_ptr_alu(), or check_unwind_goto() can push a
speculative branch with branch->speculative = true and branch->flow_entry =
id. When that speculative state reaches BPF_EXIT:
exit_func_on_its_own()
bpf_flow_exit()
flow_apply_exit()
bpf_flow_return(env, &st, &x->st, call_idx)
In bpf_flow_return(), caller->unwound is copied from exit->unwound, but
exit->speculative is not propagated to caller->speculative. As a result, st
is delivered back to the caller at call_idx + 1 with st.speculative == false.
In the caller, wouldn't this cause sanitize_mark_insn_seen() to mark
architecturally unreachable instructions as seen (bypassing
sanitize_dead_code()) and prevent do_check_block() from inserting nospec
barriers on recoverable errors because state->speculative is false? Also, in
flow_merge(), can a non-speculative state t->st from another call entry
subsume a speculative state s->st via bpf_state_subsumes() and call
flow_unite(flow, s->st.flow_entry, t->st.flow_entry), cross-delivering exits
between the two calls?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261105204513.2230344-1-alexei.starovoitov@gmail.com?part=21
next prev parent reply other threads:[~2026-11-05 22:45 UTC|newest]
Thread overview: 66+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-05 20:44 [PATCH bpf-next v2 00/35] bpf: A verifier that joins states Alexei Starovoitov
2026-11-05 20:44 ` [PATCH bpf-next v2 01/35] bpf: Add may_goto to the loops that the verifier marks Alexei Starovoitov
2026-11-05 21:14 ` sashiko-bot
2026-11-05 20:44 ` [PATCH bpf-next v2 02/35] bpf: Limit depth and time of recursive subprogs at run time Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 20:44 ` [PATCH bpf-next v2 03/35] bpf: Add union and widening of the range of a register Alexei Starovoitov
2026-11-05 21:34 ` bot+bpf-ci
2026-11-05 20:44 ` [PATCH bpf-next v2 04/35] bpf: Add join of registers and stack slots Alexei Starovoitov
2026-11-05 21:16 ` sashiko-bot
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 20:44 ` [PATCH bpf-next v2 05/35] bpf: Add join of verifier states Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 21:40 ` sashiko-bot
2026-11-05 20:44 ` [PATCH bpf-next v2 06/35] bpf: Tell which arguments of a call a join must leave alone Alexei Starovoitov
2026-11-05 20:44 ` [PATCH bpf-next v2 07/35] bpf: Add the data structures of the main pass over the control flow graph Alexei Starovoitov
2026-11-05 20:44 ` [PATCH bpf-next v2 08/35] bpf: Order the insns, find what stops a loop and where may_goto can go Alexei Starovoitov
2026-11-05 20:44 ` [PATCH bpf-next v2 09/35] bpf: Find the registers that hold a number or a pointer to arena Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 21:40 ` sashiko-bot
2026-11-05 20:44 ` [PATCH bpf-next v2 10/35] bpf: Find the registers and stack slots that later checks depend on Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 20:44 ` [PATCH bpf-next v2 11/35] bpf: Add the sets of states at an insn and the entries of functions Alexei Starovoitov
2026-11-05 20:44 ` [PATCH bpf-next v2 12/35] bpf: Compare states of different iterations of a loop Alexei Starovoitov
2026-11-05 20:44 ` [PATCH bpf-next v2 13/35] bpf: Decide which states are not joined Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 20:44 ` [PATCH bpf-next v2 14/35] bpf: Merge the state that arrives into the set at an insn Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 20:44 ` [PATCH bpf-next v2 15/35] bpf: Move states along edges, into loops and subprogs and out of them Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 20:44 ` [PATCH bpf-next v2 16/35] bpf: Make the state after a call from the caller's state and an exit state Alexei Starovoitov
2026-11-05 20:44 ` [PATCH bpf-next v2 17/35] bpf: Enter and leave loops in the main pass, build flow.c Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 20:44 ` [PATCH bpf-next v2 18/35] bpf: Tell the passes over the graph where an added may_goto goes Alexei Starovoitov
2026-11-05 22:19 ` sashiko-bot
2026-11-05 20:44 ` [PATCH bpf-next v2 19/35] bpf: Verify programs as a fixpoint over the control flow graph Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 22:45 ` sashiko-bot
2026-11-05 20:44 ` [PATCH bpf-next v2 20/35] bpf: Keep the count of a loop when a register has no closed form Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 20:44 ` [PATCH bpf-next v2 21/35] bpf: Verify static subprogs on their own Alexei Starovoitov
2026-11-05 21:51 ` bot+bpf-ci
2026-11-05 22:45 ` sashiko-bot [this message]
2026-11-05 20:44 ` [PATCH bpf-next v2 22/35] bpf: Allow recursive subprogs Alexei Starovoitov
2026-11-05 21:51 ` bot+bpf-ci
2026-11-05 23:09 ` sashiko-bot
2026-11-05 20:45 ` [PATCH bpf-next v2 23/35] bpf: Remove is_state_visited(), bpf_compute_scc() and iter.depth Alexei Starovoitov
2026-11-05 20:45 ` [PATCH bpf-next v2 24/35] bpf: Remove precision tracking Alexei Starovoitov
2026-11-05 20:45 ` [PATCH bpf-next v2 25/35] bpf: Remove prune points, levels of state comparison and pop_stack() Alexei Starovoitov
2026-11-05 20:45 ` [PATCH bpf-next v2 26/35] bpf: Update comments about paths, pruning and explored states Alexei Starovoitov
2026-11-05 20:45 ` [PATCH bpf-next v2 27/35] bpf: Track a pointer to one of a set of functions Alexei Starovoitov
2026-11-05 20:45 ` [PATCH bpf-next v2 28/35] bpf: callx reads what the subprogs whose address is taken read Alexei Starovoitov
2026-11-05 20:45 ` [PATCH bpf-next v2 29/35] selftests/bpf: Adjust tests to log lines that are gone Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 20:45 ` [PATCH bpf-next v2 30/35] selftests/bpf: Adjust tests to new verdicts, messages and order Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 20:45 ` [PATCH bpf-next v2 31/35] selftests/bpf: Test states that are not joined Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 20:45 ` [PATCH bpf-next v2 32/35] selftests/bpf: Test static subprogs that are verified on their own Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 20:45 ` [PATCH bpf-next v2 33/35] selftests/bpf: Test loops that are counted, walked or widened Alexei Starovoitov
2026-11-05 20:45 ` [PATCH bpf-next v2 34/35] selftests/bpf: Test progs that run out of budget Alexei Starovoitov
2026-11-05 21:35 ` bot+bpf-ci
2026-11-05 23:19 ` sashiko-bot
2026-11-05 20:45 ` [PATCH bpf-next v2 35/35] selftests/bpf: Test callx of one of a set of functions Alexei Starovoitov
2026-11-05 23:17 ` sashiko-bot
2026-11-06 0:34 ` [PATCH bpf-next v2 00/35] bpf: A verifier that joins states Alexei Starovoitov
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-200328@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=alexei.starovoitov@gmail.com \
--cc=bpf@vger.kernel.org \
--cc=sashiko-reviews@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox