Re: [PATCH bpf-next v4 07/13] bpf: Add verifier support for 16-byte returns in R0:R2

Yonghong Song <[email protected]>
Newsgroups org.kernel.vger.bpf
Message-ID <[email protected]>

On 8/10/26 6:24 PM, [email protected] wrote:
>> bpf: Add verifier support for 16-byte returns in R0:R2
>>
>> LLVM 23 added support for returning a value in two registers for an
>> __int128, or a struct/union whose size is greater than 8 but not more than
>> 16 bytes. See LLVM patches [1] and [2].
>>
>> Before LLVM 23 the BPF backend could not return these values at all. A
>> by-value struct or union return (of any size) was rejected at compile time
>> with:
>>
>>    error: aggregate returns are not supported
>>
>> and an __int128 return failed later in the backend with:
>>
>>    fatal error: error in backend: unable to allocate function return #1
>>
>> Both are resolved in LLVM 23, which lowers such returns into the R0:R2
>> register pair.
>>
>> This patch adds handling for returns greater than 8 bytes in several
>> places: BPF subprogram returns (the main program, and both global and
>> static subprograms) and kfunc returns.
>>
>> The R0:R2 convention is only implemented in the JIT. The BPF interpreter
>> has no notion of a second return register: a BPF-to-BPF call goes through
>> JMP_CALL_ARGS and a BPF_EXIT hands back BPF_R0 alone, so a caller reading
>> R2 would see a stale value. Force the JIT wherever a caller can observe the
>> pair, that is at the call to a global subprogram in check_func_call() and
>> at the return from a static subprogram in prepare_func_exit(). Kfunc calls
>> need no separate handling since bpf_add_kfunc_call() already sets
>> jit_required for every kfunc call.
>>
>> A by-value struct or union returned by a kfunc must be composed only of
>> scalars, since the verifier models the returned register bits as an unknown
>> scalar and a pointer field would otherwise be laundered into one, escaping
>> provenance and reference tracking.
>>
>> A global subprogram must return a scalar in every return register. The
>> existing exemption for arena pointers now applies only when the return
>> value fits in R0 alone: both halves of a register pair carry a piece of a
>>> 8 byte scalar, so an arena pointer in either of them is a leak rather than
>> a legitimate return value. A subprogram whose whole return value is an
>> arena pointer is unaffected.
>>
>> A static subprogram is handled differently. The verifier walks into its
>> frame, so prepare_func_exit() propagates the return register(s) to the
>> caller. R0 holding a stack pointer has long been rejected outright there,
>> but R2 is deliberately not treated the same way. LLVM owns both sides of a
>> static call and is not bound by the ABI, so even with a 9..16 byte declared
>> return type it may leave R2 untouched when the caller only consumes the low
>> half; R2 can then hold an incidental stack pointer that is not a return
>> value at all, and rejecting the program would be a false positive.
>> Propagating the register as is would be worse: the callee frame is freed
>> immediately afterwards, leaving the caller with a PTR_TO_STACK that refers
>> to a frame which no longer exists. So the caller's R2 is marked
>> uninitialized instead, and only a caller that actually reads the returned
>> upper half fails. As with R0, a pointer into the caller's own frame is
>> scrubbed too, which is conservative but keeps the two registers consistent.
>>
>> Once callers read R0:R2, an extension program can no longer replace a
>> function with a >8 byte return value: an extension's own return is
>> capped at 8 bytes by the program-exit check above, so it would leave R2
>> stale for the target's callers. btf_check_type_match() cannot catch
>> this, as it compares return types by btf_type->info only and an int
>> carries no vlen, so a 16-byte __int128 and an 8-byte long compare equal.
>> Reject such an attach in bpf_check_attach_target() instead.
>>
>>    [1] https://github.com/llvm/llvm-project/pull/190894
>>    [2] https://github.com/llvm/llvm-project/pull/206876
>>
>> Signed-off-by: Yonghong Song <[email protected]>
> The main program is listed among the places that gain handling for a >8
> byte return:
>
>>    This patch adds handling for returns greater than 8 bytes in several
>>    places: BPF subprogram returns (the main program, and both global and
>>    static subprograms) and kfunc returns.
> For the main program the patch does the opposite, though.
> process_bpf_exit_full() gains an outright rejection:
>
> 	if (!cur_frame->subprogno && bpf_ret_reg_pair(env, 0)) {
> 		verbose(env,
> 			"return value larger than 8 bytes is not supported at program exit\n");
> 		return -EINVAL;
> 	}
>
> That rejection and its error message are not described anywhere in the
> changelog.  The only trace of it is the later backward reference:
>
>>    an extension's own return is
>>    capped at 8 bytes by the program-exit check above
> which points at a check the changelog never introduced, and the extension
> reject added in bpf_check_attach_target() rests entirely on that cap.
>
> Could the changelog say directly that a >8 byte return from the main
> program is rejected at BPF_EXIT, rather than listing the main program as a
> place where such returns are now handled?

Okay, will update commit message as you suggested in the above.

>
>
> ---
> 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/31446101762
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.