Re: [PATCH bpf-next v4 08/13] bpf: Reject register-pair returns when the subprog BTF is unreliable

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

On 8/10/26 6:24 PM, [email protected] wrote:
>> commit cb2551cf6e866ae6692739f7cbb75ff97e2d1a3b
>> Author: Yonghong Song <[email protected]>
>> Date:   Mon Aug 10 17:09:52 2026 -0700
>>
>>      bpf: Reject register-pair returns when the subprog BTF is unreliable
>>
>>      The R0:R2 return convention is derived from the BTF function prototype:
>>      bpf_compute_subprog_ret_regs() inspects the return type of every
>>      subprogram and records whether its value comes back in a register pair.
>>
>>      btf_check_subprog_call() can decide, at a call site, that this BTF is
>>      not to be trusted and mark the subprogram unreliable, which happens when
>>      compiler optimizations remove arguments from a static function or when a
>>      mismatched type is passed to a global one. From that point on the
>>      verifier falls back to conservative, R0-only, semantics for the
>>      subprogram, while the compiled code keeps returning a pair and leaves
>>      the upper half in R2 behind the verifier's back.
>>
>>      Rather than silently mistracking R2, reject a return value larger than
>>      8 bytes as soon as the prototype it was derived from becomes unreliable.
>>      Add subprog_ret_pair_unreliable() and test it at the two places that can
>>      observe the flag: check_func_call(), for the call itself, and
>>      prepare_func_exit(), for the return from an inlined static subprogram.
>>
>>      Note that the main program needs no such check: a >8 byte return from
>>      subprog 0 is rejected at BPF_EXIT regardless of whether its BTF is
>>      reliable. Callbacks need none either: a callback address only becomes a
>>      PTR_TO_FUNC through check_ld_imm(), which already rejects any callback
>>      returning more than 8 bytes.
>>
>>      Signed-off-by: Yonghong Song <[email protected]>
> This looks like a bug fix for register-pair return support. Should this
> include:
>
>    Fixes: d280e9c973c5 ("bpf: Add verifier support for 16-byte returns in R0:R2")

Typically we don't do 'Fixes' within the same series.

>
>> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
>> index 944f0721086bd..8f0c4aed0781f 100644
>> --- a/kernel/bpf/verifier.c
>> +++ b/kernel/bpf/verifier.c
>> @@ -438,6 +438,21 @@ static void bpf_compute_subprog_ret_regs(struct bpf_verifier_env *env)
>>   	}
>>   }
>>
>> +/*
>> + * A >8 byte BPF return changes the calling convention to R0:R2, so the
>> + * verifier can only allow it while the subprogram's prototype remains
>> + * reliable. Once BTF is marked unreliable, reject the feature instead of
>> + * silently falling back to R0-only semantics.
>> + */
> Does the verifier actually fall back to R0-only semantics? The return
> convention is read through bpf_ret_reg_pair():
>
>    include/linux/bpf_verifier.h:
>    static inline bool bpf_ret_reg_pair(struct bpf_verifier_env *env, int subprog)
>    {
>            return subprog_info(env, subprog)->ret_reg_pair;
>    }
>
> and ret_reg_pair is computed once in bpf_compute_subprog_ret_regs() from
> the BTF return type. It is never recomputed or cleared when unreliable
> becomes true.
>
> Grepping every reader of func_info_aux[].unreliable finds exactly three,
> none of which touches the return convention:
>
>    - btf_check_subprog_call(): returns -EINVAL early on a later call site
>    - btf_prepare_func_args(): verifier_bug() + -EFAULT
>    - bpf_check_attach_target(): sets 'conservative' for fentry/fexit
>
> So when unreliable becomes true, prepare_func_exit() still computes
> nregs = 2 and still models R2. For a static subprogram the verifier walks
> the callee, prepare_func_exit() propagates the callee's real R0/R2, and
> clear_caller_saved_regs() marks R0-R5 NOT_INIT at every call, so a caller
> that reads R2 without the callee having written it gets 'R2 !read_ok'.
>
> Worth either citing the concrete path where R2 is mistracked, or dropping
> the claim and stating the real motivation (unreliable BTF means the
> prototype the convention was derived from cannot be trusted at all)?

I will change to mean that for unreliable BTF, the convention was
derived from the prototype cannot be trusted.

>
>> +static bool subprog_ret_pair_unreliable(struct bpf_verifier_env *env, int subprog)
>> +{
>> +	struct bpf_prog_aux *aux = env->prog->aux;
>> +
>> +	return bpf_ret_reg_pair(env, subprog) &&
>> +	       aux->func_info_aux &&
>> +	       aux->func_info_aux[subprog].unreliable;
>> +}
>> +
>>   static const char *subprog_name(const struct bpf_verifier_env *env, int subprog)
>>   {
>>   	struct bpf_func_info *info;
>> @@ -9459,6 +9474,11 @@ static int check_func_call(struct bpf_verifier_env *env, struct bpf_insn *insn,
>>   	err = btf_check_subprog_call(env, subprog, caller->regs);
>>   	if (err == -EFAULT)
>>   		return err;
>> +	if (subprog_ret_pair_unreliable(env, subprog)) {
>> +		verbose(env, "Func#%d ('%s') returns >8 bytes, which requires reliable BTF\n",
>> +			subprog, subprog_name(env, subprog));
>> +		return -EINVAL;
>> +	}
> Does this break git bisect? At this commit the new check makes every
> static (local) subprogram that returns a 9..16 byte struct or union
> unloadable, which the parent commit d280e9c973c59 accepted.
>
> The two conditions in subprog_ret_pair_unreliable() are guaranteed to be
> true together for such a subprogram at this point in the series:
>
> 1. bpf_compute_subprog_ret_regs() at verifier.c:429 sets ret_reg_pair
>     from the BTF return type for structs as well as scalars:
>
>       if (type && (btf_type_is_struct(type) || btf_type_is_scalar(type)))
>               subprog_info(env, subprog)->ret_reg_pair = ret_regs_cnt(type->size) > 1;
>
>     so a 16-byte struct return gets ret_reg_pair = 1.
>
> 2. btf_validate_return_type() in kernel/bpf/btf.c still ends with 'return
>     -EOPNOTSUPP;' for anything that is not void/int/enum (or an arena
>     pointer), i.e. for every struct/union return. That -EOPNOTSUPP
>     propagates through btf_prepare_func_args() and btf_check_func_arg_match()
>     to btf_check_subprog_call(), which sets
>     prog->aux->func_info_aux[subprog].unreliable = true precisely because it
>     deliberately swallows any error other than -EFAULT for static
>     subprograms.
>
> So btf_check_subprog_call() marks the subprogram unreliable, returns
> -EOPNOTSUPP (not -EFAULT, so the early return above does not fire), and
> the new check then rejects the call with 'Func#N (...) returns >8 bytes,
> which requires reliable BTF'. Before this patch the swallowed error was
> harmless: the verifier walked into the static callee and prepare_func_exit()
> copied the real R0 and R2 state back to the caller.
>
> The series' own selftest proves the intended behaviour. The file
> tools/testing/selftests/bpf/progs/aggregate_ret_func.c (added later in the
> series by f40812d018cc3) has:
>
>    static __naked __noinline struct ptr_pair static_ret_ptr_pair(void)  /* 16 bytes */
>    ...
>    SEC("tc") __success __retval(0)
>    __naked int aggregate_ret_static_ptr_pair(void)
>
> tagged __success, with the comment 'A static subprogram is verified inline,
> so prepare_func_exit() hands the caller the callee's actual R0:R2 register
> state ... which is why btf_validate_return_type() does not apply the
> scalar-only restriction to a local function.' Built at this commit that
> program would be rejected.
>
> The later commit b12c58bbc5e59 'bpf: Enable aggregate return types up to
> 16 bytes' relaxes btf_validate_return_type() to accept a struct of size
> <= 16 for a local function, and its own comment states the reason: 'What it
> avoids is needlessly marking the subprogram's BTF unreliable.' Only
> __int128 returns are unaffected (int passes btf_validate_return_type()).
>
> Consider reordering so the return-type relaxation lands before this
> rejection, or noting the dependency in this commit's changelog?
I will do reordering between patch 8 and patch 9.
>
>> @@ -9832,6 +9852,11 @@ static int prepare_func_exit(struct bpf_verifier_env *env, int *insn_idx)
>>
>>   	callee = state->frame[state->curframe];
>>   	r0 = &callee->regs[BPF_REG_0];
>> +	if (subprog_ret_pair_unreliable(env, callee->subprogno)) {
>> +		verbose(env, "Func#%d ('%s') returns >8 bytes, which requires reliable BTF\n",
>> +			callee->subprogno, subprog_name(env, callee->subprogno));
>> +		return -EINVAL;
>> +	}
> Can this check ever fire? The changelog claims it is needed 'for the
> return from an inlined static subprogram', but the check appears to be
> unreachable.
>
> unreliable is monotonic (only ever set to true) and has exactly two
> writers:
>
>    - btf_check_subprog_call() at verifier.c:9357, called from
>      check_func_call() and push_callback_call()
>    - do_check_common() at verifier.c:18591, which sets
>      func_info_aux[0].unreliable for subprog 0 only
>
> For a frame to reach prepare_func_exit() it must have been pushed by
> setup_func_entry() via one of these paths:
>
>    - check_func_call(): the new hunk above runs immediately after
>      btf_check_subprog_call() and before setup_func_entry(), so a subprog
>      that just became unreliable is rejected there and never gets a frame.
>      Since the flag cannot be set later for that subprog without going through
>      check_func_call() again (which would reject again), the exit check can
>      never see it true.
>
>    - push_callback_call(): check_ld_imm() is the only producer of
>      PTR_TO_FUNC and already rejects bpf_ret_reg_pair() callbacks ('callback
>      function with >8-byte return value is not supported'), so a callback
>      frame never has ret_reg_pair set.
>
>    - subprog 0 is frame 0 and is never a callee frame; check_cfg() rejects
>      a call back into subprog 0.
>
> The two other callers of prepare_func_exit() are equally excluded: the
> tail-call path at verifier.c:10797 and the LD_ABS path at verifier.c:16533
> can only run in a subprog whose BTF return type is a small int, because
> check_btf_func() in kernel/bpf/check_btf.c rejects BPF_LD_ABS and tail
> calls in any subprog that fails btf_type_is_small_int() (int of size <= 8),
> which excludes every ret_reg_pair subprog.
>
> If the check is intended as defence in depth for a future caller of
> btf_check_subprog_call(), saying so in the changelog would help; otherwise
> the hunk can be dropped?

Thanks. I will drop this.

>
>
> ---
> 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.