* [PATCH bpf 0/2] bpf: Preserve rdonly_cast dynptr slice lifetime
@ 2026-06-15 8:57 Nuoqi Gui
2026-06-15 8:57 ` [PATCH bpf 1/2] bpf: Preserve dynptr slice lifetime across rdonly_cast Nuoqi Gui
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Nuoqi Gui @ 2026-06-15 8:57 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Martin KaFai Lau, John Fastabend, Shuah Khan, bpf,
linux-kselftest, linux-kernel, Nuoqi Gui
bpf_rdonly_cast() is an identity operation at runtime, but the verifier
currently assigns the result fresh return-state metadata. When the source
pointer comes from bpf_dynptr_slice(), that drops the relationship needed
to invalidate the alias when the dynptr is released.
Preserve the dynptr-slice lifetime metadata across bpf_rdonly_cast(), and
add verifier selftests covering cases where a dynptr slice is cast and then
read after the dynptr is released or after the originating dynptr stack slot is
overwritten while a clone still holds the reference. Also cover the valid clone
case where the cast alias is derived from the clone and remains usable after
the original dynptr is overwritten.
This fixes regressions introduced by 66e3a13e7c2c ("bpf: Add
bpf_dynptr_slice and bpf_dynptr_slice_rdwr") and a35b9af4ec2c ("bpf: Add a
kfunc for generic type cast").
Impact is bounded to verifier lifetime tracking for CAP-required BPF
programs. The evidence confirms stale read acceptance after dynptr release,
including a typed field-read shape, but does not demonstrate unprivileged
reachability, cross-context disclosure, controlled reuse, arbitrary kernel
read, write-capable aliasing, privilege escalation, crash, or KASAN/UAF
signal. No CVE, embargo, or security escalation is claimed.
Validation:
- The new dynptr_fail.c subtests exercise the verifier's dynptr-slice
lifetime invariant across bpf_rdonly_cast(): a cast alias of a
bpf_dynptr_slice() result must be invalidated when the dynptr is released or
its stack slot overwritten, while a cast alias derived from a surviving clone
must stay usable.
- Unpatched bpf at 76f62d237538: test_progs -t dynptr FAILs as expected,
because the four __failure subtests
(dynptr_rdonly_cast_{,typed_}slice_after_{release,overwrite}) are wrongly
accepted - the stale read survives dynptr release.
- Patched with this series: test_progs -t dynptr PASSes, because those four
casts are now rejected with "invalid mem access 'scalar'" and the two
__success clone subtests remain accepted (all six dynptr_rdonly_cast_*
subtests OK, rc 0).
Signed-off-by: Nuoqi Gui <gnq25@mails.tsinghua.edu.cn>
---
Nuoqi Gui (2):
bpf: Preserve dynptr slice lifetime across rdonly_cast
selftests/bpf: Cover rdonly_cast dynptr slice lifetime
include/linux/bpf_verifier.h | 5 +
kernel/bpf/verifier.c | 36 ++++--
tools/testing/selftests/bpf/progs/dynptr_fail.c | 160 ++++++++++++++++++++++++
3 files changed, 193 insertions(+), 8 deletions(-)
---
base-commit: 76f62d237538b456354a44e796a541cde03c6e28
change-id: 20260615-f01-09-rdonly-cast-dynptr-lifetime-de114e5a1838
Best regards,
--
Nuoqi Gui <gnq25@mails.tsinghua.edu.cn>
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH bpf 1/2] bpf: Preserve dynptr slice lifetime across rdonly_cast
2026-06-15 8:57 [PATCH bpf 0/2] bpf: Preserve rdonly_cast dynptr slice lifetime Nuoqi Gui
@ 2026-06-15 8:57 ` Nuoqi Gui
2026-06-15 11:24 ` sashiko-bot
2026-06-15 8:57 ` [PATCH bpf 2/2] selftests/bpf: Cover rdonly_cast dynptr slice lifetime Nuoqi Gui
2026-06-15 9:40 ` [PATCH bpf 0/2] bpf: Preserve " Kumar Kartikeya Dwivedi
2 siblings, 1 reply; 7+ messages in thread
From: Nuoqi Gui @ 2026-06-15 8:57 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Martin KaFai Lau, John Fastabend, Shuah Khan, bpf,
linux-kselftest, linux-kernel, Nuoqi Gui
bpf_rdonly_cast() is an identity operation at runtime and is also fixed
up to a register move by the verifier. Its verifier return-state setup
must preserve lifetime metadata that belongs to the source pointer.
On this verifier model, dynptr-slice kfunc returns carry dynptr_id, while
refcounted dynptr release invalidates aliases by ref_obj_id. Attach the
ref_obj_id to bpf_dynptr_slice() and bpf_dynptr_slice_rdwr() returns.
Propagate source id and release metadata through bpf_rdonly_cast().
For typed rdonly_cast() aliases, keep the source dynptr_id in reg id so
stack-overwrite invalidation can distinguish the originating dynptr when a
clone keeps the ref alive.
Otherwise, a cast alias can survive bpf_ringbuf_discard_dynptr() and
remain readable after the dynptr release.
Fixes: 66e3a13e7c2c ("bpf: Add bpf_dynptr_slice and bpf_dynptr_slice_rdwr")
Fixes: a35b9af4ec2c ("bpf: Add a kfunc for generic type cast")
Signed-off-by: Nuoqi Gui <gnq25@mails.tsinghua.edu.cn>
---
include/linux/bpf_verifier.h | 5 +++++
kernel/bpf/verifier.c | 36 ++++++++++++++++++++++++++++--------
2 files changed, 33 insertions(+), 8 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 185b2aa43a420..0bc4b40e76bd0 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -1366,6 +1366,11 @@ struct bpf_kfunc_call_arg_meta {
u32 id;
u32 ref_obj_id;
} initialized_dynptr;
+ struct {
+ u32 id;
+ u32 dynptr_id;
+ u32 ref_obj_id;
+ } rdonly_cast_src;
struct {
u8 spi;
u8 frameno;
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index ff9b1f68ceca4..7cdf20944f31a 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -832,10 +832,16 @@ static int destroy_if_dynptr_stack_slot(struct bpf_verifier_env *env,
dynptr_id = state->stack[spi].spilled_ptr.id;
/* Invalidate any slices associated with this dynptr */
bpf_for_each_reg_in_vstate(env->cur_state, fstate, dreg, ({
- /* Dynptr slices are only PTR_TO_MEM_OR_NULL and PTR_TO_MEM */
- if (dreg->type != (PTR_TO_MEM | PTR_MAYBE_NULL) && dreg->type != PTR_TO_MEM)
+ if (base_type(dreg->type) == PTR_TO_MEM &&
+ dreg->dynptr_id == dynptr_id) {
+ mark_reg_invalid(env, dreg);
continue;
- if (dreg->dynptr_id == dynptr_id)
+ }
+
+ /* Typed bpf_rdonly_cast() aliases keep dynptr_id in reg id. */
+ if (base_type(dreg->type) == PTR_TO_BTF_ID &&
+ (dreg->type & PTR_UNTRUSTED) &&
+ dreg->id == dynptr_id)
mark_reg_invalid(env, dreg);
}));
@@ -12085,8 +12091,17 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
continue;
}
- if (is_kfunc_arg_ignore(btf, &args[i]) || is_kfunc_arg_implicit(meta, i))
+ if (is_kfunc_arg_ignore(btf, &args[i]) || is_kfunc_arg_implicit(meta, i)) {
+ if (meta->func_id == special_kfunc_list[KF_bpf_rdonly_cast] &&
+ i == 0 && is_spillable_regtype(reg->type)) {
+ meta->rdonly_cast_src.id = reg->id;
+ if (base_type(reg->type) == PTR_TO_MEM)
+ meta->rdonly_cast_src.dynptr_id = reg->dynptr_id;
+ meta->rdonly_cast_src.ref_obj_id = reg->ref_obj_id;
+ }
+
continue;
+ }
t = btf_type_skip_modifiers(btf, args[i].type, NULL);
@@ -12935,10 +12950,17 @@ static int check_special_kfunc(struct bpf_verifier_env *env, struct bpf_kfunc_ca
regs[BPF_REG_0].type = PTR_TO_BTF_ID | PTR_UNTRUSTED;
regs[BPF_REG_0].btf = desc_btf;
regs[BPF_REG_0].btf_id = meta->arg_constant.value;
+ regs[BPF_REG_0].id = meta->rdonly_cast_src.id;
+ if (!regs[BPF_REG_0].id)
+ regs[BPF_REG_0].id = meta->rdonly_cast_src.dynptr_id;
+ regs[BPF_REG_0].ref_obj_id = meta->rdonly_cast_src.ref_obj_id;
} else if (btf_type_is_void(ret_t)) {
mark_reg_known_zero(env, regs, BPF_REG_0);
regs[BPF_REG_0].type = PTR_TO_MEM | MEM_RDONLY | PTR_UNTRUSTED;
regs[BPF_REG_0].mem_size = 0;
+ regs[BPF_REG_0].id = meta->rdonly_cast_src.id;
+ regs[BPF_REG_0].dynptr_id = meta->rdonly_cast_src.dynptr_id;
+ regs[BPF_REG_0].ref_obj_id = meta->rdonly_cast_src.ref_obj_id;
} else {
verbose(env,
"kfunc bpf_rdonly_cast type ID argument must be of a struct or void\n");
@@ -12975,11 +12997,9 @@ static int check_special_kfunc(struct bpf_verifier_env *env, struct bpf_kfunc_ca
return -EFAULT;
}
regs[BPF_REG_0].dynptr_id = meta->initialized_dynptr.id;
+ regs[BPF_REG_0].ref_obj_id = meta->initialized_dynptr.ref_obj_id;
- /* we don't need to set BPF_REG_0's ref obj id
- * because packet slices are not refcounted (see
- * dynptr_type_refcounted)
- */
+ /* ref_obj_id is zero for non-refcounted packet slices. */
} else {
return 0;
}
--
2.34.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH bpf 2/2] selftests/bpf: Cover rdonly_cast dynptr slice lifetime
2026-06-15 8:57 [PATCH bpf 0/2] bpf: Preserve rdonly_cast dynptr slice lifetime Nuoqi Gui
2026-06-15 8:57 ` [PATCH bpf 1/2] bpf: Preserve dynptr slice lifetime across rdonly_cast Nuoqi Gui
@ 2026-06-15 8:57 ` Nuoqi Gui
2026-06-15 9:08 ` sashiko-bot
2026-06-15 9:40 ` [PATCH bpf 0/2] bpf: Preserve " Kumar Kartikeya Dwivedi
2 siblings, 1 reply; 7+ messages in thread
From: Nuoqi Gui @ 2026-06-15 8:57 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Martin KaFai Lau, John Fastabend, Shuah Khan, bpf,
linux-kselftest, linux-kernel, Nuoqi Gui
Add verifier tests that cast a ringbuf dynptr slice with
bpf_rdonly_cast(), release the dynptr, and read through the cast alias.
Also cover stack-overwrite invalidation when a clone still holds the
reference. Also cover clone-derived cast aliases that remain valid after
overwriting the original dynptr.
Cover both void and typed return paths. The alias should be invalidated by
release or overwrite of its originating dynptr, while aliases from the
still-live clone should remain usable.
Signed-off-by: Nuoqi Gui <gnq25@mails.tsinghua.edu.cn>
---
tools/testing/selftests/bpf/progs/dynptr_fail.c | 160 ++++++++++++++++++++++++
1 file changed, 160 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/dynptr_fail.c b/tools/testing/selftests/bpf/progs/dynptr_fail.c
index b62773ce5219b..7ffe55ba28663 100644
--- a/tools/testing/selftests/bpf/progs/dynptr_fail.c
+++ b/tools/testing/selftests/bpf/progs/dynptr_fail.c
@@ -5,6 +5,7 @@
#include <string.h>
#include <stdbool.h>
#include <linux/bpf.h>
+#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <linux/if_ether.h>
@@ -13,6 +14,18 @@
char _license[] SEC("license") = "GPL";
+struct sock {
+ int __unused;
+};
+
+static __always_inline void overwrite_dynptr(struct bpf_dynptr *ptr)
+{
+ asm volatile("*(u8 *)(%[ptr] + 0) = 0"
+ :
+ : [ptr] "r"(ptr)
+ : "memory");
+}
+
struct test_info {
int x;
struct bpf_dynptr ptr;
@@ -2077,6 +2090,153 @@ int dynptr_overwrite_ref_invalidate_slice(void *ctx)
return 0;
}
+/* A read-only cast of a dynptr slice should be invalidated on release */
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int dynptr_rdonly_cast_slice_after_release(void *ctx)
+{
+ struct bpf_dynptr ptr;
+ __u64 *data;
+
+ bpf_ringbuf_reserve_dynptr(&ringbuf, sizeof(__u64), 0, &ptr);
+
+ data = bpf_dynptr_slice(&ptr, 0, NULL, sizeof(*data));
+ if (!data) {
+ bpf_ringbuf_discard_dynptr(&ptr, 0);
+ return 0;
+ }
+
+ data = bpf_rdonly_cast(data, 0);
+ bpf_ringbuf_discard_dynptr(&ptr, 0);
+
+ return *data;
+}
+
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int dynptr_rdonly_cast_typed_slice_after_release(void *ctx)
+{
+ struct bpf_dynptr ptr;
+ __u64 *data;
+
+ bpf_ringbuf_reserve_dynptr(&ringbuf, sizeof(__u64), 0, &ptr);
+
+ data = bpf_dynptr_slice(&ptr, 0, NULL, sizeof(*data));
+ if (!data) {
+ bpf_ringbuf_discard_dynptr(&ptr, 0);
+ return 0;
+ }
+
+ data = bpf_rdonly_cast(data, bpf_core_type_id_kernel(struct sock));
+ bpf_ringbuf_discard_dynptr(&ptr, 0);
+
+ return *data;
+}
+
+/* A read-only cast alias from an overwritten dynptr should be invalidated
+ * even when a clone still holds the underlying reference.
+ */
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int dynptr_rdonly_cast_slice_after_overwrite(void *ctx)
+{
+ struct bpf_dynptr ptr, clone;
+ __u64 *data;
+
+ bpf_ringbuf_reserve_dynptr(&ringbuf, sizeof(__u64), 0, &ptr);
+
+ data = bpf_dynptr_slice(&ptr, 0, NULL, sizeof(*data));
+ if (!data) {
+ bpf_ringbuf_discard_dynptr(&ptr, 0);
+ return 0;
+ }
+
+ bpf_dynptr_clone(&ptr, &clone);
+ data = bpf_rdonly_cast(data, 0);
+
+ overwrite_dynptr(&ptr);
+
+ return *data;
+}
+
+SEC("?raw_tp")
+__failure __msg("invalid mem access 'scalar'")
+int dynptr_rdonly_cast_typed_slice_after_overwrite(void *ctx)
+{
+ struct bpf_dynptr ptr, clone;
+ __u64 *data;
+
+ bpf_ringbuf_reserve_dynptr(&ringbuf, sizeof(__u64), 0, &ptr);
+
+ data = bpf_dynptr_slice(&ptr, 0, NULL, sizeof(*data));
+ if (!data) {
+ bpf_ringbuf_discard_dynptr(&ptr, 0);
+ return 0;
+ }
+
+ bpf_dynptr_clone(&ptr, &clone);
+ data = bpf_rdonly_cast(data, bpf_core_type_id_kernel(struct sock));
+
+ overwrite_dynptr(&ptr);
+
+ return *data;
+}
+
+/* A read-only cast alias from a clone should remain valid after overwriting
+ * the original dynptr while the clone still holds the reference.
+ */
+SEC("?raw_tp")
+__success
+int dynptr_rdonly_cast_clone_slice_after_parent_overwrite(void *ctx)
+{
+ struct bpf_dynptr ptr, clone;
+ __u64 *data;
+ int ret;
+
+ bpf_ringbuf_reserve_dynptr(&ringbuf, sizeof(__u64), 0, &ptr);
+ bpf_dynptr_clone(&ptr, &clone);
+
+ data = bpf_dynptr_slice(&clone, 0, NULL, sizeof(*data));
+ if (!data) {
+ bpf_ringbuf_discard_dynptr(&clone, 0);
+ return 0;
+ }
+
+ data = bpf_rdonly_cast(data, 0);
+ overwrite_dynptr(&ptr);
+
+ ret = *data;
+ bpf_ringbuf_discard_dynptr(&clone, 0);
+
+ return ret;
+}
+
+SEC("?raw_tp")
+__success
+int dynptr_rdonly_cast_typed_clone_slice_after_parent_overwrite(void *ctx)
+{
+ struct bpf_dynptr ptr, clone;
+ __u64 *data;
+ int ret;
+
+ bpf_ringbuf_reserve_dynptr(&ringbuf, sizeof(__u64), 0, &ptr);
+ bpf_dynptr_clone(&ptr, &clone);
+
+ data = bpf_dynptr_slice(&clone, 0, NULL, sizeof(*data));
+ if (!data) {
+ bpf_ringbuf_discard_dynptr(&clone, 0);
+ return 0;
+ }
+
+ data = bpf_rdonly_cast(data, bpf_core_type_id_kernel(struct sock));
+ overwrite_dynptr(&ptr);
+
+ ret = *data;
+ bpf_ringbuf_discard_dynptr(&clone, 0);
+
+ return ret;
+}
+
/*
* Data slices from a dynptr clone should remain valid after
* overwriting the original dynptr
--
2.34.1
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH bpf 2/2] selftests/bpf: Cover rdonly_cast dynptr slice lifetime
2026-06-15 8:57 ` [PATCH bpf 2/2] selftests/bpf: Cover rdonly_cast dynptr slice lifetime Nuoqi Gui
@ 2026-06-15 9:08 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-06-15 9:08 UTC (permalink / raw)
To: Nuoqi Gui; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Low] Multi-line comments do not follow the BPF subsystem comment style guidelines. The opening `/*` must be on its own line.
--
commit 23e88c2468fdbcf78bd114cff5b73d79a87a4337
Author: Nuoqi Gui <gnq25@mails.tsinghua.edu.cn>
selftests/bpf: Cover rdonly_cast dynptr slice lifetime
This commit adds verifier tests for bpf_rdonly_cast() alias lifetime. It
covers casting a ringbuf dynptr slice, releasing the dynptr, reading through
the cast alias, and stack-overwrite invalidations.
> diff --git a/tools/testing/selftests/bpf/progs/dynptr_fail.c b/tools/testing/selftests/bpf/progs/dynptr_fail.c
> index b62773ce5219b..7ffe55ba28663 100644
> --- a/tools/testing/selftests/bpf/progs/dynptr_fail.c
> +++ b/tools/testing/selftests/bpf/progs/dynptr_fail.c
[ ... ]
> +/* A read-only cast alias from an overwritten dynptr should be invalidated
> + * even when a clone still holds the underlying reference.
> + */
> +SEC("?raw_tp")
> +__failure __msg("invalid mem access 'scalar'")
> +int dynptr_rdonly_cast_slice_after_overwrite(void *ctx)
[Severity: Low]
This isn't a bug, but does this multi-line comment follow the preferred
BPF subsystem style? The opening /* is typically expected to be on its
own line.
[ ... ]
> +/* A read-only cast alias from a clone should remain valid after overwriting
> + * the original dynptr while the clone still holds the reference.
> + */
> +SEC("?raw_tp")
> +__success
> +int dynptr_rdonly_cast_clone_slice_after_parent_overwrite(void *ctx)
[Severity: Low]
This isn't a bug, but similar to the comment above, should the opening /*
be placed on its own line to match the expected multi-line comment style
for the BPF subsystem?
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260615-f01-09-rdonly-cast-dynptr-lifetime-v1-0-2dd0a369e153@mails.tsinghua.edu.cn?part=2
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH bpf 0/2] bpf: Preserve rdonly_cast dynptr slice lifetime
2026-06-15 8:57 [PATCH bpf 0/2] bpf: Preserve rdonly_cast dynptr slice lifetime Nuoqi Gui
2026-06-15 8:57 ` [PATCH bpf 1/2] bpf: Preserve dynptr slice lifetime across rdonly_cast Nuoqi Gui
2026-06-15 8:57 ` [PATCH bpf 2/2] selftests/bpf: Cover rdonly_cast dynptr slice lifetime Nuoqi Gui
@ 2026-06-15 9:40 ` Kumar Kartikeya Dwivedi
2026-06-15 17:24 ` Eduard Zingerman
2 siblings, 1 reply; 7+ messages in thread
From: Kumar Kartikeya Dwivedi @ 2026-06-15 9:40 UTC (permalink / raw)
To: Nuoqi Gui, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Martin KaFai Lau, John Fastabend, Shuah Khan, bpf,
linux-kselftest, linux-kernel
On Mon Jun 15, 2026 at 10:57 AM CEST, Nuoqi Gui wrote:
> bpf_rdonly_cast() is an identity operation at runtime, but the verifier
> currently assigns the result fresh return-state metadata. When the source
> pointer comes from bpf_dynptr_slice(), that drops the relationship needed
> to invalidate the alias when the dynptr is released.
>
> Preserve the dynptr-slice lifetime metadata across bpf_rdonly_cast(), and
> add verifier selftests covering cases where a dynptr slice is cast and then
> read after the dynptr is released or after the originating dynptr stack slot is
> overwritten while a clone still holds the reference. Also cover the valid clone
> case where the cast alias is derived from the clone and remains usable after
> the original dynptr is overwritten.
>
> This fixes regressions introduced by 66e3a13e7c2c ("bpf: Add
> bpf_dynptr_slice and bpf_dynptr_slice_rdwr") and a35b9af4ec2c ("bpf: Add a
> kfunc for generic type cast").
>
> Impact is bounded to verifier lifetime tracking for CAP-required BPF
> programs. The evidence confirms stale read acceptance after dynptr release,
> including a typed field-read shape, but does not demonstrate unprivileged
> reachability, cross-context disclosure, controlled reuse, arbitrary kernel
> read, write-capable aliasing, privilege escalation, crash, or KASAN/UAF
> signal. No CVE, embargo, or security escalation is claimed.
>
> Validation:
> - The new dynptr_fail.c subtests exercise the verifier's dynptr-slice
> lifetime invariant across bpf_rdonly_cast(): a cast alias of a
> bpf_dynptr_slice() result must be invalidated when the dynptr is released or
> its stack slot overwritten, while a cast alias derived from a surviving clone
> must stay usable.
Won't fix. The semantics of bpf_rdonly_cast() do not require soruce provenance
tracking, and thus don't need any invalidation. It can take any unknown value
and produces safe to read pointer. Please try to reason about whether it's a
real issue before posting AI's output directly.
pw-bot: cr
> - Unpatched bpf at 76f62d237538: test_progs -t dynptr FAILs as expected,
> because the four __failure subtests
> (dynptr_rdonly_cast_{,typed_}slice_after_{release,overwrite}) are wrongly
> accepted - the stale read survives dynptr release.
> - Patched with this series: test_progs -t dynptr PASSes, because those four
> casts are now rejected with "invalid mem access 'scalar'" and the two
> __success clone subtests remain accepted (all six dynptr_rdonly_cast_*
> subtests OK, rc 0).
>
> Signed-off-by: Nuoqi Gui <gnq25@mails.tsinghua.edu.cn>
> ---
> Nuoqi Gui (2):
> bpf: Preserve dynptr slice lifetime across rdonly_cast
> selftests/bpf: Cover rdonly_cast dynptr slice lifetime
>
> include/linux/bpf_verifier.h | 5 +
> kernel/bpf/verifier.c | 36 ++++--
> tools/testing/selftests/bpf/progs/dynptr_fail.c | 160 ++++++++++++++++++++++++
> 3 files changed, 193 insertions(+), 8 deletions(-)
> ---
> base-commit: 76f62d237538b456354a44e796a541cde03c6e28
> change-id: 20260615-f01-09-rdonly-cast-dynptr-lifetime-de114e5a1838
>
> Best regards,
> --
> Nuoqi Gui <gnq25@mails.tsinghua.edu.cn>
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH bpf 1/2] bpf: Preserve dynptr slice lifetime across rdonly_cast
2026-06-15 8:57 ` [PATCH bpf 1/2] bpf: Preserve dynptr slice lifetime across rdonly_cast Nuoqi Gui
@ 2026-06-15 11:24 ` sashiko-bot
0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-06-15 11:24 UTC (permalink / raw)
To: Nuoqi Gui; +Cc: bpf
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The verifier tracking logic intended to invalidate `bpf_rdonly_cast` aliases during dynptr stack overwrites can be bypassed by passing an unchecked `PTR_MAYBE_NULL` slice or by chaining casts.
--
commit 4dccf7918016a6c3b330526f04f7eae4bbeed8d8
Author: Nuoqi Gui <gnq25@mails.tsinghua.edu.cn>
Subject: bpf: Preserve dynptr slice lifetime across rdonly_cast
This patch updates the verifier to track lifetime metadata across
bpf_rdonly_cast(). It attaches the ref_obj_id and dynptr_id to casted
registers and ensures typed aliases are invalidated when a dynptr stack
slot is overwritten.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -12082,8 +12088,17 @@ static int check_kfunc_args(struct bpf_verifier_env *env, struct bpf_kfunc_call_
> continue;
> }
>
> - if (is_kfunc_arg_ignore(btf, &args[i]) || is_kfunc_arg_implicit(meta, i))
> + if (is_kfunc_arg_ignore(btf, &args[i]) || is_kfunc_arg_implicit(meta, i)) {
> + if (meta->func_id == special_kfunc_list[KF_bpf_rdonly_cast] &&
> + i == 0 && is_spillable_regtype(reg->type)) {
> + meta->rdonly_cast_src.id = reg->id;
> + if (base_type(reg->type) == PTR_TO_MEM)
> + meta->rdonly_cast_src.dynptr_id = reg->dynptr_id;
[Severity: Medium]
Could this logic be bypassed by chaining casts?
If a slice is first cast to a struct, the result is PTR_TO_BTF_ID and it
stores the dynptr_id in its id field.
If that result is then cast to void * (returning PTR_TO_MEM),
base_type(reg->type) == PTR_TO_MEM will be false, so
meta->rdonly_cast_src.dynptr_id remains 0.
The returned PTR_TO_MEM will get dynptr_id = 0, which would bypass the
invalidation checks in destroy_if_dynptr_stack_slot() entirely.
> + meta->rdonly_cast_src.ref_obj_id = reg->ref_obj_id;
> + }
> +
> continue;
> + }
>
> t = btf_type_skip_modifiers(btf, args[i].type, NULL);
[ ... ]
> @@ -12932,10 +12947,17 @@ static int check_special_kfunc(struct bpf_verifier_env *env, struct bpf_kfunc_ca
> regs[BPF_REG_0].type = PTR_TO_BTF_ID | PTR_UNTRUSTED;
> regs[BPF_REG_0].btf = desc_btf;
> regs[BPF_REG_0].btf_id = meta->arg_constant.value;
> + regs[BPF_REG_0].id = meta->rdonly_cast_src.id;
> + if (!regs[BPF_REG_0].id)
> + regs[BPF_REG_0].id = meta->rdonly_cast_src.dynptr_id;
[Severity: Medium]
Does this correctly handle PTR_MAYBE_NULL slices passed to bpf_rdonly_cast?
Because bpf_rdonly_cast uses an __ign argument, it can accept an unchecked
PTR_MAYBE_NULL slice. These slices have a non-zero null-tracking id.
If meta->rdonly_cast_src.id is non-zero, this logic prioritizes the
null-tracking ID over dynptr_id. The returned typed alias will inherit the
null-tracking ID instead of the dynptr_id.
Later, when the parent dynptr is overwritten, the invalidation in
destroy_if_dynptr_stack_slot() will fail because it checks if the
dreg->id == dynptr_id.
> + regs[BPF_REG_0].ref_obj_id = meta->rdonly_cast_src.ref_obj_id;
> } else if (btf_type_is_void(ret_t)) {
> mark_reg_known_zero(env, regs, BPF_REG_0);
> regs[BPF_REG_0].type = PTR_TO_MEM | MEM_RDONLY | PTR_UNTRUSTED;
--
Sashiko AI review · https://sashiko.dev/#/patchset/20260615-f01-09-rdonly-cast-dynptr-lifetime-v1-0-2dd0a369e153@mails.tsinghua.edu.cn?part=1
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH bpf 0/2] bpf: Preserve rdonly_cast dynptr slice lifetime
2026-06-15 9:40 ` [PATCH bpf 0/2] bpf: Preserve " Kumar Kartikeya Dwivedi
@ 2026-06-15 17:24 ` Eduard Zingerman
0 siblings, 0 replies; 7+ messages in thread
From: Eduard Zingerman @ 2026-06-15 17:24 UTC (permalink / raw)
To: Kumar Kartikeya Dwivedi, Nuoqi Gui, Alexei Starovoitov,
Daniel Borkmann, Andrii Nakryiko
Cc: Martin KaFai Lau, John Fastabend, Shuah Khan, bpf,
linux-kselftest, linux-kernel
On Mon, 2026-06-15 at 11:40 +0200, Kumar Kartikeya Dwivedi wrote:
> On Mon Jun 15, 2026 at 10:57 AM CEST, Nuoqi Gui wrote:
> > bpf_rdonly_cast() is an identity operation at runtime, but the verifier
> > currently assigns the result fresh return-state metadata. When the source
> > pointer comes from bpf_dynptr_slice(), that drops the relationship needed
> > to invalidate the alias when the dynptr is released.
> >
> > Preserve the dynptr-slice lifetime metadata across bpf_rdonly_cast(), and
> > add verifier selftests covering cases where a dynptr slice is cast and then
> > read after the dynptr is released or after the originating dynptr stack slot is
> > overwritten while a clone still holds the reference. Also cover the valid clone
> > case where the cast alias is derived from the clone and remains usable after
> > the original dynptr is overwritten.
> >
> > This fixes regressions introduced by 66e3a13e7c2c ("bpf: Add
> > bpf_dynptr_slice and bpf_dynptr_slice_rdwr") and a35b9af4ec2c ("bpf: Add a
> > kfunc for generic type cast").
> >
> > Impact is bounded to verifier lifetime tracking for CAP-required BPF
> > programs. The evidence confirms stale read acceptance after dynptr release,
> > including a typed field-read shape, but does not demonstrate unprivileged
> > reachability, cross-context disclosure, controlled reuse, arbitrary kernel
> > read, write-capable aliasing, privilege escalation, crash, or KASAN/UAF
> > signal. No CVE, embargo, or security escalation is claimed.
> >
> > Validation:
> > - The new dynptr_fail.c subtests exercise the verifier's dynptr-slice
> > lifetime invariant across bpf_rdonly_cast(): a cast alias of a
> > bpf_dynptr_slice() result must be invalidated when the dynptr is released or
> > its stack slot overwritten, while a cast alias derived from a surviving clone
> > must stay usable.
>
> Won't fix. The semantics of bpf_rdonly_cast() do not require soruce provenance
> tracking, and thus don't need any invalidation. It can take any unknown value
> and produces safe to read pointer. Please try to reason about whether it's a
> real issue before posting AI's output directly.
>
> pw-bot: cr
+1, the memory access through untrusted pointers is handled via
prob-read pseudo-instructions. Nothing to fix here.
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-06-15 17:24 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-06-15 8:57 [PATCH bpf 0/2] bpf: Preserve rdonly_cast dynptr slice lifetime Nuoqi Gui
2026-06-15 8:57 ` [PATCH bpf 1/2] bpf: Preserve dynptr slice lifetime across rdonly_cast Nuoqi Gui
2026-06-15 11:24 ` sashiko-bot
2026-06-15 8:57 ` [PATCH bpf 2/2] selftests/bpf: Cover rdonly_cast dynptr slice lifetime Nuoqi Gui
2026-06-15 9:08 ` sashiko-bot
2026-06-15 9:40 ` [PATCH bpf 0/2] bpf: Preserve " Kumar Kartikeya Dwivedi
2026-06-15 17:24 ` Eduard Zingerman
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox