BPF List
 help / color / mirror / Atom feed
* [PATCH 6.6.y 0/2] bpf: fix dynptr lifetime invalidation on release
@ 2026-08-29  3:27 Xu Yunxiang
  2026-08-29  3:36 ` [PATCH 6.6.y 1/2] bpf: Invalidate dynptr slices by dynptr_id " Xu Yunxiang
                   ` (2 more replies)
  0 siblings, 3 replies; 11+ messages in thread
From: Xu Yunxiang @ 2026-08-29  3:27 UTC (permalink / raw)
  To: stable
  Cc: gregkh, bpf, ast, daniel, andrii, eddyz87, martin.lau, ameryhung,
	joannelkoong

Upstream commit 308c7a0ae885 ("bpf: Refactor object relationship
tracking and fix dynptr UAF bug") fixes these lifetime bugs as part of
an 11-file parent_id refactor.  That refactor cannot be applied to
6.6.y, which still represents the relationships with dynptr_id and
ref_obj_id.

This series supplies two stable-sized equivalents using the existing
6.6.y representation.  Patch 1 invalidates slice registers with the
released dynptr_id.  Patch 2 scans every call frame when invalidating
cloned dynptr stack slots.  They are split because the two bugs have
different introducing commits and independently testable effects.

Both patches preserve Amery's upstream authorship and complete trailer
chain.  Each commit message explicitly documents the deviations from
308c7a0ae885 and the reason for the stable-only implementation.

Tested on top of Linux 6.6.155 (a4a971135a2f):

- built kernel/bpf/verifier.o and bzImage;
- built the updated dynptr_fail.bpf.o selftest object;
- booted the patched kernel under QEMU and reran fresh reproducers;
- the slice-after-submit case is rejected with
  "R7 invalid mem access 'scalar'";
- the cross-frame control case loads and executes successfully, while
  the double-submit and submit/discard cases are rejected with
  "arg 1 is an unacquired reference".

Amery Hung (2):
  bpf: Invalidate dynptr slices by dynptr_id on release
  bpf: Invalidate dynptr clones in every call frame on release

 kernel/bpf/verifier.c                         | 60 ++++++++++++++-----
 .../testing/selftests/bpf/progs/dynptr_fail.c | 42 +++++++++++++
 2 files changed, 88 insertions(+), 14 deletions(-)


base-commit: a4a971135a2ff64382ae4235b3ae60503bb1036a
-- 
2.43.0


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH 6.6.y 1/2] bpf: Invalidate dynptr slices by dynptr_id on release
  2026-08-29  3:27 [PATCH 6.6.y 0/2] bpf: fix dynptr lifetime invalidation on release Xu Yunxiang
@ 2026-08-29  3:36 ` Xu Yunxiang
  2026-08-29  3:56   ` sashiko-bot
  2026-08-29  3:37 ` [PATCH 6.6.y 2/2] bpf: Invalidate dynptr clones in every call frame " Xu Yunxiang
  2026-08-29 10:26 ` [PATCH 6.6.y v2 0/3] bpf: fix dynptr release handling Xu Yunxiang
  2 siblings, 1 reply; 11+ messages in thread
From: Xu Yunxiang @ 2026-08-29  3:36 UTC (permalink / raw)
  To: stable
  Cc: gregkh, bpf, ast, daniel, andrii, eddyz87, martin.lau, ameryhung,
	joannelkoong

From: Amery Hung <ameryhung@gmail.com>

[ Upstream commit 308c7a0ae8859b34d9d90a3dff953b2d14242145 ]

This is a 6.6.y-sized equivalent, not a cherry-pick of the upstream
diff.

unmark_stack_slots_dynptr() comments say it invalidates slices of the
released dynptr, but it only calls release_reference(ref_obj_id).
bpf_dynptr_slice() and bpf_dynptr_slice_rdwr() stamp dynptr_id and
leave ref_obj_id at 0, so LDX/STX through those slices remains allowed
after bpf_ringbuf_submit_dynptr() / bpf_ringbuf_discard_dynptr().
bpf_dynptr_data() slices already carry ref_obj_id.

Walk registers with a matching dynptr_id, the same way
destroy_if_dynptr_stack_slot() already does when a dynptr stack slot is
overwritten. Do this for the released dynptr, for each clone, and for
the non-refcounted path.

The upstream commit is Amery Hung's parent_id refactor (Fixes:
870c28588afa, qdisc kfuncs). It cannot be applied here:

- It is an 11-file bpf-next change, well over the 100-line stable
  limit, and it removes dynptr_id / ref_obj_id.
- 6.6.y still uses dynptr_id and does not have the qdisc kfuncs in
  that Fixes: tag, so AUTOSEL never pulled it.

Deviations from 308c7a0ae8859: keep dynptr_id and ref_obj_id; do not
introduce parent_id; only add slice invalidation on
unmark_stack_slots_dynptr().

Fixes: 870c28588afa ("bpf: net_sched: Add basic bpf qdisc kfuncs")
Signed-off-by: Amery Hung <ameryhung@gmail.com>
Acked-by: Eduard Zingerman <eddyz87@gmail.com>
Link: https://lore.kernel.org/r/20260529014936.2811085-6-ameryhung@gmail.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
[ Xu Yunxiang: Split the upstream parent_id refactor into two
  stable-sized equivalent fixes and rewrite the subject and changelog
  to describe each one. This first patch keeps dynptr_id/ref_obj_id and
  invalidates slices from unmark_stack_slots_dynptr(). Add a regression
  test for a bpf_dynptr_slice_rdwr() pointer used after submit. ]
Fixes: 66e3a13e7c2c ("bpf: Add bpf_dynptr_slice and bpf_dynptr_slice_rdwr")
Reported-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>
Assisted-by: Pi:GLM-5.3
Signed-off-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>
---
 kernel/bpf/verifier.c                         | 27 +++++++++++++++++--
 .../testing/selftests/bpf/progs/dynptr_fail.c | 20 ++++++++++++++
 2 files changed, 45 insertions(+), 2 deletions(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index ad679bc42427c8..62fa2895a92050 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -990,6 +990,9 @@ static void invalidate_dynptr(struct bpf_verifier_env *env, struct bpf_func_stat
 	state->stack[spi - 1].spilled_ptr.live |= REG_LIVE_WRITTEN;
 }
 
+static void mark_reg_invalid(const struct bpf_verifier_env *env, struct bpf_reg_state *reg);
+static void invalidate_slices_of_dynptr(struct bpf_verifier_env *env, int dynptr_id);
+
 static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_state *reg)
 {
 	struct bpf_func_state *state = func(env, reg);
@@ -1000,6 +1003,7 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re
 		return spi;
 
 	if (!dynptr_type_refcounted(state->stack[spi].spilled_ptr.dynptr.type)) {
+		invalidate_slices_of_dynptr(env, state->stack[spi].spilled_ptr.id);
 		invalidate_dynptr(env, state, spi);
 		return 0;
 	}
@@ -1013,8 +1017,14 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re
 	 * 2) Any slices derived from this dynptr.
 	 */
 
-	/* Invalidate any slices associated with this dynptr */
+	/* Invalidate any slices associated with this dynptr.
+	 * release_reference() only walks registers that carry ref_obj_id;
+	 * slices are PTR_TO_MEM with dynptr_id set and ref_obj_id left 0.
+	 * Mirror destroy_if_dynptr_stack_slot(), which already invalidates
+	 * slices by dynptr_id.
+	 */
 	WARN_ON_ONCE(release_reference(env, ref_obj_id));
+	invalidate_slices_of_dynptr(env, state->stack[spi].spilled_ptr.id);
 
 	/* Invalidate any dynptr clones */
 	for (i = 1; i < state->allocated_stack / BPF_REG_SIZE; i++) {
@@ -1029,8 +1039,10 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re
 			verbose(env, "verifier internal error: misconfigured ref_obj_id\n");
 			return -EFAULT;
 		}
-		if (state->stack[i].spilled_ptr.dynptr.first_slot)
+		if (state->stack[i].spilled_ptr.dynptr.first_slot) {
+			invalidate_slices_of_dynptr(env, state->stack[i].spilled_ptr.id);
 			invalidate_dynptr(env, state, i);
+		}
 	}
 
 	return 0;
@@ -1047,6 +1059,17 @@ static void mark_reg_invalid(const struct bpf_verifier_env *env, struct bpf_reg_
 		__mark_reg_unknown(env, reg);
 }
 
+static void invalidate_slices_of_dynptr(struct bpf_verifier_env *env, int dynptr_id)
+{
+	struct bpf_func_state *fstate;
+	struct bpf_reg_state *dreg;
+
+	bpf_for_each_reg_in_vstate(env->cur_state, fstate, dreg, ({
+		if (dreg->dynptr_id == dynptr_id)
+			mark_reg_invalid(env, dreg);
+	}));
+}
+
 static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env,
 				        struct bpf_func_state *state, int spi)
 {
diff --git a/tools/testing/selftests/bpf/progs/dynptr_fail.c b/tools/testing/selftests/bpf/progs/dynptr_fail.c
index 66a60bfb58672f..131f55fbd6ed31 100644
--- a/tools/testing/selftests/bpf/progs/dynptr_fail.c
+++ b/tools/testing/selftests/bpf/progs/dynptr_fail.c
@@ -315,6 +315,26 @@ int data_slice_use_after_release1(void *ctx)
 	return 0;
 }
 
+/* Releasing a dynptr must invalidate slices that only carry dynptr_id. */
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int slice_kfunc_use_after_submit(void *ctx)
+{
+	struct bpf_dynptr ptr;
+	struct sample *sample;
+
+	bpf_ringbuf_reserve_dynptr(&ringbuf, sizeof(*sample), 0, &ptr);
+	sample = bpf_dynptr_slice_rdwr(&ptr, 0, NULL, sizeof(*sample));
+	if (!sample) {
+		bpf_ringbuf_discard_dynptr(&ptr, 0);
+		return 0;
+	}
+	bpf_ringbuf_submit_dynptr(&ptr, 0);
+	/* this should fail */
+	val = sample->pid;
+	return 0;
+}
+
 /* A data slice can't be used after it has been released.
  *
  * This tests the case where the data slice tracks a dynptr (ptr2)
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 6.6.y 2/2] bpf: Invalidate dynptr clones in every call frame on release
  2026-08-29  3:27 [PATCH 6.6.y 0/2] bpf: fix dynptr lifetime invalidation on release Xu Yunxiang
  2026-08-29  3:36 ` [PATCH 6.6.y 1/2] bpf: Invalidate dynptr slices by dynptr_id " Xu Yunxiang
@ 2026-08-29  3:37 ` Xu Yunxiang
  2026-08-29  4:07   ` sashiko-bot
  2026-08-29 10:26 ` [PATCH 6.6.y v2 0/3] bpf: fix dynptr release handling Xu Yunxiang
  2 siblings, 1 reply; 11+ messages in thread
From: Xu Yunxiang @ 2026-08-29  3:37 UTC (permalink / raw)
  To: stable
  Cc: gregkh, bpf, ast, daniel, andrii, eddyz87, martin.lau, ameryhung,
	joannelkoong

From: Amery Hung <ameryhung@gmail.com>

[ Upstream commit 308c7a0ae8859b34d9d90a3dff953b2d14242145 ]

This is a 6.6.y-sized equivalent, not a cherry-pick of the upstream
diff.

unmark_stack_slots_dynptr() walked clones only in the submitting
pointer's frame. A program that clones a ringbuf dynptr in a subprog,
submits the clone, then submits the original in the caller is
accepted: the caller's STACK_DYNPTR slot is never cleared. The second
bpf_ringbuf_commit() flips BUSY on an already-committed header and a
userspace consumer stalls.

Walk every frame and invalidate matching STACK_DYNPTR slots. Leave
WARN_ON_ONCE(release_reference()) as a swallow: a hard -EINVAL would
reject legitimate programs that release once in a different frame
than they acquired.

Depends on "bpf: Invalidate dynptr slices by dynptr_id on release"
only for the slice helper call inside the new loop.

The upstream commit is Amery Hung's parent_id refactor. It cannot be
applied here (see the previous patch). Deviations from 308c7a0ae8859:
keep dynptr_id / ref_obj_id; walk frames with the 6.6 STACK_DYNPTR
slot loop instead of the parent_id DFS.

Fixes: 870c28588afa ("bpf: net_sched: Add basic bpf qdisc kfuncs")
Signed-off-by: Amery Hung <ameryhung@gmail.com>
Acked-by: Eduard Zingerman <eddyz87@gmail.com>
Link: https://lore.kernel.org/r/20260529014936.2811085-6-ameryhung@gmail.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
[ Xu Yunxiang: Split the upstream parent_id refactor into two
  stable-sized equivalent fixes and rewrite the subject and changelog
  to describe each one. This second patch keeps dynptr_id/ref_obj_id
  and scans every call frame instead of using the upstream parent_id
  DFS. Add a cross-frame clone regression test. ]
Fixes: 361f129f3cc1 ("bpf: Add bpf_dynptr_clone")
Reported-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>
Assisted-by: Pi:GLM-5.3
Signed-off-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>
---
 kernel/bpf/verifier.c                         | 39 ++++++++++++-------
 .../testing/selftests/bpf/progs/dynptr_fail.c | 22 +++++++++++
 2 files changed, 46 insertions(+), 15 deletions(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 62fa2895a92050..fa88e7a35f9014 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -996,7 +996,7 @@ static void invalidate_slices_of_dynptr(struct bpf_verifier_env *env, int dynptr
 static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_state *reg)
 {
 	struct bpf_func_state *state = func(env, reg);
-	int spi, ref_obj_id, i;
+	int spi, ref_obj_id, i, frm;
 
 	spi = dynptr_get_spi(env, reg);
 	if (spi < 0)
@@ -1026,22 +1026,31 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re
 	WARN_ON_ONCE(release_reference(env, ref_obj_id));
 	invalidate_slices_of_dynptr(env, state->stack[spi].spilled_ptr.id);
 
-	/* Invalidate any dynptr clones */
-	for (i = 1; i < state->allocated_stack / BPF_REG_SIZE; i++) {
-		if (state->stack[i].spilled_ptr.ref_obj_id != ref_obj_id)
+	/* Invalidate any dynptr clones, including those in other frames.
+	 * Do not turn a failing release_reference() into -EINVAL: that
+	 * rejects legitimate cross-frame single releases on 6.6/6.12/6.13.
+	 */
+	for (frm = 0; frm <= env->cur_state->curframe; frm++) {
+		struct bpf_func_state *f = env->cur_state->frame[frm];
+
+		if (!f)
 			continue;
+		for (i = 1; i < f->allocated_stack / BPF_REG_SIZE; i++) {
+			if (f->stack[i].spilled_ptr.ref_obj_id != ref_obj_id)
+				continue;
 
-		/* it should always be the case that if the ref obj id
-		 * matches then the stack slot also belongs to a
-		 * dynptr
-		 */
-		if (state->stack[i].slot_type[0] != STACK_DYNPTR) {
-			verbose(env, "verifier internal error: misconfigured ref_obj_id\n");
-			return -EFAULT;
-		}
-		if (state->stack[i].spilled_ptr.dynptr.first_slot) {
-			invalidate_slices_of_dynptr(env, state->stack[i].spilled_ptr.id);
-			invalidate_dynptr(env, state, i);
+			/* it should always be the case that if the ref obj id
+			 * matches then the stack slot also belongs to a
+			 * dynptr
+			 */
+			if (f->stack[i].slot_type[0] != STACK_DYNPTR) {
+				verbose(env, "verifier internal error: misconfigured ref_obj_id\n");
+				return -EFAULT;
+			}
+			if (f->stack[i].spilled_ptr.dynptr.first_slot) {
+				invalidate_slices_of_dynptr(env, f->stack[i].spilled_ptr.id);
+				invalidate_dynptr(env, f, i);
+			}
 		}
 	}
 
diff --git a/tools/testing/selftests/bpf/progs/dynptr_fail.c b/tools/testing/selftests/bpf/progs/dynptr_fail.c
index 131f55fbd6ed31..91146cf18f4f8b 100644
--- a/tools/testing/selftests/bpf/progs/dynptr_fail.c
+++ b/tools/testing/selftests/bpf/progs/dynptr_fail.c
@@ -1487,6 +1487,28 @@ int clone_invalid2(struct xdp_md *xdp)
 }
 
 /* Invalidating a dynptr should invalidate its clones */
+static __noinline void submit_dynptr_clone(struct bpf_dynptr *ptr)
+{
+	struct bpf_dynptr clone;
+
+	bpf_dynptr_clone(ptr, &clone);
+	bpf_ringbuf_submit_dynptr(&clone, 0);
+}
+
+SEC("?raw_tp")
+__failure __msg("is an unacquired reference")
+int clone_invalidate_cross_frame(void *ctx)
+{
+	struct bpf_dynptr ptr;
+
+	bpf_ringbuf_reserve_dynptr(&ringbuf, sizeof(struct sample), 0, &ptr);
+	submit_dynptr_clone(&ptr);
+
+	/* The clone release in the callee must invalidate the caller's ptr. */
+	bpf_ringbuf_submit_dynptr(&ptr, 0);
+	return 0;
+}
+
 SEC("?raw_tp")
 __failure __msg("Expected an initialized dynptr as arg #3")
 int clone_invalidate1(void *ctx)
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [PATCH 6.6.y 1/2] bpf: Invalidate dynptr slices by dynptr_id on release
  2026-08-29  3:36 ` [PATCH 6.6.y 1/2] bpf: Invalidate dynptr slices by dynptr_id " Xu Yunxiang
@ 2026-08-29  3:56   ` sashiko-bot
  0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-08-29  3:56 UTC (permalink / raw)
  To: Xu Yunxiang; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] The newly added `invalidate_slices_of_dynptr()` accesses `dreg->dynptr_id` without verifying that `dreg->type` is `PTR_TO_MEM` or `PTR_TO_MEM | PTR_MAYBE_NULL`. Because `dynptr_id` is part of a union in `struct bpf_reg_state`, this leads to unsafe alias reads and incorrect register invalidation.
