* [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