From: Yonghong Song <yonghong.song@linux.dev>
To: bot+bpf-ci@kernel.org, bpf@vger.kernel.org
Cc: ast@kernel.org, andrii@kernel.org, daniel@iogearbox.net,
eddyz87@gmail.com, kernel-team@fb.com, martin.lau@kernel.org,
clm@meta.com, ihor.solodrai@linux.dev
Subject: Re: [PATCH bpf-next v4 08/13] bpf: Reject register-pair returns when the subprog BTF is unreliable
Date: Wed, 12 Aug 2026 13:26:44 -0700 [thread overview]
Message-ID: <780f14d1-b752-40d9-98e6-8218077bfdf2@linux.dev> (raw)
In-Reply-To: <b0796c556995285a17bc51b371fb413f000498e0d78d5a040ef8dba50010726d@mail.kernel.org>
On 8/10/26 6:24 PM, bot+bpf-ci@kernel.org wrote:
>> commit cb2551cf6e866ae6692739f7cbb75ff97e2d1a3b
>> Author: Yonghong Song <yonghong.song@linux.dev>
>> 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 <yonghong.song@linux.dev>
> 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
next prev parent reply other threads:[~2026-08-12 20:26 UTC|newest]
Thread overview: 36+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-11 0:09 [PATCH bpf-next v4 00/13] bpf: Support aggregate return values up to 16 bytes Yonghong Song
2026-08-11 0:09 ` [PATCH bpf-next v4 01/13] bpf: Factor check_global_ret_scalar_reg() out of the global return check Yonghong Song
2026-08-11 0:09 ` [PATCH bpf-next v4 02/13] bpf: Add helpers to describe the R0:R2 return register pair Yonghong Song
2026-08-11 1:24 ` bot+bpf-ci
2026-08-12 19:31 ` Yonghong Song
2026-08-12 20:07 ` Eduard Zingerman
2026-08-11 0:09 ` [PATCH bpf-next v4 03/13] bpf: Wire up JIT support for 16-byte kfunc returns Yonghong Song
2026-08-11 1:24 ` bot+bpf-ci
2026-08-12 19:48 ` Yonghong Song
2026-08-12 20:42 ` Eduard Zingerman
2026-08-11 0:09 ` [PATCH bpf-next v4 04/13] bpf: Track R2 of register-pair returns in precision backtracking Yonghong Song
2026-08-12 21:16 ` Eduard Zingerman
2026-08-11 0:09 ` [PATCH bpf-next v4 05/13] bpf: Account R2 of register-pair returns in live register analysis Yonghong Song
2026-08-11 1:09 ` bot+bpf-ci
2026-08-12 19:55 ` Yonghong Song
2026-08-12 21:21 ` Eduard Zingerman
2026-08-11 0:09 ` [PATCH bpf-next v4 06/13] bpf: Reject callbacks returning more than 8 bytes Yonghong Song
2026-08-11 0:09 ` [PATCH bpf-next v4 07/13] bpf: Add verifier support for 16-byte returns in R0:R2 Yonghong Song
2026-08-11 1:24 ` bot+bpf-ci
2026-08-12 20:12 ` Yonghong Song
2026-08-11 0:09 ` [PATCH bpf-next v4 08/13] bpf: Reject register-pair returns when the subprog BTF is unreliable Yonghong Song
2026-08-11 1:24 ` bot+bpf-ci
2026-08-12 20:26 ` Yonghong Song [this message]
2026-08-11 0:09 ` [PATCH bpf-next v4 09/13] bpf: Enable aggregate return types up to 16 bytes Yonghong Song
2026-08-11 1:24 ` bot+bpf-ci
2026-08-12 20:29 ` Yonghong Song
2026-08-11 0:10 ` [PATCH bpf-next v4 10/13] selftests/bpf: Add C tests for 16-byte returns in R0:R2 Yonghong Song
2026-08-11 1:24 ` bot+bpf-ci
2026-08-12 20:49 ` Yonghong Song
2026-08-11 0:10 ` [PATCH bpf-next v4 11/13] selftests/bpf: Add inline-asm and subprog tests for R0:R2 returns Yonghong Song
2026-08-11 1:24 ` bot+bpf-ci
2026-08-12 21:08 ` Yonghong Song
2026-08-11 0:10 ` [PATCH bpf-next v4 12/13] selftests/bpf: Add tests for callbacks returning more than 8 bytes Yonghong Song
2026-08-11 1:24 ` bot+bpf-ci
2026-08-12 21:22 ` Yonghong Song
2026-08-11 0:10 ` [PATCH bpf-next v4 13/13] Documentation/bpf: Document up to 16-byte kfunc return values in R0:R2 Yonghong Song
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=780f14d1-b752-40d9-98e6-8218077bfdf2@linux.dev \
--to=yonghong.song@linux.dev \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bot+bpf-ci@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=clm@meta.com \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=ihor.solodrai@linux.dev \
--cc=kernel-team@fb.com \
--cc=martin.lau@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox