Re: [PATCH bpf] bpf: Keep generic __uninit kfunc arguments live
From: Amery Hung
Date: Mon Sep 14 2026 - 20:42:08 EST
On Mon, Sep 14, 2026 at 4:43 PM Tejun Heo <tj@xxxxxxxxxx> 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@xxxxxxxxxxxxxxx # v7.1+
> Signed-off-by: Tejun Heo <tj@xxxxxxxxxx>
> ---
> 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@xxxxxxxxxx> */
> +
> +#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;