BPF List
 help / color / mirror / Atom feed
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

  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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox