Skip to content

Commit f941479

Browse files
author
Alexei Starovoitov
committed
Merge branch 'bpf-verifier-improve-state-pruning-for-scalar-registers'
Puranjay Mohan says: ==================== bpf: Improve state pruning for scalar registers V2: https://lore.kernel.org/all/20260203022229.1630849-1-puranjay@kernel.org/ Changes in V3: - Fix spelling mistakes in commit logs (AI) - Fix an incorrect comment in the selftest added in patch 5 (AI) - Improve the title of patch 5 V1: https://lore.kernel.org/all/20260202104414.3103323-1-puranjay@kernel.org/ Changes in V2: - Collected acked by Eduard - Removed some unnecessary comments - Added a selftest for id=0 equivalence in Patch 5 This series improves BPF verifier state pruning by relaxing scalar ID equivalence requirements. Scalar register IDs are used to track relationships between registers for bounds propagation. However, once an ID becomes "singular" (only one register/stack slot carries it), it can no longer participate in bounds propagation and becomes stale. These stale IDs can prevent pruning of otherwise equivalent states. The series addresses this in four patches: Patch 1: Assign IDs on stack fills to ensure stack slots have IDs before being read into registers, preparing for the singular ID clearing in patch 2. Patch 2: Clear IDs that appear only once before caching, as they cannot contribute to bounds propagation. Patch 3: Relax maybe_widen_reg() to only compare value-tracking fields (bounds, tnum, var_off) rather than also requiring ID matches. Two scalars with identical value constraints but different IDs represent the same abstract value and don't need widening. Patch 4: Relax scalar ID equivalence in state comparison by treating rold->id == 0 as "independent". If the old state didn't rely on ID relationships for a register, any linking in the current state only adds constraints and is safe to accept for pruning. Patch 5: Add a selftest to show the exact case being handled by Patch 4 I ran veristat on BPF programs from sched_ext, meta's internal programs, and on selftest programs, showing programs with insn diff > 5%: Scx Progs File Program States (A) States (B) States (DIFF) Insns (A) Insns (B) Insns (DIFF) ------------------ ------------------- ---------- ---------- ------------- --------- --------- --------------- scx_rusty.bpf.o rusty_set_cpumask 320 230 -90 (-28.12%) 4478 3259 -1219 (-27.22%) scx_bpfland.bpf.o bpfland_select_cpu 55 49 -6 (-10.91%) 691 618 -73 (-10.56%) scx_beerland.bpf.o beerland_select_cpu 27 25 -2 (-7.41%) 320 295 -25 (-7.81%) scx_p2dq.bpf.o p2dq_init 265 250 -15 (-5.66%) 3423 3233 -190 (-5.55%) scx_layered.bpf.o layered_enqueue 1461 1386 -75 (-5.13%) 14541 13792 -749 (-5.15%) FB Progs File Program States (A) States (B) States (DIFF) Insns (A) Insns (B) Insns (DIFF) ------------ ------------------- ---------- ---------- -------------- --------- --------- --------------- bpf007.bpf.o bpfj_free 1726 1342 -384 (-22.25%) 25671 19096 -6575 (-25.61%) bpf041.bpf.o armr_net_block_init 22373 20411 -1962 (-8.77%) 651697 602873 -48824 (-7.49%) bpf227.bpf.o layered_quiescent 28 26 -2 (-7.14%) 365 340 -25 (-6.85%) bpf248.bpf.o p2dq_init 263 248 -15 (-5.70%) 3370 3159 -211 (-6.26%) bpf254.bpf.o p2dq_init 263 248 -15 (-5.70%) 3388 3177 -211 (-6.23%) bpf241.bpf.o p2dq_init 264 249 -15 (-5.68%) 3428 3240 -188 (-5.48%) bpf230.bpf.o p2dq_init 287 271 -16 (-5.57%) 3666 3431 -235 (-6.41%) bpf251.bpf.o lavd_cpu_offline 321 316 -5 (-1.56%) 6221 5891 -330 (-5.30%) bpf251.bpf.o lavd_cpu_online 321 316 -5 (-1.56%) 6219 5889 -330 (-5.31%) Selftest Progs File Program States (A) States (B) States (DIFF) Insns (A) Insns (B) Insns (DIFF) ---------------------------------- ----------------- ---------- ---------- ------------- --------- --------- --------------- verifier_iterating_callbacks.bpf.o test2 4 2 -2 (-50.00%) 29 18 -11 (-37.93%) verifier_iterating_callbacks.bpf.o test3 4 2 -2 (-50.00%) 31 19 -12 (-38.71%) strobemeta_bpf_loop.bpf.o on_event 318 221 -97 (-30.50%) 3938 2755 -1183 (-30.04%) bpf_qdisc_fq.bpf.o bpf_fq_dequeue 133 105 -28 (-21.05%) 1686 1385 -301 (-17.85%) iters.bpf.o delayed_read_mark 6 5 -1 (-16.67%) 60 46 -14 (-23.33%) arena_strsearch.bpf.o arena_strsearch 107 106 -1 (-0.93%) 1394 1258 -136 (-9.76%) ==================== Link: https://patch.msgid.link/20260203165102.2302462-1-puranjay@kernel.org Signed-off-by: Alexei Starovoitov <ast@kernel.org>
2 parents d95d76a + f6ef558 commit f941479

3 files changed

Lines changed: 199 additions & 25 deletions

File tree

include/linux/bpf_verifier.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,8 +697,11 @@ struct bpf_idmap {
697697
};
698698

699699
struct bpf_idset {
700-
u32 count;
701-
u32 ids[BPF_ID_MAP_SIZE];
700+
u32 num_ids;
701+
struct {
702+
u32 id;
703+
u32 cnt;
704+
} entries[BPF_ID_MAP_SIZE];
702705
};
703706

704707
/* see verifier.c:compute_scc_callchain() */

kernel/bpf/verifier.c

Lines changed: 144 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5518,6 +5518,12 @@ static int check_stack_read_fixed_off(struct bpf_verifier_env *env,
55185518
*/
55195519
s32 subreg_def = state->regs[dst_regno].subreg_def;
55205520

5521+
if (env->bpf_capable && size == 4 && spill_size == 4 &&
5522+
get_reg_width(reg) <= 32)
5523+
/* Ensure stack slot has an ID to build a relation
5524+
* with the destination register on fill.
5525+
*/
5526+
assign_scalar_id_before_mov(env, reg);
55215527
copy_register_state(&state->regs[dst_regno], reg);
55225528
state->regs[dst_regno].subreg_def = subreg_def;
55235529

@@ -5563,6 +5569,11 @@ static int check_stack_read_fixed_off(struct bpf_verifier_env *env,
55635569
}
55645570
} else if (dst_regno >= 0) {
55655571
/* restore register state from stack */
5572+
if (env->bpf_capable)
5573+
/* Ensure stack slot has an ID to build a relation
5574+
* with the destination register on fill.
5575+
*/
5576+
assign_scalar_id_before_mov(env, reg);
55665577
copy_register_state(&state->regs[dst_regno], reg);
55675578
/* mark reg as written since spilled pointer state likely
55685579
* has its liveness marks cleared by is_state_visited()
@@ -8984,15 +8995,24 @@ static bool regs_exact(const struct bpf_reg_state *rold,
89848995
const struct bpf_reg_state *rcur,
89858996
struct bpf_idmap *idmap);
89868997

8998+
/*
8999+
* Check if scalar registers are exact for the purpose of not widening.
9000+
* More lenient than regs_exact()
9001+
*/
9002+
static bool scalars_exact_for_widen(const struct bpf_reg_state *rold,
9003+
const struct bpf_reg_state *rcur)
9004+
{
9005+
return !memcmp(rold, rcur, offsetof(struct bpf_reg_state, id));
9006+
}
9007+
89879008
static void maybe_widen_reg(struct bpf_verifier_env *env,
8988-
struct bpf_reg_state *rold, struct bpf_reg_state *rcur,
8989-
struct bpf_idmap *idmap)
9009+
struct bpf_reg_state *rold, struct bpf_reg_state *rcur)
89909010
{
89919011
if (rold->type != SCALAR_VALUE)
89929012
return;
89939013
if (rold->type != rcur->type)
89949014
return;
8995-
if (rold->precise || rcur->precise || regs_exact(rold, rcur, idmap))
9015+
if (rold->precise || rcur->precise || scalars_exact_for_widen(rold, rcur))
89969016
return;
89979017
__mark_reg_unknown(env, rcur);
89989018
}
@@ -9004,16 +9024,14 @@ static int widen_imprecise_scalars(struct bpf_verifier_env *env,
90049024
struct bpf_func_state *fold, *fcur;
90059025
int i, fr, num_slots;
90069026

9007-
reset_idmap_scratch(env);
90089027
for (fr = old->curframe; fr >= 0; fr--) {
90099028
fold = old->frame[fr];
90109029
fcur = cur->frame[fr];
90119030

90129031
for (i = 0; i < MAX_BPF_REG; i++)
90139032
maybe_widen_reg(env,
90149033
&fold->regs[i],
9015-
&fcur->regs[i],
9016-
&env->idmap_scratch);
9034+
&fcur->regs[i]);
90179035

90189036
num_slots = min(fold->allocated_stack / BPF_REG_SIZE,
90199037
fcur->allocated_stack / BPF_REG_SIZE);
@@ -9024,8 +9042,7 @@ static int widen_imprecise_scalars(struct bpf_verifier_env *env,
90249042

90259043
maybe_widen_reg(env,
90269044
&fold->stack[i].spilled_ptr,
9027-
&fcur->stack[i].spilled_ptr,
9028-
&env->idmap_scratch);
9045+
&fcur->stack[i].spilled_ptr);
90299046
}
90309047
}
90319048
return 0;
@@ -19370,13 +19387,29 @@ static bool check_ids(u32 old_id, u32 cur_id, struct bpf_idmap *idmap)
1937019387
return false;
1937119388
}
1937219389

