From: sashiko-bot@kernel.org
To: "Kumar Kartikeya Dwivedi" <memxor@gmail.com>, bpf@vger.kernel.org
Subject: Re: [RFC PATCH bpf-next v1 09/11] bpf: Let the comparator of bpf_rbtree_add unwind
Date: Wed, 28 Oct 2026 01:21:21 +0000 [thread overview]
Message-ID: <sashiko-outbox-187313@kernel.org> (raw)
In-Reply-To: <d81d996f09e0fd0b07816dcd0541f39c0f451a23.1793147364.git.memxor@gmail.com>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Critical] bpf: clobbered R2 on bpf_rbtree_add() callback unwind causes UAF
- [High] bpf: __bpf_rbtree_add() leaves node->owner poisoned on comparator unwind
--
commit 55e1518aab2a62c50b8098afe20794d027a69689
Author: Kumar Kartikeya Dwivedi <memxor@gmail.com>
bpf: Let the comparator of bpf_rbtree_add unwind
This commit allows the comparator callback of bpf_rbtree_add() and
bpf_rbtree_add_impl() to unwind via unwind_exit. When the comparator
unwinds, the kfunc drops the uninserted node before leaving to the landing
pad, and the verifier invalidates non-owning references and releases the
caller's owning reference on revisit.
> diff --git a/kernel/bpf/helpers.c b/kernel/bpf/helpers.c
> index 648abe1ba63ae..f8708b7181d72 100644
> --- a/kernel/bpf/helpers.c
> +++ b/kernel/bpf/helpers.c
[ ... ]
> @@ -2784,7 +2788,8 @@ static int __bpf_rbtree_add(struct bpf_rb_root *root,
>
> while (*link) {
> parent = *link;
> - if (cb((uintptr_t)node, (uintptr_t)parent, 0, 0, 0)) {
> + bpf_call_cb(cb, node, parent, 0, 0, 0, r0, unwound);
> + if (r0) {
> link = &parent->rb_left;
> } else {
> link = &parent->rb_right;
> @@ -2796,6 +2801,15 @@ static int __bpf_rbtree_add(struct bpf_rb_root *root,
> rb_insert_color_cached(n, (struct rb_root_cached *)root, leftmost);
> WRITE_ONCE(node->owner, root);
> return 0;
> +
> +unwound:
> + /*
> + * The comparator left with unwind_exit. The node is not in the tree, and
> + * the caller has let go of it: drop it, as the error path above does.
> + */
> + __bpf_obj_drop_impl((void *)n - off, rec, false);
> + bpf_unwind_leave();
> + return r0;
> }
[Severity: High]
Does the unwound path in __bpf_rbtree_add() leave node->owner poisoned with
BPF_PTR_POISON when the object survives __bpf_obj_drop_impl()?
At the start of __bpf_rbtree_add() in kernel/bpf/helpers.c, node->owner is
transitioned from NULL to BPF_PTR_POISON before the traversal loop:
if (cmpxchg(&node->owner, NULL, BPF_PTR_POISON)) {
/* Only called from BPF prog, no need to migrate_disable */
__bpf_obj_drop_impl((void *)n - off, rec, false);
return -EINVAL;
}
When the comparator callback cb exits via unwind_exit during the while (*link)
loop, control jumps to unwound after cmpxchg() has already set node->owner to
BPF_PTR_POISON, and before WRITE_ONCE(node->owner, root) is reached.
If the object contains a bpf_refcount field and another active reference to it
exists (for example via bpf_refcount_acquire() before calling bpf_rbtree_add()
or inside the comparator before unwinding), __bpf_obj_drop_impl() decrements
the refcount and returns without freeing the object:
if (rec && rec->refcount_off >= 0 &&
!refcount_dec_and_test((refcount_t *)(p + rec->refcount_off))) {
return;
}
Unlike bpf_rb_root_free(), which clears node->owner back to NULL before
dropping the reference:
/* Ensure __bpf_rbtree_add() sees the node as unlinked. */
smp_store_release(&node->owner, NULL);
__bpf_obj_drop_impl(obj, field->graph_root.value_rec, false);
the unwound path in __bpf_rbtree_add() leaves node->owner set to
BPF_PTR_POISON on the surviving unlinked object, causing any subsequent
bpf_rbtree_add() or bpf_rbtree_add_impl() call on that object to fail the
cmpxchg() check with -EINVAL. Should node->owner be reset to NULL before
calling __bpf_obj_drop_impl() in the unwound path?
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index fd63ff36eb909..50c3d44c37e15 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -15738,7 +15738,21 @@ static int check_kfunc_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
> id = regs[BPF_REG_2].id;
> insn_aux->insert_off = regs[BPF_REG_2].var_off.value;
> insn_aux->kptr_struct_meta = btf_find_struct_meta(meta.arg_btf, meta.arg_btf_id);
> - ref_convert_owning_non_owning(env, id);
> + /*
> + * The comparator unwound: the node never went in, and the
> + * kfunc dropped it on its way to the pad. The comparator was
> + * handed the node too, and may have unwound with it in r0, so
> + * every non-owning reference goes. Otherwise the node is in
> + * the tree now.
> + */
> + if (env->cur_state->unwound) {
> + invalidate_non_owning_refs(env);
> + err = release_reference(env, id);
> + if (err)
> + return err;
> + } else {
> + ref_convert_owning_non_owning(env, id);
> + }
> }
[Severity: Critical]
Can an unwinding comparator callback with a two-register return type clobber
caller->regs[BPF_REG_2] in prepare_func_exit(), causing check_kfunc_call() to
release the wrong reference ID and leave a dangling owning pointer to the
dropped node?
During verifier setup, bpf_compute_subprog_ret_regs() sets ret_reg_pair = true
for any subprogram whose BTF return type is a 9-16 byte struct, and
btf_validate_return_type() allows static subprograms to return structs up to
16 bytes. Neither check_func_arg(), push_callback_call(), nor
check_cb_unwind() prevents the less callback passed to bpf_rbtree_add() or
bpf_rbtree_add_impl() from having ret_reg_pair == true.
When such a callback executes unwind_exit, prepare_func_exit() in
kernel/bpf/verifier.c computes nregs = 2 (where ret_regs[] = { BPF_REG_0,
BPF_REG_2 }) and copies both registers from the callee frame into the caller
frame before rewinding *insn_idx to callee->callsite:
callee = state->frame[state->curframe];
r0 = &callee->regs[BPF_REG_0];
nregs = bpf_ret_reg_pair(env, callee->subprogno) ? 2 : 1;
...
if (callee->in_callback_fn && !unwinds) {
...
} else {
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);
}
}
If the caller passed &n1->node (with id = n1_id) in BPF_REG_2 while keeping a
copy of n1 in a callee-saved register (R6-R9) or stack spill, and the
comparator callback acquires an owning reference to a second node n2 of the
same BTF type (for example via bpf_refcount_acquire() or
bpf_list_pop_front()), places &n2->node (with id = n2_id) into BPF_REG_2, and
executes unwind_exit:
1. prepare_func_exit() overwrites caller->regs[BPF_REG_2] with &n2->node
(id = n2_id).
2. On the revisit of check_kfunc_call() at bpf_rbtree_add() with
env->cur_state->unwound == true, check_func_args() succeeds because
caller->regs[BPF_REG_2] holds a valid owning pointer of the expected
node type, and release_reference(env, id) releases n2_id instead of n1_id.
3. At runtime, __bpf_rbtree_add() drops the caller's original node n1 via
__bpf_obj_drop_impl(), while in the verifier state at the landing pad after
unwind_goto, n1_id was never released and the caller's saved pointer to n1
remains a valid owning PTR_TO_BTF_ID | MEM_ALLOC pointer to freed memory.
--
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1793147364.git.memxor@gmail.com?part=9
next prev parent reply other threads:[~2026-10-28 1:21 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
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 [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 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-187313@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