From: sashiko-bot@kernel.org
To: "Kumar Kartikeya Dwivedi" <memxor@gmail.com>, bpf@vger.kernel.org
Subject: Re: [RFC PATCH bpf-next v1 05/11] bpf: Let the callback of bpf_loop unwind
Date: Wed, 28 Oct 2026 01:19:03 +0000 [thread overview]
Message-ID: <sashiko-outbox-187310@kernel.org> (raw)
In-Reply-To: <25728094090d9bf3e4cd88feecec336f0493a5a9.1793147364.git.memxor@gmail.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] bpf: inline_bpf_loop() emits dangling jump to dead unwind pad
- [Medium] bpf: clobbering of caller R2 in prepare_func_exit() on callback unwind
--
commit 342597e9c392b90ccb12e07eaf21a9e820d7f364
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Let the callback of bpf_loop unwind
This commit adds an unwind landing pad to bpf_loop() using bpf_call_cb() and
bpf_unwind_leave(), and updates helper_has_cb_pad() so the verifier permits
bpf_loop() callbacks to unwind.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index ac9dbe0821811..4b4d8479fa87b 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -11402,10 +11402,15 @@ static int btf_check_subprog_call(struct bpf_verifier_env *env, int subprog)
> return err;
> }
>
> -/* The helper calls its callback with a pad, see bpf_call_cb(). None yet. */
> +/* The helper calls its callback with a pad, see bpf_call_cb() */
> static bool helper_has_cb_pad(enum bpf_func_id func_id)
> {
> - return false;
> + switch (func_id) {
> + case BPF_FUNC_loop:
> + return true;
> + default:
> + return false;
> + }
> }
[Severity: Critical]
When helper_has_cb_pad(BPF_FUNC_loop) returns true, can inline_bpf_loop() in
kernel/bpf/fixups.c emit a live BPF_JMP32_A instruction that jumps to a dead
landing pad which is subsequently removed by dead code elimination, or leave a
stray BPF_UNWIND_GOTO after a load instruction?
In kernel/bpf/verifier.c:check_unwind_insns(), subprog_info[subprog].can_unwind
is set statically whenever the callback contains an unwind_exit instruction,
and check_cb_unwind() then requires an unwind_goto instruction at position + 1
immediately after the bpf_loop() call.
If a privileged program calls bpf_loop() with flags == 0 and a single callback
target (so fit_for_inline is true), and the callback does not dynamically take
its unwind_exit path during verification of that call (for example, when
guarded by callback_ctx or loop index, or when nr_loops == 0), state->unwound
is never set at position + 1. In check_cond_jmp_op(), the unwind_goto branch
to the caller's landing pad (caller_pad = position + 2 + unwind_goto->off) is
never taken, leaving env->insn_aux_data[caller_pad].seen == 0.
In kernel/bpf/verifier.c:bpf_check(), bpf_optimize_bpf_loop() runs before dead
code elimination:
if (ret == 0)
ret = bpf_optimize_bpf_loop(env);
if (is_priv) {
if (ret == 0)
bpf_opt_hard_wire_dead_code_branches(env);
if (ret == 0)
ret = bpf_opt_remove_dead_code(env);
Inside kernel/bpf/fixups.c:inline_bpf_loop(), pad is computed without checking
whether the target of unwind_goto at position + 1 was actually visited:
bool pad = env->subprog_info[callback_subprogno].can_unwind &&
bpf_is_unwind_goto(&env->prog->insnsi[position + 1]);
...
if (pad) {
struct bpf_insn *unwind_goto = &new_prog->insnsi[position + cnt];
/* the jump of the pad, one insn before, takes the target of the unwind_goto */
new_prog->insnsi[position + cnt - 1].imm = unwind_goto->off + 1;
/* and the unwind_goto after the call of bpf_loop follows no call anymore */
*unwind_goto = BPF_JMP_A(0);
}
Because adjust_insn_aux_data() copies the call instruction's non-zero seen
count to all patched instructions, the inlined pad and its trailing
BPF_JMP32_A are marked seen != 0. bpf_opt_hard_wire_dead_code_branches() does
not fold the inlined BPF_UNWIND_GOTO (its target is the inlined pad, which has
seen != 0) and ignores the unconditional BPF_JMP32_A.
When bpf_opt_remove_dead_code() then deletes the unvisited caller_pad via
verifier_remove_insns() -> bpf_remove_insns() -> bpf_adj_branches() ->
bpf_adj_delta_to_off() in kernel/bpf/core.c:
if (curr < pos && curr + off + 1 >= end_old)
off += delta;
else if (curr >= end_new && curr + off + 1 < end_new)
off -= delta;
BPF_JMP32_A's offset is not adjusted because its target falls inside the
removed range [pos, end_old). If caller_pad was at the end of the subprogram,
func[i]->len shrinks while BPF_JMP32_A still jumps past the end of the
subprogram, causing out-of-bounds reads of addrs[i + insn->imm] in do_jit() in
arch/x86/net/bpf_jit_comp.c and ctx->offset[bpf_insn + off] in
bpf2a64_offset() in arch/arm64/net/bpf_jit_comp.c, or jumping into post-return
int3 padding or subsequent instructions.
Additionally, if the callback has can_unwind == false and an unwind_goto
follows the bpf_loop() call (which check_unwind_insns() and check_cb_unwind()
allow), pad is false in inline_bpf_loop(), so *unwind_goto = BPF_JMP_A(0) is
skipped, leaving BPF_UNWIND_GOTO immediately after BPF_LDX_MEM instead of a
call instruction when the landing pad is shared (seen != 0) or !is_priv.
[Severity: Medium]
What happens in prepare_func_exit() in kernel/bpf/verifier.c when a bpf_loop()
callback subprogram with a 9 to 16 byte BTF return type (ret_reg_pair == true)
unwinds via unwind_exit?
In kernel/bpf/verifier.c:bpf_compute_subprog_ret_regs(), any static subprogram
whose BTF return type has size in [9, 16] bytes gets ret_reg_pair = true, and
neither check_func_args() nor btf_check_subprog_call() rejects such a
subprogram when passed as ARG_PTR_TO_FUNC.
With helper_has_cb_pad(BPF_FUNC_loop) now returning true, the
callee->in_callback_fn && unwinds path in prepare_func_exit() becomes
reachable:
callee = state->frame[state->curframe];
r0 = &callee->regs[BPF_REG_0];
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;
}
...
for (i = 0; i < nregs; i++) {
u32 regno = ret_regs[i];
bpf_diag_mod_begin(env, &caller->regs[regno], &callee->regs[regno],
BPF_DIAG_MOD_WRITE);
caller->regs[regno] = callee->regs[regno];
bpf_diag_mod_end(env);
}
Even though bpf_call_cb() in bpf_loop() only propagates R0 to the landing pad,
prepare_func_exit() uses nregs = 2 (ret_regs = { BPF_REG_0, BPF_REG_2 })
without checking !callee->in_callback_fn.
First, because set_loop_callback_state() initializes the callback's R2 to
caller->regs[BPF_REG_3] (callback_ctx), passing a stack pointer as
callback_ctx causes the PTR_TO_STACK check on callee->regs[BPF_REG_2] to
falsely reject the unwind if the callback did not overwrite R2.
Second, prepare_func_exit() copies callee->regs[BPF_REG_2] into
caller->regs[BPF_REG_2] before setting *insn_idx = callee->callsite to revisit
the bpf_loop() call instruction. When check_helper_call() re-runs with
env->cur_state->unwound == true, check_func_args() re-validates
caller->regs[BPF_REG_1..5] before caller-saved registers are scrubbed. Because
caller->regs[BPF_REG_2] (the callback_fn argument) was overwritten with the
callback's R2, could check_func_args() fail verification or update
meta.subprogno in update_loop_inline_state() to a different subprogram?
--
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1793147364.git.memxor@gmail.com?part=5
next prev parent reply other threads:[~2026-10-28 1:19 UTC|newest]
Thread overview: 46+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-28 0:44 [RFC PATCH bpf-next v1 00/11] Unwinding for synchronous callbacks Kumar Kartikeya Dwivedi
2026-10-28 0:44 ` [RFC PATCH bpf-next v1 01/11] bpf: Give a helper a way to call its callback with a pad Kumar Kartikeya Dwivedi
2026-10-28 1:20 ` sashiko-bot
2026-10-28 2:15 ` Kumar Kartikeya Dwivedi
2026-10-28 1:39 ` bot+bpf-ci
2026-10-28 2:15 ` Kumar Kartikeya Dwivedi
2026-10-28 0:44 ` [RFC PATCH bpf-next v1 02/11] bpf, arm64: Let the pad after a helper call have r0 Kumar Kartikeya Dwivedi
2026-10-28 1:02 ` sashiko-bot
2026-10-28 2:15 ` Kumar Kartikeya Dwivedi
2026-10-28 0:44 ` [RFC PATCH bpf-next v1 03/11] bpf: Let a callback unwind into the pad of its helper Kumar Kartikeya Dwivedi
2026-10-28 1:31 ` sashiko-bot
2026-10-28 2:15 ` Kumar Kartikeya Dwivedi
2026-10-28 1:39 ` bot+bpf-ci
2026-10-28 2:15 ` Kumar Kartikeya Dwivedi
2026-10-28 0:44 ` [RFC PATCH bpf-next v1 04/11] bpf: Give the inlined bpf_loop a pad Kumar Kartikeya Dwivedi
2026-10-28 1:10 ` sashiko-bot
2026-10-28 2:15 ` Kumar Kartikeya Dwivedi
2026-10-28 0:44 ` [RFC PATCH bpf-next v1 05/11] bpf: Let the callback of bpf_loop unwind Kumar Kartikeya Dwivedi
2026-10-28 1:19 ` sashiko-bot [this message]
2026-10-28 2:15 ` Kumar Kartikeya Dwivedi
2026-10-28 1:25 ` bot+bpf-ci
2026-10-28 2:15 ` Kumar Kartikeya Dwivedi
2026-10-28 0:44 ` [RFC PATCH bpf-next v1 06/11] bpf: Let the callback of bpf_for_each_map_elem unwind Kumar Kartikeya Dwivedi
2026-10-28 1:25 ` bot+bpf-ci
2026-10-28 2:15 ` Kumar Kartikeya Dwivedi
2026-10-28 0:44 ` [RFC PATCH bpf-next v1 07/11] bpf: Let the callback of bpf_find_vma unwind Kumar Kartikeya Dwivedi
2026-10-28 1:08 ` sashiko-bot
2026-10-28 2:15 ` Kumar Kartikeya Dwivedi
2026-10-28 0:44 ` [RFC PATCH bpf-next v1 08/11] bpf: Let the callback of bpf_user_ringbuf_drain unwind Kumar Kartikeya Dwivedi
2026-10-28 1:01 ` sashiko-bot
2026-10-28 1:04 ` Kumar Kartikeya Dwivedi
2026-10-28 0:44 ` [RFC PATCH bpf-next v1 09/11] bpf: Let the comparator of bpf_rbtree_add unwind Kumar Kartikeya Dwivedi
2026-10-28 1:21 ` sashiko-bot
2026-10-28 2:15 ` Kumar Kartikeya Dwivedi
2026-10-28 1:25 ` bot+bpf-ci
2026-10-28 2:15 ` Kumar Kartikeya Dwivedi
2026-10-28 0:44 ` [RFC PATCH bpf-next v1 10/11] selftests/bpf: Test callbacks that unwind Kumar Kartikeya Dwivedi
2026-10-28 1:25 ` bot+bpf-ci
2026-10-28 2:15 ` Kumar Kartikeya Dwivedi
2026-10-28 0:44 ` [RFC PATCH bpf-next v1 11/11] selftests/bpf: Test what a callback unwinds with Kumar Kartikeya Dwivedi
2026-10-28 5:06 ` [RFC PATCH bpf-next v1 00/11] Unwinding for synchronous callbacks Alexei Starovoitov
2026-10-28 10:20 ` Kumar Kartikeya Dwivedi
2026-10-28 20:17 ` Alexei Starovoitov
2026-10-28 21:42 ` Kumar Kartikeya Dwivedi
2026-10-28 23:12 ` Eduard Zingerman
2026-10-29 0:14 ` 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-187310@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=memxor@gmail.com \
--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