19373-
/* Similar to check_ids(), but allocate a unique temporary ID
19374-
* for 'old_id' or 'cur_id' of zero.
19375-
* This makes pairs like '0 vs unique ID', 'unique ID vs 0' valid.
19390+
/*
19391+
* Compare scalar register IDs for state equivalence.
19392+
*
19393+
* When old_id == 0, the old register is independent - not linked to any
19394+
* other register. Any linking in the current state only adds constraints,
19395+
* making it more restrictive. Since the old state didn't rely on any ID
19396+
* relationships for this register, it's always safe to accept cur regardless
19397+
* of its ID. Hence, return true immediately.
19398+
*
19399+
* When old_id != 0 but cur_id == 0, we need to ensure that different
19400+
* independent registers in cur don't incorrectly satisfy the ID matching
19401+
* requirements of linked registers in old.
19402+
*
19403+
* Example: if old has r6.id=X and r7.id=X (linked), but cur has r6.id=0
19404+
* and r7.id=0 (both independent), without temp IDs both would map old_id=X
19405+
* to cur_id=0 and pass. With temp IDs: r6 maps X->temp1, r7 tries to map
19406+
* X->temp2, but X is already mapped to temp1, so the check fails correctly.
1937619407
*/
1937719408
static bool check_scalar_ids(u32 old_id, u32 cur_id, struct bpf_idmap *idmap)
1937819409
{
19379-
old_id = old_id ? old_id : ++idmap->tmp_id_gen;
19410+
if (!old_id)
19411+
return true;
19412+
1938019413
cur_id = cur_id ? cur_id : ++idmap->tmp_id_gen;
1938119414

1938219415
return check_ids(old_id, cur_id, idmap);
@@ -19450,6 +19483,72 @@ static void clean_verifier_state(struct bpf_verifier_env *env,
1945019483
* doesn't meant that the states are DONE. The verifier has to compare
1945119484
* the callsites
1945219485
*/
19486+
19487+
/* Find id in idset and increment its count, or add new entry */
19488+
static void idset_cnt_inc(struct bpf_idset *idset, u32 id)
19489+
{
19490+
u32 i;
19491+
19492+
for (i = 0; i < idset->num_ids; i++) {
19493+
if (idset->entries[i].id == id) {
19494+
idset->entries[i].cnt++;
19495+
return;
19496+
}
19497+
}
19498+
/* New id */
19499+
if (idset->num_ids < BPF_ID_MAP_SIZE) {
19500+
idset->entries[idset->num_ids].id = id;
19501+
idset->entries[idset->num_ids].cnt = 1;
19502+
idset->num_ids++;
19503+
}
19504+
}
19505+
19506+
/* Find id in idset and return its count, or 0 if not found */
19507+
static u32 idset_cnt_get(struct bpf_idset *idset, u32 id)
19508+
{
19509+
u32 i;
19510+
19511+
for (i = 0; i < idset->num_ids; i++) {
19512+
if (idset->entries[i].id == id)
19513+
return idset->entries[i].cnt;
19514+
}
19515+
return 0;
19516+
}
19517+
19518+
/*
19519+
* Clear singular scalar ids in a state.
19520+
* A register with a non-zero id is called singular if no other register shares
19521+
* the same base id. Such registers can be treated as independent (id=0).
19522+
*/
19523+
static void clear_singular_ids(struct bpf_verifier_env *env,
19524+
struct bpf_verifier_state *st)
19525+
{
19526+
struct bpf_idset *idset = &env->idset_scratch;
19527+
struct bpf_func_state *func;
19528+
struct bpf_reg_state *reg;
19529+
19530+
idset->num_ids = 0;
19531+
19532+
bpf_for_each_reg_in_vstate(st, func, reg, ({
19533+
if (reg->type != SCALAR_VALUE)
19534+
continue;
19535+
if (!reg->id)
19536+
continue;
19537+
idset_cnt_inc(idset, reg->id & ~BPF_ADD_CONST);
19538+
}));
19539+
19540+
bpf_for_each_reg_in_vstate(st, func, reg, ({
19541+
if (reg->type != SCALAR_VALUE)
19542+
continue;
19543+
if (!reg->id)
19544+
continue;
19545+
if (idset_cnt_get(idset, reg->id & ~BPF_ADD_CONST) == 1) {
19546+
reg->id = 0;
19547+
reg->off = 0;
19548+
}
19549+
}));
19550+
}
19551+
1945319552
static void clean_live_states(struct bpf_verifier_env *env, int insn,
1945419553
struct bpf_verifier_state *cur)
1945519554
{
@@ -19535,11 +19634,21 @@ static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
1953519634
}
1953619635
if (!rold->precise && exact == NOT_EXACT)
1953719636
return true;
19538-
if ((rold->id & BPF_ADD_CONST) != (rcur->id & BPF_ADD_CONST))
19539-
return false;
19540-
if ((rold->id & BPF_ADD_CONST) && (rold->off != rcur->off))
19541-
return false;
19542-
/* Why check_ids() for scalar registers?
19637+
/*
19638+
* Linked register tracking uses rold->id to detect relationships.
19639+
* When rold->id == 0, the register is independent and any linking
19640+
* in rcur only adds constraints. When rold->id != 0, we must verify
19641+
* id mapping and (for BPF_ADD_CONST) offset consistency.
19642+
*
19643+
* +------------------+-----------+------------------+---------------+
19644+
* | | rold->id | rold + ADD_CONST | rold->id == 0 |
19645+
* |------------------+-----------+------------------+---------------|
19646+
* | rcur->id | range,ids | false | range |
19647+
* | rcur + ADD_CONST | false | range,ids,off | range |
19648+
* | rcur->id == 0 | range,ids | false | range |
19649+
* +------------------+-----------+------------------+---------------+
19650+
*
19651+
* Why check_ids() for scalar registers?
1954319652
*
1954419653
* Consider the following BPF code:
1954519654
* 1: r6 = ... unbound scalar, ID=a ...
@@ -19563,9 +19672,22 @@ static bool regsafe(struct bpf_verifier_env *env, struct bpf_reg_state *rold,
1956319672
* ---
1956419673
* Also verify that new value satisfies old value range knowledge.
1956519674
*/
19566-
return range_within(rold, rcur) &&
19567-
tnum_in(rold->var_off, rcur->var_off) &&
19568-
check_scalar_ids(rold->id, rcur->id, idmap);
19675+
19676+
/* ADD_CONST mismatch: different linking semantics */
19677+
if ((rold->id & BPF_ADD_CONST) && !(rcur->id & BPF_ADD_CONST))
19678+
return false;
19679+
19680+
if (rold->id && !(rold->id & BPF_ADD_CONST) && (rcur->id & BPF_ADD_CONST))
19681+
return false;
19682+
19683+
/* Both have offset linkage: offsets must match */
19684+
if ((rold->id & BPF_ADD_CONST) && rold->off != rcur->off)
19685+
return false;
19686+
19687+
if (!check_scalar_ids(rold->id, rcur->id, idmap))
19688+
return false;
19689+
19690+
return range_within(rold, rcur) && tnum_in(rold->var_off, rcur->var_off);
1956919691
case PTR_TO_MAP_KEY:
1957019692
case PTR_TO_MAP_VALUE:
1957119693
case PTR_TO_MEM:
@@ -20448,6 +20570,8 @@ static int is_state_visited(struct bpf_verifier_env *env, int insn_idx)
2044820570
if (env->bpf_capable)
2044920571
mark_all_scalars_imprecise(env, cur);
2045020572

20573+
clear_singular_ids(env, cur);
20574+
2045120575
/* add new state to the head of linked list */
2045220576
new = &new_sl->state;
2045320577
err = copy_verifier_state(new, cur);

tools/testing/selftests/bpf/progs/verifier_scalar_ids.c

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,51 @@ __naked void ignore_unique_scalar_ids_old(void)
715715
: __clobber_all);
716716
}
717717