--

commit e7cd53e464cb9cab28779b0a9fe2dde83ad7d53c
Author: Amery Hung <ameryhung@gmail.com>

bpf: Invalidate dynptr slices by dynptr_id on release

This patch invalidates dynptr slices by matching dynptr_id when a dynptr is
released. It adds invalidate_slices_of_dynptr() to iterate over registers
and invalidate those matching the released dynptr_id.

Link: https://lore.kernel.org/r/20260529014936.2811085-6-ameryhung@gmail.com

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> +static void invalidate_slices_of_dynptr(struct bpf_verifier_env *env, int dynptr_id)
> +{
> +	struct bpf_func_state *fstate;
> +	struct bpf_reg_state *dreg;
> +
> +	bpf_for_each_reg_in_vstate(env->cur_state, fstate, dreg, ({
> +		if (dreg->dynptr_id == dynptr_id)
[Severity: High]
Is it safe to read dreg->dynptr_id here without checking the register type?

Because dynptr_id is part of a union in struct bpf_reg_state, it aliases
other fields like btf_id and map_uid (especially on 32-bit systems where
these fields occupy the same bytes).

If a register holds a PTR_TO_BTF_ID or an inner map PTR_TO_MAP_VALUE, and
its btf_id or map_uid happens to match the released dynptr_id, could this
result in a false positive that invalidates an unrelated valid register?

For comparison, destroy_if_dynptr_stack_slot() restricts this check:

    if (dreg->type != (PTR_TO_MEM | PTR_MAYBE_NULL) &&
        dreg->type != PTR_TO_MEM)
        continue;
    if (dreg->dynptr_id == dynptr_id)
        mark_reg_invalid(env, dreg);

Should invalidate_slices_of_dynptr() include a similar type check before
accessing dynptr_id?

> +			mark_reg_invalid(env, dreg);
> +	}));
> +}

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260829032754.2324760-1-xyx2021@mail.ustc.edu.cn?part=1

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 6.6.y 2/2] bpf: Invalidate dynptr clones in every call frame on release
  2026-08-29  3:37 ` [PATCH 6.6.y 2/2] bpf: Invalidate dynptr clones in every call frame " Xu Yunxiang
@ 2026-08-29  4:07   ` sashiko-bot
  0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-08-29  4:07 UTC (permalink / raw)
  To: Xu Yunxiang; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:

Pre-existing issues:
- [Critical] Dynptr slices belonging to a cross-frame clone are not invalidated if the subprogram containing the clone returns before the original dynptr is released, leading to a Use-After-Free on ringbuf memory.
--

