BPF List
 help / color / mirror / Atom feed
* [PATCH bpf-next v4 0/2] bpf: Destroy callee-local dynptrs on subprog return
@ 2026-09-17  5:19 Xu Yunxiang
  2026-09-17  5:19 ` [PATCH bpf-next v4 1/2] " Xu Yunxiang
  2026-09-17  5:20 ` [PATCH bpf-next v4 2/2] selftests/bpf: Test dynptr teardown " Xu Yunxiang
  0 siblings, 2 replies; 5+ messages in thread
From: Xu Yunxiang @ 2026-09-17  5:19 UTC (permalink / raw)
  To: bpf; +Cc: ast, daniel, andrii, eddyz87, memxor, ameryhung, sashiko-bot

A dynptr data slice is valid only while the specific dynptr that produced
it remains valid. prepare_func_exit() currently frees a callee stack without
running the normal dynptr teardown. A slice derived from a callee-local
dynptr can therefore escape into the caller and remain usable after its
source dynptr has disappeared.

Patch 1 destroys every callee-local dynptr before freeing the frame, using
the existing stack-slot teardown so exact descendants are invalidated and
the last release-capable referenced dynptr cannot be lost. It distinguishes
function-return teardown from stack-slot overwrite in the diagnostic and
keeps the affected existing callback expectation in the same commit.

Patch 2 covers immediate use of a ring buffer clone slice, release-time
invalidation of a caller dynptr slice after it crosses a subprogram return,
an immediate skb use, and the last-reference error path. Its success control
checks that destroying a callee-local clone preserves a slice derived from
the caller-owned dynptr.

The release-time case protects existing submit-time invalidation; the
return-time cases check the lifetime of the source descriptor. The
positive control checks verifier acceptance without a runtime assertion.

Please queue the verifier fix for stable after it reaches the BPF tree.

This local v4 refresh is based on bpf-next 10c4f610b215. Teardown
follows the complete return-register copy loop, including R0:R2 on this
base.

Validation on this exact candidate, with a matching bpf_testmod:
  - W=1 verifier object and complete kernel/modules builds passed.
  - test_progs -t dynptr: 2/136 passed, no skips or failures.
  - test_progs -t file_reader: 1/8 passed, no skips or failures.
  - The qdisc invalid_dynptr_returned_slice case passed (1/1).
  - test_progs -t global_func: 3/29 passed, one conditional skip.
  - test_progs -t cb_refs: 1/0 passed, no skips or failures.
  - test_progs -t subprogs: 5/33 passed, no skips or failures.

The global-function skip is the compatibility ctx_arg_rewrite case,
which skips when the kernel supports the native context-argument tag.
The first subprogs run lacked two auxiliary BPF object files; after
packaging those already-built objects, only that group was rerun. The
kernel, runner and module were unchanged. No kernel WARN, Oops or panic
was found. The full unfiltered suite and sanitizer configurations were
not run; these results do not claim a dedicated R2 slice-return test.

Changes in v4:
  - Emit a function-return-specific diagnostic when a departing frame holds
    the last dynptr capable of releasing a referenced resource.
  - Run callee dynptr teardown before switching to the caller instruction so
    diagnostics and invalidation records identify the actual BPF_EXIT.
  - Move the existing callback expectation update into patch 1 to keep it
    with the diagnostic change.
  - Make the release-time test derive its slice from the caller-owned dynptr,
    isolating submit-time invalidation from callee teardown.
  - Rebase from e4a62833adff to bpf-next 10c4f610b215.

v3: https://lore.kernel.org/r/20260911084209.3481285-1-xyx2021@mail.ustc.edu.cn

Changes in v3:
  - Replace slice reparenting with normal destruction of every dynptr in the
    departing callee stack, as suggested by Amery.
  - Invalidate escaped clone slices on frame return and test immediate use
    before resource release.
  - Cover loss of the last release-capable dynptr and add a control preserving
    a caller-owned slice while destroying a separate callee-local clone.

v2: https://lore.kernel.org/r/20260910044006.2279547-1-xyx2021@mail.ustc.edu.cn
v1: https://lore.kernel.org/r/20260909042858.1734125-1-xyx2021@mail.ustc.edu.cn

Xu Yunxiang (2):
  bpf: Destroy callee-local dynptrs on subprog return
  selftests/bpf: Test dynptr teardown on subprog return

 kernel/bpf/verifier.c                         | 51 ++++++++--
 .../selftests/bpf/prog_tests/bpf_qdisc.c      |  2 +
 ...disc_fail__invalid_dynptr_returned_slice.c | 76 ++++++++++++++
 .../testing/selftests/bpf/progs/dynptr_fail.c | 99 ++++++++++++++++++-
 4 files changed, 220 insertions(+), 8 deletions(-)
 create mode 100644 tools/testing/selftests/bpf/progs/bpf_qdisc_fail__invalid_dynptr_returned_slice.c


base-commit: 10c4f610b215bf961235141161992f010cf7e451
-- 
2.43.0


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

* [PATCH bpf-next v4 1/2] bpf: Destroy callee-local dynptrs on subprog return
  2026-09-17  5:19 [PATCH bpf-next v4 0/2] bpf: Destroy callee-local dynptrs on subprog return Xu Yunxiang
@ 2026-09-17  5:19 ` Xu Yunxiang
  2026-09-17  6:15   ` bot+bpf-ci
  2026-09-17 14:48   ` Alexei Starovoitov
  2026-09-17  5:20 ` [PATCH bpf-next v4 2/2] selftests/bpf: Test dynptr teardown " Xu Yunxiang
  1 sibling, 2 replies; 5+ messages in thread
From: Xu Yunxiang @ 2026-09-17  5:19 UTC (permalink / raw)
  To: bpf; +Cc: ast, daniel, andrii, eddyz87, memxor, ameryhung, sashiko-bot

A data slice is valid only while the specific dynptr that produced it
remains valid. prepare_func_exit() copies a subprogram return value into
the caller and then frees the callee state without applying the normal
destruction semantics to dynptrs in the callee stack.

A callee can therefore derive a slice from a local dynptr, return or spill
the slice into its caller, and then let the dynptr disappear with the
callee frame. The slice retains the id of a dynptr that is no longer in the
verifier state, so the verifier continues to accept accesses through it.

Before the object relationship refactor, bpf_dynptr_data() slices from
referenced dynptrs also carried the shared reference id, allowing a later
release to catch some of these escaped slices. The refactor made those
slices precise children of their source dynptr and exposed the missing
teardown as an accepted stale access even after the shared resource is
released.

Before freeing a callee, scan the first slot of every dynptr in its stack
and call destroy_if_dynptr_stack_slot(). The existing teardown rejects
losing the last dynptr for a referenced resource and invalidates the dynptr
and all of its descendants, including slices held in the caller. Propagate
any teardown error through prepare_func_exit().

Tell destroy_if_dynptr_stack_slot() when it is running for a departing
frame so the last-reference diagnostic describes a function return and
recommends releasing the resource before returning. Update the affected
callback test expectation in this change so this commit remains test-clean.

Fixes: 308c7a0ae885 ("bpf: Refactor object relationship tracking and fix dynptr UAF bug")
Reported-by: Sashiko <sashiko-bot@kernel.org>
Closes: https://lore.kernel.org/r/20260829040712.B1B511F000E9@smtp.kernel.org
Suggested-by: Amery Hung <ameryhung@gmail.com>
Link: https://lore.kernel.org/r/CAMB2axOmdHwSrHihSRskDywkCmEGQOX+6dZPN_A9u-HhxY3UDA@mail.gmail.com
Assisted-by: LLM
Signed-off-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>
---
 kernel/bpf/verifier.c                         | 51 ++++++++++++++++---
 .../testing/selftests/bpf/progs/dynptr_fail.c |  4 +-
 2 files changed, 47 insertions(+), 8 deletions(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 6c6b8d8520cdf..4beca102c11d1 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -709,7 +709,8 @@ static void mark_dynptr_cb_reg(struct bpf_verifier_env *env,
 }
 
 static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env,
-				        struct bpf_func_state *state, int spi);
+					struct bpf_func_state *state, int spi,
+					bool on_func_exit);
 
 static int mark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_state *reg,
 				   enum bpf_arg_type arg_type, int insn_idx,
@@ -732,10 +733,10 @@ static int mark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_
 	 * case they do belong to same dynptr, second call won't see slot_type
 	 * as STACK_DYNPTR and will simply skip destruction.
 	 */
-	err = destroy_if_dynptr_stack_slot(env, state, spi);
+	err = destroy_if_dynptr_stack_slot(env, state, spi, false);
 	if (err)
 		return err;
-	err = destroy_if_dynptr_stack_slot(env, state, spi - 1);
+	err = destroy_if_dynptr_stack_slot(env, state, spi - 1, false);
 	if (err)
 		return err;
 
@@ -843,8 +844,34 @@ static int dynptr_ref_cnt(struct bpf_verifier_env *env, int v_parent_id)
 	return ref_cnt;
 }
 
+static int destroy_dynptrs_on_func_exit(struct bpf_verifier_env *env,
+					struct bpf_func_state *callee)
+{
+	int i, err;
+
+	for (i = 0; i < callee->allocated_stack / BPF_REG_SIZE; i++) {
+		struct bpf_stack_state *slot = &callee->stack[i];
+
+		if (slot->slot_type[0] != STACK_DYNPTR ||
+		    !slot->spilled_ptr.dynptr.first_slot)
+			continue;
+
+		/*
+		 * A callee-local dynptr is destroyed when its stack frame goes
+		 * away. Apply the normal stack-slot teardown so references cannot
+		 * be lost and slices derived from that dynptr are invalidated.
+		 */
+		err = destroy_if_dynptr_stack_slot(env, callee, i, true);
+		if (err)
+			return err;
+	}
+
+	return 0;
+}
+
 static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env,
-				        struct bpf_func_state *state, int spi)
+					struct bpf_func_state *state, int spi,
+					bool on_func_exit)
 {
 	int err = 0;
 
@@ -867,6 +894,15 @@ static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env,
 	 */
 	if (dynptr_type_referenced(state->stack[spi].spilled_ptr.dynptr.type) &&
 	    dynptr_ref_cnt(env, state->stack[spi].spilled_ptr.parent_id) <= 1) {
+		if (on_func_exit) {
+			verbose(env, "cannot return with referenced dynptr in callee stack\n");
+			bpf_diag_res(env, env->insn_idx,
+				     "referenced dynptr on function return",
+				     "The departing stack frame contains the last dynptr that can release a referenced resource.",
+				     "Release the referenced resource before returning from this function.");
+			return -EINVAL;
+		}
+
 		verbose(env, "cannot overwrite referenced dynptr\n");
 		bpf_diag_res(
 			env, env->insn_idx, "referenced dynptr overwrite",
@@ -3630,7 +3666,7 @@ static int check_stack_write_fixed_off(struct bpf_verifier_env *env,
 			env->insn_aux_data[insn_idx].nospec_result = true;
 	}
 
-	err = destroy_if_dynptr_stack_slot(env, state, spi);
+	err = destroy_if_dynptr_stack_slot(env, state, spi, false);
 	if (err)
 		return err;
 
@@ -3750,7 +3786,7 @@ static int check_stack_write_var_off(struct bpf_verifier_env *env,
 		int spi;
 
 		spi = bpf_get_spi(i);
-		err = destroy_if_dynptr_stack_slot(env, state, spi);
+		err = destroy_if_dynptr_stack_slot(env, state, spi, false);
 		if (err)
 			return err;
 	}
@@ -11043,6 +11079,9 @@ static int prepare_func_exit(struct bpf_verifier_env *env, int *insn_idx)
 			bpf_diag_mod_end(env);
 		}
 	}
+	err = destroy_dynptrs_on_func_exit(env, callee);
+	if (err)
+		return err;
 
 	/* for callbacks like bpf_loop or bpf_for_each_map_elem go back to callsite,
 	 * there function call logic would reschedule callback visit. If iteration
diff --git a/tools/testing/selftests/bpf/progs/dynptr_fail.c b/tools/testing/selftests/bpf/progs/dynptr_fail.c
index 1cd61d72c166f..24122addad327 100644
--- a/tools/testing/selftests/bpf/progs/dynptr_fail.c
+++ b/tools/testing/selftests/bpf/progs/dynptr_fail.c
@@ -125,9 +125,9 @@ static int missing_release_callback_fn(__u32 index, void *data)
 	return 0;
 }
 
-/* Any dynptr initialized within a callback must have bpf_dynptr_put called */
+/* A callback cannot return with the last dynptr for a referenced resource. */
 SEC("?raw_tp")
-__failure __msg("Unreleased reference id")
+__failure __msg("cannot return with referenced dynptr")
 int ringbuf_missing_release_callback(void *ctx)
 {
 	bpf_loop(10, missing_release_callback_fn, NULL, 0);
-- 
2.43.0


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

* [PATCH bpf-next v4 2/2] selftests/bpf: Test dynptr teardown on subprog return
  2026-09-17  5:19 [PATCH bpf-next v4 0/2] bpf: Destroy callee-local dynptrs on subprog return Xu Yunxiang
  2026-09-17  5:19 ` [PATCH bpf-next v4 1/2] " Xu Yunxiang
@ 2026-09-17  5:20 ` Xu Yunxiang
  1 sibling, 0 replies; 5+ messages in thread
From: Xu Yunxiang @ 2026-09-17  5:20 UTC (permalink / raw)
  To: bpf; +Cc: ast, daniel, andrii, eddyz87, memxor, ameryhung, sashiko-bot

Add failure tests for slices that escape a subprogram from a dynptr in the
callee stack. Cover immediate use of a ring buffer clone slice,
release-time invalidation of a caller dynptr slice after it crosses a
subprogram return, and immediate use of an skb dynptr slice in a qdisc
program.

Cover the error path where a subprogram would otherwise lose the last
dynptr capable of releasing a referenced resource.

Also add a success control which derives a slice from a caller-owned dynptr
and creates a separate clone in the callee. Destroying that local clone on
return must not invalidate the caller dynptr's slice.

Assisted-by: LLM
Signed-off-by: Xu Yunxiang <xyx2021@mail.ustc.edu.cn>
---
 .../selftests/bpf/prog_tests/bpf_qdisc.c      |  2 +
 ...disc_fail__invalid_dynptr_returned_slice.c | 76 +++++++++++++++
 .../testing/selftests/bpf/progs/dynptr_fail.c | 95 +++++++++++++++++++
 3 files changed, 173 insertions(+)
 create mode 100644 tools/testing/selftests/bpf/progs/bpf_qdisc_fail__invalid_dynptr_returned_slice.c

diff --git a/tools/testing/selftests/bpf/prog_tests/bpf_qdisc.c b/tools/testing/selftests/bpf/prog_tests/bpf_qdisc.c
index 6dbd1487343c0..122ecb7e98e2a 100644
--- a/tools/testing/selftests/bpf/prog_tests/bpf_qdisc.c
+++ b/tools/testing/selftests/bpf/prog_tests/bpf_qdisc.c
@@ -11,6 +11,7 @@
 #include "bpf_qdisc_fail__invalid_dynptr.skel.h"
 #include "bpf_qdisc_fail__invalid_dynptr_slice.skel.h"
 #include "bpf_qdisc_fail__invalid_dynptr_cross_frame.skel.h"
+#include "bpf_qdisc_fail__invalid_dynptr_returned_slice.skel.h"
 #include "bpf_qdisc_fail__untrusted_write.skel.h"
 #include "bpf_qdisc_dynptr_use_after_invalidate_clone.skel.h"
 
@@ -230,6 +231,7 @@ void test_ns_bpf_qdisc(void)
 		test_incompl_ops();
 	RUN_TESTS(bpf_qdisc_fail__invalid_dynptr);
 	RUN_TESTS(bpf_qdisc_fail__invalid_dynptr_cross_frame);
+	RUN_TESTS(bpf_qdisc_fail__invalid_dynptr_returned_slice);
 	RUN_TESTS(bpf_qdisc_fail__invalid_dynptr_slice);
 	RUN_TESTS(bpf_qdisc_fail__untrusted_write);
 	RUN_TESTS(bpf_qdisc_dynptr_use_after_invalidate_clone);
diff --git a/tools/testing/selftests/bpf/progs/bpf_qdisc_fail__invalid_dynptr_returned_slice.c b/tools/testing/selftests/bpf/progs/bpf_qdisc_fail__invalid_dynptr_returned_slice.c
new file mode 100644
index 0000000000000..8217f4c4c00c4
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/bpf_qdisc_fail__invalid_dynptr_returned_slice.c
@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <vmlinux.h>
+#include "bpf_experimental.h"
+#include "bpf_qdisc_common.h"
+#include "bpf_misc.h"
+
+char _license[] SEC("license") = "GPL";
+
+int proto;
+
+static __noinline struct ethhdr *slice_in_subprog(struct sk_buff *skb)
+{
+	struct bpf_dynptr ptr;
+
+	bpf_dynptr_from_skb((struct __sk_buff *)skb, 0, &ptr);
+	return bpf_dynptr_slice(&ptr, 0, NULL, sizeof(struct ethhdr));
+}
+
+SEC("struct_ops")
+__failure __msg("invalid mem access 'scalar'")
+int BPF_PROG(invalid_dynptr_returned_slice, struct sk_buff *skb,
+	     struct Qdisc *sch, struct bpf_sk_buff_ptr *to_free)
+{
+	struct ethhdr *hdr;
+
+	hdr = slice_in_subprog(skb);
+	if (!hdr) {
+		bpf_qdisc_skb_drop(skb, to_free);
+		return NET_XMIT_DROP;
+	}
+
+	/* this should fail */
+	proto = hdr->h_proto;
+
+	bpf_qdisc_skb_drop(skb, to_free);
+
+	return NET_XMIT_DROP;
+}
+
+SEC("struct_ops")
+__auxiliary
+struct sk_buff *BPF_PROG(bpf_qdisc_test_dequeue, struct Qdisc *sch)
+{
+	return NULL;
+}
+
+SEC("struct_ops")
+__auxiliary
+int BPF_PROG(bpf_qdisc_test_init, struct Qdisc *sch, struct nlattr *opt,
+	     struct netlink_ext_ack *extack)
+{
+	return 0;
+}
+
+SEC("struct_ops")
+__auxiliary
+void BPF_PROG(bpf_qdisc_test_reset, struct Qdisc *sch)
+{
+}
+
+SEC("struct_ops")
+__auxiliary
+void BPF_PROG(bpf_qdisc_test_destroy, struct Qdisc *sch)
+{
+}
+
+SEC(".struct_ops")
+struct Qdisc_ops test = {
+	.enqueue   = (void *)invalid_dynptr_returned_slice,
+	.dequeue   = (void *)bpf_qdisc_test_dequeue,
+	.init      = (void *)bpf_qdisc_test_init,
+	.reset     = (void *)bpf_qdisc_test_reset,
+	.destroy   = (void *)bpf_qdisc_test_destroy,
+	.id        = "bpf_qdisc_test",
+};
diff --git a/tools/testing/selftests/bpf/progs/dynptr_fail.c b/tools/testing/selftests/bpf/progs/dynptr_fail.c
index 24122addad327..aed18403ae3c0 100644
--- a/tools/testing/selftests/bpf/progs/dynptr_fail.c
+++ b/tools/testing/selftests/bpf/progs/dynptr_fail.c
@@ -1892,6 +1892,101 @@ int clone_invalidate4(void *ctx)
 	return 0;
 }
 
+static __noinline void clone_slice_in_subprog(struct bpf_dynptr *ptr, int **data)
+{
+	struct bpf_dynptr clone;
+
+	bpf_dynptr_clone(ptr, &clone);
+	*data = bpf_dynptr_data(&clone, 0, sizeof(val));
+}
+
+static __noinline void caller_slice_in_subprog(struct bpf_dynptr *ptr, int **data)
+{
+	struct bpf_dynptr clone;
+
+	*data = bpf_dynptr_data(ptr, 0, sizeof(val));
+	bpf_dynptr_clone(ptr, &clone);
+}
+
+static __noinline void reserve_dynptr_in_subprog(void)
+{
+	struct bpf_dynptr ptr;
+
+	bpf_ringbuf_reserve_dynptr(&ringbuf, val, 0, &ptr);
+}
+
+/* A subprogram cannot lose the last dynptr that can release a resource. */
+SEC("?raw_tp")
+__failure __msg("cannot return with referenced dynptr")
+int referenced_dynptr_lost_on_subprog_return(void *ctx)
+{
+	reserve_dynptr_in_subprog();
+
+	return 0;
+}
+
+/*
+ * Destroying a local clone on return must not invalidate a slice whose
+ * source dynptr belongs to the caller.
+ */
+SEC("?raw_tp")
+__success
+int caller_dynptr_slice_across_subprog_valid(void *ctx)
+{
+	struct bpf_dynptr ptr;
+	int *data = NULL;
+
+	bpf_ringbuf_reserve_dynptr(&ringbuf, val, 0, &ptr);
+	caller_slice_in_subprog(&ptr, &data);
+	if (data)
+		*data = 123;
+	bpf_ringbuf_submit_dynptr(&ptr, 0);
+
+	return 0;
+}
+
+/*
+ * A slice that escapes a clone's call frame is invalid once the local
+ * clone is destroyed on return.
+ */
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int clone_slice_returned_frame_invalid(void *ctx)
+{
+	struct bpf_dynptr ptr;
+	int *data = NULL;
+
+	bpf_ringbuf_reserve_dynptr(&ringbuf, val, 0, &ptr);
+	clone_slice_in_subprog(&ptr, &data);
+	if (data)
+		/* this should fail */
+		*data = 123;
+	bpf_ringbuf_submit_dynptr(&ptr, 0);
+
+	return 0;
+}
+
+/*
+ * A slice from a caller-owned dynptr survives the subprogram return, but
+ * releasing the shared reservation must invalidate it.
+ */
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int caller_dynptr_slice_release_after_subprog_invalid(void *ctx)
+{
+	struct bpf_dynptr ptr;
+	int *data = NULL;
+
+	bpf_ringbuf_reserve_dynptr(&ringbuf, val, 0, &ptr);
+	caller_slice_in_subprog(&ptr, &data);
+	bpf_ringbuf_submit_dynptr(&ptr, 0);
+	if (data)
+		/* this should fail */
+		*data = 123;
+
+	return 0;
+}
+
 /* Invalidating a dynptr should invalidate any data slices
  * of its parent
  */
-- 
2.43.0


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

* Re: [PATCH bpf-next v4 1/2] bpf: Destroy callee-local dynptrs on subprog return
  2026-09-17  5:19 ` [PATCH bpf-next v4 1/2] " Xu Yunxiang
@ 2026-09-17  6:15   ` bot+bpf-ci
  2026-09-17 14:48   ` Alexei Starovoitov
  1 sibling, 0 replies; 5+ messages in thread
From: bot+bpf-ci @ 2026-09-17  6:15 UTC (permalink / raw)
  To: xyx2021, bpf
  Cc: ast, daniel, andrii, eddyz87, memxor, ameryhung, sashiko-bot, ast,
	andrii, daniel, martin.lau, eddyz87, yonghong.song, mason,
	ihor.solodrai

[-- Attachment #1: Type: text/plain, Size: 4251 bytes --]

> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 6c6b8d8520cdf..4beca102c11d1 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c

[ ... ]

> @@ -843,8 +844,34 @@ static int dynptr_ref_cnt(struct bpf_verifier_env *env, int v_parent_id)
>  	return ref_cnt;
>  }
>
> +static int destroy_dynptrs_on_func_exit(struct bpf_verifier_env *env,
> +					struct bpf_func_state *callee)
> +{
> +	int i, err;
> +
> +	for (i = 0; i < callee->allocated_stack / BPF_REG_SIZE; i++) {
> +		struct bpf_stack_state *slot = &callee->stack[i];
> +
> +		if (slot->slot_type[0] != STACK_DYNPTR ||
> +		    !slot->spilled_ptr.dynptr.first_slot)
> +			continue;
> +
> +		/*
> +		 * A callee-local dynptr is destroyed when its stack frame goes
> +		 * away. Apply the normal stack-slot teardown so references cannot
> +		 * be lost and slices derived from that dynptr are invalidated.
> +		 */
> +		err = destroy_if_dynptr_stack_slot(env, callee, i, true);
> +		if (err)
> +			return err;
> +	}
> +
> +	return 0;
> +}

Does this teardown need to apply to every dynptr in the departing frame?
The invalidation rejects programs that verified before this commit and
are safe at run time.

destroy_if_dynptr_stack_slot() calls release_reference() which
invalidates every register and spilled slot in EVERY frame whose id or
parent_id matches:

kernel/bpf/verifier.c:release_reference() {
    bpf_for_each_reg_in_vstate_mask(vstate, state, reg, stack, mask, ({
        if (reg->id != id && reg->parent_id != id)
            continue;
        ...
        mark_reg_invalid(env, reg);

A data slice carries parent_id equal to the source dynptr id, so a slice
returned in R0 and already copied into caller->regs[BPF_REG_0] is marked
invalid, and the caller's later dereference fails with "invalid mem
access 'scalar'".

For a dynptr whose data is the callee's own stack (bpf_dynptr_from_mem
over a callee-local buffer) that invalidation is exactly right and is the
UAF the commit fixes.

But for skb/xdp/skb_meta dynptrs, and for BPF_DYNPTR_TYPE_LOCAL dynptrs
created over map-value or ringbuf memory, the slice does not point into
the departing frame. bpf_dynptr_slice() returns pointers into skb linear
data or caller-provided buffers whose lifetime is unaffected by the
subprog returning:

kernel/bpf/helpers.c:bpf_dynptr_slice() {
    case BPF_DYNPTR_TYPE_LOCAL:
    case BPF_DYNPTR_TYPE_RINGBUF:
        return ptr->data + ptr->offset + offset;
    case BPF_DYNPTR_TYPE_SKB:
        if (buffer__nullable)
            return skb_header_pointer(...);
        else
            return skb_pointer_if_linear(ptr->data, ptr->offset + offset, len);

Nothing else about such a slice depends on the source dynptr's stack slot
still existing either: packet-change invalidation is driven by the slice
register's own type flags, not by the dynptr.

So the natural idiom

    static __noinline struct ethhdr *slice_in_subprog(struct sk_buff *skb)
    {
        struct bpf_dynptr ptr;
        bpf_dynptr_from_skb((struct __sk_buff *)skb, 0, &ptr);
        return bpf_dynptr_slice(&ptr, 0, NULL, sizeof(struct ethhdr));
    }

loaded before this commit and is rejected after it. The follow-up commit
f0ee00b2958de ("selftests/bpf: Test dynptr teardown on subprog return")
codifies the new rejection as intended in
progs/bpf_qdisc_fail__invalid_dynptr_returned_slice.c with __failure
__msg("invalid mem access 'scalar'").

Could the teardown be restricted to dynptrs whose data can live in the
departing frame, or should the changelog call out that skb/xdp and
map-value-backed slices escaping a subprogram are now rejected?

> @@ -11043,6 +11079,9 @@ static int prepare_func_exit(struct bpf_verifier_env *env, int *insn_idx)
>  			bpf_diag_mod_end(env);
>  		}
>  	}
> +	err = destroy_dynptrs_on_func_exit(env, callee);
> +	if (err)
> +		return err;
>
>  	/* for callbacks like bpf_loop or bpf_for_each_map_elem go back to callsite,
>  	 * there function call logic would reschedule callback visit. If iteration

[ ... ]


---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md

CI run summary: https://github.com/kernel-patches/bpf/actions/runs/35185711006

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

* Re: [PATCH bpf-next v4 1/2] bpf: Destroy callee-local dynptrs on subprog return
  2026-09-17  5:19 ` [PATCH bpf-next v4 1/2] " Xu Yunxiang
  2026-09-17  6:15   ` bot+bpf-ci
@ 2026-09-17 14:48   ` Alexei Starovoitov
  1 sibling, 0 replies; 5+ messages in thread
From: Alexei Starovoitov @ 2026-09-17 14:48 UTC (permalink / raw)
  To: Xu Yunxiang, bpf; +Cc: daniel, andrii, eddyz87, memxor, ameryhung, sashiko-bot

On Thu, Sep 17, 2026 at 01:19 PM Xu Yunxiang <xyx2021@mail.ustc.edu.cn> wrote:

>  static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env,
> -				        struct bpf_func_state *state, int spi)
> +					struct bpf_func_state *state, int spi,
> +					bool on_func_exit)
>  {
>  	int err = 0;
>
> @@ -867,6 +894,15 @@ static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env,
>  	 */
>  	if (dynptr_type_referenced(state->stack[spi].spilled_ptr.dynptr.type) &&
>  	    dynptr_ref_cnt(env, state->stack[spi].spilled_ptr.parent_id) <= 1) {
> +		if (on_func_exit) {
> +			verbose(env, "cannot return with referenced dynptr in callee stack\n");
> +			bpf_diag_res(env, env->insn_idx,
> +				     "referenced dynptr on function return",
> +				     "The departing stack frame contains the last dynptr that can release a referenced resource.",
> +				     "Release the referenced resource before returning from this function.");
> +			return -EINVAL;
> +		}

Pls
 don't add a bool to destroy_if_dynptr_stack_slot() and touch all of
its callers just to print a different message.

pw-bot: cr

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

end of thread, other threads:[~2026-09-17 14:48 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-17  5:19 [PATCH bpf-next v4 0/2] bpf: Destroy callee-local dynptrs on subprog return Xu Yunxiang
2026-09-17  5:19 ` [PATCH bpf-next v4 1/2] " Xu Yunxiang
2026-09-17  6:15   ` bot+bpf-ci
2026-09-17 14:48   ` Alexei Starovoitov
2026-09-17  5:20 ` [PATCH bpf-next v4 2/2] selftests/bpf: Test dynptr teardown " Xu Yunxiang

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