718+
/* Check that two registers with 0 scalar IDs in a verified state can be mapped
719+
* to the same scalar ID in current state.
720+
*/
721+
SEC("socket")
722+
__success __log_level(2)
723+
/* The states should be equivalent on reaching insn 12.
724+
*/
725+
__msg("12: safe")
726+
__msg("processed 17 insns")
727+
__flag(BPF_F_TEST_STATE_FREQ)
728+
__naked void two_nil_old_ids_one_cur_id(void)
729+
{
730+
asm volatile (
731+
/* Give unique scalar IDs to r{6,7} */
732+
"call %[bpf_ktime_get_ns];"
733+
"r0 &= 0xff;"
734+
"r6 = r0;"
735+
"r6 *= 1;"
736+
"call %[bpf_ktime_get_ns];"
737+
"r0 &= 0xff;"
738+
"r7 = r0;"
739+
"r7 *= 1;"
740+
"r0 = 0;"
741+
/* Maybe make r{6,7} IDs identical */
742+
"if r6 > r7 goto l0_%=;"
743+
"goto l1_%=;"
744+
"l0_%=:"
745+
"r6 = r7;"
746+
"l1_%=:"
747+
/* Mark r{6,7} precise.
748+
* Get here in two states:
749+
* - first: r6{.id=0}, r7{.id=0} (cached state)
750+
* - second: r6{.id=A}, r7{.id=A}
751+
* Verifier considers such states equivalent.
752+
* Thus "exit;" would be verified only once.
753+
*/
754+
"r2 = r10;"
755+
"r2 += r6;"
756+
"r2 += r7;"
757+
"exit;"
758+
:
759+
: __imm(bpf_ktime_get_ns)
760+
: __clobber_all);
761+
}
762+
718763
/* Check that two different scalar IDs in a verified state can't be
719764
* mapped to the same scalar ID in current state.
720765
*/
@@ -723,9 +768,9 @@ __success __log_level(2)
723768
/* The exit instruction should be reachable from two states,
724769
* use two matches and "processed .. insns" to ensure this.
725770
*/
726-
__msg("13: (95) exit")
727-
__msg("13: (95) exit")
728-
__msg("processed 18 insns")
771+
__msg("15: (95) exit")
772+
__msg("15: (95) exit")
773+
__msg("processed 20 insns")
729774
__flag(BPF_F_TEST_STATE_FREQ)
730775
__naked void two_old_ids_one_cur_id(void)
731776
{
@@ -734,9 +779,11 @@ __naked void two_old_ids_one_cur_id(void)
734779
"call %[bpf_ktime_get_ns];"
735780
"r0 &= 0xff;"
736781
"r6 = r0;"
782+
"r8 = r0;"
737783
"call %[bpf_ktime_get_ns];"
738784
"r0 &= 0xff;"
739785
"r7 = r0;"
786+
"r9 = r0;"
740787
"r0 = 0;"
741788
/* Maybe make r{6,7} IDs identical */
742789
"if r6 > r7 goto l0_%=;"

0 commit comments

Comments
 (0)