Re: [PATCH bpf-next 03/10] bpf: Support stack arguments for bpf functions

Amery Hung
2026-04-02T18:56:01Z

On Wed, Apr 1, 2026 at 6:27 PM Yonghong Song <yonghong.song@linux.dev> wrote:
>
> Currently BPF functions (subprogs) are limited to 5 register arguments.
> With [1], the compiler can emit code that passes additional arguments
> via a dedicated stack area through bpf register
> BPF_REG_STACK_ARG_BASE (r12), introduced in the previous patch.
>
> The following is an example to show how stack arguments are saved
> and transferred between caller and callee:
>
> int foo(int a1, int a2, int a3, int a4, int a5, int a6, int a7) {
> ...
> bar(a1, a2, a3, a4, a5, a6, a7, a8);
> ...
> }
>
> The following is a illustration of stack allocation:
>
> Caller (foo) Callee (bar)
> ============ ============
> r12-relative stack arg area: r12-relative stack arg area:
>
> r12-8: [incoming arg 6] +--> r12-8: [incoming arg 6] (from caller's outgoing r12-24)
> r12-16: [incoming arg 7] |+-> r12-16: [incoming arg 7] (from caller's outgoing r12-32)
> ||+> r12-24: [incoming arg 8] (from caller's outgoing r12-40)
> ---- incoming/outgoing boundary ||| ---- incoming/outgoing boundary
> r12-24: [outgoing arg 6 to callee]+|| ...
> r12-32: [outgoing arg 7 to callee]-+|
> r12-40: [outgoing arg 8 to callee]--+
>
> The caller writes outgoing args past its own incoming area.
> At the call site, the verifier transfers the caller's outgoing
> slots into the callee's incoming slots.
>
> The verifier tracks stack arg slots separately from the regular r10
> stack. A new 'bpf_stack_arg_state' structure mirrors the existing stack
> slot tracking (spilled_ptr + slot_type[]) but lives in a dedicated
> 'stack_arg_slots' array in bpf_func_state. This separation keeps the
> stack arg area from interfering with the normal stack and frame pointer
> (r10) bookkeeping.
>
> If the bpf function has more than one calls, e.g.,
>
> int foo(int a1, int a2, int a3, int a4, int a5, int a6, int a7) {
> ...
> bar1(a1, a2, a3, a4, a5, a6, a7, a8);
> ...
> bar2(a1, a2, a3, a4, a5, a6, a7, a8, a9);
> ...
> }
>
> The following is an illustration:
>
> Caller (foo) Callee (bar1)
> ============ =============
> r12-relative stack arg area: r12-relative stack arg area:
>
> r12-8: [incoming arg 6] +--> r12-8: [incoming arg 6] (from caller's outgoing r12-24)
> r12-16: [incoming arg 7] |+-> r12-16: [incoming arg 7] (from caller's outgoing r12-32)
> ||+> r12-24: [incoming arg 8] (from caller's outgoing r12-40)
> ---- incoming/outgoing boundary ||| ---- incoming/outgoing boundary
> r12-24: [outgoing arg 6 to callee]+|| ...
> r12-32: [outgoing arg 7 to callee]-+|
> r12-40: [outgoing arg 8 to callee]--+
> ...
> Back from bar1
> ... Callee (bar2)
> === =============
> +---> r12-8: [incoming arg 6] (from caller's outgoing r12-24)
> |+--> r12-16: [incoming arg 7] (from caller's outgoing r12-32)
> ||+-> r12-24: [incoming arg 8] (from caller's outgoing r12-40)
> |||+> r12-32: [incoming arg 9] (from caller's outgoing r12-48)
> ---- incoming/outgoing boundary |||| ---- incoming/outgoing boundary
> r12-24: [outgoing arg 6 to callee]+||| ...
> r12-32: [outgoing arg 7 to callee]-+||
> r12-40: [outgoing arg 8 to callee]--+|
> r12-48: [outgoing arg 9 to callee]---+
>
> Global subprogs with >5 args are not yet supported.
>
> [1] https://github.com/llvm/llvm-project/pull/189060
>
> Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
> ---
> include/linux/bpf.h | 2 +
> include/linux/bpf_verifier.h | 15 ++-
> kernel/bpf/btf.c | 14 +-
> kernel/bpf/verifier.c | 248 ++++++++++++++++++++++++++++++++---
> 4 files changed, 257 insertions(+), 22 deletions(-)
>
> diff --git a/include/linux/bpf.h b/include/linux/bpf.h
> index e24c4a2e95f7..a0a1e14e4394 100644
> --- a/include/linux/bpf.h
> +++ b/include/linux/bpf.h
> @@ -1666,6 +1666,8 @@ struct bpf_prog_aux {
> u32 max_pkt_offset;
> u32 max_tp_access;
> u32 stack_depth;
> + u16 incoming_stack_arg_depth;
> + u16 stack_arg_depth; /* both incoming and max outgoing of stack arguments */
> u32 id;
> u32 func_cnt; /* used by non-func prog as the number of func progs */
> u32 real_func_cnt; /* includes hidden progs, only used for JIT and freeing progs */
> diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
> index 090aa26d1c98..a260610cd1c1 100644
> --- a/include/linux/bpf_verifier.h
> +++ b/include/linux/bpf_verifier.h
> @@ -268,6 +268,11 @@ struct bpf_retval_range {
> bool return_32bit;
> };
>
> +struct bpf_stack_arg_state {
> + struct bpf_reg_state spilled_ptr; /* for spilled scalar/pointer semantics */
> + u8 slot_type[BPF_REG_SIZE];
> +};
> +
> /* state of the program:
> * type of all registers and stack info
> */
> @@ -319,6 +324,10 @@ struct bpf_func_state {
> * `stack`. allocated_stack is always a multiple of BPF_REG_SIZE.
> */
> int allocated_stack;
> +
> + u16 stack_arg_depth; /* Size of incoming + max outgoing stack args in bytes. */
> + u16 incoming_stack_arg_depth; /* Size of incoming stack args in bytes. */
> + struct bpf_stack_arg_state *stack_arg_slots;
> };
>
> #define MAX_CALL_FRAMES 8
> @@ -674,10 +683,12 @@ struct bpf_subprog_info {
> bool keep_fastcall_stack: 1;
> bool changes_pkt_data: 1;
> bool might_sleep: 1;
> - u8 arg_cnt:3;
> + u8 arg_cnt:4;
>
> enum priv_stack_mode priv_stack_mode;
> - struct bpf_subprog_arg_info args[MAX_BPF_FUNC_REG_ARGS];
> + struct bpf_subprog_arg_info args[MAX_BPF_FUNC_ARGS];
> + u16 incoming_stack_arg_depth;
> + u16 outgoing_stack_arg_depth;
> };
>
> struct bpf_verifier_env;
> diff --git a/kernel/bpf/btf.c b/kernel/bpf/btf.c
> index a62d78581207..c5f3aa05d5a3 100644
> --- a/kernel/bpf/btf.c
> +++ b/kernel/bpf/btf.c
> @@ -7887,13 +7887,19 @@ int btf_prepare_func_args(struct bpf_verifier_env *env, int subprog)
> }
> args = (const struct btf_param *)(t + 1);
> nargs = btf_type_vlen(t);
> - if (nargs > MAX_BPF_FUNC_REG_ARGS) {
> - if (!is_global)
> - return -EINVAL;
> - bpf_log(log, "Global function %s() with %d > %d args. Buggy compiler.\n",
> + if (nargs > MAX_BPF_FUNC_ARGS) {
> + bpf_log(log, "Function %s() with %d > %d args not supported.\n",
> + tname, nargs, MAX_BPF_FUNC_ARGS);
> + return -EINVAL;
> + }
> + if (is_global && nargs > MAX_BPF_FUNC_REG_ARGS) {
> + bpf_log(log, "Global function %s() with %d > %d args not supported.\n",
> tname, nargs, MAX_BPF_FUNC_REG_ARGS);
> return -EINVAL;
> }
> + if (nargs > MAX_BPF_FUNC_REG_ARGS)
> + sub->incoming_stack_arg_depth = (nargs - MAX_BPF_FUNC_REG_ARGS) * BPF_REG_SIZE;
> +
> /* check that function is void or returns int, exception cb also requires this */
> t = btf_type_by_id(btf, t->type);
> while (btf_type_is_modifier(t))
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 8c1cf2eb6cbb..d424fe611ef8 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -1488,6 +1488,19 @@ static int copy_stack_state(struct bpf_func_state *dst, const struct bpf_func_st
> return -ENOMEM;
>
> dst->allocated_stack = src->allocated_stack;
> +
> + /* copy stack_arg_slots state */
> + n = src->stack_arg_depth / BPF_REG_SIZE;
> + if (n) {
> + dst->stack_arg_slots = copy_array(dst->stack_arg_slots, src->stack_arg_slots, n,
> + sizeof(struct bpf_stack_arg_state),
> + GFP_KERNEL_ACCOUNT);
> + if (!dst->stack_arg_slots)
> + return -ENOMEM;
> +
> + dst->stack_arg_depth = src->stack_arg_depth;
> + dst->incoming_stack_arg_depth = src->incoming_stack_arg_depth;
> + }
> return 0;
> }
>
> @@ -1529,6 +1542,25 @@ static int grow_stack_state(struct bpf_verifier_env *env, struct bpf_func_state
> return 0;
> }
>
> +static int grow_stack_arg_slots(struct bpf_verifier_env *env,
> + struct bpf_func_state *state, int size)
> +{
> + size_t old_n = state->stack_arg_depth / BPF_REG_SIZE, n;
> +
> + size = round_up(size, BPF_REG_SIZE);
> + n = size / BPF_REG_SIZE;
> + if (old_n >= n)
> + return 0;
> +
> + state->stack_arg_slots = realloc_array(state->stack_arg_slots, old_n, n,
> + sizeof(struct bpf_stack_arg_state));
> + if (!state->stack_arg_slots)
> + return -ENOMEM;
> +
> + state->stack_arg_depth = size;
> + return 0;
> +}
> +
> /* Acquire a pointer id from the env and update the state->refs to include
> * this new pointer reference.
> * On success, returns a valid pointer id to associate with the register
> @@ -1699,6 +1731,7 @@ static void free_func_state(struct bpf_func_state *state)
> {
> if (!state)
> return;
> + kfree(state->stack_arg_slots);
> kfree(state->stack);
> kfree(state);
> }
> @@ -5848,6 +5881,101 @@ static int check_stack_write(struct bpf_verifier_env *env,
> return err;
> }
>
> +/* Validate that a stack arg access is 8-byte sized and aligned. */
> +static int check_stack_arg_access(struct bpf_verifier_env *env,
> + struct bpf_insn *insn, const char *op)
> +{
> + int size = bpf_size_to_bytes(BPF_SIZE(insn->code));
> +
> + if (size != BPF_REG_SIZE) {
> + verbose(env, "stack arg %s must be %d bytes, got %d\n",
> + op, BPF_REG_SIZE, size);
> + return -EINVAL;
> + }
> + if (insn->off % BPF_REG_SIZE) {
> + verbose(env, "stack arg %s offset %d not aligned to %d\n",
> + op, insn->off, BPF_REG_SIZE);
> + return -EINVAL;
> + }
> + return 0;
> +}
> +
> +/* Check that a stack arg slot has been properly initialized. */
> +static bool is_stack_arg_slot_initialized(struct bpf_func_state *state, int spi)
> +{
> + u8 type;
> +
> + if (spi >= (int)(state->stack_arg_depth / BPF_REG_SIZE))
> + return false;
> + type = state->stack_arg_slots[spi].slot_type[BPF_REG_SIZE - 1];
> + return type == STACK_SPILL || type == STACK_MISC;
> +}
> +
> +/*
> + * Write a value to the stack arg area.
> + * off is the negative offset from the stack arg frame pointer.
> + * Callers ensures off is 8-byte aligned and size is BPF_REG_SIZE.
> + */
> +static int check_stack_arg_write(struct bpf_verifier_env *env, struct bpf_func_state *state,
> + int off, int value_regno)
> +{
> + int spi = (-off - 1) / BPF_REG_SIZE;
> + struct bpf_func_state *cur;
> + struct bpf_reg_state *reg;
> + int i, err;
> + u8 type;
> +
> + err = grow_stack_arg_slots(env, state, -off);
> + if (err)
> + return err;
> +
> + cur = env->cur_state->frame[env->cur_state->curframe];
> + if (value_regno >= 0) {
> + reg = &cur->regs[value_regno];
> + state->stack_arg_slots[spi].spilled_ptr = *reg;
> + type = is_spillable_regtype(reg->type) ? STACK_SPILL : STACK_MISC;

It seems any spillable register can be passed to the callee, so reg
containing ref_obj_id can be spilled to stack_arg_slots. However,
release_reference() does not invalidate ref_obj_id in stack_arg_slots.
Can this cause UAF like below?

a6 = bpf_task_acquire(t);
if (!a6)
goto err;

// a6 now has a valid ref_obj_id
// foo1 calls bpf_task_release(a6);
foo1(a1, a2, a3, a4, a5, a6);

// a6 still has a valid ref_obj_id
// foo2 dereference a6 -> UAF
foo2(a1, a2, a3, a4, a5, a6);

Since stack_arg_slots is separated from the normal stack slots, other
types of stale registers may exist in the outgoing stack slots. For
example:
- stale pkt pointer after calling clear_all_pkt_pointers() in callee
- register with inprecise nullness after calling
mark_ptr_or_null_regs() in callee

> + for (i = 0; i < BPF_REG_SIZE; i++)
> + state->stack_arg_slots[spi].slot_type[i] = type;
> + } else {
> + /* BPF_ST: store immediate, treat as scalar */