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] bpf: Rewrite "rX <<= 32; rX >>= 32" into "wX = wX" to keep linked scalars
Date: Thu, 20 Aug 2026 21:28:37 -0700 [thread overview]
Message-ID: <20260821042837.2786719-1-yonghong.song@linux.dev> (raw)
For the following test in
tools/testing/selftests/bpf/progs/verifier_linked_scalars.c:
void alu32_negative_offset(void)
{
volatile char path[5];
volatile int offset = bpf_get_prandom_u32();
int off = offset;
if (off >= 5 && off < 10)
path[off - 5] = '.';
/* So compiler doesn't say: error: variable 'path' set but not used */
__sink(path[0]);
}
Without alu32 (-mcpu=v2), the test
verifier_linked_scalars/alu32_negative_offset will fail with llvm22 and
llvm23 like below.
3: (bf) r2 = r1 ; R1=scalar(id=1,...) R2=scalar(id=1,...)
4: (07) r2 += -5 ; R2=scalar(id=1-5,smin=-5,smax=0xfffffffa)
5: (67) r2 <<= 32 ; R2=scalar(smax=0x7fffffff00000000,...)
6: (77) r2 >>= 32 ; R2=scalar(smin=0,umax=0xffffffff,...)
7: (25) if r2 > 0x4 goto pc+5 ; R2=scalar(smin=0,smax=umax=4,...)
8: (bf) r2 = r10
9: (07) r2 += -5
10: (0f) r2 += r1 ; R1=scalar(id=1,smin=0,umax=0xffffffff)
; R2=fp(smin=-5,smax=0xfffffffa)
11: (b7) r1 = 46 ; R1=46
12: (73) *(u8 *)(r2 -5) = r1
invalid unbounded variable-offset write to stack R2
R1 is never narrowed down, so the address stays unbounded and the store
is rejected.
The test is okay for llvm21 with -mcpu=v2, see below:
3: (07) r1 += -5 ; R1=scalar(smin=-5,smax=0xfffffffa)
4: (67) r1 <<= 32 ; R1=scalar(smax=0x7fffffff00000000,...)
5: (77) r1 >>= 32 ; R1=scalar(smin=0,umax=0xffffffff,...)
6: (25) if r1 > 0x4 goto pc+5 ; R1=scalar(smin=0,smax=umax=4,...)
7: (bf) r2 = r10
8: (07) r2 += -5
9: (0f) r2 += r1 ; R2=fp(smin=-5,smax=-1)
10: (b7) r1 = 46 ; R1=46
11: (73) *(u8 *)(r2 +0) = r1 ; fp-8=ppppm???
To fix the test issue with llvm22 and llvm23, note that the shift pair
computes zext32(base + delta), which is what "wX = wX" computes as well
and which is exactly the relation BPF_ADD_CONST32 describes. So rewrite
the pair into "wX = wX" and let the mov turn the 64-bit link into a
32-bit one, which makes the -mcpu=v2 sequence track like an alu32 one.
The rewrite has to keep the program length, so the second shift becomes
a second "wX = wX" rather than being removed. A "goto pc+0" nop looks
like the obvious filler, but it is a jump, and bpf_is_state_visited()
decides where to place a checkpoint based on how many jumps it has
seen. One extra jump per shift pair re-times that heuristic and moves
the checkpoints of an enclosing loop, which can lose state pruning.
For example, clear_global_array_list() in the no_alu32 flavour of the
linked_list selftest calls the always_inline clear_list(), three
256 iteration loops in a row. With the shift pair, we have
processed 15384 insns (limit 1000000) max_states_per_insn 4 total_states 260 peak_states 125 mark_read 0
With a "goto pc+0" filler the checkpoint changes its location, we have
BPF program is too large. Processed 1000001 insn
processed 1000001 insns (limit 1000000) max_states_per_insn 4 total_states 19479 peak_states 148 mark_read 0
The duplicated mov keeps both the insn and the jump counts the same as
before the rewrite. It is idempotent, the second mov re-derives the
same bounds and finds the 32-bit link already in place.
With this, the llvm22 and llvm23 code verifies, R2 keeps its id across
the zero extension and the jump narrows down R1:
3: (bf) r2 = r1 ; R1=scalar(id=1,...) R2=scalar(id=1,...)
4: (07) r2 += -5 ; R2=scalar(id=1-5,smin=-5,smax=0xfffffffa)
5: (bc) w2 = w2 ; R2=scalar(id=1-5,smin=0,umax=0xffffffff)
6: (bc) w2 = w2 ; R2=scalar(id=1-5,smin=0,umax=0xffffffff)
7: (25) if r2 > 0x4 goto pc+5 ; R1=scalar(id=1,smin=5,smax=9,...)
; R2=scalar(id=1-5,smin=0,smax=4,...)
8: (bf) r2 = r10
9: (07) r2 += -5
10: (0f) r2 += r1 ; R2=fp(smin=0,smax=4)
11: (b7) r1 = 46 ; R1=46
12: (73) *(u8 *)(r2 -5) = r1 ; fp-8=ppppm???
The llvm21 sequence is rewritten as well, the rewrite is not tied to
BPF_ADD_CONST, so both its insns 4 and 5 become "w1 = w1". R1 carries
no id there, so the mov takes the ordinary zero extension path and ends
up with the same bounds the shift pair produced, the program verifies
as before.
Signed-off-by: Yonghong Song <yonghong.song@linux.dev>
---
kernel/bpf/verifier.c | 48 ++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 47 insertions(+), 1 deletion(-)
Changelog:
v1 -> v2:
- v1: https://lore.kernel.org/bpf/20260820013925.2515018-1-yonghong.song@linux.dev/
- Replace left/right unsigned 32bit ship with 32bit mov's. This is done before
main verification.
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 821b47ac75c5..e1de442801d9 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -15610,6 +15610,14 @@ static int adjust_scalar_min_max_vals(struct bpf_verifier_env *env,
return 0;
}
+static bool linked_base_fits_u32(const struct bpf_reg_state *reg)
+{
+ if (reg->id & BPF_ADD_CONST32)
+ return true;
+ return reg_smin(reg) >= (s64)reg->delta &&
+ reg_smax(reg) <= (s64)U32_MAX + (s64)reg->delta;
+}
+
/* Handles ALU ops other than BPF_END, BPF_NEG and BPF_MOV: computes new min/max
* and var_off.
*/
@@ -15878,7 +15886,12 @@ static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
insn->src_reg);
return -EACCES;
} else if (src_reg->type == SCALAR_VALUE) {
- if (insn->off == 0) {
+ if (insn->off == 0 && insn->src_reg == insn->dst_reg &&
+ (dst_reg->id & BPF_ADD_CONST) &&
+ linked_base_fits_u32(dst_reg)) {
+ dst_reg->id = (dst_reg->id & ~BPF_ADD_CONST64) |
+ BPF_ADD_CONST32;
+ } else if (insn->off == 0) {
bool is_src_reg_u32 = get_reg_width(src_reg) <= 32;
if (is_src_reg_u32)
@@ -19183,6 +19196,36 @@ static int check_and_resolve_insns(struct bpf_verifier_env *env)
return 0;
}
+static bool is_shift_by_32(const struct bpf_insn *insn, u8 op)
+{
+ return insn->code == (BPF_ALU64 | op | BPF_K) && insn->off == 0 && insn->imm == 32;
+}
+
+/* 'rX <<= 32; rX >>= 32' => 'wX = wX; wX = wX' */
+static void bpf_rewrite_zext_shifts(struct bpf_verifier_env *env)
+{
+ struct bpf_insn *insn = env->prog->insnsi;
+ int i;
+
+ for (i = 0; i < env->prog->len - 1; i++) {
+ if (!is_shift_by_32(&insn[i], BPF_LSH) ||
+ !is_shift_by_32(&insn[i + 1], BPF_RSH) ||
+ insn[i].dst_reg != insn[i + 1].dst_reg)
+ continue;
+ if (bpf_is_jmp_point(env, i + 1))
+ continue;
+ /*
+ * The second mov is redundant, but a nop (goto pc+0) cannot
+ * be used here. A 'goto pc+0' is a jump, and the extra jump
+ * may change where checkpoints are placed, see
+ * bpf_is_state_visited(). The second mov avoids that.
+ */
+ insn[i] = BPF_MOV32_REG(insn[i].dst_reg, insn[i].dst_reg);
+ insn[i + 1] = insn[i];
+ i++;
+ }
+}
+
/* drop refcnt of maps used by the rejected program */
static void release_maps(struct bpf_verifier_env *env)
{
@@ -21149,6 +21192,9 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr, bpfptr_t uattr,
if (ret < 0)
goto skip_full_check;
+ /* Needs the jump point marks left by bpf_check_cfg(). */
+ bpf_rewrite_zext_shifts(env);
+
ret = bpf_compute_postorder(env);
if (ret < 0)
goto skip_full_check;
--
2.53.0-Meta
next reply other threads:[~2026-08-21 4:28 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-21 4:28 Yonghong Song [this message]
2026-08-21 5:30 ` [PATCH bpf-next v2] bpf: Rewrite "rX <<= 32; rX >>= 32" into "wX = wX" to keep linked scalars bot+bpf-ci
2026-08-21 17:36 ` Eduard Zingerman
2026-08-22 16:47 ` Yonghong Song
2026-08-24 18:29 ` Eduard Zingerman
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=20260821042837.2786719-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