* [PATCH bpf] bpf: Keep generic __uninit kfunc arguments live
@ 2026-09-14 23:43 Tejun Heo
2026-09-15 0:39 ` Amery Hung
0 siblings, 1 reply; 3+ messages in thread
From: Tejun Heo @ 2026-09-14 23:43 UTC (permalink / raw)
To: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi
Cc: Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
Emil Tsalapatis, Ihor Solodrai, Amery Hung, bpf, sched-ext,
linux-kernel
Stack liveness models a kfunc's __uninit pointer argument as a pure write:
whatever the buffer holds before the call is dead, and 2cb27158adb3 ("bpf:
poison dead stack slots") poisons those slots at every checkpoint the path
visits before the call. Argument validation disagrees. Only dynptr arguments
honor __uninit. A generic fixed-size buffer goes through check_mem_reg(),
whose read pass requires every byte readable. The two rules meet at the call
and the poisoned buffer is rejected with "slot poisoned by dead code
elimination", even when the program initialized it.
Every kfunc with a generic __uninit output argument is affected,
bpf_ksock_create() through err__uninit and sched_ext's scx_bpf_cid_topo()
through its output struct. The rejection needs the buffer's slots to be
allocated and dead at a checkpoint, for example on the second iteration of a
loop around the call. Before e566701b9b0c ("bpf: Check fixed-size mem args
of helpers and kfuncs the same way") kfunc arguments could read poisoned
slots, so the call passed, but the slots stayed poisoned and the program's
next read of the buffer was rejected instead.
Restrict the liveness write-only exception to dynptr arguments, where
validation accepts uninitialized output. A generic __uninit buffer stays
live and is never poisoned, and validation keeps requiring it to be
initialized. Add a selftest with a scalar output buffer and a checkpoint
between its initialization and the call.
Fixes: 2cb27158adb3 ("bpf: poison dead stack slots")
Cc: stable@vger.kernel.org # v7.1+
Signed-off-by: Tejun Heo <tj@kernel.org>
---
kernel/bpf/verifier.c | 3
tools/testing/selftests/bpf/prog_tests/verifier.c | 2
tools/testing/selftests/bpf/progs/verifier_kfunc_uninit.c | 45 +++++++++++++
tools/testing/selftests/bpf/test_kmods/bpf_testmod.c | 9 ++
tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h | 1
5 files changed, 59 insertions(+), 1 deletion(-)
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -12755,7 +12755,8 @@ out:
/* KF_ITER_NEW kfuncs initialize the iterator state at arg 0 */
if (arg == 0 && meta.kfunc_flags & KF_ITER_NEW)
return -size;
- if (is_kfunc_arg_uninit(btf, &args[arg]))
+ /* only dynptr validation accepts an uninitialized __uninit argument */
+ if (is_kfunc_arg_dynptr(btf, &args[arg]) && is_kfunc_arg_uninit(btf, &args[arg]))
return -size;
return size;
}
--- a/tools/testing/selftests/bpf/prog_tests/verifier.c
+++ b/tools/testing/selftests/bpf/prog_tests/verifier.c
@@ -53,6 +53,7 @@
#include "verifier_iterating_callbacks.skel.h"
#include "verifier_jeq_infer_not_null.skel.h"
#include "verifier_jit_convergence.skel.h"
+#include "verifier_kfunc_uninit.skel.h"
#include "verifier_ld_ind.skel.h"
#include "verifier_ldsx.skel.h"
#include "verifier_leak_ptr.skel.h"
@@ -214,6 +215,7 @@ void test_verifier_int_ptr(void)
void test_verifier_iterating_callbacks(void) { RUN(verifier_iterating_callbacks); }
void test_verifier_jeq_infer_not_null(void) { RUN(verifier_jeq_infer_not_null); }
void test_verifier_jit_convergence(void) { RUN(verifier_jit_convergence); }
+void test_verifier_kfunc_uninit(void) { RUN_TESTS(verifier_kfunc_uninit); }
void test_verifier_load_acquire(void) { RUN(verifier_load_acquire); }
void test_verifier_ld_ind(void) { RUN(verifier_ld_ind); }
void test_verifier_ldsx(void) { RUN(verifier_ldsx); }
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/verifier_kfunc_uninit.c
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2026 Tejun Heo <tj@kernel.org> */
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include "bpf_misc.h"
+#include "../test_kmods/bpf_testmod_kfunc.h"
+
+char _license[] SEC("license") = "GPL";
+
+/* keep the BTF FUNC record for the inline assembly reference */
+void __kfunc_btf_root(void)
+{
+ bpf_kfunc_call_test_uninit(0);
+}
+
+SEC("tc")
+__success __retval(42)
+__flag(BPF_F_TEST_STATE_FREQ)
+__naked void uninit_scalar_struct(void)
+{
+ /* dead before the call, so poisoned at the checkpoint */
+ asm volatile (
+ "*(u64 *)(r10 - 16) = 0;"
+ "*(u64 *)(r10 - 8) = 0;"
+ "goto +0;"
+ "r1 = r10;"
+ "r1 += -16;"
+ "call %[bpf_kfunc_call_test_uninit];"
+ "r1 = *(u32 *)(r10 - 16);"
+ "if r1 != 1 goto 1f;"
+ "r1 = *(u32 *)(r10 - 12);"
+ "if r1 != 2 goto 1f;"
+ "r1 = *(u32 *)(r10 - 8);"
+ "if r1 != 3 goto 1f;"
+ "r1 = *(u32 *)(r10 - 4);"
+ "if r1 != 4 goto 1f;"
+ "r0 = 42;"
+ "exit;"
+"1:"
+ "r0 = 0;"
+ "exit;"
+ :: __imm(bpf_kfunc_call_test_uninit)
+ : __clobber_all);
+}
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
@@ -1087,6 +1087,14 @@ __bpf_kfunc void bpf_kfunc_call_test_pas
{
}
+__bpf_kfunc void bpf_kfunc_call_test_uninit(struct prog_test_pass1 *out__uninit)
+{
+ out__uninit->x0 = 1;
+ out__uninit->x1 = 2;
+ out__uninit->x2 = 3;
+ out__uninit->x3 = 4;
+}
+
__bpf_kfunc void bpf_kfunc_call_test_fail1(struct prog_test_fail1 *p)
{
}
@@ -1493,6 +1501,7 @@ BTF_ID_FLAGS(func, bpf_kfunc_call_int_me
BTF_ID_FLAGS(func, bpf_kfunc_call_test_pass_ctx)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_pass1)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_pass2)
+BTF_ID_FLAGS(func, bpf_kfunc_call_test_uninit)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_fail1)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_fail2)
BTF_ID_FLAGS(func, bpf_kfunc_call_test_fail3)
--- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
+++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
@@ -160,6 +160,7 @@ __u64 bpf_kfunc_call_stack_arg_big(__u64
void bpf_kfunc_call_test_pass_ctx(struct __sk_buff *skb) __ksym;
void bpf_kfunc_call_test_pass1(struct prog_test_pass1 *p) __ksym;
void bpf_kfunc_call_test_pass2(struct prog_test_pass2 *p) __ksym;
+void bpf_kfunc_call_test_uninit(struct prog_test_pass1 *out__uninit) __ksym;
void bpf_kfunc_call_test_mem_len_fail2(__u64 *mem, int len) __ksym;
void bpf_kfunc_call_test_destructive(void) __ksym;
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH bpf] bpf: Keep generic __uninit kfunc arguments live
2026-09-14 23:43 [PATCH bpf] bpf: Keep generic __uninit kfunc arguments live Tejun Heo
@ 2026-09-15 0:39 ` Amery Hung
2026-09-15 7:18 ` Tejun Heo
0 siblings, 1 reply; 3+ messages in thread
From: Amery Hung @ 2026-09-15 0:39 UTC (permalink / raw)
To: Tejun Heo
Cc: Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau,
Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis,
Ihor Solodrai, bpf, sched-ext, linux-kernel
On Mon, Sep 14, 2026 at 4:43 PM Tejun Heo <tj@kernel.org> wrote:
>
> Stack liveness models a kfunc's __uninit pointer argument as a pure write:
> whatever the buffer holds before the call is dead, and 2cb27158adb3 ("bpf:
> poison dead stack slots") poisons those slots at every checkpoint the path
> visits before the call. Argument validation disagrees. Only dynptr arguments
> honor __uninit. A generic fixed-size buffer goes through check_mem_reg(),
> whose read pass requires every byte readable. The two rules meet at the call
> and the poisoned buffer is rejected with "slot poisoned by dead code
> elimination", even when the program initialized it.
>
> Every kfunc with a generic __uninit output argument is affected,
> bpf_ksock_create() through err__uninit and sched_ext's scx_bpf_cid_topo()
> through its output struct. The rejection needs the buffer's slots to be
> allocated and dead at a checkpoint, for example on the second iteration of a
> loop around the call. Before e566701b9b0c ("bpf: Check fixed-size mem args
> of helpers and kfuncs the same way") kfunc arguments could read poisoned
> slots, so the call passed, but the slots stayed poisoned and the program's
> next read of the buffer was rejected instead.
>
> Restrict the liveness write-only exception to dynptr arguments, where
> validation accepts uninitialized output. A generic __uninit buffer stays
> live and is never poisoned, and validation keeps requiring it to be
> initialized. Add a selftest with a scalar output buffer and a checkpoint
> between its initialization and the call.
I don’t think restricting the liveness exception to dynptrs is the
right fix. The fact that generic argument validation does not honor
__uninit appears to be the underlying bug.
A generic __uninit argument should behave like a helper output buffer:
generate MEM_UNINIT | MEM_WRITE, validate it as write-only, and mark
the written range initialized after the kfunc call.
With that handling, bpf_kfunc_stack_access_bytes() should continue
returning -size for generic __uninit arguments. The current change
instead makes their previous contents live and requires callers to
initialize an argument explicitly declared uninitialized.
>
> Fixes: 2cb27158adb3 ("bpf: poison dead stack slots")
> Cc: stable@vger.kernel.org # v7.1+
> Signed-off-by: Tejun Heo <tj@kernel.org>
> ---
> kernel/bpf/verifier.c | 3
> tools/testing/selftests/bpf/prog_tests/verifier.c | 2
> tools/testing/selftests/bpf/progs/verifier_kfunc_uninit.c | 45 +++++++++++++
> tools/testing/selftests/bpf/test_kmods/bpf_testmod.c | 9 ++
> tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h | 1
> 5 files changed, 59 insertions(+), 1 deletion(-)
>
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -12755,7 +12755,8 @@ out:
> /* KF_ITER_NEW kfuncs initialize the iterator state at arg 0 */
> if (arg == 0 && meta.kfunc_flags & KF_ITER_NEW)
> return -size;
> - if (is_kfunc_arg_uninit(btf, &args[arg]))
> + /* only dynptr validation accepts an uninitialized __uninit argument */
> + if (is_kfunc_arg_dynptr(btf, &args[arg]) && is_kfunc_arg_uninit(btf, &args[arg]))
> return -size;
> return size;
> }
> --- a/tools/testing/selftests/bpf/prog_tests/verifier.c
> +++ b/tools/testing/selftests/bpf/prog_tests/verifier.c
> @@ -53,6 +53,7 @@
> #include "verifier_iterating_callbacks.skel.h"
> #include "verifier_jeq_infer_not_null.skel.h"
> #include "verifier_jit_convergence.skel.h"
> +#include "verifier_kfunc_uninit.skel.h"
> #include "verifier_ld_ind.skel.h"
> #include "verifier_ldsx.skel.h"
> #include "verifier_leak_ptr.skel.h"
> @@ -214,6 +215,7 @@ void test_verifier_int_ptr(void)
> void test_verifier_iterating_callbacks(void) { RUN(verifier_iterating_callbacks); }
> void test_verifier_jeq_infer_not_null(void) { RUN(verifier_jeq_infer_not_null); }
> void test_verifier_jit_convergence(void) { RUN(verifier_jit_convergence); }
> +void test_verifier_kfunc_uninit(void) { RUN_TESTS(verifier_kfunc_uninit); }
> void test_verifier_load_acquire(void) { RUN(verifier_load_acquire); }
> void test_verifier_ld_ind(void) { RUN(verifier_ld_ind); }
> void test_verifier_ldsx(void) { RUN(verifier_ldsx); }
> --- /dev/null
> +++ b/tools/testing/selftests/bpf/progs/verifier_kfunc_uninit.c
> @@ -0,0 +1,45 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/* Copyright (c) 2026 Tejun Heo <tj@kernel.org> */
> +
> +#include <vmlinux.h>
> +#include <bpf/bpf_helpers.h>
> +#include "bpf_misc.h"
> +#include "../test_kmods/bpf_testmod_kfunc.h"
> +
> +char _license[] SEC("license") = "GPL";
> +
> +/* keep the BTF FUNC record for the inline assembly reference */
> +void __kfunc_btf_root(void)
> +{
> + bpf_kfunc_call_test_uninit(0);
> +}
> +
> +SEC("tc")
> +__success __retval(42)
> +__flag(BPF_F_TEST_STATE_FREQ)
> +__naked void uninit_scalar_struct(void)
> +{
> + /* dead before the call, so poisoned at the checkpoint */
> + asm volatile (
> + "*(u64 *)(r10 - 16) = 0;"
> + "*(u64 *)(r10 - 8) = 0;"
> + "goto +0;"
> + "r1 = r10;"
> + "r1 += -16;"
> + "call %[bpf_kfunc_call_test_uninit];"
> + "r1 = *(u32 *)(r10 - 16);"
> + "if r1 != 1 goto 1f;"
> + "r1 = *(u32 *)(r10 - 12);"
> + "if r1 != 2 goto 1f;"
> + "r1 = *(u32 *)(r10 - 8);"
> + "if r1 != 3 goto 1f;"
> + "r1 = *(u32 *)(r10 - 4);"
> + "if r1 != 4 goto 1f;"
> + "r0 = 42;"
> + "exit;"
> +"1:"
> + "r0 = 0;"
> + "exit;"
> + :: __imm(bpf_kfunc_call_test_uninit)
> + : __clobber_all);
> +}
> --- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
> +++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod.c
> @@ -1087,6 +1087,14 @@ __bpf_kfunc void bpf_kfunc_call_test_pas
> {
> }
>
> +__bpf_kfunc void bpf_kfunc_call_test_uninit(struct prog_test_pass1 *out__uninit)
> +{
> + out__uninit->x0 = 1;
> + out__uninit->x1 = 2;
> + out__uninit->x2 = 3;
> + out__uninit->x3 = 4;
> +}
> +
> __bpf_kfunc void bpf_kfunc_call_test_fail1(struct prog_test_fail1 *p)
> {
> }
> @@ -1493,6 +1501,7 @@ BTF_ID_FLAGS(func, bpf_kfunc_call_int_me
> BTF_ID_FLAGS(func, bpf_kfunc_call_test_pass_ctx)
> BTF_ID_FLAGS(func, bpf_kfunc_call_test_pass1)
> BTF_ID_FLAGS(func, bpf_kfunc_call_test_pass2)
> +BTF_ID_FLAGS(func, bpf_kfunc_call_test_uninit)
> BTF_ID_FLAGS(func, bpf_kfunc_call_test_fail1)
> BTF_ID_FLAGS(func, bpf_kfunc_call_test_fail2)
> BTF_ID_FLAGS(func, bpf_kfunc_call_test_fail3)
> --- a/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
> +++ b/tools/testing/selftests/bpf/test_kmods/bpf_testmod_kfunc.h
> @@ -160,6 +160,7 @@ __u64 bpf_kfunc_call_stack_arg_big(__u64
> void bpf_kfunc_call_test_pass_ctx(struct __sk_buff *skb) __ksym;
> void bpf_kfunc_call_test_pass1(struct prog_test_pass1 *p) __ksym;
> void bpf_kfunc_call_test_pass2(struct prog_test_pass2 *p) __ksym;
> +void bpf_kfunc_call_test_uninit(struct prog_test_pass1 *out__uninit) __ksym;
> void bpf_kfunc_call_test_mem_len_fail2(__u64 *mem, int len) __ksym;
>
> void bpf_kfunc_call_test_destructive(void) __ksym;
^ permalink raw reply [flat|nested] 3+ messages in thread
* Re: [PATCH bpf] bpf: Keep generic __uninit kfunc arguments live
2026-09-15 0:39 ` Amery Hung
@ 2026-09-15 7:18 ` Tejun Heo
0 siblings, 0 replies; 3+ messages in thread
From: Tejun Heo @ 2026-09-15 7:18 UTC (permalink / raw)
To: Amery Hung
Cc: Tejun Heo, Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau,
Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis,
Ihor Solodrai, bpf, sched-ext, linux-kernel
Hello, Amery.
On Mon, Sep 14, 2026 at 05:39:04PM -0700, Amery Hung wrote:
> A generic __uninit argument should behave like a helper output buffer:
> generate MEM_UNINIT | MEM_WRITE, validate it as write-only, and mark
> the written range initialized after the kfunc call.
Kumar suggested a similar solution and will drive the fix. Dropping this
patch.
Thanks.
--
tejun
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-09-15 7:18 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-14 23:43 [PATCH bpf] bpf: Keep generic __uninit kfunc arguments live Tejun Heo
2026-09-15 0:39 ` Amery Hung
2026-09-15 7:18 ` Tejun Heo
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®