From: sashiko-bot@kernel.org
To: "Alexei Starovoitov" <alexei.starovoitov@gmail.com>, bpf@vger.kernel.org
Subject: Re: [PATCH bpf-next v4 19/46] bpf: Verify programs as a fixpoint over the control flow graph
Date: Sat, 07 Nov 2026 04:40:55 +0000 [thread overview]
Message-ID: <sashiko-outbox-202591@kernel.org> (raw)
In-Reply-To: <20261107033758.1547725-20-alexei.starovoitov@gmail.com>
Thank you for your contribution! Sashiko AI review found 6 potential
issues to consider:
- [High] bpf: missing loop_stack refs update in
release_reference_state()
- [High] bpf: missing bpf_head_guard_target() edge in update_insn() and
need_const
- [Medium] bpf: check_loop_guard() rejects offloaded progs during
patience walk
- [Medium] bpf: unreset loop_guard flag and ignored fatal errors in
do_check_all()
- [Medium] bpf: do_check_block() mishandles nospec ordering with
check_loop_guard()
- [Medium] bpf: do_check_all() fails to reset sub_aux->unreliable on
round restart
--
commit 493d8e622b4ec09b90d62f364aafffd0843d557b
Author: Alexei Starovoitov <ast@kernel.org>
bpf: Verify programs as a fixpoint over the control flow graph
This commit replaces path-by-path symbolic execution in do_check() with a
fixpoint iteration over basic blocks that joins compatible states and
inserts may_goto guards into loops without proven bounds.
> diff --git a/kernel/bpf/liveness.c b/kernel/bpf/liveness.c
> index 6a2ee407412b8..60296eb880dbe 100644
> --- a/kernel/bpf/liveness.c
> +++ b/kernel/bpf/liveness.c
> @@ -2837,6 +2837,11 @@ int bpf_compute_live_registers(struct bpf_verifier_env *env)
> */
> insn_aux[i].zext_dst = def32 >= 0 && (mask_hi(out) & BIT(def32));
> }
> + /* the exit that stands for 'r0 = 0; exit' may be dead code, see push_budget_exit() */
> + for (i = 0; i < env->subprog_cnt; i++)
> + if (env->subprog_info[i].bail_insn >= 0)
> + insn_aux[env->subprog_info[i].bail_insn].live_regs_before |=
> + BIT(BPF_REG_0) | (bpf_ret_reg_pair(env, i) ? BIT(BPF_REG_2) : 0);
>
> out:
> kvfree(state);
[Severity: High]
While bpf_compute_live_registers() accounts for the synthetic edge from a
loop header to bpf_head_guard_target(env, insn_idx), do stack-slot liveness
in update_insn() and constant propagation in flow_compute_need_const() miss
that edge?
kernel/bpf/liveness.c:update_insn() {
...
succ = bpf_insn_successors(env, insn_idx);
if (succ->cnt == 0)
return false;
/* All instructions of one frame array share the same mask width */
for (s = 0; s < succ->cnt; ++s) {
unsigned long *succ_live;
succ_live = rel_mask(fm, relative_idx(instance, succ->items[s]), FM_LIVE_BEFORE);
for (w = 0; w < fm->words; w++)
new_after[w] |= succ_live[w];
}
...
}
When a loop has guard_at_header set and cannot unwind (for example, while a
lock or reference is held across the loop), check_loop_guard() pushes the
state at the loop header to guard_to before do_check_insn() runs at the
header. If a stack slot is written before the loop, overwritten at the
header or on all in-loop paths to guard_to, and then read at guard_to,
update_insn() marks the slot dead at the loop header.
bpf_clean_verifier_state() in flow_arrive() then clears its spilled_ptr and
marks it STACK_POISON at the header before check_loop_guard() pushes that
state to guard_to.
At guard_to, reading the slot fails with "slot poisoned by dead code
elimination" on a valid program. Moreover, when env->allow_uninit_stack is
set and the slot held a spilled pointer before the loop that in-loop paths
overwrite with a scalar, join_slot() joins STACK_POISON from the header
guard with the scalar slot into STACK_INVALID, allowing the pointer left on
the stack when may_goto fires at the header to be read as a scalar at
guard_to.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 62a58f38f4e22..0e35f4454c805 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -1605,9 +1617,14 @@ static int acquire_irq_state(struct bpf_verifier_env *env, int insn_idx)
>
> static void release_reference_state(struct bpf_verifier_state *state, int idx)
> {
> - int last_idx;
> + int last_idx, fr;
> size_t rem;
>
> + /* the frames that were entered with it don't hold it ever since */
> + for (fr = 0; fr <= state->curframe; fr++)
> + if (idx < state->frame[fr]->entry_refs)
> + state->frame[fr]->entry_refs--;
> +
> /* IRQ state requires the relative ordering of elements remaining the
> * same, since it relies on the refs array to behave as a stack, so that
> * it can detect out-of-order IRQ restore. Hence use memmove to shift
[Severity: High]
Does release_reference_state() also need to decrement
state->frame[fr]->loop_stack[l].refs when idx <
state->frame[fr]->loop_stack[l].refs?
loop_stack_push() records env->cur_state->acquired_refs in
loop_stack[cnt].refs, and check_refs_taken_in_loop() scans from
entry->refs to st->acquired_refs - 1 at loop backedges:
kernel/bpf/verifier.c:check_refs_taken_in_loop() {
...
for (i = entry->refs; i < st->acquired_refs; i++) {
if (st->refs[i].type != REF_TYPE_PTR)
continue;
...
}
If a reference acquired before the loop (idx < entry->refs) is released
inside the loop and a new reference is acquired and leaked in the same
iteration, memmove() shifts the new reference down to entry->refs - 1 while
entry->refs stays unchanged, so check_refs_taken_in_loop() skips it at the
backedge. In addition, loop_stack_safe() in kernel/bpf/states.c does not
check old_stack[i].refs == cur_stack[i].refs (unlike join_loop_stack()).
[ ... ]
> @@ -21630,14 +21713,204 @@ int bpf_flow_loop_enter(struct bpf_verifier_env *env, bool *backedge, bool *drop
[ ... ]
> +static int check_loop_guard(struct bpf_verifier_env *env)
> +{
> + struct bpf_insn_aux_data *aux = &env->insn_aux_data[env->insn_idx];
> + struct bpf_verifier_state *branch;
> + struct bpf_func_state *frame = cur_func(env);
> + struct loop_stack_entry *entry;
> + struct bpf_loop *loop;
> + int h, err;
> +
> + h = bpf_loop_at_index(env, env->insn_idx);
> + /* The callback came back to the helper that calls it: the program is not in front of it. */
> + if (h < 0 || !frame->loop_stack_cnt || frame->callback_depth)
> + return 0;
> + loop = env->insn_aux_data[h].loop;
> + entry = &frame->loop_stack[frame->loop_stack_cnt - 1];
> + if (loop->rt_bounded || !env->insn_aux_data[env->insn_idx].guard_place ||
> + entry->loop_id != h)
> + return 0;
> + if (verifier_bug_if(!entry->terminates && !entry->guarded, env,
> + "nothing stops the loop at %d", h))
> + return -EFAULT;
> + if (!entry->guarded && !bpf_loop_walk_may_give_up(entry))
> + return 0;
> +
> + /* the device runs the insns that the driver was given, without may_goto */
> + if (bpf_prog_is_offloaded(env->prog->aux)) {
> + verbose(env, "loop at insn %d has no known bound in an offloaded program\n", h);
> + return -EINVAL;
> + }
[Severity: Medium]
As reported by bot+bpf-ci@kernel.org on v2 [1], does check_loop_guard()
reject offloaded programs during a patience walk even when the walk would
finish without needing a may_goto?
When flow_try_walk() starts walking a loop that bpf_compute_loop_iters()
could not count, entry->guarded is false, but bpf_loop_walk_may_give_up()
is true so that check_loop_guard() can track guard_no_unwind and
guard_edge_dropped in case the walk later gives up. Testing
bpf_prog_is_offloaded() before checking entry->guarded rejects the
offloaded program on the first trip of the walk, even if the walk would
reach the end of the loop and never add a may_goto.
[ ... ]
> @@ -21796,6 +22017,10 @@ static int do_check(struct bpf_verifier_env *env)
> if (state->speculative && insn_aux->nospec)
> goto process_bpf_exit;
>
> + err = check_loop_guard(env);
> + if (err)
> + return err;
> +
> err = do_check_insn(env, &do_print_state);
> if (error_recoverable_with_nospec(err) && state->speculative) {
> /* Prevent this speculative path from ever reaching the
[ ... ]
> @@ -21839,23 +22054,40 @@ static int do_check(struct bpf_verifier_env *env)
> process_bpf_exit:
> account_current_path(env);
> mark_verifier_state_scratched(env);
> - err = bpf_update_branch_counts(env, env->cur_state);
> - if (err)
> - return err;
> - err = pop_stack(env, &prev_insn_idx, &env->insn_idx,
> - pop_log);
> - if (err < 0) {
> - if (err != -ENOENT)
> - return err;
> - break;
> - } else {
> - do_print_state = true;
> - continue;
> - }
> + bpf_free_verifier_state(state, false);
> + memset(state, 0, sizeof(*state));
> + return flow_drain(env);
> + }
[Severity: Medium]
As reported by bot+bpf-ci@kernel.org on v2 [2], should check_loop_guard()
run before the state->speculative && insn_aux->nospec exit, and should a
speculative instruction that aborts with insn_aux->nospec = true discard
states it pushed onto env->head instead of draining them?
When bpf_insert_loop_guards() prepends a may_goto sequence to a guarded
instruction, patch_list_adj_aux() keeps insn_aux->nospec on the original
instruction after the prepended may_goto jump. At runtime, speculative
execution reaches the may_goto jump before the nospec barrier, but in
do_check_block() any speculative state reaching an instruction with
insn_aux->nospec already set (from an earlier path or a previous round in
do_check_all()) jumps to process_bpf_exit before check_loop_guard() can
verify the guard edge.
Conversely, when insn_aux->nospec is not yet set and do_check_insn() fails
with error_recoverable_with_nospec(err) on a speculative path, jumping to
process_bpf_exit calls flow_drain(env), which delivers both the guard
branch and any branch state pushed onto env->head by the aborted
do_check_insn() call before it failed.
[ ... ]
> @@ -23245,6 +23480,44 @@ static int do_check_main(struct bpf_verifier_env *env)
> return ret;
> }
>
> +/*
> + * What stops a loop is assumed before the main pass and checked in it, see flow_is_bound(). The
> + * main prog and global functions may get to the same loop. It has may_goto for all of them or
> + * for none, so all are done again when the assumption doesn't hold, whatever came out.
> + * The same when a loop gets may_goto, or its may_goto gets another way out, and what went through
> + * the loop before is not there any more, see bpf_flow_loop_gets_guards().
> + *
> + * A round that is dropped counts as paths that never run. What it found out about the insns, the
> + * subprogs and the prog stays. Each of these is made for more than one state that gets to an insn:
> + * it grows to the safe side, or another value is refused, or the last one that gets there sets it.
> + * Where the round got to is dropped with its verdict: the passes that follow take an insn that was
> + * seen for one that passed its checks, and a function that is called has to be safe.
> + */
> +static int do_check_all(struct bpf_verifier_env *env)
> +{
> + struct bpf_func_info_aux *sub_aux;
> + u64 log_pos = env->log.end_pos;
> + int i, ret;
> +
> + for (;;) {
> + bpf_flow_new_round(env);
> + ret = do_check_main(env);
> + ret = ret ?: do_check_subprogs(env);
> + if (!(env->restart && bpf_flow_loops_lost_bound(env)) && !env->guards_late)
> + return ret;
> + /* the log is dropped when a function is found to be safe */
> + bpf_vlog_reset(&env->log, min(log_pos, env->log.end_pos));
> + for (i = 0; i < env->prog->len; i++)
> + env->insn_aux_data[i].seen = 0;
[Severity: Medium]
When do_check_all() restarts a verification round, should it also clear
env->insn_aux_data[i].loop_guard and stop immediately on fatal errors such
as -EAGAIN, -ENOMEM, -EFAULT, or -E2BIG?
bpf_flow_guard_edge() sets aux[prev_insn_idx].loop_guard = true during a
round, and neither bpf_flow_new_round() nor do_check_all() clears it. If a
guard edge taken in round 1 is abandoned in round 2 renders an inner loop or
called static subprogram unreachable (seen == 0), bpf_insert_loop_guards()
still sees stale loop_guard == true on those dead instructions, patches in
may_goto, and appends 'r0 = 0; exit' with aux[i].seen = 1 forced on the
appended exit instructions.
Also, if env->guards_late or env->restart was set before do_check_main() or
do_check_subprogs() aborted with -EAGAIN (pending signal), -ENOMEM, -EFAULT
(verifier_bug), or -E2BIG (total_states limit), do_check_all() ignores ret,
wipes the error from env->log, and starts another round without resetting
env->total_states.
> + for (i = 1; env->prog->aux->func_info && i < env->subprog_cnt; i++) {
> + sub_aux = subprog_aux(env, i);
> + memset(sub_aux->called, 0, sizeof(sub_aux->called));
> + memset(sub_aux->verified, 0, sizeof(sub_aux->verified));
> + sub_aux->called_with_pkt_ptrs = false;
> + }
> + }
> +}
[Severity: Medium]
Should this loop also reset sub_aux->unreliable = false when discarding a
round?
In a round that gets restarted (for example when a loop header guard first
pushes a state to guard_to and a later state at the header sets
aux->guard_unset_reg = true and env->guards_late = true), the stale state
pushed to guard_to can reach a subprogram call with mismatched registers
and cause btf_check_subprog_call() to set
prog->aux->func_info_aux[subprog].unreliable = true:
kernel/bpf/verifier.c:btf_check_subprog_call() {
...
if (prog->aux->func_info_aux[subprog].unreliable)
return -EINVAL;
err = btf_check_func_arg_match(env, subprog, btf);
if (err)
prog->aux->func_info_aux[subprog].unreliable = true;
...
}
In the restarted round, btf_check_subprog_call() sees unreliable == true
and immediately returns -EINVAL for a global function (or leaves a static
subprogram marked unreliable for fentry/fexit attachment), rejecting a
valid program after its diagnostic log was already wiped by
bpf_vlog_reset().
[1] https://lore.kernel.org/r/30511ed9caaa7fab02343ad6adbd72f0c7d689b7ae89d4b0c37bdf2beb45a414@mail.kernel.org
[2] https://lore.kernel.org/r/30511ed9caaa7fab02343ad6adbd72f0c7d689b7ae89d4b0c37bdf2beb45a414@mail.kernel.org
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261107033758.1547725-1-alexei.starovoitov@gmail.com?part=19
next prev parent reply other threads:[~2026-11-07 4:40 UTC|newest]
Thread overview: 72+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-07 3:37 [PATCH bpf-next v4 00/46] bpf: A verifier that joins states Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 01/46] bpf: Add may_goto to the loops that the verifier marks Alexei Starovoitov
2026-11-07 4:12 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 02/46] bpf: Limit depth and time of recursive subprogs at run time Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 03/46] bpf: Add union and widening of the range of a register Alexei Starovoitov
2026-11-07 4:19 ` bot+bpf-ci
2026-11-07 3:37 ` [PATCH bpf-next v4 04/46] bpf: Add join of registers and stack slots Alexei Starovoitov
2026-11-07 4:18 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 05/46] bpf: Add join of verifier states Alexei Starovoitov
2026-11-07 4:18 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 06/46] bpf: Tell which arguments of a call a join must leave alone Alexei Starovoitov
2026-11-07 4:19 ` bot+bpf-ci
2026-11-07 3:37 ` [PATCH bpf-next v4 07/46] bpf: Add the data structures of the main pass over the control flow graph Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 08/46] bpf: Order the insns, find what stops a loop and where may_goto can go Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 09/46] bpf: Find the registers that hold a number or a pointer to arena Alexei Starovoitov
2026-11-07 4:11 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 10/46] bpf: Find the registers and stack slots that later checks depend on Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 11/46] bpf: Add the sets of states at an insn and the entries of functions Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 12/46] bpf: Compare states of different iterations of a loop Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 13/46] bpf: Decide which states are not joined Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 14/46] bpf: Merge the state that arrives into the set at an insn Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 15/46] bpf: Move states along edges, into loops and subprogs and out of them Alexei Starovoitov
2026-11-07 4:19 ` bot+bpf-ci
2026-11-07 3:37 ` [PATCH bpf-next v4 16/46] bpf: Make the state after a call from the caller's state and an exit state Alexei Starovoitov
2026-11-07 4:12 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 17/46] bpf: Enter and leave loops in the main pass, build flow.c Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 18/46] bpf: Tell the passes over the graph where an added may_goto goes Alexei Starovoitov
2026-11-07 4:10 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 19/46] bpf: Verify programs as a fixpoint over the control flow graph Alexei Starovoitov
2026-11-07 4:33 ` bot+bpf-ci
2026-11-07 4:40 ` sashiko-bot [this message]
2026-11-07 3:37 ` [PATCH bpf-next v4 20/46] bpf: Keep the count of a loop when a register has no closed form Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 21/46] bpf: Verify static subprogs on their own Alexei Starovoitov
2026-11-07 4:53 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 22/46] bpf: Allow recursive subprogs Alexei Starovoitov
2026-11-07 4:33 ` bot+bpf-ci
2026-11-07 4:48 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 23/46] bpf: Remove is_state_visited(), bpf_compute_scc() and iter.depth Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 24/46] bpf: Remove precision tracking Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 25/46] bpf: Remove prune points, levels of state comparison and pop_stack() Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 26/46] bpf: Update comments about paths, pruning and explored states Alexei Starovoitov
2026-11-07 4:03 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 27/46] bpf: Track a pointer to one of a set of functions Alexei Starovoitov
2026-11-07 4:31 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 28/46] bpf: callx reads what the subprogs whose address is taken read Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 29/46] bpf: Keep the first iteration of a counted loop apart Alexei Starovoitov
2026-11-07 4:34 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 30/46] bpf: What a call returns in each iteration is like what the loop loads Alexei Starovoitov
2026-11-07 4:26 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 31/46] bpf: Find where the jumps of a loop stop a number Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 32/46] bpf: A loop is one iteration shorter behind the jump that counts it Alexei Starovoitov
2026-11-07 4:19 ` bot+bpf-ci
2026-11-07 4:28 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 33/46] bpf: Give the places of a back edge to the jumps that count Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 34/46] bpf: Count a loop whose exit tests a copy of the counter Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 35/46] bpf: Allow tail calls and LD_ABS in subprogs that return void Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 36/46] bpf: A part of a pointer that is spilled is a number Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 37/46] bpf: 32-bit add to a pointer and pointer op pointer make " Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 38/46] selftests/bpf: Adjust tests to log lines that are gone Alexei Starovoitov
2026-11-07 4:19 ` bot+bpf-ci
2026-11-07 3:37 ` [PATCH bpf-next v4 39/46] selftests/bpf: Adjust tests to new verdicts, messages and order Alexei Starovoitov
2026-11-07 4:24 ` sashiko-bot
2026-11-07 3:37 ` [PATCH bpf-next v4 40/46] selftests/bpf: Test states that are not joined Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 41/46] selftests/bpf: Test static subprogs that are verified on their own Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 42/46] selftests/bpf: Test loops that are counted, walked or widened Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 43/46] selftests/bpf: Test progs that run out of budget Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 44/46] selftests/bpf: Test callx of one of a set of functions Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 45/46] selftests/bpf: Test pointers that become numbers and subprogs that return void Alexei Starovoitov
2026-11-07 3:37 ` [PATCH bpf-next v4 46/46] selftests/bpf: Test progs without bpf_for(), zero, can_loop and such Alexei Starovoitov
2026-11-07 4:19 ` bot+bpf-ci
2026-11-07 4:37 ` sashiko-bot
2026-11-07 5:00 ` [PATCH bpf-next v4 00/46] bpf: A verifier that joins states patchwork-bot+netdevbpf
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-202591@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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.