From nobody Fri Sep 25 16:01:34 2026 Received: from mta0.migadu.com (out-198.mta0.migadu.com [91.218.175.198]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id DFD504AB1C2 for ; Thu, 10 Sep 2026 16:46:59 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.198 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058824; cv=none; b=M0EhN5BYG75mRe3qBlVe9ukjqwIsNzPplFDaQGOUL22o5wfvL9ay1Adxwd/UM7WA4RMX3PTrSLiX0jX4wmaG5ey8PfjzOeP0mzPMcB9sYDkuOcroJcEuOufkWKcGQ+wZXcyqndqwobSTtlzM4giMJ9c9aVYdi0x3mjv1qAc/gH4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058824; c=relaxed/simple; bh=nqFQ9oxsGrAj7hA8jLiWqmOy+bWh5bP0VEzW3mMQKO0=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=DoKBrVYdUyUHpWcJtU2XZBX1fTxAoPWQHJBuuHjQLyXlnHl1d/Tatl5dfc2xVWVZvxIl9XukhntvCSu49yQAZntSYoIFA4Sx6r+LO41Ovl8bW25fykBrhC1HJC4VzHAPvlVC7JtmWGMzjCTwnPUecCaWGg0VW/4+MgKHBkRx3mE= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=SdEaH6Ms; arc=none smtp.client-ip=91.218.175.198 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="SdEaH6Ms" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=nqFQ9oxsGrAj7hA8jLiWqmOy+bWh5bP0VEzW3mMQKO0=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789058815; v=1; x=1789663615; b=SdEaH6Ms+TFgPEIXCdBVWNLAzNXYNdSm//u7+C2QqJ31PYU8nfpGehMttvrYRMdnLrRQ89Xn tedDGKOSSkOMWBW/hOxlm8cjgi9W+g0zt1cxBPFT10SoKLgii7XuYotx7HlFi/7GF0kUddZSI/p myovJYmPYdO7l6B8Ct34KkY4= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id c5233e1d0dae7e4e; Thu, 10 Sep 2026 16:46:54 +0000 X-Mizu-Trace-ID: c5233e1d0dae7e4e X-Migadu-Flow: FLOW_OUT From: Vineet Gupta To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org, eddyz87@gmail.com, memxor@gmail.com Cc: martin.lau@linux.dev, song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org, emil@etsalapatis.com, ihor.solodrai@linux.dev, john.fastabend@gmail.com, shuah@kernel.org, bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Vineet Gupta Subject: [PATCH bpf-next v2 01/13] bpf: move linked-scalar flags out of bpf_reg_state->id [NFC] Date: Thu, 10 Sep 2026 22:16:23 +0530 Message-ID: <20260910164635.459558-2-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260910164635.459558-1-vineet.gupta@linux.dev> References: <20260910164635.459558-1-vineet.gupta@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" bpf_reg_state->id doubles as a linked-register id and, in its top two bits, as a record of how the register relates to that set: #define BPF_ADD_CONST64 (1U << 31) #define BPF_ADD_CONST32 (1U << 30) Every user of ->id therefore has to mask, and more link kinds are coming. Move the two bits into a bitfield next to ->precise, which is the last field of the struct and outside every memcmp() window used for state comparison, so the layout and all byte-wise comparisons are unchanged. The two kinds are mutually exclusive, so a 2-bit enum captures them and makes ADD_CONST_32 vs ADD_CONST_64 explicit at each use. ->id becomes a plain 32-bit identifier: no masking anywhere, and check_scalar_ids() loses its two-level "check the compound id, then the base id" dance in favour of a single check_ids(). While here, use regs_exact() for the explore_alu_limits case in regsafe(): it is what that open-coded memcmp+check_scalar_ids pair amounts to, and it picks up the add_const comparison for free (parent_id is 0 for SCALAR_VALUE). check_stack_write_fixed_off() cleared ->id directly on a narrowing spill, which would now leave ->add_const set without an id; use clear_scalar_id(). Moving the kind out of ->id also drops an incidental comparison in regs_exact(), which used to see it as part of the idmap key; the next patch restores it. Otherwise no functional change intended. Suggested-by: Eduard Zingerman Signed-off-by: Vineet Gupta --- v2: was RFC 2/6. - kinds are a 2-bit enum bitfield, not a byte of flags; RFC 1/6, which turned ->precise into that byte, is dropped (Eduard) - use regs_exact() for the explore_alu_limits case - clear_scalar_id() on the narrowing spill, which would otherwise leave ->add_const set without an id include/linux/bpf_verifier.h | 25 ++++++++----- kernel/bpf/log.c | 4 +-- kernel/bpf/states.c | 35 +++++-------------- kernel/bpf/verifier.c | 35 +++++++++++-------- .../bpf/progs/verifier_linked_scalars.c | 34 +++++++++--------- 5 files changed, 65 insertions(+), 68 deletions(-) diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h index 9727df5af83a..afb1e5628698 100644 --- a/include/linux/bpf_verifier.h +++ b/include/linux/bpf_verifier.h @@ -35,6 +35,17 @@ enum bpf_iter_state { BPF_ITER_STATE_DRAINED, }; =20 +/* + * Records that a register is (base + ->delta) within its ->id set: + * r1 +=3D 10; r1 gets ADD_CONST_64 delta + * w3 +=3D 10; r3 gets ADD_CONST_32 delta + */ +enum bpf_add_const { + ADD_CONST_NONE =3D 0, + ADD_CONST_32, /* delta was added with a 32-bit ALU op */ + ADD_CONST_64, /* ... with a 64-bit ALU op */ +}; + struct bpf_reg_state { /* Ordering of fields matters. See states_equal() */ enum bpf_reg_type type; @@ -136,16 +147,9 @@ struct bpf_reg_state { * to a specific instance of bpf_iter. */ /* - * Upper bit of ID is used to remember relationship between "linked" - * registers. Example: + * Registers sharing an ->id are "linked": * r1 =3D r2; both will have r1->id =3D=3D r2->id =3D=3D N - * r1 +=3D 10; r1->id =3D=3D N | BPF_ADD_CONST and r1->delta =3D=3D 10 - * r3 =3D r2; both will have r3->id =3D=3D r2->id =3D=3D N - * w3 +=3D 10; r3->id =3D=3D N | BPF_ADD_CONST32 and r3->delta =3D=3D 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. @@ -164,6 +168,11 @@ struct bpf_reg_state { u32 frameno; /* if (!precise && SCALAR_VALUE) min/max/tnum don't affect safety */ bool precise; + /* + * How this register relates to the others sharing its ->id. + * Non-zero only if ->id is. + */ + enum bpf_add_const add_const:2; }; =20 static inline s64 reg_smin(const struct bpf_reg_state *reg) diff --git a/kernel/bpf/log.c b/kernel/bpf/log.c index fb032dfdc0de..f8d7a5c8052f 100644 --- a/kernel/bpf/log.c +++ b/kernel/bpf/log.c @@ -651,8 +651,8 @@ static void print_reg_state(struct bpf_verifier_env *en= v, verbose(env, "%s", btf_type_name(reg->btf, reg->btf_id)); verbose(env, "("); if (reg->id) - verbose_a("id=3D%d", reg->id & ~BPF_ADD_CONST); - if (reg->id & BPF_ADD_CONST) + verbose_a("id=3D%d", reg->id); + if (reg->add_const) verbose(env, "%+d", reg->delta); if (reg->parent_id) verbose_a("parent_id=3D%d", reg->parent_id); diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c index 66fb11b6c6a7..d974baad37ee 100644 --- a/kernel/bpf/states.c +++ b/kernel/bpf/states.c @@ -369,13 +369,6 @@ static bool check_ids(u32 old_id, u32 cur_id, struct b= pf_idmap *idmap) * and r7.id=3D0 (both independent), without temp IDs both would map old_i= d=3DX * to cur_id=3D0 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 correctl= y. - * - * 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=3DA, r3.id=3DA|flag (r3 =3D r2 + delta), cur has r2.id=3DB, r3.id= =3DC|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 *idm= ap) { @@ -384,15 +377,7 @@ static bool check_scalar_ids(u32 old_id, u32 cur_id, s= truct bpf_idmap *idmap) =20 cur_id =3D cur_id ? cur_id : ++idmap->tmp_id_gen; =20 - if (!check_ids(old_id, cur_id, idmap)) - return false; - if (old_id & BPF_ADD_CONST) { - old_id &=3D ~BPF_ADD_CONST; - cur_id &=3D ~BPF_ADD_CONST; - if (!check_ids(old_id, cur_id, idmap)) - return false; - } - return true; + return check_ids(old_id, cur_id, idmap); } =20 static void __clean_func_state(struct bpf_verifier_env *env, @@ -542,8 +527,7 @@ static bool regsafe(struct bpf_verifier_env *env, struc= t 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)) =3D=3D 0 = && - check_scalar_ids(rold->id, rcur->id, idmap); + return regs_exact(rold, rcur, idmap); } if (!rold->precise && exact =3D=3D NOT_EXACT) return true; @@ -551,7 +535,8 @@ static bool regsafe(struct bpf_verifier_env *env, struc= t bpf_reg_state *rold, * Linked register tracking uses rold->id to detect relationships. * When rold->id =3D=3D 0, the register is independent and any linking * in rcur only adds constraints. When rold->id !=3D 0, we must verify - * id mapping and (for BPF_ADD_CONST) offset consistency. + * id mapping and that the link kinds agree, along with any delta + * they carry. * * +------------------+-----------+------------------+---------------+ * | | rold->id | rold + ADD_CONST | rold->id =3D=3D 0= | @@ -587,17 +572,15 @@ static bool regsafe(struct bpf_verifier_env *env, str= uct bpf_reg_state *rold, */ =20 /* - * 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 pruning across different flag types is unsafe. + * The link kinds must match: alu32 and alu64 adds have different + * linking semantics in sync_linked_regs() (alu32 zero-extends, + * alu64 does not), so pruning across them is unsafe. */ - if (rold->id && - (rold->id & BPF_ADD_CONST) !=3D (rcur->id & BPF_ADD_CONST)) + if (rold->id && rold->add_const !=3D rcur->add_const) return false; =20 /* Both have offset linkage: offsets must match */ - if ((rold->id & BPF_ADD_CONST) && rold->delta !=3D rcur->delta) + if (rold->add_const && rold->delta !=3D rcur->delta) return false; =20 if (!check_scalar_ids(rold->id, rcur->id, idmap)) diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 9e79750e2480..0ca229f6e7ac 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -1907,6 +1907,7 @@ static void __mark_reg_known(struct bpf_reg_state *re= g, u64 imm) offsetof(struct bpf_reg_state, var_off) - sizeof(reg->type)); reg->id =3D 0; reg->parent_id =3D 0; + reg->add_const =3D ADD_CONST_NONE; ___mark_reg_known(reg, imm); } =20 @@ -3480,6 +3481,7 @@ static void clear_scalar_id(struct bpf_reg_state *reg) { reg->id =3D 0; reg->delta =3D 0; + reg->add_const =3D ADD_CONST_NONE; } =20 static void assign_scalar_id_before_mov(struct bpf_verifier_env *env, @@ -3492,7 +3494,7 @@ static void assign_scalar_id_before_mov(struct bpf_ve= rifier_env *env, * rY->id has special linked register already. * Cleared it, since multiple rX +=3D const are not supported. */ - if (src_reg->id & BPF_ADD_CONST) + if (src_reg->add_const) clear_scalar_id(src_reg); /* * Ensure that src_reg has a valid ID that will be copied to @@ -3642,7 +3644,7 @@ static int check_stack_write_fixed_off(struct bpf_ver= ifier_env *env, save_register_state(env, state, spi, reg, size); /* Break the relation on a narrowing spill. */ if (!reg_value_fits) - state->stack[spi].spilled_ptr.id =3D 0; + clear_scalar_id(&state->stack[spi].spilled_ptr); } else if (!reg && !(off % BPF_REG_SIZE) && is_bpf_st_mem(insn) && env->bpf_capable) { struct bpf_reg_state *tmp_reg =3D &env->fake_reg[0]; @@ -16117,7 +16119,7 @@ static int adjust_reg_min_max_vals(struct bpf_verif= ier_env *env, off =3D -off; } =20 - if (dst_reg->id & BPF_ADD_CONST) { + if (dst_reg->add_const) { /* * If the register already went through rX +=3D val * we cannot accumulate another val into rx->off. @@ -16126,9 +16128,9 @@ static int adjust_reg_min_max_vals(struct bpf_verif= ier_env *env, clear_scalar_id(dst_reg); } else { if (alu32) - dst_reg->id |=3D BPF_ADD_CONST32; + dst_reg->add_const =3D ADD_CONST_32; else - dst_reg->id |=3D BPF_ADD_CONST64; + dst_reg->add_const =3D ADD_CONST_64; dst_reg->delta =3D off; } } else { @@ -17067,7 +17069,7 @@ static void __collect_linked_regs(struct linked_reg= s *reg_set, struct bpf_reg_st { struct linked_reg *e; =20 - if (reg->type !=3D SCALAR_VALUE || (reg->id & ~BPF_ADD_CONST) !=3D id) + if (reg->type !=3D SCALAR_VALUE || reg->id !=3D id) return; =20 e =3D linked_regs_push(reg_set); @@ -17095,7 +17097,6 @@ static void collect_linked_regs(struct bpf_verifier= _env *env, u16 live_regs; int i, j; =20 - id =3D id & ~BPF_ADD_CONST; for (i =3D vstate->curframe; i >=3D 0; i--) { live_regs =3D aux[bpf_frame_insn_idx(vstate, i)].live_regs_before; func =3D vstate->frame[i]; @@ -17131,18 +17132,20 @@ static void sync_linked_regs(struct bpf_verifier_= env *env, struct bpf_verifier_s : &vstate->frame[e->frameno]->stack[e->spi].spilled_ptr; if (reg->type !=3D SCALAR_VALUE || reg =3D=3D known_reg) continue; - if ((reg->id & ~BPF_ADD_CONST) !=3D (known_reg->id & ~BPF_ADD_CONST)) + if (reg->id !=3D known_reg->id) 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) =3D=3D BPF_ADD_CONST) + if (reg->add_const && known_reg->add_const && + reg->add_const !=3D known_reg->add_const) continue; - if ((!(reg->id & BPF_ADD_CONST) && !(known_reg->id & BPF_ADD_CONST)) || + if ((!reg->add_const && !known_reg->add_const) || reg->delta =3D=3D known_reg->delta) { *reg =3D *known_reg; } else { + enum bpf_add_const saved_add_const =3D reg->add_const; s32 saved_off =3D reg->delta; u32 saved_id =3D reg->id; =20 @@ -17152,16 +17155,18 @@ static void sync_linked_regs(struct bpf_verifier_= env *env, struct bpf_verifier_s /* reg =3D known_reg; reg +=3D delta */ *reg =3D *known_reg; /* - * Must preserve off and id, otherwise another sync_linked_regs() - * will be incorrect. + * Must preserve off, id and add_const, otherwise another + * sync_linked_regs() will be incorrect. */ reg->delta =3D saved_off; reg->id =3D saved_id; + reg->add_const =3D saved_add_const; =20 scalar32_min_max_add(reg, &fake_reg); scalar_min_max_add(reg, &fake_reg); reg->var_off =3D tnum_add(reg->var_off, fake_reg.var_off); - if ((reg->id | known_reg->id) & BPF_ADD_CONST32) + if (reg->add_const =3D=3D ADD_CONST_32 || + known_reg->add_const =3D=3D ADD_CONST_32) zext_32_to_64(reg); reg_bounds_sync(reg); } @@ -18255,7 +18260,7 @@ void bpf_clear_singular_ids(struct bpf_verifier_env= *env, continue; if (!reg->id) continue; - idset_cnt_inc(idset, reg->id & ~BPF_ADD_CONST); + idset_cnt_inc(idset, reg->id); })); =20 bpf_for_each_reg_in_vstate(st, func, reg, ({ @@ -18263,7 +18268,7 @@ void bpf_clear_singular_ids(struct bpf_verifier_env= *env, continue; if (!reg->id) continue; - if (idset_cnt_get(idset, reg->id & ~BPF_ADD_CONST) =3D=3D 1) + if (idset_cnt_get(idset, reg->id) =3D=3D 1) clear_scalar_id(reg); })); } diff --git a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c b/= tools/testing/selftests/bpf/progs/verifier_linked_scalars.c index d571fbfc86a3..da6cb961a520 100644 --- a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c +++ b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c @@ -349,8 +349,8 @@ l0_%=3D: \ } =20 /* - * Test that sync_linked_regs() checks reg->id (the linked target register) - * for BPF_ADD_CONST32 rather than known_reg->id (the branch register). + * Test that sync_linked_regs() checks the linked target register (reg) for + * ADD_CONST_32 rather than the branch register (known_reg). */ SEC("socket") __success @@ -360,7 +360,7 @@ __naked void scalars_alu32_zext_linked_reg(void) call %[bpf_get_prandom_u32]; \ w6 =3D w0; /* r6 in [0, 0xFFFFFFFF] */ \ r7 =3D r6; /* linked: same id as r6 */ \ - w7 +=3D 1; /* alu32: r7.id |=3D BPF_ADD_CONST32 */ \ + w7 +=3D 1; /* ADD_CONST_32 delta */ \ r8 =3D 0xFFFFffff ll; \ if r6 < r8 goto l0_%=3D; \ /* r6 in [0xFFFFFFFF, 0xFFFFFFFF] */ \ @@ -381,7 +381,7 @@ l0_%=3D: \ =20 /* * Test that sync_linked_regs() skips propagation when one register used - * alu32 (BPF_ADD_CONST32) and the other used alu64 (BPF_ADD_CONST64). + * alu32 (ADD_CONST_32) and the other used alu64 (ADD_CONST_64). * The delta relationship doesn't hold across different ALU widths. */ SEC("socket") @@ -392,13 +392,13 @@ __naked void scalars_alu32_alu64_cross_type(void) call %[bpf_get_prandom_u32]; \ w6 =3D w0; /* r6 in [0, 0xFFFFFFFF] */ \ r7 =3D r6; /* linked: same id as r6 */ \ - w7 +=3D 1; /* alu32: BPF_ADD_CONST32, delta =3D 1 */ \ + w7 +=3D 1; /* ADD_CONST_32 delta */ \ r8 =3D r6; /* linked: same id as r6 */ \ - r8 +=3D 2; /* alu64: BPF_ADD_CONST64, delta =3D 2 */ \ + r8 +=3D 2; /* ADD_CONST_64 delta */ \ r9 =3D 0xFFFFffff ll; \ if r7 < r9 goto l0_%=3D; \ /* r7 =3D 0xFFFFFFFF */ \ - /* sync: known_reg=3Dr7 (ADD_CONST32), reg=3Dr8 (ADD_CONST64) */ \ + /* sync: known_reg=3Dr7 (ADD_CONST_32), reg=3Dr8 (ADD_CONST_64) */ \ /* Without fix: r8 =3D zext(0xFFFFFFFF + 1) =3D 0 */ \ /* With fix: r8 stays [2, 0x100000001] (r8 >=3D 2) */ \ if r8 > 0 goto l1_%=3D; \ @@ -416,7 +416,7 @@ l0_%=3D: \ /* * Test that regsafe() prevents pruning when two paths reach the same prog= ram * point with linked registers carrying different ADD_CONST flags (one - * BPF_ADD_CONST32 from alu32, another BPF_ADD_CONST64 from alu64). + * ADD_CONST_32 from alu32, another ADD_CONST_64 from alu64). */ SEC("socket") __failure __msg("div by zero") @@ -431,11 +431,11 @@ __naked void scalars_alu32_alu64_regsafe_pruning(void) call %[bpf_get_prandom_u32]; \ if r0 > 0 goto l_pathb_%=3D; \ /* Path A: alu32 */ \ - w7 +=3D 1; /* BPF_ADD_CONST32, delta =3D 1 */\ + w7 +=3D 1; /* ADD_CONST_32 delta */\ goto l_merge_%=3D; \ l_pathb_%=3D: \ /* Path B: alu64 */ \ - r7 +=3D 1; /* BPF_ADD_CONST64, delta =3D 1 */\ + r7 +=3D 1; /* ADD_CONST_64 delta */\ l_merge_%=3D: \ /* Merge point: regsafe() compares path B against cached path A. */ \ /* Narrow r6 to trigger sync_linked_regs for r7 */ \ @@ -593,7 +593,7 @@ l_exit_%=3D: \ } =20 /* - * Test that stale delta from a cleared BPF_ADD_CONST does not leak + * Test that stale delta from a cleared ADD_CONST_* does not leak * through assign_scalar_id_before_mov() into a new id, causing * sync_linked_regs() to compute an incorrect offset. */ @@ -605,10 +605,10 @@ __naked void scalars_stale_delta_from_cleared_id(void) asm volatile (" \ call %[bpf_get_prandom_u32]; \ r6 =3D r0; /* r6 unknown, gets id A */ \ - r6 +=3D 5; /* id A|ADD_CONST, delta 5 */ \ + r6 +=3D 5; /* id A, ADD_CONST_64 delta */ \ r6 ^=3D 0; /* id cleared; delta stays 5 */ \ r8 =3D r6; /* new id B, stale delta 5 */ \ - r8 +=3D 3; /* id B|ADD_CONST, delta 3 */ \ + r8 +=3D 3; /* id B, ADD_CONST_64 delta */ \ r9 =3D r6; /* id B, stale delta 5 */ \ if r9 !=3D 10 goto l_exit_%=3D; \ /* Bug: r8 =3D 10+(3-5) =3D 8; Fix: r8 =3D 10+(3-0) =3D 13 */ \ @@ -648,10 +648,10 @@ l_exit_%=3D: \ } =20 /* - * Test that regsafe() verifies base_id consistency for BPF_ADD_CONST + * Test that regsafe() verifies base_id consistency for ADD_CONST_* * linked scalars during state pruning. * - * The false branch (explored first) links R3 to R2 via ADD_CONST. + * The false branch (explored first) links R3 to R2 via ADD_CONST_64. * The true branch (runtime path) links R3 to R4 (unrelated base_id). * At the merge point, pruning must fail because the linkage topology * differs. @@ -675,7 +675,7 @@ __naked void add_const_base_id_pruning(void) r2 =3D r0; \ r2 &=3D 0xff; /* R2 =3D scalar(id=3DA) [0,255] */ \ r3 =3D r2; /* R3 linked to R2 (id=3DA) */ \ - r3 +=3D 10; /* R3 id=3DA|ADD_CONST, delta=3D10 */\ + r3 +=3D 10; /* ADD_CONST_64 delta */\ r6 =3D 0; \ goto l_merge_%=3D; \ \ @@ -687,7 +687,7 @@ l_true_%=3D: \ r4 =3D r0; \ r4 &=3D 0xff; /* R4 =3D scalar [0,255], id=3D0 */ \ r3 =3D r4; /* R3 linked to R4 (new id=3DC) */\ - r3 +=3D 10; /* R3 id=3DC|ADD_CONST, delta=3D10 */\ + r3 +=3D 10; /* ADD_CONST_64 delta */\ r6 =3D 0; \ \ l_merge_%=3D: \ --=20 2.53.0-Meta From nobody Fri Sep 25 16:01:34 2026 Received: from mta0.migadu.com (out-208.mta0.migadu.com [91.218.175.208]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8CA003537F9 for ; Thu, 10 Sep 2026 16:47:04 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.208 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058826; cv=none; b=dVUILsq3ZJ8r+fEA1VPXiNMKryHfR9PKIlqXskDpBwwSHKS+xFwPZTsHWbm5xmqZewotpMWMQ/TR62D3kRKx8znG7LfyFp/zwsKlJwDZ8FbB5KhrpXT6tP7NCU+7sBvnWKGGjQTAri+J90JAGwwiZe4jAyZQVv7fF35LDG3Qm8Q= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058826; c=relaxed/simple; bh=5rOkBrAnAGddpfTIFywUT/GOQ42Lh1Wz2ZM+WRYQXb8=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=G6YX91NiSH5Y4o0u3Y8S+a9cO4EaJ40orBvlnAJ2ODJtWA8b8pv6uhE1WSXIDinxY0x4XNm8twxqt0hovqqhanbjgQMBi6No5BchWzNG7wC6AZli6mJWbngb+NTStoXHhv4Lhe5g8nbYdU9y99QrCyRFGmuP/lQ67DeTS79cPZI= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=HUmasok3; arc=none smtp.client-ip=91.218.175.208 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="HUmasok3" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=5rOkBrAnAGddpfTIFywUT/GOQ42Lh1Wz2ZM+WRYQXb8=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789058822; v=1; x=1789663622; b=HUmasok33Sis1DKbJF+4lp9Kx3GzS6YvaRQYpIGtcy90u8wJXS0KrxsfxhOF6Y5C/27BIChW VjFei/Z4IwPI6rc+34JIIwXjFmC3v/tkOzkYp92AohkLReIEbju1bx42w7pk6omh5U6xcfvNcKr OrzTJZNL+WQKJhbZUurFZwtU= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id d6fa58a636dcf58b; Thu, 10 Sep 2026 16:47:02 +0000 X-Mizu-Trace-ID: d6fa58a636dcf58b X-Migadu-Flow: FLOW_OUT From: Vineet Gupta To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org, eddyz87@gmail.com, memxor@gmail.com Cc: martin.lau@linux.dev, song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org, emil@etsalapatis.com, ihor.solodrai@linux.dev, john.fastabend@gmail.com, shuah@kernel.org, bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Vineet Gupta Subject: [PATCH bpf-next v2 02/13] bpf: compare linked-scalar kinds in regs_exact() Date: Thu, 10 Sep 2026 22:16:24 +0530 Message-ID: <20260910164635.459558-3-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260910164635.459558-1-vineet.gupta@linux.dev> References: <20260910164635.459558-1-vineet.gupta@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" regs_exact() memcmp()s up to offsetof(id), so it does not see ->add_const, which sits past ->frameno. Compare it explicitly. Two cases go from accepted to rejected: old {r1.id=3DA, r2.id=3DA+delta} vs cur {r1.id=3DB, r2.id=3DB} old {r2.id=3DA+delta32} vs cur {r2.id=3DB+delta64} The first was rejected before this series -- the kind lived in ->id, so A+delta was a distinct idmap key -- and the previous patch lost that. The second was accepted before the series too, which is wrong: alu32 and alu64 deltas differ in sync_linked_regs(), so the states are not interchangeable. ->delta is inside the memcmp() window and is cleared together with the kind, so either case needs a kind with delta 0, e.g. "r6 =3D r5; r6 +=3D 0". Only states_equal(..., EXACT) and env->explore_alu_limits reach regs_exact() for a scalar; regsafe() compares kinds itself on the ordinary path. Neither is easy to drive from a test program, so no selftest. Signed-off-by: Vineet Gupta --- v2: new. Splits out the comparison RFC 3/6 lost when the kind left ->id, which was raised on that patch. kernel/bpf/states.c | 1 + 1 file changed, 1 insertion(+) diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c index d974baad37ee..5505d7aaeed0 100644 --- a/kernel/bpf/states.c +++ b/kernel/bpf/states.c @@ -475,6 +475,7 @@ static bool regs_exact(const struct bpf_reg_state *rold, struct bpf_idmap *idmap) { return memcmp(rold, rcur, offsetof(struct bpf_reg_state, id)) =3D=3D 0 && + rold->add_const =3D=3D rcur->add_const && check_ids(rold->id, rcur->id, idmap) && check_ids(rold->parent_id, rcur->parent_id, idmap); } --=20 2.53.0-Meta From nobody Fri Sep 25 16:01:34 2026 Received: from mta0.migadu.com (out-217.mta0.migadu.com [91.218.175.217]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id EE19D519939 for ; Thu, 10 Sep 2026 16:47:08 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.217 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058832; cv=none; b=UOIdfs8MCUiQCyZt82286RHSGeTnguq11VRuvSR208kWUwKqZW00Mqf+9vUPm1TQ6KQwnaa2WjEBc8jB8PPkgj3n/d7Tt2pInPq1W7V9bN1e5VXn6+9JpOzRuEzduXRpvBZt0exXX4Nz8I8upqQOWyKF9Fu56kMFh50lm+vz37Y= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058832; c=relaxed/simple; bh=jcdTnpz3K2nPgGTSv8fwyNJKhwmW5PkbJFINKVaj1Uk=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=a2ZM/PQP3VokVw6DQnBASsSsCjVBA1KvJ98ZRYQyRwe5PNvb0iH+fJ8H/kKdONvXKuY9+wiS8V1kEc3JXKLInC08jMdRyCG30ovy+t2g46wqdBvbzyfLOordZ3FPvZ+2J5lmog11cxalX4okFR50pgz3OGG4xSpyv1mM53t6dZg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=qqVF3r4q; arc=none smtp.client-ip=91.218.175.217 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="qqVF3r4q" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=jcdTnpz3K2nPgGTSv8fwyNJKhwmW5PkbJFINKVaj1Uk=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789058826; v=1; x=1789663626; b=qqVF3r4qLOcDrf9mpmqwaD2zH6hMLm+IyNLmGq8uBWS3wUuX/62AdG1+vAa+V/jv5XntyR9h H0qd+xmqcXeaHwxaRuMGqEoMZwlibCVthU1fCXHW038I0fudI7LsZPB5vJa/VnVYmPkbT+lC+nx pOeiyA6nNS29bAflyR3f8h/8= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 23043581737f34db; Thu, 10 Sep 2026 16:47:06 +0000 X-Mizu-Trace-ID: 23043581737f34db X-Migadu-Flow: FLOW_OUT From: Vineet Gupta To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org, eddyz87@gmail.com, memxor@gmail.com Cc: martin.lau@linux.dev, song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org, emil@etsalapatis.com, ihor.solodrai@linux.dev, john.fastabend@gmail.com, shuah@kernel.org, bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Vineet Gupta Subject: [PATCH bpf-next v2 03/13] bpf: track low-32 scalar equality across zero-extending movs Date: Thu, 10 Sep 2026 22:16:25 +0530 Message-ID: <20260910164635.459558-4-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260910164635.459558-1-vineet.gupta@linux.dev> References: <20260910164635.459558-1-vineet.gupta@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Linked-scalar equality is full-64-bit only. A 32-bit mov from a source with unknown high bits therefore has to drop the relationship, and a later narrowing of the source never reaches the destination: r6 =3D ... /* full 64-bit unknown */ w7 =3D w6 /* 32-bit zero-extending mov */ if w6 !=3D 0 goto ... /* not taken: r6's low 32 bits are 0 */ if w7 =3D=3D 0 goto ... /* not deduced today */ Record a low-32-only link instead: dst shares src's low 32 bits and its high half is zero. On a later narrowing, sync_linked_regs() rebuilds such a register from the base rather than copying it, by re-applying the same zext_32_to_64() the mov used. The reverse direction is skipped: a ->subreg base knows nothing about a full register's high half. The link is not modelled together with an ADD_CONST delta, so forming one costs the source its delta: assign_scalar_id_before_mov() clears it, as it already does for a narrow source. That loses tracking a wide source used to keep, but the two cannot both be held -- a link the sync path skips would be inert. bpf-gcc hits this by reusing "w0 =3D idx" for "return 0" on the idx =3D=3D 0 path of a bpf_loop callback. regs_exact() and regsafe() compare the new kind, for the reasons given in the previous patch. As with the pre-existing add_const comparison, no selftest isolates it: when the kinds differ in a way a program can build, the ranges differ too and the range checks reject first. Two existing tests move with the behaviour. verifier_reg_equal's "w reg not equal if r reg upper32 bits not 0" is this exact case and becomes __success; verifier_bounds' sub32_partial_overflow pins a register dump that now carries the link. Signed-off-by: Vineet Gupta --- v2: was RFC 3/6. - no longer excludes an ADD_CONST source, for symmetry with the narrow path (Eduard). The cost is spelled out in the changelog - reconstruct_zext32() instead of an open-coded block (Eduard) - saved_id dropped: reg->id =3D=3D known_reg->id already holds (Eduard) - flattened the if-nesting (Eduard) - log.c prints the kind (Eduard) - changelog and comments trimmed (Eduard) include/linux/bpf_verifier.h | 10 +++ kernel/bpf/log.c | 2 + kernel/bpf/states.c | 4 +- kernel/bpf/verifier.c | 61 +++++++++++++++++-- .../selftests/bpf/progs/verifier_bounds.c | 2 +- .../selftests/bpf/progs/verifier_reg_equal.c | 14 ++--- 6 files changed, 78 insertions(+), 15 deletions(-) diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h index afb1e5628698..f1b01059c5da 100644 --- a/include/linux/bpf_verifier.h +++ b/include/linux/bpf_verifier.h @@ -46,6 +46,15 @@ enum bpf_add_const { ADD_CONST_64, /* ... with a 64-bit ALU op */ }; =20 +/* + * Records that a register shares only the low 32 bits of the base of its + * ->id set, and how its high bits follow from them. + */ +enum bpf_subreg { + SUBREG_NONE =3D 0, + SUBREG_ZEXT, /* high bits are zero (32-bit zero-extending mov) */ +}; + struct bpf_reg_state { /* Ordering of fields matters. See states_equal() */ enum bpf_reg_type type; @@ -173,6 +182,7 @@ struct bpf_reg_state { * Non-zero only if ->id is. */ enum bpf_add_const add_const:2; + enum bpf_subreg subreg:2; }; =20 static inline s64 reg_smin(const struct bpf_reg_state *reg) diff --git a/kernel/bpf/log.c b/kernel/bpf/log.c index f8d7a5c8052f..4047cfb0a698 100644 --- a/kernel/bpf/log.c +++ b/kernel/bpf/log.c @@ -654,6 +654,8 @@ static void print_reg_state(struct bpf_verifier_env *en= v, verbose_a("id=3D%d", reg->id); if (reg->add_const) verbose(env, "%+d", reg->delta); + if (reg->subreg =3D=3D SUBREG_ZEXT) + verbose(env, ".lo32"); if (reg->parent_id) verbose_a("parent_id=3D%d", reg->parent_id); if (type_is_non_owning_ref(reg->type)) diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c index 5505d7aaeed0..1618c77105ab 100644 --- a/kernel/bpf/states.c +++ b/kernel/bpf/states.c @@ -476,6 +476,7 @@ static bool regs_exact(const struct bpf_reg_state *rold, { return memcmp(rold, rcur, offsetof(struct bpf_reg_state, id)) =3D=3D 0 && rold->add_const =3D=3D rcur->add_const && + rold->subreg =3D=3D rcur->subreg && check_ids(rold->id, rcur->id, idmap) && check_ids(rold->parent_id, rcur->parent_id, idmap); } @@ -577,7 +578,8 @@ static bool regsafe(struct bpf_verifier_env *env, struc= t bpf_reg_state *rold, * linking semantics in sync_linked_regs() (alu32 zero-extends, * alu64 does not), so pruning across them is unsafe. */ - if (rold->id && rold->add_const !=3D rcur->add_const) + if (rold->id && (rold->add_const !=3D rcur->add_const || + rold->subreg !=3D rcur->subreg)) return false; =20 /* Both have offset linkage: offsets must match */ diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 0ca229f6e7ac..58e788f53ae5 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -1908,6 +1908,7 @@ static void __mark_reg_known(struct bpf_reg_state *re= g, u64 imm) reg->id =3D 0; reg->parent_id =3D 0; reg->add_const =3D ADD_CONST_NONE; + reg->subreg =3D SUBREG_NONE; ___mark_reg_known(reg, imm); } =20 @@ -3482,6 +3483,7 @@ static void clear_scalar_id(struct bpf_reg_state *reg) reg->id =3D 0; reg->delta =3D 0; reg->add_const =3D ADD_CONST_NONE; + reg->subreg =3D SUBREG_NONE; } =20 static void assign_scalar_id_before_mov(struct bpf_verifier_env *env, @@ -3493,6 +3495,8 @@ static void assign_scalar_id_before_mov(struct bpf_ve= rifier_env *env, * The verifier is processing rX =3D rY insn and * rY->id has special linked register already. * Cleared it, since multiple rX +=3D const are not supported. + * A ->subreg link can be shared: it describes src's own relationship + * to the set, not a delta to unwind. */ if (src_reg->add_const) clear_scalar_id(src_reg); @@ -16244,15 +16248,22 @@ static int check_alu_op(struct bpf_verifier_env *= env, struct bpf_insn *insn) } else if (src_reg->type =3D=3D SCALAR_VALUE) { if (insn->off =3D=3D 0) { bool is_src_reg_u32 =3D get_reg_width(src_reg) <=3D 32; + /* + * A wide src shares only its low 32 bits. A + * full link would let dst's [0, U32_MAX] + * propagate onto src's unknown high bits, so + * record a low-32-only link instead. A + * self-mov has nothing to link. + */ + bool subreg_link =3D !is_src_reg_u32 && + src_reg !=3D dst_reg; =20 - if (is_src_reg_u32) + if (is_src_reg_u32 || subreg_link) assign_scalar_id_before_mov(env, src_reg); *dst_reg =3D *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) + if (subreg_link && src_reg->id) + dst_reg->subreg =3D SUBREG_ZEXT; + else if (!is_src_reg_u32) clear_scalar_id(dst_reg); } else { /* case: W1 =3D (s8, s16)W2 */ @@ -17115,6 +17126,23 @@ static void collect_linked_regs(struct bpf_verifie= r_env *env, } } =20 +/* + * Set @reg to the zero-extension of @known_reg's low 32 bits: it shares t= hose + * bits and its high half is zero. Copy the base to keep its precise low-32 + * tnum, then re-apply the zext_32_to_64() the 32-bit mov itself used. + * @reg->id and ->delta already equal @known_reg's; only ->subreg is its o= wn. + */ +static void reconstruct_zext32(struct bpf_reg_state *reg, + struct bpf_reg_state *known_reg) +{ + enum bpf_subreg subreg =3D reg->subreg; + + *reg =3D *known_reg; + reg->subreg =3D subreg; + zext_32_to_64(reg); + reg_bounds_sync(reg); +} + /* For all R in linked_regs, copy known_reg range into R * if R->id =3D=3D known_reg->id. */ @@ -17134,6 +17162,27 @@ static void sync_linked_regs(struct bpf_verifier_e= nv *env, struct bpf_verifier_s continue; if (reg->id !=3D known_reg->id) continue; + /* + * A ->subreg register shares only the base's low 32 bits, so it + * is rebuilt rather than copied. Not modelled together with a + * delta, so skip if either side has one (sound, less precise). + */ + if (reg->subreg) { + if (reg->add_const || known_reg->add_const) + continue; + reconstruct_zext32(reg, known_reg); + if (e->is_reg) + mark_reg_scratched(env, e->regno); + else + mark_stack_slot_scratched(env, e->spi); + continue; + } + /* + * The reverse: known_reg knows only its low 32 bits, which say + * nothing about reg's high half. + */ + if (known_reg->subreg) + continue; /* * Skip mixed 32/64-bit links: the delta relationship doesn't * hold across different ALU widths. diff --git a/tools/testing/selftests/bpf/progs/verifier_bounds.c b/tools/te= sting/selftests/bpf/progs/verifier_bounds.c index df8d5309657e..b4eadbd88357 100644 --- a/tools/testing/selftests/bpf/progs/verifier_bounds.c +++ b/tools/testing/selftests/bpf/progs/verifier_bounds.c @@ -1516,7 +1516,7 @@ __naked void sub32_full_overflow(void) SEC("socket") __description("32-bit subtraction, partial overflow, result in unbounded u= 32 bounds") __success __log_level(2) -__msg("3: (1c) w3 -=3D w2 {{.*}} R3=3Dscalar(smin=3D0,smax=3Dumax=3D0xffff= ffff,var_off=3D(0x0; 0xffffffff))") +__msg("3: (1c) w3 -=3D w2 {{.*}} R3=3Dscalar(id=3D{{[0-9]+}}-1.lo32,smin= =3D0,smax=3Dumax=3D0xffffffff,var_off=3D(0x0; 0xffffffff))") __retval(0) __naked void sub32_partial_overflow(void) { diff --git a/tools/testing/selftests/bpf/progs/verifier_reg_equal.c b/tools= /testing/selftests/bpf/progs/verifier_reg_equal.c index dc1d8c30fb0e..34214ec92670 100644 --- a/tools/testing/selftests/bpf/progs/verifier_reg_equal.c +++ b/tools/testing/selftests/bpf/progs/verifier_reg_equal.c @@ -31,23 +31,23 @@ l1_%=3D: exit; \ } =20 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 =3D r0; \ - /* Upper 4-bytes of r2 may not be 0, thus insn \ - * w3 =3D 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, so r3 does \ + * not equal r2. It does share r2's low 32 bits \ + * though, so w2 < 9 still bounds r3: the \ + * zero-extending mov leaves nothing above them.\ */ \ w3 =3D w2; \ if w2 < 9 goto l0_%=3D; \ exit; \ l0_%=3D: if r3 < 9 goto l1_%=3D; \ - /* r1 read is illegal at this point */ \ + /* unreachable, so the r1 read is never made */ \ r0 -=3D r1; \ l1_%=3D: exit; \ " : --=20 2.53.0-Meta From nobody Fri Sep 25 16:01:34 2026 Received: from mta0.migadu.com (out-225.mta0.migadu.com [91.218.175.225]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8980F521225 for ; Thu, 10 Sep 2026 16:47:12 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.225 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058836; cv=none; b=vGSwqloJ55EA9geZmTDN4+lkScvfKBIZ6qQlU88S1bKbU1cFsDrdGWZvGRC+Yc8G7pfR0ShPJvQft+kNm0M+UojmEisa0nTSbqO/fwVGIdMRjJZNOe+nzD8ANAHJnCGQMfhFOzceCBpUmkAx2B0FnlA85/9rqvAbd3zwQBOd7EE= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058836; c=relaxed/simple; bh=2m4zsRZhVEqwAroNUaycY1tRN5knZntOtthXJsBjwOo=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=rGubUHqdLDxFbiJHxh2opNAp2cTRZChwr2DgcnfGVtAKzUG/H0NadV7fx7iXso7ZEP2ex1mGb7N+pZ+NdLmMxvVe0nu0PJrr5d/j23hJmvPjcbip+C7YCELdKjdK2vLPaC5f6PwVQzQrjP64RwXKx5othAkDE1h9lYvwe6hbtcM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=Y7Db0hma; arc=none smtp.client-ip=91.218.175.225 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="Y7Db0hma" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=2m4zsRZhVEqwAroNUaycY1tRN5knZntOtthXJsBjwOo=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789058830; v=1; x=1789663630; b=Y7Db0hmauf7xHjb3CDYlFVLxDKE20W+1opN1IzYnlYnRMY5+ad/6vlFINChayvIxdd4vN/KE 8r2XaYcJoXvSGqNoTJVEAlNvtCV3QRQftZtqeyTRRsKxkGVf6xcJ7UkWlNs5uK20pOik8s6RDEi 6A8TttpMuy/C+X7T5Cqx3C0w= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 4a9e9481228269a2; Thu, 10 Sep 2026 16:47:10 +0000 X-Mizu-Trace-ID: 4a9e9481228269a2 X-Migadu-Flow: FLOW_OUT From: Vineet Gupta To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org, eddyz87@gmail.com, memxor@gmail.com Cc: martin.lau@linux.dev, song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org, emil@etsalapatis.com, ihor.solodrai@linux.dev, john.fastabend@gmail.com, shuah@kernel.org, bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Vineet Gupta Subject: [PATCH bpf-next v2 04/13] selftests/bpf: cover the low-32 link for zero-extending movs Date: Thu, 10 Sep 2026 22:16:26 +0530 Message-ID: <20260910164635.459558-5-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260910164635.459558-1-vineet.gupta@linux.dev> References: <20260910164635.459558-1-vineet.gupta@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Exercise each decision the previous patch adds: - zext_mov_narrow_src: narrowing the source reaches the destination - zext_narrow_dst_keeps_base: the reverse must not happen, the base's high bits are still unknown - zext_u32_src_is_full_link: a provably-u32 source keeps taking the full-equality path - zext_self_mov_no_link: a self-mov mints no id - zext_chain_keeps_link: a linked source survives a further 32-bit mov - zext_no_sync_when_base_has_delta, zext_no_sync_from_subreg_base: a delta on either side stops propagation, in both directions - zext_sync_between_two_subregs: two low-32 links on one base do reach each other - zext_unlinked_path_stays_reachable: only one path links r7 to r8, so the guarded div stays reachable - zext_mov_breaks_add_const_src: forming the link drops a delta link on the source, as the narrow-source path has always done Written in asm so the bytecode is the same whichever compiler built the suite. These three check an outcome rather than a mechanism: the states they compare differ in ids, contents or ranges, so regsafe() keeps them apart for reasons of its own and no single check can be disabled to make them fail. Signed-off-by: Vineet Gupta --- v2: was RFC 4/6. - all tests live in verifier_linked_scalars.c (Eduard) - numeric labels, and the verifier_bounds regex left as-is bar the id (Eduard) - dropped the LLM-written commentary and the redundant second bpf_get_prandom_u32() (Eduard) - covers the seven cases listed on RFC 4/6 - the dest-driven test had the polarity flaw sashiko found on 6/6; its =3D=3D guard now discriminates, verified by disabling the guard it targe= ts - three tests renamed for what they check, see the cover letter .../bpf/progs/verifier_linked_scalars.c | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) diff --git a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c b/= tools/testing/selftests/bpf/progs/verifier_linked_scalars.c index da6cb961a520..65cb0efd268f 100644 --- a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c +++ b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c @@ -710,4 +710,260 @@ l_exit_%=3D: \ : __clobber_all); } =20 +/* + * A 32-bit mov from a wide source shares only the low 32 bits. Narrowing = the + * source must reach the destination through that link. + */ +SEC("socket") +__success +__naked void zext_mov_narrow_src(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + w7 =3D w6; /* forms the link */ \ + if w6 !=3D 0 goto 1f; /* narrows r6, propagates to r7 */ \ + if w7 =3D=3D 0 goto 1f; \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * The reverse does not hold: narrowing the low-32 link says nothing about= the + * base's high bits, so r6 must stay unknown and the div stays reachable. + */ +SEC("socket") +__failure __msg("div by zero") +__naked void zext_narrow_dst_keeps_base(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + w7 =3D w6; /* forms the link */ \ + if r7 !=3D 0 goto 1f; /* narrows r7, must not propagate to r6 */ \ + if r6 =3D=3D 0 goto 1f; /* taken only if r6 wrongly narrowed */ \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * A provably-u32 source takes the full-equality path, not the low-32 one: + * narrowing the destination must reach the source. + */ +SEC("socket") +__success +__naked void zext_u32_src_is_full_link(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + w6 =3D w0; /* r6 provably u32 */ \ + w7 =3D w6; /* full link, not low-32 */ \ + if r7 > 10 goto 1f; /* narrows r7, propagates to r6 */ \ + if r6 > 10 goto 2f; \ + goto 1f; \ +2: \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * A self-mov has nothing to link, so it must not mint an id for r6. + */ +SEC("socket") +__success __log_level(2) +/* an id would print as R6=3Dscalar(id=3DN.lo32,smin=3D... */ +__msg("(bc) w6 =3D w6 {{.*}} R6=3Dscalar(smin=3D0,") +__naked void zext_self_mov_no_link(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + r6 ^=3D 0; /* drop the id */ \ + w6 =3D w6; /* forms no link */ \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * A low-32-linked source keeps its id and flag across a further 32-bit mo= v, + * so narrowing the base still reaches the end of the chain. + */ +SEC("socket") +__success +__naked void zext_chain_keeps_link(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + w7 =3D w6; /* forms the link */ \ + w8 =3D w7; /* link survives the 2nd mov */ \ + if w6 !=3D 0 goto 1f; /* narrows r6, propagates to r8 */ \ + if w8 =3D=3D 0 goto 1f; \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * A delta on either side is not modelled together with a low-32 link, so = no + * range propagates: here the branch register carries the delta. + */ +SEC("socket") +__failure __msg("div by zero") +__naked void zext_no_sync_when_base_has_delta(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + w7 =3D w6; /* forms the link */ \ + r8 =3D r6; \ + r8 +=3D 3; /* delta on the branch reg */ \ + if r8 !=3D 3 goto 1f; /* must not propagate to r7 */ \ + if w7 =3D=3D 0 goto 1f; \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * ... and here the low-32 link is the branch register, so the register + * carrying the delta must not be narrowed either. + */ +SEC("socket") +__failure __msg("div by zero") +__naked void zext_no_sync_from_subreg_base(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + w7 =3D w6; /* forms the link */ \ + r8 =3D r6; \ + r8 +=3D 3; /* delta on r8 */ \ + if w7 !=3D 0 goto 1f; /* must not propagate to r8 */ \ + if r8 =3D=3D 3 goto 1f; /* taken only if r8 wrongly narrowed */ \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * Two low-32 links on the same base do propagate to each other: both are = the + * zero-extension of the same low 32 bits. + */ +SEC("socket") +__success +__naked void zext_sync_between_two_subregs(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + w7 =3D w6; /* two links on one base */ \ + w8 =3D w6; \ + if w7 !=3D 0 goto 1f; /* narrows r7, propagates to r8 */ \ + if w8 =3D=3D 0 goto 1f; \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * Only one of the two paths links r7 to r8, so the narrowing of w8 reache= s r7 + * on one and not the other and the div stays reachable. This checks the + * outcome, not the mechanism: the two states differ in their ids and cont= ents, + * so regsafe() has many reasons to keep them apart and disabling any sing= le + * one of its checks does not make this fail. + */ +SEC("socket") +__failure __msg("div by zero") +__flag(BPF_F_TEST_STATE_FREQ) +__naked void zext_unlinked_path_stays_reachable(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + r6 &=3D 1; \ + if r6 >=3D 1 goto 2f; \ + /* explored first: r7 is a low-32 link of r8 */ \ + call %[bpf_get_prandom_u32]; \ + r8 =3D r0; \ + w7 =3D w8; /* forms the link */ \ + goto 1f; \ +2: \ + /* runtime path: r7 unrelated to r8 */ \ + call %[bpf_get_prandom_u32]; \ + r8 =3D r0; \ + call %[bpf_get_prandom_u32]; \ + w7 =3D w0; /* no link here */ \ +1: \ + if w8 !=3D 0 goto 3f; /* propagates to r7 only if linked */ \ + if w7 =3D=3D 0 goto 3f; \ + r0 /=3D 0; \ +3: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * Forming the link calls assign_scalar_id_before_mov(), which drops a del= ta + * link on the source. That is what the narrow-source path has always done, + * so a wide source behaves the same: r5 stops tracking r6. + */ +SEC("socket") +__failure __msg("div by zero") +__naked void zext_mov_breaks_add_const_src(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + r5 =3D r6; /* r5, r6 linked */ \ + r5 +=3D 3; /* r5 =3D base + 3 */ \ + w7 =3D w5; /* breaks r5's delta link */ \ + if r6 > 9 goto 1f; /* r6 in [0, 9] */ \ + if r5 < 13 goto 1f; /* taken only if r5 still linked */ \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + char _license[] SEC("license") =3D "GPL"; --=20 2.53.0-Meta From nobody Fri Sep 25 16:01:34 2026 Received: from mta0.migadu.com (out-236.mta0.migadu.com [91.218.175.236]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1721F51A750 for ; Thu, 10 Sep 2026 16:47:18 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.236 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058842; cv=none; b=ktv3X08YGwWBfCkDHfqZXuS42G0u++QphSTouZdezx3UW5TQ66YKu/yTAFwepnWwL8o/aWlLX8Ys4qVppWmiFn1X8mWSYyHFKuAui3Bb87TbitfjySGy5dcykflcbMCxqVHuZOkkHziPVE9Bd/PUElFBq2HzaBYHt/mfdRuRkgY= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058842; c=relaxed/simple; bh=p6Dv5uXyCx/XL3sMcXWjyxFbQ0++l2v/EGih7Qb9vXE=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=dg5NmDdbH5dVYaJRbT842OlqjCid4KEz86hhlvSuYKgUfC3ofotVOAAv3vN/VWv6vZVVtvKqH9RrEKeh0wQqYAIZiUQ3jqVB2gZQHSy+e/ayArIdNDo0CP+EoGjRNpfYZIArGFAvbVzVt2PE9cxQShtR2HKfzekX2Ds6O57r/dg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=SHNa2Z4A; arc=none smtp.client-ip=91.218.175.236 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="SHNa2Z4A" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=p6Dv5uXyCx/XL3sMcXWjyxFbQ0++l2v/EGih7Qb9vXE=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789058834; v=1; x=1789663634; b=SHNa2Z4Ahc7T7R5WB9IJUcbSsqkkIPV23aJCty9vCRIomS/KdNKOfeDAYwgs54BQRehZrIAi y1s8efc4MMyJQxhbk3tBtkwlfWfiG5a52jvw1PnzbzypWYyujVvu9E5LoDcOAN4FP/wawPOTTDf PR/KesLIaHUA/ZP3a8TUPEyI= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id c2c93a19055d0db4; Thu, 10 Sep 2026 16:47:14 +0000 X-Mizu-Trace-ID: c2c93a19055d0db4 X-Migadu-Flow: FLOW_OUT From: Vineet Gupta To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org, eddyz87@gmail.com, memxor@gmail.com Cc: martin.lau@linux.dev, song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org, emil@etsalapatis.com, ihor.solodrai@linux.dev, john.fastabend@gmail.com, shuah@kernel.org, bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Vineet Gupta Subject: [PATCH bpf-next v2 05/13] bpf: keep the range across a sign extension that cannot change it Date: Thu, 10 Sep 2026 22:16:27 +0530 Message-ID: <20260910164635.459558-6-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260910164635.459558-1-vineet.gupta@linux.dev> References: <20260910164635.459558-1-vineet.gupta@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" coerce_reg_to_size_sx() decides whether a sign extension is lossless by comparing the bits above the field in smin and smax: top_smax_value =3D ((u64)reg_smax(reg) >> num_bits) << num_bits; top_smin_value =3D ((u64)reg_smin(reg) >> num_bits) << num_bits; if (top_smax_value !=3D top_smin_value) goto out; Equal high bits do imply the truncation is lossless, but the converse does not hold. Whenever the range straddles zero the high bits necessarily differ -- smin sign-extends to all ones, smax to all zeroes -- even when every value in the range fits the field and (sN)v =3D=3D v throughout. The second gate, "both of s64_max/s64_min positive or negative", rejects the same shape again for the same reason. So a register holding an errno-or-zero value, [-4095, 0], comes out of r0 =3D (s32)r0 as the full [S32_MIN, S32_MAX] even though the instruction is a no-op on it. The no_sext test at the call site does not help: it is an unsigned check, so it only covers non-negative values that fit. Test the range against the field directly and return early when it fits. Sign extension is then the identity, so nothing needs updating -- which also preserves var_off, where the existing path would have replaced known bits with a coarse tnum_range(). This only tightens: the early return fires exactly where the value is provably unchanged, and the cases the current tests do accept still take the same path and produce the same bounds. Signed-off-by: Vineet Gupta --- v2: new. coerce_reg_to_size_sx() is fixed rather than special-cased, which is what was asked on RFC 5/6; it removes the RFC's call-then-overwrite at the mov site. kernel/bpf/verifier.c | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 58e788f53ae5..eb093194e2a3 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -5754,6 +5754,7 @@ static void set_sext64_default_val(struct bpf_reg_sta= te *reg, int size) static void coerce_reg_to_size_sx(struct bpf_reg_state *reg, int size) { s64 init_s64_max, init_s64_min, s64_max, s64_min, u64_cval; + s64 field_smin, field_smax; u64 top_smax_value, top_smin_value; u64 num_bits =3D size * 8; =20 @@ -5773,6 +5774,27 @@ static void coerce_reg_to_size_sx(struct bpf_reg_sta= te *reg, int size) return; } =20 + if (size =3D=3D 1) { + field_smin =3D S8_MIN; + field_smax =3D S8_MAX; + } else if (size =3D=3D 2) { + field_smin =3D S16_MIN; + field_smax =3D S16_MAX; + } else { + /* size =3D=3D 4 */ + field_smin =3D S32_MIN; + field_smax =3D S32_MAX; + } + + /* + * The range already fits the field, so (sN)v =3D=3D v for every value the + * register can hold and the sign extension changes nothing. The tests + * below cannot reach this case once smin is negative: a negative smin + * and a non-negative smax never share their high bits. + */ + if (reg_smin(reg) >=3D field_smin && reg_smax(reg) <=3D field_smax) + return; + top_smax_value =3D ((u64)reg_smax(reg) >> num_bits) << num_bits; top_smin_value =3D ((u64)reg_smin(reg) >> num_bits) << num_bits; =20 --=20 2.53.0-Meta From nobody Fri Sep 25 16:01:34 2026 Received: from mta0.migadu.com (out-7.mta0.migadu.com [91.218.175.7]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C71F452B1E1 for ; Thu, 10 Sep 2026 16:47:30 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.7 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058861; cv=none; b=C9k0Au+qWcYPR0b9RS5hX+Q4j66spEvm1tS1EcuOZ32iS67JT8/UoUI5F3Nx0RdAcDVzezGVoHJ+J2vnjf8uzjgbwQfosRGkT5OH759USDKsNgMRzWA7pu9xWopzoS5cjWgGW/DM2D+oAZ+ktsMX73wUOUYVSEc6h0oU0p92LBE= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058861; c=relaxed/simple; bh=S0BTMJ/3CMiKWvRdVO/UaaNhAWtf/hCTg91uhqdRYcM=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=aBIr/Pzf2tgdy1ZYkUwmHucMkXyf971GwHw5hPIfU+5ea21tBpAZL6+G4jKzVxakJuwm6lT2AaEy8T4+Ytp5c0p6BAWXImBidaf6pa2SPqMLOKrcfxK4QwzdO/donnEdSWfXXdkhDKIF+IjYFSoNvLGVEV/Y55ltLTjDQpfQl7c= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=uPPD0DM3; arc=none smtp.client-ip=91.218.175.7 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="uPPD0DM3" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=S0BTMJ/3CMiKWvRdVO/UaaNhAWtf/hCTg91uhqdRYcM=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789058843; v=1; x=1789663643; b=uPPD0DM3ZhrLHjklNiME0axtTU8r9XXFVZlxu/BqytdbBhmMki7SwCAei4q+FdeGq0K1aM2p IIVMXTW0QUrwx36plYQssy+We9UMnHymG13Nj060msEDOuEnz+FRH0pWxleNwZiZyRb0cVwEKpd lnKoJrMcbOczi9kza973n8c0= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 88e6ca5c82b124ac; Thu, 10 Sep 2026 16:47:23 +0000 X-Mizu-Trace-ID: 88e6ca5c82b124ac X-Migadu-Flow: FLOW_OUT From: Vineet Gupta To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org, eddyz87@gmail.com, memxor@gmail.com Cc: martin.lau@linux.dev, song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org, emil@etsalapatis.com, ihor.solodrai@linux.dev, john.fastabend@gmail.com, shuah@kernel.org, bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Vineet Gupta Subject: [PATCH bpf-next v2 06/13] selftests/bpf: cover sign extensions that cannot change the range Date: Thu, 10 Sep 2026 22:16:28 +0530 Message-ID: <20260910164635.459558-7-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260910164635.459558-1-vineet.gupta@linux.dev> References: <20260910164635.459558-1-vineet.gupta@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Both programs build a range that straddles zero but still fits the target field, so the sign extension is a no-op: [-4095, 0] for (s32) and [-63, 0] for (s8). The guard that follows can only be resolved statically if the range survives, and it protects a div by zero, so a widened range is a verification failure rather than a silently weaker test. These sit in verifier_movsx.c rather than with the linked-scalar tests: they exercise the range a sign-extending mov produces, which has nothing to do with the ->id machinery, and the file already gates on the cpuv4 support they need. Signed-off-by: Vineet Gupta --- v2: new, with 5/13. .../selftests/bpf/progs/verifier_movsx.c | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tools/testing/selftests/bpf/progs/verifier_movsx.c b/tools/tes= ting/selftests/bpf/progs/verifier_movsx.c index 195b27a51224..3a0e67ebb963 100644 --- a/tools/testing/selftests/bpf/progs/verifier_movsx.c +++ b/tools/testing/selftests/bpf/progs/verifier_movsx.c @@ -202,6 +202,57 @@ l0_%=3D: \ : __clobber_all); } =20 +/* + * A range that already fits the field is unchanged by the sign extension. + * Both of these straddle zero, so the high bits of smin and smax differ a= nd + * the top_s*_value test alone would fall back to the full field range. + */ +SEC("socket") +__description("MOV64SX, S32, negative range is preserved") +__success __success_unpriv __retval(0) +__naked void mov64sx_s32_negative_range(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + w0 &=3D 0xfff; \ + r0 -=3D 0xfff; \ + /* r0 is [-4095, 0], already a valid s32 */ \ + r0 =3D (s32)r0; \ + if r0 s< -0xfff goto l0_%=3D; \ + r0 =3D 0; \ + exit; \ +l0_%=3D: \ + /* unreachable unless the range was widened */ \ + r0 /=3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +SEC("socket") +__description("MOV64SX, S8, negative range is preserved") +__success __success_unpriv __retval(0) +__naked void mov64sx_s8_negative_range(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + w0 &=3D 0x3f; \ + r0 -=3D 0x3f; \ + /* r0 is [-63, 0], already a valid s8 */ \ + r0 =3D (s8)r0; \ + if r0 s< -0x3f goto l0_%=3D; \ + r0 =3D 0; \ + exit; \ +l0_%=3D: \ + /* unreachable unless the range was widened */ \ + r0 /=3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + SEC("socket") __description("MOV64SX, S16, R10 Sign Extension") __failure __msg("R1 type=3Dscalar expected=3Dfp, pkt, pkt_meta, map_key, m= ap_value, mem, ringbuf_mem, buf, trusted_ptr_") --=20 2.53.0-Meta From nobody Fri Sep 25 16:01:34 2026 Received: from mta0.migadu.com (out-15.mta0.migadu.com [91.218.175.15]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 9D40352378E for ; Thu, 10 Sep 2026 16:47:32 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.15 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058857; cv=none; b=dPGPRM3TF1lMh1izAMQgb6SSzddMHza3RfmRHFyEaAp9pcBb+W5QeU1HCvlLCr1kuCwfbyCx7M2nyKaPttFvVxenJBu8ExRs5SrbcbTPX/nhRIsJDGqZnHwY1HSMWSAlUXsZaYFMYY+OGO07ECizMqhBiMDwSuVft38Z2Tnc168= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058857; c=relaxed/simple; bh=ZQzAHmXnQXRtT07FXharBiwXxPJzzGDcAjKVd5CAspk=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=XcyPPLwqEqhg3qoxKS85RRoIXFgZnXmSa5VRcdNreHdiCYdukYs2XJtrsgKyFE3lWxCoiNkfZEVAQSLsZKaNfyKP069HCiLVso1pozCDO0u57NTuV76pLKuM+DHP7fy8psAgYUdWke6rEp5i/sQpsDspa/3vqrV8DiSoixLfUyA= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=pHYkqW+W; arc=none smtp.client-ip=91.218.175.15 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="pHYkqW+W" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=ZQzAHmXnQXRtT07FXharBiwXxPJzzGDcAjKVd5CAspk=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789058847; v=1; x=1789663647; b=pHYkqW+WSuNvX8hTO5DUmDhjHYtYk9oOp+bh3rw4Av3BYfp1i0ohZI2zePmYJ256zOSRFjbB JMEnAaHSlBVemiykkXXfG0CO3XHkPZqql7CKduc2C3lGhboKlgiYtKd8ROidwN28HNf2Rd2fOwL LND7DT+nkZ2UpBcpkG3JJ1+s= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 28ef671506850e4e; Thu, 10 Sep 2026 16:47:27 +0000 X-Mizu-Trace-ID: 28ef671506850e4e X-Migadu-Flow: FLOW_OUT From: Vineet Gupta To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org, eddyz87@gmail.com, memxor@gmail.com Cc: martin.lau@linux.dev, song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org, emil@etsalapatis.com, ihor.solodrai@linux.dev, john.fastabend@gmail.com, shuah@kernel.org, bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Vineet Gupta Subject: [PATCH bpf-next v2 07/13] bpf: track low-32 scalar equality across sign-extending movs Date: Thu, 10 Sep 2026 22:16:29 +0530 Message-ID: <20260910164635.459558-8-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260910164635.459558-1-vineet.gupta@linux.dev> References: <20260910164635.459558-1-vineet.gupta@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" The zero-extending mov records that dst shares src's low 32 bits. A 32-bit sign extension shares them too -- it keeps the low half and fills the high half from bit 31 -- so the same link applies, with a different rule for rebuilding the high bits: r6 =3D ... /* full 64-bit unknown */ r7 =3D (s32)r6 /* 32-bit sign-extending mov */ if w6 =3D=3D -1 goto ... /* taken: r6's low 32 bits are all ones */ ... /* r7 is -1, not deduced today */ Add SUBREG_SEXT alongside SUBREG_ZEXT, and sext_32_to_64() alongside zext_32_to_64() to drive the reconstruction. Both work from the base's 32-bit range, which is what a 32-bit compare narrows. coerce_reg_to_size_sx() cannot serve here: it reads smin/smax, which straddle after such a compare and collapse to the full field range. tnum_sext() is the counterpart to tnum_cast(). Unlike a tnum_range() over the new bounds it keeps the known low bits. The enum has room for the third value, so bpf_reg_state stays 80 bytes. Unlike the zero-extending arm, a self-mov can form a link here, but only when src is already linked: r0 =3D (s32)r0 is how a sign-extended int return lands. On an unlinked register there is nothing to link to, and minting an id would leave the register describing itself. Signed-off-by: Vineet Gupta --- v2: was RFC 5/6. - no forward declaration (Eduard) - src renamed known_reg (Eduard) - sext_32_to_64() and tnum_sext() rather than reusing coerce_reg_to_size_sx(); the sync path needs the base's 32-bit range, see the cover letter - tnum_sext() keeps the known low bits a tnum_range() would drop (Eduard) - a self-mov links only when src already has an id, narrower than the RFC include/linux/bpf_verifier.h | 1 + include/linux/tnum.h | 3 +++ kernel/bpf/log.c | 2 ++ kernel/bpf/tnum.c | 15 ++++++++++++ kernel/bpf/verifier.c | 47 +++++++++++++++++++++++++++++++++--- 5 files changed, 65 insertions(+), 3 deletions(-) diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h index f1b01059c5da..920c9490ecc8 100644 --- a/include/linux/bpf_verifier.h +++ b/include/linux/bpf_verifier.h @@ -53,6 +53,7 @@ enum bpf_add_const { enum bpf_subreg { SUBREG_NONE =3D 0, SUBREG_ZEXT, /* high bits are zero (32-bit zero-extending mov) */ + SUBREG_SEXT, /* high bits repeat bit 31 (32-bit sign-extending mov) */ }; =20 struct bpf_reg_state { diff --git a/include/linux/tnum.h b/include/linux/tnum.h index ca2cfec8de08..866803de5841 100644 --- a/include/linux/tnum.h +++ b/include/linux/tnum.h @@ -63,6 +63,9 @@ struct tnum tnum_union(struct tnum t1, struct tnum t2); /* Return @a with all but the lowest @size bytes cleared */ struct tnum tnum_cast(struct tnum a, u8 size); =20 +/* Return the lowest @size bytes of @a sign-extended to 64 bits */ +struct tnum tnum_sext(struct tnum a, u8 size); + /* Swap the bytes of a tnum */ struct tnum tnum_bswap16(struct tnum a); struct tnum tnum_bswap32(struct tnum a); diff --git a/kernel/bpf/log.c b/kernel/bpf/log.c index 4047cfb0a698..b67bbd4d57f4 100644 --- a/kernel/bpf/log.c +++ b/kernel/bpf/log.c @@ -656,6 +656,8 @@ static void print_reg_state(struct bpf_verifier_env *en= v, verbose(env, "%+d", reg->delta); if (reg->subreg =3D=3D SUBREG_ZEXT) verbose(env, ".lo32"); + else if (reg->subreg =3D=3D SUBREG_SEXT) + verbose(env, ".lo32sx"); if (reg->parent_id) verbose_a("parent_id=3D%d", reg->parent_id); if (type_is_non_owning_ref(reg->type)) diff --git a/kernel/bpf/tnum.c b/kernel/bpf/tnum.c index ec9c310cf5d7..e1dc57afd3d3 100644 --- a/kernel/bpf/tnum.c +++ b/kernel/bpf/tnum.c @@ -200,6 +200,21 @@ struct tnum tnum_cast(struct tnum a, u8 size) return a; } =20 +struct tnum tnum_sext(struct tnum a, u8 size) +{ + u8 shift =3D 64 - size * 8; + + /* + * Shifting the field up to the top and back down arithmetically + * replicates its sign bit through the high half. Applying that to the + * mask as well carries over whether the sign was known: an unknown + * sign bit leaves every high bit unknown. + */ + a =3D tnum_cast(a, size); + return TNUM((s64)(a.value << shift) >> shift, + (s64)(a.mask << shift) >> shift); +} + bool tnum_is_aligned(struct tnum a, u64 size) { if (!size) diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index eb093194e2a3..308ff53232f0 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -5708,6 +5708,16 @@ static void zext_32_to_64(struct bpf_reg_state *reg) reg_set_urange64(reg, reg_u32_min(reg), reg_u32_max(reg)); } =20 +/* + * The sign-extending counterpart. Signed bounds carry over directly becau= se + * sign extension is monotonic over the signed 32-bit range. + */ +static void sext_32_to_64(struct bpf_reg_state *reg) +{ + reg->var_off =3D tnum_sext(reg->var_off, 4); + reg_set_srange64(reg, reg_s32_min(reg), reg_s32_max(reg)); +} + /* truncate register to smaller size (in bytes) * must be called with size < BPF_REG_SIZE */ @@ -16248,12 +16258,23 @@ static int check_alu_op(struct bpf_verifier_env *= env, struct bpf_insn *insn) return -EACCES; } else if (src_reg->type =3D=3D SCALAR_VALUE) { bool no_sext; + /* + * A 32-bit sign extension keeps the low 32 + * bits, so record a low-32 link as the + * zero-extending mov does. A self-mov + * qualifies only if src is already linked. + */ + bool subreg_link =3D (insn->off >> 3) =3D=3D 4 && + (src_reg !=3D dst_reg || + src_reg->id); =20 no_sext =3D reg_umax(src_reg) < (1ULL << (insn->off - 1)); - if (no_sext) + if (no_sext || subreg_link) assign_scalar_id_before_mov(env, src_reg); *dst_reg =3D *src_reg; - if (!no_sext) + if (!no_sext && subreg_link && src_reg->id) + dst_reg->subreg =3D SUBREG_SEXT; + else if (!no_sext) clear_scalar_id(dst_reg); coerce_reg_to_size_sx(dst_reg, insn->off >> 3); } else { @@ -17165,6 +17186,23 @@ static void reconstruct_zext32(struct bpf_reg_stat= e *reg, reg_bounds_sync(reg); } =20 +/* + * The sign-extending counterpart. Note this drives off the base's 32-bit + * range, not coerce_reg_to_size_sx(): after a 32-bit compare it is the low + * half that has been narrowed, and the 64-bit bounds still describe the + * base's high bits, which are not ours. + */ +static void reconstruct_sext32(struct bpf_reg_state *reg, + struct bpf_reg_state *known_reg) +{ + enum bpf_subreg subreg =3D reg->subreg; + + *reg =3D *known_reg; + reg->subreg =3D subreg; + sext_32_to_64(reg); + reg_bounds_sync(reg); +} + /* For all R in linked_regs, copy known_reg range into R * if R->id =3D=3D known_reg->id. */ @@ -17192,7 +17230,10 @@ static void sync_linked_regs(struct bpf_verifier_e= nv *env, struct bpf_verifier_s if (reg->subreg) { if (reg->add_const || known_reg->add_const) continue; - reconstruct_zext32(reg, known_reg); + if (reg->subreg =3D=3D SUBREG_ZEXT) + reconstruct_zext32(reg, known_reg); + else + reconstruct_sext32(reg, known_reg); if (e->is_reg) mark_reg_scratched(env, e->regno); else --=20 2.53.0-Meta From nobody Fri Sep 25 16:01:34 2026 Received: from mta0.migadu.com (out-30.mta0.migadu.com [91.218.175.30]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 65DDB52BE2F for ; Thu, 10 Sep 2026 16:47:37 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.30 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058863; cv=none; b=iGvM1L2So102AgVmYkkPr6mk4b+hHp0sRtDvEchPKwToKtHPeWu3wD3HzWfSUJnFtIJeYXvbIuPTbLNcIR6Q1apRD89UScYsgjMLNYCZMfddYWfT1QWRnXXG6liiAWEG6v0zq/9Sh6BAb+Q/xiN3ePALuMQyLgXG351RjWN3gyk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058863; c=relaxed/simple; bh=F+pd4LFVDzl9LIzWsn7DcNffB+umM8Xu2NR168bApxE=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=fEzgVXf82PR8mApcm9jv/b5zg3Jy47DE8CNUaGT+DwVd9A4k3EW9Kg2p38aSAUqZst9qMYA8A07Dl1DNPazn0FRI1Ym63mouDkGpfHACD7QlK2EFXQD9H+Xy5OKN/Y0lqu22oSdttavJr906me9eRGpVHXoosThq7TaLqfTAX50= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=qsP9+lrY; arc=none smtp.client-ip=91.218.175.30 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="qsP9+lrY" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=F+pd4LFVDzl9LIzWsn7DcNffB+umM8Xu2NR168bApxE=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789058852; v=1; x=1789663652; b=qsP9+lrYLQ3Iw9JcCPKy6Gz6UnUcmJ8gwhuojqJnyGAdMKJ4H2qNM2Ka5CEos0k22ugMCznR HduiUMM0yk+Kt8NiqHS5tSf8pFjcqb2Q/bUGBjnI6R/MnuZ4b9so8hac+29GIXU3UWR3OUCYLpZ jBSVgbs9jueIotagJIo9pYt0= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 29e31eec917ca39b; Thu, 10 Sep 2026 16:47:31 +0000 X-Mizu-Trace-ID: 29e31eec917ca39b X-Migadu-Flow: FLOW_OUT From: Vineet Gupta To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org, eddyz87@gmail.com, memxor@gmail.com Cc: martin.lau@linux.dev, song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org, emil@etsalapatis.com, ihor.solodrai@linux.dev, john.fastabend@gmail.com, shuah@kernel.org, bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Vineet Gupta Subject: [PATCH bpf-next v2 08/13] selftests/bpf: cover the low-32 link for sign-extending movs Date: Thu, 10 Sep 2026 22:16:30 +0530 Message-ID: <20260910164635.459558-9-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260910164635.459558-1-vineet.gupta@linux.dev> References: <20260910164635.459558-1-vineet.gupta@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Five programs, mirroring the zero-extending set: - sext_mov_wide_src: narrowing the source reaches the destination, and the value that arrives is sign-extended rather than zero-extended - sext_self_mov_keeps_link: r0 =3D (s32)r0 on an already-linked register stays in its set, the shape a sign-extended int return takes - sext_self_mov_no_link: the same mov on an unlinked register mints no id - sext_no_sync_when_base_has_delta: a delta on the base stops propagation - sext_no_sync_from_subreg_base: narrowing the link does not run backwards into the rest of the set - sext_kinds_reach_different_values: the same low half reaches -1 through a sign extension and 0xffffffff through a zero extension Each guards a div by zero that is only unreachable if the propagation happened, so losing the link is a verification failure rather than a test that quietly stops checking anything. The sign-extending mov is cpuv4, so the block needs a feature gate. CAN_USE_MOVSX follows CAN_USE_GOTOL: the arch list, then clang 18 or __BPF_FEATURE_MOVSX, so bpf-gcc gets the coverage too. Signed-off-by: Vineet Gupta --- v2: was RFC 6/6. - fixed the backwards guard sashiko reported - CAN_USE_MOVSX so bpf-gcc builds these too - renamed as in 4/13 tools/testing/selftests/bpf/progs/bpf_misc.h | 8 + .../bpf/progs/verifier_linked_scalars.c | 165 ++++++++++++++++++ 2 files changed, 173 insertions(+) diff --git a/tools/testing/selftests/bpf/progs/bpf_misc.h b/tools/testing/s= elftests/bpf/progs/bpf_misc.h index eb88d9ce6c34..45bfa02a5338 100644 --- a/tools/testing/selftests/bpf/progs/bpf_misc.h +++ b/tools/testing/selftests/bpf/progs/bpf_misc.h @@ -269,6 +269,14 @@ #define CAN_USE_BPF_ST #endif =20 +#if (defined(__TARGET_ARCH_arm64) || defined(__TARGET_ARCH_x86) || \ + (defined(__TARGET_ARCH_riscv) && __riscv_xlen =3D=3D 64) || \ + defined(__TARGET_ARCH_arm) || defined(__TARGET_ARCH_s390) || \ + defined(__TARGET_ARCH_loongarch)) && \ + (__clang_major__ >=3D 18 || defined(__BPF_FEATURE_MOVSX)) +#define CAN_USE_MOVSX +#endif + #if __clang_major__ >=3D 18 && defined(ENABLE_ATOMICS_TESTS) && \ (defined(__TARGET_ARCH_arm64) || \ defined(__TARGET_ARCH_x86) || \ diff --git a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c b/= tools/testing/selftests/bpf/progs/verifier_linked_scalars.c index 65cb0efd268f..9d060d8b0c1f 100644 --- a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c +++ b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c @@ -966,4 +966,169 @@ __naked void zext_mov_breaks_add_const_src(void) : __clobber_all); } =20 +#ifdef CAN_USE_MOVSX + +/* + * A 32-bit sign extension keeps the low 32 bits, so narrowing the source + * reaches the destination the same way it does for a zero extension. The = high + * half follows the sign, so the value seen here is negative. + */ +SEC("socket") +__success +__naked void sext_mov_wide_src(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + r7 =3D (s32)r6; /* forms the link */ \ + if w6 !=3D -1 goto 1f; /* narrows r6, propagates to r7 */ \ + if r7 =3D=3D -1 goto 1f; /* sign-extended, not 0xffffffff */ \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * r0 =3D (s32)r0 is how a sign-extended int return lands. src and dst are= the + * same register, but r0 is already linked, so there is a set for it to st= ay + * in and the narrowing still propagates. + */ +SEC("socket") +__success +__naked void sext_self_mov_keeps_link(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + r7 =3D r6; /* r6, r7 linked */ \ + r7 =3D (s32)r7; /* self-mov, keeps the id */ \ + if w6 !=3D -1 goto 1f; /* narrows r6, propagates to r7 */ \ + if r7 =3D=3D -1 goto 1f; \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * The same self-mov on an unlinked register has nothing to link to, so it + * must not mint an id that would leave r6 describing itself. + */ +SEC("socket") +__success __log_level(2) +/* an id would print as R6=3Dscalar(id=3DN.lo32sx,smin=3D... */ +__msg("(bf) r6 =3D (s32)r6 {{.*}} R6=3Dscalar(smin=3D") +__naked void sext_self_mov_no_link(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + r6 ^=3D 0; /* drop the id */ \ + r6 =3D (s32)r6; /* forms no link */ \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * A delta on the branch register is not modelled together with a low-32 + * link, so the propagation is skipped rather than guessed at. + */ +SEC("socket") +__failure __msg("div by zero") +__naked void sext_no_sync_when_base_has_delta(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + r7 =3D (s32)r6; /* forms the link */ \ + r8 =3D r6; \ + r8 +=3D 3; /* delta on the branch reg */ \ + if r8 !=3D 3 goto 1f; /* must not propagate to r7 */ \ + if r7 =3D=3D 0 goto 1f; \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * ... and with the link as the branch register the propagation must not r= un + * backwards: a ->subreg register knows nothing about the base's high half= , so + * narrowing it must leave the rest of the set alone. Were the guard missi= ng, + * r8 would be rebuilt as r7 + 3 =3D=3D 2, treating r7 as if it were the b= ase. + */ +SEC("socket") +__failure __msg("div by zero") +__naked void sext_no_sync_from_subreg_base(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + r7 =3D (s32)r6; /* forms the link */ \ + r8 =3D r6; \ + r8 +=3D 3; /* delta on r8 */ \ + if r7 !=3D -1 goto 1f; /* must not propagate to r8 */ \ + if r8 =3D=3D 2 goto 1f; /* taken only if r8 wrongly narrowed */ \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * The two kinds rebuild the high half differently: the same low half reac= hes + * -1 through a sign extension and 0xffffffff through a zero extension, so= only + * one path clears the guard. As above this checks the outcome -- the diff= ering + * ranges keep the states apart on their own. + */ +SEC("socket") +__failure __msg("div by zero") +__flag(BPF_F_TEST_STATE_FREQ) +__naked void sext_kinds_reach_different_values(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + r6 &=3D 1; \ + if r6 >=3D 1 goto 2f; \ + /* explored first: r7 is a sign-extended link of r8 */ \ + call %[bpf_get_prandom_u32]; \ + r8 =3D r0; \ + r7 =3D (s32)r8; \ + goto 1f; \ +2: \ + /* runtime path: r7 is a zero-extended link of r8 */ \ + call %[bpf_get_prandom_u32]; \ + r8 =3D r0; \ + w7 =3D w8; \ +1: \ + if w8 !=3D -1 goto 3f; \ + if r7 =3D=3D -1 goto 3f; /* only the sign-extended path */ \ + r0 /=3D 0; \ +3: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +#endif /* CAN_USE_MOVSX */ + char _license[] SEC("license") =3D "GPL"; --=20 2.53.0-Meta From nobody Fri Sep 25 16:01:35 2026 Received: from mta0.migadu.com (out-37.mta0.migadu.com [91.218.175.37]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 6FBFE50EBE0 for ; Thu, 10 Sep 2026 16:47:41 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.37 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058865; cv=none; b=MnZDhN0KZdbssGAXDAKjcQOqoAsa7PQtxsClZbBDQ0KtzKL0nZaLyEaPm1EyF/PM+7PsVhCZh3s6oN54X5AJAcd5lcFAKxP9gD7HezZ4Z96/T1FWKfxNWD9FxhJYSe52ERRMYplmq/jL1AdY8MPfw+hRHNQSexngX8NBmYXL3is= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058865; c=relaxed/simple; bh=VR1yaMEw1CjP6Rj29hwF41luwN/ydtgEvpkD8dmD2sw=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=uhy0bIZSBqPDMnhCAWnA+UmXUZPx7t26Pb5NBXGPPY6UhnSaWSvoxF/MQFyzX8LvNeph5Wv9Dukc+y8lzw53US1bvs2L1m5yVufnAw6RPMXyb59f7IjdVPxFzoCA35+1WmM9YuNYtQPmjkVm0sw3nZT1yaVdQNnn4uK6lT5CVAw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=faPOX+ZI; arc=none smtp.client-ip=91.218.175.37 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="faPOX+ZI" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=VR1yaMEw1CjP6Rj29hwF41luwN/ydtgEvpkD8dmD2sw=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789058856; v=1; x=1789663656; b=faPOX+ZIOIL9EF3xtlY4gm4gD5Fbfr6eTfMvm9NVqJdyKSSKzaYKMmXj4/UNkYt8A7OdwbmJ xZLVtJzwVj5uBmP0n6GaHRVLIVDJLUFxJgy9A2CVDqmo7X3CXrRAJ4oMpcEB5IKKe0iHtlqTMv+ TuAn7ma4ARiDIrcfcAzlLmZY= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 9c02b38f9a68efa5; Thu, 10 Sep 2026 16:47:36 +0000 X-Mizu-Trace-ID: 9c02b38f9a68efa5 X-Migadu-Flow: FLOW_OUT From: Vineet Gupta To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org, eddyz87@gmail.com, memxor@gmail.com Cc: martin.lau@linux.dev, song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org, emil@etsalapatis.com, ihor.solodrai@linux.dev, john.fastabend@gmail.com, shuah@kernel.org, bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Vineet Gupta Subject: [PATCH bpf-next v2 09/13] bpf: track low-32 scalar equality across narrowing stack fills Date: Thu, 10 Sep 2026 22:16:31 +0530 Message-ID: <20260910164635.459558-10-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260910164635.459558-1-vineet.gupta@linux.dev> References: <20260910164635.459558-1-vineet.gupta@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" A 32-bit fill from a wider spilled scalar has the same shape as a 32-bit mov from a wider source: the destination shares the slot's low 32 bits and nothing else. The relation was dropped instead of recorded, so a later narrowing of the spilled value never reached the filled register: r6 =3D ... /* full 64-bit unknown */ *(u64 *)(r10 - 8) =3D r6; /* slot linked to r6 */ r2 =3D *(u32 *)(r10 - 8); /* narrowing fill */ if w6 !=3D 0 goto ... /* not taken: r6's low 32 bits are 0 */ if r2 =3D=3D 0 goto ... /* not deduced today */ Record a low-32 link, as the mov arm does. Stack slots are already first-class members of an ->id set, so sync_linked_regs() and the reconstruction helpers apply unchanged. A fill narrower than 32 bits has no expressible relation and still drops it. Which kind to record depends on how the load fills the high half, so check_stack_read() takes is_ldsx. Doing it there rather than correcting afterwards in check_mem_access() keeps a single assignment. check_mem_access() in turn no longer clears the id of a ->subreg register on a sign-extending load: the link already records how the high half follows, which is what that sign extension produced. Signed-off-by: Vineet Gupta --- v2: new. Loads were asked for on the RFC cover letter; the fill is the only load whose destination inherits an id. kernel/bpf/verifier.c | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 308ff53232f0..89be1240c99a 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -3927,7 +3927,8 @@ static void bpf_diag_stack_read_uninit(struct bpf_ver= ifier_env *env, int off, in static int check_stack_read_fixed_off(struct bpf_verifier_env *env, /* func where src register points to */ struct bpf_func_state *reg_state, - int off, int size, int dst_regno) + int off, int size, int dst_regno, + bool is_ldsx) { struct bpf_verifier_state *vstate =3D env->cur_state; struct bpf_func_state *state =3D vstate->frame[vstate->curframe]; @@ -3968,18 +3969,34 @@ static int check_stack_read_fixed_off(struct bpf_ve= rifier_env *env, =20 if (size <=3D spill_size && bpf_stack_narrow_access_ok(off, size, spill_size)) { - if (env->bpf_capable && size =3D=3D 4 && spill_size =3D=3D 4 && - get_reg_width(reg) <=3D 32) + bool narrowing =3D get_reg_width(reg) > size * BITS_PER_BYTE; + /* + * A narrowing fill keeps only the slot's low 32 bits, + * so record a low-32 link rather than dropping the + * relation, as a 32-bit mov from a wide source does. + * Which kind depends on how the load fills the high + * half, hence is_ldsx. + */ + bool subreg_link =3D narrowing && size =3D=3D 4; + + if (env->bpf_capable && size =3D=3D 4 && + (subreg_link || (spill_size =3D=3D 4 && !narrowing))) /* 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] =3D *reg; =20 - /* Break the relation on a narrowing fill. - * coerce_reg_to_size will adjust the boundaries. - */ - if (get_reg_width(reg) > size * BITS_PER_BYTE) + if (subreg_link && reg->id) + state->regs[dst_regno].subreg =3D + is_ldsx ? SUBREG_SEXT : SUBREG_ZEXT; + else if (narrowing) + /* + * Nothing to relate: either the slot has + * no id to share, or the fill is narrower + * than the 32 bits a link can describe. + * coerce_reg_to_size adjusts the bounds. + */ clear_scalar_id(&state->regs[dst_regno]); } else { int spill_cnt =3D 0, zero_cnt =3D 0; @@ -4144,7 +4161,7 @@ static int check_stack_read_var_off(struct bpf_verifi= er_env *env, struct bpf_reg */ static int check_stack_read(struct bpf_verifier_env *env, struct bpf_reg_state *reg, argno_t ptr_argno, int off, int size, - int dst_regno) + int dst_regno, bool is_ldsx) { struct bpf_func_state *state =3D bpf_func(env, reg); int err; @@ -4183,7 +4200,7 @@ static int check_stack_read(struct bpf_verifier_env *= env, if (!var_off) { off +=3D reg->var_off.value; err =3D check_stack_read_fixed_off(env, state, off, size, - dst_regno); + dst_regno, is_ldsx); } else { /* Variable offset stack reads need more conservative handling * than fixed offset ones. Note that dst_regno >=3D 0 on this @@ -6639,7 +6656,7 @@ static int check_mem_access(struct bpf_verifier_env *= env, int insn_idx, struct b =20 if (t =3D=3D BPF_READ) err =3D check_stack_read(env, reg, argno, off, size, - value_regno); + value_regno, is_ldsx); else err =3D check_stack_write(env, reg, off, size, value_regno, insn_idx); @@ -6736,13 +6753,15 @@ static int check_mem_access(struct bpf_verifier_env= *env, int insn_idx, struct b * 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. + * drop the shared id in that case. A ->subreg link is + * the exception: it already records that only the low + * 32 bits are shared, and how the high half follows. */ bool no_sext =3D reg_umax(®s[value_regno]) < (1ULL << (size * BITS_PER_BYTE - 1)); =20 coerce_reg_to_size_sx(®s[value_regno], size); - if (!no_sext) + if (!no_sext && !regs[value_regno].subreg) clear_scalar_id(®s[value_regno]); } } --=20 2.53.0-Meta From nobody Fri Sep 25 16:01:35 2026 Received: from mta0.migadu.com (out-44.mta0.migadu.com [91.218.175.44]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id A48864BE44C for ; Thu, 10 Sep 2026 16:47:45 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.44 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058874; cv=none; b=CmCmiChHqSIdegqBb88wi2rIOTES8IQNRzpTI8xFKmoSPO+OvpMOXRTM8He+Z+my9E7bcoAN7uqkHRvHG80Fvw15q9t6C66H9F07uNlWMc8ukPeamAcj6itDlmMuC6tUSPKwH2e1aAuTbbic/8DoKAIrNfbI7iuKT2xBq8T5uKg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058874; c=relaxed/simple; bh=GS8eAaa+t8Q/6CXhV0Ju/1mP6N+mvNFoKi/CMt2gzac=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=EiI5i2hKTkZz+tGLmGsHpVxbYYOeqcmb3t2wCHbSh/64dhSLgt0J9/+oYwNTTrRxUhn/2l0Almc66qkvvXaOlmi2TpNSehQWyIHAWGdqkhfh2gSaJTYZw3P/tMqK+Tma/ZnNehKdXbxM3hNIQcwFT1dH0Nc8i/WQcWqOKtD7Stk= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=aCUpaTLn; arc=none smtp.client-ip=91.218.175.44 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="aCUpaTLn" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=GS8eAaa+t8Q/6CXhV0Ju/1mP6N+mvNFoKi/CMt2gzac=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789058860; v=1; x=1789663660; b=aCUpaTLnGFz4QHt+ns2P+KfYizkLSf+VQfuOpkMHPU+09Jw5FxLOkyrnx3GxCyV+rdpPUeBZ 4q8iqTui5uovUzSpWfDJ16o6rHNo4oyccM8BhyFhYRqFSsC+tFjXXVMlTGO/ObnxdFqj0Uk8RU/ jaonoalb6SLuEk27oT5Trzog= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id c6509f25ec2fa41e; Thu, 10 Sep 2026 16:47:40 +0000 X-Mizu-Trace-ID: c6509f25ec2fa41e X-Migadu-Flow: FLOW_OUT From: Vineet Gupta To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org, eddyz87@gmail.com, memxor@gmail.com Cc: martin.lau@linux.dev, song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org, emil@etsalapatis.com, ihor.solodrai@linux.dev, john.fastabend@gmail.com, shuah@kernel.org, bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Vineet Gupta Subject: [PATCH bpf-next v2 10/13] selftests/bpf: cover the low-32 link for narrowing stack fills Date: Thu, 10 Sep 2026 22:16:32 +0530 Message-ID: <20260910164635.459558-11-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260910164635.459558-1-vineet.gupta@linux.dev> References: <20260910164635.459558-1-vineet.gupta@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Five programs, following the mov set: - zext_fill_narrow_from_wide_spill: narrowing the spilled value reaches the filled register - sext_fill_narrow_from_wide_spill: the same for a sign-extending fill, and the value that arrives is sign-extended - zext_fill_full_width_keeps_full_link: a full-width fill is a plain 64-bit equality and must keep propagating as before - zext_fill_byte_forms_no_link: a sub-word fill is below the low-32 model, so no link is formed - fill_kinds_reach_different_values: the same, for the two fill kinds off one slot The first two are the ones that pin the new behaviour: disabling link formation in the fill arm makes both fail. The other three are guards against the feature applying where it should not, and hold either way. Signed-off-by: Vineet Gupta --- v2: new, with 9/13. .../bpf/progs/verifier_linked_scalars.c | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c b/= tools/testing/selftests/bpf/progs/verifier_linked_scalars.c index 9d060d8b0c1f..e8a44e7579c8 100644 --- a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c +++ b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c @@ -966,6 +966,88 @@ __naked void zext_mov_breaks_add_const_src(void) : __clobber_all); } =20 +/* + * A narrowing fill keeps only the slot's low 32 bits, so a later narrowin= g of + * the spilled value must still reach the filled register. Same relation a= s a + * 32-bit mov from a wide source, with the stack slot as the base. + */ +SEC("socket") +__success +__naked void zext_fill_narrow_from_wide_spill(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + call %[bpf_get_prandom_u32]; \ + r0 <<=3D 32; \ + r6 |=3D r0; /* r6 =3D full 64-bit unknown */ \ + *(u64 *)(r10 - 8) =3D r6; /* slot linked to r6 */ \ + r2 =3D *(u32 *)(r10 - 8); /* narrowing fill, forms the link */ \ + if w6 !=3D 0 goto 1f; /* narrows r6, propagates to r2 */ \ + if r2 =3D=3D 0 goto 1f; \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * A full-width fill of a wide slot is a plain 64-bit equality, not a low-= 32 + * link, so it must keep propagating exactly as before. + */ +SEC("socket") +__success +__naked void zext_fill_full_width_keeps_full_link(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + call %[bpf_get_prandom_u32]; \ + r0 <<=3D 32; \ + r6 |=3D r0; \ + *(u64 *)(r10 - 8) =3D r6; \ + r2 =3D *(u64 *)(r10 - 8); /* no narrowing */ \ + if r6 !=3D 0 goto 1f; \ + if r2 =3D=3D 0 goto 1f; \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * A sub-word fill is below the low-32 model, so no link is formed and the + * relation is dropped as before. + */ +SEC("socket") +__failure __msg("div by zero") +__naked void zext_fill_byte_forms_no_link(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + call %[bpf_get_prandom_u32]; \ + r0 <<=3D 32; \ + r6 |=3D r0; \ + *(u64 *)(r10 - 8) =3D r6; \ + r2 =3D *(u8 *)(r10 - 8); /* 1-byte fill: no link */ \ + if w6 !=3D 0 goto 1f; \ + if r2 =3D=3D 0 goto 1f; /* not deduced */ \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + #ifdef CAN_USE_MOVSX =20 /* @@ -1129,6 +1211,68 @@ __naked void sext_kinds_reach_different_values(void) : __clobber_all); } =20 +/* + * The sign-extending counterpart: the filled register is the sign extensi= on of + * the slot's low 32 bits, so a narrowing of those bits arrives sign-exten= ded. + */ +SEC("socket") +__success +__naked void sext_fill_narrow_from_wide_spill(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + call %[bpf_get_prandom_u32]; \ + r0 <<=3D 32; \ + r6 |=3D r0; /* r6 =3D full 64-bit unknown */ \ + *(u64 *)(r10 - 8) =3D r6; /* slot linked to r6 */ \ + r2 =3D *(s32 *)(r10 - 8); /* narrowing sx fill */ \ + if w6 !=3D -1 goto 1f; /* narrows r6, propagates to r2 */ \ + if r2 =3D=3D -1 goto 1f; /* sign-extended, not 0xffffffff */ \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * The same, for the two fill kinds off one slot. + */ +SEC("socket") +__failure __msg("div by zero") +__flag(BPF_F_TEST_STATE_FREQ) +__naked void fill_kinds_reach_different_values(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + r6 &=3D 1; \ + call %[bpf_get_prandom_u32]; \ + r8 =3D r0; \ + call %[bpf_get_prandom_u32]; \ + r0 <<=3D 32; \ + r8 |=3D r0; \ + *(u64 *)(r10 - 8) =3D r8; \ + if r6 >=3D 1 goto 2f; \ + r2 =3D *(s32 *)(r10 - 8); /* sign-extending fill */ \ + goto 1f; \ +2: \ + r2 =3D *(u32 *)(r10 - 8); /* zero-extending fill */ \ +1: \ + if w8 !=3D -1 goto 3f; \ + if r2 =3D=3D -1 goto 3f; /* only the sign-extending path */ \ + r0 /=3D 0; \ +3: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + #endif /* CAN_USE_MOVSX */ =20 char _license[] SEC("license") =3D "GPL"; --=20 2.53.0-Meta From nobody Fri Sep 25 16:01:35 2026 Received: from mta0.migadu.com (out-54.mta0.migadu.com [91.218.175.54]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 846A1485931 for ; Thu, 10 Sep 2026 16:47:49 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.54 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058878; cv=none; b=mHIgibxQ4ZXrhNoOtj0PsaX8y0NTJfDkW+S/aksm7G85BPObrkDwDKFpFws8JXOtOJ0OcGdyMU2TrB+S0pQLCikEhQpk7A5g2pF8xBxrXuMaOXcLDASd/lhjBx977Jb+7MaBOI6QOYeBVDy2SUn5rAIiRPgCnwvvvbCXFaiIQYk= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058878; c=relaxed/simple; bh=mq7GOoQK30iIGJJGlQCVwVgC5MxtIYTjkgeg/GW2OnI=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=rRvBh8GDycpplke9WXIDjguhGyk0NrRmkNnYS7G/hYnvxqCMng7Lc0AwAnWHVLiKqpCPIC3jjCAJ6/94hWMF3Lnyzh1OTUQZ1KezDbX+NA+zEOfmn/CLz7xgDlvXfPiKbp63VxuEfWx9x5RZ+6v1V8REU7xNc5diAUQMKdGWpng= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=kg5Vg68y; arc=none smtp.client-ip=91.218.175.54 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="kg5Vg68y" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=mq7GOoQK30iIGJJGlQCVwVgC5MxtIYTjkgeg/GW2OnI=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789058864; v=1; x=1789663664; b=kg5Vg68yP6kF3dtU/xcNm+rhdyYFY1ZPNkCuLEA80pBnSiU/LBHMcN/PKayXR8RO3xnV7yZ8 56pkrassfA9rAyfTHqQcPR/3NEYEQfIe+PbURiRI99TtAAVNMRh6a+8FKZ86/l3luhMYYk06cYd j9jSs6cskuVb3lx6B/visEgw= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id 100b8265b6c2deba; Thu, 10 Sep 2026 16:47:44 +0000 X-Mizu-Trace-ID: 100b8265b6c2deba X-Migadu-Flow: FLOW_OUT From: Vineet Gupta To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org, eddyz87@gmail.com, memxor@gmail.com Cc: martin.lau@linux.dev, song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org, emil@etsalapatis.com, ihor.solodrai@linux.dev, john.fastabend@gmail.com, shuah@kernel.org, bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Vineet Gupta Subject: [PATCH bpf-next v2 11/13] bpf: record what a narrowing spill actually stores Date: Thu, 10 Sep 2026 22:16:33 +0530 Message-ID: <20260910164635.459558-12-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260910164635.459558-1-vineet.gupta@linux.dev> References: <20260910164635.459558-1-vineet.gupta@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" A spill narrower than its source saves the register state untruncated: save_register_state() does state->stack[spi].spilled_ptr =3D *reg; and marks only @size bytes STACK_SPILL. So after r6 =3D ... /* full 64-bit unknown */ *(u32 *)(r10 - 8) =3D r6; /* four bytes reach memory */ the slot claims to hold all of r6 while memory holds its low half. That is sound and the fill truncates later, but stacksafe() compares the slot through regsafe(), so the imprecision reaches state comparison. Truncate the recorded state to the bytes stored. Nothing reads the slot as wider: the remaining bytes are STACK_MISC, so a larger access fails the size <=3D spill_size test and never takes the register-fill path. This is independent of linking -- it applies whether or not the source carries an id -- hence a patch of its own. coerce_reg_to_size() moves up to be visible at the spill site, otherwise unchanged. spill_subregs_preserve_stack_zero pins the old state and moves with it. It checks that STACK_ZERO bytes survive a subreg spill, which is unaffected; only the recorded scalar tightens. Signed-off-by: Vineet Gupta --- v2: new. kernel/bpf/verifier.c | 60 +++++++++++-------- .../selftests/bpf/progs/verifier_spill_fill.c | 10 +++- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 89be1240c99a..6cb35fc0d0fb 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -3509,6 +3509,31 @@ static void assign_scalar_id_before_mov(struct bpf_v= erifier_env *env, src_reg->id =3D ++env->id_gen; } =20 +static void coerce_reg_to_size(struct bpf_reg_state *reg, int size) +{ + u64 mask; + + /* clear high bits in bit representation */ + reg->var_off =3D tnum_cast(reg->var_off, size); + + /* fix arithmetic bounds */ + mask =3D ((u64)1 << (size * 8)) - 1; + if ((reg_umin(reg) & ~mask) =3D=3D (reg_umax(reg) & ~mask)) + reg_set_urange64(reg, reg_umin(reg) & mask, reg_umax(reg) & mask); + else + reg_set_urange64(reg, 0, mask); + + /* + * If size is smaller than 32bit register the 32bit register + * values are also truncated so we push 64-bit bounds into + * 32-bit bounds. Above were truncated < 32-bits already. + */ + if (size < 4) + __mark_reg32_unbounded(reg); + + reg_bounds_sync(reg); +} + static void save_register_state(struct bpf_verifier_env *env, struct bpf_func_state *state, int spi, struct bpf_reg_state *reg, @@ -3646,9 +3671,16 @@ static int check_stack_write_fixed_off(struct bpf_ve= rifier_env *env, 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) + if (!reg_value_fits) { + /* + * Only the low @size bytes reach memory, so record + * what the slot holds rather than the wider source + * it came from. + */ + coerce_reg_to_size(&state->stack[spi].spilled_ptr, size); + /* Break the relation on a narrowing spill. */ clear_scalar_id(&state->stack[spi].spilled_ptr); + } } else if (!reg && !(off % BPF_REG_SIZE) && is_bpf_st_mem(insn) && env->bpf_capable) { struct bpf_reg_state *tmp_reg =3D &env->fake_reg[0]; @@ -5738,30 +5770,6 @@ static void sext_32_to_64(struct bpf_reg_state *reg) /* truncate register to smaller size (in bytes) * must be called with size < BPF_REG_SIZE */ -static void coerce_reg_to_size(struct bpf_reg_state *reg, int size) -{ - u64 mask; - - /* clear high bits in bit representation */ - reg->var_off =3D tnum_cast(reg->var_off, size); - - /* fix arithmetic bounds */ - mask =3D ((u64)1 << (size * 8)) - 1; - if ((reg_umin(reg) & ~mask) =3D=3D (reg_umax(reg) & ~mask)) - reg_set_urange64(reg, reg_umin(reg) & mask, reg_umax(reg) & mask); - else - reg_set_urange64(reg, 0, mask); - - /* If size is smaller than 32bit register the 32bit register - * values are also truncated so we push 64-bit bounds into - * 32-bit bounds. Above were truncated < 32-bits already. - */ - if (size < 4) - __mark_reg32_unbounded(reg); - - reg_bounds_sync(reg); -} - static void set_sext64_default_val(struct bpf_reg_state *reg, int size) { if (size =3D=3D 1) { diff --git a/tools/testing/selftests/bpf/progs/verifier_spill_fill.c b/tool= s/testing/selftests/bpf/progs/verifier_spill_fill.c index 39a1766dae3f..487e0a1f395c 100644 --- a/tools/testing/selftests/bpf/progs/verifier_spill_fill.c +++ b/tools/testing/selftests/bpf/progs/verifier_spill_fill.c @@ -464,9 +464,13 @@ l0_%=3D: r1 >>=3D 16; \ SEC("raw_tp") __log_level(2) __success -__msg("fp-8=3D0m??scalar()") -__msg("fp-16=3D00mm??scalar()") -__msg("fp-24=3D00mm???scalar()") +/* + * The slot records what the store put there, not the wider source it came + * from, so each scalar is bounded by the size of its spill. + */ +__msg("fp-8=3D0m??scalar(smin=3D0,smax=3Dumax=3D0xffffffff,var_off=3D(0x0;= 0xffffffff))") +__msg("fp-16=3D00mm??scalar(smin=3Dsmin32=3D0,smax=3Dumax=3Dsmax32=3Dumax3= 2=3D0xffff,var_off=3D(0x0; 0xffff))") +__msg("fp-24=3D00mm???scalar(smin=3Dsmin32=3D0,smax=3Dumax=3Dsmax32=3Dumax= 32=3D255,var_off=3D(0x0; 0xff))") __naked void spill_subregs_preserve_stack_zero(void) { asm volatile ( --=20 2.53.0-Meta From nobody Fri Sep 25 16:01:35 2026 Received: from mta0.migadu.com (out-69.mta0.migadu.com [91.218.175.69]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 8E3FD534461 for ; Thu, 10 Sep 2026 16:47:52 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.69 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058882; cv=none; b=Q6Tr2Q0EPuyUZt7MkMq7SJMMpSLeOoeW2G0MDb8YnXGL83YKnTmQ2XLvpKWpCP3fKBpnNG+25AQVj1XbmQTpi5D6XjqSEJqeFBo+l8Wp1ZBroBrhZqox+EUhTciU2wBvOXDnf5+uf3Dq3cNQluNawIrBt0Wlu7WuaTIEva/LIKI= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058882; c=relaxed/simple; bh=mrYY+8ju/CiFozDqUvghoNRZx9UJQJPLR3n4E5rpxG0=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=bo6hZECj+mIH6gkPgFTWcyDUEGnjumxqxnC+OFvybabM5KLC+9MhnxOKcWPdcSffNh9qE0v8RUe2el2bheLkNmIUrol1ZuLjQSCqgfwGAHYjfwrv9KzKVc28jYDqcq3cDPsFfw5d1tsZ70LSAH9eL46HYQ/Whc8mQ2dfvb8l2tw= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=rE19zifN; arc=none smtp.client-ip=91.218.175.69 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="rE19zifN" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=mrYY+8ju/CiFozDqUvghoNRZx9UJQJPLR3n4E5rpxG0=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789058869; v=1; x=1789663669; b=rE19zifNqL4EhL9G/ZjzTZzXUIPPtab0xTAGXEI3CeSYz/kUbb980Ow+J35oLW4Fr/jyq6JU Z7YT1WuCG6LP9iIP8xzYE905uMHqotKN50nM51T/8bBAzKptqQFUk71pyKF5Pi86IpMI8rkjncI xSct+xch90Nu+d4pU1gziH80= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id dc8a79c2cacc61c9; Thu, 10 Sep 2026 16:47:48 +0000 X-Mizu-Trace-ID: dc8a79c2cacc61c9 X-Migadu-Flow: FLOW_OUT From: Vineet Gupta To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org, eddyz87@gmail.com, memxor@gmail.com Cc: martin.lau@linux.dev, song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org, emil@etsalapatis.com, ihor.solodrai@linux.dev, john.fastabend@gmail.com, shuah@kernel.org, bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Vineet Gupta Subject: [PATCH bpf-next v2 12/13] bpf: track low-32 scalar equality across narrowing stack spills Date: Thu, 10 Sep 2026 22:16:34 +0530 Message-ID: <20260910164635.459558-13-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260910164635.459558-1-vineet.gupta@linux.dev> References: <20260910164635.459558-1-vineet.gupta@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" A spill narrower than its source stores the source's low 32 bits, so the slot is their zero-extension -- the same relation a 32-bit mov records, in the store direction: r6 =3D ... /* full 64-bit unknown */ *(u32 *)(r10 - 8) =3D r6; /* slot holds r6's low 32 bits */ r2 =3D *(u32 *)(r10 - 8); if w6 !=3D 0 goto ... /* not taken: r6's low 32 bits are 0 */ if r2 =3D=3D 0 goto ... /* not deduced today */ Record a low-32 link instead of dropping the relation. A store never sign-extends, so SUBREG_ZEXT is the only kind that arises here and no is_ldsx equivalent is needed. This composes with the fill side rather than stacking on it. The previous patch left the slot recording exactly what memory holds, so its width is already within 32 bits and the fill sees an ordinary narrow source: the destination copies the slot's descriptor, staying one hop from the same base rather than becoming a link to a link. spill_subregs_preserve_stack_zero picks up the id on its 4-byte spill. The 16- and 8-bit spills in the same program do not, which is the size gate working: a link can only describe the low 32 bits. Signed-off-by: Vineet Gupta --- v2: new, with 11/13. kernel/bpf/verifier.c | 22 ++++++++++++++++--- .../selftests/bpf/progs/verifier_spill_fill.c | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 6cb35fc0d0fb..9e63eddb9023 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -3665,10 +3665,19 @@ static int check_stack_write_fixed_off(struct bpf_v= erifier_env *env, mark_stack_slot_scratched(env, spi); if (reg && !(off % BPF_REG_SIZE) && reg->type =3D=3D SCALAR_VALUE && env-= >bpf_capable) { bool reg_value_fits; + bool subreg_link; =20 reg_value_fits =3D get_reg_width(reg) <=3D BITS_PER_BYTE * size; + /* + * A narrowing spill stores the low 32 bits of the source, so + * the slot is their zero-extension: record a low-32 link + * rather than dropping the relation, as a 32-bit mov does. + * A store never sign-extends, so there is only one kind here. + */ + subreg_link =3D !reg_value_fits && size =3D=3D 4; + /* Make sure that reg had an ID to build a relation on spill. */ - if (reg_value_fits) + if (reg_value_fits || subreg_link) assign_scalar_id_before_mov(env, reg); save_register_state(env, state, spi, reg, size); if (!reg_value_fits) { @@ -3678,8 +3687,15 @@ static int check_stack_write_fixed_off(struct bpf_ve= rifier_env *env, * it came from. */ coerce_reg_to_size(&state->stack[spi].spilled_ptr, size); - /* Break the relation on a narrowing spill. */ - clear_scalar_id(&state->stack[spi].spilled_ptr); + if (subreg_link && reg->id) + state->stack[spi].spilled_ptr.subreg =3D SUBREG_ZEXT; + else + /* + * Nothing to relate: either the source has no + * id to share, or the store is narrower than + * the 32 bits a link can describe. + */ + clear_scalar_id(&state->stack[spi].spilled_ptr); } } else if (!reg && !(off % BPF_REG_SIZE) && is_bpf_st_mem(insn) && env->bpf_capable) { diff --git a/tools/testing/selftests/bpf/progs/verifier_spill_fill.c b/tool= s/testing/selftests/bpf/progs/verifier_spill_fill.c index 487e0a1f395c..1042856b97c2 100644 --- a/tools/testing/selftests/bpf/progs/verifier_spill_fill.c +++ b/tools/testing/selftests/bpf/progs/verifier_spill_fill.c @@ -468,7 +468,7 @@ __success * The slot records what the store put there, not the wider source it came * from, so each scalar is bounded by the size of its spill. */ -__msg("fp-8=3D0m??scalar(smin=3D0,smax=3Dumax=3D0xffffffff,var_off=3D(0x0;= 0xffffffff))") +__msg("fp-8=3D0m??scalar(id=3D{{[0-9]+}}.lo32,smin=3D0,smax=3Dumax=3D0xfff= fffff,var_off=3D(0x0; 0xffffffff))") __msg("fp-16=3D00mm??scalar(smin=3Dsmin32=3D0,smax=3Dumax=3Dsmax32=3Dumax3= 2=3D0xffff,var_off=3D(0x0; 0xffff))") __msg("fp-24=3D00mm???scalar(smin=3Dsmin32=3D0,smax=3Dumax=3Dsmax32=3Dumax= 32=3D255,var_off=3D(0x0; 0xff))") __naked void spill_subregs_preserve_stack_zero(void) --=20 2.53.0-Meta From nobody Fri Sep 25 16:01:35 2026 Received: from mta0.migadu.com (out-81.mta0.migadu.com [91.218.175.81]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 69D8A514767 for ; Thu, 10 Sep 2026 16:47:56 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.81 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058882; cv=none; b=jyzHsb0tXZf4NOjZbs9InOqhLIKH4CvVpiNlu/2ju8ZdOs/V/mcdF4v/Xk7utVSZaTw0vAAHLTrc9ji2/s+CfnRMtljhL3lUAJu8P6P+9vELLR+r3ovWlt+aEyhia5mKMINOIHI7zz0EgvvzsoMZbyLuXJBmDTB17+88FqIkyus= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1789058882; c=relaxed/simple; bh=0ha2sGkGjon+eeZcJRjjPBS803qtaTj8dM/pQbd2s7w=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=hCoDU0cURUGVdIuiTENDXwFZ6DrwHwIBadUTZ5VzHhJfKj1TGaxWqfeP0yc11O4Hosd5Y+Y9Ws+suPXTXPorTMhXyuSklng3MAqiBNR8/uYbrsrkBc8hftls3jcy6cIBts9vjs7JKqg0WfkhLcgy1zWjoMT3SFnMqkBgUfZdFrg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev; spf=pass smtp.mailfrom=linux.dev; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b=Nw7nC7sZ; arc=none smtp.client-ip=91.218.175.81 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.dev Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.dev Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=linux.dev header.i=@linux.dev header.b="Nw7nC7sZ" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=0ha2sGkGjon+eeZcJRjjPBS803qtaTj8dM/pQbd2s7w=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1789058873; v=1; x=1789663673; b=Nw7nC7sZHlrBu1Mxta+EC44VdEdsyWl8pqsRoTjoZgl1d/85uUZ6L8DcVoHFVJS4Bo2k69FT r5fvZ1B9iZiBfRALjaYwDgZobKQHrg+07C7mPjMyc7hbqeqicGp6T199iTXaMHyYTnAuu2ILE2f ME+2kvdlju54kfBpVBxVCb34= X-Envelope-To: linux-kernel@vger.kernel.org Received: by smtp.migadu.com with ESMTPS id b8644919bbe2bd5d; Thu, 10 Sep 2026 16:47:53 +0000 X-Mizu-Trace-ID: b8644919bbe2bd5d X-Migadu-Flow: FLOW_OUT From: Vineet Gupta To: ast@kernel.org, daniel@iogearbox.net, andrii@kernel.org, eddyz87@gmail.com, memxor@gmail.com Cc: martin.lau@linux.dev, song@kernel.org, yonghong.song@linux.dev, jolsa@kernel.org, emil@etsalapatis.com, ihor.solodrai@linux.dev, john.fastabend@gmail.com, shuah@kernel.org, bpf@vger.kernel.org, linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org, Vineet Gupta Subject: [PATCH bpf-next v2 13/13] selftests/bpf: cover the low-32 link for narrowing stack spills Date: Thu, 10 Sep 2026 22:16:35 +0530 Message-ID: <20260910164635.459558-14-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260910164635.459558-1-vineet.gupta@linux.dev> References: <20260910164635.459558-1-vineet.gupta@linux.dev> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Two programs: - zext_narrowing_spill_keeps_link: a 32-bit spill of a wide register, filled back out, still follows a later narrowing of the source - zext_narrowing_spill_byte_no_link: a 1-byte spill forms no link, since the relation is not expressible Signed-off-by: Vineet Gupta --- v2: new, with 12/13. .../bpf/progs/verifier_linked_scalars.c | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c b/= tools/testing/selftests/bpf/progs/verifier_linked_scalars.c index e8a44e7579c8..ca7471f0293d 100644 --- a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c +++ b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c @@ -1048,6 +1048,60 @@ __naked void zext_fill_byte_forms_no_link(void) : __clobber_all); } =20 +/* + * A narrowing spill stores only the source's low 32 bits, so the slot is = their + * zero-extension. A later narrowing of the source must still reach anythi= ng + * filled back out of that slot. + */ +SEC("socket") +__success +__naked void zext_narrowing_spill_keeps_link(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + call %[bpf_get_prandom_u32]; \ + r0 <<=3D 32; \ + r6 |=3D r0; /* r6 =3D full 64-bit unknown */ \ + *(u32 *)(r10 - 8) =3D r6; /* narrowing spill, forms the link */ \ + r2 =3D *(u32 *)(r10 - 8); /* fill it back */ \ + if w6 !=3D 0 goto 1f; /* narrows r6, propagates to r2 */ \ + if r2 =3D=3D 0 goto 1f; \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * A sub-word spill is below the low-32 model, so no link is formed there. + */ +SEC("socket") +__failure __msg("div by zero") +__naked void zext_narrowing_spill_byte_no_link(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; \ + call %[bpf_get_prandom_u32]; \ + r0 <<=3D 32; \ + r6 |=3D r0; \ + *(u8 *)(r10 - 8) =3D r6; /* 1-byte spill: no link */ \ + r2 =3D *(u8 *)(r10 - 8); \ + if w6 !=3D 0 goto 1f; \ + if r2 =3D=3D 0 goto 1f; /* not deduced */ \ + r0 /=3D 0; \ +1: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + #ifdef CAN_USE_MOVSX =20 /* --=20 2.53.0-Meta