* [PATCH bpf-next 1/2] bpf: Walk a short loop for what it loads if something looks at it
@ 2026-11-07 17:58 Alexei Starovoitov
2026-11-07 17:58 ` [PATCH bpf-next 2/2] selftests/bpf: Test short loops that carry what they load Alexei Starovoitov
2026-11-07 19:50 ` [PATCH bpf-next 1/2] bpf: Walk a short loop for what it loads if something looks at it patchwork-bot+netdevbpf
0 siblings, 2 replies; 3+ messages in thread
From: Alexei Starovoitov @ 2026-11-07 17:58 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
for (i = 0; i < 64; i++) {
if (have)
sum += t->pid;
t = v->task; /* __kptr: a task or NULL */
have = t && t->prio > 100;
}
t is loaded anew in each iteration. The first iteration of such a loop is
verified on its own, and the states that return to the loop head stand for
the other iterations. There have becomes [0, 1], also next to t == NULL:
R2 invalid mem access 'scalar'
for (i = 0; i < 64; i++) {
sum += a[cur]; /* u8 a[16] */
cur = order[i]; /* static const u8 order[64] */
}
cur is order[0] after the first iteration and a byte after the next one:
R4 unbounded memory access, make sure to bounds check any such access
Both load if the verifier goes through the iterations one by one: have goes
with t, each order[i] is a constant. It did so before the first iteration
was kept apart, and it still does for what a call returns.
Do the same for what is loaded and then looked at: t is the base of a load,
cur is an offset. A stack slot that the loop fills and that is only copied
later is no reason for a walk. test_rbtree_remove_many_noalloc() of
libarena is walked again: 22060 -> 27699 insns.
Fixes: df9b13a441c9 ("bpf: Keep the first iteration of a counted loop apart")
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
kernel/bpf/scev.c | 9 +++++----
kernel/bpf/verifier.c | 2 +-
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/kernel/bpf/scev.c b/kernel/bpf/scev.c
index 5415fcf9df2f..2a7d0c59f73b 100644
--- a/kernel/bpf/scev.c
+++ b/kernel/bpf/scev.c
@@ -3749,13 +3749,14 @@ int bpf_compute_loop_iters(struct bpf_verifier_env *env, struct bpf_verifier_sta
}
/*
* What is loaded from memory or returned by a call in every iteration is the same
- * kind of thing in all of them. One iteration tells what. There is nothing more
- * to find out when iterations are looked at one by one. But for what goes with it:
- * a call returns NULL and a flag is set. So a walk comes first for a call.
+ * kind of thing in all of them. One iteration tells what. A walk through the
+ * iterations tells more: what goes with it, like a flag that is set if it's not
+ * NULL, and each number of a table in .rodata. So a walk comes first. What is
+ * loaded is often a leftover in a stack slot: not if nothing looks at it.
*/
if (expr_is_fresh(env, header_env->reg2expr[r], &call)) {
__set_bit(r, iters->fresh_regs);
- if (!call)
+ if (!call && !bpf_flow_is_needed(env, insn_idx, r))
continue;
} else {
__set_bit(r, iters->forget_regs);
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 09a44723bb2e..479c52cc2f56 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -21636,7 +21636,7 @@ int bpf_flow_loop_enter(struct bpf_verifier_env *env, bool *backedge, bool *drop
entry->entry_state = entry_state;
entry->terminates = true;
/*
- * idx = 0; for (i = 0; i < 100; i++) idx = table[idx];
+ * idx = 0; for (i = 0; i < 100000; i++) idx = table[idx];
* What the loop makes of idx is not known before its body is looked at, and what idx is
* at the entry goes with i == 0 only. So the state goes through the first iteration as
* it is. The state that comes back to the head has idx in 0..255.
--
2.55.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* [PATCH bpf-next 2/2] selftests/bpf: Test short loops that carry what they load
2026-11-07 17:58 [PATCH bpf-next 1/2] bpf: Walk a short loop for what it loads if something looks at it Alexei Starovoitov
@ 2026-11-07 17:58 ` Alexei Starovoitov
2026-11-07 19:50 ` [PATCH bpf-next 1/2] bpf: Walk a short loop for what it loads if something looks at it patchwork-bot+netdevbpf
1 sibling, 0 replies; 3+ messages in thread
From: Alexei Starovoitov @ 2026-11-07 17:58 UTC (permalink / raw)
To: bpf; +Cc: daniel, andrii, eddyz87, memxor
From: Alexei Starovoitov <ast@kernel.org>
A loop of 4 iterations loads a number and sets a flag if the number is a
valid index. The next iteration tests the flag only. Another loop takes the
index of the next iteration from a table in .rodata. Both load if the loop
is walked. Each has a twin that reads past the end of the map value.
A loop that fills a stack slot with what it loads is not walked if the slot
is only copied later: 17 insns for 40 iterations.
The two tests of a first iteration that is kept apart get 100000
iterations. 10 are walked now.
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
---
.../selftests/bpf/progs/verifier_loop_need.c | 95 +++++++++++++++++++
.../selftests/bpf/progs/verifier_scev.c | 14 +--
2 files changed, 102 insertions(+), 7 deletions(-)
diff --git a/tools/testing/selftests/bpf/progs/verifier_loop_need.c b/tools/testing/selftests/bpf/progs/verifier_loop_need.c
index 281f94bfeb47..cfc98aeb155b 100644
--- a/tools/testing/selftests/bpf/progs/verifier_loop_need.c
+++ b/tools/testing/selftests/bpf/progs/verifier_loop_need.c
@@ -346,6 +346,101 @@ __naked void read_in_the_first_iteration(void)
SUM_OF_PREV(1, 40, "*(u64 *)(r10 - 8) = r9;");
}
+/*
+ * for (i = 0; i < 4; i++) { if (ok) sum += v[x]; x = *v; ok = x <= max; }
+ * x is loaded in every iteration and ok goes with it. One state for all iterations has both values
+ * of ok for each x.
+ */
+#define LOADED_WITH_FLAG(max) \
+ asm volatile ( \
+ "r6 = %[vals] ll;" \
+ "r7 = 0;" \
+ "r8 = 0;" \
+ "r9 = 0;" \
+"1: if r9 == 0 goto 2f;" \
+ "r1 = r6;" \
+ "r1 += r8;" \
+ "r0 = *(u8 *)(r1 + 0);" \
+"2: r8 = *(u64 *)(r6 + 0);" \
+ "r9 = 0;" \
+ "if r8 > " #max " goto 3f;" \
+ "r9 = 1;" \
+"3: r7 += 1;" \
+ "if r7 < 4 goto 1b;" \
+ "r0 = 0;" \
+ "exit;" \
+ :: __imm_addr(vals) : __clobber_all)
+
+OK("a short loop is walked for the flag that goes with what it loads")
+__naked void loaded_with_flag(void)
+{
+ LOADED_WITH_FLAG(63);
+}
+
+BAD("the flag is set for a number that is too large")
+__msg("invalid access to map value")
+__naked void loaded_with_wrong_flag(void)
+{
+ LOADED_WITH_FLAG(64);
+}
+
+/*
+ * for (i = 0; i < 4; i++) { sum += v[x]; x = table[i]; }
+ * The table is in .rodata: in a walk each x is a known number.
+ */
+const u8 in_vals[4] = { 8, 16, 56, 255 }, past_vals[4] = { 8, 16, 57, 0 };
+
+#define INDEX_FROM_RODATA(table) \
+ asm volatile ( \
+ "r6 = %[vals] ll;" \
+ "r7 = 0;" \
+ "r8 = 0;" \
+"1: r1 = r6;" \
+ "r1 += r8;" \
+ "r0 = *(u64 *)(r1 + 0);" \
+ "r1 = %[" #table "] ll;" \
+ "r1 += r7;" \
+ "r8 = *(u8 *)(r1 + 0);" \
+ "r7 += 1;" \
+ "if r7 < 4 goto 1b;" \
+ "r0 = 0;" \
+ "exit;" \
+ :: __imm_addr(vals), __imm_addr(table) : __clobber_all)
+
+OK("a short loop is walked for the numbers that it loads from .rodata")
+__naked void index_from_rodata(void)
+{
+ INDEX_FROM_RODATA(in_vals);
+}
+
+BAD("a number from .rodata is too large")
+__msg("invalid access to map value")
+__naked void index_from_rodata_too_large(void)
+{
+ INDEX_FROM_RODATA(past_vals);
+}
+
+/* A slot holds a leftover pointer. The loop fills it with what it loads. It's only copied later. */
+OK("no walk for what the loop loads if nothing looks at it")
+__log_level(2) __msg("processed 17 insns")
+__naked void leftover_in_a_slot(void)
+{
+ asm volatile (
+ "r6 = %[vals] ll;"
+ "*(u64 *)(r10 - 8) = r6;"
+ "r7 = 0;"
+"1: if r7 >= 40 goto 2f;"
+ "r1 = *(u64 *)(r6 + 0);"
+ "*(u64 *)(r10 - 8) = r1;"
+ "r7 += 1;"
+ "goto 1b;"
+"2: r1 = *(u64 *)(r10 - 8);"
+ "*(u64 *)(r6 + 0) = r1;"
+ "r0 = 0;"
+ "exit;"
+ :: __imm_addr(vals) : __clobber_all);
+}
+
/* What a call returns is made anew in every iteration, like what is loaded */
OK("a long loop carries what a call returns")
int call_in_a_long_loop(void *ctx)
diff --git a/tools/testing/selftests/bpf/progs/verifier_scev.c b/tools/testing/selftests/bpf/progs/verifier_scev.c
index c1aede6fa4ae..5c9c832fbbe0 100644
--- a/tools/testing/selftests/bpf/progs/verifier_scev.c
+++ b/tools/testing/selftests/bpf/progs/verifier_scev.c
@@ -4411,17 +4411,17 @@ __naked void post_cond_jlt32_copy(void)
}
/*
- * The loop loads r6 again in every iteration. It's 0 at the entry and a byte in all iterations
- * but the first one: the first iteration is verified on its own, the state that comes back to
- * the head stands for the rest.
+ * The loop loads r6 again in every iteration, and is too long for a walk. r6 is 0 at the entry and
+ * a byte in all iterations but the first one: the first iteration is verified on its own, the state
+ * that comes back to the head stands for the rest.
*/
SEC("socket")
__success __retval(0)
__log_level(2)
-__msg("header_count is 10 ")
+__msg("header_count is 100000 ")
__msg("can't widen r6, expr is _")
__not_msg("widening r8")
-__msg("clamping r8 to 0..9 step 1")
+__msg("clamping r8 to 0..0x1869f step 1")
__naked void index_loaded_in_previous_iteration(void)
{
asm volatile (" \
@@ -4439,7 +4439,7 @@ __naked void index_loaded_in_previous_iteration(void)
r0 = *(u8 *)(r1 + 0); \
r6 = *(u8 *)(r7 + 0); \
r8 += 1; \
- if r8 < 10 goto 1b; \
+ if r8 < 100000 goto 1b; \
2: r0 = 0; \
exit; \
" :
@@ -4473,7 +4473,7 @@ __naked void first_iteration_loads_less(void)
goto 3f; \
2: r6 = *(u16 *)(r7 + 0); \
3: r8 += 1; \
- if r8 < 10 goto 1b; \
+ if r8 < 100000 goto 1b; \
4: r0 = 0; \
exit; \
" :
--
2.55.0
^ permalink raw reply related [flat|nested] 3+ messages in thread
* Re: [PATCH bpf-next 1/2] bpf: Walk a short loop for what it loads if something looks at it
2026-11-07 17:58 [PATCH bpf-next 1/2] bpf: Walk a short loop for what it loads if something looks at it Alexei Starovoitov
2026-11-07 17:58 ` [PATCH bpf-next 2/2] selftests/bpf: Test short loops that carry what they load Alexei Starovoitov
@ 2026-11-07 19:50 ` patchwork-bot+netdevbpf
1 sibling, 0 replies; 3+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-11-07 19:50 UTC (permalink / raw)
To: Alexei Starovoitov; +Cc: bpf, daniel, andrii, eddyz87, memxor
Hello:
This series was applied to bpf/bpf-next.git (master)
by Alexei Starovoitov <ast@kernel.org>:
On Sat, 7 Nov 2026 17:58:18 +0000 you wrote:
> From: Alexei Starovoitov <ast@kernel.org>
>
> for (i = 0; i < 64; i++) {
> if (have)
> sum += t->pid;
> t = v->task; /* __kptr: a task or NULL */
> have = t && t->prio > 100;
> }
>
> [...]
Here is the summary with links:
- [bpf-next,1/2] bpf: Walk a short loop for what it loads if something looks at it
https://git.kernel.org/bpf/bpf-next/c/3fba1a6cf43a
- [bpf-next,2/2] selftests/bpf: Test short loops that carry what they load
https://git.kernel.org/bpf/bpf-next/c/1f56fd122f47
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply [flat|nested] 3+ messages in thread
end of thread, other threads:[~2026-11-07 19:50 UTC | newest]
Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-07 17:58 [PATCH bpf-next 1/2] bpf: Walk a short loop for what it loads if something looks at it Alexei Starovoitov
2026-11-07 17:58 ` [PATCH bpf-next 2/2] selftests/bpf: Test short loops that carry what they load Alexei Starovoitov
2026-11-07 19:50 ` [PATCH bpf-next 1/2] bpf: Walk a short loop for what it loads if something looks at it patchwork-bot+netdevbpf
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox