* [PATCH bpf-next v2 01/29] bpf: Keep the 32-bit range across a no-op sign extension
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 02/29] bpf: Model 32-bit shifts and divisions exactly in scalar evolution Alexei Starovoitov
` (27 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Vineet Gupta <vineet.gupta@linux.dev>
A fix of precision in the main pass. It stands alone in this series. Its
tests come with the tests of sign extensions of linked registers.
1: w0 &= 63
2: w0 -= 63
3: w0 = (s8)w0
4: if w0 s< 0xffffffc1 goto pc+2
..
7: w0 /= 0
div by zero
w0 is in [-63, 0] before 3. Every value of that range fits in s8, so the
sign extension is a no-op. The verifier drops the range all the same:
after 3 w0 is in [-128, 127], is_branch_taken() cannot tell at 4, and 7 is
verified though that branch is dead.
coerce_subreg_to_size_sx() keeps a range if both bounds have the same bits
above the low 8 or 16 bits. In a range that crosses 0 they differ: all ones
in -63, all zeros in 0. The check before that one is on umax, which is huge
when the range has a negative value.
Return early if the 32-bit range fits in s8 or s16. A register that passed
the check of umax has such a range, so that check goes.
coerce_reg_to_size_sx() does the same in 64 bits since
commit 0892ef2a7ef3 ("bpf: Representation for intervals with steps").
No verdict of the selftests changes.
Fixes: 8100928c8814 ("bpf: Support new sign-extension mov insns")
Signed-off-by: Vineet Gupta <vineet.gupta@linux.dev>
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/verifier.c | 20 +++++++++++++++++---
1 file changed, 17 insertions(+), 3 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 20b1b9ff574c..5bffc1e288f6 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -6441,14 +6441,28 @@ static void set_sext32_default_val(struct bpf_reg_state *reg, int size)
static void coerce_subreg_to_size_sx(struct bpf_reg_state *reg, int size)
{
s32 init_s32_max, init_s32_min, s32_max, s32_min, u32_val;
+ s32 field_smin, field_smax;
u32 top_smax_value, top_smin_value;
u32 num_bits = size * 8;
+ if (size == 1) {
+ field_smin = S8_MIN;
+ field_smax = S8_MAX;
+ } else {
+ /* size == 2 */
+ field_smin = S16_MIN;
+ field_smax = S16_MAX;
+ }
+
/*
- * If value fits the non-negative half of the signed N-bit range,
- * sign extension is a noop.
+ * The range already fits the signed N-bit range, so (sN)v == v for every
+ * value of the low 32 bits and the sign extension is a no-op.
+ *
+ * This has to precede reg_step_reset() below: the base/step line stays.
+ * It is about all 64 bits: zext_32_to_64() in the caller discards it
+ * if the upper 32 bits are not zero.
*/
- if (reg_umax(reg) < (1ULL << (num_bits - 1)))
+ if (reg_s32_min(reg) >= field_smin && reg_s32_max(reg) <= field_smax)
return;
/* See comment at reg_step_check_unsigned(). */
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 02/29] bpf: Model 32-bit shifts and divisions exactly in scalar evolution
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 01/29] bpf: Keep the 32-bit range across a no-op sign extension Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 03/29] bpf: Construct scalar evolution expressions without allocations Alexei Starovoitov
` (26 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A fix of the representation, for the patches that take two registers with
the same expression for the same value. Today two insns that compute
distinct values get the same expression:
r5 >>= r4; w5 = w5 (zext32 (>> r5 r4))
w5 >>= w4 (zext32 (>> r5 r4))
The second one shifts the low 32 bits of r5, by r4 & 31. Nothing goes wrong
so far: the code that analyzes loops does not look into a shift or
a division. cubic_root() of bpf_cubic.c is going to depend on it:
shift = (a >> (b * 3));
if (shift >= 64)
return 0;
x = ((__u32)(((__u32)v[shift] + 10) << b)) >> 6;
GCC tests one register and uses another one as the index:
r5 >>= r4
w2 = w5
if w5 > 0x3f goto ..
r4 = v ll
r4 += r2
The prog loads because r5 fits in u32 in the state: 'w2 = w5' assigns one
scalar id to r2 and r5, so the jump bounds both. Without scalar ids it is
going to load because r5 is (>> a b) and r2 is (zext32 (>> a b)), which is
r5 truncated to 32 bits.
An ALU32 insn is (zext32 (op a b)) with the 64-bit expressions of its
operands. That is exact for + - * | & ^ and for the value that << shifts:
the low 32 bits of the result depend on the low 32 bits of the operands
only. It is not exact for a right shift, a division, a modulo and a shift
amount. Zero or sign extend these operands, mask the shift amount:
w5 >>= w4 (zext32 (>> (zext32 r5) (& r4 31)))
w5 s>>= 3 (zext32 (s>> (sext32 r5) 3))
w5 /= w4 (zext32 (/ (zext32 r5) (zext32 r4)))
w5 s%= w4 (zext32 (s% (sext32 r5) (sext32 r4)))
Every expression is a 64-bit operation now. (>> x 32) shifts by 32. Before
the patch it could also be 'w1 >>= w2' with 32 in w2, which shifts by 0.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/scev.c | 46 +++++++++++++++----
.../selftests/bpf/progs/verifier_scev.c | 26 +++++------
2 files changed, 51 insertions(+), 21 deletions(-)
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index 4187ce955be9..1cb4bc7c0f96 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -686,6 +686,38 @@ static int maybe_load_fp(struct scev *scev, struct env *e, struct bpf_insn *insn
return replace_reg(scev, e, insn->dst_reg, id);
}
+/*
+ * The low 32 bits of a + b depend on the low 32 bits of a and b only: 'w1 += w2' is
+ * (zext32 (+ a b)). That doesn't hold for a right shift and for a division. Their operands are zero
+ * or sign extended and a shift amount is masked, so that every expression is a 64-bit operation.
+ */
+static void alu32_operands(struct scev *scev, u32 op, int *a, int *b)
+{
+ s64 imm = 0;
+ bool imm_b = *b >= 0 && is_imm(scev, *b, &imm);
+
+ switch (op) {
+ case BPF_RSH:
+ case BPF_ARSH:
+ *a = expr1(scev, op == BPF_RSH ? ZEXT32 : SEXT32, *a);
+ fallthrough;
+ case BPF_LSH:
+ *b = imm_b ? imm_expr(scev, imm & 31) :
+ expr2(scev, BPF_AND, *b, imm_expr(scev, 31));
+ break;
+ case BPF_DIV:
+ case BPF_MOD:
+ *a = expr1(scev, ZEXT32, *a);
+ *b = imm_b ? imm_expr(scev, (u32)imm) : expr1(scev, ZEXT32, *b);
+ break;
+ case SDIV:
+ case SMOD:
+ *a = expr1(scev, SEXT32, *a);
+ *b = imm_b ? imm_expr(scev, (s32)imm) : expr1(scev, SEXT32, *b);
+ break;
+ }
+}
+
static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
{
const bool little_endian = htons(0x3412) == 0x1234;
@@ -700,7 +732,7 @@ static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
u32 src = insn->src_reg;
u32 op, sext, l, r;
bool need_bswap;
- int i, id;
+ int i, id, a;
s64 imm;
/* reg2expr[12] is a stack slot, not r12 */
@@ -764,19 +796,17 @@ static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
else
goto mark_dst_unknown;
- /*
- * 'r1 <<= 32; r1 >>= 32' is (u32)r1 for cpu v1 and v2. Only here both
- * shifts are known to be 64-bit ones: the result of a 32-bit shift is
- * (zext32 ...), and (>> x 32) is 'w1 >>= w2' with 32 in w2 as well,
- * which shifts by 0.
- */
+ /* 'r1 <<= 32; r1 >>= 32' is (u32)r1 for cpu v1 and v2 */
if (class == BPF_ALU64 && op == BPF_RSH && id >= 0 &&
is_imm(scev, id, &imm) && imm == 32 &&
is_binop(scev, BPF_LSH, reg2expr[dst], &l, &r) &&
is_imm(scev, r, &imm) && imm == 32)
return replace_reg(scev, e, dst, expr1(scev, ZEXT32, l));
- id = expr2(scev, op, reg2expr[dst], id);
+ a = reg2expr[dst];
+ if (class == BPF_ALU)
+ alu32_operands(scev, op, &a, &id);
+ id = expr2(scev, op, a, id);
if (class == BPF_ALU)
id = expr1(scev, ZEXT32, id);
diff --git a/tools/testing/selftests/bpf/progs/verifier_scev.c b/tools/testing/selftests/bpf/progs/verifier_scev.c
index 5c9c832fbbe0..f2a53fe5f6ad 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scev.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scev.c
@@ -334,20 +334,20 @@ __msg("w1 *= 2 {{.*}}; r1 r6 -> (zext32 (* r6 2))")
__msg("w1 *= w7 {{.*}}; r1 r6 -> (zext32 (* r6 r7))")
__msg("r1 /= 2 {{.*}}; r1 r6 -> (/ r6 2)")
__msg("r1 /= r7 {{.*}}; r1 r6 -> (/ r6 r7)")
-__msg("w1 /= 2 {{.*}}; r1 r6 -> (zext32 (/ r6 2))")
-__msg("w1 /= w7 {{.*}}; r1 r6 -> (zext32 (/ r6 r7))")
+__msg("w1 /= 2 {{.*}}; r1 r6 -> (zext32 (/ (zext32 r6) 2))")
+__msg("w1 /= w7 {{.*}}; r1 r6 -> (zext32 (/ (zext32 r6) (zext32 r7)))")
__msg("r1 s/= 2 {{.*}}; r1 r6 -> (s/ r6 2)")
__msg("r1 s/= r7 {{.*}}; r1 r6 -> (s/ r6 r7)")
-__msg("w1 s/= 2 {{.*}}; r1 r6 -> (zext32 (s/ r6 2))")
-__msg("w1 s/= w7 {{.*}}; r1 r6 -> (zext32 (s/ r6 r7))")
+__msg("w1 s/= 2 {{.*}}; r1 r6 -> (zext32 (s/ (sext32 r6) 2))")
+__msg("w1 s/= w7 {{.*}}; r1 r6 -> (zext32 (s/ (sext32 r6) (sext32 r7)))")
__msg("r1 %= 2 {{.*}}; r1 r6 -> (% r6 2)")
__msg("r1 %= r7 {{.*}}; r1 r6 -> (% r6 r7)")
-__msg("w1 %= 2 {{.*}}; r1 r6 -> (zext32 (% r6 2))")
-__msg("w1 %= w7 {{.*}}; r1 r6 -> (zext32 (% r6 r7))")
+__msg("w1 %= 2 {{.*}}; r1 r6 -> (zext32 (% (zext32 r6) 2))")
+__msg("w1 %= w7 {{.*}}; r1 r6 -> (zext32 (% (zext32 r6) (zext32 r7)))")
__msg("r1 s%= 2 {{.*}}; r1 r6 -> (s% r6 2)")
__msg("r1 s%= r7 {{.*}}; r1 r6 -> (s% r6 r7)")
-__msg("w1 s%= 2 {{.*}}; r1 r6 -> (zext32 (s% r6 2))")
-__msg("w1 s%= w7 {{.*}}; r1 r6 -> (zext32 (s% r6 r7))")
+__msg("w1 s%= 2 {{.*}}; r1 r6 -> (zext32 (s% (sext32 r6) 2))")
+__msg("w1 s%= w7 {{.*}}; r1 r6 -> (zext32 (s% (sext32 r6) (sext32 r7)))")
__msg("r1 |= 2 {{.*}}; r1 r6 -> (| r6 2)")
__msg("r1 |= r7 {{.*}}; r1 r6 -> (| r6 r7)")
__msg("w1 |= 2 {{.*}}; r1 r6 -> (zext32 (| r6 2))")
@@ -363,15 +363,15 @@ __msg("w1 ^= w7 {{.*}}; r1 r6 -> (zext32 (^ r6 r7))")
__msg("r1 <<= 2 {{.*}}; r1 r6 -> (<< r6 2)")
__msg("r1 <<= r7 {{.*}}; r1 r6 -> (<< r6 r7)")
__msg("w1 <<= 2 {{.*}}; r1 r6 -> (zext32 (<< r6 2))")
-__msg("w1 <<= w7 {{.*}}; r1 r6 -> (zext32 (<< r6 r7))")
+__msg("w1 <<= w7 {{.*}}; r1 r6 -> (zext32 (<< r6 (& r7 31)))")
__msg("r1 >>= 2 {{.*}}; r1 r6 -> (>> r6 2)")
__msg("r1 >>= r7 {{.*}}; r1 r6 -> (>> r6 r7)")
-__msg("w1 >>= 2 {{.*}}; r1 r6 -> (zext32 (>> r6 2))")
-__msg("w1 >>= w7 {{.*}}; r1 r6 -> (zext32 (>> r6 r7))")
+__msg("w1 >>= 2 {{.*}}; r1 r6 -> (zext32 (>> (zext32 r6) 2))")
+__msg("w1 >>= w7 {{.*}}; r1 r6 -> (zext32 (>> (zext32 r6) (& r7 31)))")
__msg("r1 s>>= 2 {{.*}}; r1 r6 -> (s>> r6 2)")
__msg("r1 s>>= r7 {{.*}}; r1 r6 -> (s>> r6 r7)")
-__msg("w1 s>>= 2 {{.*}}; r1 r6 -> (zext32 (s>> r6 2))")
-__msg("w1 s>>= w7 {{.*}}; r1 r6 -> (zext32 (s>> r6 r7))")
+__msg("w1 s>>= 2 {{.*}}; r1 r6 -> (zext32 (s>> (sext32 r6) 2))")
+__msg("w1 s>>= w7 {{.*}}; r1 r6 -> (zext32 (s>> (sext32 r6) (& r7 31)))")
__msg("r1 = -r1 {{.*}}; r1 r6 -> (- r6)")
__msg("w1 = -w1 {{.*}}; r1 r6 -> (zext32 (- r6))")
__msg("r1 = 42 {{.*}}; r1 r6 -> 42")
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 03/29] bpf: Construct scalar evolution expressions without allocations
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 01/29] bpf: Keep the 32-bit range across a no-op sign extension Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 02/29] bpf: Model 32-bit shifts and divisions exactly in scalar evolution Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 04/29] bpf: Don't join dead registers in scalar evolution Alexei Starovoitov
` (25 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
This is about the cost of scalar evolution. The time in scev.c goes down by
a constant factor: 1.7 per insn, 5 per subprog, 11 for the smallest prog.
No expression changes, only the way expressions are stored and found.
Scalar evolution analyzes loops only. A later patch computes expressions
for whole subprogs, to find the registers that are linked at a conditional
jump:
j = i + 1;
if (i > 100)
return 0;
use(buf[j]);
Then every prog pays for it, with a loop or without, and 1.8 expressions
are constructed per insn, not 0.13. Two things that a few loops do not feel
would be a third of that cost:
- Per expression: expressions are hash consed, and each bucket of exprs_ht
is an array of ids of its own. That is a kvrealloc(), charged to the
memcg, and a kvfree() for every second expression. The hash table has
max(256, len / 4) buckets and never grows.
- Per loop: the analysis starts with a lookup of (REG r) for all
267 registers and stack slots. Each subprog would do the same. The first
one constructs them: 'r0 = 0; exit' would take 180 allocations.
Chain the ids of a bucket through exprs_next[], an array next to exprs[].
exprs_ht[] grows with exprs[]. A load has these three arrays, which double.
(REG r) has the fixed id 2 + r and is not in the hash table.
What it would cost to compute expressions for all code of the selftests,
Cilium, sched_ext, katran, bpfjailer and programs in Rust. The last two
rows are against loops only:
before after
allocations in add_expr() per load 471 6
scev.c for 'r0 = 0; exit' 12.1 us 1.1 us
scev.c for each more subprog 3.6 us 0.7 us
scev.c per insn 485 ns 291 ns
kernel instructions (perf stat) +6.2 % +4.2 %
load time (veristat) +8.5 % +5.7 %
The allocations go with the chains, the cost per subprog with the fixed
ids, the cost of 'r0 = 0; exit' with both: the first lookups of (REG r)
fill the buckets. Not joining dead registers takes most of what is left.
The time in scev.c is linear in the number of insns before and after, so
small progs gain most: 12 % of the load time up to 100 insns, 3 % from
1000 to 10000, 2 % above.
With loops only, as today, the kernel spends 0.3 % fewer instructions on
loading these progs.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/scev.c | 125 +++++++++++++++++++++++-----------------------
1 file changed, 62 insertions(+), 63 deletions(-)
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index 1cb4bc7c0f96..8f8ca69482ca 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -14,6 +14,8 @@
#define REGS_NUM BPF_SCEV_REGS_NUM
#define UNKNOWN_EXPR_ID 0
#define OPAQUE_EXPR_ID 1
+/* (REG r). UNKNOWN, OPAQUE and (REG r) have fixed ids and are not in the hash table. */
+#define REG_EXPR_ID(r) (2 + (r))
/*
* BPF instructions use 'code', 'src_reg', 'off' and 'imm' fields for instruction encoding.
@@ -58,12 +60,6 @@ struct expr {
};
};
-struct expr_bucket {
- u32 cnt;
- u32 cap;
- u32 ids[];
-};
-
struct env {
bool empty;
u32 reg2expr[REGS_NUM];
@@ -96,8 +92,11 @@ struct scev {
* Expressions are identified by id, exprs_ht ensures that
* each expression exists as a unique instance.
* This allows for fast equivalence check: id1 === id2.
+ * exprs_ht[hash] is the first id of a chain, exprs_next[id] is the next one, 0 ends it.
+ * Both have exprs_cap elements, like exprs[].
*/
- struct expr_bucket **exprs_ht;
+ u32 *exprs_ht;
+ u32 *exprs_next;
struct scev_worklist worklist;
struct insn_envs **envs;
struct expr *exprs;
@@ -106,7 +105,6 @@ struct scev {
* instruction was visited on a current pass.
*/
u32 *discovered;
- u32 exprs_ht_cnt;
int exprs_cnt;
int exprs_cap;
int envs_cnt;
@@ -125,44 +123,58 @@ static int expr_eq(struct expr *a, struct expr *b)
return a->op == b->op && a->imm == b->imm;
}
-static int add_expr(struct scev *scev, struct expr e)
+static void exprs_ht_add(struct scev *scev, u32 id)
+{
+ u32 *bucket = &scev->exprs_ht[expr_hash(&scev->exprs[id]) & (scev->exprs_cap - 1)];
+
+ scev->exprs_next[id] = *bucket;
+ *bucket = id;
+}
+
+/* @cap is a power of two. No warning for a size that kvmalloc() rejects. */
+static int grow_exprs(struct scev *scev, u32 cap)
{
- struct expr_bucket *bucket;
- u32 i, id, hash, new_cap;
+ gfp_t gfp = GFP_KERNEL_ACCOUNT | __GFP_NOWARN;
void *tmp;
+ u32 id;
- hash = expr_hash(&e) & (scev->exprs_ht_cnt - 1);
- bucket = scev->exprs_ht[hash];
+ tmp = kvrealloc(scev->exprs, array_size(cap, sizeof(*scev->exprs)), gfp);
+ if (!tmp)
+ return -ENOMEM;
+ scev->exprs = tmp;
+ tmp = kvrealloc(scev->exprs_next, array_size(cap, sizeof(u32)), gfp);
+ if (!tmp)
+ return -ENOMEM;
+ scev->exprs_next = tmp;
+ tmp = kvrealloc(scev->exprs_ht, array_size(cap, sizeof(u32)), gfp);
+ if (!tmp)
+ return -ENOMEM;
+ scev->exprs_ht = tmp;
+ scev->exprs_cap = cap;
+ memset(scev->exprs_ht, 0, array_size(cap, sizeof(u32)));
+ for (id = REG_EXPR_ID(REGS_NUM); id < scev->exprs_cnt; id++)
+ exprs_ht_add(scev, id);
+ return 0;
+}
- if (bucket) {
- for (i = 0; i < bucket->cnt; i++) {
- id = bucket->ids[i];
- if (expr_eq(&e, &scev->exprs[id]))
- return id;
- }
- }
+static int add_expr(struct scev *scev, struct expr e)
+{
+ u32 id;
- if (!bucket || bucket->cap == bucket->cnt) {
- new_cap = bucket ? bucket->cap * 2 : 4;
- bucket = kvrealloc(bucket, sizeof(*bucket) + sizeof(u32) * new_cap, GFP_KERNEL_ACCOUNT | __GFP_ZERO);
- if (!bucket)
- return -ENOMEM;
- scev->exprs_ht[hash] = bucket;
- bucket->cap = new_cap;
- }
+ if (e.op == REG)
+ return REG_EXPR_ID(e.params[0]);
- if (scev->exprs_cnt == scev->exprs_cap) {
- new_cap = scev->exprs_cap ? scev->exprs_cap * 2 : 256;
- tmp = kvrealloc(scev->exprs, sizeof(struct expr) * new_cap, GFP_KERNEL_ACCOUNT);
- if (!tmp)
- return -ENOMEM;
- scev->exprs = tmp;
- scev->exprs_cap = new_cap;
- }
+ id = scev->exprs_ht[expr_hash(&e) & (scev->exprs_cap - 1)];
+ for (; id; id = scev->exprs_next[id])
+ if (expr_eq(&e, &scev->exprs[id]))
+ return id;
+
+ if (scev->exprs_cnt == scev->exprs_cap && grow_exprs(scev, scev->exprs_cap * 2))
+ return -ENOMEM;
id = scev->exprs_cnt++;
scev->exprs[id] = e;
- bucket->ids[bucket->cnt++] = id;
+ exprs_ht_add(scev, id);
return id;
}
@@ -182,11 +194,6 @@ static int expr1(struct scev *scev, u32 op, int a)
return expr2(scev, op, a, 0);
}
-static int expr0(struct scev *scev, u32 op)
-{
- return expr2(scev, op, 0, 0);
-}
-
static int imm_expr(struct scev *scev, s64 value)
{
return add_expr(scev, (struct expr){ .op = IMM, .imm = value });
@@ -576,10 +583,9 @@ static int replace_reg(struct scev *scev, struct env *e, u32 reg, int id)
return 0;
}
-static int setup_initial_loop_env(struct bpf_verifier_env *env, struct env *e, int insn_idx)
+static void setup_initial_loop_env(struct env *e)
{
- struct scev *scev = env->scev;
- int i, err;
+ int i;
/*
* Initial assignments are of form rX -> rX are needed as seeds for subsequent
@@ -587,12 +593,8 @@ static int setup_initial_loop_env(struct bpf_verifier_env *env, struct env *e, i
* join() will overwrite the them. compute_header_scevs() accounts for register
* states at loop entry.
*/
- for (i = 0; i < REGS_NUM; i++) {
- err = replace_reg(scev, e, i, expr1(scev, REG, i));
- if (err)
- return err;
- }
- return 0;
+ for (i = 0; i < REGS_NUM; i++)
+ e->reg2expr[i] = REG_EXPR_ID(i);
}
static void forget_call_regs(struct env *e)
@@ -1404,9 +1406,7 @@ static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header)
header_env = get_loop_env(scev, cur_header, cur_header);
if (!header_env)
goto nomem;
- err = setup_initial_loop_env(env, header_env, cur_header);
- if (err)
- goto out;
+ setup_initial_loop_env(header_env);
err = worklist_push(env, cur_header, cur_header);
if (err)
goto out;
@@ -1966,10 +1966,8 @@ void bpf_free_scev(struct bpf_verifier_env *env)
kfree(envs->entries[j].env);
kfree(envs);
}
- if (scev->exprs_ht)
- for (i = 0; i < scev->exprs_ht_cnt; i++)
- kvfree(scev->exprs_ht[i]);
kvfree(scev->exprs_ht);
+ kvfree(scev->exprs_next);
kvfree(scev->worklist.data);
kvfree(scev->envs);
kvfree(scev->exprs);
@@ -1981,6 +1979,7 @@ void bpf_free_scev(struct bpf_verifier_env *env)
int bpf_init_scev(struct bpf_verifier_env *env)
{
struct scev *scev;
+ u32 r;
scev = kzalloc(sizeof(struct scev), GFP_KERNEL_ACCOUNT);
if (!scev)
@@ -1988,13 +1987,13 @@ int bpf_init_scev(struct bpf_verifier_env *env)
env->scev = scev;
bpf_mark_reg_known_scalar(&scev->zero_reg, 0);
bpf_mark_reg_unknown(&scev->unknown_reg);
- /* A power of two for hash & (exprs_ht_cnt - 1)` indexing. */
- scev->exprs_ht_cnt = roundup_pow_of_two(max(256U, DIV_ROUND_UP(env->prog->len, 4)));
- scev->exprs_ht = kvcalloc(scev->exprs_ht_cnt, sizeof(*scev->exprs_ht), GFP_KERNEL_ACCOUNT);
- if (!scev->exprs_ht)
- goto nomem;
- if (expr0(scev, UNKNOWN) < 0 || expr0(scev, OPAQUE) < 0)
+ if (grow_exprs(scev, roundup_pow_of_two(REG_EXPR_ID(REGS_NUM))))
goto nomem;
+ scev->exprs[UNKNOWN_EXPR_ID] = (struct expr){ .op = UNKNOWN };
+ scev->exprs[OPAQUE_EXPR_ID] = (struct expr){ .op = OPAQUE };
+ for (r = 0; r < REGS_NUM; r++)
+ scev->exprs[REG_EXPR_ID(r)] = (struct expr){ .op = REG, .params = { r, 0 } };
+ scev->exprs_cnt = REG_EXPR_ID(REGS_NUM);
scev->envs = kvcalloc(env->prog->len, sizeof(*scev->envs), GFP_KERNEL_ACCOUNT);
scev->discovered = kvcalloc(env->prog->len, sizeof(*scev->discovered), GFP_KERNEL_ACCOUNT);
if (!scev->envs || !scev->discovered)
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 04/29] bpf: Don't join dead registers in scalar evolution
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (2 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 03/29] bpf: Construct scalar evolution expressions without allocations Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 8:27 ` bot+bpf-ci
2026-11-11 5:13 ` [PATCH bpf-next v2 05/29] bpf: Simplify scalar evolution expressions at construction Alexei Starovoitov
` (24 subsequent siblings)
28 siblings, 1 reply; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
This is about the cost of scalar evolution: the work per join point. The
time in scev.c goes down by a factor of 1.7, the number of expressions
by 2.3. It is what pruned SSA does: no phi for a dead register. Expressions
change, but only those that have no users: a register or stack slot that is
dead at a join point is '?' there.
join() constructs (any a b), which stands for a phi, for each register and
stack slot that has distinct expressions on the two edges. That is
a traversal of both and two or three calls of add_expr(). In loops it is
rare. A later patch computes expressions for whole subprogs. There
every 11th insn is a join point, and 25 of the 267 registers and stack
slots differ. One of them is live. The others:
- Branches leave temporaries in r0 - r5 and in the slots of their spills
and buffers. Where two branches join, 7 of them are dead.
- A subprog that is out of budget goes to the last exit of the subprog, on
an edge that the verifier adds and the CFG does not have. Nothing is
known on that edge, so every jump to that exit is joined with '?'
in all 267. Only r0 is live there. These are 6 % of the join points and
have 72 % of the dead registers and stack slots.
56 % of all expressions would be constructed for them, by 76 % of the calls
of add_expr().
Ask reg_alive_at() first. '?' is the conservative answer: liveness is used
to lose precision only, never to gain it.
What it would cost to compute expressions for all code of the selftests,
Cilium, sched_ext, katran, bpfjailer and programs in Rust. The last two
rows are against loops only:
before after
expressions per insn 1.79 0.78
calls of add_expr() per insn 6.4 1.5
scev.c per insn 291 ns 170 ns
kernel instructions (perf stat) +4.2 % +1.9 %
load time (veristat) +5.7 % +3.2 %
mem_peak of veristat is 0.7 % less.
An edge to a join point cost up to 267 calls of mk_any(). It costs one per
live register that differs. That is a bound per edge: the time in scev.c is
linear in the number of insns before and after. Progs of 100 to 1000 insns
gain most, 5 % of the load time. Above 10000 insns it is less than 1 %.
With loops only, as today, it makes no difference that can be measured.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/scev.c | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index 8f8ca69482ca..7031ab00a22a 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -989,8 +989,10 @@ next:;
return id;
}
-static int join(struct scev *scev, struct env *acc, struct env *cur)
+/* @acc is the env before the insn at @idx */
+static int join(struct bpf_verifier_env *env, struct env *acc, struct env *cur, int idx)
{
+ struct scev *scev = env->scev;
int i, id;
if (acc->empty) {
@@ -1001,7 +1003,9 @@ static int join(struct scev *scev, struct env *acc, struct env *cur)
for (i = 0; i < REGS_NUM; i++) {
if (!same_exprs(scev, acc->reg2expr[i], cur->reg2expr[i])) {
- id = mk_any(scev, acc->reg2expr[i], cur->reg2expr[i]);
+ /* no phi for a dead register: pruned SSA */
+ id = reg_alive_at(env, i, idx) ?
+ mk_any(scev, acc->reg2expr[i], cur->reg2expr[i]) : UNKNOWN_EXPR_ID;
if (id < 0)
return id;
acc->reg2expr[i] = id;
@@ -1339,7 +1343,7 @@ static int join_successor(struct bpf_verifier_env *env, int cur_header, int succ
return -ENOMEM;
if (log_level2)
memcpy(old_env, succ_env, sizeof(*old_env));
- err = join(scev, succ_env, cur_env);
+ err = join(env, succ_env, cur_env, succ_idx);
if (err)
return err;
err = worklist_push(env, cur_header, succ_idx);
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* Re: [PATCH bpf-next v2 04/29] bpf: Don't join dead registers in scalar evolution
2026-11-11 5:13 ` [PATCH bpf-next v2 04/29] bpf: Don't join dead registers in scalar evolution Alexei Starovoitov
@ 2026-11-11 8:27 ` bot+bpf-ci
0 siblings, 0 replies; 36+ messages in thread
From: bot+bpf-ci @ 2026-11-11 8:27 UTC (permalink / raw)
To: alexei.starovoitov, bpf
Cc: daniel, andrii, eddyz87, memxor, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 5323 bytes --]
> bpf: Don't join dead registers in scalar evolution
TL;DR:
- Dead-register pruning in scalar evolution join() may lose precision for live
stack slots after must-write probe reads, contradicting the commit message
- Liveness may be queried at the wrong index for irreducible nested loops,
possibly causing false rejections
> diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
> --- a/kernel/bpf/scev.c
> +++ b/kernel/bpf/scev.c
[ ... ]
> @@ -1001,7 +1003,9 @@ next:;
>
> for (i = 0; i < REGS_NUM; i++) {
> if (!same_exprs(scev, acc->reg2expr[i], cur->reg2expr[i])) {
> - id = mk_any(scev, acc->reg2expr[i], cur->reg2expr[i]);
> + /* no phi for a dead register: pruned SSA */
> + id = reg_alive_at(env, i, idx) ?
> + mk_any(scev, acc->reg2expr[i], cur->reg2expr[i]) : UNKNOWN_EXPR_ID;
The commit message says "Expressions change, but only those that have no
users". Is that accurate for stack slots?
reset_scevs_at_indirect_writes() reads the old expression of a slot before
overwriting it:
mask = bpf_may_write_mask(env, idx);
opaque = bpf_helper_call(insn) && is_probe_read_helper(insn->imm);
for_each_set_bit(spi, mask, MAX_BPF_STACK_SLOTS) {
reg = MAX_BPF_REG + spi;
if (cur_env->reg2expr[reg] == UNKNOWN_EXPR_ID)
continue;
replace_reg(scev, cur_env, reg, opaque ? OPAQUE_EXPR_ID : UNKNOWN_EXPR_ID);
}
Consider a privileged program calling bpf_probe_read_kernel(fp-16, 16, ptr)
with a constant size. bpf_helper_stack_access_bytes() reports a must_write,
so the slot is dead at every join before the call. Take a loop where the
slot is read at the top of the iteration and written on only one of two
branches, and the branches join at J before the probe read.
Before this patch the slot at J was (any opaque fpN), and the probe read
turned it into OPAQUE. Now it is '?' at J, the UNKNOWN_EXPR_ID check above
skips it, and it stays '?' after the helper even though it now holds freshly
read memory and is live.
That '?' reaches the back edge and the header env. In
bpf_compute_loop_iters(), expr_is_fresh() returns true for OPAQUE but false
for '?', so the slot moves from iters->fresh_regs to iters->forget_regs.
made_from_partial_spill() also returns true for '?'. A non-empty fresh_regs
sets entry->first/entry->apart in verifier.c, whereas without it
bpf_widen_scev_regs() runs at once.
The result is conservative, but it is a precision change for a live value,
not the "no users" change the commit message describes. It can change what
loads or how many states are needed.
Could join() leave dead stack slots alone when the next write is a
must-write, or could reset_scevs_at_indirect_writes() tell a dead-at-join
'?' apart from a real unknown? If the change is intended, could the commit
message say so?
[ ... ]
> @@ -1339,7 +1343,7 @@ static int join_successor(struct bpf_verifier_env *env, int cur_header, int succ
> return -ENOMEM;
> if (log_level2)
> memcpy(old_env, succ_env, sizeof(*old_env));
> - err = join(scev, succ_env, cur_env);
> + err = join(env, succ_env, cur_env, succ_idx);
Is succ_idx the right index for the liveness query here? The new comment on
join() says "@acc is the env before the insn at @idx", but join_successor()
has already remapped succ_idx to the topmost nested header:
succ_idx = nested_header;
...
succ_env = get_loop_env(scev, cur_header, succ_idx);
For a reducible nested loop every entering edge targets that header, so this
is fine. For an irreducible loop nested in a computable outer loop,
compute_loops_in_subprog() marks only the inner loop irreducible, and an
edge from the outer loop body into a secondary entry s of the inner loop is
mapped by topmost_nested_loop() to the header H. It is then joined into H's
env with liveness taken at H, not at s.
A register can be live at s and dead at H, for example when the path through
H writes it before s reads it. The two entering edges then give '?' instead
of (any a b).
That env is used. When H is popped in compute_scev_for_loop(), the nested
loop's masks are pulled up from it:
for_each_set_bit(r, nested_loop->store_base_regs, BPF_SCEV_REGS_NUM)
or_expr_regs(env, cur_env->reg2expr[r], cur_loop->store_base_regs);
for_each_set_bit(r, nested_loop->tested_regs, BPF_SCEV_REGS_NUM)
or_expr_regs(env, cur_env->reg2expr[r], cur_loop->tested_regs);
For an irreducible nested loop both masks are bitmap_fill()ed, so every
register's expression is consulted. With '?', the outer loop's
store_base_regs and tested_regs lose the outer header registers that the
register live at s was computed from.
Missing store_base_regs bits could let bpf_compute_loop_iters() widen a
register that a stack address in the nested loop depends on. Missing
tested_regs bits could wrongly put a sum into loop->linear_regs, which makes
join_place_is_linear()/bpf_reg_widen_range() forget its range. The
or_expr_regs() comment already notes this "may cause false rejections".
Could join() skip the liveness pruning when succ_idx was remapped to an
irreducible nested header, or take liveness at the original successor?
---
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/42982641817
^ permalink raw reply [flat|nested] 36+ messages in thread
* [PATCH bpf-next v2 05/29] bpf: Simplify scalar evolution expressions at construction
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (3 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 04/29] bpf: Don't join dead registers in scalar evolution Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 06/29] bpf: Give SSA names to the values that scalar evolution does not track Alexei Starovoitov
` (23 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A prerequisite of the patches that find linked registers. It is about the
shape of an expression, not about cost: a constant offset has to stand at
the root. These patches compare the expressions of registers at every
conditional jump:
r2 = r1
r2 += 1
r2 += 2
if r1 > 100 goto ..
The jump bounds r2 too if r2 is known to be r1 + 3. The scalar id of r2 is
dropped at the second '+='. Its expression has to be (+ r1 3), however many
insns computed it.
Today it is (+ (+ r1 1) 2). transfer() records what each insn does and
folds nothing. simplify() is called later, and only for the registers at
the header, the latches and the exits of a loop. It traverses the whole
expression until nothing changes, and beyond depth 8 its answer is '?'.
Simplify in expr2(). The operands of a new expression are simplified
already, so simplify() handles the root only. Its rules construct their
results with expr2() too, so one rule can enable the next, 8 deep at most:
a prog can nest partial spills as deep as it likes. An expression beyond
that limit is correct, but not as simple as it could be. This is no
canonical form: operands are not ordered. transform_expr(), the repeated
pass over a whole expression, goes away.
What can be seen at once: GCC unrolls the loop of while_true() in loop3.c
8 times, with 'volatile __u64 i' on the stack. Before and after:
*fp-16=(+ (+ ? ?) 7) / ?
loop header at 5, non-linear latch at 76
*fp-16=(+ *fp-16 8) / (linear *fp-16 8)
loop header at 5, header_count is [0..536870912]
The loop gets an iteration count (header_count: how often the header is
executed), 4406 -> 150 insns processed. And two edges that bring the same
value, 'r6 += 1; r6 += 1' and 'r6 += 2', bring the same expression to
a join point. No verdict changes in the selftests, Cilium, sched_ext,
katran, bpfjailer and the programs in Rust. A verdict can change where
a loop gets an iteration count: it is summarized then, not unrolled. That
is how long loops load, and it is less precise for a register that has no
SCEV.
New rules. (+ x 0) is x. A fill that is not wider than the spill gets the
low bits of the spilled value: (sext8 (spill16 x)) is (sext8 x). Only
(zext32 (spill32 x)) had that. Little endian only, as before. No rule drops
a register from an expression: collect_tested_regs() and
collect_store_base_regs() see simplified expressions now.
The level 2 log has (+ r6 -2) for 'r6 -= 2' and 42 for 'w1 = 42'.
A by-product: the kernel spends 0.8 % fewer instructions (perf stat) on
loading the selftests, Cilium, sched_ext, katran, bpfjailer and programs in
Rust.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/scev.c | 112 +++++++++++-------
.../selftests/bpf/progs/verifier_scev.c | 20 +++-
2 files changed, 84 insertions(+), 48 deletions(-)
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index 7031ab00a22a..4f02cd62305c 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -109,6 +109,7 @@ struct scev {
int exprs_cap;
int envs_cnt;
int stack_sz;
+ int simplify_depth;
struct expr_stack_elt expr_stack[EXPR_STACK_DEPTH];
u32 ids_buf[EXPR_STACK_DEPTH];
};
@@ -178,13 +179,30 @@ static int add_expr(struct scev *scev, struct expr e)
return id;
}
+static int simplify(struct scev *scev, u32 id);
+
+/*
+ * An expression is simplified at construction. Its params are simplified already, so simplify()
+ * rewrites the root only, whatever the depth of the expression.
+ * The rules construct expressions too, and one rule can enable the next: the depth of (zext32
+ * (spill32 (+ (spill32 (+ ..) 1)) 1)) is up to the prog. Beyond depth EXPR_STACK_DEPTH an
+ * expression is correct, but not simplified.
+ */
static int expr2(struct scev *scev, u32 op, int a, int b)
{
+ int id;
+
if (a < 0)
return a;
if (b < 0)
return b;
- return add_expr(scev, (struct expr){ .op = op, .params = {a, b} });
+ id = add_expr(scev, (struct expr){ .op = op, .params = {a, b} });
+ if (id < 0 || scev->simplify_depth == EXPR_STACK_DEPTH)
+ return id;
+ scev->simplify_depth++;
+ id = simplify(scev, id);
+ scev->simplify_depth--;
+ return id;
}
static int expr1(struct scev *scev, u32 op, int a)
@@ -251,14 +269,47 @@ static bool is_zext32(struct scev *scev, int id, u32 *left)
return is_unop(scev, ZEXT32, id, left);
}
-/* The low half of the slot is the low half of @left. The other half is not known. */
-static bool is_spill32(struct scev *scev, int id, u32 *left)
+/* (zextN x), (sextN x) and (spillN x) depend on the low N bits of x only */
+static u32 op_low_bits(u32 op)
+{
+ switch (op) {
+ case ZEXT8:
+ case SEXT8:
+ case SPILL8:
+ return 8;
+ case ZEXT16:
+ case SEXT16:
+ case SPILL16:
+ return 16;
+ case ZEXT32:
+ case SEXT32:
+ case SPILL32:
+ return 32;
+ }
+ return 0;
+}
+
+/* The low @bits bits of the slot are those of @left. The other bits are not known. */
+static bool is_spill(struct scev *scev, int id, u32 *bits, u32 *left)
{
+ u32 op = scev->exprs[id].op;
+
#ifdef __BIG_ENDIAN
/* '*(u32 *)(r10 - 8) = rX' writes the high half, see mk_fill() */
return false;
#endif
- return is_unop(scev, SPILL32, id, left);
+ if (op != SPILL8 && op != SPILL16 && op != SPILL32)
+ return false;
+ *bits = op_low_bits(op);
+ *left = scev->exprs[id].params[0];
+ return true;
+}
+
+static bool is_spill32(struct scev *scev, int id, u32 *left)
+{
+ u32 bits;
+
+ return is_spill(scev, id, &bits, left) && bits == 32;
}
static bool is_any(struct scev *scev, int id, u32 *left, u32 *right)
@@ -1579,29 +1630,10 @@ static int transform_expr_once(struct scev *scev, u32 lvl, u32 root, void *priv,
return id < 0 ? id : fn(scev, id, priv);
}
-static int transform_expr(struct scev *scev, u32 root, void *priv,
- int (*fn)(struct scev *scev, u32 id, void *priv))
-{
- int i = 0, id_old, id_new = root;
-
- do {
- /*
- * The depth limit is somewhat arbitrary and is essentially a failsafe.
- * oscillation should not really happen. Still, it seems harsh to error out.
- */
- if (i++ == 4 * EXPR_STACK_DEPTH)
- return UNKNOWN_EXPR_ID;
- id_old = id_new;
- id_new = transform_expr_once(scev, 0, id_old, priv, fn);
- if (id_new < 0)
- return id_new;
- } while (id_old != id_new);
- return id_new;
-}
-
-static int simplify(struct scev *scev, u32 id, void *priv)
+static int simplify(struct scev *scev, u32 id)
{
int a, b, l, r, t, base, slope;
+ u32 bits, op = scev->exprs[id].op;
s64 imm1, imm2;
/* (- x imm) -> (+ x -imm) */
@@ -1609,6 +1641,10 @@ static int simplify(struct scev *scev, u32 id, void *priv)
is_imm(scev, r, &imm1) && imm1 != S64_MIN)
return expr2(scev, BPF_ADD, l, imm_expr(scev, -imm1));
+ /* (+ x 0) -> x */
+ if (is_add(scev, id, &l, &r) && is_imm(scev, r, &imm1) && !imm1)
+ return l;
+
/* (+ (+ x imm) imm) -> (+ x imm) */
if (is_add(scev, id, &l, &r) &&
is_imm(scev, r, &imm2) &&
@@ -1658,12 +1694,14 @@ static int simplify(struct scev *scev, u32 id, void *priv)
return expr1(scev, SPILL32, expr2(scev, BPF_ADD, a, b));
/*
- * 'rX = *(u32 *)(r10 - 8)' loads what was stored:
+ * 'rX = *(u32 *)(r10 - 8)' fills what was spilled, a narrower fill the low bits of it:
* (zext32 (spill32 $a)) -> (zext32 $a)
+ * (sext8 (spill16 $a)) -> (sext8 $a)
* (zext32 (+ (spill32 $a) $b)) -> (zext32 (+ $a $b))
*/
- if (is_zext32(scev, id, &t) && is_spill32(scev, t, &a))
- return expr1(scev, ZEXT32, a);
+ if (op_low_bits(op) && is_spill(scev, scev->exprs[id].params[0], &bits, &a) &&
+ op_low_bits(op) <= bits)
+ return expr1(scev, op, a);
if (is_zext32(scev, id, &t) && is_add(scev, t, &l, &b) && is_spill32(scev, l, &a))
return expr1(scev, ZEXT32, expr2(scev, BPF_ADD, a, b));
@@ -1692,10 +1730,7 @@ static int compute_header_scevs(struct bpf_verifier_env *env, struct env *header
* ... at exit r1 is in {7,42} ...
*/
for (ra = 0; ra < REGS_NUM; ra++) {
- ra_expr = transform_expr(scev, header_env->reg2expr[ra], NULL, simplify);
- if (ra_expr < 0)
- return ra_expr;
- header_env->reg2expr[ra] = ra_expr;
+ ra_expr = header_env->reg2expr[ra];
/* rA = (+ rA IMM) -> (linear rA IMM) */
if (is_add(scev, ra_expr, &l, &r) &&
is_reg(scev, l, &rb) &&
@@ -1843,17 +1878,8 @@ static int compute_insn_scevs(struct bpf_verifier_env *env, struct env *eheader,
int id, reg;
for (reg = 0; reg < REGS_NUM; reg++) {
- /*
- * Simplify first. 'store, load, test' of a 32-bit counter on the stack with the
- * equation of the header in it is deeper than EXPR_STACK_DEPTH.
- */
- id = transform_expr(scev, einsn->reg2expr[reg], NULL, simplify);
- if (id < 0)
- return id;
- id = transform_expr_once(scev, 0, id, eheader->reg2scev, instantiate_header_scevs);
- if (id < 0)
- return id;
- id = transform_expr(scev, id, NULL, simplify);
+ id = transform_expr_once(scev, 0, einsn->reg2expr[reg], eheader->reg2scev,
+ instantiate_header_scevs);
if (id < 0)
return id;
einsn->reg2scev[reg] = id;
diff --git a/tools/testing/selftests/bpf/progs/verifier_scev.c b/tools/testing/selftests/bpf/progs/verifier_scev.c
index f2a53fe5f6ad..3d73a7107cba 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scev.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scev.c
@@ -324,10 +324,11 @@ __msg("r1 += 2 {{.*}}; r1 r6 -> (+ r6 2)")
__msg("r1 += r7 {{.*}}; r1 r6 -> (+ r6 r7)")
__msg("w1 += 2 {{.*}}; r1 r6 -> (zext32 (+ r6 2))")
__msg("w1 += w7 {{.*}}; r1 r6 -> (zext32 (+ r6 r7))")
-__msg("r1 -= 2 {{.*}}; r1 r6 -> (- r6 2)")
+__msg("r1 -= 2 {{.*}}; r1 r6 -> (+ r6 -2)")
__msg("r1 -= r7 {{.*}}; r1 r6 -> (- r6 r7)")
-__msg("w1 -= 2 {{.*}}; r1 r6 -> (zext32 (- r6 2))")
+__msg("w1 -= 2 {{.*}}; r1 r6 -> (zext32 (+ r6 -2))")
__msg("w1 -= w7 {{.*}}; r1 r6 -> (zext32 (- r6 r7))")
+__msg("r1 -= 2 {{.*}}; r1 (+ r6 2) -> r6")
__msg("r1 *= 2 {{.*}}; r1 r6 -> (* r6 2)")
__msg("r1 *= r7 {{.*}}; r1 r6 -> (* r6 r7)")
__msg("w1 *= 2 {{.*}}; r1 r6 -> (zext32 (* r6 2))")
@@ -375,7 +376,7 @@ __msg("w1 s>>= w7 {{.*}}; r1 r6 -> (zext32 (s>> (sext32 r6) (& r7 31)))")
__msg("r1 = -r1 {{.*}}; r1 r6 -> (- r6)")
__msg("w1 = -w1 {{.*}}; r1 r6 -> (zext32 (- r6))")
__msg("r1 = 42 {{.*}}; r1 r6 -> 42")
-__msg("w1 = 42 {{.*}}; r1 r6 -> (zext32 42)")
+__msg("w1 = 42 {{.*}}; r1 r6 -> 42")
__msg("r1 = r7 {{.*}}; r1 r6 -> r7")
__msg("w1 = w7 {{.*}}; r1 r6 -> (zext32 r7)")
__msg("r1 = (s8)r6 {{.*}}; r1 r6 -> (sext8 r6)")
@@ -417,6 +418,7 @@ __naked void alu_ops_exprs(void)
ALU_OP("r1 -= r7")
ALU_OP("w1 -= 2")
ALU_OP("w1 -= w7")
+ ALU_OP("r1 += 2; r1 -= 2")
ALU_OP("r1 *= 2")
ALU_OP("r1 *= r7")
ALU_OP("w1 *= 2")
@@ -511,7 +513,7 @@ __msg("*(u16 *)(r10 -8) = r1 {{.*}}; *fp-8 -8 -> (spill16 1)")
__msg("*(u8 *)(r10 -8) = r1 {{.*}}; *fp-8 -8 -> (spill8 1)")
__msg("r1 = *(u64 *)(r10 -8) {{.*}}; r1 1 -> -8")
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
-__msg("r1 = *(u32 *)(r10 -8) {{.*}}; r1 1 -> (zext32 -8)")
+__msg("r1 = *(u32 *)(r10 -8) {{.*}}; r1 1 -> 4294967288")
__msg("r1 = *(u16 *)(r10 -8) {{.*}}; r1 1 -> (zext16 -8)")
__msg("r1 = *(u8 *)(r10 -8) {{.*}}; r1 1 -> (zext8 -8)")
__msg("r1 = *(s32 *)(r10 -8) {{.*}}; r1 1 -> (sext32 -8)")
@@ -519,7 +521,7 @@ __msg("r1 = *(s16 *)(r10 -8) {{.*}}; r1 1 -> (sext16 -8)")
__msg("r1 = *(s8 *)(r10 -8) {{.*}}; r1 1 -> (sext8 -8)")
__msg("r1 = load_acquire((u8 *)(r10 -8)) {{.*}}; r1 1 -> (zext8 -8)")
__msg("r1 = load_acquire((u16 *)(r10 -8)) {{.*}}; r1 1 -> (zext16 -8)")
-__msg("r1 = load_acquire((u32 *)(r10 -8)) {{.*}}; r1 1 -> (zext32 -8)")
+__msg("r1 = load_acquire((u32 *)(r10 -8)) {{.*}}; r1 1 -> 4294967288")
#else
__msg("r1 = *(u32 *)(r10 -8) {{.*}}; r1 1 -> (>> -8 32)")
__msg("r1 = *(u16 *)(r10 -8) {{.*}}; r1 1 -> (>> -8 48)")
@@ -543,6 +545,11 @@ __msg("r1 = atomic64_xchg((u64 *)(r10 -8), r1) {{.*}}; r1 1 -> _, *fp-8 -8 -> _"
__msg("r0 = atomic64_cmpxchg((u64 *)(r10 -8), r0, r2) {{.*}}; r0 0 -> _, *fp-8 -8 -> _")
__msg("*(u64 *)(r10 -8) = 42 {{.*}}; *fp-8 -8 -> 42")
__msg("*(u8 *)(r10 -7) = r0 {{.*}}; *fp-8 -8 -> _")
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+/* a fill that is not wider than the spill reads what was spilled */
+__msg("r1 = *(s8 *)(r10 -8) {{.*}}; r1 r7 -> (sext8 r7)")
+__msg("r1 = *(u16 *)(r10 -8) {{.*}}; r1 r7 -> (zext16 (spill8 r7))")
+#endif
__naked void store_load_exprs(void)
{
asm volatile (
@@ -579,6 +586,9 @@ __naked void store_load_exprs(void)
ST_OP("*(u64 *)(r10 - 8) = 42")
ST_OP("*(u8 *)(r10 - 7) = r0")
+ ST_OP("r1 = r7; *(u16 *)(r10 - 8) = r1; r1 = *(s8 *)(r10 - 8)")
+ ST_OP("r1 = r7; *(u8 *)(r10 - 8) = r1; r1 = *(u16 *)(r10 - 8)")
+
"r7 += 1;"
"if r7 < 2 goto 1b;"
"r0 = 0;"
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 06/29] bpf: Give SSA names to the values that scalar evolution does not track
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (4 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 05/29] bpf: Simplify scalar evolution expressions at construction Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 8:27 ` bot+bpf-ci
2026-11-11 5:13 ` [PATCH bpf-next v2 07/29] bpf: Compute scalar evolution expressions for whole subprogs Alexei Starovoitov
` (22 subsequent siblings)
28 siblings, 1 reply; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A prerequisite of the patch that refines linked registers at conditional
jumps: it turns the id of an expression into a value number. No users yet,
no verdict changes.
i = v->idx;
j = i;
if (i > 100)
return 0;
use(buf[j]);
i and j hold the same value. The verifier knows it from the scalar id that
'j = i' assigns to both registers in a state. Scalar evolution could decide
it statically, by comparing two expression ids. But a loaded value is '_',
the result of a call is '_' too, and a value that is not tracked is '?'.
All loads have the same id, so the same id in two registers is not the same
value. Nor at a join point: 'x = c ? 1 : 2' and 'y = d ? 1 : 2' are
(any 1 2).
Give such a value an SSA name, struct expr_def: the def of a register or
stack slot by the insn at idx, or by a phi before the insn at idx, a join
point. forget_reg() makes the defs, join() the phis. struct expr has four
bytes of padding for it. The level 2 log prints '%', the register or stack
slot, '.', idx, and for a phi its operands:
1: call bpf_get_prandom_u32 r0 is %r0.1
2: r6 = r0 r6 is %r0.1: the same value
5: call bpf_get_prandom_u32 r0 is %r0.5
7: join point r0 is %r0.7=phi(_)
r8 is %r8.7=phi(1, 2)
The op of %r0.1 is '_', a trailing '?' says that it is '?'. phi(_) joins
distinct '_', phi(?) distinct '?' or more than 8 values. A def is printed
in full at every use: 'r0 += r8' at 7 is (+ %r0.7=phi(_) %r8.7=phi(1, 2)).
Where it differs from SSA in a compiler:
- Only a value that no expression describes gets a name. With a def, '?'
and '_' are what LLVM calls SCEVUnknown: a leaf that stands for one
value. Without a def '?' is SCEVCouldNotCompute.
- An insn that may write defines too (may_def in liveness.c, a MemoryDef in
MemorySSA): insn 8 may write fp-8, %fp-8.8? is what the slot holds after
it. A nested loop defines at its header what it leaves.
- The operands of a phi are a set, 8 at most, and have no defs:
'if (c) sum += a[i]; else sum += b[i];' is %r6.9=phi((+ r6 _)), not
phi((+ r6 %r2.4), (+ r6 %r2.7)). Every phi is a value number of its own.
A later patch finds the congruent ones.
- Without its backedges and with nested loops collapsed the body of a loop
is acyclic: an insn executes at most once per iteration, so a def is one
value per iteration. A name is unique within the env of one loop.
- In the env of loop H a phi at H merges the backedge values only (the phi
of the single latch that LoopSimplify would make). The header phi itself
is (REG r). 'r6=%r6.1=phi(r6, (+ r6 1)) / ?' at header 1 is not {r6,+,1}:
one backedge keeps r6, the other one adds 1.
def.phi is not only for the log. r0 before an insn and after it are
distinct values, so it is a part of the name. And later patches look
through the op of a def, which the insn at idx computed, but not through
a phi, whose op is what the joined values have in common.
Op and operands stay. A def tells one load from another. The code that
analyzes loops asks how a value is computed, and that is the same on the
two paths above. So strip_defs() takes the defs out of the operands of
a phi, all the way down, and that code matches what it matched.
struct expr::is_vn says that an id is a value number: all registers and
stack slots of an env that have it hold the same value. That is (REG r),
a constant, a def, an operation on such.
28 expected messages of verifier_scev.c follow the notation: 'r7=?' becomes
'r7=%r7.4?', 'r7=(any r7 100)' becomes 'r7=%r7.6=phi(r7, 100)'. The new
test phi_operands_have_no_defs has the sum above.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/scev.c | 339 ++++++++++++++----
.../selftests/bpf/progs/verifier_scev.c | 87 +++--
2 files changed, 333 insertions(+), 93 deletions(-)
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index 4f02cd62305c..5fb31d6be97e 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -52,8 +52,39 @@ enum expr_op {
COPY_SCEV,
};
+/*
+ * An SSA name for a value that no expression describes: the def of the SCEV register 'reg'
+ * - by the insn at 'idx'. An insn that may write defines too (may_def in liveness.c). A nested loop
+ * defines at its header what it leaves.
+ * - by a phi before the insn at 'idx': a join point, a loop header.
+ * Without its backedges and with nested loops collapsed the body of a loop is acyclic: an insn
+ * executes at most once per iteration, so a def is one value per iteration. A name is unique within
+ * the env of one loop.
+ * The operands of a phi are a set, 8 at most; a phi(a, b) among them is expanded. They have no
+ * defs, see strip_defs(): (+ r6 %r2.4) and (+ r6 %r2.7) are one operand, (+ r6 _). Every phi is
+ * a value number of its own.
+ * In the env of loop H a phi at H merges the backedge values only, the header phi itself is
+ * (REG r). In the env of the enclosing loop a phi at H merges the entering edges.
+ */
+struct expr_def {
+ u32 set:1; /* 0: the expression has no def */
+ u32 phi:1;
+ u32 reg:9;
+ u32 idx:21;
+};
+static_assert(REGS_NUM <= 1 << 9 && BPF_COMPLEXITY_LIMIT_INSNS <= 1 << 21);
+
+static const struct expr_def no_def;
+
struct expr {
- u32 op;
+ u16 op;
+ /* A value number: all SCEV registers of an env that have this id hold the same value */
+ bool is_vn;
+ /*
+ * UNKNOWN, OPAQUE and ANY don't describe a value. With a def the id stands for one value,
+ * see struct expr_def. Without a def every use of the id can be another value.
+ */
+ struct expr_def def;
union {
u32 params[2];
s64 imm;
@@ -116,12 +147,19 @@ struct scev {
static u32 expr_hash(struct expr *e)
{
- return jhash_3words(e->op, e->params[0], e->params[1], 0);
+ /* def.phi is not hashed: the def by an insn and the phi before it share a chain */
+ return jhash_3words(e->op, e->params[0], e->params[1],
+ e->def.idx * REGS_NUM + e->def.reg);
+}
+
+static bool same_def(struct expr_def a, struct expr_def b)
+{
+ return a.set == b.set && a.phi == b.phi && a.reg == b.reg && a.idx == b.idx;
}
static int expr_eq(struct expr *a, struct expr *b)
{
- return a->op == b->op && a->imm == b->imm;
+ return a->op == b->op && a->imm == b->imm && same_def(a->def, b->def);
}
static void exprs_ht_add(struct scev *scev, u32 id)
@@ -158,12 +196,41 @@ static int grow_exprs(struct scev *scev, u32 cap)
return 0;
}
+static u32 op_params_num(u32 op);
+
+static bool expr_is_vn(struct scev *scev, struct expr *e)
+{
+ u32 i;
+
+ if (e->def.set)
+ return true;
+ switch (e->op) {
+ case UNKNOWN:
+ case OPAQUE:
+ case ANY:
+ /* a partial spill: the other bits of the slot are not known */
+ case SPILL8:
+ case SPILL16:
+ case SPILL32:
+ /* a function of the iteration number */
+ case LINEAR_SCEV:
+ case COPY_SCEV:
+ return false;
+ }
+ for (i = 0; i < op_params_num(e->op); i++)
+ if (!scev->exprs[e->params[i]].is_vn)
+ return false;
+ return true;
+}
+
static int add_expr(struct scev *scev, struct expr e)
{
u32 id;
if (e.op == REG)
return REG_EXPR_ID(e.params[0]);
+ if (!e.def.set && !e.imm && (e.op == UNKNOWN || e.op == OPAQUE))
+ return e.op == UNKNOWN ? UNKNOWN_EXPR_ID : OPAQUE_EXPR_ID;
id = scev->exprs_ht[expr_hash(&e) & (scev->exprs_cap - 1)];
for (; id; id = scev->exprs_next[id])
@@ -174,6 +241,7 @@ static int add_expr(struct scev *scev, struct expr e)
return -ENOMEM;
id = scev->exprs_cnt++;
+ e.is_vn = expr_is_vn(scev, &e);
scev->exprs[id] = e;
exprs_ht_add(scev, id);
return id;
@@ -217,6 +285,33 @@ static int imm_expr(struct scev *scev, s64 value)
return add_expr(scev, (struct expr){ .op = IMM, .imm = value });
}
+/*
+ * The insn at @idx defines @reg. Without its backedges and with nested loops collapsed the body is
+ * acyclic: an insn executes at most once per iteration, so a def is one value per iteration.
+ */
+static struct expr_def insn_def(int idx, u32 reg)
+{
+ return (struct expr_def){ .set = 1, .reg = reg, .idx = idx };
+}
+
+/* The phi of @reg before the insn at @idx. One value per iteration for the same reason. */
+static struct expr_def phi_def(int idx, u32 reg)
+{
+ return (struct expr_def){ .set = 1, .phi = 1, .reg = reg, .idx = idx };
+}
+
+/* @id with another def */
+static int expr_with_def(struct scev *scev, int id, struct expr_def def)
+{
+ struct expr e;
+
+ if (id < 0)
+ return id;
+ e = scev->exprs[id];
+ e.def = def;
+ return add_expr(scev, e);
+}
+
static bool same_exprs(struct scev *scev, int id_a, int id_b)
{
return id_a == id_b;
@@ -486,27 +581,69 @@ static bool expr_next(struct scev *scev, u32 *id, u32 *order)
return expr_next(scev, id, order);
}
+/*
+ * An SSA name is printed as '%', the SCEV register, '.', the insn. A def is printed in full at
+ * every use: (+ %r8.7=phi(1, 2) 4).
+ * %r0.6 insn 6 defines r0. The value is OPAQUE, '_' without a def
+ * %fp-8.8? insn 8 may write fp-8. The value is UNKNOWN, '?' without a def
+ * %r8.7=phi(1, 2) the phi of r8 before insn 7
+ * %r0.7=phi(_) a phi of distinct OPAQUE values
+ * %r6.9=phi(?) a phi of distinct UNKNOWN values, or of more than 8 values
+ * A name is unique within the env of one loop. In the env of loop H a phi at H merges the backedge
+ * values only, the header phi itself is (REG r): in 'scev at header 1: r6=%r6.1=phi(r6, (+ r6 1))'
+ * one backedge leaves r6 as it is, the other one adds 1.
+ * '%' in front of r or fp is a name, followed by a blank it is BPF_MOD.
+ */
+static void log_def(struct bpf_verifier_env *env, struct expr_def def)
+{
+ if (!def.set)
+ return;
+ if (def.reg < MAX_BPF_REG)
+ bpf_log(&env->log, "%%r%d.%d", def.reg, def.idx);
+ else
+ bpf_log(&env->log, "%%fp%d.%d", (MAX_BPF_REG - def.reg - 1) * 8, def.idx);
+}
+
static void log_expr(struct bpf_verifier_env *env, u32 id)
{
struct bpf_verifier_log *log = &env->log;
+ bool operand, bare, first = false;
struct scev *scev = env->scev;
+ u32 order, d, bare_any = 0;
+ struct expr_def def;
struct expr *expr;
const char *str;
- u32 order;
scev->stack_sz = 0;
expr_stack_push(scev, id);
while (expr_next(scev, &id, &order)) {
- if ((order & PRE) && scev->stack_sz > 1)
- bpf_log(log, " ");
expr = &scev->exprs[id];
+ def = expr->def;
+ /* the depth of @id in expr_stack[], expr_next() pops it before POST */
+ d = order & PRE ? scev->stack_sz - 1 : scev->stack_sz;
+ operand = d && (bare_any & BIT(d - 1));
+ /* No '(any' for the operands of phi(a, b). (any a b) among them is expanded. */
+ if (order & PRE) {
+ bare_any &= ~BIT(d);
+ if (expr->op == ANY && (def.set ? def.phi : operand))
+ bare_any |= BIT(d);
+ }
+ bare = bare_any & BIT(d);
+ if ((order & PRE) && (def.set || !bare)) {
+ if (d)
+ bpf_log(log, "%s", !operand ? " " : first ? "" : ", ");
+ log_def(env, def);
+ if (def.phi)
+ bpf_log(log, "=phi(");
+ first = bare;
+ }
switch (expr->op) {
case UNKNOWN:
if (order & PRE)
bpf_log(log, "?");
break;
case OPAQUE:
- if (order & PRE)
+ if ((order & PRE) && (!def.set || def.phi))
bpf_log(log, "_");
break;
case REG:
@@ -518,19 +655,23 @@ static void log_expr(struct bpf_verifier_env *env, u32 id)
bpf_log(log, "%lld", expr->imm);
break;
default:
- if (order & PRE) {
+ if ((order & PRE) && !bare) {
str = op_str(expr->op);
- bpf_log(log, "(");
+ bpf_log(log, def.set && !def.phi ? "=(" : "(");
if (str)
bpf_log(log, "%s", str);
else
bpf_log(log, "bad-expr-op %x", expr->op);
}
- if (order & DEPTH_LIMIT)
- bpf_log(log, "...");
- if (order & POST)
+ if (order & DEPTH_LIMIT) {
+ bpf_log(log, bare && !def.set && !first ? ", ..." : "...");
+ first = false;
+ }
+ if ((order & POST) && !bare)
bpf_log(log, ")");
}
+ if ((order & POST) && def.phi)
+ bpf_log(log, ")");
}
}
@@ -570,7 +711,7 @@ static void print_env(struct bpf_verifier_env *env, struct env *e, u32 insn_idx)
log_reg(env, i);
bpf_log(log, "=");
log_expr(env, e->reg2expr[i]);
- if (e->reg2expr[i] != UNKNOWN_EXPR_ID) {
+ if (!is_op(scev, e->reg2expr[i], UNKNOWN)) {
bpf_log(log, " / ");
log_expr(env, e->reg2scev[i]);
}
@@ -634,6 +775,12 @@ static int replace_reg(struct scev *scev, struct env *e, u32 reg, int id)
return 0;
}
+/* The insn at @idx defines @reg. The value is not tracked: @id is UNKNOWN or OPAQUE. */
+static int forget_reg(struct scev *scev, struct env *e, u32 reg, int id, int idx)
+{
+ return replace_reg(scev, e, reg, expr_with_def(scev, id, insn_def(idx, reg)));
+}
+
static void setup_initial_loop_env(struct env *e)
{
int i;
@@ -648,12 +795,20 @@ static void setup_initial_loop_env(struct env *e)
e->reg2expr[i] = REG_EXPR_ID(i);
}
-static void forget_call_regs(struct env *e)
+/* A subprog can return a pair in r0 and r2: a call defines r1 - r5 too if they are live after it */
+static int forget_call_regs(struct bpf_verifier_env *env, struct env *e, int idx)
{
- int i;
+ int i, err;
- for (i = BPF_REG_0; i <= BPF_REG_5; i++)
+ for (i = BPF_REG_0; i <= BPF_REG_5; i++) {
e->reg2expr[i] = UNKNOWN_EXPR_ID;
+ if (idx + 1 >= env->prog->len || !reg_alive_at(env, i, idx + 1))
+ continue;
+ err = forget_reg(env->scev, e, i, UNKNOWN_EXPR_ID, idx);
+ if (err)
+ return err;
+ }
+ return 0;
}
static u32 spill_spi(struct bpf_insn *insn)
@@ -673,16 +828,19 @@ static bool is_spill_off(int off)
off >= -MAX_BPF_STACK_JIT;
}
-static void mark_opaque(struct env *e, int off, int size)
+static int mark_opaque(struct scev *scev, struct env *e, int idx, int off, int size)
{
- int b, spi;
+ int b, spi, err;
for (b = off; b < off + size; b++) {
if (b >= 0 || b < -MAX_BPF_STACK_JIT)
continue;
spi = (-b - 1) / BPF_REG_SIZE;
- e->reg2expr[MAX_BPF_REG + spi] = OPAQUE_EXPR_ID;
+ err = forget_reg(scev, e, MAX_BPF_REG + spi, OPAQUE_EXPR_ID, idx);
+ if (err)
+ return err;
}
+ return 0;
}
static int mk_spill(struct scev *scev, u8 size, int id)
@@ -716,7 +874,7 @@ static int mk_fill(struct scev *scev, int id, u8 code)
return UNKNOWN_EXPR_ID;
}
-static int maybe_store_fp(struct scev *scev, struct env *e, struct bpf_insn *insn, int id)
+static int maybe_store_fp(struct scev *scev, struct env *e, struct bpf_insn *insn, int idx, int id)
{
u8 size = BPF_SIZE(insn->code);
@@ -724,19 +882,15 @@ static int maybe_store_fp(struct scev *scev, struct env *e, struct bpf_insn *ins
return 0;
if (is_spill_off(insn->off))
return replace_reg(scev, e, off_to_reg(insn), mk_spill(scev, size, id));
- mark_opaque(e, insn->off, bpf_size_to_bytes(size));
- return 0;
+ return mark_opaque(scev, e, idx, insn->off, bpf_size_to_bytes(size));
}
-static int maybe_load_fp(struct scev *scev, struct env *e, struct bpf_insn *insn)
+static int maybe_load_fp(struct scev *scev, struct env *e, struct bpf_insn *insn, int idx)
{
- int id;
-
- if (insn->src_reg == BPF_REG_FP && is_spill_off(insn->off))
- id = mk_fill(scev, e->reg2expr[off_to_reg(insn)], insn->code);
- else
- id = OPAQUE_EXPR_ID;
- return replace_reg(scev, e, insn->dst_reg, id);
+ if (insn->src_reg != BPF_REG_FP || !is_spill_off(insn->off))
+ return forget_reg(scev, e, insn->dst_reg, OPAQUE_EXPR_ID, idx);
+ return replace_reg(scev, e, insn->dst_reg,
+ mk_fill(scev, e->reg2expr[off_to_reg(insn)], insn->code));
}
/*
@@ -912,30 +1066,34 @@ static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
imm = env->insn_aux_data[idx + 1].const_reg_vals[dst];
return replace_reg(scev, e, dst, imm_expr(scev, imm));
}
- return maybe_load_fp(scev, e, insn);
+ return maybe_load_fp(scev, e, insn, idx);
default:
goto mark_dst_unknown;
}
case BPF_STX:
switch (mode) {
case BPF_MEM:
- return maybe_store_fp(scev, e, insn, reg2expr[src]);
+ return maybe_store_fp(scev, e, insn, idx, reg2expr[src]);
case BPF_ATOMIC:
if (insn->imm == BPF_LOAD_ACQ)
- return maybe_load_fp(scev, e, insn);
+ return maybe_load_fp(scev, e, insn, idx);
if (insn->imm == BPF_STORE_REL) {
- return maybe_store_fp(scev, e, insn, reg2expr[src]);
+ return maybe_store_fp(scev, e, insn, idx, reg2expr[src]);
}
/*
* verifier does not track other atomic ops precisely,
* hence mark the results as opaque.
*/
- if (insn->dst_reg == BPF_REG_FP)
- mark_opaque(e, insn->off, bpf_size_to_bytes(BPF_SIZE(insn->code)));
+ if (insn->dst_reg == BPF_REG_FP) {
+ id = mark_opaque(scev, e, idx, insn->off,
+ bpf_size_to_bytes(BPF_SIZE(insn->code)));
+ if (id < 0)
+ return id;
+ }
if (insn->imm == BPF_CMPXCHG)
- return replace_reg(scev, e, BPF_REG_0, OPAQUE_EXPR_ID);
+ return forget_reg(scev, e, BPF_REG_0, OPAQUE_EXPR_ID, idx);
if (insn->imm & BPF_FETCH)
- return replace_reg(scev, e, src, OPAQUE_EXPR_ID);
+ return forget_reg(scev, e, src, OPAQUE_EXPR_ID, idx);
break;
}
break;
@@ -944,15 +1102,17 @@ static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
id = imm_expr(scev, insn->imm);
if (id < 0)
return id;
- return maybe_store_fp(scev, e, insn, id);
+ return maybe_store_fp(scev, e, insn, idx, id);
}
break;
case BPF_JMP:
case BPF_JMP32:
if (opcode == BPF_CALL) {
- forget_call_regs(e);
+ id = forget_call_regs(env, e, idx);
+ if (id < 0)
+ return id;
/* made anew, like what is loaded. The parameter tells the two apart. */
- return replace_reg(scev, e, BPF_REG_0, expr1(scev, OPAQUE, 1));
+ return forget_reg(scev, e, BPF_REG_0, expr1(scev, OPAQUE, 1), idx);
}
/* for non-CALL there are no changes in register states */
break;
@@ -966,12 +1126,11 @@ static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
}
/* map, map value, BTF id, function */
if (BPF_SIZE(insn->code) == BPF_DW)
- return replace_reg(scev, e, dst, OPAQUE_EXPR_ID);
+ return forget_reg(scev, e, dst, OPAQUE_EXPR_ID, idx);
goto mark_dst_unknown;
case BPF_ABS:
case BPF_IND:
- forget_call_regs(e);
- break;
+ return forget_call_regs(env, e, idx);
default:
goto mark_dst_unknown;
}
@@ -985,15 +1144,41 @@ static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
return 0;
mark_dst_unknown:
- reg2expr[dst] = UNKNOWN_EXPR_ID;
- return 0;
+ return forget_reg(scev, e, dst, UNKNOWN_EXPR_ID, idx);
+}
+
+/*
+ * if (c) sum += a[i]; else sum += b[i];
+ * @id without defs: (+ r6 %r2.4) -> (+ r6 _). A def tells one load from another. The code that
+ * analyzes loops asks how a value is computed, and that is the same on both paths.
+ * Beyond depth EXPR_STACK_DEPTH an expression keeps its defs: no user looks there.
+ */
+static int strip_defs(struct scev *scev, u32 id, u32 lvl)
+{
+ struct expr e = scev->exprs[id]; /* snapshot the expr before potential realloc */
+ int a;
+
+ switch (op_params_num(e.op)) {
+ case 0:
+ return e.def.set ? expr_with_def(scev, id, no_def) : id;
+ case 1:
+ if (lvl == EXPR_STACK_DEPTH)
+ return id;
+ return expr1(scev, e.op, strip_defs(scev, e.params[0], lvl + 1));
+ default:
+ if (lvl == EXPR_STACK_DEPTH)
+ return id;
+ a = strip_defs(scev, e.params[0], lvl + 1);
+ return expr2(scev, e.op, a, strip_defs(scev, e.params[1], lvl + 1));
+ }
}
/*
* Construct minimal 'ANY' expression by traversing 'a' and 'b'
- * and accumulating non-duplicated non-ANY entries.
+ * and accumulating non-duplicated non-ANY entries, without their defs.
+ * 'def' is the def of the result, no_def if it has none.
*/
-static int mk_any(struct scev *scev, int a, int b)
+static int mk_any(struct scev *scev, int a, int b, struct expr_def def)
{
struct expr_stack_elt *elt;
struct expr *expr;
@@ -1016,14 +1201,18 @@ static int mk_any(struct scev *scev, int a, int b)
if (expr->op == ANY) {
if (!expr_stack_push(scev, expr->params[1]) ||
!expr_stack_push(scev, expr->params[0]))
- return UNKNOWN_EXPR_ID;
+ return expr_with_def(scev, UNKNOWN_EXPR_ID, def);
} else {
- for (j = 0; j < ids_buf_sz; j++) {
+ /* the operands of an ANY have no defs already */
+ if (id == roots[i])
+ id = strip_defs(scev, id, 0);
+ if (id < 0)
+ return id;
+ for (j = 0; j < ids_buf_sz; j++)
if (same_exprs(scev, scev->ids_buf[j], id))
goto next;
- }
if (ids_buf_sz == ARRAY_SIZE(scev->ids_buf))
- return UNKNOWN_EXPR_ID;
+ return expr_with_def(scev, UNKNOWN_EXPR_ID, def);
scev->ids_buf[ids_buf_sz++] = id;
}
next:;
@@ -1037,7 +1226,7 @@ next:;
if (id < 0)
return id;
}
- return id;
+ return expr_with_def(scev, id, def);
}
/* @acc is the env before the insn at @idx */
@@ -1056,7 +1245,8 @@ static int join(struct bpf_verifier_env *env, struct env *acc, struct env *cur,
if (!same_exprs(scev, acc->reg2expr[i], cur->reg2expr[i])) {
/* no phi for a dead register: pruned SSA */
id = reg_alive_at(env, i, idx) ?
- mk_any(scev, acc->reg2expr[i], cur->reg2expr[i]) : UNKNOWN_EXPR_ID;
+ mk_any(scev, acc->reg2expr[i], cur->reg2expr[i],
+ phi_def(idx, i)) : UNKNOWN_EXPR_ID;
if (id < 0)
return id;
acc->reg2expr[i] = id;
@@ -1078,12 +1268,12 @@ static void forget_modified_regs(struct scev *scev, struct env *acc, struct env
}
/* For a loop H at `header`, mark any register modified as a result of H execution as unknown. */
-static void forget_non_invariants(struct bpf_verifier_env *env, struct env *acc, int header)
+static int forget_non_invariants(struct bpf_verifier_env *env, struct env *acc, int header)
{
struct bpf_insn_aux_data *aux = env->insn_aux_data;
struct bpf_loop *loop = aux[header].loop;
struct scev *scev = env->scev;
- int i, idx, h;
+ int i, idx, h, err;
acc->empty = false;
forget_modified_regs(scev, acc, find_header_env(scev, header));
@@ -1120,6 +1310,15 @@ static void forget_non_invariants(struct bpf_verifier_env *env, struct env *acc,
*/
forget_modified_regs(scev, acc, find_loop_env(scev, header, idx));
}
+ /* the nested loop defines them at its header */
+ for (i = 0; i < REGS_NUM; i++) {
+ if (acc->reg2expr[i] != UNKNOWN_EXPR_ID)
+ continue;
+ err = forget_reg(scev, acc, i, UNKNOWN_EXPR_ID, header);
+ if (err)
+ return err;
+ }
+ return 0;
}
/* Order worklist in reverse post-order. */
@@ -1236,7 +1435,7 @@ static bool is_probe_read_helper(u32 func_id)
* If instruction is an indirect write to stack, invalidate SCEVs for spi's
* that this instruction can touch.
*/
-static void reset_scevs_at_indirect_writes(struct bpf_verifier_env *env, struct env *cur_env, int idx)
+static int reset_scevs_at_indirect_writes(struct bpf_verifier_env *env, struct env *cur_env, int idx)
{
struct bpf_insn *insn = &env->prog->insnsi[idx];
struct scev *scev = env->scev;
@@ -1244,19 +1443,24 @@ static void reset_scevs_at_indirect_writes(struct bpf_verifier_env *env, struct
const unsigned long *mask;
u32 spi, reg;
bool opaque;
+ int err;
/* Direct fp stores are fine. */
if ((class == BPF_ST || class == BPF_STX) && insn->dst_reg == BPF_REG_FP)
- return;
+ return 0;
mask = bpf_may_write_mask(env, idx);
opaque = bpf_helper_call(insn) && is_probe_read_helper(insn->imm);
for_each_set_bit(spi, mask, MAX_BPF_STACK_SLOTS) {
reg = MAX_BPF_REG + spi;
- if (cur_env->reg2expr[reg] == UNKNOWN_EXPR_ID)
- continue;
- replace_reg(scev, cur_env, reg, opaque ? OPAQUE_EXPR_ID : UNKNOWN_EXPR_ID);
+ /* '?' stays '?' */
+ err = forget_reg(scev, cur_env, reg,
+ opaque && !is_op(scev, cur_env->reg2expr[reg], UNKNOWN) ?
+ OPAQUE_EXPR_ID : UNKNOWN_EXPR_ID, idx);
+ if (err)
+ return err;
}
+ return 0;
}
/*
@@ -1486,7 +1690,9 @@ static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header)
/* And what its jumps depend on. */
for_each_set_bit(r, nested_loop->tested_regs, BPF_SCEV_REGS_NUM)
or_expr_regs(env, cur_env->reg2expr[r], cur_loop->tested_regs);
- forget_non_invariants(env, cur_env, idx);
+ err = forget_non_invariants(env, cur_env, idx);
+ if (err)
+ goto out;
if (log_level2)
log_env_changes(env, LOG_AT_TRANSFER, old_env, cur_env, idx);
/*
@@ -1512,7 +1718,9 @@ static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header)
err = transfer(env, cur_env, idx);
if (err)
goto out;
- reset_scevs_at_indirect_writes(env, cur_env, idx);
+ err = reset_scevs_at_indirect_writes(env, cur_env, idx);
+ if (err)
+ goto out;
if (log_level2)
log_env_changes(env, LOG_AT_TRANSFER, old_env, cur_env, idx);
succ = bpf_insn_successors(env, idx);
@@ -1778,7 +1986,7 @@ static int compute_header_scevs(struct bpf_verifier_env *env, struct env *header
}
/* rA = (any 1 2 3 4 ...) -> (any rA 1 2 3 4 ...) */
if (is_any_imm_reg_opaque(scev, ra_expr)) {
- id = mk_any(scev, expr1(scev, REG, ra), ra_expr);
+ id = mk_any(scev, expr1(scev, REG, ra), ra_expr, no_def);
if (id < 0)
return id;
header_env->reg2scev[ra] = id;
@@ -2022,7 +2230,8 @@ int bpf_init_scev(struct bpf_verifier_env *env)
scev->exprs[UNKNOWN_EXPR_ID] = (struct expr){ .op = UNKNOWN };
scev->exprs[OPAQUE_EXPR_ID] = (struct expr){ .op = OPAQUE };
for (r = 0; r < REGS_NUM; r++)
- scev->exprs[REG_EXPR_ID(r)] = (struct expr){ .op = REG, .params = { r, 0 } };
+ scev->exprs[REG_EXPR_ID(r)] = (struct expr){ .op = REG, .is_vn = true,
+ .params = { r, 0 } };
scev->exprs_cnt = REG_EXPR_ID(REGS_NUM);
scev->envs = kvcalloc(env->prog->len, sizeof(*scev->envs), GFP_KERNEL_ACCOUNT);
scev->discovered = kvcalloc(env->prog->len, sizeof(*scev->discovered), GFP_KERNEL_ACCOUNT);
diff --git a/tools/testing/selftests/bpf/progs/verifier_scev.c b/tools/testing/selftests/bpf/progs/verifier_scev.c
index 3d73a7107cba..cab2fe974d44 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scev.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scev.c
@@ -135,7 +135,7 @@ exit_%=: \
SEC("xdp")
__success
__log_level(2)
-__msg("8: (7b) *(u64 *)(r1 +0) = r0 ; *fp-8 (+ *fp-8 1) -> ?")
+__msg("8: (7b) *(u64 *)(r1 +0) = r0 ; *fp-8 (+ *fp-8 1) -> %fp-8.8?")
__naked void indirect_write_invalidates_scev(void)
{
asm volatile (" \
@@ -165,7 +165,7 @@ __noinline int global_noop_counter(__u64 *p)
SEC("xdp")
__success
__log_level(2)
-__msg("8: (85) call pc+{{[0-9]+}} {{.*}}; *fp-8 (+ *fp-8 1) -> ?")
+__msg("8: (85) call pc+{{[0-9]+}} {{.*}}; *fp-8 (+ *fp-8 1) -> %fp-8.8?")
__naked void global_call_invalidates_scev(void)
{
asm volatile (
@@ -242,7 +242,7 @@ SEC("xdp")
__success __retval(0)
__log_level(2)
__msg_next("scev at header 1:")
-__msg_next(" r6=(any r6 (+ r6 1)) / ?")
+__msg_next(" r6=%r6.1=phi(r6, (+ r6 1)) / ?")
__naked void meet_disagrees(void)
{
asm volatile (" \
@@ -261,6 +261,37 @@ __naked void meet_disagrees(void)
: __clobber_all);
}
+/* 'if (c) sum += a; else sum += b;': two loads, one way to compute the sum */
+SEC("socket")
+__success
+__log_level(2)
+__msg("scev at header 3:")
+__msg_next(" r6=%r6.9=phi((+ r6 _)) / ?")
+__naked void phi_operands_have_no_defs(void)
+{
+ asm volatile (" \
+ r9 = r1; \
+ r6 = 0; \
+ r7 = 0; \
+1: \
+ if r7 & 1 goto 2f; \
+ r2 = *(u32 *)(r9 + %[len]); \
+ r6 += r2; \
+ goto 3f; \
+2: \
+ r2 = *(u32 *)(r9 + %[mark]); \
+ r6 += r2; \
+3: \
+ r7 += 1; \
+ if r7 < 10 goto 1b; \
+ r0 = r6; \
+ exit; \
+" :
+ : __imm_const(len, offsetof(struct __sk_buff, len)),
+ __imm_const(mark, offsetof(struct __sk_buff, mark))
+ : __clobber_all);
+}
+
/* Both entry state values for r1 should be accounted for */
SEC("xdp")
__success
@@ -538,13 +569,13 @@ __msg("store_release((u8 *)(r10 -8), r1) {{.*}}; *fp-8 -8 -> (spill8 1)")
__msg("store_release((u16 *)(r10 -8), r1) {{.*}}; *fp-8 -8 -> (spill16 1)")
__msg("store_release((u32 *)(r10 -8), r1) {{.*}}; *fp-8 -8 -> (spill32 1)")
__msg("store_release((u64 *)(r10 -8), r1) {{.*}}; *fp-8 -8 -> 1")
-__msg("lock *(u64 *)(r10 -8) += r1 {{.*}}; *fp-8 -8 -> _")
-__msg("r1 = atomic64_fetch_add((u64 *)(r10 -8), r1) {{.*}}; r1 1 -> _, *fp-8 -8 -> _")
-__msg("r1 = atomic_fetch_add((u32 *)(r10 -8), r1) {{.*}}; r1 1 -> _, *fp-8 -8 -> _")
-__msg("r1 = atomic64_xchg((u64 *)(r10 -8), r1) {{.*}}; r1 1 -> _, *fp-8 -8 -> _")
-__msg("r0 = atomic64_cmpxchg((u64 *)(r10 -8), r0, r2) {{.*}}; r0 0 -> _, *fp-8 -8 -> _")
+__msg("lock *(u64 *)(r10 -8) += r1 {{.*}}; *fp-8 -8 -> %fp-8.{{[0-9]+}}")
+__msg("r1 = atomic64_fetch_add((u64 *)(r10 -8), r1) {{.*}}; r1 1 -> %r1.{{[0-9]+}}, *fp-8 -8 -> %fp-8.{{[0-9]+}}")
+__msg("r1 = atomic_fetch_add((u32 *)(r10 -8), r1) {{.*}}; r1 1 -> %r1.{{[0-9]+}}, *fp-8 -8 -> %fp-8.{{[0-9]+}}")
+__msg("r1 = atomic64_xchg((u64 *)(r10 -8), r1) {{.*}}; r1 1 -> %r1.{{[0-9]+}}, *fp-8 -8 -> %fp-8.{{[0-9]+}}")
+__msg("r0 = atomic64_cmpxchg((u64 *)(r10 -8), r0, r2) {{.*}}; r0 0 -> %r0.{{[0-9]+}}, *fp-8 -8 -> %fp-8.{{[0-9]+}}")
__msg("*(u64 *)(r10 -8) = 42 {{.*}}; *fp-8 -8 -> 42")
-__msg("*(u8 *)(r10 -7) = r0 {{.*}}; *fp-8 -8 -> _")
+__msg("*(u8 *)(r10 -7) = r0 {{.*}}; *fp-8 -8 -> %fp-8.{{[0-9]+}}")
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
/* a fill that is not wider than the spill reads what was spilled */
__msg("r1 = *(s8 *)(r10 -8) {{.*}}; r1 r7 -> (sext8 r7)")
@@ -617,10 +648,10 @@ __success
__log_level(2)
__msg_next("scev at header 1:")
__msg_next(" r6=(+ r6 1) / (linear r6 1)")
-__msg_next(" r7=?")
+__msg_next(" r7=%r7.4?")
__msg_next(" scev at latch 1:")
__msg_next(" r6=(+ r6 1) / (linear r6 1)")
-__msg_next(" r7=?")
+__msg_next(" r7=%r7.4?")
__msg_next("scev at header 4:")
__msg_next(" r7=(+ r7 1) / (linear r7 1)")
__msg_next(" scev at latch 4:")
@@ -657,7 +688,7 @@ __msg_next(" backedge from 6, latch at 4")
__msg_next(" exit from 4 to 1")
__msg("scev at header 1:")
__msg_next(" r6=(+ r6 1) / (linear r6 1)")
-__msg_next(" r7=?")
+__msg_next(" r7=%r7.4?")
__msg_next("scev at header 4:")
__msg_next(" r7=(+ r7 1) / (linear r7 1)")
__msg_next(" scev at latch 4:")
@@ -702,7 +733,7 @@ __msg_next(" backedge from 5, latch at 5")
__msg_next(" exit from 4 to 2")
__msg_next(" exit from 5 to 6")
__msg("scev at header 2:")
-__msg_next(" r6=?")
+__msg_next(" r6=%r6.3?")
__msg_next(" r7=(+ r7 1) / (linear r7 1)")
__msg_next("scev at header 3:")
__msg_next(" r6=(+ r6 1) / (linear r6 1)")
@@ -737,8 +768,8 @@ __log_level(2)
__flag(BPF_F_TEST_STATE_FREQ)
__msg("loop at 3{{$}}")
__msg("scev at header 3:")
-__msg_next(" r6=(any r6 (+ r6 1)) / ?")
-__msg_next(" r7=(any (+ r7 1) 0) / ?")
+__msg_next(" r6=%r6.3=phi(r6, (+ r6 1)) / ?")
+__msg_next(" r7=%r7.3=phi((+ r7 1), 0) / ?")
__msg_next(" r8=(+ r8 1) / (linear r8 1)")
__msg("loop header at 3, can't compute iterations count")
__naked void nested_loops_shared_header(void)
@@ -1539,7 +1570,7 @@ __success __retval(7)
__log_level(2)
__msg("scev at header 1:")
__msg_next("r6=(+ r6 1) / (linear r6 1)")
-__msg_next("*fp-8=(any *fp-8 7) / (any *fp-8 7)")
+__msg_next("*fp-8=%fp-8.3=phi(*fp-8, 7) / (any *fp-8 7)")
__msg("loop header at 1, widening r6 to 0..2 step 1")
__msg("3: (79) r0 = *(u64 *)(r10 -8) ; R0=scalar() R10=fp0 loop_stack=1")
__naked void widen_any_unallocated_slot(void)
@@ -1580,7 +1611,7 @@ __success __retval(7)
__log_level(2)
__msg("scev at header 2:")
__msg_next("r6=(+ r6 1) / (linear r6 1)")
-__msg_next("*fp-8=(any *fp-8 7) / (any *fp-8 7)")
+__msg_next("*fp-8=%fp-8.5=phi(*fp-8, 7) / (any *fp-8 7)")
__msg("loop header at 2, widening r6 to 0..2 step 1")
__msg("3: (79) r1 = *(u64 *)(r10 -8) ; R1=scalar() R10=fp0 fp-8=m???????")
__naked void widen_any_misc_slot(void)
@@ -1683,7 +1714,7 @@ __success
__log_level(2)
__msg("scev at header 4:")
__msg_next(" r6=(+ r6 1) / (linear r6 1)")
-__msg_next(" *fp-8=?")
+__msg_next(" *fp-8=%fp-8.7?")
__msg("loop header at 4, widening r6 to 0..9 step 1")
__naked void widen_subprog_probe_read(void)
{
@@ -2604,7 +2635,7 @@ __msg("loop at 1{{$}}")
__msg("loop at 5, nested in 1, irreducible")
__msg("loop at 11, nested in 5")
__msg("scev at header 1:")
-__msg_next(" r6=?")
+__msg_next(" r6=%r6.5?")
__msg("scev at header 11:")
__msg_next(" r9=(+ r9 1) / (linear r9 1)")
__msg("loop header at 11, widening r9 to 0..2 step 1")
@@ -2851,7 +2882,7 @@ __success
__log_level(2)
__msg("scev at header 2:")
__msg_next(" r6=(+ r6 1) / (linear r6 1)")
-__msg_next(" r7=(any r7 100) / (any r7 100)")
+__msg_next(" r7=%r7.6=phi(r7, 100) / (any r7 100)")
__msg_next(" scev at latch 3:")
__msg_next(" r6=(+ r6 1) / (linear (+ r6 1) 1)")
__msg_next(" r7=r7 / (any r7 100)")
@@ -3126,12 +3157,12 @@ __naked void name(void) \
#define NOT_UNROLLED "widening r6 to 0..0xf423f step 1"
/* 'if (...) cnt++' */
-PARTIAL_SPILL_TEST(partial_spill32_inc, u32, 10, "can't widen *fp-8, expr is (any *fp-8 (spill32 ",
+PARTIAL_SPILL_TEST(partial_spill32_inc, u32, 10, "can't widen *fp-8, expr is %fp-8.{{[0-9]+}}=phi(*fp-8, (spill32 ",
SPILL32_0, "if r6 & 1 goto 2f;" SPILL32_INC "2:")
PARTIAL_SPILL_TEST(partial_spill16_inc, u16, 10, "can't widen *fp-8, expr is (spill16 ",
"*(u16 *)(r10 - 8) = r1;", "r1 = *(u16 *)(r10 - 8); w1 += 1; *(u16 *)(r10 - 8) = r1;")
/* A loop in the loop does it */
-PARTIAL_SPILL_TEST(partial_spill32_inc_in_nested_loop, u32, 10, "can't widen *fp-8, expr is ?",
+PARTIAL_SPILL_TEST(partial_spill32_inc_in_nested_loop, u32, 10, "can't widen *fp-8, expr is %fp-8.{{[0-9]+}}?",
SPILL32_0, "r7 = 0; 2:" SPILL32_INC "r7 += 1; if r7 < 3 goto 2b;")
/* Two slots feed each other */
PARTIAL_SPILL_TEST(partial_spill32_two_slots, u32, 10, "can't widen *fp-8, expr is (spill32 ",
@@ -3296,7 +3327,7 @@ NOT_LINEAR_TEST(zext32_or_nothing, "r7 >>= 8;", "if r8 == 0 goto +1; w7 = w7;",
NOT_LINEAR_TEST(and_64_to_127, "r7 &= 63; r7 |= 64;", "r7 &= 127;", UNROLLED("(& r7 127)"))
/* '>>' does not make a number greater, but iterations one by one tell more: 0 after 8 shifts */
NOT_LINEAR_TEST(rsh_on_one_path, "r7 &= 255;", "if r8 == 0 goto +1; r7 >>= 1;",
- UNROLLED("(any r7 (>> r7 1))"))
+ UNROLLED("%r7.8=phi(r7, (>> r7 1))"))
NOT_LINEAR_TEST(rsh_then_and, "r7 &= 255;", "r7 >>= 1; r7 &= r8;", UNROLLED("(& (>> r7 1) r8)"))
NOT_LINEAR_TEST(lsh_grows, "r7 &= 15;", "r7 <<= 1;", UNROLLED("(<< r7 1)"))
@@ -3458,9 +3489,9 @@ __failure
__log_level(2)
__flag(BPF_F_TEST_STATE_FREQ)
__msg("scev at header 9:")
-__msg_next(" r6=(+ ? 1) / ?")
+__msg_next(" r6=(+ %r6.13? 1) / ?")
__msg_next(" scev at latch 20:")
-__msg_next(" r6=(+ ? 1) / (+ ? 1)")
+__msg_next(" r6=(+ %r6.13? 1) / (+ %r6.13? 1)")
__msg_next("scev at header 13:")
__msg_next(" r9=(+ r9 1) / (linear r9 1)")
__msg_next(" scev at latch 16:")
@@ -3534,7 +3565,7 @@ SEC("xdp")
__success __retval(3)
__log_level(2)
__flag(BPF_F_TEST_STATE_FREQ)
-__msg("r6 = *(u64 *)(r10 -8) {{.*}}; r6 ? -> r6")
+__msg("r6 = *(u64 *)(r10 -8) {{.*}}; r6 %r6.4? -> r6")
__msg("scev at header 1:")
__msg_next(" r6=(+ r6 1) / (linear r6 1)")
__msg_next(" scev at latch 16:")
@@ -3655,7 +3686,7 @@ __success
__log_level(2)
__flag(BPF_F_TEST_STATE_FREQ)
__msg("scev at header 4:")
-__msg_next("r7=(any r7 8) / (any r7 8)")
+__msg_next("r7=%r7.11=phi(r7, 8) / (any r7 8)")
__msg("loop header at 4, can't widen r7, expr is {{.*}}, requires exact stack-offset tracking")
__naked void no_widen_stack_fill_any(void)
{
@@ -4429,7 +4460,7 @@ SEC("socket")
__success __retval(0)
__log_level(2)
__msg("header_count is 100000 ")
-__msg("can't widen r6, expr is _")
+__msg("can't widen r6, expr is %r6.13")
__not_msg("widening r8")
__msg("clamping r8 to 0..0x1869f step 1")
__naked void index_loaded_in_previous_iteration(void)
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* Re: [PATCH bpf-next v2 06/29] bpf: Give SSA names to the values that scalar evolution does not track
2026-11-11 5:13 ` [PATCH bpf-next v2 06/29] bpf: Give SSA names to the values that scalar evolution does not track Alexei Starovoitov
@ 2026-11-11 8:27 ` bot+bpf-ci
0 siblings, 0 replies; 36+ messages in thread
From: bot+bpf-ci @ 2026-11-11 8:27 UTC (permalink / raw)
To: alexei.starovoitov, bpf
Cc: daniel, andrii, eddyz87, memxor, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 2093 bytes --]
> bpf: Give SSA names to the values that scalar evolution does not track
TL;DR: Nits only: comments on insn_def() and log_def() in kernel/bpf/scev.c
repeat explanations from the struct expr_def comment; could they refer to it
instead?
> diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
> --- a/kernel/bpf/scev.c
> +++ b/kernel/bpf/scev.c
[ ... ]
> @@ -217,6 +285,33 @@ static int imm_expr(struct scev *scev, s64 value)
> return add_expr(scev, (struct expr){ .op = IMM, .imm = value });
> }
>
> +/*
> + * The insn at @idx defines @reg. Without its backedges and with nested loops collapsed the body is
> + * acyclic: an insn executes at most once per iteration, so a def is one value per iteration.
> + */
> +static struct expr_def insn_def(int idx, u32 reg)
This isn't a bug, but the "acyclic body, one value per iteration"
explanation is also in the struct expr_def comment, and phi_def() just
says "for the same reason".
Could insn_def() refer to the struct expr_def comment instead of
repeating it?
[ ... ]
> +/*
> + * An SSA name is printed as '%', the SCEV register, '.', the insn. A def is printed in full at
> + * every use: (+ %r8.7=phi(1, 2) 4).
[ ... ]
> + * A name is unique within the env of one loop. In the env of loop H a phi at H merges the backedge
> + * values only, the header phi itself is (REG r): in 'scev at header 1: r6=%r6.1=phi(r6, (+ r6 1))'
> + * one backedge leaves r6 as it is, the other one adds 1.
> + * '%' in front of r or fp is a name, followed by a blank it is BPF_MOD.
> + */
> +static void log_def(struct bpf_verifier_env *env, struct expr_def def)
This isn't a bug, but the "unique within the env of one loop" and "phi at
H merges the backedge values only" sentences are also in the struct
expr_def comment.
Could log_def() point to that comment as well, so the explanation lives in
one place?
---
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/42982641817
^ permalink raw reply [flat|nested] 36+ messages in thread
* [PATCH bpf-next v2 07/29] bpf: Compute scalar evolution expressions for whole subprogs
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (5 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 06/29] bpf: Give SSA names to the values that scalar evolution does not track Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 08/29] bpf: Mark the attr argument of bpf_sys_bpf() as MEM_WRITE Alexei Starovoitov
` (21 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A prerequisite of the patches that use scalar evolution at conditional
jumps. Expressions exist in loops only. Compute them for all code. They
have no users yet: no verdict changes, the prog below is still rejected.
char *buf = bpf_map_lookup_elem(&map, &key); /* 128 bytes */
..
p = buf + i; /* i is a byte */
if (i > 100)
return 0;
*p = buf[0];
The jump bounds i. p was computed before:
invalid access to map value, value_size=128 off=255 size=1
That p is buf + i is a static fact: it holds on every path. Treat the
subprog as the outermost loop, which runs once: compute_scev_for_loop()
with -1 for the header. (REG r) is the entry value of r. A loop is a single
insn for the subprog, as a nested loop is for the enclosing one, so
join_successor(), topmost_nested_loop(), forget_non_invariants() and the
worklist work as they are. The body is acyclic: each basic block is visited
once, and a def is one value per call of the subprog. The prog above, from
the level 2 log after 'Computing SCEV for subprog at 0:':
2: r7 &= 255 r7 is (& %r0.0 255)
8: call bpf_map_lookup_elem#1 r0 is %r0.8
10: r6 = r0
11: r2 = r6
12: r2 += r7 r2 is (+ %r0.8 (& %r0.0 255))
13: if r7 > 0x64 goto pc+2
%r0.8 is the def of r0 by insn 8.
The verifier adds edges that the CFG does not have, see
bpf_head_guard_target(): from may_goto before a loop header, and from
a subprog that is out of budget to its last exit. Nothing is known on them:
join_added_edges() joins '?' at their targets, as bpf_compute_const_regs()
does. A subprog that has a loop with a truncated exit list is not analyzed:
the exits are the successors of the loop as a single insn.
The worklist is in reverse post-order: all predecessors come first. So the
env of a basic block, 2140 bytes, is freed when the block is done, unless
it starts with a header, a latch or an exit. That goes for loops too. Their
envs stayed until the end of bpf_check(), so memory goes down.
To load the selftests, Cilium, sched_ext, katran, bpfjailer and programs in
Rust the kernel spends 1.9 % more instructions (perf stat), and load time
goes up by 2 to 3 % (veristat). mem_peak of veristat goes down by 1.1 % for
the C programs and by 7.3 % for those in Rust.
reg_bounds.c looks for '18: (bf) r0 = r6' and ';' in the log. Scalar
evolution prints that for code outside of loops now, after 't'.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/scev.c | 130 +++++++++++++++---
.../selftests/bpf/prog_tests/reg_bounds.c | 5 +-
.../selftests/bpf/progs/verifier_scev.c | 4 +-
3 files changed, 118 insertions(+), 21 deletions(-)
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index 5fb31d6be97e..a0324fbc875e 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -287,7 +287,8 @@ static int imm_expr(struct scev *scev, s64 value)
/*
* The insn at @idx defines @reg. Without its backedges and with nested loops collapsed the body is
- * acyclic: an insn executes at most once per iteration, so a def is one value per iteration.
+ * acyclic: an insn executes at most once per iteration, so a def is one value per iteration. A
+ * subprog is the outermost loop, which runs once.
*/
static struct expr_def insn_def(int idx, u32 reg)
{
@@ -767,6 +768,25 @@ static struct env *get_loop_env(struct scev *scev, int loop_header, int insn_idx
return e;
}
+/* The env has no users any more */
+static void free_loop_env(struct scev *scev, int loop_header, int insn_idx)
+{
+ struct insn_envs *envs = scev->envs[insn_idx];
+ u32 i;
+
+ for (i = 0; i < envs->cnt; i++) {
+ if (envs->entries[i].loop_header != loop_header)
+ continue;
+ kfree(envs->entries[i].env);
+ envs->entries[i] = envs->entries[--envs->cnt];
+ break;
+ }
+ if (envs->cnt)
+ return;
+ kfree(envs);
+ scev->envs[insn_idx] = NULL;
+}
+
static int replace_reg(struct scev *scev, struct env *e, u32 reg, int id)
{
if (id < 0)
@@ -1337,7 +1357,7 @@ static int worklist_push(struct bpf_verifier_env *env, int loop_header, int idx)
{
struct scev *scev = env->scev;
struct scev_worklist *worklist = &scev->worklist;
- u32 tag = (u32)loop_header + 1;
+ u32 tag = loop_header + 2; /* -1 is a subprog */
if (scev->discovered[idx] == tag)
return 0;
@@ -1621,11 +1641,52 @@ static bool can_compute_loop_scev(const struct bpf_loop *loop)
return !loop->irreducible && !loop->nested_exits_overflow;
}
-static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header)
+/* A loop is a single instruction for the subprog too: no exit list may be truncated. */
+static bool can_compute_subprog_scev(struct bpf_verifier_env *env, struct bpf_subprog_info *sub)
+{
+ struct bpf_loop *loop;
+ int i;
+
+ for (i = sub->start; i < (sub + 1)->start; i++) {
+ loop = env->insn_aux_data[i].loop;
+ if (loop && (loop->exits_overflow || loop->nested_exits_overflow))
+ return false;
+ }
+ return true;
+}
+
+/*
+ * The edges that the verifier adds, see bpf_head_guard_target(): bpf_insn_successors() doesn't have
+ * them. They leave all loops. The env at their source is not known: @cur_env is all '?'. So their
+ * target is a join point with a phi for every live register.
+ */
+static int join_added_edges(struct bpf_verifier_env *env, struct bpf_subprog_info *sub,
+ struct env *old_env, struct env *cur_env)
+{
+ int i, t, err;
+
+ for (i = sub->start; i < (sub + 1)->start; i++) {
+ t = bpf_head_guard_target(env, i);
+ if (t < 0)
+ continue;
+ err = join_successor(env, -1, t, old_env, cur_env);
+ if (err)
+ return err;
+ }
+ return sub->bail_insn < 0 ? 0 : join_successor(env, -1, sub->bail_insn, old_env, cur_env);
+}
+
+/*
+ * @cur_header and @first are a loop header. Or -1 and the first insn of a subprog: the subprog is
+ * the outermost loop, which runs once. (REG r) is the entry value of r, the loops in it are single
+ * instructions.
+ */
+static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header, int first)
{
struct scev_worklist *worklist = &env->scev->worklist;
struct bpf_insn_aux_data *aux = env->insn_aux_data;
- struct bpf_loop *cur_loop = aux[cur_header].loop;
+ struct bpf_loop *cur_loop = cur_header < 0 ? NULL : aux[cur_header].loop;
+ struct bpf_subprog_info *sub = bpf_find_containing_subprog(env, first);
struct env *header_env;
struct scev *scev = env->scev;
struct bpf_loop *nested_loop;
@@ -1633,17 +1694,21 @@ static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header)
struct env *old_env = NULL;
struct bpf_iarray *succ;
bool log_level2 = env->log.level & BPF_LOG_LEVEL2;
- int s, i, err, idx, succ_idx;
+ int s, i, err, idx, succ_idx, start;
u32 r;
if (log_level2)
- bpf_log(&env->log, "Computing SCEV for loop at %d:\n", cur_header);
+ bpf_log(&env->log, "Computing SCEV for %s at %d:\n",
+ cur_loop ? "loop" : "subprog", first);
+
+ if (!cur_loop && !can_compute_subprog_scev(env, sub))
+ return 0;
/*
* For irreducible loops, and for loops nesting a loop with a truncated
* exit list, just assume that everything is clobbered for now.
*/
- if (!can_compute_loop_scev(cur_loop)) {
+ if (cur_loop && !can_compute_loop_scev(cur_loop)) {
header_env = get_loop_env(scev, cur_header, cur_header);
if (!header_env)
return -ENOMEM;
@@ -1662,17 +1727,30 @@ static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header)
if (!old_env)
goto nomem;
}
- header_env = get_loop_env(scev, cur_header, cur_header);
+ header_env = get_loop_env(scev, cur_header, first);
if (!header_env)
goto nomem;
setup_initial_loop_env(header_env);
- err = worklist_push(env, cur_header, cur_header);
+ err = worklist_push(env, cur_header, first);
if (err)
goto out;
+ if (!cur_loop) {
+ /* no backedge overwrites it */
+ header_env->empty = false;
+ err = join_added_edges(env, sub, old_env, cur_env);
+ if (err)
+ goto out;
+ }
while (worklist->nr) {
idx = *min_heap_peek_inline(worklist);
min_heap_pop_inline(worklist, &worklist_cb, env->cfg.postorder_nums);
+ start = idx;
+ if (signal_pending(current)) {
+ err = -EAGAIN;
+ goto out;
+ }
+ cond_resched();
/* join_successor() maps nested-loop entries to their representative header. */
nested_loop = idx != cur_header ? aux[idx].loop : NULL;
@@ -1684,12 +1762,16 @@ static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header)
*/
if (log_level2)
memcpy(old_env, cur_env, sizeof(*old_env));
- /* Pull the nested loop's stack-store base dependencies up. */
- for_each_set_bit(r, nested_loop->store_base_regs, BPF_SCEV_REGS_NUM)
- or_expr_regs(env, cur_env->reg2expr[r], cur_loop->store_base_regs);
- /* And what its jumps depend on. */
- for_each_set_bit(r, nested_loop->tested_regs, BPF_SCEV_REGS_NUM)
- or_expr_regs(env, cur_env->reg2expr[r], cur_loop->tested_regs);
+ if (cur_loop) {
+ /* Pull the nested loop's stack-store base dependencies up. */
+ for_each_set_bit(r, nested_loop->store_base_regs, BPF_SCEV_REGS_NUM)
+ or_expr_regs(env, cur_env->reg2expr[r],
+ cur_loop->store_base_regs);
+ /* And what its jumps depend on. */
+ for_each_set_bit(r, nested_loop->tested_regs, BPF_SCEV_REGS_NUM)
+ or_expr_regs(env, cur_env->reg2expr[r],
+ cur_loop->tested_regs);
+ }
err = forget_non_invariants(env, cur_env, idx);
if (err)
goto out;
@@ -1713,8 +1795,12 @@ static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header)
for (;;) {
if (log_level2)
memcpy(old_env, cur_env, sizeof(*old_env));
- collect_store_base_regs(env, cur_env, idx, cur_loop->store_base_regs);
- collect_tested_regs(env, cur_env, idx, cur_loop->tested_regs);
+ if (cur_loop) {
+ collect_store_base_regs(env, cur_env, idx,
+ cur_loop->store_base_regs);
+ collect_tested_regs(env, cur_env, idx,
+ cur_loop->tested_regs);
+ }
err = transfer(env, cur_env, idx);
if (err)
goto out;
@@ -1739,6 +1825,9 @@ static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header)
goto out;
}
}
+ /* Reverse post-order: all predecessors came first, the env has no users any more */
+ if (!aux[start].need_scev)
+ free_loop_env(scev, cur_header, start);
}
err = 0;
@@ -2146,7 +2235,12 @@ int bpf_compute_scev(struct bpf_verifier_env *env)
idx = postorder[i];
if (!aux[idx].loop)
continue;
- err = compute_scev_for_loop(env, idx);
+ err = compute_scev_for_loop(env, idx, idx);
+ if (err)
+ return err;
+ }
+ for (i = 0; i < env->subprog_cnt; i++) {
+ err = compute_scev_for_loop(env, -1, env->subprog_info[i].start);
if (err)
return err;
}
diff --git a/tools/testing/selftests/bpf/prog_tests/reg_bounds.c b/tools/testing/selftests/bpf/prog_tests/reg_bounds.c
index 09f7930733be..c2079eb64201 100644
--- a/tools/testing/selftests/bpf/prog_tests/reg_bounds.c
+++ b/tools/testing/selftests/bpf/prog_tests/reg_bounds.c
@@ -1290,7 +1290,8 @@ static int parse_range_cmp_log(const char *log_buf, struct case_spec spec,
* 18: (bf) r0 = r6 ; R0=... R6=...
* Different verifier passes may print
* 18: (bf) r0 = r6
- * as well, but never followed by ';'.
+ * as well, but never followed by ';'. Except for scalar evolution:
+ * t 18: (bf) r0 = r6 ; r0 ... -> ...
* The branches are in the log in the order the verifier looked at them.
*/
q = log_buf;
@@ -1299,7 +1300,7 @@ static int parse_range_cmp_log(const char *log_buf, struct case_spec spec,
while (*s == ' ' || *s == '\t')
s++;
- if (*s == ';')
+ if (*s == ';' && (q == log_buf || q[-1] == '\n'))
break;
q = s;
}
diff --git a/tools/testing/selftests/bpf/progs/verifier_scev.c b/tools/testing/selftests/bpf/progs/verifier_scev.c
index cab2fe974d44..6d1b8542f801 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scev.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scev.c
@@ -2377,12 +2377,14 @@ __naked __noinline __used static int returns_zero(void)
* A loop that the compiler unrolls in part has an exit for every 'break' and 'return' in every
* copy of its body. How many there are says nothing about the number of iterations.
* Scalar evolution takes a nested loop for one insn, and its exits are the successors of this insn.
- * The list of exits is not complete: the loop around has no formulas.
+ * The exit list is truncated: the enclosing loop has no SCEVs, the subprog is not analyzed.
*/
SEC("xdp")
__success __log_level(2)
__msg("loop at 1, too many exits, too many exits of a nested loop")
__msg("loop at 2, nested in 1, too many exits")
+__msg("Computing SCEV for subprog at 0:")
+__msg_next("Computing SCEV for subprog at {{[0-9]+}}:")
__msg("loop header at 1, unsupported loop: too many exits of a nested loop")
__msg("loop header at 2, header_count is [0..4] ")
__naked void loop_around_301_exits(void)
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 08/29] bpf: Mark the attr argument of bpf_sys_bpf() as MEM_WRITE
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (6 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 07/29] bpf: Compute scalar evolution expressions for whole subprogs Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 09/29] bpf: Include older frames in a fill through an imprecise stack pointer Alexei Starovoitov
` (20 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A fix of the declaration of a helper. This series needs it: scalar
evolution keeps the expression of a stack slot across a call unless
bpf_may_write_mask() of the call has the slot, and liveness.c computes that
mask from the argument types of the helper.
union bpf_attr attr = { .btf = .., .btf_size = .., .btf_log_level = 1 };
bpf_sys_bpf(BPF_BTF_LOAD, &attr, sizeof(attr));
BPF_BTF_LOAD and BPF_PROG_LOAD store log_true_size into the attr of the
caller. When BPF_PROG_LOAD rejects func_info, line_info or core_relo
records that are larger than it knows, it stores the record size that it
expects. For the helper the attr is a kernel pointer, so
copy_to_bpfptr_offset() writes to the memory of the program.
arg2_type in struct bpf_func_proto bpf_sys_bpf_proto is
ARG_PTR_TO_MEM | MEM_RDONLY. So the verifier keeps what it knows about an
attr on the stack across the call: a slot that held 0 is taken for 0 after
it. bpf_may_write_mask() of the call does not have the slots of the attr.
And an attr in a map that the program cannot write to is accepted.
Say MEM_WRITE. An attr in read-only memory, such as
a 'static const union bpf_attr' in .rodata, is refused now:
"write into map forbidden".
Fixes: 37cce22dbd51 ("bpf: verifier: Refactor helper access type tracking")
Fixes: 216e3cd2f28d ("bpf: Add MEM_RDONLY for helper args that are pointers to rdonly mem.")
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/syscall.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/kernel/bpf/syscall.c b/kernel/bpf/syscall.c
index 9004f2673ccf..04a3b3c80c74 100644
--- a/kernel/bpf/syscall.c
+++ b/kernel/bpf/syscall.c
@@ -6692,7 +6692,7 @@ static const struct bpf_func_proto bpf_sys_bpf_proto = {
.might_sleep = true,
.ret_type = RET_INTEGER,
.arg1_type = ARG_ANYTHING,
- .arg2_type = ARG_PTR_TO_MEM | MEM_RDONLY,
+ .arg2_type = ARG_PTR_TO_MEM | MEM_WRITE,
.arg3_type = ARG_MEM_SIZE,
};
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 09/29] bpf: Include older frames in a fill through an imprecise stack pointer
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (7 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 08/29] bpf: Mark the attr argument of bpf_sys_bpf() as MEM_WRITE Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 10/29] bpf: Detect a dead branch when a range and var_off don't overlap Alexei Starovoitov
` (19 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A fix of liveness.c. This series needs it: scalar evolution keeps the
expression of a stack slot across a call unless bpf_may_write_mask() of the
call has the slot, and this is a write that the mask does not have.
struct scope { struct scope *parent; u64 hits; };
static __noinline int leaf(struct scope *up)
{
for (s = up; s && n < 8; s = s->parent, n++)
s->hits++;
..
}
static __noinline int level2(struct scope *up)
{
struct scope me = { .parent = up };
return leaf(&me) + me.hits;
}
level1() is like level2() and calls it. The program has the first scope and
calls level1(). In leaf(), frame 3, s points to frame 2, then to frame 1:
at the loop header struct arg_track is ARG_IMPRECISE with the mask
BIT(1) | BIT(2). For the load of s->parent arg_track_deref() returns the
pointer that it was given, so for liveness.c s never points to frame 0. The
scope of the program gets neither stack_use nor may_def at the call of
level1(), and the program is rejected:
reading from stack off -16+0 size 8, slot poisoned by dead code elimination
put(cond ? out : &tmp) with out in frame 1, tmp in frame 2 and out->buf in
frame 0 has such a pointer without a loop. That program loads, but the
call in frame 0 has no may_def for buf.
A frame holds pointers to its own stack and to the stacks of its callers:
a pointer to the stack is never spilled into the frame of a caller. So what
is loaded through a pointer to one of the frames in the mask points to the
youngest of them or to an older one. fill_from_stack() does the same for
a known frame and an unknown offset: GENMASK(frame, 0).
Fixes: bf0c571f7feb ("bpf: introduce forward arg-tracking dataflow analysis")
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/liveness.c | 4 +-
.../selftests/bpf/progs/verifier_live_stack.c | 60 +++++++++++++++++++
2 files changed, 63 insertions(+), 1 deletion(-)
diff --git a/kernel/bpf/liveness.c b/kernel/bpf/liveness.c
index e930d928afa5..c52f0435193b 100644
--- a/kernel/bpf/liveness.c
+++ b/kernel/bpf/liveness.c
@@ -1398,8 +1398,10 @@ static struct arg_track arg_track_deref(struct bpf_verifier_env *env, struct bpf
return fill_from_stack(insn, at_out, reg, parent->at, parent->slots, ptr->frame);
}
+ /* A frame holds pointers to its own stack and to the stacks of its callers */
if (ptr->frame == ARG_IMPRECISE && !(ptr->mask & BIT(depth)) && ptr->mask)
- return *ptr;
+ return (struct arg_track){ .mask = GENMASK(fls(ptr->mask) - 1, 0),
+ .frame = ARG_IMPRECISE };
return (struct arg_track){ .frame = ARG_NONE };
}
diff --git a/tools/testing/selftests/bpf/progs/verifier_live_stack.c b/tools/testing/selftests/bpf/progs/verifier_live_stack.c
index 32bab3cb3fcb..d6a6420d219d 100644
--- a/tools/testing/selftests/bpf/progs/verifier_live_stack.c
+++ b/tools/testing/selftests/bpf/progs/verifier_live_stack.c
@@ -3143,6 +3143,66 @@ static __used __naked void imprecise_fill_cross_frame(void)
: __clobber_all);
}
+/*
+ * fill_older_frame_3() loads through a pointer to the stack of frame 1 or of frame 2. Both slots
+ * hold a pointer to fp-8 of frame 0: the store is a may_def of that slot at the call in frame 0.
+ */
+SEC("socket")
+__log_level(2)
+__success
+__msg("3: (85) call pc+2{{ +}}; may_def: fp0-8..-{{[0-9]+}}")
+__naked void imprecise_parent_fill_loses_older_frame(void)
+{
+ asm volatile (
+ "*(u64 *)(r10 - 8) = 0;"
+ "r1 = r10;"
+ "r1 += -8;"
+ "call fill_older_frame_1;"
+ "r0 = *(u64 *)(r10 - 8);"
+ "exit;"
+ ::: __clobber_all);
+}
+
+static __used __naked void fill_older_frame_1(void)
+{
+ asm volatile (
+ "*(u64 *)(r10 - 8) = r1;"
+ "r1 = r10;"
+ "r1 += -8;"
+ "call fill_older_frame_2;"
+ "exit;"
+ ::: __clobber_all);
+}
+
+static __used __naked void fill_older_frame_2(void)
+{
+ asm volatile (
+ "r2 = *(u64 *)(r1 + 0);"
+ "*(u64 *)(r10 - 8) = r2;"
+ "r2 = r10;"
+ "r2 += -8;"
+ "call fill_older_frame_3;"
+ "exit;"
+ ::: __clobber_all);
+}
+
+static __used __naked void fill_older_frame_3(void)
+{
+ asm volatile (
+ "r6 = r1;"
+ "r7 = r2;"
+ "call %[bpf_get_prandom_u32];"
+ "if r0 == 0 goto +1;"
+ "r6 = r7;"
+ /* ARG_IMPRECISE{mask=BIT(1)|BIT(2)} */
+ "r1 = *(u64 *)(r6 + 0);"
+ "*(u64 *)(r1 + 0) = 7;"
+ "r0 = 0;"
+ "exit;"
+ :: __imm(bpf_get_prandom_u32)
+ : __clobber_all);
+}
+
/* Test that spill_to_stack with multi-offset dst (sz=8) joins instead
* of overwriting. r1 has offsets [-8, -16]. Both slots hold FP-derived
* pointers. Writing through r1 should join *val with existing values,
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 10/29] bpf: Detect a dead branch when a range and var_off don't overlap
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (8 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 09/29] bpf: Include older frames in a fill through an imprecise stack pointer Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 8:27 ` bot+bpf-ci
2026-11-11 5:13 ` [PATCH bpf-next v2 11/29] bpf: Don't treat half of a 64-bit spill as a 32-bit spill on big endian Alexei Starovoitov
` (18 subsequent siblings)
28 siblings, 1 reply; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A fix of the main pass that stands alone. It is also a prerequisite of the
patch that refines linked registers at conditional jumps: that patch
intersects the range of one register with var_off of another one and asks
reg_is_empty() if the intersection is empty.
1: r0 <<= 3
2: r0 += 5
3: if r0 s< -2 goto pc+2
4: if r0 s> 1 goto pc+1
5: r0 += 3
verifier bug: REG INVARIANTS VIOLATION (alu): range bounds violation
The low three bits of r0 are 5. No value in [-2, 1] has them: the range and
var_off don't overlap, the fall-through branch of insn 4 is dead. The
verifier takes it with an empty register, and the addition at insn 5 trips
over that.
simulate_both_branches_taken() refines the registers as each branch does
and drops a branch in which a range is empty. It relies on
reg_bounds_sync() to empty the range. __update_reg64_bounds() steps through
var_off from umin to umax, which are 0 and U64_MAX for a range that
crosses 0. __update_reg32_bounds() uses only the smallest and the largest
member of var_off.
tnum_step() returns the next member of var_off. reg_is_empty() asks for the
first one from the start of the range on and compares it with the end, in
64 and in 32 bits. A range that crosses 0 wraps around at U64_MAX or
U32_MAX: tnum_overlaps_cnum() splits it there. The test is sufficient, not
exact: 64 and 32 bits are checked each on its own.
Two tests in verifier_bounds.c, with the values above shifted left by 32:
no member of var_off is in [-2, 1] and the branch is dead, -3 is one in
[-3, 1] and the branch is verified.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/verifier.c | 34 +++++++++++++++++--
.../selftests/bpf/progs/verifier_bounds.c | 33 ++++++++++++++++++
2 files changed, 65 insertions(+), 2 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 5bffc1e288f6..bc0937b98818 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -2325,6 +2325,36 @@ static bool range_bounds_violation(struct bpf_reg_state *reg)
return cnum32_is_empty(reg->r32) || cnum64_is_empty(reg->r64);
}
+/* Do @t and [min, max] overlap: is a member of @t in the range? */
+static bool tnum_overlaps_urange(struct tnum t, u64 min, u64 max)
+{
+ u64 tnum_next = tnum_step(t, min);
+
+ return (min & ~t.mask) == t.value || (tnum_next > min && tnum_next <= max);
+}
+
+/* The same for the cnum [@base, @base + @size]. It wraps around at @ut_max. */
+static bool tnum_overlaps_cnum(struct tnum t, u64 base, u64 size, u64 ut_max)
+{
+ u64 end = (base + size) & ut_max;
+
+ if (base <= end)
+ return tnum_overlaps_urange(t, base, end);
+ return tnum_overlaps_urange(t, base, ut_max) || tnum_overlaps_urange(t, 0, end);
+}
+
+/*
+ * A range and var_off don't overlap. reg_bounds_sync() compares var_off with umin and umax: it
+ * doesn't see that [-2, 1], which crosses 0, has no member with 5 in its low three bits.
+ */
+static bool reg_is_empty(struct bpf_reg_state *reg)
+{
+ return range_bounds_violation(reg) ||
+ !tnum_overlaps_cnum(reg->var_off, reg->r64.base, reg->r64.size, U64_MAX) ||
+ !tnum_overlaps_cnum(tnum_subreg(reg->var_off), reg->r32.base, reg->r32.size,
+ U32_MAX);
+}
+
static int reg_bounds_sanity_check(struct bpf_verifier_env *env,
struct bpf_reg_state *reg, const char *ctx)
{
@@ -18916,7 +18946,7 @@ static int simulate_both_branches_taken(struct bpf_verifier_env *env, u8 opcode,
* reg_states in the FALSE branch (i.e. reg1, reg2), the FALSE branch must be dead. Only
* TRUE branch will be taken.
*/
- if (range_bounds_violation(&env->false_reg1) || range_bounds_violation(&env->false_reg2))
+ if (reg_is_empty(&env->false_reg1) || reg_is_empty(&env->false_reg2))
return 1;
/* Jump (TRUE) branch */
@@ -18928,7 +18958,7 @@ static int simulate_both_branches_taken(struct bpf_verifier_env *env, u8 opcode,
* reg_states in the TRUE branch (i.e. true_reg1, true_reg2), the TRUE branch must be dead.
* Only FALSE branch will be taken.
*/
- if (range_bounds_violation(&env->true_reg1) || range_bounds_violation(&env->true_reg2))
+ if (reg_is_empty(&env->true_reg1) || reg_is_empty(&env->true_reg2))
return 0;
/* Both branches are possible, we can't determine which one will be taken. */
diff --git a/tools/testing/selftests/bpf/progs/verifier_bounds.c b/tools/testing/selftests/bpf/progs/verifier_bounds.c
index f55d58af8004..316e9377caa8 100644
--- a/tools/testing/selftests/bpf/progs/verifier_bounds.c
+++ b/tools/testing/selftests/bpf/progs/verifier_bounds.c
@@ -1995,6 +1995,39 @@ __naked void bounds_refinement_multiple_overlaps(void *ctx)
: __clobber_all);
}
+/*
+ * r0 is x << 32, the low three bits of x are 5. No such x is in [-2, 1], -3 is in [-3, 1].
+ * umin and umax are unbounded for a range that crosses 0.
+ */
+#define RANGE_CROSSES_0(name, smin) \
+__naked void name(void *ctx) \
+{ \
+ asm volatile( \
+ "call %[bpf_ktime_get_ns];" \
+ "r0 <<= 35;" \
+ "r1 = 0x500000000 ll;" \
+ "r0 += r1;" \
+ "r1 = " #smin " ll;" \
+ "if r0 s< r1 goto 1f;" \
+ "r1 = 0x100000000 ll;" \
+ "if r0 s> r1 goto 1f;" \
+ "r10 = 0;" \
+"1: exit;" \
+ : \
+ : __imm(bpf_ktime_get_ns) \
+ : __clobber_all); \
+}
+
+SEC("socket")
+__description("dead branch: tnum and a range that crosses 0 don't overlap")
+__success
+RANGE_CROSSES_0(range_crossing_0_no_tnum_overlap, -0x200000000)
+
+SEC("socket")
+__description("tnum overlaps the negative part of a range that crosses 0")
+__failure __msg("frame pointer is read only")
+RANGE_CROSSES_0(range_crossing_0_tnum_overlap_below_0, -0x300000000)
+
SEC("socket")
__success
__naked void signed_unsigned_intersection32_case1(void *ctx)
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* Re: [PATCH bpf-next v2 10/29] bpf: Detect a dead branch when a range and var_off don't overlap
2026-11-11 5:13 ` [PATCH bpf-next v2 10/29] bpf: Detect a dead branch when a range and var_off don't overlap Alexei Starovoitov
@ 2026-11-11 8:27 ` bot+bpf-ci
0 siblings, 0 replies; 36+ messages in thread
From: bot+bpf-ci @ 2026-11-11 8:27 UTC (permalink / raw)
To: alexei.starovoitov, bpf
Cc: daniel, andrii, eddyz87, memxor, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 3109 bytes --]
> bpf: Detect a dead branch when a range and var_off don't overlap
TL;DR: Possible regression: when reg_is_empty() marks a branch dead, the
surviving branch loses its refined register bounds, so programs that loaded
before this patch may now be rejected by the verifier (hand-traced, not run).
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
[ ... ]
> @@ -18916,7 +18946,7 @@ static int simulate_both_branches_taken(struct bpf_verifier_env *env, u8 opcode,
> * reg_states in the FALSE branch (i.e. reg1, reg2), the FALSE branch must be dead. Only
> * TRUE branch will be taken.
> */
> - if (range_bounds_violation(&env->false_reg1) || range_bounds_violation(&env->false_reg2))
> + if (reg_is_empty(&env->false_reg1) || reg_is_empty(&env->false_reg2))
> return 1;
When reg_is_empty() newly finds the fall-through branch dead, the jump
branch no longer gets the refinement it used to get. Could this cause
programs that loaded before this patch to be rejected?
Before the patch, a fall-through whose refined range had no member of
var_off was not detected, both branches stayed live, and
check_cond_jmp_op() put the refined registers into the jump state:
other_branch_regs[insn->dst_reg] = env->true_reg1;
With this patch simulate_both_branches_taken() returns 1 here before it
refines env->true_reg1. With pred == 1, check_cond_jmp_op() only does
`*insn_idx += insn->off; return 0;`, so the jump target keeps the
unrefined dst_reg, which is strictly looser than the refined one.
The commit message example, extended (hand-traced, not run):
0: call bpf_ktime_get_ns
1: r0 <<= 3
2: r0 += 5 // var_off=(0x5; ~0x7)
3: if r0 s< -2 goto out
4: if r0 s> 1 goto L // fall-through s[-2, 1]: no member, now dead
5: r0 = 0
6: exit
L: if r0 s> 100 goto out
r1 = <map value, size > 100>; r1 += r0; r0 = *(u8 *)(r1 + 0)
Before the patch, at L r0 is env->true_reg1 = s[2, S64_MAX-2], and
reg_bounds_sync() (via __reg_bound_offset() knowing bit 63 is 0) turns
that into [5, S64_MAX-2]. The fall-through of "s> 100" is [5, 100] and
the access is accepted.
After the patch, at L r0 is still s[-2, S64_MAX-2] with the sign bit
unknown. The fall-through of "s> 100" is s[-2, 100], which is not empty
(5 is a member), and since it crosses 0, __update_reg64_bounds() and
__reg_bound_offset() cannot tighten it. The access then fails with "min
value is negative".
The same loss seems to happen for pred == 0: env->false_reg1 and
env->false_reg2 are refined and found live, then thrown away. Would it
make sense to use the refined registers of the surviving branch when the
other branch is found empty? That looks sound, since the refined state
then has the same members as the incoming one.
> /* Jump (TRUE) branch */
[ ... ]
---
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/42982641817
^ permalink raw reply [flat|nested] 36+ messages in thread
* [PATCH bpf-next v2 11/29] bpf: Don't treat half of a 64-bit spill as a 32-bit spill on big endian
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (9 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 10/29] bpf: Detect a dead branch when a range and var_off don't overlap Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 12/29] bpf: Refine registers linked by scalar evolution at conditional jumps Alexei Starovoitov
` (17 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A fix of the main pass for big endian that stands alone. The patches that
follow refine more registers and stack slots at conditional jumps, so more
progs would run into it.
*(u64 *)(r10 - 8) = r1
r2 = *(u32 *)(r10 - 8)
r2 gets the low 32 bits of r1 on little endian and the upper 32 bits on big
endian. In two cases the verifier gives r2 the range of the low 32 bits on
big endian too.
The fill itself is right: check_stack_read_fixed_off() asks
bpf_stack_narrow_access_ok() where the low bytes of a spill are. A fill
from elsewhere gets an unbounded scalar.
After the 32-bit spill '*(u32 *)(r10 - 8) = r1' the half-slot at r10 - 8 is
STACK_SPILL and spilled_ptr describes r1, whose low 32 bits it holds on
both byte orders. Two functions take a 64-bit spill for such a slot:
- __clean_func_state() marks a dead half-slot STACK_POISON. If that is the
one at r10 - 4, what remains of a 64-bit spill has the slot types of
a 32-bit spill.
- stacksafe() skips the bytes that are STACK_MISC in the old state. An old
32-bit spill and a current 64-bit spill are then compared by regsafe()
of spilled_ptr.
That is right on little endian, where the half-slot at r10 - 8 has the low
32 bits of a 64-bit spill. On big endian it has the upper 32 bits.
Keep such a 64-bit spill whole in __clean_func_state(). In stacksafe()
compare two spills only if the bytes of the old one are the low bytes of
the current one. bpf_stack_narrow_access_ok() decides in both.
Two tests in verifier_spill_fill.c, one per function. They load on little
endian and are rejected on big endian.
Fixes: be23266b4a08 ("bpf: 4-byte precise clean_verifier_state")
Fixes: 6715df8d5d24 ("bpf: Allow reads from uninit stack")
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/states.c | 19 ++++++++++++
.../selftests/bpf/progs/verifier_spill_fill.c | 31 +++++++++++++++++++
2 files changed, 50 insertions(+)
diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c
index efc0870b85c2..b60676349655 100644
--- a/kernel/bpf/states.c
+++ b/kernel/bpf/states.c
@@ -168,6 +168,16 @@ static void __clean_func_state(struct bpf_verifier_env *env,
stype == STACK_IRQ_FLAG)
continue;
+ /*
+ * When the half-slot closer to FP is dead, what remains of a
+ * 64-bit spill looks like a 32-bit spill: the low 32 bits of
+ * the value. On big endian the other half-slot holds the upper
+ * 32 bits.
+ */
+ if (hi_live && bpf_spill_size(bpf_stack_slot(st, i)) == BPF_REG_SIZE &&
+ !bpf_stack_narrow_access_ok(0, BPF_REG_SIZE / 2, BPF_REG_SIZE))
+ continue;
+
/*
* Only scalar spills can be degraded to raw stack bytes
* when their high half is dead. Pointer spills need the
@@ -446,6 +456,15 @@ static bool stacksafe(struct bpf_verifier_env *env, struct bpf_func_state *old,
if (!cur_slot)
return false;
+ /*
+ * The old spill is the first bytes of a wider spill. On big endian
+ * these are not the low bytes of the value.
+ */
+ if (old_type == STACK_SPILL && bpf_is_spilled_reg(cur_slot) &&
+ !bpf_stack_narrow_access_ok(0, bpf_spill_size(old_slot),
+ bpf_spill_size(cur_slot)))
+ return false;
+
/*
* 64 and 32-bit scalar spills vs MISC/INVALID slots and vice versa.
* Load from MISC/INVALID slots produces unbound scalar.
diff --git a/tools/testing/selftests/bpf/progs/verifier_spill_fill.c b/tools/testing/selftests/bpf/progs/verifier_spill_fill.c
index 0baa8e06b8d1..623ed5ef3630 100644
--- a/tools/testing/selftests/bpf/progs/verifier_spill_fill.c
+++ b/tools/testing/selftests/bpf/progs/verifier_spill_fill.c
@@ -1451,6 +1451,37 @@ __naked void imprecise_scalar_spill_half_dead(void)
: __clobber_all);
}
+/*
+ * The first half-slot of a 64-bit spill holds its low 32 bits on little endian only.
+ * The test spills to r10 - 8. The first four bytes of the slot are an offset into one byte.
+ */
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+#define FIRST_HALF_IS_LOW __success
+#else
+#define FIRST_HALF_IS_LOW __failure __msg("access may be outside object bounds")
+#endif
+#define FIRST_HALF(name, stores) \
+SEC("raw_tp") FIRST_HALF_IS_LOW \
+__naked void name(void) \
+{ \
+ asm volatile ( \
+ "call %[bpf_ktime_get_ns];" \
+ stores \
+ "r7 = *(u32 *)(r10 - 8); r1 = %[single_byte_buf] ll; r1 += r7; r0 = *(u8 *)(r1 + 0);" \
+ "out_%=: r0 = 0; exit;" \
+ : \
+ : __imm(bpf_ktime_get_ns), __imm_addr(single_byte_buf) \
+ : __clobber_all); \
+}
+
+/* The low 32 bits of the value are 0. The second half-slot is dead after the jump. */
+FIRST_HALF(first_half_of_spill_second_half_dead,
+ "r0 <<= 32; *(u64 *)(r10 - 8) = r0; if r0 == 0 goto out_%=;")
+/* One path has a 32-bit spill of r6, whose low 32 bits are 0, the other one a 64-bit spill */
+FIRST_HALF(spill_of_64_bits_vs_spill_of_32_bits,
+ "r6 = r0; r6 <<= 32; if r0 > 5 goto 1f; *(u32 *)(r10 - 8) = r6; goto 2f;"
+ "1: r6 = 0x7ffffff000000000 ll; *(u64 *)(r10 - 8) = r6; 2:")
+
/* check valid spill/fill, ptr to tp buffer */
SEC("raw_tracepoint.w")
__success
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 12/29] bpf: Refine registers linked by scalar evolution at conditional jumps
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (10 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 11/29] bpf: Don't treat half of a 64-bit spill as a 32-bit spill on big endian Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 13/29] bpf: Value number the phis of scalar evolution Alexei Starovoitov
` (16 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A new ability, and the first user of scalar evolution outside of loops.
The linked registers of a conditional jump are the live registers and
stack slots that are a tested register + a constant on every path to the
jump. scev.c computes them before the main pass, and the main pass refines
them together with the tested registers. Scalar ids do some of that today.
They stay for now, later patches remove them.
if (bpf_dynptr_from_skb(skb, 0, &ptr))
return NF_DROP;
..
return NF_DROP;
That is ip_check_defrag.c of the selftests. NF_DROP is 0, and GCC returns
the 0 that the call returned:
27: call bpf_dynptr_from_skb
28: r5 = (s32)r0
29: if w0 != 0x0 goto pc-12
..
46: r0 = r5
50: exit
At program exit the register R0 has smin=-2147483648
smax=2147483647 should have been in [0, 1]
The jump bounds r0. r5 got no scalar id at 28: r0 was not known to be below
2^31.
Before insn 29 r0 is %r0.27, the def of r0 by insn 27, and r5 is
(sext32 %r0.27). split_const_offset() strips added constants, extensions
and partial spills: expr == base + off (mod 2^bits). Expressions with the
same base, a value number, have a constant difference: const_diff(). Then
find_linked_regs() links the registers:
linked == tested + delta (mod 2^bits), here r5 == r0 (mod 2^32).
Unlike scalar ids the links are not stored in a state and have no limit of
five registers (LINKED_REGS_MAX). They also cover registers that agree in
their low 8, 16 or 32 bits only, like r5 and r0 above, where a scalar gets
no id or loses it. A link stays within one frame. At the header, a latch or
an exit of a loop the difference may also be one that holds at the header
in every iteration: it holds at loop entry and every backedge preserves it,
const_diff_at_header().
refine_linked_regs() adds delta to the tested register and intersects the
linked one with the result, as 'if r5 == r0' does, in 64 or 32 bits. Values
that are equal modulo 2^8, 2^16 or 2^32 are equal if both fit in uN or both
in sN: then all 64 bits are intersected. Equal in 64 bits is equal in the
low 32 bits: 'if w1 s> 200' on a zero-extended int refines nothing but the
32-bit range, and the linked registers get that too. The intersection is
empty, or the two var_off don't overlap: no execution gets here with this
state (the path is infeasible), the register is left as it is. Types are
read in the state: both have to be scalars. So is the width of a spill:
__clean_func_state() leaves a 32-bit spill of a 64-bit spill whose other
half-slot is dead, and a link in 64 bits refines its low 32 bits.
scev.c has the insns, no types and no states. Where that matters:
- Arena: an ALU64 insn is converted to ALU32 at fixup if an operand is an
arena pointer. In a prog with an arena its result gets a def too:
%r2.12=(+ x 4) is x + 4 in 64 or in 32 bits. It is trusted modulo 2^32,
and modulo 2^64 once the main pass has verified insn 12 without an arena
pointer: prevent_zext is only ever set, and the def dominates every use.
%r2.25=phi((+ x 4)) joins two such defs. The phi is not (+ x 4).
- Slices: a store through what bpf_dynptr_slice_rdwr() returned may alias
the buffer on the stack and is in no bpf_may_write_mask(). In a prog
with that kfunc a slot that a call may write is a value number only
after a store through r10, which invalidates the slices. Two fills from
such a slot can read distinct values, one fill reads one: the fill
defines the register, like a load from other memory.
- Phis: the op of a phi is what the joined values have in common, any op.
simplify() and expr_spill_size() look through extensions and partial
spills, not through a phi.
20 more progs of the selftests load when GCC builds them, among them
profiler1-3, test_sk_lookup and setget_sockopt. The kernel spends
0.7 % more instructions on loading the selftests, Cilium, sched_ext, katran
and bpfjailer, 1.5 % more on programs in Rust.
scalars_double_add and subreg_equality_2 expected a missing link and load
now. bounds_deduct_negative_overlap, linked_regs_too_many_regs and
ldsx_fill_scalar_id_not_shared pin a range or a message that is more
precise now. The arena tests of verifier_scev.c move to verifier_arena.c:
libbpf binds an arena to all progs of an object, so every prog of
verifier_scev.c had one.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
include/linux/bpf_verifier.h | 4 +
kernel/bpf/scev.c | 493 ++++++++++++++++--
kernel/bpf/verifier.c | 118 +++++
.../selftests/bpf/progs/verifier_arena.c | 28 +
.../selftests/bpf/progs/verifier_bounds.c | 2 +-
.../bpf/progs/verifier_linked_scalars.c | 5 +-
.../selftests/bpf/progs/verifier_reg_equal.c | 11 +-
.../selftests/bpf/progs/verifier_scalar_ids.c | 6 +-
.../selftests/bpf/progs/verifier_scev.c | 36 --
9 files changed, 616 insertions(+), 87 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 4771c55d8f41..d1841f68437d 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -1902,6 +1902,7 @@ u32 btf_func_arg_align(const struct btf *btf, const struct btf_type *t);
int bpf_find_subprog(struct bpf_verifier_env *env, int off);
bool bpf_is_throw_kfunc(struct bpf_insn *insn);
+bool bpf_is_dynptr_slice_rdwr_kfunc(struct bpf_insn *insn);
int bpf_compute_const_regs(struct bpf_verifier_env *env);
int bpf_prune_dead_branches(struct bpf_verifier_env *env);
int bpf_check_cfg(struct bpf_verifier_env *env);
@@ -2224,6 +2225,9 @@ int bpf_init_scev(struct bpf_verifier_env *env);
void bpf_free_scev(struct bpf_verifier_env *env);
int bpf_compute_scev(struct bpf_verifier_env *env);
int bpf_scev_jump_counts(struct bpf_verifier_env *env, int header, int n, bool taken_leaves);
+bool bpf_scev_next_linked_reg(struct bpf_verifier_env *env, int insn_idx, u32 regno, u32 *pos,
+ u32 *linked_regno, u32 *fill_off, s64 *delta, u32 *bits);
+bool bpf_scev_in_slice_slots(struct bpf_verifier_env *env, int off, u32 size, bool fixed_off);
int bpf_compute_loop_iters(struct bpf_verifier_env *env, struct bpf_verifier_state *st,
struct bpf_loop_iters *iters);
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index a0324fbc875e..5506e0d4e534 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -65,6 +65,10 @@ enum expr_op {
* a value number of its own.
* In the env of loop H a phi at H merges the backedge values only, the header phi itself is
* (REG r). In the env of the enclosing loop a phi at H merges the entering edges.
+ * Op and params of a def by an insn are what that insn computes, see alu64_with_def():
+ * set_expr_base() looks through such an add, split_const_offset() checks prevent_zext of the insn.
+ * Those of a phi are the shape that its operands share, no insn computes them: nothing looks
+ * through a phi.
*/
struct expr_def {
u32 set:1; /* 0: the expression has no def */
@@ -80,6 +84,8 @@ struct expr {
u16 op;
/* A value number: all SCEV registers of an env that have this id hold the same value */
bool is_vn;
+ /* expr == base + off (mod 2^bits), see split_const_offset() */
+ u8 bits;
/*
* UNKNOWN, OPAQUE and ANY don't describe a value. With a def the id stands for one value,
* see struct expr_def. Without a def every use of the id can be another value.
@@ -89,6 +95,13 @@ struct expr {
u32 params[2];
s64 imm;
};
+ u32 base;
+ /*
+ * id of the first add on the way to 'base' whose insn may still become ALU32, see
+ * prevent_zext. 0: none
+ */
+ u32 arena_add;
+ s64 off;
};
struct env {
@@ -97,8 +110,17 @@ struct env {
u32 reg2scev[REGS_NUM];
};
+/* A SCEV register and the id of its expression before a conditional jump */
+struct scev_linked_reg {
+ u32 reg;
+ u32 id;
+};
+
struct insn_envs {
u32 cnt;
+ /* The linked registers of a jump, see find_linked_regs(): linked_cnt of scev->linked[] */
+ u32 linked_first;
+ u32 linked_cnt;
struct {
int loop_header;
struct env *env;
@@ -119,6 +141,8 @@ struct scev {
/* Read-only representatives for stack slots without a saved register. */
struct bpf_reg_state zero_reg;
struct bpf_reg_state unknown_reg;
+ /* r -> (REG r): before the first insn of a loop every SCEV register is its header phi */
+ struct env identity_env;
/*
* Expressions are identified by id, exprs_ht ensures that
* each expression exists as a unique instance.
@@ -131,6 +155,9 @@ struct scev {
struct scev_worklist worklist;
struct insn_envs **envs;
struct expr *exprs;
+ struct scev_linked_reg *linked;
+ /* The stack slots that a store through a dynptr slice may alias, see expr_is_vn() */
+ DECLARE_BITMAP(slice_slots, MAX_BPF_STACK_SLOTS);
/*
* Loops are analyzed one by one, this array keeps track if a particular
* instruction was visited on a current pass.
@@ -138,6 +165,8 @@ struct scev {
u32 *discovered;
int exprs_cnt;
int exprs_cap;
+ u32 linked_cnt;
+ u32 linked_cap;
int envs_cnt;
int stack_sz;
int simplify_depth;
@@ -200,10 +229,17 @@ static u32 op_params_num(u32 op);
static bool expr_is_vn(struct scev *scev, struct expr *e)
{
- u32 i;
+ u32 i, reg = e->def.reg;
+ /*
+ * bpf_dynptr_slice_rdwr() can return the buffer that it got. A store through that slice may
+ * alias the slots of the buffer and is in no bpf_may_write_mask(): it is no def, so a def
+ * of such a slot is no value number. A spill through r10 has the expression of the
+ * register, and invalidate_stack_slices() invalidates the slices there. The next slice
+ * comes from a call that may write to the slot.
+ */
if (e->def.set)
- return true;
+ return reg < MAX_BPF_REG || !test_bit(reg - MAX_BPF_REG, scev->slice_slots);
switch (e->op) {
case UNKNOWN:
case OPAQUE:
@@ -223,6 +259,37 @@ static bool expr_is_vn(struct scev *scev, struct expr *e)
return true;
}
+static u32 op_low_bits(u32 op);
+
+/*
+ * @e gets the id @id. (+ x imm), an extension and a partial spill of x have the base of x, which
+ * was constructed earlier. Every other expression is its own base. So split_const_offset() does not
+ * traverse the expression.
+ */
+static void set_expr_base(struct scev *scev, struct expr *e, u32 id)
+{
+ u32 low = op_low_bits(e->op);
+ struct expr *x;
+ s64 imm = 0;
+
+ e->base = id;
+ e->off = 0;
+ e->bits = 64;
+ e->arena_add = 0;
+ /* nothing looks through a phi, see struct expr_def */
+ if (e->def.phi)
+ return;
+ if (e->op == BPF_ADD && scev->exprs[e->params[1]].op == IMM)
+ imm = scev->exprs[e->params[1]].imm;
+ else if (!low || e->def.set)
+ return;
+ x = &scev->exprs[e->params[0]];
+ e->base = x->base;
+ e->off = (u64)x->off + imm;
+ e->bits = low ? min(x->bits, low) : x->bits;
+ e->arena_add = e->def.set ? id : x->arena_add;
+}
+
static int add_expr(struct scev *scev, struct expr e)
{
u32 id;
@@ -242,6 +309,7 @@ static int add_expr(struct scev *scev, struct expr e)
id = scev->exprs_cnt++;
e.is_vn = expr_is_vn(scev, &e);
+ set_expr_base(scev, &e, id);
scev->exprs[id] = e;
exprs_ht_add(scev, id);
return id;
@@ -590,6 +658,9 @@ static bool expr_next(struct scev *scev, u32 *id, u32 *order)
* %r8.7=phi(1, 2) the phi of r8 before insn 7
* %r0.7=phi(_) a phi of distinct OPAQUE values
* %r6.9=phi(?) a phi of distinct UNKNOWN values, or of more than 8 values
+ * %r2.12=(+ x 4) insn 12 defines r2, see alu64_with_def()
+ * %r2.25=phi((+ x 4)) two adds of one shape, each may have been converted to ALU32:
+ * the phi is not (+ x 4)
* A name is unique within the env of one loop. In the env of loop H a phi at H merges the backedge
* values only, the header phi itself is (REG r): in 'scev at header 1: r6=%r6.1=phi(r6, (+ r6 1))'
* one backedge leaves r6 as it is, the other one adds 1.
@@ -754,7 +825,7 @@ static struct env *get_loop_env(struct scev *scev, int loop_header, int insn_idx
if (!e)
return NULL;
- tmp = krealloc(envs, struct_size(envs, entries, cnt + 1), GFP_KERNEL_ACCOUNT);
+ tmp = krealloc(envs, struct_size(envs, entries, cnt + 1), GFP_KERNEL_ACCOUNT | __GFP_ZERO);
if (!tmp) {
kfree(e);
return NULL;
@@ -781,7 +852,7 @@ static void free_loop_env(struct scev *scev, int loop_header, int insn_idx)
envs->entries[i] = envs->entries[--envs->cnt];
break;
}
- if (envs->cnt)
+ if (envs->cnt || envs->linked_cnt)
return;
kfree(envs);
scev->envs[insn_idx] = NULL;
@@ -833,7 +904,7 @@ static int forget_call_regs(struct bpf_verifier_env *env, struct env *e, int idx
static u32 spill_spi(struct bpf_insn *insn)
{
- return -insn->off / BPF_REG_SIZE - 1;
+ return (-insn->off - 1) / BPF_REG_SIZE;
}
static u32 off_to_reg(struct bpf_insn *insn)
@@ -874,18 +945,40 @@ static int mk_spill(struct scev *scev, u8 size, int id)
return UNKNOWN_EXPR_ID;
}
-static int mk_fill(struct scev *scev, int id, u8 code)
+/* The size of the spill in a slot with the expression @id. It holds the low bytes of *@x. */
+static u32 expr_spill_size(struct scev *scev, u32 id, u32 *x)
{
- bool sx = BPF_MODE(code) == BPF_MEMSX;
+ struct expr *e = &scev->exprs[id];
+ /* nothing looks through a phi, see struct expr_def */
+ bool partial = (e->op == SPILL8 || e->op == SPILL16 || e->op == SPILL32) && !e->def.phi;
+
+ *x = partial ? e->params[0] : id;
+ return partial ? op_low_bits(e->op) / BITS_PER_BYTE : BPF_REG_SIZE;
+}
+
+/* -ENOENT: the value of the fill is not known */
+static int mk_fill(struct scev *scev, int id, struct bpf_insn *insn)
+{
+ u32 x, spill_size = expr_spill_size(scev, id, &x);
+ u32 size = bpf_size_to_bytes(BPF_SIZE(insn->code));
+ bool sx = BPF_MODE(insn->code) == BPF_MEMSX;
+ /* The fill reads the low bytes of the spill, as in check_stack_read_fixed_off() */
+ if (size <= spill_size && bpf_stack_narrow_access_ok(insn->off, size, spill_size))
+ id = x;
+ else if (!is_spill_off(insn->off))
+ return -ENOENT;
#ifdef __BIG_ENDIAN
+ /* No register becomes (spillN x): its low bits would not be those of x */
+ else if (spill_size < BPF_REG_SIZE)
+ return -ENOENT;
/* A narrow fill reads the high bits of the slot on big-endian. */
- if (BPF_SIZE(code) != BPF_DW)
+ else
return expr2(scev, sx ? BPF_ARSH : BPF_RSH, id,
- imm_expr(scev, 64 - BITS_PER_BYTE * bpf_size_to_bytes(BPF_SIZE(code))));
+ imm_expr(scev, BITS_PER_BYTE * (BPF_REG_SIZE - size)));
#endif
- switch (BPF_SIZE(code)) {
+ switch (BPF_SIZE(insn->code)) {
case BPF_B: return expr1(scev, sx ? SEXT8 : ZEXT8, id);
case BPF_H: return expr1(scev, sx ? SEXT16 : ZEXT16, id);
case BPF_W: return expr1(scev, sx ? SEXT32 : ZEXT32, id);
@@ -907,10 +1000,21 @@ static int maybe_store_fp(struct scev *scev, struct env *e, struct bpf_insn *ins
static int maybe_load_fp(struct scev *scev, struct env *e, struct bpf_insn *insn, int idx)
{
- if (insn->src_reg != BPF_REG_FP || !is_spill_off(insn->off))
+ int id = -ENOENT;
+
+ if (insn->src_reg == BPF_REG_FP && insn->off < 0 && insn->off >= -MAX_BPF_STACK_JIT)
+ id = e->reg2expr[off_to_reg(insn)];
+ /*
+ * The def of a slot that a slice may alias is no value number, see expr_is_vn(): two fills
+ * can read distinct values. One fill reads one value: the insn defines the register.
+ */
+ if (id >= 0 && scev->exprs[id].def.set && !scev->exprs[id].is_vn)
+ id = -ENOENT;
+ if (id >= 0)
+ id = mk_fill(scev, id, insn);
+ if (id == -ENOENT)
return forget_reg(scev, e, insn->dst_reg, OPAQUE_EXPR_ID, idx);
- return replace_reg(scev, e, insn->dst_reg,
- mk_fill(scev, e->reg2expr[off_to_reg(insn)], insn->code));
+ return replace_reg(scev, e, insn->dst_reg, id);
}
/*
@@ -945,10 +1049,28 @@ static void alu32_operands(struct scev *scev, u32 op, int *a, int *b)
}
}
+/*
+ * In a prog with an arena an ALU64 insn is converted to ALU32 at fixup if an operand is a pointer
+ * to the arena, see adjust_reg_min_max_vals(). Types are not known here. So @id, what the insn at
+ * @idx computes from @a, gets a def: the value of @dst after the insn, converted or not. See
+ * split_const_offset() for when the op can be trusted. Constant operands are scalars in every
+ * state.
+ */
+static int alu64_with_def(struct scev *scev, int id, int a, int idx, u32 dst)
+{
+ s64 imm;
+
+ if (id < 0 || is_imm(scev, id, &imm))
+ return id;
+ /* (+ x 0) is x only if the insn stays ALU64 */
+ return expr_with_def(scev, id == a ? UNKNOWN_EXPR_ID : id, insn_def(idx, dst));
+}
+
static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
{
const bool little_endian = htons(0x3412) == 0x1234;
struct bpf_insn *insn = &env->prog->insnsi[idx];
+ bool arena = env->prog->aux->arena;
struct scev *scev = env->scev;
u32 *reg2expr = e->reg2expr;
u8 class = BPF_CLASS(insn->code);
@@ -957,7 +1079,7 @@ static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
u8 mode = BPF_MODE(insn->code);
u32 dst = insn->dst_reg;
u32 src = insn->src_reg;
- u32 op, sext, l, r;
+ u32 op, sext;
bool need_bswap;
int i, id, a;
s64 imm;
@@ -1023,13 +1145,6 @@ static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
else
goto mark_dst_unknown;
- /* 'r1 <<= 32; r1 >>= 32' is (u32)r1 for cpu v1 and v2 */
- if (class == BPF_ALU64 && op == BPF_RSH && id >= 0 &&
- is_imm(scev, id, &imm) && imm == 32 &&
- is_binop(scev, BPF_LSH, reg2expr[dst], &l, &r) &&
- is_imm(scev, r, &imm) && imm == 32)
- return replace_reg(scev, e, dst, expr1(scev, ZEXT32, l));
-
a = reg2expr[dst];
if (class == BPF_ALU)
alu32_operands(scev, op, &a, &id);
@@ -1037,6 +1152,8 @@ static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
if (class == BPF_ALU)
id = expr1(scev, ZEXT32, id);
+ else if (arena)
+ id = alu64_with_def(scev, id, a, idx, dst);
return replace_reg(scev, e, dst, id);
@@ -1676,6 +1793,227 @@ static int join_added_edges(struct bpf_verifier_env *env, struct bpf_subprog_inf
return sub->bail_insn < 0 ? 0 : join_successor(env, -1, sub->bail_insn, old_env, cur_env);
}
+/*
+ * @id == *@base + *@off (mod 2^*@bits): constant offsets, extensions and partial spills are
+ * stripped, see set_expr_base(). An add with a def holds modulo 2^32 whether its insn is converted
+ * to ALU32 or not, see alu64_with_def(). It holds modulo 2^64 once the main pass has set
+ * prevent_zext: it verified the insn without a pointer to the arena and rejects a state that brings
+ * one. The def dominates every use, and prevent_zext is only ever set: such an add is not checked
+ * again.
+ */
+static void split_const_offset(struct bpf_verifier_env *env, u32 id, u32 *base, s64 *off, u32 *bits)
+{
+ struct scev *scev = env->scev;
+ struct expr *add, *e = &scev->exprs[id];
+
+ *base = e->base;
+ *off = e->off;
+ *bits = e->bits;
+ while (e->arena_add) {
+ add = &scev->exprs[e->arena_add];
+ if (!env->insn_aux_data[add->def.idx].prevent_zext) {
+ *bits = min(*bits, 32);
+ break;
+ }
+ e->arena_add = scev->exprs[add->params[0]].arena_add;
+ }
+}
+
+static bool const_diff_at_header(struct bpf_verifier_env *env, int h, u32 ra, u32 rb, s64 *delta);
+
+/*
+ * @id_a and @id_b are expressions of one env of the loop at @h, or of a subprog: @h is -1. Do they
+ * have a constant difference, @id_b == @id_a + *@delta (mod 2^*@bits)?
+ */
+static bool const_diff(struct bpf_verifier_env *env, int h, u32 id_a, u32 id_b, s64 *delta,
+ u32 *bits)
+{
+ struct scev *scev = env->scev;
+ u32 base_a, base_b, bits_a, ra, rb;
+ s64 off_a, c;
+
+ split_const_offset(env, id_a, &base_a, &off_a, &bits_a);
+ split_const_offset(env, id_b, &base_b, delta, bits);
+ *delta = (u64)*delta - off_a;
+ *bits = min(*bits, bits_a);
+ /* copies are equal in all bits */
+ if (id_a == id_b && scev->exprs[id_a].is_vn) {
+ *bits = 64;
+ return true;
+ }
+ if (base_a == base_b)
+ return scev->exprs[base_a].is_vn;
+ if (h < 0 || !is_reg(scev, base_a, &ra) || !is_reg(scev, base_b, &rb) ||
+ !const_diff_at_header(env, h, ra, rb, &c))
+ return false;
+ *delta = (u64)*delta + c;
+ return true;
+}
+
+/*
+ * old = i = 0; do { i++; if (old > 1021) break; c = buf[old]; old = i; } while (c);
+ * Does @rb == @ra + *@delta hold at the header of the loop at @h in every iteration? It does if it
+ * holds at loop entry, by the env of the enclosing loop at @h, and every backedge preserves it: the
+ * backedge values have this constant difference, or both are the header phi plus one constant.
+ * Loops nest LOOP_STACK_SIZE deep at most, see bpf_flow_place_guards().
+ */
+static bool const_diff_at_header(struct bpf_verifier_env *env, int h, u32 ra, u32 rb, s64 *delta)
+{
+ int outer_header = env->insn_aux_data[h].loop_header;
+ struct env *entry_env = find_loop_env(env->scev, outer_header, h);
+ struct env *header_env = find_header_env(env->scev, h);
+ u32 base_a, base_b, bits_a, bits;
+ s64 off_a, c;
+
+ if (!entry_env || !can_compute_loop_scev(env->insn_aux_data[h].loop) ||
+ !const_diff(env, outer_header, entry_env->reg2expr[ra], entry_env->reg2expr[rb],
+ delta, &bits) || bits != 64)
+ return false;
+ if (const_diff(env, -1, header_env->reg2expr[ra], header_env->reg2expr[rb], &c, &bits))
+ return bits == 64 && c == *delta;
+ split_const_offset(env, header_env->reg2expr[ra], &base_a, &off_a, &bits_a);
+ split_const_offset(env, header_env->reg2expr[rb], &base_b, &c, &bits);
+ return base_a == REG_EXPR_ID(ra) && base_b == REG_EXPR_ID(rb) && off_a == c &&
+ bits_a == 64 && bits == 64;
+}
+
+/* The first bytes of a stack slot are its high bits */
+static bool is_big_endian_slot(u32 reg)
+{
+ return IS_ENABLED(CONFIG_CPU_BIG_ENDIAN) && reg >= MAX_BPF_REG;
+}
+
+/*
+ * const_diff() for the SCEV register @reg, which has the expression @id_b. A fill from byte
+ * *@fill_off of a slot reads these bits. On big endian the low bytes of a spill are the last ones,
+ * and a narrow fill from the start of a slot is a right shift, see mk_fill().
+ */
+static bool const_diff_reg(struct bpf_verifier_env *env, int h, u32 id_a, u32 reg, u32 id_b,
+ u32 *fill_off, s64 *delta, u32 *bits)
+{
+ struct scev *scev = env->scev;
+ struct expr *e;
+ u32 x;
+ s64 k;
+
+ *fill_off = 0;
+ if (const_diff(env, h, id_a, id_b, delta, bits)) {
+ if (is_big_endian_slot(reg))
+ *fill_off = expr_spill_size(scev, id_b, &x) - *bits / BITS_PER_BYTE;
+ return true;
+ }
+ if (!is_big_endian_slot(reg) || !scev->exprs[id_b].is_vn)
+ return false;
+ split_const_offset(env, id_a, &x, delta, bits);
+ e = &scev->exprs[x];
+ if ((e->op != BPF_RSH && e->op != BPF_ARSH) || e->def.set || e->params[0] != id_b ||
+ !is_imm(scev, e->params[1], &k) || (k != 32 && k != 48 && k != 56))
+ return false;
+ *delta = -(u64)*delta;
+ *bits = min_t(u32, *bits, 64 - k);
+ *fill_off = (64 - k - *bits) / BITS_PER_BYTE;
+ return true;
+}
+
+/*
+ * n = bpf_get_stack(..); m = n; if (n < 0) ..; use(m);
+ * The main pass refines the registers that the conditional jump at @idx tests. Find the live SCEV
+ * registers that are linked to one of them in @e, the env before the jump, and keep them with their
+ * expressions: what bpf_scev_next_linked_reg() needs of @e.
+ */
+static int find_linked_regs(struct bpf_verifier_env *env, struct env *e, int h, int idx)
+{
+ struct bpf_insn_aux_data *aux = &env->insn_aux_data[idx];
+ struct bpf_insn *insn = &env->prog->insnsi[idx];
+ u32 dst = e->reg2expr[insn->dst_reg], src = dst;
+ struct insn_envs *envs = env->scev->envs[idx];
+ struct scev *scev = env->scev;
+ u32 r, id, fill_off, base, base_dst, base_src, bits, cap, tested = 0;
+ u32 first = scev->linked_cnt;
+ DECLARE_BITMAP(live, REGS_NUM) = {};
+ void *tmp;
+ s64 delta;
+
+ if (!bpf_insn_is_cond_jump(insn->code) || BPF_OP(insn->code) == BPF_JCOND)
+ return 0;
+ if (BPF_SRC(insn->code) == BPF_X)
+ src = e->reg2expr[insn->src_reg];
+ bitmap_copy(live, aux->live_stack_before, MAX_BPF_STACK_SLOTS);
+ bitmap_shift_left(live, live, MAX_BPF_REG, REGS_NUM);
+ live[0] |= aux->live_regs_before;
+ split_const_offset(env, dst, &base_dst, &delta, &bits);
+ split_const_offset(env, src, &base_src, &delta, &bits);
+ for_each_set_bit(r, live, REGS_NUM) {
+ id = e->reg2expr[r];
+ /* most are not linked */
+ split_const_offset(env, id, &base, &delta, &bits);
+ if (base != base_dst && base != base_src && (h < 0 || !is_op(scev, base, REG)) &&
+ !is_big_endian_slot(r))
+ continue;
+ if (!const_diff_reg(env, h, dst, r, id, &fill_off, &delta, &bits) &&
+ (src == dst || !const_diff_reg(env, h, src, r, id, &fill_off, &delta, &bits)))
+ continue;
+ if (scev->linked_cnt == scev->linked_cap) {
+ cap = max(scev->linked_cap * 2, 16);
+ tmp = kvrealloc(scev->linked, array_size(cap, sizeof(*scev->linked)),
+ GFP_KERNEL_ACCOUNT);
+ if (!tmp)
+ return -ENOMEM;
+ scev->linked = tmp;
+ scev->linked_cap = cap;
+ }
+ tested += r == insn->dst_reg || (src != dst && r == insn->src_reg);
+ scev->linked[scev->linked_cnt].reg = r;
+ scev->linked[scev->linked_cnt++].id = id;
+ }
+ /* nothing but the tested registers */
+ if (scev->linked_cnt - first == tested) {
+ scev->linked_cnt = first;
+ return 0;
+ }
+ if (!envs) {
+ envs = kzalloc(sizeof(*envs), GFP_KERNEL_ACCOUNT);
+ if (!envs)
+ return -ENOMEM;
+ scev->envs[idx] = envs;
+ }
+ envs->linked_first = first;
+ envs->linked_cnt = scev->linked_cnt - first;
+ return 0;
+}
+
+/*
+ * Find the next SCEV register that is linked to @regno at the conditional jump at @insn_idx: on
+ * every path to the jump *@linked_regno == @regno + *@delta (mod 2^*@bits). It is live there and of
+ * the same frame. It is a register, or MAX_BPF_REG + the index of a stack slot: a fill from byte
+ * *@fill_off of the slot reads these bits. *@pos is the position of the search, 0 at first. False:
+ * no more links are known. Types are not known here: the caller checks them in its state.
+ */
+bool bpf_scev_next_linked_reg(struct bpf_verifier_env *env, int insn_idx, u32 regno, u32 *pos,
+ u32 *linked_regno, u32 *fill_off, s64 *delta, u32 *bits)
+{
+ struct insn_envs *envs = env->scev->envs[insn_idx];
+ int h = bpf_loop_at_index(env, insn_idx);
+ struct scev_linked_reg *linked;
+ u32 i;
+
+ if (!envs || !envs->linked_cnt)
+ return false;
+ linked = &env->scev->linked[envs->linked_first];
+ /* registers come first */
+ for (i = 0; linked[i].reg != regno; i++)
+ if (i + 1 == envs->linked_cnt || linked[i].reg > regno)
+ return false;
+ for (; *pos < envs->linked_cnt; ++*pos) {
+ *linked_regno = linked[*pos].reg;
+ if (*pos != i &&
+ const_diff_reg(env, h, linked[i].id, *linked_regno, linked[*pos].id, fill_off,
+ delta, bits))
+ return true;
+ }
+ return false;
+}
+
/*
* @cur_header and @first are a loop header. Or -1 and the first insn of a subprog: the subprog is
* the outermost loop, which runs once. (REG r) is the entry value of r, the loops in it are single
@@ -1801,6 +2139,12 @@ static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header, i
collect_tested_regs(env, cur_env, idx,
cur_loop->tested_regs);
}
+ /* the others wait for the envs of all loop headers */
+ if (!aux[idx].need_scev) {
+ err = find_linked_regs(env, cur_env, -1, idx);
+ if (err)
+ goto out;
+ }
err = transfer(env, cur_env, idx);
if (err)
goto out;
@@ -1942,13 +2286,20 @@ static int simplify(struct scev *scev, u32 id)
if (is_add(scev, id, &l, &r) && is_imm(scev, r, &imm1) && !imm1)
return l;
- /* (+ (+ x imm) imm) -> (+ x imm) */
+ /* (+ (+ x imm) imm) -> (+ x imm). Not through a def, see alu64_with_def() */
if (is_add(scev, id, &l, &r) &&
is_imm(scev, r, &imm2) &&
is_add(scev, l, &a, &b) &&
- is_imm(scev, b, &imm1))
+ is_imm(scev, b, &imm1) && !scev->exprs[l].def.set)
return expr2(scev, BPF_ADD, a, imm_expr(scev, (s64)((u64)imm1 + (u64)imm2)));
+ /* 'r1 <<= 32; r1 >>= 32' is (u32)r1 for cpu v1 and v2: (>> (<< x 32) 32) -> (zext32 x) */
+ if (is_binop(scev, BPF_RSH, id, &l, &r) &&
+ is_imm(scev, r, &imm2) && imm2 == 32 &&
+ is_binop(scev, BPF_LSH, l, &a, &b) &&
+ is_imm(scev, b, &imm1) && imm1 == 32 && !scev->exprs[l].def.set)
+ return expr1(scev, ZEXT32, a);
+
/* (+ (linear base slope) imm) -> (linear (+ base imm) slope) */
if (is_add(scev, id, &l, &r) &&
is_linear(scev, l, &base, &slope) &&
@@ -1965,12 +2316,18 @@ static int simplify(struct scev *scev, u32 id)
if (is_zext32(scev, id, &l) && is_imm(scev, l, &imm1))
return imm_expr(scev, (u64)(u32)imm1);
+ /* The rules below look through the operand, not through a phi: see struct expr_def */
+ if (op_low_bits(op) && scev->exprs[scev->exprs[id].params[0]].def.phi)
+ return id;
+
/*
* (zext32 (+ (zext32 $a) $b)) -> (zext32 (+ $a $b))
* (zext32 (+ $a (zext32 $b))) -> (zext32 (+ $a $b))
*/
- if ((is_zext32(scev, id, &t) && is_add(scev, t, &l, &b) && is_zext32(scev, l, &a)) ||
- (is_zext32(scev, id, &t) && is_add(scev, t, &a, &r) && is_zext32(scev, r, &b))) {
+ if ((is_zext32(scev, id, &t) && is_add(scev, t, &l, &b) && is_zext32(scev, l, &a) &&
+ !scev->exprs[l].def.phi) ||
+ (is_zext32(scev, id, &t) && is_add(scev, t, &a, &r) && is_zext32(scev, r, &b) &&
+ !scev->exprs[r].def.phi)) {
t = expr2(scev, BPF_ADD, a, b);
t = expr1(scev, ZEXT32, t);
return t;
@@ -1987,7 +2344,8 @@ static int simplify(struct scev *scev, u32 id)
*/
if (is_spill32(scev, id, &t) && is_zext32(scev, t, &a))
return expr1(scev, SPILL32, a);
- if (is_spill32(scev, id, &t) && is_add(scev, t, &l, &b) && is_zext32(scev, l, &a))
+ if (is_spill32(scev, id, &t) && is_add(scev, t, &l, &b) && is_zext32(scev, l, &a) &&
+ !scev->exprs[l].def.phi)
return expr1(scev, SPILL32, expr2(scev, BPF_ADD, a, b));
/*
@@ -1999,12 +2357,24 @@ static int simplify(struct scev *scev, u32 id)
if (op_low_bits(op) && is_spill(scev, scev->exprs[id].params[0], &bits, &a) &&
op_low_bits(op) <= bits)
return expr1(scev, op, a);
- if (is_zext32(scev, id, &t) && is_add(scev, t, &l, &b) && is_spill32(scev, l, &a))
+ if (is_zext32(scev, id, &t) && is_add(scev, t, &l, &b) && is_spill32(scev, l, &a) &&
+ !scev->exprs[l].def.phi)
return expr1(scev, ZEXT32, expr2(scev, BPF_ADD, a, b));
return id;
}
+static int instantiate_header_scevs(struct scev *scev, u32 id, void *priv)
+{
+ u32 reg, *reg2scev = priv;
+
+ /* (reg r) -> ... equation at header ... */
+ if (is_reg(scev, id, ®))
+ return reg2scev[reg];
+ /* keep expression shapes */
+ return id;
+}
+
static int compute_header_scevs(struct bpf_verifier_env *env, struct env *header_env)
{
struct scev *scev = env->scev;
@@ -2028,6 +2398,12 @@ static int compute_header_scevs(struct bpf_verifier_env *env, struct env *header
*/
for (ra = 0; ra < REGS_NUM; ra++) {
ra_expr = header_env->reg2expr[ra];
+ /* Constructed again it has no defs, see alu64_with_def(): they hide the shape */
+ if (env->prog->aux->arena)
+ ra_expr = transform_expr_once(scev, 0, ra_expr, scev->identity_env.reg2expr,
+ instantiate_header_scevs);
+ if (ra_expr < 0)
+ return ra_expr;
/* rA = (+ rA IMM) -> (linear rA IMM) */
if (is_add(scev, ra_expr, &l, &r) &&
is_reg(scev, l, &rb) &&
@@ -2158,17 +2534,6 @@ static void scev_compute_linear_regs(struct scev *scev, struct bpf_loop *loop,
}
}
-static int instantiate_header_scevs(struct scev *scev, u32 id, void *priv)
-{
- u32 reg, *reg2scev = priv;
-
- /* (reg r) -> ... equation at header ... */
- if (is_reg(scev, id, ®))
- return reg2scev[reg];
- /* keep expression shapes */
- return id;
-}
-
static int compute_insn_scevs(struct bpf_verifier_env *env, struct env *eheader, struct env *einsn)
{
struct scev *scev = env->scev;
@@ -2218,6 +2583,27 @@ static int log_scevs(struct bpf_verifier_env *env)
return 0;
}
+/*
+ * A slice comes from a call that may write to the buffer: the kfunc, or a subprog that calls it.
+ * See expr_is_vn().
+ */
+static void find_slice_slots(struct bpf_verifier_env *env)
+{
+ struct bpf_insn *insn = env->prog->insnsi;
+ struct scev *scev = env->scev;
+ bool slices = false;
+ u32 i, spi;
+
+ for (i = 0; i < env->prog->len; i++)
+ slices |= bpf_is_dynptr_slice_rdwr_kfunc(&insn[i]);
+ for (i = 0; slices && i < env->prog->len; i++)
+ if (BPF_CLASS(insn[i].code) == BPF_JMP && BPF_OP(insn[i].code) == BPF_CALL)
+ bitmap_or(scev->slice_slots, scev->slice_slots, bpf_may_write_mask(env, i),
+ MAX_BPF_STACK_SLOTS);
+ for_each_set_bit(spi, scev->slice_slots, MAX_BPF_STACK_SLOTS)
+ scev->exprs[REG_EXPR_ID(MAX_BPF_REG + spi)].is_vn = false;
+}
+
int bpf_compute_scev(struct bpf_verifier_env *env)
{
struct bpf_insn_aux_data *aux = env->insn_aux_data;
@@ -2226,6 +2612,7 @@ int bpf_compute_scev(struct bpf_verifier_env *env)
int cnt = env->cfg.cur_postorder;
int i, idx, err, header;
+ find_slice_slots(env);
mark_latches(env);
/*
* Visit loop headers in postorder, to guarantee that scevs
@@ -2254,6 +2641,9 @@ int bpf_compute_scev(struct bpf_verifier_env *env)
idx = postorder[i];
if (!aux[idx].need_scev)
continue;
+ if (signal_pending(current))
+ return -EAGAIN;
+ cond_resched();
header = bpf_loop_at_index(env, idx);
err = aux[idx].loop
? compute_header_scevs(env, find_header_env(scev, header))
@@ -2265,6 +2655,10 @@ int bpf_compute_scev(struct bpf_verifier_env *env)
if (aux[idx].loop)
scev_compute_linear_regs(scev, aux[idx].loop,
find_header_env(scev, header));
+ err = find_linked_regs(env, aux[idx].loop ? &scev->identity_env :
+ find_loop_env(scev, header, idx), header, idx);
+ if (err)
+ return err;
}
/* Now it's known which jumps test a counter: the places of a back edge are for them */
@@ -2281,6 +2675,21 @@ int bpf_compute_scev(struct bpf_verifier_env *env)
return 0;
}
+/*
+ * Are all slots of the buffer [off, off + size) in slice_slots? Without @fixed_off the buffer is
+ * somewhere in the range, and the range can be wider than slice_slots: two offsets become a range
+ * at a join point.
+ */
+bool bpf_scev_in_slice_slots(struct bpf_verifier_env *env, int off, u32 size, bool fixed_off)
+{
+ int first = bpf_get_spi(off + size - 1), last = bpf_get_spi(off);
+ const unsigned long *slots = env->scev->slice_slots;
+
+ if (fixed_off)
+ return find_next_zero_bit(slots, last + 1, first) > last;
+ return find_next_bit(slots, last + 1, first) <= last;
+}
+
void bpf_free_scev(struct bpf_verifier_env *env)
{
struct scev *scev = env->scev;
@@ -2303,6 +2712,7 @@ void bpf_free_scev(struct bpf_verifier_env *env)
kvfree(scev->worklist.data);
kvfree(scev->envs);
kvfree(scev->exprs);
+ kvfree(scev->linked);
kvfree(scev->discovered);
kfree(scev);
env->scev = NULL;
@@ -2319,6 +2729,7 @@ int bpf_init_scev(struct bpf_verifier_env *env)
env->scev = scev;
bpf_mark_reg_known_scalar(&scev->zero_reg, 0);
bpf_mark_reg_unknown(&scev->unknown_reg);
+ setup_initial_loop_env(&scev->identity_env);
if (grow_exprs(scev, roundup_pow_of_two(REG_EXPR_ID(REGS_NUM))))
goto nomem;
scev->exprs[UNKNOWN_EXPR_ID] = (struct expr){ .op = UNKNOWN };
@@ -2326,6 +2737,8 @@ int bpf_init_scev(struct bpf_verifier_env *env)
for (r = 0; r < REGS_NUM; r++)
scev->exprs[REG_EXPR_ID(r)] = (struct expr){ .op = REG, .is_vn = true,
.params = { r, 0 } };
+ for (r = 0; r < REG_EXPR_ID(REGS_NUM); r++)
+ set_expr_base(scev, &scev->exprs[r], r);
scev->exprs_cnt = REG_EXPR_ID(REGS_NUM);
scev->envs = kvcalloc(env->prog->len, sizeof(*scev->envs), GFP_KERNEL_ACCOUNT);
scev->discovered = kvcalloc(env->prog->len, sizeof(*scev->discovered), GFP_KERNEL_ACCOUNT);
@@ -2394,6 +2807,10 @@ static struct bpf_reg_state *scev_reg_for_spill32(struct bpf_func_state *st, u32
{
struct bpf_stack_state *stack = scev_regno_to_stack(st, r);
+#ifdef __BIG_ENDIAN
+ /* a 32-bit spill is in the upper 32 bits of the slot */
+ return NULL;
+#endif
if (!stack || bpf_spill_size(stack) != 4 || stack->spilled_ptr.type != SCALAR_VALUE)
return NULL;
return &stack->spilled_ptr;
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index bc0937b98818..138581a759bb 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -15194,6 +15194,12 @@ bool bpf_is_throw_kfunc(struct bpf_insn *insn)
insn->imm == special_kfunc_list[KF_bpf_throw];
}
+bool bpf_is_dynptr_slice_rdwr_kfunc(struct bpf_insn *insn)
+{
+ return bpf_pseudo_kfunc_call(insn) && insn->off == 0 &&
+ insn->imm == special_kfunc_list[KF_bpf_dynptr_slice_rdwr];
+}
+
static bool is_bpf_wq_set_callback_kfunc(u32 btf_id)
{
return btf_id == special_kfunc_list[KF_bpf_wq_set_callback];
@@ -16018,6 +16024,15 @@ static int check_special_kfunc(struct bpf_verifier_env *env, struct bpf_call_arg
regs[BPF_REG_0].buf = meta->slice_buf;
if (meta->slice_buf.size)
env->seen_stack_slice = true;
+ /* No insn stores through a slice of 0 bytes */
+ if (verifier_bug_if(meta->slice_buf.size && meta->arg_constant.value &&
+ !type_is_rdonly_mem(regs[BPF_REG_0].type) &&
+ !bpf_scev_in_slice_slots(env, meta->slice_buf.off,
+ meta->slice_buf.size,
+ meta->slice_buf.size ==
+ meta->arg_constant.value),
+ env, "slice buffer is not in scev slice_slots"))
+ return -EFAULT;
} else {
return 0;
}
@@ -19694,6 +19709,101 @@ static void sync_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier_s
}
}
+/* a == b (mod 2^N) implies a == b if both fit in uN or both fit in sN. N is @bits. */
+static bool regs_fit_in_bits(const struct bpf_reg_state *a, const struct bpf_reg_state *b,
+ u32 bits)
+{
+ u64 umax = GENMASK_ULL(bits - 1, 0);
+ s64 smax = umax >> 1;
+
+ return (reg_umax(a) <= umax && reg_umax(b) <= umax) ||
+ (reg_smin(a) >= -smax - 1 && reg_smax(a) <= smax &&
+ reg_smin(b) >= -smax - 1 && reg_smax(b) <= smax);
+}
+
+/*
+ * The state of the SCEV register @r. As in check_stack_read_fixed_off() a fill of @bits bits from
+ * byte @fill_off of the slot reads the low bits of the spill.
+ */
+static struct bpf_reg_state *scev_reg_for_fill(struct bpf_func_state *frame, u32 r, u32 fill_off,
+ u32 bits)
+{
+ struct bpf_stack_state *ss;
+
+ if (r < MAX_BPF_REG)
+ return &frame->regs[r];
+ if (r - MAX_BPF_REG >= bpf_stack_nr_slots(frame))
+ return NULL;
+ ss = bpf_stack_slot(frame, r - MAX_BPF_REG);
+ if (!bpf_is_spilled_reg(ss) || bpf_spill_size(ss) * BITS_PER_BYTE < bits ||
+ !bpf_stack_narrow_access_ok(fill_off, bits / BITS_PER_BYTE, bpf_spill_size(ss)))
+ return NULL;
+ return &ss->spilled_ptr;
+}
+
+/*
+ * The jump at @idx refined @regno. A register that scalar evolution links to it holds
+ * @regno + delta on every path to the jump: intersect the two. The intersection is empty: no
+ * execution gets here with this state (the path is infeasible), the register is left as it is.
+ */
+static void refine_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier_state *vstate,
+ int idx, u32 regno)
+{
+ struct bpf_func_state *frame = vstate->frame[vstate->curframe];
+ struct bpf_reg_state *reg, implied, saved;
+ u32 pos, linked_regno, fill_off, bits;
+ s64 delta;
+
+ if (frame->regs[regno].type != SCALAR_VALUE)
+ return;
+ for (pos = 0;
+ bpf_scev_next_linked_reg(env, idx, regno, &pos, &linked_regno, &fill_off, &delta,
+ &bits);
+ pos++) {
+ reg = scev_reg_for_fill(frame, linked_regno, fill_off, bits);
+ /*
+ * The other half-slot of a 64-bit spill is dead: __clean_func_state() left
+ * a 32-bit spill, the low 32 bits of the value.
+ */
+ if (!reg && bits == 64 &&
+ bpf_stack_narrow_access_ok(fill_off, BPF_REG_SIZE / 2, BPF_REG_SIZE)) {
+ bits = 32;
+ reg = scev_reg_for_fill(frame, linked_regno, fill_off, bits);
+ }
+ if (!reg || reg->type != SCALAR_VALUE)
+ continue;
+
+ implied = frame->regs[regno];
+ add_scalar_to_reg(&implied, delta);
+ if (regs_fit_in_bits(reg, &implied, bits))
+ bits = 64;
+ if (bits < 32)
+ continue;
+ /* tnum_intersect() of two tnums that don't overlap is not empty */
+ if (!tnum_overlap(bits == 32 ? tnum_subreg(reg->var_off) : reg->var_off,
+ bits == 32 ? tnum_subreg(implied.var_off) : implied.var_off))
+ continue;
+ saved = *reg;
+ /*
+ * Equal in 64 bits is equal in the low 32 bits too. 'if w1 s> 200' on a zero
+ * extended int refines nothing but the 32-bit range.
+ */
+ regs_refine_cond_op(reg, &implied, BPF_JEQ, true);
+ if (bits == 64)
+ regs_refine_cond_op(reg, &implied, BPF_JEQ, false);
+ reg_bounds_sync(reg);
+ if (reg_is_empty(reg) || const_tnum_range_mismatch(reg) ||
+ const_tnum_range_mismatch_32(reg)) {
+ *reg = saved;
+ continue;
+ }
+ if (linked_regno < MAX_BPF_REG)
+ mark_reg_scratched(env, linked_regno);
+ else
+ mark_stack_slot_scratched(env, linked_regno - MAX_BPF_REG);
+ }
+}
+
/* It's known which way the jump at env->insn_idx goes */
static void mark_loop_exit_decided(struct bpf_verifier_env *env, struct bpf_verifier_state *st)
{
@@ -19866,6 +19976,14 @@ static int check_cond_jmp_op(struct bpf_verifier_env *env,
sync_linked_regs(env, other_branch, &other_branch_regs[insn->dst_reg],
&linked_regs);
}
+ if (env->bpf_capable) {
+ if (BPF_SRC(insn->code) == BPF_X) {
+ refine_linked_regs(env, this_branch, *insn_idx, insn->src_reg);
+ refine_linked_regs(env, other_branch, *insn_idx, insn->src_reg);
+ }
+ refine_linked_regs(env, this_branch, *insn_idx, insn->dst_reg);
+ refine_linked_regs(env, other_branch, *insn_idx, insn->dst_reg);
+ }
/* if one pointer register is compared to another pointer
* register check if PTR_MAYBE_NULL could be lifted.
diff --git a/tools/testing/selftests/bpf/progs/verifier_arena.c b/tools/testing/selftests/bpf/progs/verifier_arena.c
index cadb13f023d2..bebb607000a5 100644
--- a/tools/testing/selftests/bpf/progs/verifier_arena.c
+++ b/tools/testing/selftests/bpf/progs/verifier_arena.c
@@ -597,6 +597,34 @@ __naked void mixed_arena_scalar_alu64_arena_first(void)
: __clobber_all);
}
+/* The offset of a pointer to arena is not tracked */
+#define ARENA_TEST(name, op, expect) \
+SEC("syscall") \
+__log_level(2) \
+expect \
+__naked void name(void) \
+{ \
+ asm volatile ( \
+ "r1 = %[arena] ll;" \
+ "r6 = 0;" \
+ "r6 = addr_space_cast(r6, 0, 1);" \
+ "r7 = 0;" \
+"1: *(u64 *)(r6 + 0) = r7;" \
+ op \
+ "r7 += 1;" \
+ "if r7 < 100 goto 1b;" \
+ "r0 = 0;" \
+ "exit;" \
+ :: __imm_addr(arena) : __clobber_all); \
+}
+
+#define NOT_UNROLLED100 __msg("loop header at 5, widening r7 to 0..99 step 1")
+/* A loop that moves the pointer is not unrolled */
+ARENA_TEST(arena_pointer_moves, "r6 += 8;", __success NOT_UNROLLED100)
+/* The loop makes a number of it: the state that returns to the loop header tells */
+ARENA_TEST(arena_pointer_becomes_a_number, "if r7 != 5 goto +1; r6 = 4096;",
+ __failure NOT_UNROLLED100 __msg("R6 invalid mem access 'scalar'"))
+
SEC("syscall")
__description("arena: a number is not an address in arena")
__failure __msg("R6 invalid mem access 'scalar'")
diff --git a/tools/testing/selftests/bpf/progs/verifier_bounds.c b/tools/testing/selftests/bpf/progs/verifier_bounds.c
index 316e9377caa8..94d52154b0c6 100644
--- a/tools/testing/selftests/bpf/progs/verifier_bounds.c
+++ b/tools/testing/selftests/bpf/progs/verifier_bounds.c
@@ -1572,7 +1572,7 @@ l0_%=: r0 = 0; \
SEC("socket")
__description("bounds deduction cross sign boundary, negative overlap")
__success __log_level(2)
-__msg("7: (1f) r0 -= r6 {{.*}} R0=scalar(smin=smin32=-655,smax=smax32=-146,umin=0xfffffffffffffd71,umax=0xffffffffffffff6e,umin32=0xfffffd71,umax32=0xffffff6e,var_off=(0xfffffffffffffc00; 0x3ff))")
+__msg("7: (1f) r0 -= r6 {{.*}} R0=scalar(smin=smin32=-527,smax=smax32=-146,umin=0xfffffffffffffdf1,umax=0xffffffffffffff6e,umin32=0xfffffdf1,umax32=0xffffff6e,var_off=(0xfffffffffffffc00; 0x3ff))")
__retval(0)
__naked void bounds_deduct_negative_overlap(void)
{
diff --git a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c
index caae67feaf5b..42b9e46a88af 100644
--- a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c
+++ b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c
@@ -190,10 +190,9 @@ l0_%=: \
: __clobber_all);
}
-/* Double ADD clears the ID (can't accumulate offsets) */
+/* Double ADD clears the ID. That r1 = r0 + 4 is known all the same: r0 /= 0 is dead code */
SEC("socket")
-__failure
-__msg("div by zero")
+__success
__naked void scalars_double_add(void)
{
asm volatile (" \
diff --git a/tools/testing/selftests/bpf/progs/verifier_reg_equal.c b/tools/testing/selftests/bpf/progs/verifier_reg_equal.c
index dc1d8c30fb0e..91daa5b323c9 100644
--- a/tools/testing/selftests/bpf/progs/verifier_reg_equal.c
+++ b/tools/testing/selftests/bpf/progs/verifier_reg_equal.c
@@ -31,17 +31,16 @@ l1_%=: exit; \
}
SEC("socket")
-__description("check w reg not equal if r reg upper32 bits not 0")
-__failure __msg("R1 !read_ok")
+__description("check w reg equal if r reg upper32 bits not 0")
+__success
__naked void subreg_equality_2(void)
{
asm volatile (" \
call %[bpf_ktime_get_ns]; \
r2 = r0; \
- /* Upper 4-bytes of r2 may not be 0, thus insn \
- * w3 = w2 should not propagate reg id, and \
- * w2 < 9 comparison should not propagate \
- * the range for r3 either. \
+ /* Upper 4-bytes of r2 may not be 0. r3 is the \
+ * lower 4-bytes of r2, which is what w2 < 9 \
+ * compares. \
*/ \
w3 = w2; \
if w2 < 9 goto l0_%=; \
diff --git a/tools/testing/selftests/bpf/progs/verifier_scalar_ids.c b/tools/testing/selftests/bpf/progs/verifier_scalar_ids.c
index 9ea61b162736..819d0a1a6886 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scalar_ids.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scalar_ids.c
@@ -288,9 +288,9 @@ __success __log_level(2)
/*
* check that r5 is unlinked after 'if', collect_linked_regs() can't tie
* more than 5 registers for a single insn and the register compared by
- * the jump is not exempt from that.
+ * the jump is not exempt from that. The jump refines the copies all the same.
*/
-__msg("7: (25) if r5 > 0x7 goto pc+0 ; R5=scalar(smin=")
+__msg("7: (25) if r5 > 0x7 goto pc+0 ; R0=" LINKED("1", 7, 0x7) " R1=" LINKED("1", 7, 0x7) " R2=" LINKED("1", 7, 0x7) " R3=" LINKED("1", 7, 0x7) " R4=" LINKED("1", 7, 0x7) " R5=scalar(smin=")
__msg("12: (bf) r5 = r5 ; R5=scalar(id=2")
__naked void linked_regs_too_many_regs(void)
{
@@ -808,7 +808,7 @@ __naked void linked_regs_and_subreg_def(void)
* at runtime), which must be rejected.
*/
SEC("socket")
-__failure __msg("R0 max value is outside of the allowed memory range")
+__failure __msg("invalid access to map value, value_size=8 off=8 size=1")
__naked void ldsx_fill_scalar_id_not_shared(void)
{
asm volatile (" \
diff --git a/tools/testing/selftests/bpf/progs/verifier_scev.c b/tools/testing/selftests/bpf/progs/verifier_scev.c
index 6d1b8542f801..b961a12b5224 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scev.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scev.c
@@ -3338,42 +3338,6 @@ NOT_LINEAR_TEST(lsh_grows, "r7 &= 15;", "r7 <<= 1;", UNROLLED("(<< r7 1)"))
NOT_LINEAR_TEST(same_constant, "r7 = 17;", "r7 &= 31;", NOT_CHANGED)
NOT_LINEAR_TEST(same_wide_constant, "r7 = 0xffffffff00000011 ll;", "r7 &= -239;", NOT_CHANGED)
-#ifdef __BPF_FEATURE_ADDR_SPACE_CAST
-struct {
- __uint(type, BPF_MAP_TYPE_ARENA);
- __uint(map_flags, BPF_F_MMAPABLE);
- __uint(max_entries, 1);
-} arena SEC(".maps");
-
-/* The offset of a pointer to arena is not tracked */
-#define ARENA_TEST(name, op, expect) \
-SEC("syscall") \
-__log_level(2) \
-expect \
-__naked void name(void) \
-{ \
- asm volatile ( \
- "r1 = %[arena] ll;" \
- "r6 = 0;" \
- "r6 = addr_space_cast(r6, 0, 1);" \
- "r7 = 0;" \
-"1: *(u64 *)(r6 + 0) = r7;" \
- op \
- "r7 += 1;" \
- "if r7 < 100 goto 1b;" \
- "r0 = 0;" \
- "exit;" \
- :: __imm_addr(arena) : __clobber_all); \
-}
-
-#define NOT_UNROLLED100 __msg("loop header at 5, widening r7 to 0..99 step 1")
-/* A loop that moves the pointer is not unrolled */
-ARENA_TEST(arena_pointer_moves, "r6 += 8;", __success NOT_UNROLLED100)
-/* The loop makes a number of it: the state that returns to the loop header tells */
-ARENA_TEST(arena_pointer_becomes_a_number, "if r7 != 5 goto +1; r6 = 4096;",
- __failure NOT_UNROLLED100 __msg("R6 invalid mem access 'scalar'"))
-#endif
-
/*
* r7 = ctx->data;
* r8 = r7
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 13/29] bpf: Value number the phis of scalar evolution
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (11 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 12/29] bpf: Refine registers linked by scalar evolution at conditional jumps Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 8:27 ` bot+bpf-ci
2026-11-11 5:13 ` [PATCH bpf-next v2 14/29] selftests/bpf: Test registers linked by " Alexei Starovoitov
` (15 subsequent siblings)
28 siblings, 1 reply; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A prerequisite of the removal of scalar ids. Two registers are linked if
their expressions have the same base, and so far every phi is a base of
its own. A copy that is made on each of two paths is not linked after the
join point:
if (c) { n = f(); m = n; } else { n = g(); m = n; }
if (n > 100)
return 0;
use(buf[m]);
n is %r6.9=phi(_), m is %r7.9=phi(_): two value numbers. Scalar ids link
them, each path gives n and m one id. recvfrom_x of Falco's modern_bpf
probe depends on it: 'long ret' lives in a stack slot, is filled on each of
two paths, tested after the join point and filled again. Without ids:
R2 unbounded memory access, use 'var &= const' or 'if (var < const)'
Number phis as global value numbering does, by join point and operands
(Alpern, Wegman, Zadeck 1988; LLVM's GVN and NewGVN), up to a constant
offset (InstCombine's foldPHIArgBinOpIntoPHI()). With a == base_a + off_a
on the edges so far and b == base_b + off_b on this edge
phi(a, b) == phi(base_a, base_b + off_b - off_a) + off_a
The phi of the bases is hashed by the join point and its two operands,
not by the register. All registers that have a constant difference on
each edge get the same one, and const_diff() finds them like 'x' and
'x + 4'. There is no search for pairs. The next edge merges the phi of
the bases with the next base, so the id stands for the list of operands,
for any number of edges.
The base is a part of the key of the phi of a register: the shape of a
phi can be the same after the second and after the third edge, its base
is not. The level 2 log prints the phi of a register as before, without
its base.
It is sound because the equation above holds on each edge by what
split_const_offset64() says about a and b there, and an execution gets to
the join point over one of the edges. The body of a loop without its
backedges is acyclic: no edge brings a phi of the join point itself, a phi
of bases is one value per iteration like every other def.
Phis are numbered in 64 bits: split_const_offset64() strips no extension
and no partial spill from an operand. (zext32 x) is a value number of its
own there. That it is x modulo 2^32 is not seen after a join point.
The selftest "add_const base_id must be consistent for pruning" expects
a rejection, but its r2 and r4 are both 'r0 & 0xff': r3 == r2 + 10 on both
paths, the prog is safe and loads now. 'r4 >>= 8' makes r4 the unrelated
value that the test wants. With it the prog is rejected before and after
the patch.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/scev.c | 97 +++++++++++++++++--
.../bpf/progs/verifier_linked_scalars.c | 2 +
2 files changed, 89 insertions(+), 10 deletions(-)
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index 5506e0d4e534..f45abe90d498 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -61,8 +61,8 @@ enum expr_op {
* executes at most once per iteration, so a def is one value per iteration. A name is unique within
* the env of one loop.
* The operands of a phi are a set, 8 at most; a phi(a, b) among them is expanded. They have no
- * defs, see strip_defs(): (+ r6 %r2.4) and (+ r6 %r2.7) are one operand, (+ r6 _). Every phi is
- * a value number of its own.
+ * defs, see strip_defs(): (+ r6 %r2.4) and (+ r6 %r2.7) are one operand, (+ r6 _). So they don't
+ * tell one phi from another: phis are value numbered by their bases, see mk_phi().
* In the env of loop H a phi at H merges the backedge values only, the header phi itself is
* (REG r). In the env of the enclosing loop a phi at H merges the entering edges.
* Op and params of a def by an insn are what that insn computes, see alu64_with_def():
@@ -76,7 +76,9 @@ struct expr_def {
u32 reg:9;
u32 idx:21;
};
-static_assert(REGS_NUM <= 1 << 9 && BPF_COMPLEXITY_LIMIT_INSNS <= 1 << 21);
+/* def.reg of the phi of two bases, see mk_phi() */
+#define BASE_REG REGS_NUM
+static_assert(BASE_REG < 1 << 9 && BPF_COMPLEXITY_LIMIT_INSNS <= 1 << 21);
static const struct expr_def no_def;
@@ -186,9 +188,16 @@ static bool same_def(struct expr_def a, struct expr_def b)
return a.set == b.set && a.phi == b.phi && a.reg == b.reg && a.idx == b.idx;
}
-static int expr_eq(struct expr *a, struct expr *b)
+/* Is @e the expression @id? The base of a phi is a part of its key. 0: the phi is its own base. */
+static int expr_eq(struct scev *scev, struct expr *e, u32 id)
{
- return a->op == b->op && a->imm == b->imm && same_def(a->def, b->def);
+ struct expr *x = &scev->exprs[id];
+
+ if (e->op != x->op || e->imm != x->imm || !same_def(e->def, x->def))
+ return false;
+ if (!e->def.phi)
+ return true;
+ return e->base ? e->base == x->base && e->off == x->off : x->base == id;
}
static void exprs_ht_add(struct scev *scev, u32 id)
@@ -239,7 +248,8 @@ static bool expr_is_vn(struct scev *scev, struct expr *e)
* comes from a call that may write to the slot.
*/
if (e->def.set)
- return reg < MAX_BPF_REG || !test_bit(reg - MAX_BPF_REG, scev->slice_slots);
+ return reg < MAX_BPF_REG || reg == BASE_REG ||
+ !test_bit(reg - MAX_BPF_REG, scev->slice_slots);
switch (e->op) {
case UNKNOWN:
case OPAQUE:
@@ -272,10 +282,13 @@ static void set_expr_base(struct scev *scev, struct expr *e, u32 id)
struct expr *x;
s64 imm = 0;
+ e->arena_add = 0;
+ /* mk_phi() found the base */
+ if (e->def.phi && e->base)
+ return;
e->base = id;
e->off = 0;
e->bits = 64;
- e->arena_add = 0;
/* nothing looks through a phi, see struct expr_def */
if (e->def.phi)
return;
@@ -301,7 +314,7 @@ static int add_expr(struct scev *scev, struct expr e)
id = scev->exprs_ht[expr_hash(&e) & (scev->exprs_cap - 1)];
for (; id; id = scev->exprs_next[id])
- if (expr_eq(&e, &scev->exprs[id]))
+ if (expr_eq(scev, &e, id))
return id;
if (scev->exprs_cnt == scev->exprs_cap && grow_exprs(scev, scev->exprs_cap * 2))
@@ -378,6 +391,7 @@ static int expr_with_def(struct scev *scev, int id, struct expr_def def)
return id;
e = scev->exprs[id];
e.def = def;
+ e.base = 0;
return add_expr(scev, e);
}
@@ -1366,6 +1380,69 @@ next:;
return expr_with_def(scev, id, def);
}
+static void split_const_offset(struct bpf_verifier_env *env, u32 id, u32 *base, s64 *off,
+ u32 *bits);
+
+/* @id == *@base + *@off in 64 bits: no extension and no partial spill is stripped */
+static void split_const_offset64(struct bpf_verifier_env *env, u32 id, u32 *base, s64 *off)
+{
+ struct scev *scev = env->scev;
+ u32 bits, l, r;
+
+ split_const_offset(env, id, base, off, &bits);
+ if (bits == 64)
+ return;
+ *base = id;
+ *off = 0;
+ if (!scev->exprs[id].def.set && is_add(scev, id, &l, &r) && is_imm(scev, r, off))
+ *base = l;
+}
+
+/*
+ * if (c) { n = f(); m = n; } else { n = g(); m = n; } if (n > 100) return 0; use(m);
+ * The phi of @reg before the insn at @idx. It merges @a, which came over the edges so far, and @b.
+ * Phis are value numbered as GVN does it, by join point and operands, up to a constant offset:
+ * a == base_a + off_a and b == base_b + off_b, so
+ * phi == phi(base_a, base_b + off_b - off_a) + off_a
+ * The phi of the bases is hashed without @reg. All registers that have a constant difference on
+ * each edge get the same one, and const_diff() finds them as it finds 'x' and 'x + 4'. The next
+ * edge merges the phi of the bases with the next base: the id stands for the list of the operands,
+ * for any number of edges. No edge brings a phi of this join point, so every phi here gets a new
+ * base with every edge.
+ */
+static int mk_phi(struct bpf_verifier_env *env, u32 a, u32 b, int idx, u32 reg)
+{
+ struct scev *scev = env->scev;
+ u32 base_a, base_b;
+ s64 off_a, off_b;
+ struct expr e;
+ int id;
+
+ id = mk_any(scev, a, b, no_def);
+ if (id < 0)
+ return id;
+ e = scev->exprs[id];
+ e.def = phi_def(idx, reg);
+ e.base = 0;
+ split_const_offset64(env, a, &base_a, &off_a);
+ split_const_offset64(env, b, &base_b, &off_b);
+ if (!expr_is_vn(scev, &e) || !scev->exprs[base_a].is_vn || !scev->exprs[base_b].is_vn)
+ return add_expr(scev, e);
+ id = base_b;
+ if (off_a != off_b)
+ id = expr2(scev, BPF_ADD, base_b, imm_expr(scev, (u64)off_b - off_a));
+ if (id < 0)
+ return id;
+ id = add_expr(scev, (struct expr){ .op = ANY, .params = { base_a, id },
+ .def = phi_def(idx, BASE_REG) });
+ if (id < 0)
+ return id;
+ e.base = id;
+ e.off = off_a;
+ e.bits = 64;
+ return add_expr(scev, e);
+}
+
/* @acc is the env before the insn at @idx */
static int join(struct bpf_verifier_env *env, struct env *acc, struct env *cur, int idx)
{
@@ -1382,8 +1459,8 @@ static int join(struct bpf_verifier_env *env, struct env *acc, struct env *cur,
if (!same_exprs(scev, acc->reg2expr[i], cur->reg2expr[i])) {
/* no phi for a dead register: pruned SSA */
id = reg_alive_at(env, i, idx) ?
- mk_any(scev, acc->reg2expr[i], cur->reg2expr[i],
- phi_def(idx, i)) : UNKNOWN_EXPR_ID;
+ mk_phi(env, acc->reg2expr[i], cur->reg2expr[i], idx, i) :
+ UNKNOWN_EXPR_ID;
if (id < 0)
return id;
acc->reg2expr[i] = id;
diff --git a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c
index 42b9e46a88af..c8b3690dd810 100644
--- a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c
+++ b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c
@@ -676,6 +676,7 @@ l_false_%=: \
r2 = r0; \
r2 &= 0xff; /* R2 = scalar(id=A) [0,255] */ \
r4 = r0; \
+ r4 >>= 8; \
r4 &= 0xff; /* R4 = scalar [0,255], id=0 */ \
r3 = r2; /* R3 linked to R2 (id=A) */ \
r3 += 10; /* R3 id=A|ADD_CONST, delta=10 */\
@@ -688,6 +689,7 @@ l_true_%=: \
r2 = r0; \
r2 &= 0xff; /* R2 = scalar [0,255], id=0 */ \
r4 = r0; \
+ r4 >>= 8; \
r4 &= 0xff; /* R4 = scalar [0,255], id=0 */ \
r3 = r4; /* R3 linked to R4 (new id=C) */\
r3 += 10; /* R3 id=C|ADD_CONST, delta=10 */\
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* Re: [PATCH bpf-next v2 13/29] bpf: Value number the phis of scalar evolution
2026-11-11 5:13 ` [PATCH bpf-next v2 13/29] bpf: Value number the phis of scalar evolution Alexei Starovoitov
@ 2026-11-11 8:27 ` bot+bpf-ci
0 siblings, 0 replies; 36+ messages in thread
From: bot+bpf-ci @ 2026-11-11 8:27 UTC (permalink / raw)
To: alexei.starovoitov, bpf
Cc: daniel, andrii, eddyz87, memxor, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 1419 bytes --]
> bpf: Value number the phis of scalar evolution
TL;DR: Nit only: the header comment of set_expr_base() in kernel/bpf/scev.c is
now stale, since phis given a base by mk_phi() are no longer their own base;
no functional bug.
> diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
> --- a/kernel/bpf/scev.c
> +++ b/kernel/bpf/scev.c
[ ... ]
> @@ -272,10 +282,13 @@ static void set_expr_base(struct scev *scev, struct expr *e, u32 id)
> struct expr *x;
> s64 imm = 0;
>
> + e->arena_add = 0;
> + /* mk_phi() found the base */
> + if (e->def.phi && e->base)
> + return;
> e->base = id;
> e->off = 0;
> e->bits = 64;
This isn't a bug, but the header comment of set_expr_base() was not updated
and now contradicts this early return:
* @e gets the id @id. (+ x imm), an extension and a partial spill of x have
* the base of x, which was constructed earlier. Every other expression is its
* own base.
A phi that mk_phi() value numbered reaches here with e->base set to the phi
of the bases, so it is not its own base.
Could the sentence name that case as an exception, for example "Every other
expression is its own base, except a phi that mk_phi() gave a base."?
---
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/42982641817
^ permalink raw reply [flat|nested] 36+ messages in thread
* [PATCH bpf-next v2 14/29] selftests/bpf: Test registers linked by scalar evolution
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (12 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 13/29] bpf: Value number the phis of scalar evolution Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 15/29] selftests/bpf: Test sign extensions of a range and of a linked register Alexei Starovoitov
` (14 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
Tests for the patch that refines the linked registers of a conditional
jump, and for the value numbering of phis. Linked registers are the live
registers and stack slots that are a tested register + a constant on every
path to the jump.
The existing tests of linked scalars, spills and loops cover the mechanism:
a copy, an added constant, the low 32 bits, a spill and its fill, two
calls, a store through a pointer. These tests are for the hazards that only
a static analysis has. scev.c computes the links before the main pass, from
the insns alone, without types and without states.
r8 is an unbounded scalar. Each prog computes a second value from it, tests
one of the two and uses the other one as an offset into a map value of
64 bytes. What loads is run.
- An add that becomes ALU32: 'r3 += 1' is converted to 'w3 += 1' at fixup
if r3 is an arena pointer. r3 == r7 + 1 holds modulo 2^32 only, also
after 'r3 = le64 r3; r7 = le64 r7' has turned both into scalars, and
also for the phi of two such adds. Rejected. A scalar + two constants
in a prog with an arena is linked modulo 2^64: loads.
- A store through a dynptr slice may alias the buffer on the stack: a fill
before the store and a fill after it are not linked. Rejected. One fill
and its copy are linked, whatever is stored after the fill. Loads.
- The buffer of a slice is at one of two stack offsets, a range in the
state, with a live slot between them. A slice of 0 bytes has its buffer
at a variable offset. Both load.
- An edge that the verifier adds: each iteration of the outer loop saves
r3, which is r8, and restores it. may_goto that the verifier adds before
the header of the nested loop leaves both loops while r3 is r7.
Rejected.
- A link at a loop header: two induction variables that are linked at loop
entry and have the same step are linked there. Negative test: the steps
differ.
- A pointer is not a scalar: r3 is the address in r1 as a scalar. A bound
of r3 does not bound the offset of r1. Rejected.
- Big endian: a 32-bit spill goes to the first half-slot. The low 32 bits
of a 64-bit fill are that spill on little endian only, also if the slot
is a phi. Loads there, rejected on big endian. The same for a 64-bit
spill of which the first half-slot is filled: the other one is dead, the
state has a 32-bit spill at the jump.
- An empty intersection: the range that the jump implies and the known
bits of the linked register don't overlap. The register is left as it
is: loads with BPF_F_TEST_REG_INVARIANTS. The same if var_off of the
two registers don't overlap.
- A link modulo 2^8: after 'r2 = (s8)r8; if r8 > 63 goto' both fit in s8,
so r2 is refined. Loads.
- A link in 64 bits and a JMP32: 'if w3 s> 63' refines nothing but the
32-bit range of r3. The registers that are equal to r3 get it. Loads.
- Value numbered phis: a copy + 1 on each of two paths is linked after
the join point. Negative tests: another delta on one path, unrelated
values on one of three edges. (zext32 x) on both paths is linked in 64
bits.
The tests with an arena are in verifier_arena.c, the others in
verifier_scev.c.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
.../selftests/bpf/progs/verifier_arena.c | 39 +++++
.../selftests/bpf/progs/verifier_scev.c | 162 ++++++++++++++++++
2 files changed, 201 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/verifier_arena.c b/tools/testing/selftests/bpf/progs/verifier_arena.c
index bebb607000a5..bf50413f6c08 100644
--- a/tools/testing/selftests/bpf/progs/verifier_arena.c
+++ b/tools/testing/selftests/bpf/progs/verifier_arena.c
@@ -625,6 +625,45 @@ ARENA_TEST(arena_pointer_moves, "r6 += 8;", __success NOT_UNROLLED100)
ARENA_TEST(arena_pointer_becomes_a_number, "if r7 != 5 goto +1; r6 = 4096;",
__failure NOT_UNROLLED100 __msg("R6 invalid mem access 'scalar'"))
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 1);
+ __type(key, __u32);
+ __type(value, char[64]);
+} map64 SEC(".maps");
+
+/*
+ * r6: 64 bytes. r8: an unbounded scalar. r3, r7: an arena pointer and a copy of it.
+ * The test leaves an address in r1.
+ */
+#define ARENA_LINKED(name, expect, body) \
+SEC("syscall") expect __load_if_JITed() \
+__naked void name(void) \
+{ \
+ asm volatile ( \
+ "r1 = %[arena] ll; r1 = 0; *(u32 *)(r10 - 4) = r1; r2 = r10; r2 += -4;" \
+ "r1 = %[map64] ll; call %[bpf_map_lookup_elem]; if r0 == 0 goto out_%=; r6 = r0;" \
+ "call %[bpf_ktime_get_ns]; r8 = r0; r3 = addr_space_cast(r8, 0, 1); r7 = r3;" \
+ body \
+ "r0 = *(u8 *)(r1 + 0);" \
+ "out_%=: r0 = 0; exit;" \
+ : \
+ : __imm(bpf_map_lookup_elem), __imm(bpf_ktime_get_ns), \
+ __imm_addr(map64), __imm_addr(arena) \
+ : __clobber_all); \
+}
+
+ARENA_LINKED(scalar_plus_two_constants, __success __retval(0),
+ "r2 = r8; r2 += 1; r2 += 1; if r8 > 61 goto out_%=; r1 = r6; r1 += r2;")
+/* 'r3 += 1' is converted to 'w3 += 1': r3 is 0 and not 1 << 32 */
+ARENA_LINKED(arena_ptr_plus_constant, __failure,
+ "r3 += 1; r3 = le64 r3; r7 = le64 r7; r2 = 0xffffffff ll; if r7 != r2 goto out_%=;"
+ "r3 >>= 32; r3 -= 1; r1 = r6; r1 += r3;")
+/* At the join point r3 is phi((+ x 1)). The insn there, an add to a scalar, does not define it. */
+ARENA_LINKED(arena_ptr_plus_constant_on_two_paths, __failure,
+ "if r8 == 0 goto 1f; r3 += 1; goto 2f; 1: r3 += 1; 2: r8 += 1; r3 = le64 r3; r7 = le64 r7;"
+ "r2 = 0xffffffff ll; if r7 != r2 goto out_%=; r3 >>= 32; r3 -= 1; r1 = r6; r1 += r3;")
+
SEC("syscall")
__description("arena: a number is not an address in arena")
__failure __msg("R6 invalid mem access 'scalar'")
diff --git a/tools/testing/selftests/bpf/progs/verifier_scev.c b/tools/testing/selftests/bpf/progs/verifier_scev.c
index b961a12b5224..1f9aa7e691be 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scev.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scev.c
@@ -3338,6 +3338,168 @@ NOT_LINEAR_TEST(lsh_grows, "r7 &= 15;", "r7 <<= 1;", UNROLLED("(<< r7 1)"))
NOT_LINEAR_TEST(same_constant, "r7 = 17;", "r7 &= 31;", NOT_CHANGED)
NOT_LINEAR_TEST(same_wide_constant, "r7 = 0xffffffff00000011 ll;", "r7 &= -239;", NOT_CHANGED)
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 1);
+ __type(key, __u32);
+ __type(value, char[64]);
+} map64 SEC(".maps");
+
+/*
+ * A conditional jump refines the registers and stack slots that are linked to what it tests.
+ * r6: 64 bytes. r7: a scalar below 256. r8: an unbounded scalar. r9: the context.
+ * The test leaves an address in r1.
+ */
+#define LINKED_SEC(sec, name, expect, body) \
+SEC(sec) expect \
+__naked void name(void) \
+{ \
+ asm volatile ( \
+ "r9 = r1; r1 = 0; *(u32 *)(r10 - 4) = r1; r2 = r10; r2 += -4; r1 = %[map64] ll;" \
+ "call %[bpf_map_lookup_elem]; if r0 == 0 goto out_%=; r6 = r0;" \
+ "call %[bpf_get_prandom_u32]; r7 = r0; r7 &= 255;" \
+ "call %[bpf_ktime_get_ns]; r8 = r0;" \
+ body \
+ "r0 = *(u8 *)(r1 + 0);" \
+ "out_%=: r0 = 0; exit;" \
+ : \
+ : __imm(bpf_map_lookup_elem), __imm(bpf_get_prandom_u32), __imm(bpf_ktime_get_ns), \
+ __imm(bpf_dynptr_from_skb), __imm(bpf_dynptr_slice_rdwr), \
+ __imm(bpf_ringbuf_reserve), __imm(bpf_ringbuf_discard), \
+ __imm_addr(map64), __imm_addr(ringbuf) \
+ : __clobber_all); \
+}
+#define LINKED(name, expect, body) LINKED_SEC("socket", name, expect, body)
+
+#define VALID __success __retval(0)
+#define UNBOUND __failure __msg("unbounded min value")
+
+/* r3 is the address in r1 as a scalar. It does not bound the offset of r1. */
+LINKED(pointer_is_not_scalar, __failure,
+ "r7 &= 127; r1 = r6; r1 += r7; r3 = r1; r3 = le64 r3; if r3 < 64 goto out_%=; r1 += -64;")
+
+LINKED(sign_extension_that_fits, VALID, "r2 = (s8)r8; if r8 > 63 goto out_%=; r1 = r6; r1 += r2;")
+
+/* 'int n' in three registers. 'if w3 s> 63' refines only 32-bit ranges: those of the copies too. */
+LINKED(copies_get_the_32_bit_range, VALID,
+ "w2 = w8; r3 = r2; r4 = r2; if w3 s> 63 goto out_%=; if w4 s< 0 goto out_%=;"
+ "r1 = r6; r1 += r2;")
+
+/* A copy on each of two paths: r2 and r3 are phis with the same operands, up to a constant */
+#define TWO_PATHS(a, b) \
+ "if r7 > 9 goto l_%=; r2 = r8; " a " goto m_%=; l_%=: r2 = r8; r2 >>= 1; " b \
+ "m_%=: if r2 > 62 goto out_%=; r1 = r6; r1 += r3;"
+
+LINKED(congruent_phis, VALID, TWO_PATHS("r3 = r2; r3 += 1;", "r3 = r2; r3 += 1;"))
+LINKED(phis_with_other_delta_on_one_path, UNBOUND,
+ TWO_PATHS("r3 = r2; r3 += 1;", "r3 = r2; r3 += 2;"))
+/* One of three edges brings unrelated values */
+LINKED(phis_not_linked_on_one_of_three_edges, UNBOUND,
+ "r3 = r8; r3 >>= 2; r2 = 0; if r7 == 20 goto m_%=;"
+ TWO_PATHS("r3 = r2; r3 += 1;", "r3 = r2; r3 += 1;"))
+/* (zext32 x) on both paths: linked in 64 bits */
+LINKED(congruent_phis_of_zext32, VALID,
+ "if r7 > 9 goto l_%=; w2 = w8; r3 = r2; goto m_%=; l_%=: r2 = r8; r2 >>= 1; w2 = w2;"
+ "r3 = r2; m_%=: r3 += 1; if r2 > 62 goto out_%=; r1 = r6; r1 += r3;")
+
+/* The low 3 bits of r3 are 5. It is r8 + 5 once r8 fits in s8: no overlap with [-2, 1]. */
+LINKED(empty_intersection, VALID __flag(BPF_F_TEST_REG_INVARIANTS),
+ "r3 = (s8)r8; if r3 & 7 goto out_%=; r3 += 5; if r3 s> 100 goto out_%=;"
+ "if r3 s< -100 goto out_%=; if r8 s< -7 goto out_%=; if r8 s> -4 goto out_%=;"
+ "r3 += 3; r1 = r6;")
+
+/* r2 is even. It is r8 once r8 fits in s8. r8 is odd after the last jump: var_off don't overlap. */
+LINKED(var_off_dont_overlap, VALID,
+ "r2 = (s8)r8; if r2 & 1 goto out_%=; if r2 s> 100 goto out_%=; if r2 s< -100 goto out_%=;"
+ "if r8 > 5 goto out_%=; if r8 & 1 goto l_%=; goto out_%=;"
+ "l_%=: r2 &= 1; r2 <<= 6; r1 = r6; r1 += r2;")
+
+/* The low bytes of the spill to r10 - 16 come first or last */
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+#define FIRST_BYTES_ARE_LOW VALID
+#else
+#define FIRST_BYTES_ARE_LOW __failure __msg("R1 unbounded memory access")
+#endif
+
+/* r2 is the low 32 bits of the slot. The 32-bit spill is in its first half-slot. */
+LINKED(spill_32_bits_fill_64_take_low_half, FIRST_BYTES_ARE_LOW,
+ "*(u64 *)(r10 - 16) = r7; *(u32 *)(r10 - 16) = r8; r2 = *(u64 *)(r10 - 16); w2 = w2;"
+ "w3 = w8; if r2 > 63 goto out_%=; r1 = r6; r1 += r3;")
+/* The slot is a phi: two paths spill distinct values to its first half-slot */
+LINKED(spill_32_bits_on_two_paths_fill_64_take_low_half, FIRST_BYTES_ARE_LOW,
+ "*(u64 *)(r10 - 16) = r7; *(u32 *)(r10 - 16) = r8; if r7 > 9 goto l_%=;"
+ "r3 = r8; r3 >>= 1; *(u32 *)(r10 - 16) = r3;"
+ "l_%=: r2 = *(u64 *)(r10 - 16); w2 = w2; if r2 > 63 goto out_%=;"
+ "r3 = *(u32 *)(r10 - 16); r1 = r6; r1 += r3;")
+/* The half-slot at r10 - 12 is dead: at the second jump the state has a 32-bit spill */
+LINKED(spill_64_bits_fill_32, FIRST_BYTES_ARE_LOW,
+ "*(u64 *)(r10 - 16) = r8; if r8 s> 63 goto out_%=; if r8 s< 0 goto out_%=;"
+ "r2 = *(u32 *)(r10 - 16); r1 = r6; r1 += r2;")
+
+/* r0 is r10 - 24 when the first 8 bytes of the packet are not in one piece */
+LINKED_SEC("tc", slot_written_through_a_slice, UNBOUND,
+ "r1 = r9; r2 = 0; r3 = r10; r3 += -16; call %[bpf_dynptr_from_skb];"
+ "r1 = r10; r1 += -16; r2 = 0; r3 = r10; r3 += -24; r4 = 8;"
+ "call %[bpf_dynptr_slice_rdwr]; if r0 == 0 goto out_%=;"
+ "r3 = *(u64 *)(r10 - 24); *(u64 *)(r0 + 0) = r8; r2 = *(u64 *)(r10 - 24);"
+ "if r3 > 63 goto out_%=; r1 = r6; r1 += r2;")
+/* One fill reads one value: r2 is a copy of r3, whatever is stored through the slice */
+LINKED_SEC("tc", copy_of_a_fill_from_a_slice_buffer, VALID,
+ "r1 = r9; r2 = 0; r3 = r10; r3 += -16; call %[bpf_dynptr_from_skb];"
+ "r1 = r10; r1 += -16; r2 = 0; r3 = r10; r3 += -24; r4 = 8;"
+ "call %[bpf_dynptr_slice_rdwr]; if r0 == 0 goto out_%=;"
+ "r3 = *(u64 *)(r10 - 24); r2 = r3; *(u64 *)(r0 + 0) = r8;"
+ "if r3 > 63 goto out_%=; r1 = r6; r1 += r2;")
+
+/* The buffer is r10 - 32 or r10 - 48, a range in the state. The call does not write to r10 - 40. */
+LINKED_SEC("tc", slice_buffer_at_two_offsets, VALID,
+ "r1 = r9; r2 = 0; r3 = r10; r3 += -16; call %[bpf_dynptr_from_skb];"
+ "*(u64 *)(r10 - 32) = r7; *(u64 *)(r10 - 40) = r7; *(u64 *)(r10 - 48) = r7;"
+ "r8 = r10; r8 += -48; r9 = r10; r9 += -32; r7 = 0;"
+ "l_%=: call %[bpf_get_prandom_u32]; if r0 == 0 goto s_%=; r9 = r8;"
+ "s_%=: r7 += 1; if r7 < 64 goto l_%=;"
+ "r1 = r10; r1 += -16; r2 = 0; r3 = r9; r4 = 8; call %[bpf_dynptr_slice_rdwr];"
+ "r1 = *(u64 *)(r10 - 40); r1 = r6;")
+/* No insn stores through a slice of 0 bytes, and the call does not write to the buffer */
+LINKED_SEC("tc", slice_of_0_bytes_at_variable_offset, VALID,
+ "r1 = r9; r2 = 0; r3 = r10; r3 += -16; call %[bpf_dynptr_from_skb];"
+ "*(u64 *)(r10 - 24) = r7; *(u64 *)(r10 - 32) = r7; r7 &= 8;"
+ "r1 = r10; r1 += -16; r2 = 0; r3 = r10; r3 += -32; r3 += r7; r4 = 0;"
+ "call %[bpf_dynptr_slice_rdwr]; r1 = r6;")
+
+/* BTF of the kfuncs that only asm calls */
+void __kfunc_btf_root(void)
+{
+ bpf_dynptr_from_skb(0, 0, 0);
+ bpf_dynptr_slice_rdwr(0, 0, 0, 0);
+}
+
+/* Linked at the header in every iteration: r2 is r3 + 2 at loop entry, both have the step 1 */
+#define LOOP_HEADER(enter, step) \
+ enter "l_%=: if r3 > 61 goto out_%=; r1 = r6; r1 += r2; r0 = *(u8 *)(r1 + 0);" step \
+ "if r0 != 0 goto l_%=;"
+
+LINKED(linked_at_loop_header, VALID,
+ LOOP_HEADER("r3 = r8; r2 = r8; r2 += 1; r2 += 1;", "r2 += 1; r3 += 1;"))
+LINKED(linked_at_loop_entry_only, __failure,
+ LOOP_HEADER("r3 = r8; r2 = r8; r2 += 1; r2 += 1;", "r2 += 2; r3 += 1;"))
+
+/*
+ * No exit of the inner loop dominates its backedges: may_goto is added before its header. The prog
+ * holds a reference, so may_goto goes to the exit of the outer loop: an edge that the verifier
+ * adds. Every iteration of the outer loop restores r3. At the header of the inner one it is r7.
+ */
+LINKED(added_edge, UNBOUND,
+ "r1 = %[ringbuf] ll; r2 = 8; r3 = 0; call %[bpf_ringbuf_reserve]; if r0 == 0 goto out_%=;"
+ "*(u64 *)(r10 - 16) = r0; r3 = r8;"
+ "o_%=: r2 = r3; r3 = r7;"
+ "i_%=: r1 = *(u8 *)(r6 + 0); if r1 == 1 goto a_%=; if r1 == 2 goto b_%=; goto i_%=;"
+ "a_%=: goto i_%=;"
+ "b_%=: r3 = r2; r1 = *(u8 *)(r6 + 1); if r1 == 3 goto o_%=;"
+ "r4 = r6; if r8 > 63 goto r_%=; r4 += r3;"
+ "r_%=: *(u64 *)(r10 - 24) = r4; r1 = *(u64 *)(r10 - 16); r2 = 0;"
+ "call %[bpf_ringbuf_discard]; r1 = *(u64 *)(r10 - 24);")
+
/*
* r7 = ctx->data;
* r8 = r7
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 15/29] selftests/bpf: Test sign extensions of a range and of a linked register
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (13 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 14/29] selftests/bpf: Test registers linked by " Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 16/29] bpf: Recompute a pointer after the jump that bounds its index Alexei Starovoitov
` (13 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Vineet Gupta <vineet.gupta@linux.dev>
Tests for two patches of this series. Two progs are for
coerce_subreg_to_size_sx(), which keeps the 32-bit range across a no-op
sign extension. Five are for refine_linked_regs(), which refines the linked
registers of a conditional jump: the live registers and stack slots that
are a tested register + a constant on every path to the jump, here
modulo 2^32.
call bpf_get_prandom_u32
r7 = (s32)r0
if w0 != -1 goto ..
GCC keeps an int that a call returned that way. The jump tests the low
32 bits of r0, the prog goes on with r7.
'w0 = (s8)w0' and 'w0 = (s16)w0' keep a 32-bit range that crosses 0 and
fits in s8 or s16.
After 'r7 = (s32)r6' r7 == r6 (mod 2^32). A test of the low 32 bits of the
source refines r7. The source is the def of r0 by a call, or (u32)(r6 + 1),
which is 'i += 1' of an int as GCC emits it.
Three negative tests get to 'r0 /= 0' and are rejected:
- A test of r7 does not bound the upper 32 bits of r6 + 3.
- If r6 is a u32 then 'r7 s<= 100' keeps the values from 2^31 on in r6.
- If r6 is a multiple of 3 then r7 need not be one: from 2^31 on it is
r6 - 2^32.
Signed-off-by: Vineet Gupta <vineet.gupta@linux.dev>
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
.../selftests/bpf/progs/verifier_movsx.c | 186 ++++++++++++++++++
1 file changed, 186 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/verifier_movsx.c b/tools/testing/selftests/bpf/progs/verifier_movsx.c
index 2f4a678c31c2..6f90dac78fcd 100644
--- a/tools/testing/selftests/bpf/progs/verifier_movsx.c
+++ b/tools/testing/selftests/bpf/progs/verifier_movsx.c
@@ -202,6 +202,192 @@ l0_%=: \
: __clobber_all);
}
+/*
+ * A 32-bit range that already fits the signed N-bit range is unchanged by the sign
+ * extension. Both of these straddle zero. The guard that follows can only be
+ * resolved statically if the range survives, and it protects a div by zero.
+ */
+SEC("socket")
+__description("MOV32SX, S8, negative range is preserved")
+__success __success_unpriv __retval(0)
+__naked void mov32sx_s8_negative_range(void)
+{
+ asm volatile (" \
+ call %[bpf_get_prandom_u32]; \
+ w0 &= 0x3f; \
+ w0 -= 0x3f; \
+ /* w0 is [-63, 0], already a valid s8 */ \
+ w0 = (s8)w0; \
+ if w0 s< -0x3f goto l0_%=; \
+ w0 = 0; \
+ exit; \
+l0_%=: \
+ /* dead unless the range was widened */ \
+ w0 /= 0; \
+ exit; \
+" :
+ : __imm(bpf_get_prandom_u32)
+ : __clobber_all);
+}
+
+SEC("socket")
+__description("MOV32SX, S16, negative range is preserved")
+__success __success_unpriv __retval(0)
+__naked void mov32sx_s16_negative_range(void)
+{
+ asm volatile (" \
+ call %[bpf_get_prandom_u32]; \
+ w0 &= 0xfff; \
+ w0 -= 0xfff; \
+ /* w0 is [-4095, 0], already a valid s16 */ \
+ w0 = (s16)w0; \
+ if w0 s< -0xfff goto l0_%=; \
+ w0 = 0; \
+ exit; \
+l0_%=: \
+ /* dead unless the range was widened */ \
+ w0 /= 0; \
+ exit; \
+" :
+ : __imm(bpf_get_prandom_u32)
+ : __clobber_all);
+}
+
+/*
+ * A 32-bit sign extension keeps the low 32 bits, so refining the source
+ * reaches the destination. The source is a call return, which is the shape
+ * bpf-gcc builds for "int err = helper(); if (err) ..".
+ */
+SEC("socket")
+__success __retval(0)
+__naked void sext_mov_unlinked_src(void)
+{
+ asm volatile (" \
+ call %[bpf_get_prandom_u32]; \
+ r7 = (s32)r0; /* r0 as the call defines it */ \
+ if w0 != -1 goto l0_%=; /* refines r0, propagates to r7 */ \
+ if r7 == -1 goto l0_%=; /* sign-extended, not 0xffffffff */ \
+ r0 /= 0; \
+l0_%=: \
+ r0 = 0; \
+ exit; \
+" :
+ : __imm(bpf_get_prandom_u32)
+ : __clobber_all);
+}
+
+/*
+ * The propagation must not run backwards over the upper 32 bits: a sign extension
+ * knows nothing about the upper bits of its source. r8's low 32 bits do become
+ * 2, r8 == 2 does not follow.
+ */
+SEC("socket")
+__failure __msg("div by zero")
+__naked void sext_no_sync_from_subreg_base(void)
+{
+ asm volatile (" \
+ call %[bpf_get_prandom_u32]; \
+ r6 = r0; \
+ r7 = (s32)r6; \
+ r8 = r6; \
+ r8 += 3; \
+ if r7 != -1 goto 1f; /* pins the low 32 bits of r8 only */ \
+ if r8 == 2 goto 1f; /* taken only if r8 wrongly refined */ \
+ r0 /= 0; \
+1: \
+ r0 = 0; \
+ exit; \
+" :
+ : __imm(bpf_get_prandom_u32)
+ : __clobber_all);
+}
+
+/*
+ * r2 is (u32)(r6 + 1) and r7 is (s32)(r6 + 1), so w2 == 1 means r7 == 1.
+ * bpf-gcc builds "i += 1" for an int induction variable into exactly this shape.
+ */
+SEC("socket")
+__success __retval(0)
+__naked void sext_sync_from_zext_sibling(void)
+{
+ asm volatile (" \
+ call %[bpf_get_prandom_u32]; \
+ r6 = r0; \
+ r6 <<= 32; \
+ call %[bpf_get_prandom_u32]; \
+ r6 |= r0; /* r6 = full 64-bit unknown */ \
+ r2 = r6; \
+ w2 += 1; /* r2 = (u32)(r6 + 1) */ \
+ r7 = (s32)r2; /* r7 = (s32)(r6 + 1) */ \
+ if w2 != 1 goto 1f; /* propagates to r7 */ \
+ if r7 == 1 goto 1f; \
+ r0 /= 0; \
+1: \
+ r0 = 0; \
+ exit; \
+" :
+ : __imm(bpf_get_prandom_u32)
+ : __clobber_all);
+}
+
+/*
+ * "w6 = w6" leaves r6 in [0, U32_MAX]. A signed compare on its sign extension
+ * must not hand r6 the sign-extended value: that drops every r6 >= 2^31 from
+ * the range and makes the guarded div provably dead.
+ */
+SEC("socket")
+__failure __msg("div by zero")
+__naked void sext_narrow_base_keeps_high_values(void)
+{
+ asm volatile (" \
+ call %[bpf_get_prandom_u32]; \
+ r6 = r0; \
+ w6 = w6; /* r6 in [0, U32_MAX] */ \
+ r7 = (s32)r6; \
+ r8 = 1; \
+ r8 <<= 31; /* 0x80000000 */ \
+ if r7 s> 100 goto 1f; /* r6 is [0,100] or >= 2^31 */ \
+ if r6 < r8 goto 1f; /* reachable: r6 may be >= 2^31 */ \
+ r0 /= 0; \
+1: \
+ r0 = 0; \
+ exit; \
+" :
+ : __imm(bpf_get_prandom_u32)
+ : __clobber_all);
+}
+
+/*
+ * A base/step line does not survive the sign extension: it subtracts 2^32,
+ * and 2^32 is not a multiple of 3. r6 = 0x80000007 is a multiple of 3, so
+ * r7 = -2147483641 is reachable.
+ */
+SEC("socket")
+__failure __msg("div by zero")
+__naked void sext_drops_stale_step(void)
+{
+ asm volatile (" \
+ call %[bpf_get_prandom_u32]; \
+ r6 = r0; \
+ w6 = w6; \
+ r6 *= 3; /* base 0, step 3 */ \
+ r7 = (s32)r6; \
+ r8 = 1; \
+ r8 <<= 31; \
+ r9 = r8; \
+ r9 += 7; \
+ if r6 < r8 goto 1f; \
+ if r6 > r9 goto 1f; /* r6 in [0x80000000, 0x80000007] */ \
+ if r7 != -2147483641 goto 1f; /* reachable: r6 == 0x80000007 */ \
+ r0 /= 0; \
+1: \
+ r0 = 0; \
+ exit; \
+" :
+ : __imm(bpf_get_prandom_u32)
+ : __clobber_all);
+}
+
SEC("socket")
__description("MOV64SX, S16, R10 Sign Extension")
__failure __msg("R1 type=scalar expected=fp, pkt, pkt_meta, map_key, map_value, mem, ringbuf_mem, buf, trusted_ptr_")
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 16/29] bpf: Recompute a pointer after the jump that bounds its index
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (14 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 15/29] selftests/bpf: Test sign extensions of a range and of a linked register Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:48 ` sashiko-bot
2026-11-11 5:13 ` [PATCH bpf-next v2 17/29] selftests/bpf: Test a pointer computed before its index is bounded Alexei Starovoitov
` (12 subsequent siblings)
28 siblings, 1 reply; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A new ability. It extends the patch that refines linked registers at
conditional jumps to 'p = buf + i' that is computed before i is bounded.
Nothing links a pointer to its index today.
while (buf[i]) {
if (i < 0 || i > 32)
break;
if (bpf_map_lookup_elem(&names, buf + i))
return 0;
while (buf[i++] && i < 32)
;
}
Strings are stored back to back in a map value of 64 bytes, a key has 32.
clang computes 'buf + i' for the load at the end of an iteration and tests
i at the start of the next one. The jumps bound r8, the offset of r2 stays:
27: r2 = r6
28: r2 += r8
30: r1 = *(u8 *)(r2 +0)
36: if r8 s> 0x20 goto pc-4
38: if r8 s< 0x0 goto pc-6
41: call bpf_map_lookup_elem#1
invalid access to map value, value_size=64 off=33 size=32
The linked registers of a conditional jump are the live registers and
stack slots that are a tested register + a constant on every path to the
jump. bpf_scev_next_linked_reg() gets one more result. A linked register
can be a pointer: *base_regno + regno + delta. *base_regno is the address
base, as in store_base_regs. Here r2 == r6 + r8.
So 'r2 = r6; r2 += r8' after the jump would change no value at run time,
and a state that is sound before two insns is sound after them.
recompute_ptr() applies their transfer function. Type, object and offset of
r2 are taken from r6 as it is now; how r2 was computed does not matter. The
range is that of r8 after the jump.
match_linked_add(): the expression of r2 is (+ x y), y has a constant
difference from the tested register, and a live register or stack slot has
one from x. At a loop header r2 is a phi. There ADD_SCEV has its operands:
scev at header 35:
r2=(+ r6 %r8.21?) / (add r6 r8)
r8=%r8.21?
(add r6 r8): r2 == r6 + r8 at the header in every iteration. It holds at
loop entry, where r2 is r6 and r8 is 0. Every backedge preserves it: the
backedge values, before the '/', are r6 + %r8.21? and %r8.21?, the def of
r8 by the nested loop at 21. r6 and r8 are the header phis here, not the
entry values as below LINEAR_SCEV.
recompute_ptr() replaces r2 only if
- r6 is a pointer that may have a variable offset, and no packet pointer,
- type, object and ids of r6 are those of r2,
- the new range is a subset of the old one and not empty (an empty range:
the path is infeasible),
- the loader has CAP_PERFMON: the cpu can speculate past the jump with an
out-of-bounds index.
The old offset is replaced, not intersected: a state cannot distinguish two
values of a hash map, so the two offsets may belong to two objects. In
a prog with an arena the add has a def, %r2.28=(add r6 r8), and is trusted
once the main pass has verified insn 28 without an arena pointer.
Not found: 'buf += i', which overwrites the base, and an add whose base is
dead at the jump. No register holds buf there.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
include/linux/bpf_verifier.h | 4 +-
kernel/bpf/scev.c | 174 +++++++++++++++++++++++++++++++----
kernel/bpf/verifier.c | 53 ++++++++++-
3 files changed, 208 insertions(+), 23 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index d1841f68437d..11025ed77a7f 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -2226,7 +2226,9 @@ void bpf_free_scev(struct bpf_verifier_env *env);
int bpf_compute_scev(struct bpf_verifier_env *env);
int bpf_scev_jump_counts(struct bpf_verifier_env *env, int header, int n, bool taken_leaves);
bool bpf_scev_next_linked_reg(struct bpf_verifier_env *env, int insn_idx, u32 regno, u32 *pos,
- u32 *linked_regno, u32 *fill_off, s64 *delta, u32 *bits);
+ u32 *linked_regno, u32 *fill_off, int *base_regno, s64 *delta,
+ u32 *bits);
+bool bpf_is_widenable_reg_type(const struct bpf_reg_state *reg);
bool bpf_scev_in_slice_slots(struct bpf_verifier_env *env, int off, u32 size, bool fixed_off);
int bpf_compute_loop_iters(struct bpf_verifier_env *env, struct bpf_verifier_state *st,
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index f45abe90d498..50b440142bf9 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -50,6 +50,11 @@ enum expr_op {
* equal when the loop is entered. Not known otherwise.
*/
COPY_SCEV,
+ /*
+ * (add rB rC) is the SCEV of rA: rA == rB + rC at the header in every iteration. rB and rC
+ * are the header phis here, not the entry values as below LINEAR_SCEV.
+ */
+ ADD_SCEV,
};
/*
@@ -66,9 +71,9 @@ enum expr_op {
* In the env of loop H a phi at H merges the backedge values only, the header phi itself is
* (REG r). In the env of the enclosing loop a phi at H merges the entering edges.
* Op and params of a def by an insn are what that insn computes, see alu64_with_def():
- * set_expr_base() looks through such an add, split_const_offset() checks prevent_zext of the insn.
- * Those of a phi are the shape that its operands share, no insn computes them: nothing looks
- * through a phi.
+ * set_expr_base() and match_linked_add() look through such an add, split_const_offset() and
+ * match_linked_add() check prevent_zext of the insn. Those of a phi are the shape that its operands
+ * share, no insn computes them: nothing looks through a phi.
*/
struct expr_def {
u32 set:1; /* 0: the expression has no def */
@@ -261,6 +266,8 @@ static bool expr_is_vn(struct scev *scev, struct expr *e)
/* a function of the iteration number */
case LINEAR_SCEV:
case COPY_SCEV:
+ /* a relation between header phis */
+ case ADD_SCEV:
return false;
}
for (i = 0; i < op_params_num(e->op); i++)
@@ -568,6 +575,7 @@ static const char *op_str(u32 op)
case ANY: return "any";
case LINEAR_SCEV: return "linear";
case COPY_SCEV: return "copy";
+ case ADD_SCEV: return "add";
}
return NULL;
}
@@ -591,6 +599,7 @@ static u32 op_params_num(u32 op)
case ANY:
case LINEAR_SCEV:
case COPY_SCEV:
+ case ADD_SCEV:
return 2;
case BPF_NEG:
case SEXT8:
@@ -1954,6 +1963,41 @@ static bool const_diff_at_header(struct bpf_verifier_env *env, int h, u32 ra, u3
bits_a == 64 && bits == 64;
}
+/*
+ * p = buf + i; if (i > 100) return 0; *p = 0;
+ * @tested is the expression of the tested register and @id that of p, in one env as for
+ * const_diff(). Is @id an add, and one operand @tested + a constant? Then
+ * @id == *@other + @tested + *@delta (mod 2^*@bits). A header phi can be an add, see ADD_SCEV. For
+ * an add with a def see split_const_offset(). Which operand is the pointer is not known here.
+ */
+static bool match_linked_add(struct bpf_verifier_env *env, int h, u32 tested, u32 id, u32 *other,
+ s64 *delta, u32 *bits)
+{
+ struct scev *scev = env->scev;
+ u32 reg, r, b, op = BPF_ADD;
+ struct expr_def def;
+ s64 c;
+
+ split_const_offset(env, id, &id, delta, bits);
+ if (h >= 0 && is_reg(scev, id, ®)) {
+ id = find_header_env(scev, h)->reg2scev[reg];
+ op = ADD_SCEV;
+ }
+ def = scev->exprs[id].def;
+ if (!is_binop(scev, op, id, other, &r) || def.phi)
+ return false;
+ if (def.set && !env->insn_aux_data[def.idx].prevent_zext)
+ *bits = min(*bits, 32);
+ if (!const_diff(env, h, tested, r, &c, &b)) {
+ swap(*other, r);
+ if (!const_diff(env, h, tested, r, &c, &b))
+ return false;
+ }
+ *delta = (u64)*delta + c;
+ *bits = min(*bits, b);
+ return true;
+}
+
/* The first bytes of a stack slot are its high bits */
static bool is_big_endian_slot(u32 reg)
{
@@ -2005,9 +2049,10 @@ static int find_linked_regs(struct bpf_verifier_env *env, struct env *e, int h,
u32 dst = e->reg2expr[insn->dst_reg], src = dst;
struct insn_envs *envs = env->scev->envs[idx];
struct scev *scev = env->scev;
- u32 r, id, fill_off, base, base_dst, base_src, bits, cap, tested = 0;
+ u32 r, q, other, id, fill_off, base, base_dst, base_src, bits, cap, tested = 0;
u32 first = scev->linked_cnt;
DECLARE_BITMAP(live, REGS_NUM) = {};
+ DECLARE_BITMAP(found, REGS_NUM) = {};
void *tmp;
s64 delta;
@@ -2024,12 +2069,27 @@ static int find_linked_regs(struct bpf_verifier_env *env, struct env *e, int h,
id = e->reg2expr[r];
/* most are not linked */
split_const_offset(env, id, &base, &delta, &bits);
- if (base != base_dst && base != base_src && (h < 0 || !is_op(scev, base, REG)) &&
- !is_big_endian_slot(r))
+ if (base != base_dst && base != base_src && !is_op(scev, base, BPF_ADD) &&
+ (h < 0 || !is_op(scev, base, REG)) && !is_big_endian_slot(r))
continue;
- if (!const_diff_reg(env, h, dst, r, id, &fill_off, &delta, &bits) &&
- (src == dst || !const_diff_reg(env, h, src, r, id, &fill_off, &delta, &bits)))
+ if (const_diff_reg(env, h, dst, r, id, &fill_off, &delta, &bits) ||
+ (src != dst && const_diff_reg(env, h, src, r, id, &fill_off, &delta, &bits))) {
+ __set_bit(r, found);
+ continue;
+ }
+ if (!match_linked_add(env, h, dst, id, &other, &delta, &bits) &&
+ (src == dst || !match_linked_add(env, h, src, id, &other, &delta, &bits)))
continue;
+ /* an add is of use only with a live register that holds its other operand */
+ for_each_set_bit(q, live, REGS_NUM) {
+ if (q == r || !const_diff(env, h, e->reg2expr[q], other, &delta, &bits))
+ continue;
+ __set_bit(r, found);
+ __set_bit(q, found);
+ break;
+ }
+ }
+ for_each_set_bit(r, found, REGS_NUM) {
if (scev->linked_cnt == scev->linked_cap) {
cap = max(scev->linked_cap * 2, 16);
tmp = kvrealloc(scev->linked, array_size(cap, sizeof(*scev->linked)),
@@ -2041,7 +2101,7 @@ static int find_linked_regs(struct bpf_verifier_env *env, struct env *e, int h,
}
tested += r == insn->dst_reg || (src != dst && r == insn->src_reg);
scev->linked[scev->linked_cnt].reg = r;
- scev->linked[scev->linked_cnt++].id = id;
+ scev->linked[scev->linked_cnt++].id = e->reg2expr[r];
}
/* nothing but the tested registers */
if (scev->linked_cnt - first == tested) {
@@ -2065,14 +2125,18 @@ static int find_linked_regs(struct bpf_verifier_env *env, struct env *e, int h,
* the same frame. It is a register, or MAX_BPF_REG + the index of a stack slot: a fill from byte
* *@fill_off of the slot reads these bits. *@pos is the position of the search, 0 at first. False:
* no more links are known. Types are not known here: the caller checks them in its state.
+ * A linked register can be a pointer: *base_regno + regno + delta. *base_regno is the address base,
+ * as in store_base_regs. It is -1 for a link without a base.
*/
bool bpf_scev_next_linked_reg(struct bpf_verifier_env *env, int insn_idx, u32 regno, u32 *pos,
- u32 *linked_regno, u32 *fill_off, s64 *delta, u32 *bits)
+ u32 *linked_regno, u32 *fill_off, int *base_regno, s64 *delta,
+ u32 *bits)
{
struct insn_envs *envs = env->scev->envs[insn_idx];
int h = bpf_loop_at_index(env, insn_idx);
struct scev_linked_reg *linked;
- u32 i;
+ u32 i, j, other, b;
+ s64 c;
if (!envs || !envs->linked_cnt)
return false;
@@ -2083,10 +2147,22 @@ bool bpf_scev_next_linked_reg(struct bpf_verifier_env *env, int insn_idx, u32 re
return false;
for (; *pos < envs->linked_cnt; ++*pos) {
*linked_regno = linked[*pos].reg;
- if (*pos != i &&
- const_diff_reg(env, h, linked[i].id, *linked_regno, linked[*pos].id, fill_off,
+ *base_regno = -1;
+ if (*pos == i)
+ continue;
+ if (const_diff_reg(env, h, linked[i].id, *linked_regno, linked[*pos].id, fill_off,
delta, bits))
return true;
+ if (!match_linked_add(env, h, linked[i].id, linked[*pos].id, &other, delta, bits))
+ continue;
+ for (j = 0; j < envs->linked_cnt; j++) {
+ if (j == *pos || !const_diff(env, h, linked[j].id, other, &c, &b))
+ continue;
+ *base_regno = linked[j].reg;
+ *delta = (u64)*delta + c;
+ *bits = min(*bits, b);
+ return true;
+ }
}
return false;
}
@@ -2561,6 +2637,64 @@ static int compute_header_scevs(struct bpf_verifier_env *env, struct env *header
return 0;
}
+/*
+ * Find a SCEV register with the entry value @x and the backedge value @l, and one with @y and @r.
+ * All that match are copies of each other at the header: take the first.
+ */
+static bool find_operands(struct env *entry_env, struct env *header_env, u32 x, u32 y, u32 l, u32 r,
+ u32 *rb, u32 *rc)
+{
+ for (*rb = 0; *rb < REGS_NUM; ++*rb)
+ if (entry_env->reg2expr[*rb] == x && header_env->reg2expr[*rb] == l)
+ break;
+ for (*rc = 0; *rc < REGS_NUM; ++*rc)
+ if (entry_env->reg2expr[*rc] == y && header_env->reg2expr[*rc] == r)
+ break;
+ return *rb < REGS_NUM && *rc < REGS_NUM;
+}
+
+/*
+ * p = buf; i = 0; while (*p) { if (i > 512) break; use(p); i += len(p); p = buf + i; }
+ * rA == rB + rC holds at the header in every iteration: it holds at loop entry and every backedge
+ * preserves it. The backedge values are rA = (+ l r), rB = l, rC = r. The entry values, by the env
+ * of the enclosing loop at @h, are rA = x, rB = x, rC = 0 or rA = (+ x y), rB = x, rC = y.
+ * (add rB rC) keeps the def of (+ l r), see alu64_with_def().
+ */
+static int compute_header_adds(struct bpf_verifier_env *env, int h)
+{
+ struct scev *scev = env->scev;
+ struct env *entry_env = find_loop_env(scev, env->insn_aux_data[h].loop_header, h);
+ struct env *header_env = find_header_env(scev, h);
+ u32 ra, rb, rc, l, r, x, y, entry_expr;
+ struct expr_def def;
+ int id, zero = imm_expr(scev, 0);
+
+ if (zero < 0)
+ return zero;
+ for (ra = 0; entry_env && ra < REGS_NUM; ra++) {
+ entry_expr = entry_env->reg2expr[ra];
+ if (header_env->reg2scev[ra] != UNKNOWN_EXPR_ID ||
+ !is_add(scev, header_env->reg2expr[ra], &l, &r) ||
+ !scev->exprs[entry_expr].is_vn || !scev->exprs[l].is_vn ||
+ !scev->exprs[r].is_vn)
+ continue;
+ if (!find_operands(entry_env, header_env, entry_expr, zero, l, r, &rb, &rc) &&
+ !find_operands(entry_env, header_env, zero, entry_expr, l, r, &rb, &rc) &&
+ (scev->exprs[entry_expr].def.set || !is_add(scev, entry_expr, &x, &y) ||
+ (!find_operands(entry_env, header_env, x, y, l, r, &rb, &rc) &&
+ !find_operands(entry_env, header_env, y, x, l, r, &rb, &rc))))
+ continue;
+ /* expr2() can move exprs[] */
+ def = scev->exprs[header_env->reg2expr[ra]].def;
+ id = expr_with_def(scev, expr2(scev, ADD_SCEV, REG_EXPR_ID(rb), REG_EXPR_ID(rc)),
+ def);
+ if (id < 0)
+ return id;
+ header_env->reg2scev[ra] = id;
+ }
+ return 0;
+}
+
/*
* 'sum += a[i]', 'if (..) { len -= n; len -= 2; }': is 'id', what an iteration makes of
* register 'r', the register plus or minus something, or the register as it was?
@@ -2729,9 +2863,13 @@ int bpf_compute_scev(struct bpf_verifier_env *env)
find_loop_env(scev, header, idx));
if (err)
return err;
- if (aux[idx].loop)
+ if (aux[idx].loop) {
scev_compute_linear_regs(scev, aux[idx].loop,
find_header_env(scev, header));
+ err = compute_header_adds(env, idx);
+ if (err)
+ return err;
+ }
err = find_linked_regs(env, aux[idx].loop ? &scev->identity_env :
find_loop_env(scev, header, idx), header, idx);
if (err)
@@ -3970,7 +4108,7 @@ static bool is_const_scev_reg(struct bpf_verifier_env *env, int insn_idx, u32 r)
}
/* Main logic in verifier.c forbids varying offsets for certain register types. */
-static bool is_widenable_reg_type(const struct bpf_reg_state *reg)
+bool bpf_is_widenable_reg_type(const struct bpf_reg_state *reg)
{
if (type_may_be_null(reg->type))
return false;
@@ -4017,7 +4155,7 @@ static bool is_widenable_any(struct bpf_verifier_env *env, struct bpf_func_state
if (!is_any(scev, id, &l, &rr))
return false;
reg = scev_reg_for_read(env, loop_entry, r);
- if (!reg || !is_widenable_reg_type(reg))
+ if (!reg || !bpf_is_widenable_reg_type(reg))
return false;
scev->stack_sz = 0;
@@ -4550,7 +4688,7 @@ int bpf_compute_loop_iters(struct bpf_verifier_env *env, struct bpf_verifier_sta
}
/* If SCEV for r is (linear <reg> <slope>) */
if (reg && is_linear_at_header(scev, cur_func, r_scev, r, &coefs) &&
- is_widenable_reg_type(reg)) {
+ bpf_is_widenable_reg_type(reg)) {
/* 32-bit ALU operations on pointers convert them to scalars */
if (coefs.zext32 && reg->type != SCALAR_VALUE)
goto cant_widen;
@@ -4893,7 +5031,7 @@ int bpf_clamp_scev_regs(struct bpf_verifier_env *env, struct bpf_func_state *cur
* bpf_compute_loop_iters() gives no range to a pointer to arena that moves, and
* it does not look at dead registers. Set a range only where it would.
*/
- if (!is_widenable_reg_type(entry_reg) ||
+ if (!bpf_is_widenable_reg_type(entry_reg) ||
(coefs.zext32 && entry_reg->type != SCALAR_VALUE))
continue;
clamp:
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 138581a759bb..c2a3f74d441f 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -19741,6 +19741,43 @@ static struct bpf_reg_state *scev_reg_for_fill(struct bpf_func_state *frame, u32
return &ss->spilled_ptr;
}
+/*
+ * p = buf + i; if (i > 100) return 0; *p = 0;
+ * p is @dst_reg, x is @ptr_reg, i is @off_reg. Scalar evolution says that p == x + i. So the insns
+ * 'p = x; p += i' are a no-op at run time, and a state that is sound before them is sound after
+ * them: apply their transfer function. Type, object and offset of p are taken from x as it is now;
+ * how p was computed does not matter.
+ * The old p is replaced, not intersected: a state does not distinguish two values of one hash map,
+ * and the intersection of an offset into the one with an offset into the other holds for neither.
+ * It is replaced only if the new range is a subset of the old one. An empty range is a subset too:
+ * the path is infeasible, p is left as it is.
+ */
+static bool recompute_ptr(struct bpf_verifier_env *env, struct bpf_reg_state *dst_reg,
+ const struct bpf_reg_state *ptr_reg, const struct bpf_reg_state *off_reg)
+{
+ struct bpf_reg_state sum = *ptr_reg;
+
+ /* the cpu can speculate past the jump with an out-of-bounds index */
+ if (!env->bypass_spec_v1)
+ return false;
+ /* 'p += i' assigns a new id to a packet pointer */
+ if (ptr_reg->type == SCALAR_VALUE || type_is_pkt_pointer(ptr_reg->type) ||
+ !bpf_is_widenable_reg_type(ptr_reg))
+ return false;
+ __mark_reg32_unbounded(&sum);
+ scalar_step_add(&sum, ptr_reg, off_reg);
+ sum.r64 = cnum64_add(ptr_reg->r64, off_reg->r64);
+ sum.var_off = tnum_add(ptr_reg->var_off, off_reg->var_off);
+ reg_bounds_sync(&sum);
+ if (reg_is_empty(&sum) || const_tnum_range_mismatch(&sum) ||
+ const_tnum_range_mismatch_32(&sum))
+ return false;
+ if (!bpf_reg_union_compatible(dst_reg, &sum) || !cnum64_is_subset(dst_reg->r64, sum.r64))
+ return false;
+ *dst_reg = sum;
+ return true;
+}
+
/*
* The jump at @idx refined @regno. A register that scalar evolution links to it holds
* @regno + delta on every path to the jump: intersect the two. The intersection is empty: no
@@ -19750,15 +19787,16 @@ static void refine_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier
int idx, u32 regno)
{
struct bpf_func_state *frame = vstate->frame[vstate->curframe];
- struct bpf_reg_state *reg, implied, saved;
+ struct bpf_reg_state *reg, *base_reg, implied, saved;
u32 pos, linked_regno, fill_off, bits;
+ int base_regno;
s64 delta;
if (frame->regs[regno].type != SCALAR_VALUE)
return;
for (pos = 0;
- bpf_scev_next_linked_reg(env, idx, regno, &pos, &linked_regno, &fill_off, &delta,
- &bits);
+ bpf_scev_next_linked_reg(env, idx, regno, &pos, &linked_regno, &fill_off, &base_regno,
+ &delta, &bits);
pos++) {
reg = scev_reg_for_fill(frame, linked_regno, fill_off, bits);
/*
@@ -19770,11 +19808,17 @@ static void refine_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier
bits = 32;
reg = scev_reg_for_fill(frame, linked_regno, fill_off, bits);
}
- if (!reg || reg->type != SCALAR_VALUE)
+ if (!reg || (base_regno < 0 && reg->type != SCALAR_VALUE))
continue;
implied = frame->regs[regno];
add_scalar_to_reg(&implied, delta);
+ if (base_regno >= 0) {
+ base_reg = scev_reg_for_fill(frame, base_regno, 0, 64);
+ if (base_reg && bits == 64 && recompute_ptr(env, reg, base_reg, &implied))
+ goto scratch;
+ continue;
+ }
if (regs_fit_in_bits(reg, &implied, bits))
bits = 64;
if (bits < 32)
@@ -19797,6 +19841,7 @@ static void refine_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier
*reg = saved;
continue;
}
+scratch:
if (linked_regno < MAX_BPF_REG)
mark_reg_scratched(env, linked_regno);
else
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* Re: [PATCH bpf-next v2 16/29] bpf: Recompute a pointer after the jump that bounds its index
2026-11-11 5:13 ` [PATCH bpf-next v2 16/29] bpf: Recompute a pointer after the jump that bounds its index Alexei Starovoitov
@ 2026-11-11 5:48 ` sashiko-bot
0 siblings, 0 replies; 36+ messages in thread
From: sashiko-bot @ 2026-11-11 5:48 UTC (permalink / raw)
To: Alexei Starovoitov, bpf
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [High] bpf: missing BPF_MAX_VAR_OFF offset bounds check in
recompute_ptr()
--
commit 5f6f31940968145851bf6579a0206e69bee69836
Author: Alexei Starovoitov <ast@kernel.org>
bpf: Recompute a pointer after the jump that bounds its index
This commit extends linked-register refinement at conditional jumps to
recompute a pointer register p = x + i once a conditional jump bounds the
index i.
> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
> index 138581a759bbb..c2a3f74d441fe 100644
> --- a/kernel/bpf/verifier.c
> +++ b/kernel/bpf/verifier.c
> @@ -19741,6 +19741,43 @@ static struct bpf_reg_state *scev_reg_for_fill(struct bpf_func_state *frame, u32
> return &ss->spilled_ptr;
> }
>
> +/*
> + * p = buf + i; if (i > 100) return 0; *p = 0;
> + * p is @dst_reg, x is @ptr_reg, i is @off_reg. Scalar evolution says that p == x + i. So the insns
> + * 'p = x; p += i' are a no-op at run time, and a state that is sound before them is sound after
> + * them: apply their transfer function. Type, object and offset of p are taken from x as it is now;
> + * how p was computed does not matter.
> + * The old p is replaced, not intersected: a state does not distinguish two values of one hash map,
> + * and the intersection of an offset into the one with an offset into the other holds for neither.
> + * It is replaced only if the new range is a subset of the old one. An empty range is a subset too:
> + * the path is infeasible, p is left as it is.
> + */
> +static bool recompute_ptr(struct bpf_verifier_env *env, struct bpf_reg_state *dst_reg,
> + const struct bpf_reg_state *ptr_reg, const struct bpf_reg_state *off_reg)
> +{
> + struct bpf_reg_state sum = *ptr_reg;
> +
> + /* the cpu can speculate past the jump with an out-of-bounds index */
> + if (!env->bypass_spec_v1)
> + return false;
> + /* 'p += i' assigns a new id to a packet pointer */
> + if (ptr_reg->type == SCALAR_VALUE || type_is_pkt_pointer(ptr_reg->type) ||
> + !bpf_is_widenable_reg_type(ptr_reg))
> + return false;
> + __mark_reg32_unbounded(&sum);
> + scalar_step_add(&sum, ptr_reg, off_reg);
> + sum.r64 = cnum64_add(ptr_reg->r64, off_reg->r64);
> + sum.var_off = tnum_add(ptr_reg->var_off, off_reg->var_off);
> + reg_bounds_sync(&sum);
> + if (reg_is_empty(&sum) || const_tnum_range_mismatch(&sum) ||
> + const_tnum_range_mismatch_32(&sum))
> + return false;
> + if (!bpf_reg_union_compatible(dst_reg, &sum) || !cnum64_is_subset(dst_reg->r64, sum.r64))
> + return false;
> + *dst_reg = sum;
> + return true;
> +}
[Severity: High]
Can recompute_ptr() overwrite *dst_reg with a pointer offset outside
(-BPF_MAX_VAR_OFF, BPF_MAX_VAR_OFF)?
When adjust_ptr_min_max_vals() verifies the earlier p = x + i while i is
not yet constant, check_reg_sane_offset_scalar() and
check_reg_sane_offset_ptr() only check known constant offsets and smin,
so dst_reg->r64 can have an smax >= BPF_MAX_VAR_OFF (for example
[-16, 0xfffffff0ULL] on PTR_TO_STACK). If a subsequent conditional jump
narrows i to the constant 0xfffffff0ULL, cnum64_is_subset(dst_reg->r64,
sum.r64) succeeds and recompute_ptr() sets dst_reg->var_off.value to
0xfffffff0ULL without calling check_reg_sane_offset_scalar() or
check_reg_sane_offset_ptr().
Downstream callers that assume a constant var_off.value fits in 32 bits
then truncate it, allowing out-of-bounds kernel memory access at runtime:
kernel/bpf/verifier.c:stack_slot_obj_get_spi() {
...
off = reg->var_off.value;
if (off >= 0 || off % BPF_REG_SIZE) {
...
}
Here 0xfffffff0ULL is truncated to int off = -16, validating stack slot
fp-16 while passing r10 + 0xfffffff0ULL to the helper or kfunc.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261111051341.2800252-1-alexei.starovoitov@gmail.com?part=16
^ permalink raw reply [flat|nested] 36+ messages in thread
* [PATCH bpf-next v2 17/29] selftests/bpf: Test a pointer computed before its index is bounded
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (15 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 16/29] bpf: Recompute a pointer after the jump that bounds its index Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 18/29] bpf: Promote memory behind a pointer to SCEV registers Alexei Starovoitov
` (11 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
Tests for the patch that recomputes a pointer after the jump that bounds
its index. Since that patch a linked register of a conditional jump can be
a pointer: an address base + the tested register + a constant on every path
to the jump.
Each prog adds an index to a pointer to 64 bytes, tests the index and reads
through the result. What loads is run.
- The add comes before the jump: loads.
- Only the low 32 bits of the index are tested. Rejected.
- The jumps leave no value for the index, base + index is empty: the
pointer is left as it is. Loads with BPF_F_TEST_REG_INVARIANTS.
- base + index at a loop header: the add is at the end of an iteration,
the jump at the header. Negative tests: it holds at loop entry only, on
the backedge only.
- In a prog with an arena 'r1 += r2' is trusted once the main pass has
verified it without an arena pointer (prevent_zext): loads.
The test with an arena is in verifier_arena.c, the others in
verifier_scev.c.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
.../selftests/bpf/progs/verifier_arena.c | 3 ++
.../selftests/bpf/progs/verifier_scev.c | 29 +++++++++++++++++++
2 files changed, 32 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/verifier_arena.c b/tools/testing/selftests/bpf/progs/verifier_arena.c
index bf50413f6c08..3931a1d9b6da 100644
--- a/tools/testing/selftests/bpf/progs/verifier_arena.c
+++ b/tools/testing/selftests/bpf/progs/verifier_arena.c
@@ -663,6 +663,9 @@ ARENA_LINKED(arena_ptr_plus_constant, __failure,
ARENA_LINKED(arena_ptr_plus_constant_on_two_paths, __failure,
"if r8 == 0 goto 1f; r3 += 1; goto 2f; 1: r3 += 1; 2: r8 += 1; r3 = le64 r3; r7 = le64 r7;"
"r2 = 0xffffffff ll; if r7 != r2 goto out_%=; r3 >>= 32; r3 -= 1; r1 = r6; r1 += r3;")
+/* 'r1 += r2' was verified without a pointer to arena: it stays ALU64 */
+ARENA_LINKED(index_tested_after_the_add, __success __retval(0),
+ "r2 = r8; r2 &= 255; r1 = r6; r1 += r2; if r2 > 63 goto out_%=; r0 = *(u8 *)(r6 + 0);")
SEC("syscall")
__description("arena: a number is not an address in arena")
diff --git a/tools/testing/selftests/bpf/progs/verifier_scev.c b/tools/testing/selftests/bpf/progs/verifier_scev.c
index 1f9aa7e691be..3588a0a005e2 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scev.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scev.c
@@ -3372,12 +3372,41 @@ __naked void name(void) \
#define LINKED(name, expect, body) LINKED_SEC("socket", name, expect, body)
#define VALID __success __retval(0)
+#define MAP_OOB __failure __msg("invalid access to map value")
#define UNBOUND __failure __msg("unbounded min value")
/* r3 is the address in r1 as a scalar. It does not bound the offset of r1. */
LINKED(pointer_is_not_scalar, __failure,
"r7 &= 127; r1 = r6; r1 += r7; r3 = r1; r3 = le64 r3; if r3 < 64 goto out_%=; r1 += -64;")
+/*
+ * p = buf + i; if (i > 63) return 0; c = *p;
+ * The jump bounds i. r1 is r6 + r7 and r6 is live: r1 is recomputed.
+ */
+#define USE_R6 "r0 = *(u8 *)(r6 + 0);"
+
+LINKED(index_tested_after_the_add, VALID, "r1 = r6; r1 += r7; if r7 > 63 goto out_%=;" USE_R6)
+LINKED(low_32_bits_of_the_index_tested, __failure __msg("R1 unbounded memory access"),
+ "r2 = 0x1000000ff ll; r2 &= r8; r1 = r6; r1 += r2; w3 = w2; if r3 > 63 goto out_%=;" USE_R6)
+/* r7 is -2 or 5. Its low 2 bits are not 3: the path is infeasible, and r6 + r7 + 2 is empty. */
+LINKED(index_is_empty, VALID __flag(BPF_F_TEST_REG_INVARIANTS),
+ "r7 &= 1; r7 *= 7; r7 += -2; r1 = r6; r1 += r7; r1 += 2; if r7 & 1 goto l_%=; goto out_%=;"
+ "l_%=: if r7 & 2 goto m_%=; goto out_%=; m_%=: r1 += 1;" USE_R6)
+
+/*
+ * p = buf; i = 0; do { if (i > 62) break; i = *(u16 *)p; p = buf + i; } while (i);
+ * r1 == r6 + r2 at the header in every iteration: it holds at loop entry, the backedge preserves it
+ */
+#define LOOP(add) \
+ "l_%=: if r2 > 62 goto out_%=; r2 = *(u16 *)(r1 + 0);" add \
+ "if r2 != 0 goto l_%=; r1 = r6;"
+
+LINKED(base_plus_index_at_loop_header, VALID, "r1 = r6; r2 = 0;" LOOP("r1 = r6; r1 += r2;"))
+LINKED(base_plus_index_at_loop_entry_only, MAP_OOB,
+ "r1 = r6; r2 = 0;" LOOP("r1 = r6; r1 += r2; r2 >>= 1;"))
+LINKED(base_plus_index_on_backedge_only, MAP_OOB,
+ "r1 = r6; r1 += r7; r2 = r8; r2 &= 255;" LOOP("r1 = r6; r1 += r2;"))
+
LINKED(sign_extension_that_fits, VALID, "r2 = (s8)r8; if r8 > 63 goto out_%=; r1 = r6; r1 += r2;")
/* 'int n' in three registers. 'if w3 s> 63' refines only 32-bit ranges: those of the copies too. */
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 18/29] bpf: Promote memory behind a pointer to SCEV registers
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (16 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 17/29] selftests/bpf: Test a pointer computed before its index is bounded Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 19/29] selftests/bpf: Test the memory of a caller that a jump tests Alexei Starovoitov
` (10 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A prerequisite of the removal of scalar ids. Linked registers belong to the
frame of the jump. A scalar id also links a register to a stack slot of
a caller, and programs depend on that:
struct { char *buf; u64 len; } c = { buf, 0 };
bpf_loop(100, append, &c, 0);
static long append(u64 i, void *ctx)
{
if (c->len > 63)
return 1;
n = bpf_get_prandom_u32();
c->buf[c->len] = n;
c->len++;
return 0;
}
c is on the stack of the caller. The compiler loads c->len again after the
call. The first load gives the register the id of the stack slot, so the
jump refines both. Without ids the second load gets the old range:
R2 unbounded memory access, make sure to bounds check any such access
A program that builds a file path in a bpf_loop() callback has this shape.
So has a clamp in a static subprog, 'if (b->len > 63) b->len = 0;'.
A stack slot is memory that scev.c promotes to a register: a spill defines
it, a join point has a phi, an insn that may write is a may-def. Do the
same behind other pointers. struct env gets 16 more SCEV registers, memory
locations. The expression of its address names a location:
29: r6 = r2
30: r1 = *(u64 *)(r6 + 8) r1 is %r1.30, mem0=*(+ r2 8) is %r1.30
31: if r1 > 63 goto .. mem0 is linked to r1
32: call bpf_get_prandom_u32 mem0 is '?'
A store defines a location like a spill. A load defines its register as
before, and the location gets that value number (EarlyCSE, GVN). A call, an
atomic, a nested loop and a store through a pointer are may-defs of all
locations. A store spares those with its address base that it does not
overlap. mk_phi(), find_linked_regs(), const_diff_reg() and the iterator
take a location like a stack slot.
Which memory it is depends on the caller, so the main pass answers that.
bpf_scev_mem_addr() names a live register that holds the address,
scev_mem_to_slot() reads it in the state at the jump. If it points to the
stack of a caller, refine_linked_regs() goes on with that frame and slot.
Why it is sound:
- No expression of a register depends on memory: what is not stack can
change between two loads. Loop summaries and links between registers
stay as they are.
- 'f(&c, &c)' makes two names for one memory. A store through one address
base forgets the locations of all others.
- A store through r10 is no may-def: no slot of the frame of the jump is
taken.
- Nothing is kept in a state, and only what is live is read there.
Why 16: a location is dead once no live register holds its value, no jump
can link it any more. Its SCEV register is reused. In the programs of
Tetragon, Tracee, Falco and the Datadog agent at most 15 are alive at
a time, while one subprog loads and stores at more than 100 addresses.
Not found: no live register holds the address at the jump, as in
'c->n = x; if (x >= 16) return -1; return 0;' and in 'if (w->c->n > 63)'.
A value number of a loop means nothing outside: no location is tracked at
a header or across a nested loop. Liveness does not follow a pointer to
the stack through callx: no location is tracked in a subprog that is only
called through a function pointer.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
include/linux/bpf_verifier.h | 2 +
kernel/bpf/scev.c | 265 +++++++++++++++++++++++++++++++----
kernel/bpf/verifier.c | 43 +++++-
3 files changed, 281 insertions(+), 29 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 11025ed77a7f..5a6bb40310f2 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -2228,6 +2228,8 @@ int bpf_scev_jump_counts(struct bpf_verifier_env *env, int header, int n, bool t
bool bpf_scev_next_linked_reg(struct bpf_verifier_env *env, int insn_idx, u32 regno, u32 *pos,
u32 *linked_regno, u32 *fill_off, int *base_regno, s64 *delta,
u32 *bits);
+bool bpf_scev_mem_addr(struct bpf_verifier_env *env, int insn_idx, u32 regno, u32 *base_regno,
+ s64 *off);
bool bpf_is_widenable_reg_type(const struct bpf_reg_state *reg);
bool bpf_scev_in_slice_slots(struct bpf_verifier_env *env, int off, u32 size, bool fixed_off);
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index 50b440142bf9..98a05f97196a 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -12,6 +12,9 @@
#include <linux/overflow.h>
#define REGS_NUM BPF_SCEV_REGS_NUM
+/* The SCEV registers of an env: r0 - r10, the stack slots and memory locations, see struct env */
+#define MEM_REGS_NUM 16
+#define ENV_REGS_NUM (REGS_NUM + MEM_REGS_NUM)
#define UNKNOWN_EXPR_ID 0
#define OPAQUE_EXPR_ID 1
/* (REG r). UNKNOWN, OPAQUE and (REG r) have fixed ids and are not in the hash table. */
@@ -82,7 +85,7 @@ struct expr_def {
u32 idx:21;
};
/* def.reg of the phi of two bases, see mk_phi() */
-#define BASE_REG REGS_NUM
+#define BASE_REG ENV_REGS_NUM
static_assert(BASE_REG < 1 << 9 && BPF_COMPLEXITY_LIMIT_INSNS <= 1 << 21);
static const struct expr_def no_def;
@@ -113,14 +116,31 @@ struct expr {
struct env {
bool empty;
- u32 reg2expr[REGS_NUM];
+ u32 reg2expr[ENV_REGS_NUM];
u32 reg2scev[REGS_NUM];
+ /*
+ * Register promotion of memory that is not behind r10. The SCEV register REGS_NUM + i is
+ * the memory at the address mem_addr[i], a value number of this env. 0: not in use.
+ * - A store through a pointer defines it like a spill defines a stack slot.
+ * - A load defines its register as before, and the memory gets that value number: it holds
+ * what a store of the register would leave. No expression of a register depends on
+ * memory, which is private only if it is stack. That is not known here.
+ * - A join point has a phi. An insn that may write is a may-def, see forget_mem_regs().
+ * - A store through r10 is not: the main pass takes the stack of a caller only.
+ * A value number of a loop means nothing in the enclosing loop: no memory is tracked at
+ * a header, a nested loop is a may-def.
+ */
+ u32 mem_addr[MEM_REGS_NUM];
};
-/* A SCEV register and the id of its expression before a conditional jump */
+/*
+ * A SCEV register and the id of its expression before a conditional jump. 'addr' is mem_addr[] of
+ * a memory location.
+ */
struct scev_linked_reg {
u32 reg;
u32 id;
+ u32 addr;
};
struct insn_envs {
@@ -253,7 +273,7 @@ static bool expr_is_vn(struct scev *scev, struct expr *e)
* comes from a call that may write to the slot.
*/
if (e->def.set)
- return reg < MAX_BPF_REG || reg == BASE_REG ||
+ return reg < MAX_BPF_REG || reg >= REGS_NUM ||
!test_bit(reg - MAX_BPF_REG, scev->slice_slots);
switch (e->op) {
case UNKNOWN:
@@ -539,8 +559,10 @@ static void log_reg(struct bpf_verifier_env *env, u32 reg)
{
if (reg < MAX_BPF_REG)
bpf_log(&env->log, "r%d", reg);
- else
+ else if (reg < REGS_NUM)
bpf_log(&env->log, "*fp%d", (MAX_BPF_REG - reg - 1) * 8);
+ else
+ bpf_log(&env->log, "mem%d", reg - REGS_NUM);
}
static const char *op_str(u32 op)
@@ -695,8 +717,10 @@ static void log_def(struct bpf_verifier_env *env, struct expr_def def)
return;
if (def.reg < MAX_BPF_REG)
bpf_log(&env->log, "%%r%d.%d", def.reg, def.idx);
- else
+ else if (def.reg < REGS_NUM)
bpf_log(&env->log, "%%fp%d.%d", (MAX_BPF_REG - def.reg - 1) * 8, def.idx);
+ else
+ bpf_log(&env->log, "%%mem%d.%d", def.reg - REGS_NUM, def.idx);
}
static void log_expr(struct bpf_verifier_env *env, u32 id)
@@ -775,6 +799,9 @@ static bool reg_alive_at(struct bpf_verifier_env *env, u32 reg, u32 insn_idx)
struct bpf_insn_aux_data *aux = env->insn_aux_data;
u16 live_regs = aux[insn_idx].live_regs_before;
+ /* memory */
+ if (reg >= REGS_NUM)
+ return true;
return reg < MAX_BPF_REG
? (BIT(reg) & live_regs)
: test_bit(reg - MAX_BPF_REG, aux[insn_idx].live_stack_before);
@@ -1040,6 +1067,129 @@ static int maybe_load_fp(struct scev *scev, struct env *e, struct bpf_insn *insn
return replace_reg(scev, e, insn->dst_reg, id);
}
+static bool const_diff(struct bpf_verifier_env *env, int h, u32 id_a, u32 id_b, s64 *delta,
+ u32 *bits);
+
+/* The memory location of @e at the address @addr, 0: one that is not in use. MEM_REGS_NUM: none. */
+static u32 find_mem_reg(const struct env *e, u32 addr)
+{
+ u32 i;
+
+ for (i = 0; i < MEM_REGS_NUM && e->mem_addr[i] != addr; i++)
+ ;
+ return i;
+}
+
+static void forget_mem_reg(struct env *e, u32 i)
+{
+ e->mem_addr[i] = 0;
+ e->reg2expr[REGS_NUM + i] = UNKNOWN_EXPR_ID;
+}
+
+/*
+ * The insn may write @size bytes at the address @addr, 0: anywhere. A may-def of all memory
+ * locations but those that have the same address base and don't overlap. Two bases can be two names
+ * of one memory, 'f(&c, &c)'.
+ */
+static void forget_mem_regs(struct bpf_verifier_env *env, struct env *e, u32 addr, u32 size)
+{
+ u32 i, x, bits;
+ s64 delta;
+
+ for (i = 0; i < MEM_REGS_NUM; i++) {
+ /* the caller defines it */
+ if (e->mem_addr[i] == addr)
+ continue;
+ if (addr && const_diff(env, -1, addr, e->mem_addr[i], &delta, &bits) &&
+ bits == 64 && (delta >= size ||
+ delta + expr_spill_size(env->scev, e->reg2expr[REGS_NUM + i], &x) <= 0))
+ continue;
+ forget_mem_reg(e, i);
+ }
+}
+
+/*
+ * A jump tests a register, and a load makes a new value number. So a memory location is dead once
+ * no register that is live after the insn at @idx has the base of its value: no jump is going to
+ * link it.
+ */
+static void forget_dead_mem_regs(struct bpf_verifier_env *env, struct env *e, int idx)
+{
+ struct expr *exprs = env->scev->exprs;
+ u32 i, r, base;
+
+ for (i = 0; i < MEM_REGS_NUM; i++) {
+ base = exprs[e->reg2expr[REGS_NUM + i]].base;
+ for (r = 0; r < REGS_NUM; r++)
+ if (exprs[e->reg2expr[r]].base == base && reg_alive_at(env, r, idx + 1))
+ break;
+ if (r == REGS_NUM)
+ forget_mem_reg(e, i);
+ }
+}
+
+/*
+ * The address of the load or store at @idx through @reg, 0: the memory is not tracked. r10 has the
+ * stack slots, r11 and r12 are no SCEV registers. The main pass takes nothing but stack.
+ */
+static int mk_mem_addr(struct bpf_verifier_env *env, struct env *e, int idx, u32 reg)
+{
+ struct scev *scev = env->scev;
+
+ if (reg >= BPF_REG_FP || !(env->insn_aux_data[idx].stack_ptrs & BIT(reg)) ||
+ !scev->exprs[e->reg2expr[reg]].is_vn)
+ return 0;
+ return expr2(scev, BPF_ADD, e->reg2expr[reg], imm_expr(scev, env->prog->insnsi[idx].off));
+}
+
+/*
+ * The insn at @idx stored the low bytes of @id at the address @addr, or it loaded them from there.
+ * The location takes a SCEV register that is not in use, or that of a dead one.
+ */
+static int replace_mem_reg(struct bpf_verifier_env *env, struct env *e, int idx, int addr, int id)
+{
+ u32 i;
+
+ if (addr <= 0)
+ return addr;
+ i = find_mem_reg(e, addr);
+ if (i == MEM_REGS_NUM)
+ i = find_mem_reg(e, 0);
+ if (i == MEM_REGS_NUM) {
+ forget_dead_mem_regs(env, e, idx);
+ i = find_mem_reg(e, 0);
+ }
+ if (i == MEM_REGS_NUM)
+ return 0;
+ e->mem_addr[i] = addr;
+ return replace_reg(env->scev, e, REGS_NUM + i,
+ mk_spill(env->scev, BPF_SIZE(env->prog->insnsi[idx].code), id));
+}
+
+static int load_mem(struct bpf_verifier_env *env, struct env *e, int idx)
+{
+ struct bpf_insn *insn = &env->prog->insnsi[idx];
+ /* the insn defines dst_reg, which can be src_reg */
+ int addr = mk_mem_addr(env, e, idx, insn->src_reg);
+
+ return maybe_load_fp(env->scev, e, insn, idx) ?:
+ replace_mem_reg(env, e, idx, addr, e->reg2expr[insn->dst_reg]);
+}
+
+static int store_mem(struct bpf_verifier_env *env, struct env *e, int idx, int id)
+{
+ struct bpf_insn *insn = &env->prog->insnsi[idx];
+ int addr;
+
+ if (insn->dst_reg == BPF_REG_FP)
+ return maybe_store_fp(env->scev, e, insn, idx, id);
+ addr = mk_mem_addr(env, e, idx, insn->dst_reg);
+ if (addr < 0)
+ return addr;
+ forget_mem_regs(env, e, addr, bpf_size_to_bytes(BPF_SIZE(insn->code)));
+ return replace_mem_reg(env, e, idx, addr, id);
+}
+
/*
* The low 32 bits of a + b depend on the low 32 bits of a and b only: 'w1 += w2' is
* (zext32 (+ a b)). That doesn't hold for a right shift and for a division. Their operands are zero
@@ -1226,19 +1376,19 @@ static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
imm = env->insn_aux_data[idx + 1].const_reg_vals[dst];
return replace_reg(scev, e, dst, imm_expr(scev, imm));
}
- return maybe_load_fp(scev, e, insn, idx);
+ return load_mem(env, e, idx);
default:
goto mark_dst_unknown;
}
case BPF_STX:
switch (mode) {
case BPF_MEM:
- return maybe_store_fp(scev, e, insn, idx, reg2expr[src]);
+ return store_mem(env, e, idx, reg2expr[src]);
case BPF_ATOMIC:
if (insn->imm == BPF_LOAD_ACQ)
return maybe_load_fp(scev, e, insn, idx);
if (insn->imm == BPF_STORE_REL) {
- return maybe_store_fp(scev, e, insn, idx, reg2expr[src]);
+ return store_mem(env, e, idx, reg2expr[src]);
}
/*
* verifier does not track other atomic ops precisely,
@@ -1249,6 +1399,8 @@ static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
bpf_size_to_bytes(BPF_SIZE(insn->code)));
if (id < 0)
return id;
+ } else {
+ forget_mem_regs(env, e, 0, 0);
}
if (insn->imm == BPF_CMPXCHG)
return forget_reg(scev, e, BPF_REG_0, OPAQUE_EXPR_ID, idx);
@@ -1258,16 +1410,14 @@ static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
}
break;
case BPF_ST:
- if (insn->dst_reg == BPF_REG_FP) {
- id = imm_expr(scev, insn->imm);
- if (id < 0)
- return id;
- return maybe_store_fp(scev, e, insn, idx, id);
- }
- break;
+ id = imm_expr(scev, insn->imm);
+ if (id < 0)
+ return id;
+ return store_mem(env, e, idx, id);
case BPF_JMP:
case BPF_JMP32:
if (opcode == BPF_CALL) {
+ forget_mem_regs(env, e, 0, 0);
id = forget_call_regs(env, e, idx);
if (id < 0)
return id;
@@ -1299,6 +1449,7 @@ static int transfer(struct bpf_verifier_env *env, struct env *e, int idx)
/* unknown instruction, nuke state */
for (i = 0; i < REGS_NUM; i++)
reg2expr[i] = UNKNOWN_EXPR_ID;
+ forget_mem_regs(env, e, 0, 0);
break;
}
return 0;
@@ -1456,7 +1607,7 @@ static int mk_phi(struct bpf_verifier_env *env, u32 a, u32 b, int idx, u32 reg)
static int join(struct bpf_verifier_env *env, struct env *acc, struct env *cur, int idx)
{
struct scev *scev = env->scev;
- int i, id;
+ int i, j, id;
if (acc->empty) {
memcpy(acc, cur, sizeof(*acc));
@@ -1464,11 +1615,17 @@ static int join(struct bpf_verifier_env *env, struct env *acc, struct env *cur,
return 0;
}
- for (i = 0; i < REGS_NUM; i++) {
- if (!same_exprs(scev, acc->reg2expr[i], cur->reg2expr[i])) {
+ for (i = 0; i < ENV_REGS_NUM; i++) {
+ /* the address names a memory location. It has a phi if all edges track it. */
+ j = i < REGS_NUM ? i : REGS_NUM + find_mem_reg(cur, acc->mem_addr[i - REGS_NUM]);
+ if (j == ENV_REGS_NUM) {
+ forget_mem_reg(acc, i - REGS_NUM);
+ continue;
+ }
+ if (!same_exprs(scev, acc->reg2expr[i], cur->reg2expr[j])) {
/* no phi for a dead register: pruned SSA */
id = reg_alive_at(env, i, idx) ?
- mk_phi(env, acc->reg2expr[i], cur->reg2expr[i], idx, i) :
+ mk_phi(env, acc->reg2expr[i], cur->reg2expr[j], idx, i) :
UNKNOWN_EXPR_ID;
if (id < 0)
return id;
@@ -1499,6 +1656,7 @@ static int forget_non_invariants(struct bpf_verifier_env *env, struct env *acc,
int i, idx, h, err;
acc->empty = false;
+ forget_mem_regs(env, acc, 0, 0);
forget_modified_regs(scev, acc, find_header_env(scev, header));
for (i = 0; i < loop->exits_cnt; i++) {
idx = loop->exits[i].from;
@@ -1616,7 +1774,7 @@ static void log_env_changes(struct bpf_verifier_env *env, enum changes_at at,
len = log->end_pos - null_pos;
bpf_log(log, "%*s", max(37 - (int)len, 1), " ");
bpf_log(log, " ; ");
- for (r = 0; r < REGS_NUM; r++) {
+ for (r = 0; r < ENV_REGS_NUM; r++) {
old_id = old->reg2expr[r];
new_id = new->reg2expr[r];
if (at == LOG_AT_TRANSFER && !reg_alive_at_succ(env, r, insn_idx))
@@ -1631,6 +1789,10 @@ static void log_env_changes(struct bpf_verifier_env *env, enum changes_at at,
if (any_changes)
bpf_log(log, ", ");
log_reg(env, r);
+ if (r >= REGS_NUM && new->mem_addr[r - REGS_NUM]) {
+ bpf_log(log, "=*");
+ log_expr(env, new->mem_addr[r - REGS_NUM]);
+ }
bpf_log(log, " ");
log_expr(env, old_id);
bpf_log(log, " -> ");
@@ -2036,6 +2198,19 @@ static bool const_diff_reg(struct bpf_verifier_env *env, int h, u32 id_a, u32 re
return true;
}
+/* The first of the registers and stack slots @live that holds @id + a constant. REGS_NUM: none. */
+static u32 find_leader(struct bpf_verifier_env *env, struct env *e, int h,
+ const unsigned long *live, u32 id)
+{
+ u32 r, bits;
+ s64 delta;
+
+ for_each_set_bit(r, live, REGS_NUM)
+ if (const_diff(env, h, e->reg2expr[r], id, &delta, &bits))
+ break;
+ return r;
+}
+
/*
* n = bpf_get_stack(..); m = n; if (n < 0) ..; use(m);
* The main pass refines the registers that the conditional jump at @idx tests. Find the live SCEV
@@ -2051,8 +2226,8 @@ static int find_linked_regs(struct bpf_verifier_env *env, struct env *e, int h,
struct scev *scev = env->scev;
u32 r, q, other, id, fill_off, base, base_dst, base_src, bits, cap, tested = 0;
u32 first = scev->linked_cnt;
- DECLARE_BITMAP(live, REGS_NUM) = {};
- DECLARE_BITMAP(found, REGS_NUM) = {};
+ DECLARE_BITMAP(live, ENV_REGS_NUM) = {};
+ DECLARE_BITMAP(found, ENV_REGS_NUM) = {};
void *tmp;
s64 delta;
@@ -2063,9 +2238,12 @@ static int find_linked_regs(struct bpf_verifier_env *env, struct env *e, int h,
bitmap_copy(live, aux->live_stack_before, MAX_BPF_STACK_SLOTS);
bitmap_shift_left(live, live, MAX_BPF_REG, REGS_NUM);
live[0] |= aux->live_regs_before;
+ for (r = REGS_NUM; r < ENV_REGS_NUM; r++)
+ if (e->mem_addr[r - REGS_NUM])
+ __set_bit(r, live);
split_const_offset(env, dst, &base_dst, &delta, &bits);
split_const_offset(env, src, &base_src, &delta, &bits);
- for_each_set_bit(r, live, REGS_NUM) {
+ for_each_set_bit(r, live, ENV_REGS_NUM) {
id = e->reg2expr[r];
/* most are not linked */
split_const_offset(env, id, &base, &delta, &bits);
@@ -2089,7 +2267,17 @@ static int find_linked_regs(struct bpf_verifier_env *env, struct env *e, int h,
break;
}
}
- for_each_set_bit(r, found, REGS_NUM) {
+ /* the main pass finds memory by a live register that holds its address */
+ for (r = REGS_NUM; r < ENV_REGS_NUM; r++) {
+ if (!test_bit(r, found))
+ continue;
+ q = find_leader(env, e, h, live, e->mem_addr[r - REGS_NUM]);
+ if (q < REGS_NUM)
+ __set_bit(q, found);
+ else
+ __clear_bit(r, found);
+ }
+ for_each_set_bit(r, found, ENV_REGS_NUM) {
if (scev->linked_cnt == scev->linked_cap) {
cap = max(scev->linked_cap * 2, 16);
tmp = kvrealloc(scev->linked, array_size(cap, sizeof(*scev->linked)),
@@ -2101,6 +2289,7 @@ static int find_linked_regs(struct bpf_verifier_env *env, struct env *e, int h,
}
tested += r == insn->dst_reg || (src != dst && r == insn->src_reg);
scev->linked[scev->linked_cnt].reg = r;
+ scev->linked[scev->linked_cnt].addr = r < REGS_NUM ? 0 : e->mem_addr[r - REGS_NUM];
scev->linked[scev->linked_cnt++].id = e->reg2expr[r];
}
/* nothing but the tested registers */
@@ -2127,6 +2316,7 @@ static int find_linked_regs(struct bpf_verifier_env *env, struct env *e, int h,
* no more links are known. Types are not known here: the caller checks them in its state.
* A linked register can be a pointer: *base_regno + regno + delta. *base_regno is the address base,
* as in store_base_regs. It is -1 for a link without a base.
+ * BPF_SCEV_REGS_NUM and above is a memory location, see struct env and bpf_scev_mem_addr().
*/
bool bpf_scev_next_linked_reg(struct bpf_verifier_env *env, int insn_idx, u32 regno, u32 *pos,
u32 *linked_regno, u32 *fill_off, int *base_regno, s64 *delta,
@@ -2167,6 +2357,31 @@ bool bpf_scev_next_linked_reg(struct bpf_verifier_env *env, int insn_idx, u32 re
return false;
}
+/*
+ * The memory location @regno is a linked register of the conditional jump at @insn_idx. Its address
+ * is *@base_regno + *@off. *@base_regno is a live register or stack slot. Which memory it is
+ * depends on the state.
+ */
+bool bpf_scev_mem_addr(struct bpf_verifier_env *env, int insn_idx, u32 regno, u32 *base_regno,
+ s64 *off)
+{
+ struct insn_envs *envs = env->scev->envs[insn_idx];
+ struct scev_linked_reg *linked = &env->scev->linked[envs->linked_first];
+ int h = bpf_loop_at_index(env, insn_idx);
+ u32 i, j, bits;
+
+ for (i = 0; linked[i].reg != regno; i++)
+ if (i + 1 == envs->linked_cnt)
+ return false;
+ for (j = 0; linked[j].reg < REGS_NUM; j++) {
+ if (!const_diff(env, h, linked[j].id, linked[i].addr, off, &bits) || bits != 64)
+ continue;
+ *base_regno = linked[j].reg;
+ return true;
+ }
+ return false;
+}
+
/*
* @cur_header and @first are a loop header. Or -1 and the first insn of a subprog: the subprog is
* the outermost loop, which runs once. (REG r) is the entry value of r, the loops in it are single
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index c2a3f74d441f..cc4cbd9da270 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -19741,6 +19741,36 @@ static struct bpf_reg_state *scev_reg_for_fill(struct bpf_func_state *frame, u32
return &ss->spilled_ptr;
}
+/*
+ * Scalar evolution promoted memory to the SCEV register *@regno, see struct env there, and names
+ * its address at the jump at @idx. This state knows which memory it is: the frame that is returned,
+ * its stack slot *@regno, *@fill_off counts from the start of the slot. The stack of a caller only:
+ * other memory has no state, and a store through r10 is no def of memory for scalar evolution.
+ * What holds the address is live: it was compared all the way from its def. Every place that
+ * compares, joins or drops states cleans them of what is dead first, see flow_arrive().
+ */
+static struct bpf_func_state *scev_mem_to_slot(struct bpf_verifier_env *env,
+ struct bpf_verifier_state *vstate, int idx,
+ u32 *regno, u32 *fill_off)
+{
+ struct bpf_reg_state *ptr;
+ u32 base_regno;
+ s64 addr;
+
+ if (!bpf_scev_mem_addr(env, idx, *regno, &base_regno, &addr))
+ return NULL;
+ ptr = scev_reg_for_fill(vstate->frame[vstate->curframe], base_regno, 0, 64);
+ if (!ptr || ptr->type != PTR_TO_STACK || !tnum_is_const(ptr->var_off) ||
+ ptr->frameno >= vstate->curframe)
+ return NULL;
+ addr += ptr->var_off.value + *fill_off;
+ if (addr >= 0 || addr < -MAX_BPF_STACK_JIT)
+ return NULL;
+ *regno = MAX_BPF_REG + bpf_get_spi(addr);
+ *fill_off = addr & (BPF_REG_SIZE - 1);
+ return vstate->frame[ptr->frameno];
+}
+
/*
* p = buf + i; if (i > 100) return 0; *p = 0;
* p is @dst_reg, x is @ptr_reg, i is @off_reg. Scalar evolution says that p == x + i. So the insns
@@ -19786,7 +19816,7 @@ static bool recompute_ptr(struct bpf_verifier_env *env, struct bpf_reg_state *ds
static void refine_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier_state *vstate,
int idx, u32 regno)
{
- struct bpf_func_state *frame = vstate->frame[vstate->curframe];
+ struct bpf_func_state *frame = vstate->frame[vstate->curframe], *linked_frame;
struct bpf_reg_state *reg, *base_reg, implied, saved;
u32 pos, linked_regno, fill_off, bits;
int base_regno;
@@ -19798,7 +19828,12 @@ static void refine_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier
bpf_scev_next_linked_reg(env, idx, regno, &pos, &linked_regno, &fill_off, &base_regno,
&delta, &bits);
pos++) {
- reg = scev_reg_for_fill(frame, linked_regno, fill_off, bits);
+ linked_frame = frame;
+ if (linked_regno >= BPF_SCEV_REGS_NUM)
+ linked_frame = scev_mem_to_slot(env, vstate, idx, &linked_regno, &fill_off);
+ if (!linked_frame)
+ continue;
+ reg = scev_reg_for_fill(linked_frame, linked_regno, fill_off, bits);
/*
* The other half-slot of a 64-bit spill is dead: __clean_func_state() left
* a 32-bit spill, the low 32 bits of the value.
@@ -19806,7 +19841,7 @@ static void refine_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier
if (!reg && bits == 64 &&
bpf_stack_narrow_access_ok(fill_off, BPF_REG_SIZE / 2, BPF_REG_SIZE)) {
bits = 32;
- reg = scev_reg_for_fill(frame, linked_regno, fill_off, bits);
+ reg = scev_reg_for_fill(linked_frame, linked_regno, fill_off, bits);
}
if (!reg || (base_regno < 0 && reg->type != SCALAR_VALUE))
continue;
@@ -19844,7 +19879,7 @@ static void refine_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier
scratch:
if (linked_regno < MAX_BPF_REG)
mark_reg_scratched(env, linked_regno);
- else
+ else if (linked_frame == frame)
mark_stack_slot_scratched(env, linked_regno - MAX_BPF_REG);
}
}
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 19/29] selftests/bpf: Test the memory of a caller that a jump tests
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (17 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 18/29] bpf: Promote memory behind a pointer to SCEV registers Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 20/29] bpf: Find a copy of an induction variable without scalar ids Alexei Starovoitov
` (9 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
Tests for memory locations in scev.c: a jump that tests what was loaded or
stored through a pointer refines the stack slot of the caller that the
pointer points to. They use LINKED() of verifier_scev.c: r6 is a map value
of 64 bytes, r8 an unbounded scalar. What loads is run.
- A static subprog clamps memory of its caller in place,
'if (b->len > 63) b->len = 0;', and the caller takes b.len for an index.
Negative test: the subprog stores to b->len between load and jump.
- The subprog tests what it stored to b->len.
- It stores to the next field between load and jump: the same address
base, no overlap. Negative test: it stores through a second pointer to
b->len, another address base.
- A bpf_loop() callback tests a field of its ctx, which is on the stack of
the caller, calls a helper and loads the field again. Negative test: the
field is written between load and jump.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
.../selftests/bpf/progs/verifier_scev.c | 63 ++++++++++++++++++-
1 file changed, 62 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/bpf/progs/verifier_scev.c b/tools/testing/selftests/bpf/progs/verifier_scev.c
index 3588a0a005e2..0f737ca6bd07 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scev.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scev.c
@@ -3365,7 +3365,7 @@ __naked void name(void) \
: \
: __imm(bpf_map_lookup_elem), __imm(bpf_get_prandom_u32), __imm(bpf_ktime_get_ns), \
__imm(bpf_dynptr_from_skb), __imm(bpf_dynptr_slice_rdwr), \
- __imm(bpf_ringbuf_reserve), __imm(bpf_ringbuf_discard), \
+ __imm(bpf_ringbuf_reserve), __imm(bpf_ringbuf_discard), __imm(bpf_loop), \
__imm_addr(map64), __imm_addr(ringbuf) \
: __clobber_all); \
}
@@ -3393,6 +3393,67 @@ LINKED(index_is_empty, VALID __flag(BPF_F_TEST_REG_INVARIANTS),
"r7 &= 1; r7 *= 7; r7 += -2; r1 = r6; r1 += r7; r1 += 2; if r7 & 1 goto l_%=; goto out_%=;"
"l_%=: if r7 & 2 goto m_%=; goto out_%=; m_%=: r1 += 1;" USE_R6)
+/*
+ * static void clamp(struct buf *b) { if (b->len > 63) b->len = 0; }
+ * clamp(&b); c = b.data[b.len];
+ * The callee tests memory of its caller: the slot is refined. Not if it may have been written since
+ * the load. r1 and r4 point to b.len, r2 is not bounded.
+ */
+#define CLAMP(name, before_the_jump) \
+static __naked __noinline __used void name(void) \
+{ \
+ asm volatile ( \
+ "r3 = *(u64 *)(r1 + 0);" before_the_jump \
+ "if r3 <= 63 goto +2; r3 = 0; *(u64 *)(r1 + 0) = r3; r0 = 0; exit;" \
+ ::: __clobber_all); \
+}
+#define CALL(callee) \
+ "*(u64 *)(r10 - 16) = r8; r1 = r10; r1 += -16; r4 = r1; r2 = r8; r2 >>= 1;" \
+ "call " #callee "; r2 = *(u64 *)(r10 - 16); r1 = r6; r1 += r2;"
+
+CLAMP(clamp, "")
+LINKED(callee_tests_memory_of_caller, VALID, CALL(clamp))
+CLAMP(store_and_clamp, "*(u64 *)(r1 + 0) = r2;")
+LINKED(callee_writes_memory_of_caller, __failure, CALL(store_and_clamp))
+/* a store defines the memory */
+CLAMP(store_and_test, "r3 = r2; *(u64 *)(r1 + 0) = r3;")
+LINKED(callee_tests_what_it_stored, VALID, CALL(store_and_test))
+/* the same address base, no overlap */
+CLAMP(store_next_and_clamp, "*(u64 *)(r1 + 8) = r2;")
+LINKED(callee_writes_next_field, VALID, CALL(store_next_and_clamp))
+/* another address base can be another name of the memory */
+CLAMP(store_alias_and_clamp, "*(u64 *)(r4 + 0) = r2;")
+LINKED(callee_writes_through_alias, __failure, CALL(store_alias_and_clamp))
+
+/*
+ * struct { char *buf; u64 len; } c = { buf, 0 }; bpf_loop(100, append, &c, 0);
+ * append: if (c->len > 63) return 1; bpf_get_prandom_u32(); c->buf[c->len]; c->len++;
+ * The compiler loads c->len again after a call. c is on the stack of the caller, where len grows
+ * until it is widened. The jump refines that slot: it still holds what the tested register was
+ * loaded from.
+ */
+#define APPEND(name, before_the_jump) \
+static __naked __used void name(void) \
+{ \
+ asm volatile ( \
+ "r6 = r2; r1 = *(u64 *)(r6 + 8);" before_the_jump \
+ "if r1 > 63 goto l_%=;" \
+ "call %[bpf_get_prandom_u32];" \
+ "r2 = *(u64 *)(r6 + 8); r1 = *(u64 *)(r6 + 0); r1 += r2; r0 = *(u8 *)(r1 + 0);" \
+ "r2 += 1; *(u64 *)(r6 + 8) = r2; r0 = 0; exit;" \
+ "l_%=: r0 = 1; exit;" \
+ :: __imm(bpf_get_prandom_u32) : __clobber_all); \
+}
+#define CALL_APPEND(name) \
+ "*(u64 *)(r10 - 16) = r6; r1 = 0; *(u64 *)(r10 - 8) = r1;" \
+ "r1 = 100; r2 = " #name " ll; r3 = r10; r3 += -16; r4 = 0; call %[bpf_loop]; r1 = r6;"
+
+APPEND(append, "")
+LINKED(callback_tests_slot_of_caller, VALID, CALL_APPEND(append))
+/* The slot is written between the load and the jump: len is 0 1 3 7 15 31 63, the slot 126 */
+APPEND(append_stale, "r2 = r1; r2 <<= 1; *(u64 *)(r6 + 8) = r2;")
+LINKED(callback_tests_stale_copy_of_slot, UNBOUND, CALL_APPEND(append_stale))
+
/*
* p = buf; i = 0; do { if (i > 62) break; i = *(u16 *)p; p = buf + i; } while (i);
* r1 == r6 + r2 at the header in every iteration: it holds at loop entry, the backedge preserves it
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 20/29] bpf: Find a copy of an induction variable without scalar ids
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (18 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 19/29] selftests/bpf: Test the memory of a caller that a jump tests Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:57 ` sashiko-bot
2026-11-11 5:13 ` [PATCH bpf-next v2 21/29] bpf: Compute scalar evolution expressions in irreducible loops Alexei Starovoitov
` (8 subsequent siblings)
28 siblings, 1 reply; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A prerequisite of the removal of scalar ids. scev.c has two users of them:
scev_regs_equal() reads them, link_copies() assigns them. Both are for
a loop like this one. It has an iteration count (header_count: how often
the header is executed) before and after the patch:
u32 i = 0;
while (buf[i++] && i < 1023)
;
return buf[i];
clang keeps the value of i from before the increment in r2, and the linear
latch tests r2:
r2 = 0
r1 = 0
l: r1 += 1
if r2 > 1021 goto out
r2 += r0
w3 = *(u8 *)(r2 + 0)
r2 = r1
if w3 != 0 goto l
out: r0 += r1
r1 and r2 have the same backedge value. r2 is a copy of the induction
variable r1 if their entry values are equal too (at the header the two are
congruent induction variables, LLVM: replaceCongruentIVs()).
Commit ab5fa7281144 ("bpf: Count a loop whose exit tests a copy of the
counter") added (copy r2 (linear r1 1)), COPY_SCEV, for that. scev.c
analyzed loops only then, so it left the entry values to the main pass:
scev_regs_equal() accepts the same constant or the same scalar id in the
state at loop entry.
By now scev.c computes expressions for whole subprogs, the code before the
loop included. const_diff_at_header() decides if r2 == r1 holds at the
header in every iteration: it holds at loop entry and every backedge
preserves it. Then r2 is an induction variable of its own, (linear r2 1),
and but for one place no code after compute_header_scevs() has to know that
it is a copy.
It finds more than scev_regs_equal() did: const_diff_at_header() compares
expressions, it does not know types and needs no id. The copy of a pointer
induction variable, and a copy that is equal by value number only
('r1 = r0 & 255; r2 = r0 & 255'), are induction variables too.
bpf_compute_loop_iters() checks the type of each register that it widens,
as for any induction variable.
The one place: if the entry value is a range,
'entry value + step * header_count' is far off.
'for (i = n; i > 0; i--) a[i - 1]' with n in [1, 4096] gives
[-4094, 4096]. latch_iv_range() has a better bound for the
induction variable that the latch tests, and as (copy ..) r2 had the range
of that one. is_iv_at_header() gives it to every induction variable that is
congruent to the tested one. It is the range at the header and
bpf_clamp_scev_regs() takes it there only. After the linear latch
a register can be the zero extension of its header value, and its state
has what still holds of the range.
link_copies() assigns one scalar id to r1 and r2 at the header, so that the
test of r2 bounds r1 at 'out'. The linked registers of a conditional jump
do that without an id: the live registers and stack slots that are a tested
register + a constant on every path to the jump. At the header, the latches
and the exits of a loop they include what const_diff_at_header() finds.
Remove COPY_SCEV, is_copy(), copy_reg, scev_regs_equal(), link_copies().
Precision that is lost:
- Two registers enter the loop with the same constant, and only the state
knows it, not scev.c. The loop has no iteration count.
- At a jump of the loop that is no latch and no exit a test of r1 does not
refine r2: const_diff_at_header() is not used there. copy_less and
copy_more of verifier_loop_latch.c stored at the copy - 1 and + 1 after
such a test. They store at the copy now.
- A short loop that was unrolled because the copy of a pointer had no SCEV
is summarized. After the linear latch a packet pointer is not clamped,
so the copy has the range of one more iteration there, like the
induction variable itself.
No other verdict changes in the selftests, Cilium, sched_ext, katran,
bpfjailer and the programs in Rust.
The new test copy_ahead enters the loop with 0 in the induction variable
and 1 in the copy: no iteration count. copy_of_pointer stores through the
value of a pointer from before the increment, 14350 -> 21 insns;
copy_of_pointer_bad is one byte further and is rejected. In
copy_same_value_number the two registers are computed by the same insns.
copy_down_from_range is the loop that runs down from n, copy_zext zero
extends the copy in it. copy_zext_bad zero extends a copy that starts
below 0, stores at it after the latch and is rejected.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/scev.c | 128 +++++-------------
.../selftests/bpf/progs/verifier_loop_latch.c | 49 ++++++-
2 files changed, 84 insertions(+), 93 deletions(-)
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index 98a05f97196a..28fd06c16fe4 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -48,11 +48,6 @@ enum expr_op {
* bpf_loop_at_index().
*/
LINEAR_SCEV,
- /*
- * param[1], given that register param[0] and the register that param[1] starts from are
- * equal when the loop is entered. Not known otherwise.
- */
- COPY_SCEV,
/*
* (add rB rC) is the SCEV of rA: rA == rB + rC at the header in every iteration. rB and rC
* are the header phis here, not the entry values as below LINEAR_SCEV.
@@ -285,7 +280,6 @@ static bool expr_is_vn(struct scev *scev, struct expr *e)
case SPILL32:
/* a function of the iteration number */
case LINEAR_SCEV:
- case COPY_SCEV:
/* a relation between header phis */
case ADD_SCEV:
return false;
@@ -527,13 +521,6 @@ static bool is_linear(struct scev *scev, int id, u32 *base, u32 *slope)
return is_binop(scev, LINEAR_SCEV, id, base, slope);
}
-static bool is_copy(struct scev *scev, int id, u32 *reg, u32 *orig)
-{
- u32 t;
-
- return is_binop(scev, COPY_SCEV, id, &t, orig) && is_reg(scev, t, reg);
-}
-
static bool is_linear_or_linear32(struct scev *scev, int id, u32 *base, u32 *slope, bool *zext32)
{
u32 t;
@@ -596,7 +583,6 @@ static const char *op_str(u32 op)
case SPILL32: return "spill32";
case ANY: return "any";
case LINEAR_SCEV: return "linear";
- case COPY_SCEV: return "copy";
case ADD_SCEV: return "add";
}
return NULL;
@@ -620,7 +606,6 @@ static u32 op_params_num(u32 op)
case SMOD:
case ANY:
case LINEAR_SCEV:
- case COPY_SCEV:
case ADD_SCEV:
return 2;
case BPF_NEG:
@@ -2743,9 +2728,10 @@ static int instantiate_header_scevs(struct scev *scev, u32 id, void *priv)
return id;
}
-static int compute_header_scevs(struct bpf_verifier_env *env, struct env *header_env)
+static int compute_header_scevs(struct bpf_verifier_env *env, int h)
{
struct scev *scev = env->scev;
+ struct env *header_env = find_header_env(scev, h);
int l, r, t, id, ra, rb, ra_expr;
bool zext32;
s64 imm;
@@ -2830,7 +2816,7 @@ static int compute_header_scevs(struct bpf_verifier_env *env, struct env *header
/*
* do { i++; if (old > 1021) break; c = buf[old]; old = i; } while (c);
* rA = (+ rB IMM) and rB = (+ rB IMM): an iteration leaves the same number in both. If
- * they are equal when the loop is entered rA is at the header what rB is.
+ * they are equal at loop entry too, rA is an induction variable with the same step.
*/
for (ra = 0; ra < REGS_NUM; ra++) {
if (header_env->reg2scev[ra] != UNKNOWN_EXPR_ID ||
@@ -2840,9 +2826,12 @@ static int compute_header_scevs(struct bpf_verifier_env *env, struct env *header
id = header_env->reg2scev[rb];
if (header_env->reg2expr[rb] != header_env->reg2expr[ra] ||
!is_linear_or_linear32(scev, id, &l, &r, &zext32) ||
- !is_reg(scev, l, &t) || t != rb)
+ !is_reg(scev, l, &t) || t != rb ||
+ !const_diff_at_header(env, h, rb, ra, &imm) || imm)
continue;
- id = expr2(scev, COPY_SCEV, expr1(scev, REG, ra), id);
+ id = expr2(scev, LINEAR_SCEV, expr1(scev, REG, ra), r);
+ if (zext32)
+ id = expr1(scev, ZEXT32, id);
if (id < 0)
return id;
header_env->reg2scev[ra] = id;
@@ -3072,7 +3061,7 @@ int bpf_compute_scev(struct bpf_verifier_env *env)
cond_resched();
header = bpf_loop_at_index(env, idx);
err = aux[idx].loop
- ? compute_header_scevs(env, find_header_env(scev, header))
+ ? compute_header_scevs(env, header)
: compute_insn_scevs(env,
find_header_env(scev, header),
find_loop_env(scev, header, idx));
@@ -3219,19 +3208,6 @@ static struct bpf_reg_state *scev_reg_for_linear(struct bpf_func_state *st, u32
return &stack->spilled_ptr;
}
-/* Do the registers or stack slots @a and @b hold the same number in @st? */
-static bool scev_regs_equal(struct bpf_func_state *st, u32 a, u32 b)
-{
- const struct bpf_reg_state *ra = scev_reg_for_linear(st, a);
- const struct bpf_reg_state *rb = scev_reg_for_linear(st, b);
-
- if (!ra || !rb || ra->type != SCALAR_VALUE || rb->type != SCALAR_VALUE)
- return false;
- if (tnum_is_const(ra->var_off) && tnum_is_const(rb->var_off))
- return ra->var_off.value == rb->var_off.value;
- return ra->id && ra->id == rb->id && ra->delta == rb->delta;
-}
-
/* '*(u32 *)(r10 - 8) = rX' left a number in the low half of the slot. */
static struct bpf_reg_state *scev_reg_for_spill32(struct bpf_func_state *st, u32 r)
{
@@ -3349,8 +3325,7 @@ static bool scev_reg_alive(struct bpf_verifier_env *env, struct bpf_verifier_sta
* 'i' is the loop iteration number, starting from 0.
* 'zext32' indicates the latch is for (zext32 (linear ...)) SCEV.
* 'jmp32' indicates that the latch is a JMP32 instruction.
- * 'base_reg' is the register in <base>. 'copy_reg' is -1, or <reg> is made from that register,
- * which has to be equal to 'base_reg' at the entry of the loop, see COPY_SCEV.
+ * 'base_reg' is the register in <base>.
*/
struct linear_latch {
u32 insn_idx;
@@ -3358,7 +3333,6 @@ struct linear_latch {
u32 step_expr;
u32 bound_expr;
u32 base_reg;
- int copy_reg;
u8 op;
bool zext32;
bool jmp32;
@@ -3382,7 +3356,7 @@ static int match_linear_latch(struct bpf_verifier_env *env,
struct env *latch_env;
u32 src_reg_scev;
u32 dst_reg_scev;
- u32 t, l, r, op;
+ u32 l, r, op;
int id;
op = BPF_OP(insn->code);
@@ -3412,11 +3386,6 @@ static int match_linear_latch(struct bpf_verifier_env *env,
return -EFAULT;
dst_reg_scev = latch_env->reg2scev[insn->dst_reg];
src_reg_scev = latch_env->reg2scev[insn->src_reg];
- latch->copy_reg = -1;
- if (is_copy(scev, dst_reg_scev, &latch->copy_reg, &t))
- dst_reg_scev = t;
- else if (BPF_SRC(insn->code) == BPF_X && is_copy(scev, src_reg_scev, &latch->copy_reg, &t))
- src_reg_scev = t;
/*
* Make sure (linear ...) is in dst_reg_scev:
* 'if <invariant> <op> <linear>' ⇔
@@ -4114,8 +4083,6 @@ static bool compute_max_iters(struct bpf_verifier_env *env,
iters->iv_reg = -1;
- if (latch->copy_reg >= 0 && !scev_regs_equal(st, latch->copy_reg, latch->base_reg))
- return false;
if (!eval_expr(env, scev, st, latch->base_expr, &initial_val) ||
!eval_expr(env, scev, st, latch->step_expr, &step_val) ||
!eval_expr(env, scev, st, latch->bound_expr, &bound_val))
@@ -4451,49 +4418,15 @@ static bool is_simple_linear(struct scev *scev, u32 id, struct linear_coefs *out
}
/*
- * Is @r 'what it is in @st + slope * k' at the header in iteration k? @id is its SCEV there, @st is
- * the state that entered the loop. At a jump in the loop @id can be what another register was at
- * the header. @st says nothing about @r then.
+ * Is @r 'its entry value + slope * k' at the header in iteration k? @id is its SCEV there. At a
+ * jump in the loop @id can be linear in the entry value of another register: the entry value of @r
+ * does not bound it then.
*/
-static bool is_linear_at_header(struct scev *scev, struct bpf_func_state *st, u32 id, u32 r,
- struct linear_coefs *out)
+static bool is_linear_at_header(struct scev *scev, u32 id, u32 r, struct linear_coefs *out)
{
- u32 copy, orig;
-
- if (is_copy(scev, id, ©, &orig))
- return copy == r && is_simple_linear(scev, orig, out) &&
- scev_regs_equal(st, r, out->base_reg);
return is_simple_linear(scev, id, out) && out->base_reg == r;
}
-/*
- * @st is at the header of a loop that @entry entered. A copy of an induction variable is equal to
- * it there in every iteration. They got their ranges one by one: give them one id, like 'r2 = r1'
- * does, so that a test of one of them is a test of both.
- */
-static void link_copies(struct bpf_verifier_env *env, struct env *header_env,
- struct bpf_func_state *entry, struct bpf_func_state *st)
-{
- struct bpf_reg_state *reg, *orig;
- struct linear_coefs coefs;
- u32 r;
-
- for (r = 0; r < REGS_NUM; r++) {
- if (!is_linear_at_header(env->scev, entry, header_env->reg2scev[r], r, &coefs) ||
- coefs.base_reg == r)
- continue;
- reg = scev_reg_for_linear(st, r);
- orig = scev_reg_for_linear(st, coefs.base_reg);
- if (!reg || !orig || reg->type != SCALAR_VALUE || orig->type != SCALAR_VALUE ||
- tnum_is_const(orig->var_off))
- continue;
- if (!orig->id)
- orig->id = ++env->id_gen;
- reg->id = orig->id;
- reg->delta = orig->delta;
- }
-}
-
/*
* A 32-bit counter that lives on the stack: the SCEV of slot @r is
* (spill32 (linear (zext32 @r) slope)) and there is a 32-bit number in the slot in @st.
@@ -4585,6 +4518,19 @@ static void linear_bounds(struct bpf_reg_state *reg, u32 max_header_count,
out->r32 = r32;
}
+/*
+ * Is @r the induction variable that the latch tests, or congruent to it: equal to it at the header
+ * of the loop at @h in every iteration? Then the range of the one is the range of the other.
+ */
+static bool is_iv_at_header(struct bpf_verifier_env *env, int h, int iv_reg, u32 r)
+{
+ s64 delta;
+
+ if (iv_reg < 0)
+ return false;
+ return r == iv_reg || (const_diff_at_header(env, h, iv_reg, r, &delta) && !delta);
+}
+
static void iv_bounds(struct bounds *bounds, struct cnum64 iv_range)
{
struct cnum64 r64 = cnum64_intersect(bounds->r64, iv_range);
@@ -4902,7 +4848,7 @@ int bpf_compute_loop_iters(struct bpf_verifier_env *env, struct bpf_verifier_sta
continue;
}
/* If SCEV for r is (linear <reg> <slope>) */
- if (reg && is_linear_at_header(scev, cur_func, r_scev, r, &coefs) &&
+ if (reg && is_linear_at_header(scev, r_scev, r, &coefs) &&
bpf_is_widenable_reg_type(reg)) {
/* 32-bit ALU operations on pointers convert them to scalars */
if (coefs.zext32 && reg->type != SCALAR_VALUE)
@@ -5118,7 +5064,7 @@ int bpf_widen_scev_regs(struct bpf_verifier_env *env, struct bpf_verifier_state
}
/* If SCEV for r is (linear <reg> <slope>)*/
- if (spill32 || is_linear_at_header(scev, entry_func, r_expr, r, &coefs)) {
+ if (spill32 || is_linear_at_header(scev, r_expr, r, &coefs)) {
/* the range is derived from @loop_entry: bpf_clamp_scev_regs() */
if (back)
continue;
@@ -5128,7 +5074,7 @@ int bpf_widen_scev_regs(struct bpf_verifier_env *env, struct bpf_verifier_state
if (err)
return err;
linear_bounds(reg, iters->max_header_count, &coefs, &bounds);
- if (coefs.base_reg == iters->iv_reg)
+ if (is_iv_at_header(env, insn_idx, iters->iv_reg, coefs.base_reg))
iv_bounds(&bounds, iters->iv_range);
if (log->level & BPF_LOG_LEVEL2) {
bpf_log(log, "loop header at %d, widening ", insn_idx);
@@ -5166,7 +5112,6 @@ int bpf_widen_scev_regs(struct bpf_verifier_env *env, struct bpf_verifier_state
mark_scev_reg_scratched(env, r);
}
}
- link_copies(env, header_env, entry_func, cur_func);
return 1;
}
@@ -5223,7 +5168,7 @@ int bpf_clamp_scev_regs(struct bpf_verifier_env *env, struct bpf_func_state *cur
goto clamp;
/* If SCEV for r is (linear <reg> <slope>)*/
- if (!is_linear_at_header(scev, entry_st, r_scev, r, &coefs) &&
+ if (!is_linear_at_header(scev, r_scev, r, &coefs) &&
!(behind && is_moved_linear(scev, r_scev, &coefs, &moved) &&
coefs.base_reg == r))
continue;
@@ -5251,7 +5196,11 @@ int bpf_clamp_scev_regs(struct bpf_verifier_env *env, struct bpf_func_state *cur
continue;
clamp:
linear_bounds(entry_reg, max_header_count, &coefs, &bounds);
- if (coefs.base_reg == entry->iv_reg && !moved)
+ /*
+ * iv_range is the range at the header. After the latch @r can be the zero extension
+ * of its header value. @reg went through that insn and has what holds of the range.
+ */
+ if (!behind && is_iv_at_header(env, insn_idx, entry->iv_reg, coefs.base_reg))
iv_bounds(&bounds, entry->iv_range);
if (moved) {
/* a sum that can wrap has another residue, see scalar_step_add() */
@@ -5293,8 +5242,5 @@ int bpf_clamp_scev_regs(struct bpf_verifier_env *env, struct bpf_func_state *cur
return err;
mark_scev_reg_scratched(env, r);
}
- /* behind the jump the variable may have moved on: its copy is what it was at the head */
- if (!behind)
- link_copies(env, header_env, entry_st, cur_func_state);
return 0;
}
diff --git a/tools/testing/selftests/bpf/progs/verifier_loop_latch.c b/tools/testing/selftests/bpf/progs/verifier_loop_latch.c
index 35bb3e16db3d..17222e1332fd 100644
--- a/tools/testing/selftests/bpf/progs/verifier_loop_latch.c
+++ b/tools/testing/selftests/bpf/progs/verifier_loop_latch.c
@@ -271,6 +271,51 @@ LOOP(copy_bad, "r8 = 0; r9 = r8;", COPY(2047))
BAD("a copy of the counter, another number at the entry")
LOOP(copy_other_const, "r8 = 2047; r9 = 0; r6 += 2047;", COPY(2046))
+/* the latch exits one iteration later than for an induction variable that starts from 1 */
+NO_COUNT("a copy of the counter, one more at the entry")
+LOOP(copy_ahead, "r8 = 0; r9 = 1;", COPY(2047))
+
+/* r5 is a copy of the pointer r6 from before the increment: an induction variable too */
+#define PTR_COPY(off) "*(u8 *)(r5 + " #off ") = r1; r5 = r6; r8 += 1; if r8 < 2048 goto 1b;"
+
+OK("a copy of the pointer", "2048") __msg("widening r5 to 0..2047 step 1")
+LOOP(copy_of_pointer, "r5 = r6; r8 = 0;", PTR_COPY(0))
+
+BAD("a copy of the pointer, one too many")
+LOOP(copy_of_pointer_bad, "r5 = r6; r8 = 0;", PTR_COPY(1))
+
+/* r8 and r9 are computed in the same way from r7: equal by value number, 'r9 = r8' is not needed */
+OK("a copy of the counter, equal by value number", "[0..2]")
+LOOP(copy_same_value_number,
+ "r5 = r6; r8 = r7; r8 &= 1; r8 += 2045; r9 = r7; r9 &= 1; r9 += 2045;", COPY(2045) AT_R8)
+
+/*
+ * 'for (i = n; i > 0; i--) buf[i - 1] = 0;': r9 is i from before the decrement, the latch tests r8.
+ * r9 gets the range that the latch implies for r8.
+ */
+#define DOWN(off) \
+ "r8 += -1; r2 = r5; r2 += r9; *(u8 *)(r2 " off ") = r1; r9 = r8; if r8 > 0 goto 1b;"
+#define FROM_RANGE "if r7 > 2048 goto 9f; if r7 == 0 goto 9f; r5 = r6; r8 = r7; r9 = r7;"
+
+OK("a copy of the counter, down from a range", "[0..2048]")
+LOOP(copy_down_from_range, FROM_RANGE, DOWN("- 1"))
+
+BAD("a copy of the counter, down from a range, one too many")
+LOOP(copy_down_from_range_bad, FROM_RANGE, DOWN("+ 0"))
+
+/*
+ * r9 is zero extended. [1, 2048], its range at the header, is the range of the zero extension too.
+ * [-1000, 1001] is not: the first store is 4G behind the value.
+ */
+OK("a copy of the counter, zero extended", "[0..2048]")
+LOOP(copy_zext, FROM_RANGE, "w9 = w9;" DOWN("- 1"))
+
+SEC("socket") __description("latch: a copy of the counter, zero extended, from below 0")
+__failure __msg("R4 unbounded memory access")
+LOOP(copy_zext_bad, "r5 = r6; r8 = r7; r8 &= 255; r8 += -1000; r9 = r8;",
+ "w9 = w9; if r8 s> 1000 goto 9f; r4 = r5; r4 += r9; *(u8 *)(r4 + 1000) = r1;"
+ "r8 += 1; r9 = r8; if r7 != 0 goto 1b;")
+
OK("a copy of the counter, a range", "[0..2]")
LOOP(copy_range, "r5 = r6; r8 = r7; r8 &= 1; r8 += 2045; r9 = r8;", COPY(2045) AT_R8)
@@ -297,13 +342,13 @@ LOOP(copy_bounds_both, "r5 = r6; if r7 > 2047 goto 9f; r8 = r7; r9 = r8;",
"r4 = r5; r4 += r9; *(u8 *)(r4 " off ") = r1; 2: r9 = r8; if r7 != 0 goto 1b;"
OK("a copy of the counter, one less behind the jump", "[0..2048]")
-LOOP(copy_less, "r5 = r6; r8 = 0; r9 = r8;", NOT_EQUAL(1, "> 2046", "< 2", "- 1"))
+LOOP(copy_less, "r5 = r6; r8 = 0; r9 = r8;", NOT_EQUAL(1, "> 2046", "< 2", "+ 0"))
BAD("a copy of the counter, one less behind the jump, not equal")
LOOP(copy_less_bad, "r5 = r6; r8 = 0; r9 = r8;", NOT_EQUAL(1, "> 2046", "< 2", "- 2"))
OK("a copy of the counter, one more behind the jump", "[0..2047]")
-LOOP(copy_more, "r5 = r6; r8 = 2047; r9 = r8;", NOT_EQUAL(-1, "s< 2", "> 2045", "+ 1"))
+LOOP(copy_more, "r5 = r6; r8 = 2047; r9 = r8;", NOT_EQUAL(-1, "s< 2", "> 2045", "+ 0"))
BAD("a copy of the counter, one more behind the jump, not equal")
LOOP(copy_more_bad, "r5 = r6; r8 = 2047; r9 = r8;", NOT_EQUAL(-1, "s< 2", "> 2045", "+ 2"))
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* Re: [PATCH bpf-next v2 20/29] bpf: Find a copy of an induction variable without scalar ids
2026-11-11 5:13 ` [PATCH bpf-next v2 20/29] bpf: Find a copy of an induction variable without scalar ids Alexei Starovoitov
@ 2026-11-11 5:57 ` sashiko-bot
0 siblings, 0 replies; 36+ messages in thread
From: sashiko-bot @ 2026-11-11 5:57 UTC (permalink / raw)
To: Alexei Starovoitov, bpf
Thank you for your contribution! Sashiko AI review found 1 potential
issue to consider:
- [High] bpf: type confusion between pointer and scalar bounds in
is_iv_at_header()
--
commit a899486291f489b60aa159499a2316c95dbc938e
Author: Alexei Starovoitov <ast@kernel.org>
bpf: Find a copy of an induction variable without scalar ids
This commit replaces COPY_SCEV and scalar ID checks for induction variable
copies with untyped expression comparisons via const_diff_at_header().
> diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
> index 98a05f97196a6..28fd06c16fe4d 100644
> --- a/kernel/bpf/scev.c
> +++ b/kernel/bpf/scev.c
[ ... ]
> @@ -4585,6 +4518,19 @@ static void linear_bounds(struct bpf_reg_state *reg, u32 max_header_count,
> out->r32 = r32;
> }
>
> +/*
> + * Is @r the induction variable that the latch tests, or congruent to it: equal to it at the header
> + * of the loop at @h in every iteration? Then the range of the one is the range of the other.
> + */
> +static bool is_iv_at_header(struct bpf_verifier_env *env, int h, int iv_reg, u32 r)
> +{
> + s64 delta;
> +
> + if (iv_reg < 0)
> + return false;
> + return r == iv_reg || (const_diff_at_header(env, h, iv_reg, r, &delta) && !delta);
> +}
> +
> static void iv_bounds(struct bounds *bounds, struct cnum64 iv_range)
> {
> struct cnum64 r64 = cnum64_intersect(bounds->r64, iv_range);
[ ... ]
> @@ -5128,7 +5074,7 @@ int bpf_widen_scev_regs(struct bpf_verifier_env *env, struct bpf_verifier_state
> if (err)
> return err;
> linear_bounds(reg, iters->max_header_count, &coefs, &bounds);
> - if (coefs.base_reg == iters->iv_reg)
> + if (is_iv_at_header(env, insn_idx, iters->iv_reg, coefs.base_reg))
> iv_bounds(&bounds, iters->iv_range);
> if (log->level & BPF_LOG_LEVEL2) {
> bpf_log(log, "loop header at %d, widening ", insn_idx);
[ ... ]
> @@ -5251,7 +5196,11 @@ int bpf_clamp_scev_regs(struct bpf_verifier_env *env, struct bpf_func_state *cur
> continue;
> clamp:
> linear_bounds(entry_reg, max_header_count, &coefs, &bounds);
> - if (coefs.base_reg == entry->iv_reg && !moved)
> + /*
> + * iv_range is the range at the header. After the latch @r can be the zero extension
> + * of its header value. @reg went through that insn and has what holds of the range.
> + */
> + if (!behind && is_iv_at_header(env, insn_idx, entry->iv_reg, coefs.base_reg))
> iv_bounds(&bounds, entry->iv_range);
> if (moved) {
> /* a sum that can wrap has another residue, see scalar_step_add() */
[Severity: High]
Can is_iv_at_header() apply a pointer induction variable's offset range
(iv_range) to a congruent SCALAR_VALUE register?
const_diff_at_header() compares untyped SCEV expressions without checking
that iv_reg and r have the same register type in the loop-entry state. In
transfer(), a no-op 64-bit BPF_ALU | BPF_END (such as le64 on a
little-endian host) leaves reg2expr[dst] unchanged:
kernel/bpf/scev.c:transfer() {
...
need_bswap = class == BPF_ALU64 || ((x_or_k == BPF_TO_LE) != little_endian);
switch (insn->imm) {
case 16: op = need_bswap ? BSWAP16 : ZEXT16; break;
case 32: op = need_bswap ? BSWAP32 : ZEXT32; break;
case 64: op = need_bswap ? BSWAP64 : 0; break;
...
id = reg2expr[dst];
if (op)
id = expr1(scev, op, reg2expr[dst]);
return replace_reg(scev, e, dst, id);
}
When env->allow_ptr_leaks is enabled, check_alu_op() degrades a pointer
register passed to that instruction to an unknown SCALAR_VALUE while the
instruction preserves the raw 64-bit pointer value at runtime.
If iv_reg is a pointer induction variable (for example PTR_TO_MAP_VALUE
with offset in [1, 10]) and r is copied from iv_reg and passed through a
64-bit no-op BPF_END both before the loop and on the backedge,
compute_header_scevs() and is_iv_at_header() treat r as congruent to
iv_reg. bpf_widen_scev_regs() and bpf_clamp_scev_regs() then intersect
r's scalar bounds with iv_range, causing the verifier to bound a register
holding a raw 64-bit kernel pointer to the small scalar range [1, 10] and
allowing out-of-bounds memory access.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261111051341.2800252-1-alexei.starovoitov@gmail.com?part=20
^ permalink raw reply [flat|nested] 36+ messages in thread
* [PATCH bpf-next v2 21/29] bpf: Compute scalar evolution expressions in irreducible loops
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (19 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 20/29] bpf: Find a copy of an induction variable without scalar ids Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 22/29] bpf: Remove sync_linked_regs(), refine by scalar evolution only Alexei Starovoitov
` (7 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A prerequisite of the removal of scalar ids. bpfj_proc_file_open of
bpfjailer loads before and after the patch. Today it loads because of
a scalar id, and scev.c has no replacement for that id in an irreducible
loop:
for (int i = BPFJ_MAX_POD_PER_PID - 1; i >= 0; --i) {
if (i >= num_pods) {
continue;
}
struct bpfj_pod __arena* pod = actor->pods[i];
That is role_gate.h. i lives on the stack (clang, -mcpu=v4):
171: r1 = *(u64 *)(r10 -40)
172: if w1 > 0x7 goto pc-10
173: r1 = *(u64 *)(r10 -40)
174: r1 <<= 3
The loop is entered at 163 and at 171: 'loop at 171, irreducible' in the
level 2 log. compute_scev_for_loop() gives up on such a loop: nothing is
known at its header, its body is not analyzed. The linked registers of
a conditional jump are found while its loop is analyzed, so the jump at 172
has none. Only the scalar id that r1 and fp-40 share bounds what insn 173
fills.
scev.c has two layers. Expressions for registers and stack slots are value
numbering, which works on any CFG. Recurrences, latches and iteration
counts need a natural loop. So analyze the body of an irreducible loop like
any other body, with the same worklist, and keep the second layer off:
at the end the header env is set to unknown. For the enclosing loop and for
the main pass the loop clobbers everything.
Why that is correct:
- The worklist needs a topological order of the body, without backedges
and with nested loops collapsed. Reverse post-order is one for every
loop: compute_loops_in_subprog() makes the target of every retreating
edge of its depth-first search a loop header, and
bpf_compute_postorder() does the same search.
- (REG r) is the value of r at the header, and the header of an
irreducible loop does not dominate the body: executions enter at
a secondary entry. What they bring is not known here. So an insn
of the loop whose immediate dominator is outside of the loop gets a phi
for every live register and stack slot and tracks no memory, like
the target of an edge that the verifier adds, see join_added_edges().
- An edge that the verifier adds goes where an exit of an outermost loop
goes. In another loop that is the header or a secondary entry.
A natural loop nested in an irreducible one now has an env at loop entry,
for const_diff_at_header() and compute_header_adds(). A loop that nests
a loop with more than 256 exits is still not analyzed: the successors of
the nested loop are not known.
Tests: a link within a basic block, across basic blocks, at the header of
a nested natural loop. Each has a negative test that makes the link before
the secondary entry.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/scev.c | 50 +++++++++++--------
.../selftests/bpf/progs/verifier_scev.c | 35 +++++++++++++
2 files changed, 65 insertions(+), 20 deletions(-)
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index 28fd06c16fe4..d95b8d26dc51 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -1938,7 +1938,7 @@ static int join_successor(struct bpf_verifier_env *env, int cur_header, int succ
{
bool log_level2 = env->log.level & BPF_LOG_LEVEL2;
struct scev *scev = env->scev;
- int err, nested_header;
+ int err, nested_header, idom;
struct env *succ_env;
/*
@@ -1966,6 +1966,18 @@ static int join_successor(struct bpf_verifier_env *env, int cur_header, int succ
succ_env = get_loop_env(scev, cur_header, succ_idx);
if (!succ_env)
return -ENOMEM;
+ /*
+ * The header of an irreducible loop does not dominate its body. An insn whose immediate
+ * dominator is outside of the loop is reached from a secondary entry, without the header.
+ * The env at the source of an entering edge is not known: all '?', like a new env. So the
+ * insn is a join point with a phi for every live register, see join_added_edges().
+ */
+ idom = env->idoms[succ_idx];
+ if (cur_header >= 0 && succ_idx != cur_header &&
+ env->insn_aux_data[cur_header].loop->irreducible &&
+ bpf_loop_at_index(env, idom) != cur_header &&
+ topmost_nested_loop(env, idom, cur_header) < 0)
+ succ_env->empty = false;
if (log_level2)
memcpy(old_env, succ_env, sizeof(*old_env));
err = join(env, succ_env, cur_env, succ_idx);
@@ -1980,7 +1992,7 @@ static int join_successor(struct bpf_verifier_env *env, int cur_header, int succ
}
/*
- * Unsupported loops get only an unknown header environment.
+ * Recurrences need a natural loop. Unsupported loops get an unknown header environment.
* A nested loop is processed as a single instruction, its exits are the successors: all of
* them are needed. No result for the loop itself depends on the list of its own exits:
* mark_latches() prepares them for the loop around, and compute_max_iters() asks if there is
@@ -2371,6 +2383,10 @@ bool bpf_scev_mem_addr(struct bpf_verifier_env *env, int insn_idx, u32 regno, u3
* @cur_header and @first are a loop header. Or -1 and the first insn of a subprog: the subprog is
* the outermost loop, which runs once. (REG r) is the entry value of r, the loops in it are single
* instructions.
+ * Value numbering works on any CFG: the body of an irreducible loop is walked like any other. Every
+ * retreating edge of the depth-first search goes to a loop header, see compute_loops_in_subprog(),
+ * and bpf_compute_postorder() does the same search. So reverse post-order is a topological order of
+ * this body too.
*/
static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header, int first)
{
@@ -2395,21 +2411,6 @@ static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header, i
if (!cur_loop && !can_compute_subprog_scev(env, sub))
return 0;
- /*
- * For irreducible loops, and for loops nesting a loop with a truncated
- * exit list, just assume that everything is clobbered for now.
- */
- if (cur_loop && !can_compute_loop_scev(cur_loop)) {
- header_env = get_loop_env(scev, cur_header, cur_header);
- if (!header_env)
- return -ENOMEM;
- /* The freshly allocated environment has all expressions unknown. */
- bitmap_fill(cur_loop->store_base_regs, REGS_NUM);
- bitmap_fill(cur_loop->tested_regs, REGS_NUM);
- header_env->empty = false;
- return 0;
- }
-
cur_env = kzalloc(sizeof(*cur_env), GFP_KERNEL_ACCOUNT);
if (!cur_env)
goto nomem;
@@ -2422,9 +2423,12 @@ static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header, i
if (!header_env)
goto nomem;
setup_initial_loop_env(header_env);
- err = worklist_push(env, cur_header, first);
- if (err)
- goto out;
+ /* The successors of a nested loop with a truncated exit list are not known: no walk */
+ if (!cur_loop || !cur_loop->nested_exits_overflow) {
+ err = worklist_push(env, cur_header, first);
+ if (err)
+ goto out;
+ }
if (!cur_loop) {
/* no backedge overwrites it */
header_env->empty = false;
@@ -2527,6 +2531,12 @@ static int compute_scev_for_loop(struct bpf_verifier_env *env, int cur_header, i
free_loop_env(scev, cur_header, start);
}
+ /* For the enclosing loop and for the main pass it clobbers everything */
+ if (cur_loop && !can_compute_loop_scev(cur_loop)) {
+ memset(header_env, 0, sizeof(*header_env));
+ bitmap_fill(cur_loop->store_base_regs, REGS_NUM);
+ bitmap_fill(cur_loop->tested_regs, REGS_NUM);
+ }
err = 0;
out:
kfree(cur_env);
diff --git a/tools/testing/selftests/bpf/progs/verifier_scev.c b/tools/testing/selftests/bpf/progs/verifier_scev.c
index 0f737ca6bd07..9b1877a1b5e5 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scev.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scev.c
@@ -3574,6 +3574,41 @@ LINKED(linked_at_loop_header, VALID,
LINKED(linked_at_loop_entry_only, __failure,
LOOP_HEADER("r3 = r8; r2 = r8; r2 += 1; r2 += 1;", "r2 += 2; r3 += 1;"))
+/*
+ * An irreducible loop: 'l' is the header, 'm' is a secondary entry. A link that is made in the loop
+ * holds in the next basic blocks too. Not after 'm' if it is made before 'm': every edge of the
+ * loop to 'm' has it, the edge that enters the loop there does not. That edge can come from
+ * a natural loop 'o' around the irreducible one.
+ * In the last two tests 'o' is the header. The natural loop in it has an env at loop entry. Its
+ * header can be the secondary entry.
+ */
+LINKED(irreducible_loop, VALID,
+ "if r7 > 5 goto m_%=; l_%=: r7 += 1;"
+ "m_%=: r2 = r8; r2 += 1; r2 += 1; if r8 > 61 goto out_%=; r1 = r6; r1 += r2;"
+ "r0 = *(u8 *)(r1 + 0); if r7 < 10 goto l_%=;")
+LINKED(irreducible_loop_fallthrough_pred, UNBOUND,
+ "r2 = r7; if r7 > 5 goto m_%=; l_%=: r7 += 1; r2 = r8;"
+ "m_%=: if r8 > 63 goto out_%=; r1 = r6; r1 += r2;"
+ "r0 = *(u8 *)(r1 + 0); if r7 < 10 goto l_%=;")
+LINKED(irreducible_loop_next_bb, VALID,
+ "if r7 > 5 goto m_%=; l_%=: r7 += 1;"
+ "m_%=: r2 = r8; r2 += 1; r2 += 1; if r7 == 3 goto n_%=; r3 = 0;"
+ "n_%=: if r8 > 61 goto out_%=; r1 = r6; r1 += r2;"
+ "r0 = *(u8 *)(r1 + 0); if r7 < 10 goto l_%=;")
+LINKED(irreducible_loop_secondary_entry, UNBOUND,
+ "r9 = 0; o_%=: r9 += 1; call %[bpf_ktime_get_ns]; r8 = r0; r2 = r7; if r7 > 5 goto m_%=;"
+ "l_%=: r7 += 1; r2 = r8; r2 += 1; r2 += 1; if r7 == 3 goto m_%=; r3 = 0;"
+ "m_%=: if r8 > 61 goto out_%=; r1 = r6; r1 += r2;"
+ "r0 = *(u8 *)(r1 + 0); if r7 < 10 goto l_%=; if r9 < 3 goto o_%=;")
+LINKED(natural_loop_in_irreducible_loop, VALID,
+ "if r7 > 5 goto m_%=; o_%=: r7 += 1; m_%=:"
+ LOOP_HEADER("r3 = r8; r2 = r8; r2 += 1; r2 += 1;", "r2 += 1; r3 += 1;")
+ "if r7 < 10 goto o_%=;")
+LINKED(natural_loop_is_secondary_entry, MAP_OOB,
+ "r3 = r8; r2 = r7; if r7 > 5 goto l_%=; o_%=: r7 += 1;"
+ LOOP_HEADER("r3 = r8; r2 = r8; r2 += 1; r2 += 1;", "r2 += 1; r3 += 1;")
+ "if r7 < 10 goto o_%=;")
+
/*
* No exit of the inner loop dominates its backedges: may_goto is added before its header. The prog
* holds a reference, so may_goto goes to the exit of the outer loop: an edge that the verifier
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 22/29] bpf: Remove sync_linked_regs(), refine by scalar evolution only
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (20 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 21/29] bpf: Compute scalar evolution expressions in irreducible loops Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 23/29] bpf: Recognize copies of a scalar by their ranges in flow_join_makes_up() Alexei Starovoitov
` (6 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A removal: sync_linked_regs(), the code that scalar ids exist for, goes.
The ids themselves go in later patches. Precision is lost where only
a state knows that two scalars are linked: on one of several paths, after
a loop, across frames. One selftest has to change.
After a conditional jump sync_linked_regs() copies the range of the tested
register, plus delta, to all scalars with its id. refine_linked_regs()
does that job without ids. It refines the linked registers of the jump:
the live registers and stack slots that are a tested register + a constant
on every path to the jump. Scalar evolution computes them before the main
pass. Both run at every jump, sync_linked_regs() first. Remove it,
collect_linked_regs() and struct linked_regs. Scalar ids are still
assigned, no jump reads them.
Precision that is lost:
- A path that is dead in the main pass only:
long n = bpf_probe_read_kernel_str(dst, 16, src);
if (n < 0 || n > 16) return 0;
return n;
clang: 'r1 = 0; if r0 s> 16 goto l; r1 = r0; l: if r0 s< 0 goto ..'. At
the second jump r1 is phi(0, r0), which is not linked to r0. The helper
returns at most 16: the only state that got to l had one id in r0, r1.
- 'r1 = r0' on one of two paths to a jump in general. The id worked until
the state of that path was joined with the other one. The first
iteration of a loop is such a path. So is the conditional move that
clang makes for the off that 'send' needs. It tests 'n < 0' twice:
if (n < 0) goto send;
off += n;
if (off > MAX) goto send;
buf[off] = x;
send: emit(off);
- A link that a loop leaves. For the code after it a nested loop defines
every register that it changes, each with a name of its own:
do { len += next(); e.len = len; } while (more());
if (len > 100) return 0;
use(buf[e.len]);
- What only the state knows about an operand. 'w3 = w0' is a copy if r0
fits in 32 bits. With 'size_t off' and 'u32 sz' clang 18 emits it for
'off += (u32)n; sz = off <= MAX ? HDR + off : HDR;'. For scev.c off is
(+ (zext32 n) x) and sz is (zext32 (+ (+ n x) HDR)): no common base.
'r5 = r3; r5 += r2' is r3 + a constant if r2 is one, as in a loop that
the verifier unrolls: for 'for (i = n - 1; i >= 0; i--)' clang tests
n + 1 + k with k = -1, -2, .. in r2. The jump refined n, and the range
of n ended the loop. A write at a variable offset into the stack defines
all slots of its bpf_may_write_mask().
- Linked registers belong to the frame of the jump. Scalar ids also linked
the registers and stack slots of the callers. A slot that a subprog
loads or stores through a pointer and tests is still refined, see
scev_mem_to_slot(), if the pointer is live at the jump. It is dead in
'c->n = x; if (x >= 16) return -1; return 0;' and in
'if (w->c->n > 63) return 0;'. A subprog that is only called through
a function pointer has no memory locations: its slots of the caller are
not refined. A copy of c.n that the caller keeps in a register during
'if (!valid(&c)) return 0;' is not refined. Nor is n after
'r = rd(e, &n); if (r < 0 || r > 63) return 0;' if rd() returns what it
stores to *n.
- That two arguments hold the same value is known at the callsite, not in
the subprog. The selftest "regalloc in callee" passes the same value in
r1 and r2 and is rejected now. Its 'r2 = r0' before the call becomes
'r2 = r1' in the callee.
- refine_linked_regs() runs for loaders with CAP_BPF, sync_linked_regs()
ran for everybody.
No other prog of the selftests, Cilium, sched_ext, katran, bpfjailer and
of the programs in Rust changes its verdict.
Precision that is gained: sync_linked_regs() overwrites ranges,
refine_linked_regs() intersects them. With n in [0, 19] and 'u64 ent[16]'
clang emits for 'n = n > 4 ? n - 4 : 0; e->ent[n] = 1;':
10: w2 = w6
11: w2 += -4
14: if w2 > w6 goto pc+1
If the jump is not taken w2 did not wrap: it is at most 19. r2 and r6 have
the same id, so sync_linked_regs() overwrites r2 with r6 - 4 modulo 2^32,
which can be 0xfffffffc: "R0 unbounded memory access". With
refine_linked_regs() alone r2 is in [0, 15] and the prog loads.
The selftest linked_regs_too_many_regs sees all six registers refined.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/verifier.c | 161 ------------------
.../bpf/progs/verifier_array_access.c | 2 +-
.../selftests/bpf/progs/verifier_regalloc.c | 2 +-
.../selftests/bpf/progs/verifier_scalar_ids.c | 13 +-
4 files changed, 5 insertions(+), 173 deletions(-)
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index cc4cbd9da270..db37e5fc6c03 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -3578,30 +3578,6 @@ static void mark_non_stack_access(struct bpf_verifier_env *env, int idx)
env->insn_aux_data[idx].non_stack_access = true;
}
-#define LINKED_REGS_MAX 5
-
-struct linked_reg {
- u8 frameno;
- union {
- u16 spi;
- u16 regno;
- };
- bool is_reg;
-};
-
-struct linked_regs {
- int cnt;
- struct linked_reg entries[LINKED_REGS_MAX];
-};
-
-static struct linked_reg *linked_regs_push(struct linked_regs *s)
-{
- if (s->cnt < LINKED_REGS_MAX)
- return &s->entries[s->cnt++];
-
- return NULL;
-}
-
const char *bpf_disasm_kfunc_name(void *data, const struct bpf_insn *insn)
{
const struct btf_type *func;
@@ -19596,119 +19572,6 @@ static bool try_match_pkt_pointers(const struct bpf_insn *insn,
return true;
}
-static void __collect_linked_regs(struct linked_regs *reg_set, struct bpf_reg_state *reg,
- u32 id, u32 frameno, u32 spi_or_reg, bool is_reg)
-{
- struct linked_reg *e;
-
- if (reg->type != SCALAR_VALUE || (reg->id & ~BPF_ADD_CONST) != id)
- return;
-
- e = linked_regs_push(reg_set);
- if (e) {
- e->frameno = frameno;
- e->is_reg = is_reg;
- e->regno = spi_or_reg;
- } else {
- clear_scalar_id(reg);
- }
-}
-
-/* For all R being scalar registers or spilled scalar registers
- * in verifier state, save R in linked_regs if R->id == id.
- * If there are too many Rs sharing same id, reset id for leftover Rs.
- */
-static void collect_linked_regs(struct bpf_verifier_env *env,
- struct bpf_verifier_state *vstate,
- u32 id,
- struct linked_regs *linked_regs)
-{
- struct bpf_insn_aux_data *aux = env->insn_aux_data;
- struct bpf_func_state *func;
- struct bpf_reg_state *reg;
- u16 live_regs;
- int i, j;
-
- id = id & ~BPF_ADD_CONST;
- for (i = vstate->curframe; i >= 0; i--) {
- live_regs = aux[bpf_frame_insn_idx(vstate, i)].live_regs_before;
- func = vstate->frame[i];
- for (j = 0; j < BPF_REG_FP; j++) {
- if (!(live_regs & BIT(j)))
- continue;
- reg = &func->regs[j];
- __collect_linked_regs(linked_regs, reg, id, i, j, true);
- }
- for (j = 0; j < bpf_stack_nr_slots(func); j++) {
- struct bpf_stack_state *ss = bpf_stack_slot(func, j);
-
- if (!bpf_is_spilled_reg(ss))
- continue;
- reg = &ss->spilled_ptr;
- __collect_linked_regs(linked_regs, reg, id, i, j, false);
- }
- }
-}
-
-/* For all R in linked_regs, copy known_reg range into R
- * if R->id == known_reg->id.
- */
-static void sync_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier_state *vstate,
- struct bpf_reg_state *known_reg, struct linked_regs *linked_regs)
-{
- struct bpf_reg_state fake_reg;
- struct bpf_reg_state *reg;
- struct linked_reg *e;
- int i;
-
- for (i = 0; i < linked_regs->cnt; ++i) {
- e = &linked_regs->entries[i];
- reg = e->is_reg ? &vstate->frame[e->frameno]->regs[e->regno]
- : &bpf_stack_slot(vstate->frame[e->frameno], e->spi)->spilled_ptr;
- if (reg->type != SCALAR_VALUE || reg == known_reg)
- continue;
- if ((reg->id & ~BPF_ADD_CONST) != (known_reg->id & ~BPF_ADD_CONST))
- continue;
- /*
- * Skip mixed 32/64-bit links: the delta relationship doesn't
- * hold across different ALU widths.
- */
- if (((reg->id ^ known_reg->id) & BPF_ADD_CONST) == BPF_ADD_CONST)
- continue;
- if ((!(reg->id & BPF_ADD_CONST) && !(known_reg->id & BPF_ADD_CONST)) ||
- reg->delta == known_reg->delta) {
- *reg = *known_reg;
- } else {
- s32 saved_off = reg->delta;
- u32 saved_id = reg->id;
-
- fake_reg.type = SCALAR_VALUE;
- __mark_reg_known(&fake_reg, (s64)reg->delta - (s64)known_reg->delta);
-
- /* reg = known_reg; reg += delta */
- *reg = *known_reg;
- /*
- * Must preserve off and id, otherwise another sync_linked_regs()
- * will be incorrect.
- */
- reg->delta = saved_off;
- reg->id = saved_id;
-
- scalar_step_add(reg, reg, &fake_reg);
- scalar32_min_max_add(reg, &fake_reg);
- scalar_min_max_add(reg, &fake_reg);
- reg->var_off = tnum_add(reg->var_off, fake_reg.var_off);
- if ((reg->id | known_reg->id) & BPF_ADD_CONST32)
- zext_32_to_64(reg);
- reg_bounds_sync(reg);
- }
- if (e->is_reg)
- mark_reg_scratched(env, e->regno);
- else
- mark_stack_slot_scratched(env, e->spi);
- }
-}
-
/* a == b (mod 2^N) implies a == b if both fit in uN or both fit in sN. N is @bits. */
static bool regs_fit_in_bits(const struct bpf_reg_state *a, const struct bpf_reg_state *b,
u32 bits)
@@ -19911,7 +19774,6 @@ static int check_cond_jmp_op(struct bpf_verifier_env *env,
struct bpf_reg_state *regs = this_branch->frame[this_branch->curframe]->regs;
struct bpf_reg_state *dst_reg, *other_branch_regs, *src_reg = NULL;
struct bpf_reg_state *eq_branch_regs;
- struct linked_regs linked_regs = {};
u8 opcode = BPF_OP(insn->code);
bool is_jmp32;
int pred = -1;
@@ -19980,16 +19842,6 @@ static int check_cond_jmp_op(struct bpf_verifier_env *env,
__mark_reg_known(src_reg, insn->imm);
}
- /*
- * Collect the linked registers before env->{true,false}_reg{1,2} setup,
- * otherwise ids dropped by collect_linked_regs() would be resurrected
- * when env->{true,false}_reg{1,2} are copied back.
- */
- if (BPF_SRC(insn->code) == BPF_X && src_reg->type == SCALAR_VALUE && src_reg->id)
- collect_linked_regs(env, this_branch, src_reg->id, &linked_regs);
- if (dst_reg->type == SCALAR_VALUE && dst_reg->id)
- collect_linked_regs(env, this_branch, dst_reg->id, &linked_regs);
-
is_jmp32 = BPF_CLASS(insn->code) == BPF_JMP32;
env->false_reg1 = *dst_reg;
env->false_reg2 = *src_reg;
@@ -20043,19 +19895,6 @@ static int check_cond_jmp_op(struct bpf_verifier_env *env,
if (BPF_SRC(insn->code) == BPF_X)
other_branch_regs[insn->src_reg] = env->true_reg2;
- if (BPF_SRC(insn->code) == BPF_X &&
- src_reg->type == SCALAR_VALUE && src_reg->id &&
- !WARN_ON_ONCE(src_reg->id != other_branch_regs[insn->src_reg].id)) {
- sync_linked_regs(env, this_branch, src_reg, &linked_regs);
- sync_linked_regs(env, other_branch, &other_branch_regs[insn->src_reg],
- &linked_regs);
- }
- if (dst_reg->type == SCALAR_VALUE && dst_reg->id &&
- !WARN_ON_ONCE(dst_reg->id != other_branch_regs[insn->dst_reg].id)) {
- sync_linked_regs(env, this_branch, dst_reg, &linked_regs);
- sync_linked_regs(env, other_branch, &other_branch_regs[insn->dst_reg],
- &linked_regs);
- }
if (env->bpf_capable) {
if (BPF_SRC(insn->code) == BPF_X) {
refine_linked_regs(env, this_branch, *insn_idx, insn->src_reg);
diff --git a/tools/testing/selftests/bpf/progs/verifier_array_access.c b/tools/testing/selftests/bpf/progs/verifier_array_access.c
index 7f98822889ca..29bed1e496af 100644
--- a/tools/testing/selftests/bpf/progs/verifier_array_access.c
+++ b/tools/testing/selftests/bpf/progs/verifier_array_access.c
@@ -762,7 +762,7 @@ l0_%=: *(u32*)(r10 - 8) = r6; \
: __clobber_all);
}
-/* The slot has the id of r6, so the compare sets the range of the slot too */
+/* The slot holds the low 32 bits of r6, so the compare refines the slot too */
SEC("socket")
__description("valid map access into an array using a key that is compared after the spill")
__success __retval(0)
diff --git a/tools/testing/selftests/bpf/progs/verifier_regalloc.c b/tools/testing/selftests/bpf/progs/verifier_regalloc.c
index ee5ddea87c91..cf26617b6ba1 100644
--- a/tools/testing/selftests/bpf/progs/verifier_regalloc.c
+++ b/tools/testing/selftests/bpf/progs/verifier_regalloc.c
@@ -303,7 +303,6 @@ __naked void regalloc_in_callee(void)
r7 = r0; \
call %[bpf_get_prandom_u32]; \
r1 = r0; \
- r2 = r0; \
r3 = r7; \
call regalloc_in_callee__1; \
l0_%=: exit; \
@@ -318,6 +317,7 @@ static __naked __noinline __attribute__((used))
void regalloc_in_callee__1(void)
{
asm volatile (" \
+ r2 = r1; \
if r1 s> 20 goto l0_%=; \
if r2 s< 0 goto l0_%=; \
r3 += r1; \
diff --git a/tools/testing/selftests/bpf/progs/verifier_scalar_ids.c b/tools/testing/selftests/bpf/progs/verifier_scalar_ids.c
index 819d0a1a6886..e22120e5858f 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scalar_ids.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scalar_ids.c
@@ -285,13 +285,8 @@ __naked void precision_two_ids(void)
SEC("socket")
__success __log_level(2)
-/*
- * check that r5 is unlinked after 'if', collect_linked_regs() can't tie
- * more than 5 registers for a single insn and the register compared by
- * the jump is not exempt from that. The jump refines the copies all the same.
- */
-__msg("7: (25) if r5 > 0x7 goto pc+0 ; R0=" LINKED("1", 7, 0x7) " R1=" LINKED("1", 7, 0x7) " R2=" LINKED("1", 7, 0x7) " R3=" LINKED("1", 7, 0x7) " R4=" LINKED("1", 7, 0x7) " R5=scalar(smin=")
-__msg("12: (bf) r5 = r5 ; R5=scalar(id=2")
+/* there is no limit of 5 registers for a single insn */
+__msg("7: (25) if r5 > 0x7 goto pc+0 ; R0=" LINKED("1", 7, 0x7) " R1=" LINKED("1", 7, 0x7) " R2=" LINKED("1", 7, 0x7) " R3=" LINKED("1", 7, 0x7) " R4=" LINKED("1", 7, 0x7) " R5=" LINKED("1", 7, 0x7))
__naked void linked_regs_too_many_regs(void)
{
asm volatile (
@@ -304,14 +299,12 @@ __naked void linked_regs_too_many_regs(void)
"r3 = r0;"
"r4 = r0;"
"r5 = r0;"
- /* r{0-4} fill the record, r5 does not fit and is unlinked */
"if r5 > 7 goto +0;"
- /* keep r{1-4} live */
+ /* keep r{1-5} live */
"r1 = r1;"
"r2 = r2;"
"r3 = r3;"
"r4 = r4;"
- /* make r5 appear in the log */
"r5 = r5;"
"r7 = r10;"
"r7 += r0;"
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 23/29] bpf: Recognize copies of a scalar by their ranges in flow_join_makes_up()
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (21 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 22/29] bpf: Remove sync_linked_regs(), refine by scalar evolution only Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 24/29] bpf: Stop assigning ids to scalars Alexei Starovoitov
` (5 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A prerequisite of the removal of scalar ids. flow_join_makes_up() is the
last code that learns something about the prog from them: which registers
and stack slots hold copies of a scalar. Recognize the copies by their
ranges. The function is a heuristic that decides which states are joined.
So this patch is about the number of states. Soundness does not depend on
it.
err = update(); ret = err < 0 ? DROP : 0;
..
if (err < 0) goto out;
..
if (ret < 0) ..
The states (err >= 0, ret = 0) and (err < 0, ret = DROP) get to a join
point before the jumps. Their join loses the correlation of err and ret:
it has err < 0 with ret = 0. So two states stay apart if two scalars that
later jumps test each have disjoint ranges in the two states, as err and
ret do. Copies of a scalar are treated as one scalar there. A jump that
tests one of them refines the others, so their join makes up nothing, and
compilers keep a value in a register and in stack slots at the same time.
Copies are recognized by their id, in one of the two states:
if (one && (!id || id != (ra->id & ~BPF_ADD_CONST)))
return true;
If every copy is a scalar of its own, too many states stay apart. The
init prog of a sched_ext scheduler in Rust goes from 36 to 512 states at
one insn and takes twice as long to load. At 512 flow_has_room() is false,
and states are joined by force.
Copies have the same range and tnum, in each of the two states. Treat
scalars with that property as copies. Two scalars that are not copies can
have it too. Then two states are joined that stayed apart.
veristat over the selftests, Cilium, sched_ext, katran, bpfjailer and
programs in Rust: no verdict changes, 199 of 32913 progs move, by 0.01 % of
the insns in all. The scheduler stays at 36 states.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/flow.c | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/kernel/bpf/flow.c b/kernel/bpf/flow.c
index c9458b3ad114..778288acb5ba 100644
--- a/kernel/bpf/flow.c
+++ b/kernel/bpf/flow.c
@@ -2603,10 +2603,9 @@ static bool flow_join_makes_up(const struct bpf_flow *flow, struct bpf_verifier_
struct bpf_verifier_state *b)
{
struct bpf_func_state *fa = a->frame[a->curframe], *fb = b->frame[b->curframe];
- const struct bpf_reg_state *ra, *rb;
+ const struct bpf_reg_state *ra, *rb, *one_a = NULL, *one_b = NULL;
const struct flow_need *tested = flow->need[FLOW_NEED_JUMP], *need;
- u32 fr, i, id = 0;
- bool one = false;
+ u32 fr, i, len = offsetof(struct bpf_reg_state, id);
if (!tested)
return false;
@@ -2623,11 +2622,11 @@ static bool flow_join_makes_up(const struct bpf_flow *flow, struct bpf_verifier_
continue;
if (flow_gap(ra, rb))
return true;
- /* copies of a number are one number */
- if (one && (!id || id != (ra->id & ~BPF_ADD_CONST)))
+ /* copies of a scalar count once: same range and tnum in each state */
+ if (one_a && (memcmp(ra, one_a, len) || memcmp(rb, one_b, len)))
return true;
- one = true;
- id = ra->id & ~BPF_ADD_CONST;
+ one_a = ra;
+ one_b = rb;
}
}
if (!flow_needed_consts_differ(flow, FLOW_NEED_FLAG, a, b))
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 24/29] bpf: Stop assigning ids to scalars
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (22 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 23/29] bpf: Recognize copies of a scalar by their ranges in flow_join_makes_up() Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 25/29] bpf: Remove scalar ids from regsafe() Alexei Starovoitov
` (4 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A removal. Nothing learns anything about a prog from a scalar id any more.
The code that is left only maintains the ids. Stop assigning them. No
verdict changes, the number of states goes down.
r2 = r1 r1 and r2 get id 7
r2 += 4 id 7 | BPF_ADD_CONST64, delta 4
*(u64 *)(r10 - 8) = r1 fp-8 has id 7
if r1 > 100 goto .. all three were refined
A scalar id in a state said that scalars hold the same value, or differ by
delta. Each insn that moves or changes a scalar had to assign, keep or
clear it. regsafe() and the join of states had to map the ids of one state
to those of the other.
What does that job now:
- After a conditional jump refine_linked_regs() refines the linked
registers of the jump: the live registers and stack slots that are
a tested register + a constant on every path to the jump. Scalar
evolution computes them before the main pass.
- Memory behind a pointer is a linked register too: the stack slot of
a caller that the tested register was loaded from is refined.
- Scalar evolution compares expressions to find a copy of an induction
variable: const_diff_at_header().
- flow_join_makes_up() recognizes copies of a scalar by their ranges.
Remove assign_scalar_id_before_mov(), BPF_ADD_CONST and delta in
adjust_reg_min_max_vals(), the ids that a spill and a fill keep or clear,
the callers of clear_scalar_id(), bpf_clear_singular_ids() and its caller.
get_reg_width() has no user left.
The ids of pointers, references, dynptrs and iterators stay.
States that differed in scalar ids only are one state now. veristat over
the selftests, Cilium, sched_ext, katran, bpfjailer and programs in Rust:
2.3 % fewer insns and 3.7 % fewer states, in Rust 5.3 % and 6.3 %. No
verdict changes. The kernel spends 0.7 % fewer instructions on loading the
C programs and 4.9 % fewer on those in Rust.
Tests that change:
- Log lines that pin 'id=' of a scalar lose it. The id of a packet pointer
is smaller: ids are assigned less often.
- two_old_ids_one_cur_id maps the scalar ids of two states: removed.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/flow.c | 2 -
kernel/bpf/scev.c | 8 +-
kernel/bpf/verifier.c | 247 +-----------------
.../selftests/bpf/progs/verifier_bounds.c | 2 +-
.../bpf/progs/verifier_bounds_step.c | 6 +-
.../bpf/progs/verifier_direct_packet_access.c | 2 +-
.../selftests/bpf/progs/verifier_scalar_ids.c | 55 +---
7 files changed, 19 insertions(+), 303 deletions(-)
diff --git a/kernel/bpf/flow.c b/kernel/bpf/flow.c
index 778288acb5ba..abbadba1e79f 100644
--- a/kernel/bpf/flow.c
+++ b/kernel/bpf/flow.c
@@ -2924,8 +2924,6 @@ static struct flow_state *flow_arrive(struct bpf_verifier_env *env, struct bpf_v
flow_free_state(env, s);
return ERR_PTR(err);
}
- /* when the registers that it was shared with are dead, or are those of the caller */
- bpf_clear_singular_ids(env, &s->st);
return s;
}
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index d95b8d26dc51..b46eb37234a3 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -4237,7 +4237,7 @@ static bool is_unbound_scev_reg(struct bpf_verifier_env *env, const struct bpf_f
{
const struct bpf_reg_state *reg = scev_reg_for_read(env, st, r);
- return reg && reg->type == SCALAR_VALUE && !reg->id && reg->step <= 1 &&
+ return reg && reg->type == SCALAR_VALUE && reg->step <= 1 &&
tnum_is_unknown(reg->var_off) &&
reg->r64.size == U64_MAX && reg->r32.size == U32_MAX;
}
@@ -4323,9 +4323,6 @@ bool bpf_is_widenable_reg_type(const struct bpf_reg_state *reg)
static void scratch_widened_reg_id(struct bpf_verifier_env *env, struct bpf_reg_state *reg)
{
switch (base_type(reg->type)) {
- case SCALAR_VALUE:
- clear_scalar_id(reg);
- break;
case PTR_TO_PACKET:
case PTR_TO_PACKET_META:
reg->id = ++env->id_gen;
@@ -5056,7 +5053,7 @@ int bpf_widen_scev_regs(struct bpf_verifier_env *env, struct bpf_verifier_state
if (back && (!val || !entry_val || !bpf_reg_union_compatible(val, entry_val)))
continue;
if (test_bit(r, iters->mask_regs)) {
- /* The range stays. It's no longer equal to the registers with its id. */
+ /* the range stays */
err = scev_reg_for_write(env, cur_func, r, ®);
if (!err && back)
err = bpf_reg_union(env, reg, entry_val);
@@ -5068,7 +5065,6 @@ int bpf_widen_scev_regs(struct bpf_verifier_env *env, struct bpf_verifier_state
bpf_log(log, " stays in 0..%llu, it loses bits only\n",
reg_umax(reg));
}
- scratch_widened_reg_id(env, reg);
mark_scev_reg_scratched(env, r);
continue;
}
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index db37e5fc6c03..d549644235c5 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -3634,27 +3634,6 @@ static bool __is_pointer_value(bool allow_ptr_leaks,
return is_pointer_regtype(reg->type);
}
-static void assign_scalar_id_before_mov(struct bpf_verifier_env *env,
- struct bpf_reg_state *src_reg)
-{
- if (src_reg->type != SCALAR_VALUE)
- return;
- /*
- * The verifier is processing rX = rY insn and
- * rY->id has special linked register already.
- * Cleared it, since multiple rX += const are not supported.
- */
- if (src_reg->id & BPF_ADD_CONST)
- clear_scalar_id(src_reg);
- /*
- * Ensure that src_reg has a valid ID that will be copied to
- * dst_reg and then will be used by sync_linked_regs() to
- * propagate min/max range.
- */
- if (!src_reg->id && !tnum_is_const(src_reg->var_off))
- src_reg->id = ++env->id_gen;
-}
-
static void save_register_state(struct bpf_verifier_env *env,
struct bpf_func_state *state,
int spi, struct bpf_reg_state *reg,
@@ -3681,11 +3660,6 @@ static bool is_bpf_st_mem(struct bpf_insn *insn)
return BPF_CLASS(insn->code) == BPF_ST && BPF_MODE(insn->code) == BPF_MEM;
}
-static int get_reg_width(struct bpf_reg_state *reg)
-{
- return fls64(reg_umax(reg));
-}
-
/* See comment for mark_fastcall_pattern_for_call() */
static void check_fastcall_stack_contract(struct bpf_verifier_env *env,
struct bpf_func_state *state, int insn_idx, int off)
@@ -3786,16 +3760,7 @@ static int check_stack_write_fixed_off(struct bpf_verifier_env *env,
mark_stack_slot_scratched(env, spi);
if (reg && !(off % BPF_REG_SIZE) && reg->type == SCALAR_VALUE && env->bpf_capable) {
- bool reg_value_fits;
-
- reg_value_fits = get_reg_width(reg) <= BITS_PER_BYTE * size;
- /* Make sure that reg had an ID to build a relation on spill. */
- if (reg_value_fits)
- assign_scalar_id_before_mov(env, reg);
save_register_state(env, state, spi, reg, size);
- /* Break the relation on a narrowing spill. */
- if (!reg_value_fits)
- clear_scalar_id(&ss->spilled_ptr);
} else if (!reg && !(off % BPF_REG_SIZE) && is_bpf_st_mem(insn) &&
env->bpf_capable) {
struct bpf_reg_state *tmp_reg = &env->fake_reg[0];
@@ -4098,19 +4063,8 @@ static int check_stack_read_fixed_off(struct bpf_verifier_env *env,
if (dst_regno < 0)
return 0;
- if (env->bpf_capable && size == 4 && spill_size == 4 &&
- get_reg_width(reg) <= 32)
- /* Ensure stack slot has an ID to build a relation
- * with the destination register on fill.
- */
- assign_scalar_id_before_mov(env, reg);
+ /* coerce_reg_to_size will adjust the boundaries. */
state->regs[dst_regno] = *reg;
-
- /* Break the relation on a narrowing fill.
- * coerce_reg_to_size will adjust the boundaries.
- */
- if (get_reg_width(reg) > size * BITS_PER_BYTE)
- clear_scalar_id(&state->regs[dst_regno]);
} else {
int spill_cnt = 0, zero_cnt = 0;
@@ -4155,11 +4109,6 @@ static int check_stack_read_fixed_off(struct bpf_verifier_env *env,
}
} else if (dst_regno >= 0) {
/* restore register state from stack */
- if (env->bpf_capable)
- /* Ensure stack slot has an ID to build a relation
- * with the destination register on fill.
- */
- assign_scalar_id_before_mov(env, reg);
state->regs[dst_regno] = *reg;
} else if (__is_pointer_value(env->allow_ptr_leaks, reg)) {
/* If dst_regno==-1, the caller is asking us whether
@@ -7689,18 +7638,7 @@ static int check_mem_access(struct bpf_verifier_env *env, int insn_idx, struct b
/* b/h/w load zero-extends, mark upper bits as known 0 */
coerce_reg_to_size(®s[value_regno], size);
} else {
- /*
- * Sign-extension can change the register value relative
- * to a scalar it is linked with by id (e.g. a zero-
- * extending fill of the same spilled stack slot), thus
- * drop the shared id in that case.
- */
- bool no_sext = reg_umax(®s[value_regno]) <
- (1ULL << (size * BITS_PER_BYTE - 1));
-
coerce_reg_to_size_sx(®s[value_regno], size);
- if (!no_sext)
- clear_scalar_id(®s[value_regno]);
}
}
@@ -11736,16 +11674,11 @@ static int call_func_on_its_own(struct bpf_verifier_env *env, int subprog, int i
callee->entry_rcu_locks = entry.active_rcu_locks;
callee->entry_preempt_locks = entry.active_preempt_locks;
for (i = BPF_REG_1; i <= BPF_REG_5; i++) {
- /* what a scalar is linked to stays with the caller, so does the delta to it */
- if (callee->regs[i].type == SCALAR_VALUE)
- clear_scalar_id(&callee->regs[i]);
callee->regs[i].id = inherited_id(state, callee->regs[i].id);
callee->regs[i].parent_id = inherited_id(state, callee->regs[i].parent_id);
callee->regs[i].map_uid = inherited_id(state, callee->regs[i].map_uid);
}
for (i = 0; i < callee->out_stack_arg_cnt; i++) {
- if (callee->stack_arg_regs[i].type == SCALAR_VALUE)
- clear_scalar_id(&callee->stack_arg_regs[i]);
callee->stack_arg_regs[i].id = inherited_id(state, callee->stack_arg_regs[i].id);
callee->stack_arg_regs[i].parent_id =
inherited_id(state, callee->stack_arg_regs[i].parent_id);
@@ -11791,19 +11724,18 @@ static int exit_func_on_its_own(struct bpf_verifier_env *env)
static u32 fresh_id(struct bpf_verifier_env *env, struct bpf_idmap *idmap, u32 id)
{
- u32 flags = id & BPF_ADD_CONST, i;
+ u32 i;
- id &= ~BPF_ADD_CONST;
if (!id)
return 0;
for (i = 0; i < idmap->cnt; i++)
if (idmap->map[i].old == id)
- return idmap->map[i].cur | flags;
+ return idmap->map[i].cur;
if (idmap->cnt == BPF_ID_MAP_SIZE)
return 0;
idmap->map[idmap->cnt].old = id;
idmap->map[idmap->cnt].cur = ++env->id_gen;
- return idmap->map[idmap->cnt++].cur | flags;
+ return idmap->map[idmap->cnt++].cur;
}
/*
@@ -17876,13 +17808,6 @@ static void scalar_byte_swap(struct bpf_reg_state *dst_reg, struct bpf_insn *ins
/* Apply bswap if alu64 or switch between big-endian and little-endian machines */
bool need_bswap = alu64 || (to_le == is_big_endian);
- /*
- * If the register is mutated, manually reset its scalar ID to break
- * any existing ties and avoid incorrect bounds propagation.
- */
- if (need_bswap || insn->imm == 16 || insn->imm == 32)
- clear_scalar_id(dst_reg);
-
if (need_bswap) {
if (insn->imm == 16)
dst_reg->var_off = tnum_bswap16(dst_reg->var_off);
@@ -17983,7 +17908,6 @@ static int maybe_fork_scalars(struct bpf_verifier_env *env, struct bpf_insn *ins
* like any other. At this insn the branch is a part of the state that is looked at:
* it must not be compared with it.
*/
- clear_scalar_id(reg);
return adjust_scalar_min_max_vals(env, insn, reg, *src_reg);
}
@@ -18193,10 +18117,8 @@ static int adjust_reg_min_max_vals(struct bpf_verifier_env *env,
struct bpf_func_state *state = vstate->frame[vstate->curframe];
struct bpf_reg_state *regs = state->regs, *dst_reg, *src_reg;
struct bpf_reg_state *ptr_reg = NULL, off_reg = {0};
- bool alu32 = (BPF_CLASS(insn->code) != BPF_ALU64);
struct bpf_insn_aux_data *aux = cur_aux(env);
u8 opcode = BPF_OP(insn->code);
- int err;
dst_reg = ®s[insn->dst_reg];
if (BPF_SRC(insn->code) == BPF_X)
@@ -18303,69 +18225,7 @@ static int adjust_reg_min_max_vals(struct bpf_verifier_env *env,
verbose(env, "verifier internal error: no src_reg\n");
return -EFAULT;
}
- /*
- * For alu32 linked register tracking, we need to check dst_reg's
- * umax_value before the ALU operation. After adjust_scalar_min_max_vals(),
- * alu32 ops will have zero-extended the result, making umax_value <= U32_MAX.
- */
- u64 dst_umax = reg_umax(dst_reg);
-
- err = adjust_scalar_min_max_vals(env, insn, dst_reg, *src_reg);
- if (err)
- return err;
- /*
- * Compilers can generate the code
- * r1 = r2
- * r1 += 0x1
- * if r2 < 1000 goto ...
- * use r1 in memory access
- * So remember constant delta between r2 and r1 and update r1 after
- * 'if' condition.
- */
- if (env->bpf_capable &&
- (BPF_OP(insn->code) == BPF_ADD || BPF_OP(insn->code) == BPF_SUB) &&
- dst_reg->id && is_reg_const(src_reg, alu32) &&
- !(BPF_SRC(insn->code) == BPF_X && insn->src_reg == insn->dst_reg)) {
- u64 val = reg_const_value(src_reg, alu32);
- s32 off;
-
- if (!alu32 && ((s64)val < S32_MIN || (s64)val > S32_MAX))
- goto clear_id;
-
- if (alu32 && (dst_umax > U32_MAX))
- goto clear_id;
-
- off = (s32)val;
-
- if (BPF_OP(insn->code) == BPF_SUB) {
- /* Negating S32_MIN would overflow */
- if (off == S32_MIN)
- goto clear_id;
- off = -off;
- }
-
- if (dst_reg->id & BPF_ADD_CONST) {
- /*
- * If the register already went through rX += val
- * we cannot accumulate another val into rx->off.
- */
-clear_id:
- clear_scalar_id(dst_reg);
- } else {
- if (alu32)
- dst_reg->id |= BPF_ADD_CONST32;
- else
- dst_reg->id |= BPF_ADD_CONST64;
- dst_reg->delta = off;
- }
- } else {
- /*
- * Make sure ID is cleared otherwise dst_reg min/max could be
- * incorrectly propagated into other registers by sync_linked_regs()
- */
- clear_scalar_id(dst_reg);
- }
- return 0;
+ return adjust_scalar_min_max_vals(env, insn, dst_reg, *src_reg);
}
/*
@@ -18579,7 +18439,6 @@ void bpf_reg_union_range(struct bpf_reg_state *acc, const struct bpf_reg_state *
/*
* Extends acc's scalar range to include src's scalar range.
* Caller must ensure that other kinds of metadata is compatible.
- * Caller must clear acc's scalar ID.
*/
int bpf_reg_union(struct bpf_verifier_env *env, struct bpf_reg_state *acc,
const struct bpf_reg_state *src)
@@ -18720,7 +18579,6 @@ static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
/* case: R1 = R2
* copy register state to dest reg
*/
- assign_scalar_id_before_mov(env, src_reg);
*dst_reg = *src_reg;
} else {
/* case: R1 = (s8, s16 s32)R2 */
@@ -18730,14 +18588,7 @@ 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) {
- bool no_sext;
-
- no_sext = reg_umax(src_reg) < (1ULL << (insn->off - 1));
- if (no_sext)
- assign_scalar_id_before_mov(env, src_reg);
*dst_reg = *src_reg;
- if (!no_sext)
- clear_scalar_id(dst_reg);
coerce_reg_to_size_sx(dst_reg, insn->off >> 3);
} else {
mark_reg_unknown(env, regs, insn->dst_reg);
@@ -18751,29 +18602,10 @@ 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) {
- bool is_src_reg_u32 = get_reg_width(src_reg) <= 32;
-
- if (is_src_reg_u32)
- assign_scalar_id_before_mov(env, src_reg);
- *dst_reg = *src_reg;
- /* Make sure ID is cleared if src_reg is not in u32
- * range otherwise dst_reg min/max could be incorrectly
- * propagated into src_reg by sync_linked_regs()
- */
- if (!is_src_reg_u32)
- clear_scalar_id(dst_reg);
- } else {
- /* case: W1 = (s8, s16)W2 */
- bool no_sext = reg_umax(src_reg) < (1ULL << (insn->off - 1));
-
- if (no_sext)
- assign_scalar_id_before_mov(env, src_reg);
- *dst_reg = *src_reg;
- if (!no_sext)
- clear_scalar_id(dst_reg);
+ *dst_reg = *src_reg;
+ /* case: W1 = (s8, s16)W2 */
+ if (insn->off)
coerce_subreg_to_size_sx(dst_reg, insn->off >> 3);
- }
} else {
mark_reg_unknown(env, regs,
insn->dst_reg);
@@ -20755,69 +20587,6 @@ static void adjust_btf_func(struct bpf_verifier_env *env)
aux->func_info[i].insn_off = env->subprog_info[i].start;
}
-/* Find id in idset and increment its count, or add new entry */
-static void idset_cnt_inc(struct bpf_idset *idset, u32 id)
-{
- u32 i;
-
- for (i = 0; i < idset->num_ids; i++) {
- if (idset->entries[i].id == id) {
- idset->entries[i].cnt++;
- return;
- }
- }
- /* New id */
- if (idset->num_ids < BPF_ID_MAP_SIZE) {
- idset->entries[idset->num_ids].id = id;
- idset->entries[idset->num_ids].cnt = 1;
- idset->num_ids++;
- }
-}
-
-/* Find id in idset and return its count, or 0 if not found */
-static u32 idset_cnt_get(struct bpf_idset *idset, u32 id)
-{
- u32 i;
-
- for (i = 0; i < idset->num_ids; i++) {
- if (idset->entries[i].id == id)
- return idset->entries[i].cnt;
- }
- return 0;
-}
-
-/*
- * Clear singular scalar ids in a state.
- * A register with a non-zero id is called singular if no other register shares
- * the same base id. Such registers can be treated as independent (id=0).
- */
-void bpf_clear_singular_ids(struct bpf_verifier_env *env,
- struct bpf_verifier_state *st)
-{
- struct bpf_idset *idset = &env->idset_scratch;
- struct bpf_func_state *func;
- struct bpf_reg_state *reg;
-
- idset->num_ids = 0;
-
- bpf_for_each_reg_in_vstate(st, func, reg, ({
- if (reg->type != SCALAR_VALUE)
- continue;
- if (!reg->id)
- continue;
- idset_cnt_inc(idset, reg->id & ~BPF_ADD_CONST);
- }));
-
- bpf_for_each_reg_in_vstate(st, func, reg, ({
- if (reg->type != SCALAR_VALUE)
- continue;
- if (!reg->id)
- continue;
- if (idset_cnt_get(idset, reg->id & ~BPF_ADD_CONST) == 1)
- clear_scalar_id(reg);
- }));
-}
-
/* Return true if it's OK to have the same insn return a different type. */
static bool reg_type_mismatch_ok(enum bpf_reg_type type)
{
diff --git a/tools/testing/selftests/bpf/progs/verifier_bounds.c b/tools/testing/selftests/bpf/progs/verifier_bounds.c
index 94d52154b0c6..b78e7fc96fa7 100644
--- a/tools/testing/selftests/bpf/progs/verifier_bounds.c
+++ b/tools/testing/selftests/bpf/progs/verifier_bounds.c
@@ -1476,7 +1476,7 @@ __naked void sub64_full_overflow(void)
SEC("socket")
__description("64-bit subtraction, partial overflow, result in unbounded reg")
__success __log_level(2)
-__msg("3: (1f) r3 -= r2 {{.*}} R3=scalar(id=1-1)")
+__msg("3: (1f) r3 -= r2 {{.*}} R3=scalar()")
__retval(0)
__naked void sub64_partial_overflow(void)
{
diff --git a/tools/testing/selftests/bpf/progs/verifier_bounds_step.c b/tools/testing/selftests/bpf/progs/verifier_bounds_step.c
index 51b703058cf7..b42717a825c6 100644
--- a/tools/testing/selftests/bpf/progs/verifier_bounds_step.c
+++ b/tools/testing/selftests/bpf/progs/verifier_bounds_step.c
@@ -299,7 +299,7 @@ SEC("socket")
__success __log_level(2)
__msg("r0 += 8 {{.*}}smin=umin=smin32=umin32=8,smax=umax=smax32=umax32=176,{{.*}}step=8+24)")
__msg("from 4 to 6: R0=scalar(smin=umin=smin32=umin32=32,smax=umax=smax32=umax32=176,{{.*}}step=8+24)")
-__msg("r1 = r0 {{.*}}R1=scalar(id={{[0-9]+}},smin=umin=smin32=umin32=104,smax=umax=smax32=umax32=176,{{.*}}step=8+24)")
+__msg("r1 = r0 {{.*}}R1=scalar(smin=umin=smin32=umin32=104,smax=umax=smax32=umax32=176,{{.*}}step=8+24)")
__msg("r1 += 24 {{.*}}R1=scalar({{.*}}smin=umin=smin32=umin32=128,smax=umax=smax32=umax32=200,{{.*}}step=8+24)")
__naked void step_bounds_resync_after_arith(void)
{
@@ -336,10 +336,10 @@ l_out_%=: \
*/
SEC("socket")
__success __log_level(2)
-__msg("r1 = (s16)r0 {{.*}}R1=scalar(id={{[0-9]+}},smin=smin32=0,smax=umax=smax32=umax32=168,{{.*}}step=0+24)")
+__msg("r1 = (s16)r0 {{.*}}R1=scalar(smin=smin32=0,smax=umax=smax32=umax32=168,{{.*}}step=0+24)")
__msg("r2 = (s16)r0 {{.*}}R2=scalar(smin=smin32=-24,smax=smax32=144,{{.*}}step=0+24)")
__msg("r3 = (s8)r0 {{.*}}R3=scalar(smin=smin32=-128,smax=smax32=127)")
-__msg("w4 = (s16)w1 {{.*}}R4=scalar(id={{[0-9]+}},smin=smin32=0,smax=umax=smax32=umax32=168,{{.*}}step=0+24)")
+__msg("w4 = (s16)w1 {{.*}}R4=scalar(smin=smin32=0,smax=umax=smax32=umax32=168,{{.*}}step=0+24)")
__msg("w5 = (s8)w1 {{.*}}R5=scalar({{.*}}smin32=-128,smax32=127,var_off=(0x0; 0xffffffff))")
__naked void step_movsx_bounds_on_line(void)
{
diff --git a/tools/testing/selftests/bpf/progs/verifier_direct_packet_access.c b/tools/testing/selftests/bpf/progs/verifier_direct_packet_access.c
index 6c1d2014b95b..212c08a27058 100644
--- a/tools/testing/selftests/bpf/progs/verifier_direct_packet_access.c
+++ b/tools/testing/selftests/bpf/progs/verifier_direct_packet_access.c
@@ -569,7 +569,7 @@ l0_%=: r0 = 0; \
SEC("tc")
__description("direct packet access: test23 (x += pkt_ptr, 4)")
-__failure __msg("invalid access to packet, off=31 size=8, R5(id=3,off=31,r=0)")
+__failure __msg("invalid access to packet, off=31 size=8, R5(id=1,off=31,r=0)")
__flag(BPF_F_ANY_ALIGNMENT)
__naked void test23_x_pkt_ptr_4(void)
{
diff --git a/tools/testing/selftests/bpf/progs/verifier_scalar_ids.c b/tools/testing/selftests/bpf/progs/verifier_scalar_ids.c
index e22120e5858f..56c2412608a5 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scalar_ids.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scalar_ids.c
@@ -11,11 +11,10 @@ struct {
__type(value, long long);
} map_hash_8b SEC(".maps");
-/* scalar in [0, max] that shares id with other registers, as it is in the log */
-#define LINKED(id, max, mask) \
- "scalar(id=" id ",smin=smin32=0,smax=umax=smax32=umax32=" #max ",var_off=(0x0; " #mask "))"
+/* scalar in [0, max], as it is in the log. @id: the registers that hold copies of one value. */
#define ALONE(max, mask) \
"scalar(smin=smin32=0,smax=umax=smax32=umax32=" #max ",var_off=(0x0; " #mask "))"
+#define LINKED(id, max, mask) ALONE(max, mask)
/*
* Registers r{0,1,2} have the same scalar ID.
@@ -151,7 +150,7 @@ __naked void linked_regs_broken_link(void)
SEC("socket")
__success __log_level(2)
__msg("13: (25) if r1 > 0x7 goto pc+0 ; R1=" ALONE(7, 0x7))
-__msg("10: R0=0 R6=scalar(id={{.*}}umax32=255,{{.*}} R7=scalar(id={{.*}}umax32=255,")
+__msg("10: R0=0 R6=scalar({{.*}}umax32=255,{{.*}} R7=scalar({{.*}}umax32=255,")
__msg("5: R0=0 R6=" ALONE(255, 0xff))
__naked void precision_many_frames(void)
{
@@ -317,7 +316,7 @@ __naked void linked_regs_too_many_regs(void)
SEC("socket")
__failure __log_level(2)
-__msg("5: (3d) if r8 >= r0 goto pc+3 ; R0=scalar(umin=1) R7=scalar(id=1,umax=0xfffffffffffffffe) R8=scalar(id=1,umax=0xfffffffffffffffe)")
+__msg("5: (3d) if r8 >= r0 goto pc+3 ; R0=scalar(umin=1) R7=scalar(umax=0xfffffffffffffffe) R8=scalar(umax=0xfffffffffffffffe)")
__msg("div by zero")
__naked void linked_regs_broken_link_2(void)
{
@@ -680,52 +679,6 @@ __naked void two_nil_old_ids_one_cur_id(void)
: __clobber_all);
}
-/*
- * Check that two different scalar IDs in the state that is there can't be
- * mapped to the same scalar ID in the state that arrives.
- */
-SEC("socket")
-__success __log_level(2)
-__not_msg("from 10 to 11: within")
-__msg("11: R0=0 R6=" ALONE(255, 0xff) " R7=" ALONE(255, 0xff))
-__naked void two_old_ids_one_cur_id(void)
-{
- asm volatile (
- /* Give unique scalar IDs to r{6,7} */
- "call %[bpf_ktime_get_ns];"
- "r0 &= 0xff;"
- "r6 = r0;"
- "r8 = r0;"
- "call %[bpf_ktime_get_ns];"
- "r0 &= 0xff;"
- "r7 = r0;"
- "r9 = r0;"
- "r0 = 0;"
- /* Maybe make r{6,7} IDs identical */
- "if r6 <= r7 goto l1_%=;"
- "r6 = r7;"
-"l1_%=:"
- /*
- * Get here in two states:
- * - first: r6{.id=A}, r7{.id=B}
- * - second: r6{.id=B}, r7{.id=B}
- * Currently we don't want to consider the second one to be within
- * the first one. Thus they are joined.
- */
- "r2 = r10;"
- "r2 += r6;"
- "r2 += r7;"
- /*
- * keep r8 and r9 live, otherwise r6->id and r7->id
- * will become singular and reset to zero before if r6 <= r7
- */
- "r9 += r8;"
- "exit;"
- :
- : __imm(bpf_ktime_get_ns)
- : __clobber_all);
-}
-
SEC("socket")
/* Note the flag, see verifier.c:opt_subreg_zext_lo32_rnd_hi32() */
__flag(BPF_F_TEST_RND_HI32)
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 25/29] bpf: Remove scalar ids from regsafe()
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (23 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 24/29] bpf: Stop assigning ids to scalars Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 26/29] bpf: Remove scalar ids from the join of states Alexei Starovoitov
` (3 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
Removal of dead code. Scalars have no ids any more, so every check of
a scalar id in regsafe() passes. No prog is verified differently.
4: r6 = r7
5: if r6 > 10 goto ..
6: use r7
While a jump refined all scalars with the id of the tested register,
a state in which r6 and r7 have the same id was stronger than a state with
the same ranges and two ids: in the first one insn 5 bounds r7. So
regsafe() did not stop at the ranges. check_scalar_ids() mapped the ids of
the old state to those of the current one, an id that the current state
did not have got a temporary one, BPF_ADD_CONST and delta had to be equal.
Now no state says that two scalars hold the same value. What insn 5
refines are its linked registers. Scalar evolution computes them before the
main pass, and a link holds on every path to insn 5, whatever state gets
there. The stack slot of a caller that a jump refines is found through
a pointer in the state, and regsafe() compares pointers as before. For two
scalars range_within() and tnum_in() are enough. Remove check_scalar_ids()
and the table of cases in regsafe().
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/states.c | 108 ++------------------------------------------
1 file changed, 3 insertions(+), 105 deletions(-)
diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c
index b60676349655..48359e789faa 100644
--- a/kernel/bpf/states.c
+++ b/kernel/bpf/states.c
@@ -58,7 +58,7 @@ static bool check_ids(u32 old_id, u32 cur_id, struct bpf_idmap *idmap)
return true;
/* a name of what the caller has, see BPF_INHERITED_ID */
- if (((old_id | cur_id) & ~BPF_ADD_CONST) >= BPF_INHERITED_ID)
+ if ((old_id | cur_id) >= BPF_INHERITED_ID)
return old_id == cur_id;
for (i = 0; i < idmap->cnt; i++) {
@@ -85,49 +85,6 @@ static bool check_ids(u32 old_id, u32 cur_id, struct bpf_idmap *idmap)
return false;
}
-/*
- * Compare scalar register IDs for state equivalence.
- *
- * When old_id == 0, the old register is independent - not linked to any
- * other register. Any linking in the current state only adds constraints,
- * making it more restrictive. Since the old state didn't rely on any ID
- * relationships for this register, it's always safe to accept cur regardless
- * of its ID. Hence, return true immediately.
- *
- * When old_id != 0 but cur_id == 0, we need to ensure that different
- * independent registers in cur don't incorrectly satisfy the ID matching
- * requirements of linked registers in old.
- *
- * Example: if old has r6.id=X and r7.id=X (linked), but cur has r6.id=0
- * and r7.id=0 (both independent), without temp IDs both would map old_id=X
- * to cur_id=0 and pass. With temp IDs: r6 maps X->temp1, r7 tries to map
- * X->temp2, but X is already mapped to temp1, so the check fails correctly.
- *
- * When old_id has BPF_ADD_CONST set, the compound id (base | flag) and the
- * base id (flag stripped) must both map consistently. Example: old has
- * r2.id=A, r3.id=A|flag (r3 = r2 + delta), cur has r2.id=B, r3.id=C|flag
- * (r3 derived from unrelated r4). Without the base check, idmap gets two
- * independent entries A->B and A|flag->C|flag, missing that A->C conflicts
- * with A->B. The base ID cross-check catches this.
- */
-static bool check_scalar_ids(u32 old_id, u32 cur_id, struct bpf_idmap *idmap)
-{
- if (!old_id)
- return true;
-
- cur_id = cur_id ? cur_id : ++idmap->tmp_id_gen;
-
- if (!check_ids(old_id, cur_id, idmap))
- return false;
- if (old_id & BPF_ADD_CONST) {
- old_id &= ~BPF_ADD_CONST;
- cur_id &= ~BPF_ADD_CONST;
- if (!check_ids(old_id, cur_id, idmap))
- return false;
- }
- return true;
-}
-
static void __clean_func_state(struct bpf_verifier_env *env,
struct bpf_func_state *st,
u16 live_regs, int frame)
@@ -272,65 +229,8 @@ static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
/* explore_alu_limits disables tnum_in() and range_within()
* logic and requires everything to be strict
*/
- return memcmp(rold, rcur, offsetof(struct bpf_reg_state, id)) == 0 &&
- check_scalar_ids(rold->id, rcur->id, idmap);
+ return memcmp(rold, rcur, offsetof(struct bpf_reg_state, id)) == 0;
}
- /*
- * Linked register tracking uses rold->id to detect relationships.
- * When rold->id == 0, the register is independent and any linking
- * in rcur only adds constraints. When rold->id != 0, we must verify
- * id mapping and (for BPF_ADD_CONST) offset consistency.
- *
- * +------------------+-----------+------------------+---------------+
- * | | rold->id | rold + ADD_CONST | rold->id == 0 |
- * |------------------+-----------+------------------+---------------|
- * | rcur->id | range,ids | false | range |
- * | rcur + ADD_CONST | false | range,ids,off | range |
- * | rcur->id == 0 | range,ids | false | range |
- * +------------------+-----------+------------------+---------------+
- *
- * Why check_ids() for scalar registers?
- *
- * Consider the following BPF code:
- * 1: r6 = ... unbound scalar, ID=a ...
- * 2: r7 = ... unbound scalar, ID=b ...
- * 3: if (r6 > r7) goto +1
- * 4: r6 = r7
- * 5: if (r6 > X) goto ...
- * 6: ... memory operation using r7 ...
- *
- * First verification path is [1-6]:
- * - at (4) same bpf_reg_state::id (b) would be assigned to r6 and r7;
- * - at (5) r6 would be marked <= X, sync_linked_regs() would also mark
- * r7 <= X, because r6 and r7 share same id.
- * Next verification path is [1-4, 6].
- *
- * Instruction (6) would be reached in two states:
- * I. r6{.id=b}, r7{.id=b} via path 1-6;
- * II. r6{.id=a}, r7{.id=b} via path 1-4, 6.
- *
- * Use check_ids() to distinguish these states.
- * ---
- * Also verify that new value satisfies old value range knowledge.
- */
-
- /*
- * ADD_CONST flags must match exactly: BPF_ADD_CONST32 and
- * BPF_ADD_CONST64 have different linking semantics in
- * sync_linked_regs() (alu32 zero-extends, alu64 does not),
- * so one doesn't stand for the other.
- */
- if (rold->id &&
- (rold->id & BPF_ADD_CONST) != (rcur->id & BPF_ADD_CONST))
- return false;
-
- /* Both have offset linkage: offsets must match */
- if ((rold->id & BPF_ADD_CONST) && rold->delta != rcur->delta)
- return false;
-
- if (!check_scalar_ids(rold->id, rcur->id, idmap))
- return false;
-
return range_within(rold, rcur) && tnum_in(rold->var_off, rcur->var_off);
case PTR_TO_MAP_KEY:
case PTR_TO_MAP_VALUE:
@@ -468,8 +368,7 @@ static bool stacksafe(struct bpf_verifier_env *env, struct bpf_func_state *old,
/*
* 64 and 32-bit scalar spills vs MISC/INVALID slots and vice versa.
* Load from MISC/INVALID slots produces unbound scalar.
- * Construct a fake register for such stack and call
- * regsafe() to ensure scalar ids are compared.
+ * Construct a fake register for such stack and call regsafe().
*/
if (im == 0 || im == 4) {
old_reg = scalar_reg_for_stack(env, old_slot, im);
@@ -736,7 +635,6 @@ static void reset_idmap_scratch(struct bpf_verifier_env *env)
{
struct bpf_idmap *idmap = &env->idmap_scratch;
- idmap->tmp_id_gen = env->id_gen;
idmap->cnt = 0;
}
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 26/29] bpf: Remove scalar ids from the join of states
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (24 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 25/29] bpf: Remove scalar ids from regsafe() Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 27/29] bpf: Remove scalar ids from the log and from bpf_verifier.h Alexei Starovoitov
` (2 subsequent siblings)
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
Removal of dead code, as in regsafe(). Scalars have no ids any more. No
prog is verified differently.
Two states are joined in two passes. The first one records which id of one
state goes with which id of the other: jid_note(). The second one computes
the registers. A scalar kept its id only if the pair was good: the
registers that share the id in one state share it in the other one, with
the same BPF_ADD_CONST and delta, see scalar_link_consistent(). A link
that one state did not have was dropped.
Now the join of two scalars is the join of their ranges. Remove
scalar_link_consistent() and the scalars from jid_note() and jid_good().
All other callers of jid_note() pass true for 'consistent': the parameter
goes.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/join.c | 54 ++++++++++++++---------------------------------
1 file changed, 16 insertions(+), 38 deletions(-)
diff --git a/kernel/bpf/join.c b/kernel/bpf/join.c
index cc4494aef187..2b8c94b7dc50 100644
--- a/kernel/bpf/join.c
+++ b/kernel/bpf/join.c
@@ -75,18 +75,16 @@ static void jid_mark_bad(struct bpf_join_state *js, u32 a, u32 b)
js->pairs[i].bad = true;
}
-static void jid_note(struct bpf_join_state *js, u32 ida, u32 idb, bool consistent)
+static void jid_note(struct bpf_join_state *js, u32 a, u32 b)
{
- u32 a = ida & ~BPF_ADD_CONST, b = idb & ~BPF_ADD_CONST;
+ bool consistent, other = false;
struct join_idpair *p;
- bool other = false;
u32 i;
if (!a || !b)
return;
/* a name of what the caller has, see BPF_INHERITED_ID */
- if (a != b && (a | b) >= BPF_INHERITED_ID)
- consistent = false;
+ consistent = a == b || (a | b) < BPF_INHERITED_ID;
for (i = 0; i < js->npairs; i++) {
p = &js->pairs[i];
if (p->a == a && p->b == b) {
@@ -111,15 +109,12 @@ static void jid_note(struct bpf_join_state *js, u32 ida, u32 idb, bool consisten
p->bad = !consistent;
}
-static bool jid_good(struct bpf_join_state *js, u32 ida, u32 idb)
+static bool jid_good(struct bpf_join_state *js, u32 a, u32 b)
{
- u32 a = ida & ~BPF_ADD_CONST, b = idb & ~BPF_ADD_CONST;
u32 i;
if (!a || !b || js->pairs_overflow)
return false;
- if ((ida ^ idb) & BPF_ADD_CONST)
- return false;
for (i = 0; i < js->npairs; i++)
if (js->pairs[i].a == a && js->pairs[i].b == b)
return !js->pairs[i].bad;
@@ -199,15 +194,6 @@ static bool reg_value_equal(const struct bpf_reg_state *ra, const struct bpf_reg
return !memcmp(ra, rb, offsetof(struct bpf_reg_state, id));
}
-static bool scalar_link_consistent(const struct bpf_reg_state *ra, const struct bpf_reg_state *rb)
-{
- if ((ra->id ^ rb->id) & BPF_ADD_CONST)
- return false;
- if ((ra->id & BPF_ADD_CONST) && ra->delta != rb->delta)
- return false;
- return true;
-}
-
/*
* Join two registers, regsafe() documents what each type needs.
* Pass 1 records id pairs, pass 2 computes @dst, which is a copy of @ra on entry.
@@ -229,16 +215,8 @@ static bool join_reg(struct bpf_join_state *js, struct bpf_reg_state *dst,
case SCALAR_VALUE:
if (js->env->explore_alu_limits && !reg_value_equal(ra, rb))
return false;
- if (pass == 1) {
- jid_note(js, ra->id, rb->id, scalar_link_consistent(ra, rb));
- return true;
- }
- join_range(js, dst, ra, rb);
- /* a link that exists on one side only is dropped */
- if (ra->id && !jid_good(js, ra->id, rb->id)) {
- dst->id = 0;
- dst->delta = 0;
- }
+ if (pass == 2)
+ join_range(js, dst, ra, rb);
return true;
case PTR_TO_MAP_KEY:
case PTR_TO_MAP_VALUE:
@@ -269,7 +247,7 @@ static bool join_reg(struct bpf_join_state *js, struct bpf_reg_state *dst,
if (ra->r64.base != rb->r64.base)
return false;
if (pass == 1) {
- jid_note(js, ra->id, rb->id, true);
+ jid_note(js, ra->id, rb->id);
return true;
}
if (!jid_strict_ok(js, ra->id, rb->id))
@@ -291,9 +269,9 @@ static bool join_reg(struct bpf_join_state *js, struct bpf_reg_state *dst,
}
if (pass == 1) {
- jid_note(js, ra->id, rb->id, true);
- jid_note(js, ra->parent_id, rb->parent_id, true);
- jid_note(js, ra->map_uid, rb->map_uid, true);
+ jid_note(js, ra->id, rb->id);
+ jid_note(js, ra->parent_id, rb->parent_id);
+ jid_note(js, ra->map_uid, rb->map_uid);
return true;
}
if (!jid_strict_ok(js, ra->id, rb->id) ||
@@ -350,8 +328,8 @@ static bool join_special_slot(struct bpf_join_state *js, const struct bpf_stack_
break;
}
if (pass == 1) {
- jid_note(js, ra->id, rb->id, true);
- jid_note(js, ra->parent_id, rb->parent_id, true);
+ jid_note(js, ra->id, rb->id);
+ jid_note(js, ra->parent_id, rb->parent_id);
return true;
}
return jid_strict_ok(js, ra->id, rb->id) &&
@@ -563,12 +541,12 @@ static void join_refs_note(struct bpf_join_state *js, const struct bpf_verifier_
{
int i;
- jid_note(js, a->active_irq_id, b->active_irq_id, true);
- jid_note(js, a->active_lock_id, b->active_lock_id, true);
+ jid_note(js, a->active_irq_id, b->active_irq_id);
+ jid_note(js, a->active_lock_id, b->active_lock_id);
for (i = 0; i < a->acquired_refs; i++) {
- jid_note(js, a->refs[i].id, b->refs[i].id, true);
+ jid_note(js, a->refs[i].id, b->refs[i].id);
if (a->refs[i].type == REF_TYPE_PTR)
- jid_note(js, a->refs[i].parent_id, b->refs[i].parent_id, true);
+ jid_note(js, a->refs[i].parent_id, b->refs[i].parent_id);
}
}
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 27/29] bpf: Remove scalar ids from the log and from bpf_verifier.h
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (25 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 26/29] bpf: Remove scalar ids from the join of states Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 28/29] bpf: Support sign-extended 32-bit linear scalar evolution Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 29/29] selftests/bpf: Test loops with a sign-extended 32-bit induction variable Alexei Starovoitov
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
Removal of dead code, the rest of it. Nothing sets the id of a scalar. No
prog is verified differently, no line of a log changes.
Remove BPF_ADD_CONST*, what prints 'id=N+M', clear_scalar_id(), struct
bpf_idset, idset_scratch, tmp_id_gen and the declaration of
bpf_clear_singular_ids().
'delta' was the constant between two scalars with the same id. For
a pointer it is always 0, and log.c printed it for pointers only if it was
not. Its four bytes stay, as 'unused': they lie between 'type' and a union
that is aligned to 8 bytes, and regsafe() and the join compare the bytes
before var_off with memcmp(). Padding there has no defined content.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
include/linux/bpf_verifier.h | 46 +++++++-----------------------------
kernel/bpf/log.c | 10 ++------
2 files changed, 10 insertions(+), 46 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index 5a6bb40310f2..e7bfb2bcd954 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -60,10 +60,8 @@ struct bpf_stack_range {
struct bpf_reg_state {
/* Ordering of fields matters. See regsafe() */
enum bpf_reg_type type;
- /*
- * Constant delta between "linked" scalars with the same ID.
- */
- s32 delta;
+ /* 0. regsafe() compares the bytes before var_off: there is no hole among them. */
+ u32 unused;
union {
/* valid when type == PTR_TO_PACKET */
int range;
@@ -179,18 +177,9 @@ struct bpf_reg_state {
* the dynptr to determine proper reference freeing.
* Similarly to dynptrs, we use ID to track "belonging" of a reference
* to a specific instance of bpf_iter.
+ * A scalar has no id. The scalars that it is linked to are static: computed before the
+ * main pass, see bpf_scev_next_linked_reg().
*/
- /*
- * Upper bit of ID is used to remember relationship between "linked"
- * registers. Example:
- * r1 = r2; both will have r1->id == r2->id == N
- * r1 += 10; r1->id == N | BPF_ADD_CONST and r1->delta == 10
- * r3 = r2; both will have r3->id == r2->id == N
- * w3 += 10; r3->id == N | BPF_ADD_CONST32 and r3->delta == 10
- */
-#define BPF_ADD_CONST64 (1U << 31)
-#define BPF_ADD_CONST32 (1U << 30)
-#define BPF_ADD_CONST (BPF_ADD_CONST64 | BPF_ADD_CONST32)
u32 id;
/*
* Tracks the parent object this register was derived from.
@@ -1112,27 +1101,18 @@ struct bpf_id_pair {
* comes out for it, only if the same places have the same names.
* - bpf_flow_return() puts the ids of the call that waits back. All other ids get new values.
* - What comes out has the ranges of all calls that share the state at the entry. So a name is good
- * for what holds whatever the ranges are: the same object. Other ids have no name: a number
- * loses its id, any other argument keeps the frames of the caller, see arg_needs_caller().
- * - env->id_gen stays below BPF_MAX_ID. So no id is a name or has a bit of BPF_ADD_CONST.
+ * for what holds whatever the ranges are: the same object. Other ids have no name: such an
+ * argument keeps the frames of the caller, see arg_needs_caller().
+ * - env->id_gen stays below BPF_MAX_ID. So no id is a name.
*/
#define BPF_INHERITED_ID 0x20000000
#define BPF_MAX_ID (BPF_INHERITED_ID / 2)
struct bpf_idmap {
- u32 tmp_id_gen;
u32 cnt;
struct bpf_id_pair map[BPF_ID_MAP_SIZE];
};
-struct bpf_idset {
- u32 num_ids;
- struct {
- u32 id;
- u32 cnt;
- } entries[BPF_ID_MAP_SIZE];
-};
-
struct bpf_liveness;
struct scev;
@@ -1278,10 +1258,7 @@ struct bpf_verifier_env {
* via callx. Allocated when the first such edge is recorded.
*/
unsigned long *callx_edges;
- union {
- struct bpf_idmap idmap_scratch;
- struct bpf_idset idset_scratch;
- };
+ struct bpf_idmap idmap_scratch;
struct {
int *insn_state;
int *insn_stack;
@@ -1447,12 +1424,6 @@ void bpf_verbose_snum32(struct bpf_verifier_env *env, s32 num);
bpf_log(&env->log, "verifier bug: " fmt "\n", ##args); \
})
-static inline void clear_scalar_id(struct bpf_reg_state *reg)
-{
- reg->id = 0;
- reg->delta = 0;
-}
-
static inline void mark_calls_callback(struct bpf_verifier_env *env, int idx)
{
env->insn_aux_data[idx].calls_callback = true;
@@ -1589,7 +1560,6 @@ void bpf_mark_reg_not_init(const struct bpf_verifier_env *env,
void bpf_mark_reg_known_scalar(struct bpf_reg_state *reg, u64 imm);
void bpf_mark_reg_unknown(struct bpf_reg_state *reg);
int bpf_grow_stack_state(struct bpf_verifier_env *env, struct bpf_func_state *state, int size);
-void bpf_clear_singular_ids(struct bpf_verifier_env *env, struct bpf_verifier_state *st);
static inline int bpf_get_spi(s32 off)
{
diff --git a/kernel/bpf/log.c b/kernel/bpf/log.c
index 84659a757b43..ffbec587a916 100644
--- a/kernel/bpf/log.c
+++ b/kernel/bpf/log.c
@@ -696,7 +696,7 @@ static void print_reg_state(struct bpf_verifier_env *env,
if (state->frameno != reg->frameno)
verbose(env, "[%d]", reg->frameno);
if (tnum_is_const(reg->var_off)) {
- bpf_verbose_snum(env, reg->var_off.value + reg->delta);
+ bpf_verbose_snum(env, reg->var_off.value);
return;
}
}
@@ -704,9 +704,7 @@ static void print_reg_state(struct bpf_verifier_env *env,
verbose(env, "%s", btf_type_name(reg->btf, reg->btf_id));
verbose(env, "(");
if (reg->id)
- verbose_a("id=%d", reg->id & ~BPF_ADD_CONST);
- if (reg->id & BPF_ADD_CONST)
- verbose(env, "%+d", reg->delta);
+ verbose_a("id=%d", reg->id);
if (reg->parent_id)
verbose_a("parent_id=%d", reg->parent_id);
if (type_is_non_owning_ref(reg->type))
@@ -720,10 +718,6 @@ static void print_reg_state(struct bpf_verifier_env *env,
reg->map_ptr->key_size,
reg->map_ptr->value_size);
}
- if (t != SCALAR_VALUE && reg->delta) {
- verbose_a("off=");
- bpf_verbose_snum(env, reg->delta);
- }
if (type_is_pkt_pointer(t)) {
verbose_a("r=");
verbose_unum(env, reg->range);
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 28/29] bpf: Support sign-extended 32-bit linear scalar evolution
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (26 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 27/29] bpf: Remove scalar ids from the log and from bpf_verifier.h Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
2026-11-11 5:13 ` [PATCH bpf-next v2 29/29] selftests/bpf: Test loops with a sign-extended 32-bit induction variable Alexei Starovoitov
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A new ability: the recurrence (sext32 (linear rA IMM)), next to
(zext32 (linear ..)) and (spill32 (linear ..)). A loop over 'int i' as GCC
compiles it becomes a counted loop.
test_rhash_update_elements() of rhash.c has 'for (i = 0; i < 128; ++i)'.
GCC adds to an int in 32 bits and keeps the sign extension of the result:
53: r3 = r7
54: w3 += 1
55: r7 = (s32)r3
56: if w3 == 0x80 goto pc+27
scev at header 57:
r7=(sext32 (zext32 (+ r7 1))) / ?
The backedge value of r7, before the '/', has no closed form. No iteration
count (header_count: how often the header is executed) is computed.
header_count = backedge-taken count + 1. The loop is walked: the verifier
unrolls the loop, one state per iteration; a counted loop is summarized,
one state for all iterations. A long loop that is walked ends with "more
than 1024 states that cannot be joined". It loaded when the verifier
explored each path. GCC compiles some 50 loops of the selftests that way,
most of them with i in a register.
The latch tests the 32-bit add, which is supported. What is missing is to
look through the extension, and the range of all 64 bits:
- An extension and a partial spill truncate their operand. How it was
extended or spilled before does not matter, below an add too:
(sext32 (zext32 a)) is (sext32 a). Two rules of simplify() say so for
all widths, in place of six.
- The backedge value (sext32 (+ rA IMM)) is (sext32 (linear rA IMM)) at
the header. Its range is the sign extension of the range of the low 32
bits. A wrap is handled, not excluded: the range is all of s32 then.
- iv_range, the range that the latch condition implies, is a range of the
zero-extended low 32 bits. iv_bounds() ignores it if bit 31 can be set.
- For i in a stack slot, zero or sign extended, little endian only:
iterate_wakeupsources() of plain_c_test_wakeup_source.c has a 64-bit
spill and a 32-bit fill of i. The half-slot with the upper 32 bits is
dead, see the new bpf_half_slot_live_before(), and the states don't
have it: 'fp-16=pppp1000'. A recurrence over 64 bits has no entry value
there. (spill32 (linear (zext32 rA) IMM)) describes the low 32 bits
only, so it holds too: use it.
With the patch:
r7=(sext32 (+ r7 1)) / (sext32 (linear r7 1))
loop header at 57, header_count is [0..128]
The selftests as GCC builds them: no verdict changes, 2.6 % fewer insns.
test_rhash_update_elements (rhash.c) 6074 -> 105
iterate_wakeupsources (plain_c_test_wakeup_source.c) 4402 -> 131
nested_loops (loop1.c) 4558 -> 702
test1 (timer.c) 3973 -> 173
Precision that is lost. With 'size_t off', 'int n' and 'u32 sz' clang
-mcpu=v4 emits 'r1 = (s32)r0; r1 += r9; w3 = w1' for
off += n;
sz = off <= MAX ? HDR + off : HDR;
r3 was (zext32 (+ (sext32 r0) r9)). Now it is (zext32 (+ r0 r9)), which has
no base in common with r1: the test of off does not refine sz any more.
Three loops of these selftests keep a sign-extended i in a stack slot. Two
load before and after with the same insns. The third, in
dump_task_sleepable() of bpf_iter_tasks.c, is still rejected with the
message above: one of its two backedges has no linear latch, so the loop
has no iteration count.
Fixes: 5395aa9bbeb5 ("bpf: Verify programs as a fixpoint over the control flow graph")
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
include/linux/bpf_verifier.h | 1 +
kernel/bpf/liveness.c | 13 +++
kernel/bpf/scev.c | 154 +++++++++++++++++++----------------
3 files changed, 96 insertions(+), 72 deletions(-)
diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h
index e7bfb2bcd954..598763d128ab 100644
--- a/include/linux/bpf_verifier.h
+++ b/include/linux/bpf_verifier.h
@@ -2049,6 +2049,7 @@ int bpf_live_stack_query_init(struct bpf_verifier_env *env, struct bpf_verifier_
const unsigned long *bpf_may_write_mask(struct bpf_verifier_env *env, u32 insn_idx);
bool bpf_needs_fixed_stack_off(struct bpf_verifier_env *env, int insn_idx);
bool bpf_stack_slot_alive(struct bpf_verifier_env *env, u32 frameno, u32 spi);
+bool bpf_half_slot_live_before(struct bpf_verifier_env *env, u32 insn_idx, u32 half_spi);
int bpf_compute_live_registers(struct bpf_verifier_env *env);
#define BPF_MAP_KEY_POISON (1ULL << 63)
diff --git a/kernel/bpf/liveness.c b/kernel/bpf/liveness.c
index c52f0435193b..63d23d28f2a5 100644
--- a/kernel/bpf/liveness.c
+++ b/kernel/bpf/liveness.c
@@ -760,6 +760,19 @@ static void compute_may_write_masks(struct bpf_verifier_env *env)
}
}
+/* live_stack_before of the insn at @insn_idx for one half-slot */
+bool bpf_half_slot_live_before(struct bpf_verifier_env *env, u32 insn_idx, u32 half_spi)
+{
+ struct func_instance *instance;
+ int bkt;
+
+ hash_for_each(env->liveness->func_instances, bkt, instance, hl_node)
+ if (insn_idx - instance->subprog_start < instance->insn_cnt &&
+ is_live_before(instance, insn_idx, instance->depth, half_spi))
+ return true;
+ return false;
+}
+
const unsigned long *bpf_may_write_mask(struct bpf_verifier_env *env, u32 insn_idx)
{
return env->insn_aux_data[insn_idx].may_write_mask;
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index b46eb37234a3..e36181a00858 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -511,6 +511,23 @@ static bool is_spill32(struct scev *scev, int id, u32 *left)
return is_spill(scev, id, &bits, left) && bits == 32;
}
+/* (zextN x), (sextN x) or (spillN x) with N >= @bits: the low @bits bits are those of @left */
+static bool is_low_bits(struct scev *scev, int id, u32 bits, u32 *left)
+{
+ u32 op = scev->exprs[id].op;
+
+#ifdef __BIG_ENDIAN
+ /* see is_spill() */
+ if (op == SPILL8 || op == SPILL16 || op == SPILL32)
+ return false;
+#endif
+ /* nothing looks through a phi, see struct expr_def */
+ if (op_low_bits(op) < bits || scev->exprs[id].def.phi)
+ return false;
+ *left = scev->exprs[id].params[0];
+ return true;
+}
+
static bool is_any(struct scev *scev, int id, u32 *left, u32 *right)
{
return is_binop(scev, ANY, id, left, right);
@@ -792,6 +809,19 @@ static bool reg_alive_at(struct bpf_verifier_env *env, u32 reg, u32 insn_idx)
: test_bit(reg - MAX_BPF_REG, aux[insn_idx].live_stack_before);
}
+/* The first bytes of a stack slot are its high bits */
+static bool is_big_endian_slot(u32 reg)
+{
+ return IS_ENABLED(CONFIG_CPU_BIG_ENDIAN) && reg >= MAX_BPF_REG;
+}
+
+/* Are the upper 32 bits of @reg live before the insn at @insn_idx? */
+static bool hi32_alive_at(struct bpf_verifier_env *env, u32 reg, u32 insn_idx)
+{
+ return reg < MAX_BPF_REG || is_big_endian_slot(reg) ||
+ bpf_half_slot_live_before(env, insn_idx, (reg - MAX_BPF_REG) * 2);
+}
+
static void print_env(struct bpf_verifier_env *env, struct env *e, u32 insn_idx)
{
struct bpf_verifier_log *log = &env->log;
@@ -2157,12 +2187,6 @@ static bool match_linked_add(struct bpf_verifier_env *env, int h, u32 tested, u3
return true;
}
-/* The first bytes of a stack slot are its high bits */
-static bool is_big_endian_slot(u32 reg)
-{
- return IS_ENABLED(CONFIG_CPU_BIG_ENDIAN) && reg >= MAX_BPF_REG;
-}
-
/*
* const_diff() for the SCEV register @reg, which has the expression @id_b. A fill from byte
* *@fill_off of a slot reads these bits. On big endian the low bytes of a spill are the last ones,
@@ -2637,7 +2661,7 @@ static int transform_expr_once(struct scev *scev, u32 lvl, u32 root, void *priv,
static int simplify(struct scev *scev, u32 id)
{
int a, b, l, r, t, base, slope;
- u32 bits, op = scev->exprs[id].op;
+ u32 op = scev->exprs[id].op, bits = op_low_bits(op);
s64 imm1, imm2;
/* (- x imm) -> (+ x -imm) */
@@ -2679,50 +2703,22 @@ static int simplify(struct scev *scev, u32 id)
if (is_zext32(scev, id, &l) && is_imm(scev, l, &imm1))
return imm_expr(scev, (u64)(u32)imm1);
- /* The rules below look through the operand, not through a phi: see struct expr_def */
- if (op_low_bits(op) && scev->exprs[scev->exprs[id].params[0]].def.phi)
- return id;
-
- /*
- * (zext32 (+ (zext32 $a) $b)) -> (zext32 (+ $a $b))
- * (zext32 (+ $a (zext32 $b))) -> (zext32 (+ $a $b))
- */
- if ((is_zext32(scev, id, &t) && is_add(scev, t, &l, &b) && is_zext32(scev, l, &a) &&
- !scev->exprs[l].def.phi) ||
- (is_zext32(scev, id, &t) && is_add(scev, t, &a, &r) && is_zext32(scev, r, &b) &&
- !scev->exprs[r].def.phi)) {
- t = expr2(scev, BPF_ADD, a, b);
- t = expr1(scev, ZEXT32, t);
- return t;
- }
-
- /* (zext32 (zext32 X)) -> (zext32 X) */
- if (is_zext32(scev, id, &a) && is_zext32(scev, a, &b))
- return a;
-
- /*
- * '*(u32 *)(r10 - 8) = rX' stores the low half of rX:
- * (spill32 (zext32 $a)) -> (spill32 $a)
- * (spill32 (+ (zext32 $a) $b)) -> (spill32 (+ $a $b))
- */
- if (is_spill32(scev, id, &t) && is_zext32(scev, t, &a))
- return expr1(scev, SPILL32, a);
- if (is_spill32(scev, id, &t) && is_add(scev, t, &l, &b) && is_zext32(scev, l, &a) &&
- !scev->exprs[l].def.phi)
- return expr1(scev, SPILL32, expr2(scev, BPF_ADD, a, b));
-
/*
- * 'rX = *(u32 *)(r10 - 8)' fills what was spilled, a narrower fill the low bits of it:
- * (zext32 (spill32 $a)) -> (zext32 $a)
+ * An extension and a partial spill depend on the low bits of their operand only: look
+ * through an extension and a partial spill that is not narrower, below an add too:
+ * (sext32 (zext32 $a)) -> (sext32 $a)
* (sext8 (spill16 $a)) -> (sext8 $a)
- * (zext32 (+ (spill32 $a) $b)) -> (zext32 (+ $a $b))
+ * (zext32 (+ (sext32 $a) $b)) -> (zext32 (+ $a $b))
+ * (spill32 (+ $a (zext32 $b))) -> (spill32 (+ $a $b))
*/
- if (op_low_bits(op) && is_spill(scev, scev->exprs[id].params[0], &bits, &a) &&
- op_low_bits(op) <= bits)
+ if (!bits)
+ return id;
+ t = scev->exprs[id].params[0];
+ if (is_low_bits(scev, t, bits, &a))
return expr1(scev, op, a);
- if (is_zext32(scev, id, &t) && is_add(scev, t, &l, &b) && is_spill32(scev, l, &a) &&
- !scev->exprs[l].def.phi)
- return expr1(scev, ZEXT32, expr2(scev, BPF_ADD, a, b));
+ if (is_add(scev, t, &l, &r) && !scev->exprs[t].def.phi &&
+ (is_low_bits(scev, l, bits, &l) || is_low_bits(scev, r, bits, &r)))
+ return expr1(scev, op, expr2(scev, BPF_ADD, l, r));
return id;
}
@@ -2745,6 +2741,7 @@ static int compute_header_scevs(struct bpf_verifier_env *env, int h)
int l, r, t, id, ra, rb, ra_expr;
bool zext32;
s64 imm;
+ u32 op;
/*
* Note:
@@ -2779,30 +2776,24 @@ static int compute_header_scevs(struct bpf_verifier_env *env, int h)
header_env->reg2scev[ra] = id;
continue;
}
- /* rA = (is_zext32 (+ rA IMM)) -> (is_zext32 (linear rA IMM)) */
- if (is_zext32(scev, ra_expr, &t) &&
- is_add(scev, t, &l, &r) &&
- is_reg(scev, l, &rb) &&
- is_imm(scev, r, &imm) &&
- ra == rb) {
- id = expr2(scev, LINEAR_SCEV, l, r);
- id = expr1(scev, ZEXT32, id);
- if (id < 0)
- return id;
- header_env->reg2scev[ra] = id;
- continue;
- }
/*
+ * A 32-bit induction variable:
+ * rA = (zext32 (+ rA IMM)) -> (zext32 (linear rA IMM))
+ * rA = (sext32 (+ rA IMM)) -> (sext32 (linear rA IMM))
* rA = (spill32 (+ rA IMM)) -> (spill32 (linear (zext32 rA) IMM))
- * The low half of the slot counts. Nothing is known about the other half.
+ * The last SCEV leaves the upper 32 bits of the slot unknown, so it holds for the
+ * first two as well. They get it if these bits are dead: the states have a
+ * 32-bit spill then, see bpf_clean_verifier_state().
*/
- if (is_spill32(scev, ra_expr, &t) &&
+ if (is_low_bits(scev, ra_expr, 32, &t) &&
is_add(scev, t, &l, &r) &&
is_reg(scev, l, &rb) &&
is_imm(scev, r, &imm) &&
ra == rb) {
- id = expr2(scev, LINEAR_SCEV, expr1(scev, ZEXT32, l), r);
- id = expr1(scev, SPILL32, id);
+ op = hi32_alive_at(env, ra, h) ? scev->exprs[ra_expr].op : SPILL32;
+ if (op == SPILL32)
+ l = expr1(scev, ZEXT32, l);
+ id = expr1(scev, op, expr2(scev, LINEAR_SCEV, l, r));
if (id < 0)
return id;
header_env->reg2scev[ra] = id;
@@ -4402,8 +4393,10 @@ struct linear_coefs {
u32 base_reg;
s64 slope;
bool zext32;
- /* the low half of a stack slot */
+ /* with zext32: the low 32 bits of a stack slot */
bool spill32;
+ /* with zext32: the low 32 bits are sign extended */
+ bool sext32;
};
static bool is_simple_slope(struct scev *scev, u32 id, s64 *slope)
@@ -4419,8 +4412,12 @@ static bool is_simple_linear(struct scev *scev, u32 id, struct linear_coefs *out
u32 base, slope;
out->spill32 = false;
- return is_linear_or_linear32(scev, id, &base, &slope, &out->zext32) &&
- is_reg(scev, base, &out->base_reg) &&
+ out->sext32 = is_unop(scev, SEXT32, id, &id);
+ if (!is_linear_or_linear32(scev, id, &base, &slope, &out->zext32) ||
+ (out->sext32 && out->zext32))
+ return false;
+ out->zext32 |= out->sext32;
+ return is_reg(scev, base, &out->base_reg) &&
is_simple_slope(scev, slope, &out->slope);
}
@@ -4452,6 +4449,7 @@ static struct bpf_reg_state *spill32_counter(struct scev *scev, struct bpf_func_
return NULL;
out->zext32 = true;
out->spill32 = true;
+ out->sext32 = false;
return scev_reg_for_spill32(st, r);
}
@@ -4460,6 +4458,11 @@ static struct cnum64 cnum64_zext32(struct cnum32 r32)
return cnum64_from_urange(cnum32_umin(r32), cnum32_umax(r32));
}
+static struct cnum64 cnum64_sext32(struct cnum32 r32)
+{
+ return cnum64_from_srange(cnum32_smin(r32), cnum32_smax(r32));
+}
+
/* Compute the range an induction variable in `reg` spans over the loop. */
static void linear_bounds(struct bpf_reg_state *reg, u32 max_header_count,
struct linear_coefs *coefs, struct bounds *out)
@@ -4489,12 +4492,14 @@ static void linear_bounds(struct bpf_reg_state *reg, u32 max_header_count,
r32 = CNUM32_UNBOUNDED;
if (coefs->zext32) {
r32 = cnum32_from_cnum64(r64);
- r64 = cnum64_zext32(r32);
+ r64 = coefs->sext32 ? cnum64_sext32(r32) : cnum64_zext32(r32);
/*
* 32-bit iteration overflow is equivalent to subtracting N * 2**32:
* (base + slope * k) % slope ≠ (base + slope * k - N * 2**32) % slope.
*/
- overflow |= cnum32_contains(r32, 0) && cnum32_contains(r32, U32_MAX);
+ overflow |= coefs->sext32 ?
+ cnum32_contains(r32, S32_MAX) && cnum32_contains(r32, S32_MIN) :
+ cnum32_contains(r32, 0) && cnum32_contains(r32, U32_MAX);
/*
* The entry and zero-extended values must agree on how they define `base`,
* otherwise `base + slope * k` would define different sets of points between
@@ -4538,10 +4543,14 @@ static bool is_iv_at_header(struct bpf_verifier_env *env, int h, int iv_reg, u32
return r == iv_reg || (const_diff_at_header(env, h, iv_reg, r, &delta) && !delta);
}
-static void iv_bounds(struct bounds *bounds, struct cnum64 iv_range)
+static void iv_bounds(struct bounds *bounds, const struct linear_coefs *coefs,
+ struct cnum64 iv_range)
{
struct cnum64 r64 = cnum64_intersect(bounds->r64, iv_range);
+ /* The latch bounds the zero extension. With bit 31 set the sign extension differs. */
+ if (coefs->sext32 && cnum64_umax(iv_range) > S32_MAX)
+ return;
if (!cnum64_is_empty(r64))
bounds->r64 = r64;
}
@@ -5081,7 +5090,7 @@ int bpf_widen_scev_regs(struct bpf_verifier_env *env, struct bpf_verifier_state
return err;
linear_bounds(reg, iters->max_header_count, &coefs, &bounds);
if (is_iv_at_header(env, insn_idx, iters->iv_reg, coefs.base_reg))
- iv_bounds(&bounds, iters->iv_range);
+ iv_bounds(&bounds, &coefs, iters->iv_range);
if (log->level & BPF_LOG_LEVEL2) {
bpf_log(log, "loop header at %d, widening ", insn_idx);
log_reg(env, r);
@@ -5127,6 +5136,7 @@ static bool is_moved_linear(struct scev *scev, u32 id, struct linear_coefs *out,
u32 base, slope, l, r;
out->spill32 = false;
+ out->sext32 = false;
return is_linear_or_linear32(scev, id, &base, &slope, &out->zext32) && !out->zext32 &&
is_add(scev, base, &l, &r) && is_reg(scev, l, &out->base_reg) &&
is_imm(scev, r, moved) && is_simple_slope(scev, slope, &out->slope);
@@ -5207,7 +5217,7 @@ int bpf_clamp_scev_regs(struct bpf_verifier_env *env, struct bpf_func_state *cur
* of its header value. @reg went through that insn and has what holds of the range.
*/
if (!behind && is_iv_at_header(env, insn_idx, entry->iv_reg, coefs.base_reg))
- iv_bounds(&bounds, entry->iv_range);
+ iv_bounds(&bounds, &coefs, entry->iv_range);
if (moved) {
/* a sum that can wrap has another residue, see scalar_step_add() */
if (check_add_overflow(cnum64_smin(bounds.r64), moved, &sum) ||
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH bpf-next v2 29/29] selftests/bpf: Test loops with a sign-extended 32-bit induction variable
2026-11-11 5:13 [PATCH bpf-next v2 00/29] bpf: Find linked registers by scalar evolution, remove scalar ids Alexei Starovoitov
` (27 preceding siblings ...)
2026-11-11 5:13 ` [PATCH bpf-next v2 28/29] bpf: Support sign-extended 32-bit linear scalar evolution Alexei Starovoitov
@ 2026-11-11 5:13 ` Alexei Starovoitov
28 siblings, 0 replies; 36+ messages in thread
From: Alexei Starovoitov @ 2026-11-11 5:13 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
Tests for (sext32 (linear rA IMM)), the recurrence of 'int i' as GCC
compiles it.
'w1 = w6; w1 += 1; r6 = (s32)r1; if w1 != 4 goto' is '++i != 4' as GCC
emits it. An index that is not in 'char buf[8]' gets to a read of r5, which
is not initialized. r9 is the number of iterations and is what the prog
returns. What loads is run.
- sext32_iv: i in a register goes from -4 to 3. sext32_iv_hi32_dead: i in
a stack slot, the half-slot with the upper 32 bits is dead. Both are
counted loops with header_count 8. The negative tests, *_oob, have one
iteration more.
- sext32_iv_wraps: i wraps at INT32_MAX in the loop. Its range is all of
s32, the index is out of bounds.
- sext32_iv_unsigned_bound: 'i < n' with n above INT32_MAX. The range that
the latch condition implies is not the range of i.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
.../selftests/bpf/progs/verifier_scev.c | 52 +++++++++++++++++++
1 file changed, 52 insertions(+)
diff --git a/tools/testing/selftests/bpf/progs/verifier_scev.c b/tools/testing/selftests/bpf/progs/verifier_scev.c
index 9b1877a1b5e5..a79c00a99c89 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scev.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scev.c
@@ -1200,6 +1200,50 @@ RANGE_TEST(range_ge_dir, "", 10, 20, 2, 3, 5, PRE("<"), NO_COUNT)
RANGE_TEST(range_ne_step2, "", 0, 3, 2, 3, 8, PRE("=="), NO_COUNT)
RANGE_TEST(range_ne_behind, "", 0, 4, 1, 3, 8, PRE("=="), NO_COUNT)
+/*
+ * char buf[8];
+ * for (int i = first; ...; i++)
+ * sum += buf[i - first];
+ *
+ * GCC adds in 32 bits and keeps the sign extension of the result: a sign-extended 32-bit induction
+ * variable. r6 or fp-16 is i, r7 is first, r9 counts the iterations. r8 is 0, which the verifier
+ * doesn't know. In place of an out-of-bounds access to buf r5 is read, which is not initialized.
+ */
+#define SEXT32_TEST(name, _first, loop, expect) \
+SEC("socket") \
+__log_level(2) \
+expect \
+__naked void name(void) \
+{ \
+ asm volatile ( \
+ "r6 = %[first] ll; r7 = r6; r8 = 0; r9 = 0;" \
+ "*(u64 *)(r10 - 8) = r8; lock *(u64 *)(r10 - 8) += r8; r8 = *(u64 *)(r10 - 8);" \
+ "*(u64 *)(r10 - 16) = r6;" \
+ loop \
+ "2: r0 = r9; exit;" \
+ : \
+ : __imm_const(first, _first) \
+ : __clobber_all); \
+}
+#define BUF(i) "r9 += 1; r2 = " i "; r2 -= r7; if r2 <= 7 goto +1; r0 = *(u64 *)(r5 + 0);"
+#define OOB __failure __msg("R5 !read_ok")
+#define ALL_OF_S32 "0xffffffff80000000..0x7fffffff "
+#define SEXT32_INC "w1 = w6; w1 += 1; r6 = (s32)r1;"
+#define SEXT32_LOOP(bound) "1:" BUF("r6") SEXT32_INC "if w1 != " #bound " goto 1b;"
+
+SEXT32_TEST(sext32_iv, -4, SEXT32_LOOP(4), __success __retval(8)
+ __msg(" r6=(sext32 (+ r6 1)) / (sext32 (linear r6 1))")
+ __msg("header_count is 8 ") __msg("widening r6 to -4..3 "))
+SEXT32_TEST(sext32_iv_oob, -4, SEXT32_LOOP(5), OOB)
+/* i wraps from INT32_MAX to INT32_MIN. The latch tests 0x7ffffffd .. 0x80000004. */
+SEXT32_TEST(sext32_iv_wraps, INT32_MAX - 3, SEXT32_LOOP(0x80000004),
+ __msg("header_count is 8 ") __msg("widening r6 to " ALL_OF_S32) OOB)
+/* 'i < n' implies the unsigned range 1 .. 0x80000000. Its last value is negative in r6. */
+SEXT32_TEST(sext32_iv_unsigned_bound, 0,
+ "r8 &= 1; r6 += r8; r4 = 0x80000001 ll;"
+ "1:" SEXT32_INC "if w1 < w4 goto 1b; if r6 s>= 0 goto 2f; r0 = *(u64 *)(r5 + 0);",
+ __msg("widening r6 to " ALL_OF_S32) OOB)
+
/*
* Two back edges:
*
@@ -3228,6 +3272,14 @@ RANGE_TEST(spill32_first_is_precise, "", 0, 0, 1, 0, 1,
RANGE_TEST(spill32_first_is_64bit, "", 0, 0, 1, 5, 5,
"*(u64 *)(r10 - 8) = r6; 1:" LIMIT_ITERATIONS SPILL32_INC "if w1 != w7 goto 1b;",
SPILL32_COUNTED(5, "0..4"))
+/* A 64-bit spill of i. Its upper 32 bits are dead: the low 32 bits are the induction variable. */
+#define SEXT32_SPILL_LOOP(bound) \
+ "1:" BUF("*(u32 *)(r10 - 16)") "w1 = *(u32 *)(r10 - 16); w1 += 1; r3 = (s32)r1;" \
+ "*(u64 *)(r10 - 16) = r3; if w1 != " #bound " goto 1b;"
+SEXT32_TEST(sext32_iv_hi32_dead, 0, SEXT32_SPILL_LOOP(8), __success __retval(8)
+ __msg(" *fp-16=(sext32 (+ *fp-16 1)) / (spill32 (linear (zext32 *fp-16) 1))")
+ __msg("header_count is 8 ") __msg("widening *fp-16 to 0..7 "))
+SEXT32_TEST(sext32_iv_hi32_dead_oob, 0, SEXT32_SPILL_LOOP(9), OOB)
/* The counter makes the address of a spill. Such a loop is not summarized, as with a register. */
PARTIAL_SPILL_TEST(spill32_is_a_stack_offset, u32, 4,
"can't widen *fp-8, expr is (spill32 (+ *fp-8 8)), requires exact stack-offset tracking",
--
2.55.0
^ permalink raw reply related [flat|nested] 36+ messages in thread