From: sashiko-bot@kernel.org
To: "Kumar Kartikeya Dwivedi" <memxor@gmail.com>, bpf@vger.kernel.org
Subject: Re: [PATCH bpf-next v1 13/29] bpf: Track held resources in hidden stack slots
Date: Wed, 11 Nov 2026 16:12:52 +0000 [thread overview]
Message-ID: <sashiko-outbox-209264@kernel.org> (raw)
In-Reply-To: <0b8d0a69ecb9f74ed3e3645bc7dccbc022a83cc2.1794399387.git.memxor@gmail.com>
Thank you for your contribution! Sashiko AI review found 6 potential
issues to consider:
- [High] bpf: emit_entries() hides arena stack adjustment from recursion
check
- [High] bpf: RCU iterator outlives section when subprog unlocks and
throws
- [High] bpf: double-free on unwind due to wrong slot cleared in
emit_release()
- [High] bpf: non-LIFO lock unwind in bpf_frame_desc_build() corrupts
IRQ and lock state
- [High] bpf: hidden slot overwritten when subprog call returns NULL in
frame_desc
- [Medium] bpf: duplicate check_max_stack_depth() inflates recursive
frame depth
--
commit 197c825766b69a79fd4f8ec64eb1bed1f952135c
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Track held resources in hidden stack slots
This commit tracks held kernel resources, locks, and RCU or preemption
sections in hidden stack slots and builds per-subprogram frame descriptors so
bpf_throw() can release them during unwinding.
> diff --git a/kernel/bpf/frame_desc.c b/kernel/bpf/frame_desc.c
> new file mode 100644
> index 0000000000000..b0db3feeb9b0f
> --- /dev/null
> +++ b/kernel/bpf/frame_desc.c
[ ... ]
> +int bpf_frame_desc_record_return(struct bpf_verifier_env *env, struct bpf_verifier_state *st,
> + int idx, int callsite, const struct bpf_reg_state *r0)
> +{
> + struct bpf_reference_state *ref;
> + s16 adj = 0;
> + int acq;
> +
> + if (!env->describe_frames || st->speculative)
> + return 0;
> + ref = idx < 0 ? NULL : &st->refs[idx];
> + if (!ref || !ref->res) {
> + if (r0->type != SCALAR_VALUE || !tnum_equals_const(r0->var_off, 0))
> + env->insn_aux_data[callsite].res_none = true;
> + return 0;
> + }
[Severity: High]
Can a second call at the same callsite that returns NULL overwrite a live
handle in the callsite's hidden stack slot?
emit_acquire_after() unconditionally stores R0 into the callsite's slot
after every return. If a subprog can return either an acquired reference or
NULL (R0 == 0, idx < 0), and the caller invokes that callsite a second time
while still holding the reference from the first call,
bpf_frame_desc_record_return() returns 0 without checking whether st->refs[]
already holds a reference from that callsite. At runtime, the second return
writes 0 into the hidden slot, so a later bpf_throw() sees NULL and leaks the
object from the first call.
[ ... ]
> +static int emit_release(struct bpf_verifier_env *env, struct bpf_insn *buf,
> + const struct res_item *it, int n)
> +{
> + const struct bpf_res_rel *r = &env->res_rels[it->rec];
> + int i, cnt = 0, reg = r->reg, per = r->adj ? 5 : 4;
> +
> + if (r->flags & BPF_RES_F_DEREF) {
> + buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_0, r->reg, 0);
> + reg = BPF_REG_0;
> + }
> + for (i = 0; i < n; i++) {
> + s16 off = env->res_acqs[env->res_rels[it[i].rec].acq].off;
> + bool last = i == n - 1;
> +
> + buf[cnt++] = BPF_LDX_MEM(BPF_DW, BPF_REG_AX, BPF_REG_FP, off);
> + if (r->adj)
> + buf[cnt++] = BPF_ALU64_IMM(BPF_ADD, BPF_REG_AX, r->adj);
> + buf[cnt++] = BPF_JMP_REG(BPF_JNE, BPF_REG_AX, reg, last ? 1 : 2);
> + buf[cnt++] = BPF_ST_MEM(BPF_DW, BPF_REG_FP, off, 0);
> + /* past the slots that follow: the last one has no jump */
> + if (!last)
> + buf[cnt++] = BPF_JMP_A((n - 1 - i) * per - 1);
> + }
> + return cnt;
> +}
[Severity: High]
Can emit_release() clear the wrong hidden slot when a frame holds two
references to the same underlying pointer?
Suppose a frame acquires a = bpf_obj_new(...) (acq 0, slot0) and
b = bpf_refcount_acquire(a) (acq 1, slot1), so both slots hold the same
pointer value p. If a first release call R_A takes a on one path and b on
another, R_A is recorded with both acq 0 and acq 1. On the path that passes
b to R_A and then passes a to a second release call R_B, the verifier removes
b's ref at R_A and records only acq 0 at R_B.
At runtime on that path, R_A checks slot0 first, sees slot0 == p, clears
slot0, and jumps past slot1. R_B then only checks slot0 (already 0), leaving
p in slot1 after both references have been dropped, so a subsequent
bpf_throw() releases p a third time and causes a double free or refcount
underflow.
[ ... ]
> +static int emit_entries(struct bpf_verifier_env *env)
> +{
[ ... ]
> + for (k = 0; k < ARRAY_SIZE(sub->section_off); k++)
> + if (sub->section_off[k])
> + buf[cnt++] = BPF_ST_MEM(BPF_DW, BPF_REG_FP, sub->section_off[k], 0);
> + if (!cnt)
> + continue;
> + ret = bpf_patch_list_insert(env, sub->start, buf, cnt);
> + env->frame_desc_insns += cnt;
> + }
> + ret = ret ?: bpf_patch_list_commit(env);
[Severity: High]
Does inserting slot-zeroing instructions at sub->start hide a recursive
subprog's arena stack adjustment from bpf_insert_recursion_checks()?
bpf_frame_desc_emit() runs before bpf_insert_recursion_checks() and commits
the prologue stores at sub->start, which moves sub->start in front of the
subprog's leading is_arena_sp_adj() instruction:
kernel/bpf/fixups.c:bpf_insert_recursion_checks() {
...
/* The exit drops the frame in the stack in arena. Take it before the check. */
if (is_arena_sp_adj(&env->prog->insnsi[si[s].start])) {
buf[n++] = env->prog->insnsi[si[s].start];
...
}
Because si[s].start is now a BPF_ST_MEM instruction instead of
is_arena_sp_adj(), bpf_insert_recursion_checks() does not hoist the arena
frame allocation ahead of the recursion check's bailout exit (and also looks
up live_regs_before at the wrong offset). bpf_insert_arena_stack() still
prepends r12 -= arena_stack_depth to every BPF_EXIT in the subprog, including
the bailout exit, so hitting the recursion or may_goto budget check decrements
BPF_REG_ARENA_SP without the matching entry increment and corrupts the
caller's arena stack pointer.
[ ... ]
> +int bpf_frame_desc_build(struct bpf_verifier_env *env)
> +{
[ ... ]
> + for (i = 0; i < 2 * env->res_acq_cnt; i++) {
> + const struct bpf_res_acq *a = &env->res_acqs[i % env->res_acq_cnt];
> + bool first = i < env->res_acq_cnt;
> +
> + if (acq_subprog(env, a, new_of) != s ||
> + resource_goes_first(&env->resources[a->res]) != first)
> + continue;
> + d->ent[d->nent++] = (struct bpf_frame_desc_entry) { .res = a->res, .off = a->off };
> + }
[Severity: High]
Should bpf_frame_desc_build() emit lock and IRQ flag entries in reverse
acquisition order?
d->ent[] is populated in forward env->res_acqs[] order, and
bpf_unwind_release_frame() walks d->ent[] forward during
BPF_UNWIND_PHASE_LOCKS. If a frame entered with interrupts enabled calls
bpf_spin_lock() (saving IF=1 in per-CPU irqsave_flags and disabling IRQs),
then calls bpf_res_spin_lock_irqsave() (saving IF=0 into the stack flag slot),
and throws, pass 0 of BPF_UNWIND_PHASE_LOCKS first runs bpf_spin_unlock(),
which restores IF=1 while the resilient spinlock is still held, and then runs
bpf_local_irq_restore() on the stack slot (since arch_irqs_disabled_flags()
is true, late is false), leaving local CPU interrupts disabled during sleeping
destructors in BPF_UNWIND_PHASE_OBJS and after bpf_throw() returns.
When a frame holds two nested resilient spinlocks, unlocking them in forward
order also pops the wrong entry in res_spin_unlock() (which clears
rqh->locks[rqh->cnt - 1]), corrupting the per-CPU held-lock stack.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index cf3cfc734ed4d..4d85a42a18d5f 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -11232,21 +11321,34 @@ static void invalidate_non_owning_refs(struct bpf_verifier_env *env)
> }));
> }
>
> -static void invalidate_rcu_protected_refs(struct bpf_verifier_env *env)
> +static int invalidate_rcu_protected_refs(struct bpf_verifier_env *env)
> {
> struct bpf_stack_state *stack;
> struct bpf_func_state *state;
> struct bpf_reg_state *reg;
> u32 clear_mask = (1 << STACK_SPILL) | (1 << STACK_ITER);
> + bool iter = false;
>
> bpf_for_each_reg_in_vstate_mask(env->cur_state, state, reg, stack, clear_mask, ({
> if (reg->type & MEM_RCU) {
> + iter |= stack && stack->slot_type[BPF_REG_SIZE - 1] == STACK_ITER;
> bpf_diag_mod_begin(env, reg, NULL, BPF_DIAG_MOD_WRITE);
> reg->type &= ~(MEM_RCU | PTR_MAYBE_NULL | NON_OWN_REF);
> reg->type |= PTR_UNTRUSTED;
> bpf_diag_mod_end(env);
> }
> }));
> + /*
> + * Nothing may destroy an RCU protected iterator once its section ended:
> + * not the program, which is refused when it leaks one, and not the
> + * unwinder, which destroys what a frame holds at a throw, see
> + * frame_desc.c.
> + */
> + if (iter && env->describe_frames) {
> + verbose(env, "an RCU protected iterator may not outlive its section in a program that throws\n");
> + return -EINVAL;
> + }
> + return 0;
> }
[Severity: High]
Does this miss RCU-protected iterators on caller frames when a subprog is
verified on its own and calls bpf_rcu_read_unlock() followed by bpf_throw()?
When call_func_on_its_own() verifies a subprog, the caller frames are not in
env->cur_state->frame[], so bpf_for_each_reg_in_vstate_mask() does not see a
caller's STACK_ITER slot (while env->cur_state->refs[] carries the inherited
iterator reference with frameno == -1). If the subprog then calls
bpf_throw() instead of returning normally, process_bpf_exit_full() skips
exit_func_on_its_own(), so bpf_frame_desc_record_exit() never checks
rcu_released_below and bpf_flow_return() never sets releases_inherited. The
subprog can then drop the caller's RCU lock (and sleep in a sleepable prog)
before throwing, causing the unwinder to destroy the caller's RCU-protected
iterator after its RCU section has ended.
[ ... ]
> @@ -25600,6 +25809,21 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,
> if (ret == 0)
> ret = check_max_stack_depth(env);
>
> + /*
> + * The pass has seen what every frame holds and the stack is final:
> + * reserve the slots the frames keep their handles in and emit the insns
> + * that keep them up to date, with the insns still those the main pass
> + * recorded.
> + */
> + if (ret == 0)
> + ret = bpf_frame_desc_resolve(env);
> + if (ret == 0)
> + ret = bpf_frame_desc_emit(env);
> +
> + /* Account for the slots reserved by frame descriptor resolution. */
> + if (ret == 0)
> + ret = check_max_stack_depth(env);
[Severity: Medium]
Does calling check_max_stack_depth() both before and after
bpf_frame_desc_resolve() inflate stack_depth for recursive subprogs that use
the arena stack?
During the first check_max_stack_depth() call, pad_recursive_frames()
overwrites si[s].stack_depth in place with max(si[s].stack_depth, frame).
bpf_frame_desc_resolve() -> bpf_reserve_stack_slots() then reserves hidden
slots on top of that already-padded stack_depth instead of the verified stack
depth, which can push stack_depth past env->stack_limit on the second
check_max_stack_depth() call and cause false verifier rejections.
--
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1794399387.git.memxor@gmail.com?part=13
next prev parent reply other threads:[~2026-11-11 16:12 UTC|newest]
Thread overview: 58+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-11 12:56 [PATCH bpf-next v1 00/29] Exceptions - Resource Cleanup - Redux Kumar Kartikeya Dwivedi
2026-11-11 12:56 ` [PATCH bpf-next v1 01/29] bpf: Record where JITs save callee-saved registers Kumar Kartikeya Dwivedi
2026-11-11 12:57 ` [PATCH bpf-next v1 02/29] bpf: Add bpf_throw_unwind() and an arch entry hook Kumar Kartikeya Dwivedi
2026-11-11 14:45 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 03/29] bpf, x86: Capture registers in a native bpf_throw() entry Kumar Kartikeya Dwivedi
2026-11-11 12:57 ` [PATCH bpf-next v1 04/29] bpf, arm64: " Kumar Kartikeya Dwivedi
2026-11-11 12:57 ` [PATCH bpf-next v1 05/29] bpf: Restore callee-saved registers from unwound frames Kumar Kartikeya Dwivedi
2026-11-11 14:41 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 06/29] bpf: Keep last insn's aux data in add_hidden_subprog() Kumar Kartikeya Dwivedi
2026-11-11 14:38 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 07/29] bpf: Add one allocator for hidden stack slots Kumar Kartikeya Dwivedi
2026-11-11 14:04 ` bot+bpf-ci
2026-11-11 15:15 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 08/29] bpf: Add flags and release kfuncs to the dtor registry Kumar Kartikeya Dwivedi
2026-11-11 13:33 ` bot+bpf-ci
2026-11-11 15:36 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 09/29] bpf: Register cleanup destructors for more types Kumar Kartikeya Dwivedi
2026-11-11 14:04 ` bot+bpf-ci
2026-11-11 15:00 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 10/29] bpf: Record where JITed frames keep the BPF stack Kumar Kartikeya Dwivedi
2026-11-11 12:57 ` [PATCH bpf-next v1 11/29] bpf: Give every reference an owner frame Kumar Kartikeya Dwivedi
2026-11-11 13:33 ` bot+bpf-ci
2026-11-11 12:57 ` [PATCH bpf-next v1 12/29] bpf: Release resources in hidden slots on bpf_throw() Kumar Kartikeya Dwivedi
2026-11-11 14:04 ` bot+bpf-ci
2026-11-11 15:30 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 13/29] bpf: Track held resources in hidden stack slots Kumar Kartikeya Dwivedi
2026-11-11 14:04 ` bot+bpf-ci
2026-11-11 16:12 ` sashiko-bot [this message]
2026-11-11 12:57 ` [PATCH bpf-next v1 14/29] bpf: Describe held resources at throw sites Kumar Kartikeya Dwivedi
2026-11-11 14:04 ` bot+bpf-ci
2026-11-11 12:57 ` [PATCH bpf-next v1 15/29] bpf: Describe handles in registers " Kumar Kartikeya Dwivedi
2026-11-11 14:04 ` bot+bpf-ci
2026-11-11 16:11 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 16/29] bpf: Describe locks " Kumar Kartikeya Dwivedi
2026-11-11 14:04 ` bot+bpf-ci
2026-11-11 16:04 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 17/29] bpf: Apply hidden slot limits only when a slot is needed Kumar Kartikeya Dwivedi
2026-11-11 16:31 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 18/29] bpf: Allow a callee to leave resources with its caller Kumar Kartikeya Dwivedi
2026-11-11 14:04 ` bot+bpf-ci
2026-11-11 16:41 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 19/29] bpf: Allow a callee to release its caller's resources Kumar Kartikeya Dwivedi
2026-11-11 17:02 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 20/29] bpf: Transfer a reference to the callee that releases it Kumar Kartikeya Dwivedi
2026-11-11 14:04 ` bot+bpf-ci
2026-11-11 16:57 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 21/29] bpf: Support callx when a callee releases resources Kumar Kartikeya Dwivedi
2026-11-11 14:04 ` bot+bpf-ci
2026-11-11 17:13 ` sashiko-bot
2026-11-11 12:57 ` [PATCH bpf-next v1 22/29] selftests/bpf: Test rejected throws that hold resources Kumar Kartikeya Dwivedi
2026-11-11 12:57 ` [PATCH bpf-next v1 23/29] selftests/bpf: Test resource cleanup on bpf_throw() Kumar Kartikeya Dwivedi
2026-11-11 12:57 ` [PATCH bpf-next v1 24/29] selftests/bpf: Test cleanup of handles on the stack Kumar Kartikeya Dwivedi
2026-11-11 12:57 ` [PATCH bpf-next v1 25/29] selftests/bpf: Test cleanup of handles in registers Kumar Kartikeya Dwivedi
2026-11-11 12:57 ` [PATCH bpf-next v1 26/29] selftests/bpf: Test callees releasing caller resources Kumar Kartikeya Dwivedi
2026-11-11 12:57 ` [PATCH bpf-next v1 27/29] selftests/bpf: Test resources a callee leaves with its caller Kumar Kartikeya Dwivedi
2026-11-11 12:57 ` [PATCH bpf-next v1 28/29] selftests/bpf: Test cleanup of locks described at sites Kumar Kartikeya Dwivedi
2026-11-11 12:57 ` [PATCH bpf-next v1 29/29] selftests/bpf: Test callee releases through callx Kumar Kartikeya Dwivedi
2026-11-11 20:33 ` [PATCH bpf-next v1 00/29] Exceptions - Resource Cleanup - Redux 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-209264@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 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.