* [PATCH bpf v2] bpf: Keep generic __uninit kfunc arguments live
@ 2026-09-15 0:27 Tejun Heo
2026-09-15 1:15 ` bot+bpf-ci
0 siblings, 1 reply; 2+ messages in thread
From: Tejun Heo @ 2026-09-15 0:27 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.
v2: Rebased on bpf/master.
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
@@ -13588,7 +13588,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
@@ -55,6 +55,7 @@
#include "verifier_jeq_infer_not_null.skel.h"
#include "verifier_jit_convergence.skel.h"
#include "verifier_kfunc_perfmon.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"
@@ -220,6 +221,7 @@ void test_verifier_iterating_callbacks(v
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_perfmon(void) { RUN(verifier_kfunc_perfmon); }
+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
@@ -1102,6 +1102,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)
{
}
@@ -1508,6 +1516,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
@@ -161,6 +161,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] 2+ messages in thread
* Re: [PATCH bpf v2] bpf: Keep generic __uninit kfunc arguments live
2026-09-15 0:27 [PATCH bpf v2] bpf: Keep generic __uninit kfunc arguments live Tejun Heo
@ 2026-09-15 1:15 ` bot+bpf-ci
0 siblings, 0 replies; 2+ messages in thread
From: bot+bpf-ci @ 2026-09-15 1:15 UTC (permalink / raw)
To: tj, ast, daniel, andrii, eddyz87, memxor
Cc: martin.lau, song, yonghong.song, jolsa, emil, ihor.solodrai,
ameryhung, bpf, sched-ext, linux-kernel, ast, andrii, daniel,
martin.lau, eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 3657 bytes --]
> bpf: Keep generic __uninit kfunc arguments live
>
> 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.
>
> v2: Rebased on bpf/master.
This isn't a bug, but could the 'v2: Rebased on bpf/master.' note move
below the --- line so it stays out of the git log?
> Fixes: 2cb27158adb3 ("bpf: poison dead stack slots")
> Cc: stable@vger.kernel.org # v7.1+
This isn't a bug, but since this targets bpf, would it be worth dropping
the Cc: stable trailer and instead noting the v7.1+ backport request
under the --- line, as bpf_devel_QA.rst asks?
> Signed-off-by: Tejun Heo <tj@kernel.org>
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index ddba53eaa3331..310a3c6339a98 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -13588,7 +13588,8 @@ s64 bpf_kfunc_stack_access_bytes(struct bpf_verifier_env *env, struct bpf_insn *
> /* 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;
> }
Amery Hung raised a design concern on v1 about whether restricting the
liveness exception to dynptrs is the right fix.
The reviewer suggests that the underlying issue is that generic argument
validation doesn't honor __uninit, and that generic __uninit arguments
should behave like helper output buffers: generate MEM_UNINIT | MEM_WRITE,
validate as write-only, and mark the written range initialized after the
kfunc call. With that approach, bpf_kfunc_stack_access_bytes() could
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.
Was this comment considered for v2?
---
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/34913753963
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-09-15 1:15 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-15 0:27 [PATCH bpf v2] bpf: Keep generic __uninit kfunc arguments live Tejun Heo
2026-09-15 1:15 ` bot+bpf-ci
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®