commit f98ee666f54971a2824f38f254185708f60a777a
Author: Amery Hung <ameryhung@gmail.com>
Date:   Sat Aug 29 03:37:01 2026 +0000

    bpf: Invalidate dynptr clones in every call frame on release

This commit modifies unmark_stack_slots_dynptr to iterate over all active
call frames rather than just the current frame when invalidating dynptr clones.
It also adds a cross-frame clone regression test.

Link: https://lore.kernel.org/r/20260529014936.2811085-6-ameryhung@gmail.com

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 62fa2895a9205..fa88e7a35f901 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -1026,22 +1026,31 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re
>  	WARN_ON_ONCE(release_reference(env, ref_obj_id));
>  	invalidate_slices_of_dynptr(env, state->stack[spi].spilled_ptr.id);
>  
> -	/* Invalidate any dynptr clones */
> -	for (i = 1; i < state->allocated_stack / BPF_REG_SIZE; i++) {
> -		if (state->stack[i].spilled_ptr.ref_obj_id != ref_obj_id)
> +	/* Invalidate any dynptr clones, including those in other frames.
> +	 * Do not turn a failing release_reference() into -EINVAL: that
> +	 * rejects legitimate cross-frame single releases on 6.6/6.12/6.13.
> +	 */
> +	for (frm = 0; frm <= env->cur_state->curframe; frm++) {

[Severity: Critical]
This isn't a bug introduced by this patch, but does this loop restriction fail
to invalidate slices belonging to clones in frames that have already returned?

If a subprogram (Frame 1) clones a ringbuf dynptr, the clone receives a unique
clone->id but shares the original ref_obj_id X. If Frame 1 then creates a slice
from this clone via bpf_dynptr_slice_rdwr, the slice sets dynptr_id = clone->id
but leaves ref_obj_id = 0.

If Frame 1 spills this slice pointer to the caller's (Frame 0) stack and
returns, the clone goes out of scope without an explicit release. When Frame 0
later releases the original dynptr:

release_reference(X) executes, but does not clear the spilled slice because the
slice's ref_obj_id is 0. 

unmark_stack_slots_dynptr then executes, but since the subprogram returned,
curframe is 0. The loop only checks Frame 0 and fails to find the clone because
Frame 1's stack is already gone.

Since it never discovers clone->id, invalidate_slices_of_dynptr(clone->id) is
never called. Does this leave the spilled slice on Frame 0's stack fully valid,
allowing an unprivileged BPF program to read or write freed ringbuf memory?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260829032754.2324760-1-xyx2021@mail.ustc.edu.cn?part=2

^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH 6.6.y v2 0/3] bpf: fix dynptr release handling
  2026-08-29  3:27 [PATCH 6.6.y 0/2] bpf: fix dynptr lifetime invalidation on release Xu Yunxiang
  2026-08-29  3:36 ` [PATCH 6.6.y 1/2] bpf: Invalidate dynptr slices by dynptr_id " Xu Yunxiang
  2026-08-29  3:37 ` [PATCH 6.6.y 2/2] bpf: Invalidate dynptr clones in every call frame " Xu Yunxiang
@ 2026-08-29 10:26 ` Xu Yunxiang
  2026-08-29 10:26   ` [PATCH 6.6.y v2 1/3] bpf: Invalidate dynptr slices by dynptr_id on release Xu Yunxiang
                     ` (3 more replies)
  2 siblings, 4 replies; 11+ messages in thread
From: Xu Yunxiang @ 2026-08-29 10:26 UTC (permalink / raw)
  To: stable
  Cc: gregkh, bpf, ast, daniel, andrii, eddyz87, martin.lau, ameryhung,
	joannelkoong

Upstream commit 308c7a0ae885 ("bpf: Refactor object relationship
tracking and fix dynptr UAF bug") fixes these lifetime bugs as part of
an 11-file parent_id refactor.  That refactor cannot be applied to
6.6.y, which still represents the relationships with dynptr_id and
ref_obj_id.

This series supplies three stable-sized equivalents using the existing
6.6.y representation.  Patch 1 invalidates slice registers with the
released dynptr_id.  Patch 2 propagates a referenced-dynptr release
error instead of turning a callback rejection into a kernel warning.
Patch 3 scans every active call frame when invalidating cloned dynptr
stack slots.  They are split because the bugs have different
introducing commits and independently testable effects.

All three patches preserve Amery's upstream authorship and complete
trailer chain.  Each commit message explicitly documents the deviations
from 308c7a0ae885 and the reason for the stable-only implementation.

Changes in v2:

- check that a register has base type PTR_TO_MEM before reading its
  dynptr_id union member, while retaining DYNPTR_TYPE_* flags;
- add a bpf_dynptr_slice() use-after-submit regression that reaches the
  dynptr_id-only slice path on the target stable verifier;
- add a separate patch that propagates release_reference() errors from
  referenced dynptr release and a callback regression test.  This avoids
  the verifier WARN, and the panic it causes with panic_on_warn=1, when a
  callback tries to release its caller's dynptr;
- make the cross-frame clone regression use bpf_dynptr_data() after the
  callee releases a clone, so it uniquely tests caller-slot invalidation
  instead of a repeated release already rejected by patch 2;
- check STACK_DYNPTR before reading dynptr metadata while scanning call
  frames, so a partially overwritten ordinary spill with stale metadata
  is ignored, and add a success regression test;
- investigate the separately reported slice escape through a returned
  subprogram frame.  A minimal reproducer is also accepted by current
  mainline after 308c7a0ae885, so that issue is not folded into this
  stable-only series and needs a separate mainline fix.

Tested on Linux 6.6.155 with panic_on_warn=1:

- test_progs -t dynptr -v --workers=1: 2/92 passed, 0 failed;
- the slice-after-submit and cross-frame stale-use programs were
  rejected, while their control programs loaded successfully;
- the callback-release reproducer was rejected without a verifier
  warning;
- the independent submit/discard, stale-spill, and cross-frame
  reproducers produced the expected results.

Amery Hung (3):
  bpf: Invalidate dynptr slices by dynptr_id on release
  bpf: Propagate referenced dynptr release errors
  bpf: Invalidate dynptr clones in every call frame on release

 kernel/bpf/verifier.c                         | 55 +++++++++----
 .../testing/selftests/bpf/progs/dynptr_fail.c | 79 +++++++++++++++++++
 2 files changed, 119 insertions(+), 15 deletions(-)


base-commit: a4a971135a2ff64382ae4235b3ae60503bb1036a
-- 
2.43.0


^ permalink raw reply	[flat|nested] 11+ messages in thread

* [PATCH 6.6.y v2 1/3] bpf: Invalidate dynptr slices by dynptr_id on release
  2026-08-29 10:26 ` [PATCH 6.6.y v2 0/3] bpf: fix dynptr release handling Xu Yunxiang
@ 2026-08-29 10:26   ` Xu Yunxiang
  2026-08-29 10:26   ` [PATCH 6.6.y v2 2/3] bpf: Propagate referenced dynptr release errors Xu Yunxiang
                     ` (2 subsequent siblings)
  3 siblings, 0 replies; 11+ messages in thread
From: Xu Yunxiang @ 2026-08-29 10:26 UTC (permalink / raw)
  To: stable
  Cc: gregkh, bpf, ast, daniel, andrii, eddyz87, martin.lau, ameryhung,
	joannelkoong

From: Amery Hung <ameryhung@gmail.com>

[ Upstream commit 308c7a0ae8859b34d9d90a3dff953b2d14242145 ]

This is a 6.6.y-sized equivalent, not a cherry-pick of the upstream
diff.

unmark_stack_slots_dynptr() comments say it invalidates slices of the
released dynptr, but it only calls release_reference(ref_obj_id).
bpf_dynptr_slice() and bpf_dynptr_slice_rdwr() stamp dynptr_id and
leave ref_obj_id at 0, so LDX/STX through those slices remains allowed
after bpf_ringbuf_submit_dynptr() / bpf_ringbuf_discard_dynptr().
bpf_dynptr_data() slices already carry ref_obj_id.

Walk registers with a matching dynptr_id, the same way
destroy_if_dynptr_stack_slot() already does when a dynptr stack slot is
overwritten. Do this for the released dynptr, for each clone, and for
the non-refcounted path.

Check the register base type before reading dynptr_id because the field
shares a union with other register metadata. Use base_type() rather than
an exact type match so slices carrying DYNPTR_TYPE_* flags remain covered.

The upstream commit is Amery Hung's parent_id refactor (Fixes:
870c28588afa, qdisc kfuncs). It cannot be applied here:

- It is an 11-file bpf-next change, well over the 100-line stable
  limit, and it removes dynptr_id / ref_obj_id.
- 6.6.y still uses dynptr_id and does not have the qdisc kfuncs in
  that Fixes: tag, so AUTOSEL never pulled it.

Deviations from 308c7a0ae8859: keep dynptr_id and ref_obj_id; do not
introduce parent_id; only add slice invalidation on
unmark_stack_slots_dynptr().

Fixes: 870c28588afa ("bpf: net_sched: Add basic bpf qdisc kfuncs")
Signed-off-by: Amery Hung <ameryhung@gmail.com>
Acked-by: Eduard Zingerman <eddyz87@gmail.com>
Link: https://lore.kernel.org/r/20260529014936.2811085-6-ameryhung@gmail.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
[ Xu Yunxiang: Backport the slice-invalidation part of the upstream
  parent_id refactor and rewrite the subject and changelog.
  This patch keeps dynptr_id/ref_obj_id and
  invalidates slices from unmark_stack_slots_dynptr(). Add a regression
  test for a bpf_dynptr_slice() pointer used after submit. ]
Fixes: 66e3a13e7c2c ("bpf: Add bpf_dynptr_slice and bpf_dynptr_slice_rdwr")
Reported-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>
Closes: https://lore.kernel.org/r/20260829033645.2365631-1-xyx2021@mail.ustc.edu.cn
Assisted-by: Pi:GLM-5.3
Signed-off-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>
---
 kernel/bpf/verifier.c                         | 27 +++++++++++++++++--
 .../testing/selftests/bpf/progs/dynptr_fail.c | 20 ++++++++++++++
 2 files changed, 45 insertions(+), 2 deletions(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index ad679bc42427c8..ccd5c86706b184 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -990,6 +990,9 @@ static void invalidate_dynptr(struct bpf_verifier_env *env, struct bpf_func_stat
 	state->stack[spi - 1].spilled_ptr.live |= REG_LIVE_WRITTEN;
 }
 
+static void mark_reg_invalid(const struct bpf_verifier_env *env, struct bpf_reg_state *reg);
+static void invalidate_slices_of_dynptr(struct bpf_verifier_env *env, int dynptr_id);
+
 static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_state *reg)
 {
 	struct bpf_func_state *state = func(env, reg);
@@ -1000,6 +1003,7 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re
 		return spi;
 
 	if (!dynptr_type_refcounted(state->stack[spi].spilled_ptr.dynptr.type)) {
+		invalidate_slices_of_dynptr(env, state->stack[spi].spilled_ptr.id);
 		invalidate_dynptr(env, state, spi);
 		return 0;
 	}
@@ -1013,8 +1017,14 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re
 	 * 2) Any slices derived from this dynptr.
 	 */
 
-	/* Invalidate any slices associated with this dynptr */
+	/* Invalidate any slices associated with this dynptr.
+	 * release_reference() only walks registers that carry ref_obj_id;
+	 * slices are PTR_TO_MEM with dynptr_id set and ref_obj_id left 0.
+	 * Mirror destroy_if_dynptr_stack_slot(), which already invalidates
+	 * slices by dynptr_id.
+	 */
 	WARN_ON_ONCE(release_reference(env, ref_obj_id));
+	invalidate_slices_of_dynptr(env, state->stack[spi].spilled_ptr.id);
 
 	/* Invalidate any dynptr clones */
 	for (i = 1; i < state->allocated_stack / BPF_REG_SIZE; i++) {
@@ -1029,8 +1039,10 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re
 			verbose(env, "verifier internal error: misconfigured ref_obj_id\n");
 			return -EFAULT;
 		}
-		if (state->stack[i].spilled_ptr.dynptr.first_slot)
+		if (state->stack[i].spilled_ptr.dynptr.first_slot) {
+			invalidate_slices_of_dynptr(env, state->stack[i].spilled_ptr.id);
 			invalidate_dynptr(env, state, i);
+		}
 	}
 
 	return 0;
@@ -1047,6 +1059,17 @@ static void mark_reg_invalid(const struct bpf_verifier_env *env, struct bpf_reg_
 		__mark_reg_unknown(env, reg);
 }
 
+static void invalidate_slices_of_dynptr(struct bpf_verifier_env *env, int dynptr_id)
+{
+	struct bpf_func_state *fstate;
+	struct bpf_reg_state *dreg;
+
+	bpf_for_each_reg_in_vstate(env->cur_state, fstate, dreg, ({
+		if (base_type(dreg->type) == PTR_TO_MEM && dreg->dynptr_id == dynptr_id)
+			mark_reg_invalid(env, dreg);
+	}));
+}
+
 static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env,
 				        struct bpf_func_state *state, int spi)
 {
diff --git a/tools/testing/selftests/bpf/progs/dynptr_fail.c b/tools/testing/selftests/bpf/progs/dynptr_fail.c
index 66a60bfb58672f..6ac09d6c34f2bc 100644
--- a/tools/testing/selftests/bpf/progs/dynptr_fail.c
+++ b/tools/testing/selftests/bpf/progs/dynptr_fail.c
@@ -315,6 +315,26 @@ int data_slice_use_after_release1(void *ctx)
 	return 0;
 }
 
+/* Releasing a dynptr must invalidate slices that only carry dynptr_id. */
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int slice_kfunc_use_after_submit(void *ctx)
+{
+	struct bpf_dynptr ptr;
+	struct sample *sample;
+
+	bpf_ringbuf_reserve_dynptr(&ringbuf, sizeof(*sample), 0, &ptr);
+	sample = bpf_dynptr_slice(&ptr, 0, NULL, sizeof(*sample));
+	if (!sample) {
+		bpf_ringbuf_discard_dynptr(&ptr, 0);
+		return 0;
+	}
+	bpf_ringbuf_submit_dynptr(&ptr, 0);
+	/* this should fail */
+	val = sample->pid;
+	return 0;
+}
+
 /* A data slice can't be used after it has been released.
  *
  * This tests the case where the data slice tracks a dynptr (ptr2)
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 6.6.y v2 2/3] bpf: Propagate referenced dynptr release errors
  2026-08-29 10:26 ` [PATCH 6.6.y v2 0/3] bpf: fix dynptr release handling Xu Yunxiang
  2026-08-29 10:26   ` [PATCH 6.6.y v2 1/3] bpf: Invalidate dynptr slices by dynptr_id on release Xu Yunxiang
@ 2026-08-29 10:26   ` Xu Yunxiang
  2026-08-29 10:42     ` sashiko-bot
  2026-08-29 10:26   ` [PATCH 6.6.y v2 3/3] bpf: Invalidate dynptr clones in every call frame on release Xu Yunxiang
  2026-08-30  2:56   ` [PATCH 6.6.y v2 0/3] bpf: fix dynptr release handling Sasha Levin
  3 siblings, 1 reply; 11+ messages in thread
From: Xu Yunxiang @ 2026-08-29 10:26 UTC (permalink / raw)
  To: stable
  Cc: gregkh, bpf, ast, daniel, andrii, eddyz87, martin.lau, ameryhung,
	joannelkoong

From: Amery Hung <ameryhung@gmail.com>

[ Upstream commit 308c7a0ae8859b34d9d90a3dff953b2d14242145 ]

This is a stable-sized equivalent, not a cherry-pick of the upstream
diff.

release_reference_state() rejects an attempt from a BPF callback to
release a reference acquired by its caller.  For referenced dynptrs,
unmark_stack_slots_dynptr() instead wraps release_reference() in
WARN_ON_ONCE() and then returns success.

Consequently, a callback that submits or discards its caller's ringbuf
dynptr can turn this normal verifier rejection into a kernel warning.  A
kernel configured with panic_on_warn=1 panics while verifying the program.

Propagate the release_reference() error to check_helper_call(), which
already reports the invalid release and rejects the program.  Add a
regression test using a non-constant bpf_loop() iteration count so the
callback is verified in a separate frame.

The upstream parent_id refactor returns release_reference() directly from
unmark_stack_slots_dynptr().  Extract that behavior here while retaining
the stable kernels' ref_obj_id representation.

Fixes: 870c28588afa ("bpf: net_sched: Add basic bpf qdisc kfuncs")
Signed-off-by: Amery Hung <ameryhung@gmail.com>
Acked-by: Eduard Zingerman <eddyz87@gmail.com>
Link: https://lore.kernel.org/r/20260529014936.2811085-6-ameryhung@gmail.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
[ Xu Yunxiang: Extract only the referenced-dynptr error propagation from
  the upstream object relationship refactor for the old stable verifier.
  Add a callback regression test. ]
Fixes: 270605317366 ("bpf: Rework process_dynptr_func")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/r/20260829040110.D6A991F000E9@smtp.kernel.org
Assisted-by: Codex:GPT-5
Signed-off-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>
---
 kernel/bpf/verifier.c                         |  6 +++--
 .../testing/selftests/bpf/progs/dynptr_fail.c | 22 +++++++++++++++++++
 2 files changed, 26 insertions(+), 2 deletions(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index ccd5c86706b184..c9e3856bb59fdb 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -996,7 +996,7 @@ static void invalidate_slices_of_dynptr(struct bpf_verifier_env *env, int dynptr
 static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_state *reg)
 {
 	struct bpf_func_state *state = func(env, reg);
-	int spi, ref_obj_id, i;
+	int spi, ref_obj_id, i, err;
 
 	spi = dynptr_get_spi(env, reg);
 	if (spi < 0)
@@ -1023,7 +1023,9 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re
 	 * Mirror destroy_if_dynptr_stack_slot(), which already invalidates
 	 * slices by dynptr_id.
 	 */
-	WARN_ON_ONCE(release_reference(env, ref_obj_id));
+	err = release_reference(env, ref_obj_id);
+	if (err)
+		return err;
 	invalidate_slices_of_dynptr(env, state->stack[spi].spilled_ptr.id);
 
 	/* Invalidate any dynptr clones */
diff --git a/tools/testing/selftests/bpf/progs/dynptr_fail.c b/tools/testing/selftests/bpf/progs/dynptr_fail.c
index 6ac09d6c34f2bc..3d431e583c4999 100644
--- a/tools/testing/selftests/bpf/progs/dynptr_fail.c
+++ b/tools/testing/selftests/bpf/progs/dynptr_fail.c
@@ -133,6 +133,28 @@ int ringbuf_missing_release_callback(void *ctx)
 	return 0;
 }
 
+static int release_caller_dynptr_callback_fn(__u32 index, void *data)
+{
+	struct bpf_dynptr *ptr = data;
+
+	bpf_ringbuf_submit_dynptr(ptr, 0);
+	return 0;
+}
+
+/* A callback cannot release a dynptr reference acquired by its caller. */
+SEC("?raw_tp")
+__failure __msg("reference has not been acquired before")
+int ringbuf_release_caller_dynptr_callback(void *ctx)
+{
+	struct bpf_dynptr ptr;
+
+	if (bpf_ringbuf_reserve_dynptr(&ringbuf, 8, 0, &ptr))
+		return 0;
+	bpf_loop(val & 1, release_caller_dynptr_callback_fn, &ptr, 0);
+	bpf_ringbuf_discard_dynptr(&ptr, 0);
+	return 0;
+}
+
 /* Can't call bpf_ringbuf_submit/discard_dynptr on a non-initialized dynptr */
 SEC("?raw_tp")
 __failure __msg("arg 1 is an unacquired reference")
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* [PATCH 6.6.y v2 3/3] bpf: Invalidate dynptr clones in every call frame on release
  2026-08-29 10:26 ` [PATCH 6.6.y v2 0/3] bpf: fix dynptr release handling Xu Yunxiang
  2026-08-29 10:26   ` [PATCH 6.6.y v2 1/3] bpf: Invalidate dynptr slices by dynptr_id on release Xu Yunxiang
  2026-08-29 10:26   ` [PATCH 6.6.y v2 2/3] bpf: Propagate referenced dynptr release errors Xu Yunxiang
@ 2026-08-29 10:26   ` Xu Yunxiang
  2026-08-30  2:56   ` [PATCH 6.6.y v2 0/3] bpf: fix dynptr release handling Sasha Levin
  3 siblings, 0 replies; 11+ messages in thread
From: Xu Yunxiang @ 2026-08-29 10:26 UTC (permalink / raw)
  To: stable
  Cc: gregkh, bpf, ast, daniel, andrii, eddyz87, martin.lau, ameryhung,
	joannelkoong

From: Amery Hung <ameryhung@gmail.com>

[ Upstream commit 308c7a0ae8859b34d9d90a3dff953b2d14242145 ]

This is a 6.6.y-sized equivalent, not a cherry-pick of the upstream
diff.

unmark_stack_slots_dynptr() walked clones only in the submitting
pointer's frame. A program that clones a ringbuf dynptr in a subprog
and submits the clone leaves the original STACK_DYNPTR slot active in
the caller. The caller can then pass that stale original to
bpf_dynptr_data(), even though the shared ringbuf reference has already
been released.

Walk every frame and invalidate matching STACK_DYNPTR slots. Check the
slot type before reading dynptr metadata: a partial stack overwrite can
turn a pointer spill into ordinary stack data while stale union fields
remain, and those fields must not be interpreted as a dynptr ref_obj_id.
The preceding patch makes a repeated release an ordinary verifier
rejection, but it does not prevent non-release uses of a stale dynptr.

Depends on "bpf: Invalidate dynptr slices by dynptr_id on release"
only for the slice helper call inside the new loop.

The upstream commit is Amery Hung's parent_id refactor. It cannot be
applied here (see the first patch). Deviations from 308c7a0ae8859:
keep dynptr_id / ref_obj_id; walk frames with the 6.6 STACK_DYNPTR
slot loop instead of the parent_id DFS.

Fixes: 870c28588afa ("bpf: net_sched: Add basic bpf qdisc kfuncs")
Signed-off-by: Amery Hung <ameryhung@gmail.com>
Acked-by: Eduard Zingerman <eddyz87@gmail.com>
Link: https://lore.kernel.org/r/20260529014936.2811085-6-ameryhung@gmail.com
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
[ Xu Yunxiang: Backport the cross-frame clone-invalidation part of the
  upstream parent_id refactor and rewrite the subject and changelog.
  This patch keeps dynptr_id/ref_obj_id
  and scans every call frame instead of using the upstream parent_id
  DFS. Add cross-frame stale-use and stale-spill regression tests. ]
Fixes: 361f129f3cc1 ("bpf: Add bpf_dynptr_clone")
Reported-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>
Closes: https://lore.kernel.org/r/20260829033701.2365648-1-xyx2021@mail.ustc.edu.cn
Assisted-by: Pi:GLM-5.3
Signed-off-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>
---
 kernel/bpf/verifier.c                         | 30 +++++++--------
 .../testing/selftests/bpf/progs/dynptr_fail.c | 37 +++++++++++++++++++
 2 files changed, 52 insertions(+), 15 deletions(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index c9e3856bb59fdb..a2df5bbf9ab8d8 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -996,7 +996,7 @@ static void invalidate_slices_of_dynptr(struct bpf_verifier_env *env, int dynptr
 static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_state *reg)
 {
 	struct bpf_func_state *state = func(env, reg);
-	int spi, ref_obj_id, i, err;
+	int spi, ref_obj_id, i, err, frm;
 
 	spi = dynptr_get_spi(env, reg);
 	if (spi < 0)
@@ -1028,22 +1028,22 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re
 		return err;
 	invalidate_slices_of_dynptr(env, state->stack[spi].spilled_ptr.id);
 
-	/* Invalidate any dynptr clones */
-	for (i = 1; i < state->allocated_stack / BPF_REG_SIZE; i++) {
-		if (state->stack[i].spilled_ptr.ref_obj_id != ref_obj_id)
+	/* Invalidate any dynptr clones, including those in other frames. */
+	for (frm = 0; frm <= env->cur_state->curframe; frm++) {
+		struct bpf_func_state *f = env->cur_state->frame[frm];
+
+		if (!f)
 			continue;
+		for (i = 1; i < f->allocated_stack / BPF_REG_SIZE; i++) {
+			if (f->stack[i].slot_type[0] != STACK_DYNPTR)
+				continue;
+			if (f->stack[i].spilled_ptr.ref_obj_id != ref_obj_id)
+				continue;
 
-		/* it should always be the case that if the ref obj id
-		 * matches then the stack slot also belongs to a
-		 * dynptr
-		 */
-		if (state->stack[i].slot_type[0] != STACK_DYNPTR) {
-			verbose(env, "verifier internal error: misconfigured ref_obj_id\n");
-			return -EFAULT;
-		}
-		if (state->stack[i].spilled_ptr.dynptr.first_slot) {
-			invalidate_slices_of_dynptr(env, state->stack[i].spilled_ptr.id);
-			invalidate_dynptr(env, state, i);
+			if (f->stack[i].spilled_ptr.dynptr.first_slot) {
+				invalidate_slices_of_dynptr(env, f->stack[i].spilled_ptr.id);
+				invalidate_dynptr(env, f, i);
+			}
 		}
 	}
 
diff --git a/tools/testing/selftests/bpf/progs/dynptr_fail.c b/tools/testing/selftests/bpf/progs/dynptr_fail.c
index 3d431e583c4999..99a791ddfefcb7 100644
--- a/tools/testing/selftests/bpf/progs/dynptr_fail.c
+++ b/tools/testing/selftests/bpf/progs/dynptr_fail.c
@@ -1509,6 +1509,43 @@ int clone_invalid2(struct xdp_md *xdp)
 }
 
 /* Invalidating a dynptr should invalidate its clones */
+static __noinline void submit_dynptr_clone(struct bpf_dynptr *ptr)
+{
+	struct bpf_dynptr clone;
+
+	bpf_dynptr_clone(ptr, &clone);
+	bpf_ringbuf_submit_dynptr(&clone, 0);
+}
+
+SEC("?raw_tp")
+__failure __msg("Expected an initialized dynptr")
+int clone_invalidate_cross_frame(void *ctx)
+{
+	struct bpf_dynptr ptr;
+
+	bpf_ringbuf_reserve_dynptr(&ringbuf, sizeof(struct sample), 0, &ptr);
+	submit_dynptr_clone(&ptr);
+	bpf_dynptr_data(&ptr, 0, sizeof(struct sample));
+	return 0;
+}
+
+SEC("?raw_tp")
+__success
+int clone_invalidate_cross_frame_stale_spill(void *ctx)
+{
+	struct bpf_dynptr ptr;
+	void *data;
+
+	bpf_ringbuf_reserve_dynptr(&ringbuf, sizeof(struct sample), 0, &ptr);
+	data = bpf_dynptr_data(&ptr, 0, sizeof(struct sample));
+	asm volatile("*(u64 *)(r10 - 24) = %[data];"
+		"*(u32 *)(r10 - 20) = 0x12345678;"
+		:: [data] "r"(data) : "memory");
+	/* A dead, scrubbed spill must not be mistaken for a dynptr slot. */
+	submit_dynptr_clone(&ptr);
+	return 0;
+}
+
 SEC("?raw_tp")
 __failure __msg("Expected an initialized dynptr as arg #3")
 int clone_invalidate1(void *ctx)
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 11+ messages in thread

* Re: [PATCH 6.6.y v2 2/3] bpf: Propagate referenced dynptr release errors
  2026-08-29 10:26   ` [PATCH 6.6.y v2 2/3] bpf: Propagate referenced dynptr release errors Xu Yunxiang
@ 2026-08-29 10:42     ` sashiko-bot
  0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-08-29 10:42 UTC (permalink / raw)
  To: Xu Yunxiang; +Cc: bpf

Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:

Pre-existing issues:
- [High] The commit fixes a kernel panic where a callback releasing a caller-acquired dynptr reference triggers a WARN_ON_ONCE, but leaves the identical vulnerability unfixed for BPF iterators in unmark_stack_slots_iter().
- [High] A kernel panic can be triggered via WARN_ON_ONCE(release_reference_state(...)) in mark_ptr_or_null_regs() when performing a NULL check on a caller's spilled PTR_OR_NULL register inside a callback.
--

commit 0666efa4fca54b7a83f6a6c69d44d1c775940a2e
Author: Amery Hung <ameryhung@gmail.com>
Subject: bpf: Propagate referenced dynptr release errors

This patch fixes a panic caused by a WARN_ON_ONCE in the verifier when a BPF
callback incorrectly attempts to release a referenced dynptr acquired by its
caller. It does this by correctly propagating the error returned from
release_reference() in unmark_stack_slots_dynptr().

Link: https://lore.kernel.org/r/20260529014936.2811085-6-ameryhung@gmail.com

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index ad679bc42427c..c9e3856bb59fd 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -1013,8 +1017,16 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re
>  	 * 2) Any slices derived from this dynptr.
>  	 */
>  
> -	/* Invalidate any slices associated with this dynptr */
> -	WARN_ON_ONCE(release_reference(env, ref_obj_id));
> +	/* Invalidate any slices associated with this dynptr.
> +	 * release_reference() only walks registers that carry ref_obj_id;
> +	 * slices are PTR_TO_MEM with dynptr_id set and ref_obj_id left 0.
> +	 * Mirror destroy_if_dynptr_stack_slot(), which already invalidates
> +	 * slices by dynptr_id.
> +	 */
> +	err = release_reference(env, ref_obj_id);
> +	if (err)
> +		return err;

[Severity: High]
This isn't a bug introduced by this patch, but does the same vulnerability
still exist for BPF iterators in unmark_stack_slots_iter()?

If a caller allocates an iterator on its stack, passes a pointer to this stack
slot into a callback via bpf_loop(), and the callback invokes a destroy kfunc
(like bpf_iter_num_destroy()), the verifier will call:

kernel/bpf/verifier.c:unmark_stack_slots_iter() {
    ...
		if (i == 0)
			WARN_ON_ONCE(release_reference(env, st->ref_obj_id));
    ...
}

Because the reference belongs to the caller frame, release_reference() returns
-EINVAL, which triggers the WARN_ON_ONCE and panics the kernel if
panic_on_warn=1.

[Severity: High]
This is a pre-existing issue, but could a similar panic occur when performing
a NULL check on a caller's spilled PTR_OR_NULL register inside a callback?

If a caller acquires a reference-tracked pointer that could be NULL, spills it
to the stack, and passes a pointer to that stack slot to a callback, the
restored register preserves the caller's id and ref_obj_id.

When the callback performs a NULL check on this restored pointer:

kernel/bpf/verifier.c:mark_ptr_or_null_regs() {
    ...
	if (ref_obj_id && ref_obj_id == id && is_null)
		/* regs[regno] is in the " == NULL" branch.
		 * No one could have freed the reference state before
		 * doing the NULL check.
		 */
		WARN_ON_ONCE(release_reference_state(state, id));
    ...
}

Because the reference belongs to the caller frame, release_reference_state()
returns -EINVAL, triggering the warning.

> +	invalidate_slices_of_dynptr(env, state->stack[spi].spilled_ptr.id);
>  
>  	/* Invalidate any dynptr clones */
>  	for (i = 1; i < state->allocated_stack / BPF_REG_SIZE; i++) {

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20260829102618.105116-1-xyx2021@mail.ustc.edu.cn?part=2

^ permalink raw reply	[flat|nested] 11+ messages in thread

* Re: [PATCH 6.6.y v2 0/3] bpf: fix dynptr release handling
  2026-08-29 10:26 ` [PATCH 6.6.y v2 0/3] bpf: fix dynptr release handling Xu Yunxiang
                     ` (2 preceding siblings ...)
  2026-08-29 10:26   ` [PATCH 6.6.y v2 3/3] bpf: Invalidate dynptr clones in every call frame on release Xu Yunxiang
@ 2026-08-30  2:56   ` Sasha Levin
  3 siblings, 0 replies; 11+ messages in thread
From: Sasha Levin @ 2026-08-30  2:56 UTC (permalink / raw)
  To: stable
  Cc: Sasha Levin, gregkh, bpf, ast, daniel, andrii, eddyz87,
	martin.lau, ameryhung, joannelkoong, Xu Yunxiang

On Sat, Aug 29, 2026 at 06:26:15PM +0800, Xu Yunxiang wrote:
> This series supplies three stable-sized equivalents using the existing
> 6.6.y representation.
...
> All three patches preserve Amery's upstream authorship and complete
> trailer chain.

That is the problem. By your own changelogs this is code you wrote for the old
verifier, not a cherry-pick of anything, but the commits carry tags indicating
otherwise. None of them wrote or reviewed it. Those trailers have to go; the
DCO chain must name whoever actually wrote the code.

-- 
Thanks,
Sasha

^ permalink raw reply	[flat|nested] 11+ messages in thread

end of thread, other threads:[~2026-08-30  3:01 UTC | newest]

Thread overview: 11+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-29  3:27 [PATCH 6.6.y 0/2] bpf: fix dynptr lifetime invalidation on release Xu Yunxiang
2026-08-29  3:36 ` [PATCH 6.6.y 1/2] bpf: Invalidate dynptr slices by dynptr_id " Xu Yunxiang
2026-08-29  3:56   ` sashiko-bot
2026-08-29  3:37 ` [PATCH 6.6.y 2/2] bpf: Invalidate dynptr clones in every call frame " Xu Yunxiang
2026-08-29  4:07   ` sashiko-bot
2026-08-29 10:26 ` [PATCH 6.6.y v2 0/3] bpf: fix dynptr release handling Xu Yunxiang
2026-08-29 10:26   ` [PATCH 6.6.y v2 1/3] bpf: Invalidate dynptr slices by dynptr_id on release Xu Yunxiang
2026-08-29 10:26   ` [PATCH 6.6.y v2 2/3] bpf: Propagate referenced dynptr release errors Xu Yunxiang
2026-08-29 10:42     ` sashiko-bot
2026-08-29 10:26   ` [PATCH 6.6.y v2 3/3] bpf: Invalidate dynptr clones in every call frame on release Xu Yunxiang
2026-08-30  2:56   ` [PATCH 6.6.y v2 0/3] bpf: fix dynptr release handling Sasha Levin

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox