From: Yonghong Song <yonghong.song@linux.dev>
To: bpf@vger.kernel.org
Cc: Alexei Starovoitov <ast@kernel.org>,
Andrii Nakryiko <andrii@kernel.org>,
Daniel Borkmann <daniel@iogearbox.net>,
Eduard Zingerman <eddyz87@gmail.com>,
kernel-team@fb.com
Subject: [PATCH bpf-next v2 04/13] bpf: Track R2 of register-pair returns in precision backtracking
Date: Tue, 4 Aug 2026 13:35:43 -0700 [thread overview]
Message-ID: <20260804203543.1871663-1-yonghong.song@linux.dev> (raw)
In-Reply-To: <20260804203522.1869244-1-yonghong.song@linux.dev>
A function returning a value larger than 8 bytes (a struct/union, or an
__int128) uses R2 as a second return register alongside R0. Precision
backtracking treats only R0 as a return register at a call/return boundary,
so once the verifier starts modeling R2 that way, marking the second half
of such a return precise would trip the "unexpected regs" checks in
backtrack_insn() and reject a valid program with -EFAULT. Handle it here,
ahead of the patch that introduces the modeling.
Marking the upper half precise, for example by branching on it after a
call to a static subprogram, walks backtracking into the callee and
reaches its BPF_EXIT with R2 still set in the mask. R2 is part of
BPF_REGMASK_ARGS, so this hits "backtracking exit unexpected regs".
Returning the pair from a global subprogram or from a kfunc instead hits
the equivalent check at the call site.
Handle R2 like R0 in the three boundaries where a call defines the return
registers:
- static subprog exit (BPF_EXIT): when the callee returns a pair, R2 is a
return register rather than a clobbered argument, so its precision has to
cross the frame boundary just like R0's: clear it from the caller's mask
before the R1-R5 check, then set it again in the callee's mask after
bt_subprog_enter().
The clear has to be conditional, which is why the subprogram containing
the exit insn is looked up and queried. For a callee that does not return
a pair, check_func_call() has already invalidated the caller's R1-R5 and
prepare_func_exit() copies back only R0, so nothing after the call can
depend on R2 and backtracking should never still be asking for it here.
Clearing it unconditionally would turn that into a silent no-op instead
of reporting it through the existing "backtracking exit unexpected regs"
check.
- global subprog call: a global subprog returning >8 bytes also sets R2;
clear it before the args check.
- kfunc call (BPF_CALL): a kfunc returning >8 bytes (model ret_size > 8)
also sets R2; clear it like R0.
All three are gated on R2 actually being in the mask, so the extra BTF and
kfunc descriptor lookups stay off the common backtracking path.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
include/linux/bpf_verifier.h | 2 ++
kernel/bpf/backtrack.c | 59 ++++++++++++++++++++++++++++--------
kernel/bpf/verifier.c | 13 ++++++++
3 files changed, 62 insertions(+), 12 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 18a6ecff39c5..adb3f3019a98 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -1442,6 +1442,8 @@ int bpf_jmp_offset(struct bpf_insn *insn);
struct bpf_iarray *bpf_insn_successors(struct bpf_verifier_env *env, u32 idx);
void bpf_fmt_stack_mask(char *buf, ssize_t buf_sz, u64 stack_mask);
bool bpf_subprog_is_global(const struct bpf_verifier_env *env, int subprog);
+int bpf_get_kfunc_ret_size(const struct bpf_prog *prog, u32 func_id,
+ u16 btf_fd_idx, u8 *ret_size);
int bpf_find_subprog(struct bpf_verifier_env *env, int off);
bool bpf_is_throw_kfunc(struct bpf_insn *insn);
diff --git a/kernel/bpf/backtrack.c b/kernel/bpf/backtrack.c
index 2f473ad4fd7c..498b15082801 100644
--- a/kernel/bpf/backtrack.c
+++ b/kernel/bpf/backtrack.c
@@ -424,6 +424,14 @@ static int backtrack_insn(struct bpf_verifier_env *env, int idx, int subseq_idx,
*/
verifier_bug_if(idx + 1 != subseq_idx, env,
"extra insn from subprog");
+ /* a global subprog returning more than 8 bytes
+ * sets R2 as well. R2 is part of the args mask
+ * checked just below, so it has to be cleared
+ * here rather than next to R0.
+ */
+ if (bt_is_reg_set(bt, BPF_REG_2) &&
+ bpf_ret_reg_pair(env, subprog))
+ bt_clear_reg(bt, BPF_REG_2);
/* r1-r5 are invalidated after subprog call,
* so for global func call it shouldn't be set
* anymore
@@ -507,6 +515,17 @@ static int backtrack_insn(struct bpf_verifier_env *env, int idx, int subseq_idx,
return -ENOTSUPP;
/* regular helper call sets R0 */
bt_clear_reg(bt, BPF_REG_0);
+ /* a kfunc returning more than 8 bytes also sets R2 */
+ if (insn->src_reg == BPF_PSEUDO_KFUNC_CALL &&
+ bt_is_reg_set(bt, BPF_REG_2)) {
+ u8 ret_size;
+
+ if (bpf_get_kfunc_ret_size(env->prog, insn->imm, insn->off,
+ &ret_size))
+ return -ENOTSUPP;
+ if (ret_size > 8)
+ bt_clear_reg(bt, BPF_REG_2);
+ }
if (bt_reg_mask(bt) & BPF_REGMASK_ARGS) {
/* if backtracking was looking for registers R1-R5
* they should have been found already.
@@ -521,7 +540,29 @@ static int backtrack_insn(struct bpf_verifier_env *env, int idx, int subseq_idx,
return -EFAULT;
}
} else if (opcode == BPF_EXIT) {
- bool r0_precise;
+ bool from_subprog_call, r0_precise, r2_precise = false;
+
+ /* BPF_EXIT in subprog or callback always returns
+ * right after the call instruction, so by checking
+ * whether the instruction at subseq_idx-1 is subprog
+ * call or not we can distinguish actual exit from
+ * *subprog* from exit from *callback*. In the former
+ * case, we need to propagate the precision of the
+ * return registers, if necessary. In the latter we
+ * never do that.
+ */
+ from_subprog_call = subseq_idx - 1 >= 0 &&
+ bpf_pseudo_call(&env->prog->insnsi[subseq_idx - 1]);
+ if (from_subprog_call && bt_is_reg_set(bt, BPF_REG_2)) {
+ struct bpf_subprog_info *callee;
+
+ /* 'idx' is the exit insn, so it is in the callee */
+ callee = bpf_find_containing_subprog(env, idx);
+ if (verifier_bug_if(!callee, env,
+ "no subprog contains exit insn %d", idx))
+ return -EFAULT;
+ r2_precise = bpf_ret_reg_pair(env, callee - env->subprog_info);
+ }
/* Backtracking to a nested function call, 'idx' is a part of
* the inner frame 'subseq_idx' is a part of the outer frame.
@@ -534,23 +575,15 @@ static int backtrack_insn(struct bpf_verifier_env *env, int idx, int subseq_idx,
if (subseq_idx >= 0 && bpf_calls_callback(env, subseq_idx))
for (i = BPF_REG_1; i <= BPF_REG_5; i++)
bt_clear_reg(bt, i);
+ if (r2_precise)
+ bt_clear_reg(bt, BPF_REG_2);
if (bt_reg_mask(bt) & BPF_REGMASK_ARGS) {
verifier_bug(env, "backtracking exit unexpected regs %x",
bt_reg_mask(bt));
return -EFAULT;
}
- /* BPF_EXIT in subprog or callback always returns
- * right after the call instruction, so by checking
- * whether the instruction at subseq_idx-1 is subprog
- * call or not we can distinguish actual exit from
- * *subprog* from exit from *callback*. In the former
- * case, we need to propagate r0 precision, if
- * necessary. In the former we never do that.
- */
- r0_precise = subseq_idx - 1 >= 0 &&
- bpf_pseudo_call(&env->prog->insnsi[subseq_idx - 1]) &&
- bt_is_reg_set(bt, BPF_REG_0);
+ r0_precise = from_subprog_call && bt_is_reg_set(bt, BPF_REG_0);
bt_clear_reg(bt, BPF_REG_0);
if (bt_subprog_enter(bt))
@@ -558,6 +591,8 @@ static int backtrack_insn(struct bpf_verifier_env *env, int idx, int subseq_idx,
if (r0_precise)
bt_set_reg(bt, BPF_REG_0);
+ if (r2_precise)
+ bt_set_reg(bt, BPF_REG_2);
/* r6-r9 and stack slots will stay set in caller frame
* bitmasks until we return back from callee(s)
*/
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 4010575d6715..282aee7fc44c 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -2504,6 +2504,19 @@ int bpf_get_kfunc_addr(const struct bpf_prog *prog, u32 func_id,
return 0;
}
+int bpf_get_kfunc_ret_size(const struct bpf_prog *prog, u32 func_id,
+ u16 btf_fd_idx, u8 *ret_size)
+{
+ const struct bpf_kfunc_desc *desc;
+
+ desc = find_kfunc_desc(prog, func_id, btf_fd_idx);
+ if (!desc)
+ return -EFAULT;
+
+ *ret_size = desc->func_model.ret_size;
+ return 0;
+}
+
#define BPF_FD_SLOT_BTF 1UL
static void fd_slot_set_map(struct bpf_fd_array *slot, struct bpf_map *map)
--
2.53.0-Meta
next prev parent reply other threads:[~2026-08-04 20:35 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-04 20:35 [PATCH v2 00/13] bpf: Support aggregate return values up to 16 bytes Yonghong Song
2026-08-04 20:35 ` [PATCH bpf-next v2 01/13] bpf: Factor check_global_ret_scalar_reg() out of the global return check Yonghong Song
2026-08-04 20:35 ` [PATCH bpf-next v2 02/13] bpf: Add helpers to describe the R0:R2 return register pair Yonghong Song
2026-08-04 20:35 ` [PATCH bpf-next v2 03/13] bpf: Wire up JIT support for 16-byte kfunc returns Yonghong Song
2026-08-04 20:35 ` Yonghong Song [this message]
2026-08-04 20:35 ` [PATCH bpf-next v2 05/13] bpf: Account R2 of register-pair returns in live register analysis Yonghong Song
2026-08-04 21:14 ` sashiko-bot
2026-08-04 20:35 ` [PATCH bpf-next v2 06/13] bpf: Reject callbacks returning more than 8 bytes Yonghong Song
2026-08-04 21:54 ` bot+bpf-ci
2026-08-04 20:35 ` [PATCH bpf-next v2 07/13] bpf: Add verifier support for 16-byte returns in R0:R2 Yonghong Song
2026-08-04 20:52 ` sashiko-bot
2026-08-04 20:36 ` [PATCH bpf-next v2 08/13] bpf: Reject register-pair returns when the subprog BTF is unreliable Yonghong Song
2026-08-04 20:36 ` [PATCH bpf-next v2 09/13] bpf: Enable aggregate return types up to 16 bytes Yonghong Song
2026-08-04 20:36 ` [PATCH bpf-next v2 10/13] selftests/bpf: Add C tests for 16-byte returns in R0:R2 Yonghong Song
2026-08-04 20:47 ` sashiko-bot
2026-08-04 20:36 ` [PATCH bpf-next v2 11/13] selftests/bpf: Add inline-asm and subprog tests for R0:R2 returns Yonghong Song
2026-08-04 20:52 ` sashiko-bot
2026-08-04 20:36 ` [PATCH bpf-next v2 12/13] selftests/bpf: Add tests for callbacks returning more than 8 bytes Yonghong Song
2026-08-04 20:36 ` [PATCH bpf-next v2 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=20260804203543.1871663-1-yonghong.song@linux.dev \
--to=yonghong.song@linux.dev \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=kernel-team@fb.com \
/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