From nobody Mon Sep 28 23:51:14 2026 Received: from mta1.migadu.com (out-147.mta1.migadu.com [95.215.58.147]) (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 45E35373BEC for ; Fri, 14 Aug 2026 23:20:03 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.147 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786749606; cv=none; b=Z6yKEZy8wvTPslui+c5x8s4Re2F6Lgc0auBVYYOIhzGjn8NnDMP5sCgA52tCy5Kcr/W/+pKQ4jU6iyx8X8TsFz3pdjAmbTYcxHksqlN47bg0+ZLSbcoAy2LVE+fwyzNaf2IwgYvimHFV4ds2emdttcdI/xIgtnLBoro9Zs/oZU4= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786749606; c=relaxed/simple; bh=jeRfA0YnR5WMkbNr7KUT+VYUIX8ePdKg2Lq5oDnizEk=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=KnXCczdn68h+qglIuEhH6jp5LvbFMhCgmPFefd36zpmm8Dw/5fj8th+jV80MuGbFhpoXO0kKBz1Q5QJor6x2xNKYcSpHV7XEYUcEdE7Uoq2aYFNaTm+4P884JoE6C2xq+17099366NB26asPtabFOB2NpjYa1yBwTA0QuPDyhKQ= 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=jwGr4BS5; arc=none smtp.client-ip=95.215.58.147 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="jwGr4BS5" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=jeRfA0YnR5WMkbNr7KUT+VYUIX8ePdKg2Lq5oDnizEk=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1786749602; v=1; x=1787354402; b=jwGr4BS515Fi0N6Dh5IN9M2EUk5qIS72d+rCu4SoS35pdgWq/T/HFszunkB0x+5TIgb50iH9 /gTTBFjGiObedoWUQbexIQracEtc4/Jo3A8k+LLG/xmCtcfDTLtxr8DfowasikeKPkCDzYS+vXC uWVGrO9yd1X4v23f1dSNKNy0= X-Envelope-To: linux-kernel@vger.kernel.org Received: from gvineet-fedora-PF5JGVFY.thefacebook.com (2620:10d:c090:500::6:7be4) by smtp.migadu.com with ESMTPS id 194f4ff2d32dc14a; Fri, 14 Aug 2026 23:19:59 +0000 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: [RFC bpf-next 1/6] bpf: turn bpf_reg_state->precise into a flags field [NFC] Date: Fri, 14 Aug 2026 16:19:40 -0700 Message-ID: <20260814231945.3884596-2-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260814231945.3884596-1-vineet.gupta@linux.dev> References: <20260814231945.3884596-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 carries a single bool, ->precise. Other per-register boolean properties exist (and more are coming), so convert the bool into a u8, call it flags and give the property a name. - bool precise; +#define BPF_FLAG_PRECISE (1U << 7) + u8 flags; Both occupy 1 byte at the same offset, so the struct layout is unchanged. ->precise was the last field, after ->frameno, and ->flags takes exactly that slot, so the memcmp()/offsetof() based comparisons are unaffected: every one of them stops at offsetof(id), offsetof(var_off) or offsetof(frameno), i.e. at or before the field either way. That tail position is not an accident -- it is where fields live that are compared semantically rather than byte-wise. ->precise is never memcmp()ed; regsafe() tests it explicitly, and an imprecise old scalar is a wildcard: if (!reg_is_precise(rold) && exact =3D=3D NOT_EXACT) return true; PRECISE also takes bit 7 rather than bit 0, because it is the odd one out among the flags that will share this byte: the others describe how a regist= er relates to its ->id set and are cleared as a group, while PRECISE belongs to the register alone and must survive that clearing. Growing the rest up from bit 0 keeps a clear-the-link-bits mask from reaching it by construction. Reads go through a helper, since they are the common case and read better. Set and clear stay open-coded as the usual reg->flags |=3D / &=3D ~ bit ops. No functional change intended. Suggested-by: Eduard Zingerman Signed-off-by: Vineet Gupta --- include/linux/bpf_verifier.h | 18 ++++++++++++++++-- kernel/bpf/backtrack.c | 22 +++++++++++----------- kernel/bpf/log.c | 2 +- kernel/bpf/states.c | 10 +++++----- kernel/bpf/verifier.c | 14 +++++++++----- 5 files changed, 42 insertions(+), 24 deletions(-) diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h index 27b43fda9b17..ebab483fc7f2 100644 --- a/include/linux/bpf_verifier.h +++ b/include/linux/bpf_verifier.h @@ -162,10 +162,24 @@ struct bpf_reg_state { * pointing to bpf_func_state. */ u32 frameno; - /* if (!precise && SCALAR_VALUE) min/max/tnum don't affect safety */ - bool precise; + /* + * Register state flags. + * BPF_FLAG_PRECISE: if unset, and this is a SCALAR_VALUE, then + * min/max/tnum don't affect safety. + * + * PRECISE is a property of this register alone, so it is placed at bit 7, + * apart from the link flags, which grow up from bit 0 and are cleared as + * a group -- a clear-the-link-bits mask can then never reach it. + */ +#define BPF_FLAG_PRECISE (1U << 7) + u8 flags; }; =20 +static inline bool reg_is_precise(const struct bpf_reg_state *reg) +{ + return reg->flags & BPF_FLAG_PRECISE; +} + static inline s64 reg_smin(const struct bpf_reg_state *reg) { return cnum64_smin(reg->r64); diff --git a/kernel/bpf/backtrack.c b/kernel/bpf/backtrack.c index a2b18a9f1694..400c69152ed2 100644 --- a/kernel/bpf/backtrack.c +++ b/kernel/bpf/backtrack.c @@ -675,9 +675,9 @@ void bpf_mark_all_scalars_precise(struct bpf_verifier_e= nv *env, func =3D st->frame[i]; for (j =3D 0; j < BPF_REG_FP; j++) { reg =3D &func->regs[j]; - if (reg->type !=3D SCALAR_VALUE || reg->precise) + if (reg->type !=3D SCALAR_VALUE || reg_is_precise(reg)) continue; - reg->precise =3D true; + reg->flags |=3D BPF_FLAG_PRECISE; if (env->log.level & BPF_LOG_LEVEL2) { verbose(env, "force_precise: frame%d: forcing r%d to be precise\n", i, j); @@ -687,9 +687,9 @@ void bpf_mark_all_scalars_precise(struct bpf_verifier_e= nv *env, if (!bpf_is_spilled_reg(&func->stack[j])) continue; reg =3D &func->stack[j].spilled_ptr; - if (reg->type !=3D SCALAR_VALUE || reg->precise) + if (reg->type !=3D SCALAR_VALUE || reg_is_precise(reg)) continue; - reg->precise =3D true; + reg->flags |=3D BPF_FLAG_PRECISE; if (env->log.level & BPF_LOG_LEVEL2) { verbose(env, "force_precise: frame%d: forcing fp%d to be precise\n", i, -(j + 1) * 8); @@ -851,7 +851,7 @@ int bpf_mark_chain_precision(struct bpf_verifier_env *e= nv, reg =3D &st->frame[0]->regs[i]; bt_clear_reg(bt, i); if (reg->type =3D=3D SCALAR_VALUE) { - reg->precise =3D true; + reg->flags |=3D BPF_FLAG_PRECISE; *changed =3D true; } } @@ -912,10 +912,10 @@ int bpf_mark_chain_precision(struct bpf_verifier_env = *env, bt_clear_frame_reg(bt, fr, i); continue; } - if (reg->precise) { + if (reg_is_precise(reg)) { bt_clear_frame_reg(bt, fr, i); } else { - reg->precise =3D true; + reg->flags |=3D BPF_FLAG_PRECISE; *changed =3D true; } } @@ -932,10 +932,10 @@ int bpf_mark_chain_precision(struct bpf_verifier_env = *env, continue; } reg =3D &func->stack[i].spilled_ptr; - if (reg->precise) { + if (reg_is_precise(reg)) { bt_clear_frame_slot(bt, fr, i); } else { - reg->precise =3D true; + reg->flags |=3D BPF_FLAG_PRECISE; *changed =3D true; } } @@ -943,10 +943,10 @@ int bpf_mark_chain_precision(struct bpf_verifier_env = *env, if (!bt_is_frame_stack_arg_slot_set(bt, fr, i)) continue; reg =3D &func->stack_arg_regs[i]; - if (reg->type !=3D SCALAR_VALUE || reg->precise) { + if (reg->type !=3D SCALAR_VALUE || reg_is_precise(reg)) { bt_clear_frame_stack_arg_slot(bt, fr, i); } else { - reg->precise =3D true; + reg->flags |=3D BPF_FLAG_PRECISE; *changed =3D true; } } diff --git a/kernel/bpf/log.c b/kernel/bpf/log.c index b740fa73ee26..9a4445d492c9 100644 --- a/kernel/bpf/log.c +++ b/kernel/bpf/log.c @@ -640,7 +640,7 @@ static void print_reg_state(struct bpf_verifier_env *en= v, const char *sep =3D ""; =20 t =3D reg->type; - if (t =3D=3D SCALAR_VALUE && reg->precise) + if (t =3D=3D SCALAR_VALUE && reg_is_precise(reg)) verbose(env, "P"); if (t =3D=3D SCALAR_VALUE && tnum_is_const(reg->var_off)) { verbose_snum(env, reg->var_off.value); diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c index 4e6aafad33bd..f7a0314fa106 100644 --- a/kernel/bpf/states.c +++ b/kernel/bpf/states.c @@ -548,7 +548,7 @@ static bool regsafe(struct bpf_verifier_env *env, struc= t bpf_reg_state *rold, return memcmp(rold, rcur, offsetof(struct bpf_reg_state, id)) =3D=3D 0 = && check_scalar_ids(rold->id, rcur->id, idmap); } - if (!rold->precise && exact =3D=3D NOT_EXACT) + if (!reg_is_precise(rold) && exact =3D=3D NOT_EXACT) return true; /* * Linked register tracking uses rold->id to detect relationships. @@ -1034,7 +1034,7 @@ static int propagate_precision(struct bpf_verifier_en= v *env, first =3D true; for (i =3D 0; i < BPF_REG_FP; i++, state_reg++) { if (state_reg->type !=3D SCALAR_VALUE || - !state_reg->precise) + !reg_is_precise(state_reg)) continue; if (env->log.level & BPF_LOG_LEVEL2) { if (first) @@ -1051,7 +1051,7 @@ static int propagate_precision(struct bpf_verifier_en= v *env, continue; state_reg =3D &state->stack[i].spilled_ptr; if (state_reg->type !=3D SCALAR_VALUE || - !state_reg->precise) + !reg_is_precise(state_reg)) continue; if (env->log.level & BPF_LOG_LEVEL2) { if (first) @@ -1223,7 +1223,7 @@ static void mark_all_scalars_imprecise(struct bpf_ver= ifier_env *env, struct bpf_ reg =3D &func->regs[j]; if (reg->type !=3D SCALAR_VALUE) continue; - reg->precise =3D false; + reg->flags &=3D ~BPF_FLAG_PRECISE; } for (j =3D 0; j < func->allocated_stack / BPF_REG_SIZE; j++) { if (!bpf_is_spilled_reg(&func->stack[j])) @@ -1231,7 +1231,7 @@ static void mark_all_scalars_imprecise(struct bpf_ver= ifier_env *env, struct bpf_ reg =3D &func->stack[j].spilled_ptr; if (reg->type !=3D SCALAR_VALUE) continue; - reg->precise =3D false; + reg->flags &=3D ~BPF_FLAG_PRECISE; } } } diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 6ac1afced20b..8925749d636e 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -1830,7 +1830,9 @@ static void __mark_reg_const_zero(const struct bpf_ve= rifier_env *env, struct bpf /* all scalars are assumed imprecise initially (unless unprivileged, * in which case everything is forced to be precise) */ - reg->precise =3D !env->bpf_capable; + reg->flags &=3D ~BPF_FLAG_PRECISE; + if (!env->bpf_capable) + reg->flags |=3D BPF_FLAG_PRECISE; } =20 static void mark_reg_known_zero(struct bpf_verifier_env *env, @@ -2139,13 +2141,14 @@ void bpf_mark_reg_unknown_imprecise(struct bpf_reg_= state *reg) } =20 /* Mark a register as having a completely unknown (scalar) value, - * initialize .precise as true when not bpf capable. + * set BPF_FLAG_PRECISE when not bpf capable. */ static void __mark_reg_unknown(const struct bpf_verifier_env *env, struct bpf_reg_state *reg) { bpf_mark_reg_unknown_imprecise(reg); - reg->precise =3D !env->bpf_capable; + if (!env->bpf_capable) + reg->flags |=3D BPF_FLAG_PRECISE; } =20 static void mark_reg_unknown(struct bpf_verifier_env *env, @@ -7506,7 +7509,8 @@ static void maybe_widen_reg(struct bpf_verifier_env *= env, return; if (rold->type !=3D rcur->type) return; - if (rold->precise || rcur->precise || scalars_exact_for_widen(rold, rcur)) + if (reg_is_precise(rold) || reg_is_precise(rcur) || + scalars_exact_for_widen(rold, rcur)) return; __mark_reg_unknown(env, rcur); } @@ -14876,7 +14880,7 @@ static int adjust_reg_min_max_vals(struct bpf_verif= ier_env *env, return err; return adjust_ptr_min_max_vals(env, insn, dst_reg, src_reg); - } else if (dst_reg->precise) { + } else if (reg_is_precise(dst_reg)) { /* if dst_reg is precise, src_reg should be precise as well */ err =3D mark_chain_precision(env, insn->src_reg); if (err) --=20 2.53.0-Meta From nobody Mon Sep 28 23:51:14 2026 Received: from mta0.migadu.com (out-227.mta0.migadu.com [91.218.175.227]) (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 85C103E0080 for ; Fri, 14 Aug 2026 23:20:08 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.227 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786749611; cv=none; b=X7acwj16cGJP3UHnuD90W0WKSwmj7Z5/EneJZoTtueiEShZGu4ZwjJwkNR5caja01Ejk8+/DI1115UTrMKZRW1QJAgsKcjhzl0At2pnhb4BH+CQHO5GzhKD1IYY1+wGohxCK3ZlMypL9ZOYTfjBMqOcJ5/xIBhWywVw4OQI4ue0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786749611; c=relaxed/simple; bh=83G2vJPKsA0PHnFZqh1qlVx++CFo11gZxgu88reMHsE=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=GzWDXXcfrzlAKd75Td5KmPR7wMCqQZimf+QYm7Dy5ontIgQ+9anSHaW9LNQpFT8ORUb5H3uEXsjDPh1clsFavTEEFLVgvEDHIufN078WnweIt948hZknSHsn2m9pNmsvNRMtsoaCMkG/5JbGVzrcFEhO2sQrc5MAevGOE7wKkbo= 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=Fi5YjXtx; arc=none smtp.client-ip=91.218.175.227 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="Fi5YjXtx" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=83G2vJPKsA0PHnFZqh1qlVx++CFo11gZxgu88reMHsE=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1786749606; v=1; x=1787354406; b=Fi5YjXtx5GFUq2VAObg//TtsyuYz/fVmits/baBdggKaXK8hZLCdit+4HHtZGUs7PF4dH6sw bQ2k7UjDW0CoWJXzp1D7wnisRIKXHu288wSsY0bx1kvmqZTO3XIng/dlkaZDguGPjU+xX4qTFao EgXmgmvX3PhClFUNxX6HKmTg= X-Envelope-To: linux-kernel@vger.kernel.org Received: from gvineet-fedora-PF5JGVFY.thefacebook.com (2620:10d:c090:500::6:7be4) by smtp.migadu.com with ESMTPS id 2f2552a5b17687f9; Fri, 14 Aug 2026 23:20:06 +0000 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: [RFC bpf-next 2/6] bpf: move the linked-scalar flags into bpf_reg_state->flags [NFC] Date: Fri, 14 Aug 2026 16:19:41 -0700 Message-ID: <20260814231945.3884596-3-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260814231945.3884596-1-vineet.gupta@linux.dev> References: <20260814231945.3884596-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 is an overloaded container for: - "id" corresponding to "linked" registers - linkage type flags This was fine so far, however new linkage types are coming so better to separate them: - checking for "id" doesn't need masking out flags: this is both cleaner and future-proof - makes ->id full 32-bits The best part is no additional space needed as it piggybacks on the previous patch creating a flags field. The cleanup of check_scalar_ids() alone is worth this: - Its two-level "check the compound id, then check the base id" dance existed only because the flag was part of the key. With a plain id there is one key and a single check_ids() suffices; the flag and delta equality that regsafe() already enforces cover the rest. However, ->flags now sits past the end of every memcmp() window used for state comparison (they stop at offsetof(id), offsetof(var_off) or offsetof(frameno)), and check_ids() only ever sees the plain ->id. While the flags lived in the top bits of ->id they were compared for free -- byte-wise by states_maybe_looping(), and as part of the compound key by regs_exact(). Now they have to be compared explicitly, so add a helper and call it from both places that compare a scalar identity: static bool link_flags_match(rold, rcur) { if (!rold->id) return true; return (rold->flags & BPF_FLAG_ADD_CONST) =3D=3D (rcur->flags & BPF_FLAG_ADD_CONST); } regsafe() keeps its check in the same spot, now expressed via the helper, so its behaviour is unchanged. regs_exact() gains the check it lost; that is t= he one place this patch is not bit-identical to the old compound-key behaviour, but it restores the discrimination the compound key provided rather than adding new strictness. states_maybe_looping() is covered through states_equal(EXACT), which routes to regs_exact(). The helper is the single point to extend when further link flavours are add= ed. Two more places need care now that these flags share a byte with BPF_FLAG_PRECISE: - clear_scalar_id() and __mark_reg_known() clear only the ADD_CONST bits, n= ot the whole byte, so the precise marking survives as before. - sync_linked_regs() does "*reg =3D *known_reg" and then restores the fields that identify reg rather than known_reg. Only the ADD_CONST bits belong to that set (they used to live in ->id); BPF_FLAG_PRECISE must keep coming from known_reg, as it did when it was a separate bool. So the save/restore is masked to BPF_FLAG_ADD_CONST rather than covering ->flags wholesale. No functional change intended. Suggested-by: Eduard Zingerman Signed-off-by: Vineet Gupta --- include/linux/bpf_verifier.h | 18 +++--- kernel/bpf/log.c | 4 +- kernel/bpf/states.c | 64 ++++++++++++------- kernel/bpf/verifier.c | 27 ++++---- .../bpf/progs/verifier_linked_scalars.c | 23 +++---- 5 files changed, 81 insertions(+), 55 deletions(-) diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h index ebab483fc7f2..2b03fdba9acf 100644 --- a/include/linux/bpf_verifier.h +++ b/include/linux/bpf_verifier.h @@ -136,16 +136,13 @@ struct bpf_reg_state { * to a specific instance of bpf_iter. */ /* - * Upper bit of ID is used to remember relationship between "linked" - * registers. Example: + * ->id identifies a set of "linked" registers; how a given member + * relates to the others is recorded in ->flags. Example: * 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 + * r1 +=3D 10; r1 gets BPF_FLAG_ADD_CONST64 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 + * w3 +=3D 10; r3 gets BPF_FLAG_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. @@ -166,11 +163,16 @@ struct bpf_reg_state { * Register state flags. * BPF_FLAG_PRECISE: if unset, and this is a SCALAR_VALUE, then * min/max/tnum don't affect safety. - * * PRECISE is a property of this register alone, so it is placed at bit 7, * apart from the link flags, which grow up from bit 0 and are cleared as * a group -- a clear-the-link-bits mask can then never reach it. + * + * BPF_FLAG_ADD_CONST{32,64}: this register is (base + ->delta) within + * its ->id set, computed with a 32- or 64-bit ALU add. */ +#define BPF_FLAG_ADD_CONST32 (1U << 0) +#define BPF_FLAG_ADD_CONST64 (1U << 1) +#define BPF_FLAG_ADD_CONST (BPF_FLAG_ADD_CONST32 | BPF_FLAG_ADD_CONST64) #define BPF_FLAG_PRECISE (1U << 7) u8 flags; }; diff --git a/kernel/bpf/log.c b/kernel/bpf/log.c index 9a4445d492c9..775b91f806ac 100644 --- a/kernel/bpf/log.c +++ b/kernel/bpf/log.c @@ -662,8 +662,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->flags & BPF_FLAG_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 f7a0314fa106..d3105b9a9965 100644 --- a/kernel/bpf/states.c +++ b/kernel/bpf/states.c @@ -370,12 +370,12 @@ static bool check_ids(u32 old_id, u32 cur_id, struct = bpf_idmap *idmap) * 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. + * ->id is a plain identifier -- the ADD_CONST relationship lives in + * ->flags -- so there is no compound (base | flag) key to unpack here. + * Registers sharing a base id go through one idmap entry, which is what + * catches e.g. old r2.id=3DA, r3.id=3DA (r3 =3D r2 + delta) against cur r= 2.id=3DB, + * r3.id=3DC: A->B and A->C conflict. Matching ->flags and ->delta are che= cked + * by the caller in regsafe(). */ static bool check_scalar_ids(u32 old_id, u32 cur_id, struct bpf_idmap *idm= ap) { @@ -384,15 +384,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, @@ -488,11 +480,32 @@ static int clean_verifier_state(struct bpf_verifier_e= nv *env, return 0; } =20 +/* + * Do rold and rcur describe the same relationship to their ->id set? + * + * The link flags live in ->flags, which sits past the end of every memcmp= () + * window used for state comparison, and check_ids() only ever sees the pl= ain + * ->id. So unlike when these bits rode along in the top of ->id, they hav= e to + * be compared explicitly everywhere ->id is. + * + * Only meaningful when rold carries an id: the flags are only ever set + * together with one, so rold->id =3D=3D 0 implies none of them is set. + */ +static bool link_flags_match(const struct bpf_reg_state *rold, + const struct bpf_reg_state *rcur) +{ + if (!rold->id) + return true; + + return (rold->flags & BPF_FLAG_ADD_CONST) =3D=3D (rcur->flags & BPF_FLAG_= ADD_CONST); +} + static bool regs_exact(const struct bpf_reg_state *rold, const struct bpf_reg_state *rcur, struct bpf_idmap *idmap) { return memcmp(rold, rcur, offsetof(struct bpf_reg_state, id)) =3D=3D 0 && + link_flags_match(rold, rcur) && check_ids(rold->id, rcur->id, idmap) && check_ids(rold->parent_id, rcur->parent_id, idmap); } @@ -554,7 +567,7 @@ 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 (for BPF_FLAG_ADD_CONST) offset consistency. * * +------------------+-----------+------------------+---------------+ * | | rold->id | rold + ADD_CONST | rold->id =3D=3D 0= | @@ -590,17 +603,24 @@ 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 + * ADD_CONST flags must match exactly: BPF_FLAG_ADD_CONST32 and + * BPF_FLAG_ADD_CONST64 have different linking semantics in * sync_linked_regs() (alu32 zero-extends, alu64 does not), * so pruning across different flag types is unsafe. */ - if (rold->id && - (rold->id & BPF_ADD_CONST) !=3D (rcur->id & BPF_ADD_CONST)) + if (!link_flags_match(rold, rcur)) return false; =20 - /* Both have offset linkage: offsets must match */ - if ((rold->id & BPF_ADD_CONST) && rold->delta !=3D rcur->delta) + /* + * Both have offset linkage: offsets must match. The rold->id + * test is redundant today -- BPF_FLAG_ADD_CONST is only ever set + * together with an id -- but it used to be structural, because + * the flag lived in the id itself. Keep it explicit so the + * invariant does not rest on every ->id =3D 0 site remembering to + * clear ->flags too. + */ + if (rold->id && (rold->flags & BPF_FLAG_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 8925749d636e..93e69116ca9e 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -1806,6 +1806,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->flags &=3D ~BPF_FLAG_ADD_CONST; ___mark_reg_known(reg, imm); } =20 @@ -3308,6 +3309,7 @@ static void clear_scalar_id(struct bpf_reg_state *reg) { reg->id =3D 0; reg->delta =3D 0; + reg->flags &=3D ~BPF_FLAG_ADD_CONST; } =20 static void assign_scalar_id_before_mov(struct bpf_verifier_env *env, @@ -3320,7 +3322,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->flags & BPF_FLAG_ADD_CONST) clear_scalar_id(src_reg); /* * Ensure that src_reg has a valid ID that will be copied to @@ -14950,7 +14952,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->flags & BPF_FLAG_ADD_CONST) { /* * If the register already went through rX +=3D val * we cannot accumulate another val into rx->off. @@ -14959,9 +14961,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->flags |=3D BPF_FLAG_ADD_CONST32; else - dst_reg->id |=3D BPF_ADD_CONST64; + dst_reg->flags |=3D BPF_FLAG_ADD_CONST64; dst_reg->delta =3D off; } } else { @@ -15886,7 +15888,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); @@ -15914,7 +15916,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]; @@ -15950,18 +15951,19 @@ 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->flags ^ known_reg->flags) & BPF_FLAG_ADD_CONST) =3D=3D BPF_FL= AG_ADD_CONST) continue; - if ((!(reg->id & BPF_ADD_CONST) && !(known_reg->id & BPF_ADD_CONST)) || + if ((!(reg->flags & BPF_FLAG_ADD_CONST) && !(known_reg->flags & BPF_FLAG= _ADD_CONST)) || reg->delta =3D=3D known_reg->delta) { *reg =3D *known_reg; } else { + u8 saved_add_const =3D reg->flags & BPF_FLAG_ADD_CONST; s32 saved_off =3D reg->delta; u32 saved_id =3D reg->id; =20 @@ -15976,11 +15978,12 @@ static void sync_linked_regs(struct bpf_verifier_= env *env, struct bpf_verifier_s */ reg->delta =3D saved_off; reg->id =3D saved_id; + reg->flags =3D (reg->flags & ~BPF_FLAG_ADD_CONST) | 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->flags | known_reg->flags) & BPF_FLAG_ADD_CONST32) zext_32_to_64(reg); reg_bounds_sync(reg); } @@ -17007,7 +17010,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, ({ @@ -17015,7 +17018,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..c80747c16bcf 100644 --- a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c +++ b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c @@ -349,8 +349,9 @@ 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() consults reg->flags (the linked target + * register) for BPF_FLAG_ADD_CONST32, not just known_reg->flags (the bran= ch + * register): the gate is (reg->flags | known_reg->flags). */ SEC("socket") __success @@ -360,7 +361,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; /* alu32: r7.flags |=3D BPF_FLAG_ADD_CONST32 */ \ r8 =3D 0xFFFFffff ll; \ if r6 < r8 goto l0_%=3D; \ /* r6 in [0xFFFFFFFF, 0xFFFFFFFF] */ \ @@ -381,7 +382,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 (BPF_FLAG_ADD_CONST32) and the other used alu64 (BPF_FLAG_ADD_CON= ST64). * The delta relationship doesn't hold across different ALU widths. */ SEC("socket") @@ -392,9 +393,9 @@ __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; /* alu32: BPF_FLAG_ADD_CONST32, delta =3D 1 */ \ r8 =3D r6; /* linked: same id as r6 */ \ - r8 +=3D 2; /* alu64: BPF_ADD_CONST64, delta =3D 2 */ \ + r8 +=3D 2; /* alu64: BPF_FLAG_ADD_CONST64, delta =3D 2 */ \ r9 =3D 0xFFFFffff ll; \ if r7 < r9 goto l0_%=3D; \ /* r7 =3D 0xFFFFFFFF */ \ @@ -416,7 +417,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). + * BPF_FLAG_ADD_CONST32 from alu32, another BPF_FLAG_ADD_CONST64 from alu6= 4). */ SEC("socket") __failure __msg("div by zero") @@ -431,11 +432,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; /* BPF_FLAG_ADD_CONST32, delta =3D 1 */\ goto l_merge_%=3D; \ l_pathb_%=3D: \ /* Path B: alu64 */ \ - r7 +=3D 1; /* BPF_ADD_CONST64, delta =3D 1 */\ + r7 +=3D 1; /* BPF_FLAG_ADD_CONST64, delta =3D 1 */\ l_merge_%=3D: \ /* Merge point: regsafe() compares path B against cached path A. */ \ /* Narrow r6 to trigger sync_linked_regs for r7 */ \ @@ -593,7 +594,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 BPF_FLAG_ADD_CONST does not leak * through assign_scalar_id_before_mov() into a new id, causing * sync_linked_regs() to compute an incorrect offset. */ @@ -648,7 +649,7 @@ l_exit_%=3D: \ } =20 /* - * Test that regsafe() verifies base_id consistency for BPF_ADD_CONST + * Test that regsafe() verifies base_id consistency for BPF_FLAG_ADD_CONST * linked scalars during state pruning. * * The false branch (explored first) links R3 to R2 via ADD_CONST. --=20 2.53.0-Meta From nobody Mon Sep 28 23:51:14 2026 Received: from mta0.migadu.com (out-234.mta0.migadu.com [91.218.175.234]) (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 814CE373BEC for ; Fri, 14 Aug 2026 23:20:12 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.234 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786749618; cv=none; b=iQzk3/wDnETpBF7IFpaV7+RfS0BnFhThUk945Z1S2HxI4mVirXiJBtd1KFARCDukrWxlXkTCCiVd5COAURYBiPjIys022lx88m8WqhkYqgGUZGBhSyPhBH2+61CVzJk95YT84bLdEH1WT0gzZtWmzRed0VbE5TEnMWZJlLmC8VQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786749618; c=relaxed/simple; bh=32bCd92QrJ5CNFMIriXP22Lyz4CFQtB+v0/Z/V2E8wk=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=RwkqQ1mTyreAB2QY020CibjQhnr1qrQTa7a1Zfv013oMBC/ysIV6zSdW30ubUrsl6unHPLpncFd642nU+URrnZBd3rmNDzPgY596R0b06XdPB4nsVnUjZy9CdUIM31U2w/od2iwxwPQCD6Qe5pZ1i0cO/qkXkUwa933GeKW795s= 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=haUEDowS; arc=none smtp.client-ip=91.218.175.234 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="haUEDowS" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=32bCd92QrJ5CNFMIriXP22Lyz4CFQtB+v0/Z/V2E8wk=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1786749610; v=1; x=1787354410; b=haUEDowS3aiummw114mtZgRiZmxyPBISz5DYTwjQuB9RwHPJLoNdF2rVZOSAjTDd2CEHT9C4 dfKLG0bFosae4R8zAENcp38wbdkKO8AL1yisriJjqKJmWCrqjFgxBMUb0w0MG7OZVFleVdMKxCH YznhZDcb8BQc5fDoC8iGDXIs= X-Envelope-To: linux-kernel@vger.kernel.org Received: from gvineet-fedora-PF5JGVFY.thefacebook.com (2620:10d:c090:500::6:7be4) by smtp.migadu.com with ESMTPS id 845d4f031579e439; Fri, 14 Aug 2026 23:20:09 +0000 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: [RFC bpf-next 3/6] bpf: support low-32 subreg scalar linking for zero-extending movs Date: Fri, 14 Aug 2026 16:19:42 -0700 Message-ID: <20260814231945.3884596-4-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260814231945.3884596-1-vineet.gupta@linux.dev> References: <20260814231945.3884596-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" Problem =3D=3D=3D=3D=3D=3D=3D Currently register equality tracking and propagation only works for full 64-bits (with additional constant offset). It is missing the relationship: "these two regs share only their low 32-bits". An illustrative snippet: | r6 =3D ... /* full 64-bit unknown */ | w7 =3D w6 /* 32-bit zero-extend mov from wide src */ | if w6 !=3D 0 goto .Lxx /* branch not taken, src narrowed */ | if w7 =3D=3D 0 goto .Lok <-- missing It works if the register is narrow to begin with, e.g. | r6 =3D *(u32 *)(...) Rephrased in verifier speak: The linked-scalar equality relation sync_linked_regs() maintains is full 64-bit only; there is no subregister (low-32) equality link. A 32-bit mov (w1 =3D w2) is therefore either promoted to a full-64-bit link when the source is provably u32, or the link is dropped entirely when the wider source has unknown high bits. A later narrowing of the source to its low 32 bits never reaches dst, causing safe programs to be rejected. Note t= hat the ADD_CONST32 machinery only applies to +=3D const offset, not to equalit= y. This was seen with bpf-gcc codegen that tends to reuse "w0 =3D idx" for "return 0" on an idx=3D=3D0 path, for bpf_loop callbacks. Solution =3D=3D=3D=3D=3D=3D=3D=3D - Introduce a low-32-only link, BPF_FLAG_SUBREG_ZEXT, added to BPF_FLAG_LI= NK. - For a wide-source 32-bit mov, mark dst with BPF_FLAG_SUBREG_ZEXT instead of clearing it (when src carries a scalar id). - On a later low-32 narrowing sync_linked_regs() re-derives such a registe= r as the zero-extension of the base's low 32 bits: it copies the base (keepin= g its precise low-32 tnum) and re-applies zext_32_to_64() -- the same helper t= he 32-bit mov used -- which is sound even when the source has unknown high = bits. This is applied only when neither side carries an ADD_CONST delta (the combined subreg+delta case is not modeled). - Sites that group a subreg-linked register by its scalar id compare ->id directly: no masking is needed, since BPF_FLAG_SUBREG_ZEXT lives in ->flags. The reconstruction copies the base wholesale, so it must put back the fields that identify reg rather than known_reg -- ->id and, now, the link flag. Th= is mirrors what the ADD_CONST arm below already does ("Must preserve off and i= d, otherwise another sync_linked_regs() will be incorrect"). Dropping the flag while keeping the ->id would be worse than losing the link: the register wo= uld claim a full 64-bit equality with a base whose high bits are unknown, and t= he next sync driven by it would copy a narrowed low-32 value straight onto the base's high half. The link_flags_match() helper added by the previous patch is widened from BPF_FLAG_ADD_CONST to BPF_FLAG_LINK, so regs_exact() -- and through it states_maybe_looping() -- discriminates the new flavour as well. regsafe() additionally checks it early, before the explore_alu_limits and !precise short-circuits, which the helper's call site below them does not cover. Note: the sync_linked_regs() reconstruction is wrapped in an extra block th= at looks redundant here. It is a placeholder for the sign-extension counterpart patch, which turns it into the else arm of an if/else on the link flavour; keeping it now avoids re-indenting the whole body there. Results =3D=3D=3D=3D=3D=3D=3D Improves verifier tracking (seen in the next selftest). selftest runs: - clang: no new regressions (-mcpu=3Dv3 and v4) - bpf-gcc: no new regressions; the measurable selftest pass improvements come with the sign-extension counterpart patch. Signed-off-by: Vineet Gupta --- include/linux/bpf_verifier.h | 10 ++++ kernel/bpf/states.c | 23 ++++++++- kernel/bpf/verifier.c | 91 ++++++++++++++++++++++++++++++++---- 3 files changed, 114 insertions(+), 10 deletions(-) diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h index 2b03fdba9acf..a4cba5c5099e 100644 --- a/include/linux/bpf_verifier.h +++ b/include/linux/bpf_verifier.h @@ -169,10 +169,20 @@ struct bpf_reg_state { * * BPF_FLAG_ADD_CONST{32,64}: this register is (base + ->delta) within * its ->id set, computed with a 32- or 64-bit ALU add. + * BPF_FLAG_SUBREG_ZEXT: low-32-bit-only equality (as opposed to the + * full equality implied by a bare shared ->id): this register shares + * only the base's low 32 bits, and its high bits are zero (32-bit + * zero-extending mov). + * sync_linked_regs() propagates the low 32-bit subrange and rebuilds + * the high half accordingly, so this is sound even when the base has + * unknown high bits. */ #define BPF_FLAG_ADD_CONST32 (1U << 0) #define BPF_FLAG_ADD_CONST64 (1U << 1) #define BPF_FLAG_ADD_CONST (BPF_FLAG_ADD_CONST32 | BPF_FLAG_ADD_CONST64) +#define BPF_FLAG_SUBREG_ZEXT (1U << 2) +/* Every flag describing how this register relates to its ->id set. */ +#define BPF_FLAG_LINK (BPF_FLAG_ADD_CONST | BPF_FLAG_SUBREG_ZEXT) #define BPF_FLAG_PRECISE (1U << 7) u8 flags; }; diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c index d3105b9a9965..ef71999c4695 100644 --- a/kernel/bpf/states.c +++ b/kernel/bpf/states.c @@ -490,6 +490,9 @@ static int clean_verifier_state(struct bpf_verifier_env= *env, * * Only meaningful when rold carries an id: the flags are only ever set * together with one, so rold->id =3D=3D 0 implies none of them is set. + * + * BPF_FLAG_LINK covers every flavour, so this widens automatically as new + * ones are added. */ static bool link_flags_match(const struct bpf_reg_state *rold, const struct bpf_reg_state *rcur) @@ -497,7 +500,7 @@ static bool link_flags_match(const struct bpf_reg_state= *rold, if (!rold->id) return true; =20 - return (rold->flags & BPF_FLAG_ADD_CONST) =3D=3D (rcur->flags & BPF_FLAG_= ADD_CONST); + return (rold->flags & BPF_FLAG_LINK) =3D=3D (rcur->flags & BPF_FLAG_LINK); } =20 static bool regs_exact(const struct bpf_reg_state *rold, @@ -554,6 +557,24 @@ static bool regsafe(struct bpf_verifier_env *env, stru= ct bpf_reg_state *rold, =20 switch (base_type(rold->type)) { case SCALAR_VALUE: + /* + * A low-32-bit-only link has different sync_linked_regs() + * semantics than a full/ADD_CONST equality. check_scalar_ids() + * only ever sees the plain ->id and never looks at ->flags, so a + * mismatch must be rejected explicitly. + * Check it here, before the explore_alu_limits and !precise + * short-circuits below (neither of which tests it). Note the + * pre-existing BPF_FLAG_ADD_CONST check sits after those + * short-circuits instead. The argument for checking early + * applies to it equally, but moving it makes regsafe() stricter + * on a path that predates this series, which is a pruning change + * that wants measuring on its own; it is deliberately left + * alone here. + */ + if (rold->id && + (rold->flags & BPF_FLAG_SUBREG_ZEXT) !=3D (rcur->flags & BPF_FLAG_SU= BREG_ZEXT)) + return false; + if (env->explore_alu_limits) { /* explore_alu_limits disables tnum_in() and range_within() * logic and requires everything to be strict diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 93e69116ca9e..8a802d49d0a4 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -1806,7 +1806,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->flags &=3D ~BPF_FLAG_ADD_CONST; + reg->flags &=3D ~BPF_FLAG_LINK; ___mark_reg_known(reg, imm); } =20 @@ -3309,7 +3309,7 @@ static void clear_scalar_id(struct bpf_reg_state *reg) { reg->id =3D 0; reg->delta =3D 0; - reg->flags &=3D ~BPF_FLAG_ADD_CONST; + reg->flags &=3D ~BPF_FLAG_LINK; } =20 static void assign_scalar_id_before_mov(struct bpf_verifier_env *env, @@ -15076,15 +15076,42 @@ static int check_alu_op(struct bpf_verifier_env *= env, struct bpf_insn *insn) if (insn->off =3D=3D 0) { bool is_src_reg_u32 =3D get_reg_width(src_reg) <=3D 32; =20 - if (is_src_reg_u32) + /* + * *dst_reg =3D *src_reg below copies src's id into dst, a + * full 64-bit equality link. That is only sound when src + * fits in u32: a 32-bit mov zero-extends dst, so for a + * wider src the link would let sync_linked_regs() + * propagate dst's [0, U32_MAX] range back onto src's + * unknown high bits. For a wide src drop the full link + * and form a low-32-only BPF_FLAG_SUBREG_ZEXT link instead, so a + * later narrowing of src's low 32 bits still reaches dst. + * + * wide_subreg_link gates that low-32 link and excludes: + * - a self-mov (w6 =3D w6): src =3D=3D dst, nothing to link; + * forming one would only mint an id and a spurious + * self-link (inert in sync_linked_regs()). + * - an ADD_CONST-linked src (rX =3D base + K): + * assign_scalar_id_before_mov() would clear its + * base+delta link, and a combined subreg+delta link + * isn't modeled anyway (sync_linked_regs() skips it). + * In both cases src is left untouched and dst is cleared, + * as before this feature. + */ + bool wide_subreg_link =3D !is_src_reg_u32 && + src_reg !=3D dst_reg && + !(src_reg->flags & BPF_FLAG_ADD_CONST); + + if (is_src_reg_u32 || wide_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) - clear_scalar_id(dst_reg); + if (!is_src_reg_u32) { + if (wide_subreg_link && src_reg->id) { + /* ->id already copied above */ + dst_reg->flags |=3D BPF_FLAG_SUBREG_ZEXT; + } else { + clear_scalar_id(dst_reg); + } + } } else { /* case: W1 =3D (s8, s16)W2 */ bool no_sext =3D reg_umax(src_reg) < (1ULL << (insn->off - 1)); @@ -15953,6 +15980,52 @@ 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 low-32 linked register shares only the base's low 32 bits; + * the flag says how its high bits are derived. For + * BPF_FLAG_SUBREG_ZEXT they are zero (32-bit zero-extending mov). + * Rebuild it from known_reg's low 32 bits accordingly, but only + * when neither side carries an ADD_CONST delta -- with a delta + * the low bits differ from the base by that delta and the combined + * subreg+ADD_CONST reconstruction isn't modeled here, so leave reg + * unchanged (sound, just less precise). + */ + if (reg->flags & BPF_FLAG_SUBREG_ZEXT) { + if (!((reg->flags | known_reg->flags) & BPF_FLAG_ADD_CONST)) { + { + u32 saved_id =3D reg->id; + u8 saved_subreg =3D reg->flags & BPF_FLAG_SUBREG_ZEXT; + + /* + * reg =3D zext32(known_reg): its low 32 bits come from + * the base and its high 32 are zero. Rather than + * rebuild the value by hand, copy the base (keeping + * its precise low-32 tnum) and re-clear the high half + * with the same zext_32_to_64() the 32-bit + * zero-extending mov used -- the zero high half is a + * fallout of it, so no dedicated reconstruction is + * needed. + */ + *reg =3D *known_reg; + reg->id =3D saved_id; + reg->flags =3D (reg->flags & ~BPF_FLAG_SUBREG_ZEXT) | saved_subreg; + zext_32_to_64(reg); + reg_bounds_sync(reg); + } + if (e->is_reg) + mark_reg_scratched(env, e->regno); + else + mark_stack_slot_scratched(env, e->spi); + } + continue; + } + /* + * Dest-driven direction (known_reg is subreg-linked, reg is not): + * copying known_reg's low-32-only state into a full register would + * be unsound, so leave reg unchanged. + */ + if (known_reg->flags & BPF_FLAG_SUBREG_ZEXT) + continue; /* * Skip mixed 32/64-bit links: the delta relationship doesn't * hold across different ALU widths. --=20 2.53.0-Meta From nobody Mon Sep 28 23:51:14 2026 Received: from mta0.migadu.com (out-238.mta0.migadu.com [91.218.175.238]) (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 4ADF53EA973 for ; Fri, 14 Aug 2026 23:20:16 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=91.218.175.238 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786749621; cv=none; b=Q5M6Y/A9xe10OMo2YypEevCQ+0mVmkNSk4nGO/EQQIK/DI28oHxqOCvMIPqz5lU1sZQ0HdZidkOV8PxRlHnSSEYFsb8EPlQ2wg9cRyNHoW2VbJAgLuBHeEqNrJnJd45ThqnF+HOGmBscLPX9IjkTaDjy31/scUQx71xd6LusNCM= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786749621; c=relaxed/simple; bh=gQWfhEdKF2JjwJqUTznVUndfsb1utd+bePMKBBr0H5A=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=P2qFR+z10ajr9kk2ZehOaa9p0fZ+gXb84fKj0qOYIE2t3kSSBsSATWNCuu4KofQsaaripJeH9Ptf7RiBwiJ1t1ka+3hlXzpoivNVF2EQu1jtl0zeqP9eX0k4Ad8uLpB5u6T0HciMnu8QU9ibEsp8wbneGew99C+put/1Srx6VAI= 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=opEJJQVD; arc=none smtp.client-ip=91.218.175.238 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="opEJJQVD" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=gQWfhEdKF2JjwJqUTznVUndfsb1utd+bePMKBBr0H5A=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1786749614; v=1; x=1787354414; b=opEJJQVD3u7rnINxCL80tZM894hnpAGnTzYnFN2q4Jm8/uvq98i4LxkMqhpDQHaivWJmZMg1 TiKeqH+G/z67KNv4prfUIjHeESOUJEFD5xeCIpOIAUxtvsjii3oV1aPzLStEfz3Why+ypkNUn59 3ybwXGSeTepWeic9tZpTdPLA= X-Envelope-To: linux-kernel@vger.kernel.org Received: from gvineet-fedora-PF5JGVFY.thefacebook.com (2620:10d:c090:500::6:7be4) by smtp.migadu.com with ESMTPS id ad926ce0d5276d48; Fri, 14 Aug 2026 23:20:13 +0000 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: [RFC bpf-next 4/6] selftests/bpf: cover low-32 subreg-equal link for zero-extending movs Date: Fri, 14 Aug 2026 16:19:43 -0700 Message-ID: <20260814231945.3884596-5-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260814231945.3884596-1-vineet.gupta@linux.dev> References: <20260814231945.3884596-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" Add subreg_eq_zext_mov_narrow: a wide-source "w7 =3D w6" followed by "if w6= =3D=3D 0" must narrow w7 to 0 via the low-32 BPF_FLAG_SUBREG_ZEXT link. Update two tests for the new, more precise behaviour: - verifier_reg_equal/subreg_equality_2 now loads: with the low-32 link, "w2 < 9" narrows r3 to [0, 8], so the illegal r1 read is unreachable and= the program is safe. - verifier_bounds 32-bit subtraction partial overflow: R3 now carries an id from the link (bounds unchanged), so __msg gains an "id=3D" match. It is required rather than optional: the id is deterministic here, and an optional match would still pass if the link were ever dropped again. Also covers the dest-driven direction, which the other tests do not: they a= ll narrow the base and check the linked register. zext_dest_driven_does_not_narrow_base narrows the LINKED register instead a= nd requires that the wide base is NOT narrowed -- the "known_reg is subreg-linked" continue in sync_linked_regs(). It is a __failure test: the div must stay reachable. This is also the shape that catches a lost BPF_FLAG_SUBREG_ZEXT, since dropping the flag while the shared ->id survives makes the pair look like a full 64-bit equality and bypasses that guard. Signed-off-by: Vineet Gupta --- .../selftests/bpf/progs/verifier_bounds.c | 10 +- .../bpf/progs/verifier_linked_scalars.c | 115 ++++++++++++++++++ .../selftests/bpf/progs/verifier_reg_equal.c | 16 +-- 3 files changed, 133 insertions(+), 8 deletions(-) diff --git a/tools/testing/selftests/bpf/progs/verifier_bounds.c b/tools/te= sting/selftests/bpf/progs/verifier_bounds.c index 1a273e416fed..6169a61269b2 100644 --- a/tools/testing/selftests/bpf/progs/verifier_bounds.c +++ b/tools/testing/selftests/bpf/progs/verifier_bounds.c @@ -1516,7 +1516,15 @@ __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))") +/* + * w3 =3D w0 forms a low-32 BPF_FLAG_SUBREG_ZEXT link, so R3 carries an id= here + * where it did not before; the bounds are unchanged. The id is determinis= tic + * (raw asm, same bytecode in every flavour) so require it rather than mak= ing + * it optional -- otherwise the assertion would still pass if the link were + * dropped again. The delta suffix is left general: log.c prints ->delta + * directly after the id with no separator when BPF_FLAG_ADD_CONST is set. + */ +__msg("3: (1c) w3 -=3D w2 {{.*}} R3=3Dscalar(id=3D{{[0-9]+([+-][0-9]+)?}},= 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_linked_scalars.c b/= tools/testing/selftests/bpf/progs/verifier_linked_scalars.c index c80747c16bcf..2cc6f9e45aff 100644 --- a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c +++ b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c @@ -711,4 +711,119 @@ l_exit_%=3D: \ : __clobber_all); } =20 +/* + * A 32-bit zero-extending mov (w7 =3D w6) from a source with unknown high= bits + * shares only the low 32 bits (w7.lo =3D=3D w6.lo, w7.hi =3D=3D 0). A lat= er narrowing of + * the source's low 32 bits must propagate to the destination via the + * BPF_FLAG_SUBREG_ZEXT (low-32-only) link. This is the pattern bpf-gcc em= its when it + * reuses "w0 =3D idx" for "return 0" on the idx=3D=3D0 path of a callback. + */ +SEC("socket") +__success +__naked void subreg_eq_zext_mov_narrow(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; /* r6 =3D 64-bit unknown (helper ret is unbounded) */ \ + call %[bpf_get_prandom_u32]; \ + r0 <<=3D 32; /* r0 =3D unknown high bits */ \ + r6 |=3D r0; /* still 64-bit unknown; makes it explicit */ \ + w7 =3D w6; /* 32-bit zero-extend mov, wide src */ \ + if w6 !=3D 0 goto l_out_%=3D; /* w6 low =3D=3D 0 on fall-through */ \ + /* w7 =3D zext32(w6 low) must be 0 here */ \ + if w7 =3D=3D 0 goto l_out_%=3D; /* provably 0 iff linked */ \ + r0 /=3D 0; /* reached only if w7 not deduced 0 */ \ +l_out_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * A 32-bit zero-extending mov (w7 =3D w5) whose SOURCE is a wide ADD_CONS= T-linked + * register (r5 =3D base + K) must NOT disturb that source. Forming the lo= w-32 + * BPF_FLAG_SUBREG_ZEXT link on the destination would need assign_scalar_i= d_before_mov() + * on the source, which clears its base+delta link -- and a combined + * subreg+delta link isn't modeled anyway (sync_linked_regs() skips it). S= o for a + * wide ADD_CONST src the mov leaves the source's link intact and just cle= ars the + * destination. + * + * Here r5 =3D r6 + 3 (ADD_CONST, wide). After the mov, narrowing the base= r6 must + * still reach r5 through the preserved link: r6 in [0, 10] =3D> r5 in [3,= 13], so + * the guarded div-by-zero is unreachable. Had the mov cleared r5's link (= calling + * assign_scalar_id_before_mov() unconditionally), r5 would stay unbounded= and the + * div would be reachable (rejected). + * + * Note this is a no-regression guard rather than coverage of the new link: + * before this feature the wide-source path also left the source untouched= , so + * the test passes either way. What it pins is the choice not to call + * assign_scalar_id_before_mov() unconditionally. + * + * Written in asm so the bytecode is identical regardless of the host BPF = compiler. + */ +SEC("socket") +__success +__naked void zext_mov_keeps_add_const_src(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; /* r6 low =3D unknown u32 */ \ + call %[bpf_get_prandom_u32]; \ + r0 <<=3D 32; \ + r6 |=3D r0; /* r6 =3D full 64-bit unknown (base) */ \ + r5 =3D r6; /* r5, r6 linked (shared id) */ \ + r5 +=3D 3; /* r5 =3D base + 3: ADD_CONST, still wide */ \ + w7 =3D w5; /* 32-bit zext mov, wide ADD_CONST src */ \ + if r6 > 10 goto l_out_%=3D;/* r6 in [0, 10] */ \ + /* r5 =3D r6 + 3 must be in [3, 13] here (needs the kept link) */ \ + if r5 > 13 goto l_err_%=3D;/* taken only if r5 not narrowed */ \ + goto l_out_%=3D; \ +l_err_%=3D: \ + r0 /=3D 0; /* reachable iff r5's link was cleared */ \ +l_out_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * Dest-driven direction, zero-extend flavour: narrowing the LINKED regist= er + * must not narrow the wide base. + * + * w7 =3D w6 shares only r6's low 32 bits; r7's high half is zero, r6's is + * unknown. Learning r7 =3D=3D 0 therefore says nothing about r6, and + * sync_linked_regs() must not copy r7's state onto it. Rejected iff the b= ase + * is left alone. + * + * This is the shape that catches a lost BPF_FLAG_SUBREG_ZEXT: if the flag= is + * dropped while the shared ->id survives, the pair looks like a full 64-b= it + * equality, the dest-driven guard is bypassed and r6 wrongly becomes 0. + */ +SEC("socket") +__failure __msg("div by zero") +__flag(BPF_F_TEST_STATE_FREQ) +__naked void zext_dest_driven_does_not_narrow_base(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; /* r6 low =3D unknown u32 */ \ + call %[bpf_get_prandom_u32]; \ + r0 <<=3D 32; \ + r6 |=3D r0; /* r6 =3D full 64-bit unknown (base) */ \ + w7 =3D w6; /* low-32 ZEXT link */ \ + if r7 !=3D 0 goto l_out_%=3D;/* r7 =3D=3D 0: low 32 bits are 0 */ \ + if r6 !=3D 0 goto l_out_%=3D;/* r6 may still have high bits set */ \ + r0 /=3D 0; /* must stay reachable */ \ +l_out_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + char _license[] SEC("license") =3D "GPL"; diff --git a/tools/testing/selftests/bpf/progs/verifier_reg_equal.c b/tools= /testing/selftests/bpf/progs/verifier_reg_equal.c index dc1d8c30fb0e..e6fbbfaeedcb 100644 --- a/tools/testing/selftests/bpf/progs/verifier_reg_equal.c +++ b/tools/testing/selftests/bpf/progs/verifier_reg_equal.c @@ -31,23 +31,25 @@ 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("w reg shares r reg low32 via subreg link even if upper32 no= t 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. w3 =3D w2 is a 32-bit \ + * zero-extending mov, so w3 shares only r2 low 32 bits \ + * (a BPF_FLAG_SUBREG_ZEXT link) and its high bits are zero. The \ + * w2 < 9 comparison then narrows r3 to [0, 8] via the link, \ + * so if r3 < 9 is always taken and the illegal r1 read below \ + * is unreachable. \ */ \ 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: r3 is provably < 9 */ \ r0 -=3D r1; \ l1_%=3D: exit; \ " : --=20 2.53.0-Meta From nobody Mon Sep 28 23:51:14 2026 Received: from mta1.migadu.com (out-167.mta1.migadu.com [95.215.58.167]) (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 AD3AC3A4267 for ; Fri, 14 Aug 2026 23:20:21 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.167 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786749628; cv=none; b=BxkAwvpHBElwnzRmpuH/3I4WTqfP5wKuF2QtVguLgUgADvNmzEiHSXmjj0Zjatm874n6ixKZaio92k0mBlZWMj3VqJ5nMVrbEX1+9usgac7h49TRac6UtN4JBKVSNTD98hg+tq82XJiFjVngFLfOuE6m9zaEDGIP8d0CBDP45UE= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786749628; c=relaxed/simple; bh=80DXt7oQQEjHjvAcq2i1Z5WDeJWkqq6nBI/XE5ZjR/Q=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=Qg/uQy7cytxgj4bdpEd86jr0rKqkxPRzfbTejXe0qTedQZAu5aCIbzpV5U+u2HHj9PBWd6FFaAUGzm/jCgn4ySx0PIKJGAkbAbj6fLaJ4NjZN6jalgLeuTLhqfZ30pXJHwQMl4msiVZbE/L+W8U0dxpaazJlkm68jnu4l7kyyEQ= 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=Hk3eAfO7; arc=none smtp.client-ip=95.215.58.167 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="Hk3eAfO7" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=80DXt7oQQEjHjvAcq2i1Z5WDeJWkqq6nBI/XE5ZjR/Q=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1786749617; v=1; x=1787354417; b=Hk3eAfO7JNLAIeCN9enLd6caFh7uahkvofNaOgSVESs3JuhgNN8pv6SeIa071wrqBNtn5wZ2 MxgCdhHl7/OsIXMelAXGToDqCSV3lwAztDpmksSgSNh3fqskr85QU6M89asNIwNz/4lHOnFQndr lEVLagJkXQn5pVTYBcskeVeQ= X-Envelope-To: linux-kernel@vger.kernel.org Received: from gvineet-fedora-PF5JGVFY.thefacebook.com (2620:10d:c090:500::6:7be4) by smtp.migadu.com with ESMTPS id a2b5717daf086a07; Fri, 14 Aug 2026 23:20:17 +0000 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: [RFC bpf-next 5/6] bpf: support low-32 subreg scalar linking for sign-extending movs Date: Fri, 14 Aug 2026 16:19:44 -0700 Message-ID: <20260814231945.3884596-6-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260814231945.3884596-1-vineet.gupta@linux.dev> References: <20260814231945.3884596-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" This is the sign-extension counterpart to the prior zero-extension movs change, and the original motivation for the series: bpf-gcc codegen tends to generate many more sign-extensions than clang, some of which the verifier already understands and some of which it does not. A 32-bit sign extension (r0 =3D (s32)r0) preserves the source's low 32 bits= and sets the high bits to their sign-extension. When the sign bit is not provab= ly zero the verifier clears the destination's scalar id, so a later narrowing = of the low 32 bits (e.g. "if w1 =3D=3D 0") never reaches the sign-extended reg= ister and safe programs are rejected. Three patterns hit this under bpf-gcc, and not under clang, which emits very few sign extensions even for -mcpu=3Dv4 builds. 1. callback exit-code checks that reject "R0 ... should have been in [0, 1]= ". | 0: (61) r2 =3D *(u32 *)(r1 +24) | 1: (bf) r0 =3D (s32)r2 | 2: (56) if w2 !=3D 0x0 goto pc+10 ; R2=3D0 (branch taken) | 12: (95) exit 2. the errno-or-zero return, where a value clamped to [-4095, 0] is sign-extended again on "return ret" and coerce_reg_to_size_sx() then wid= ens it back to [S32_MIN, S32_MAX] (verify_pkcs7_sig and many lsm.s progs) | 63: (85) call bpf_verify_pkcs7_signature#88154 | ... | 69: (c5) if r0 s< 0xfffff001 goto pc+1 ; R0=3Dscalar(id=3D7,smin=3Dsmin3= 2=3D-4095,smax=3D0x7fffffff) | 70: (d5) if r0 s<=3D 0x0 goto pc+1 72: | 72: (bf) r0 =3D (s32)r0 ; R0=3Dscalar(smin=3D0xffffffff= 80000000,smax=3D0x7fffffff) | 73: (95) exit 3. loops whose induction variable is sign-extended every iteration, which without a link never converge: "The sequence of 8193 jumps is too comple= x". Solution =3D=3D=3D=3D=3D=3D=3D=3D Reuse the low-32-only link added for zero-extending movs, with a second flavour flag: - for a wide-source 32-bit sign extension, mark dst with BPF_FLAG_SUBREG_S= EXT so its high bits are known to be the sign-extension of the low field. On= ly 32-bit sign extension is tracked -- (s8)/(s16) do not form a link (not s= een in codegen so far) -- so the flag alone carries the width and no extra f= ield is needed. - the flag tells sync_linked_regs() how to rebuild the high half: reconstruct_sext32() for the sign extension, versus the plain zero-extension BPF_FLAG_SUBREG_ZEXT uses. - reconstruct_sext32() rebuilds such a register from the base's low 32 bit= s, driven both at the sign-extend site (to keep an already-narrowed range, = the errno case) and from sync_linked_regs() on a later low-32 narrowing. - as with the zero-extend link this applies only when neither side carries= an ADD_CONST delta (the combined subreg+delta case is not modeled). - also as with that link, no link is formed when src itself carries an ADD_CONST delta: forming one calls assign_scalar_id_before_mov(), which would clear src's base+delta relationship. The zero-extend arm's other exclusion, a self-mov, deliberately does NOT carry over -- r0 =3D (s32)r= 0 is the motivating case here. The no_sext path is unaffected: it already called assign_scalar_id_before_mov() before this series, so an ADD_CONST src was cleared there already. - regsafe() extends its existing low-32 link check to cover both flavours, so the two are not pruned across each other: if (rold->id && (rold->flags & BPF_FLAG_SUBREG) !=3D (rcur->flags & BPF_FLAG_SUBREG)) The rold->id gate comes from the zero-extend patch and carries over unchanged. It matters for convergence here: these flags are only ever set together with an ->id, so rold->id =3D=3D 0 implies neither is set, and = the gate admits exactly "old knows no low-32 relationship, cur does" -- cur = is then strictly more constrained than old, the safe direction for pruning, while the reverse is still rejected. Without it a register that first acquires a sext link inside a loop would never match its pre-loop state = and verification would run to the 1M instruction limit (cond_break*, iters/*, verifier_bits_iter/* and the sext_in_loop_converges case added next). Signed-off-by: Vineet Gupta --- include/linux/bpf_verifier.h | 9 ++- kernel/bpf/states.c | 13 ++++- kernel/bpf/verifier.c | 110 +++++++++++++++++++++++++++++++---- 3 files changed, 120 insertions(+), 12 deletions(-) diff --git a/include/linux/bpf_verifier.h b/include/linux/bpf_verifier.h index a4cba5c5099e..67ef54d70116 100644 --- a/include/linux/bpf_verifier.h +++ b/include/linux/bpf_verifier.h @@ -173,6 +173,10 @@ struct bpf_reg_state { * full equality implied by a bare shared ->id): this register shares * only the base's low 32 bits, and its high bits are zero (32-bit * zero-extending mov). + * BPF_FLAG_SUBREG_SEXT is the same for a 32-bit sign extension + * (r0 =3D (s32)r0); the two differ in how the high half is rebuilt. + * Only 32-bit sign extension is tracked -- (s8)/(s16) do not form a + * link -- so the flag alone carries the width. * sync_linked_regs() propagates the low 32-bit subrange and rebuilds * the high half accordingly, so this is sound even when the base has * unknown high bits. @@ -181,8 +185,11 @@ struct bpf_reg_state { #define BPF_FLAG_ADD_CONST64 (1U << 1) #define BPF_FLAG_ADD_CONST (BPF_FLAG_ADD_CONST32 | BPF_FLAG_ADD_CONST64) #define BPF_FLAG_SUBREG_ZEXT (1U << 2) +#define BPF_FLAG_SUBREG_SEXT (1U << 3) +/* A low-32-only link, of either flavour. */ +#define BPF_FLAG_SUBREG (BPF_FLAG_SUBREG_ZEXT | BPF_FLAG_SUBREG_SEXT) /* Every flag describing how this register relates to its ->id set. */ -#define BPF_FLAG_LINK (BPF_FLAG_ADD_CONST | BPF_FLAG_SUBREG_ZEXT) +#define BPF_FLAG_LINK (BPF_FLAG_ADD_CONST | BPF_FLAG_SUBREG) #define BPF_FLAG_PRECISE (1U << 7) u8 flags; }; diff --git a/kernel/bpf/states.c b/kernel/bpf/states.c index ef71999c4695..6aaedde6e9d1 100644 --- a/kernel/bpf/states.c +++ b/kernel/bpf/states.c @@ -562,6 +562,8 @@ static bool regsafe(struct bpf_verifier_env *env, struc= t bpf_reg_state *rold, * semantics than a full/ADD_CONST equality. check_scalar_ids() * only ever sees the plain ->id and never looks at ->flags, so a * mismatch must be rejected explicitly. + * The two flavours also differ from each other, in how the high + * half is rebuilt (zero-extension vs reconstruct_sext32()). * Check it here, before the explore_alu_limits and !precise * short-circuits below (neither of which tests it). Note the * pre-existing BPF_FLAG_ADD_CONST check sits after those @@ -570,9 +572,18 @@ static bool regsafe(struct bpf_verifier_env *env, stru= ct bpf_reg_state *rold, * on a path that predates this series, which is a pruning change * that wants measuring on its own; it is deliberately left * alone here. + * + * Only demand a match when the old state carries a link at all. + * These flags are only ever set together with an ->id, so + * rold->id =3D=3D 0 implies none is set, and the only case this + * admits is "old knows no low-32 relationship, cur does" -- cur + * is then strictly more constrained than old, which is the safe + * direction for pruning. The reverse is still rejected. Without + * this a register that first acquires a link inside a loop would + * never match its pre-loop state and pruning would not converge. */ if (rold->id && - (rold->flags & BPF_FLAG_SUBREG_ZEXT) !=3D (rcur->flags & BPF_FLAG_SU= BREG_ZEXT)) + (rold->flags & BPF_FLAG_SUBREG) !=3D (rcur->flags & BPF_FLAG_SUBREG)) return false; =20 if (env->explore_alu_limits) { diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 8a802d49d0a4..45cb67dc3999 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -14976,6 +14976,8 @@ static int adjust_reg_min_max_vals(struct bpf_verif= ier_env *env, return 0; } =20 +static void reconstruct_sext32(struct bpf_reg_state *reg, struct bpf_reg_s= tate *src); + /* check validity of 32-bit and 64-bit arithmetic operations */ static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *ins= n) { @@ -15052,15 +15054,65 @@ static int check_alu_op(struct bpf_verifier_env *= env, struct bpf_insn *insn) insn->src_reg); return -EACCES; } else if (src_reg->type =3D=3D SCALAR_VALUE) { + int sz =3D insn->off >> 3; bool no_sext; + bool subreg_link; =20 no_sext =3D reg_umax(src_reg) < (1ULL << (insn->off - 1)); - if (no_sext) + /* + * When no_sext, dst =3D=3D src exactly, so link them + * (existing behavior). When !no_sext for a 32-bit sign + * extension the low 32 bits are still identical (sext + * preserves them), so form a BPF_FLAG_SUBREG_SEXT + * link: a later narrowing of the low 32 bits + * propagates here, and sync_linked_regs() rebuilds + * the high half via reconstruct_sext32(). + * + * An ADD_CONST-linked src is excluded for the same + * reason as in the zero-extending arm below: + * assign_scalar_id_before_mov() would clear its + * base+delta link, and a combined subreg+delta link + * isn't modeled anyway. Unlike that arm a self-mov is + * NOT excluded -- r0 =3D (s32)r0 is the case this is + * here for. + */ + subreg_link =3D (sz =3D=3D 4) && + !(src_reg->flags & BPF_FLAG_ADD_CONST); + + if (no_sext || subreg_link) assign_scalar_id_before_mov(env, src_reg); *dst_reg =3D *src_reg; - if (!no_sext) - clear_scalar_id(dst_reg); - coerce_reg_to_size_sx(dst_reg, insn->off >> 3); + if (!no_sext) { + if (subreg_link && src_reg->id) { + /* ->id already copied above */ + dst_reg->flags =3D (dst_reg->flags & ~BPF_FLAG_SUBREG) | + BPF_FLAG_SUBREG_SEXT; + } else { + clear_scalar_id(dst_reg); + } + } + /* + * coerce_reg_to_size_sx() falls back to the full sext + * range when smin/smax straddle the sign boundary (e.g. + * an errno-or-zero value clamped to [-4095, 0]). For a + * register tracked as the sign-extension of its low 32 + * bits the high half IS that sign-extension, so rebuild + * the tighter 64-bit range from the low bounds, taken + * from a snapshot because coerce overwrites them. + * + * Gated on sz =3D=3D 4, not on the flag alone: an (s8)/(s16) + * mov whose src is already SEXT-linked copies the flag + * across in the *dst_reg =3D *src_reg above, and a 32-bit + * reconstruction must not run for a narrower operation. + */ + if (sz =3D=3D 4 && (dst_reg->flags & BPF_FLAG_SUBREG_SEXT)) { + struct bpf_reg_state sext_src =3D *dst_reg; + + coerce_reg_to_size_sx(dst_reg, sz); + reconstruct_sext32(dst_reg, &sext_src); + } else { + coerce_reg_to_size_sx(dst_reg, sz); + } } else { mark_reg_unknown(env, regs, insn->dst_reg); } @@ -15107,7 +15159,15 @@ static int check_alu_op(struct bpf_verifier_env *e= nv, struct bpf_insn *insn) if (!is_src_reg_u32) { if (wide_subreg_link && src_reg->id) { /* ->id already copied above */ - dst_reg->flags |=3D BPF_FLAG_SUBREG_ZEXT; + /* + * Zero-extension: high bits are 0, not a + * sign-extension of the low field. Drop any + * SUBREG_SEXT copied from a sext-linked src + * so sync_linked_regs() rebuilds dst by + * zero-extension, not reconstruct_sext32(). + */ + dst_reg->flags =3D (dst_reg->flags & ~BPF_FLAG_SUBREG) | + BPF_FLAG_SUBREG_ZEXT; } else { clear_scalar_id(dst_reg); } @@ -15961,6 +16021,32 @@ static void collect_linked_regs(struct bpf_verifie= r_env *env, } } =20 +/* + * Set @reg to the sign-extension of the low 32 bits currently held by @sr= c. + * A BPF_FLAG_SUBREG_SEXT-linked register came from a 32-bit sign + * extension (r0 =3D (s32)r0): it shares @src's low 32 bits and its high b= its are + * the sign-extension of that low field. Only the value fields are written; + * @reg's linkage fields (id, delta, flags) are left intact by + * the caller (___mark_reg_known touches only var_off/r64/r32). Callers mu= st + * ensure no ADD_CONST delta is involved (see sync_linked_regs()). + */ +static void reconstruct_sext32(struct bpf_reg_state *reg, struct bpf_reg_s= tate *src) +{ + s32 s32min =3D reg_s32_min(src); + s32 s32max =3D reg_s32_max(src); + + if (s32min =3D=3D s32max) { + /* Low 32 bits are constant -> the whole value is the sext constant. */ + ___mark_reg_known(reg, (u64)(s64)s32min); + } else { + /* Sign-extension is monotonic over the signed-32 range. */ + reg_set_srange64(reg, (s64)s32min, (s64)s32max); + reg_set_srange32(reg, s32min, s32max); + reg->var_off =3D tnum_range((u64)(s64)s32min, (u64)(s64)s32max); + reg_bounds_sync(reg); + } +} + /* For all R in linked_regs, copy known_reg range into R * if R->id =3D=3D known_reg->id. */ @@ -15984,17 +16070,21 @@ static void sync_linked_regs(struct bpf_verifier_= env *env, struct bpf_verifier_s * A low-32 linked register shares only the base's low 32 bits; * the flag says how its high bits are derived. For * BPF_FLAG_SUBREG_ZEXT they are zero (32-bit zero-extending mov). + * For BPF_FLAG_SUBREG_SEXT they are the sign-extension of the low + * field (32-bit sign extension). * Rebuild it from known_reg's low 32 bits accordingly, but only * when neither side carries an ADD_CONST delta -- with a delta * the low bits differ from the base by that delta and the combined * subreg+ADD_CONST reconstruction isn't modeled here, so leave reg * unchanged (sound, just less precise). */ - if (reg->flags & BPF_FLAG_SUBREG_ZEXT) { + if (reg->flags & BPF_FLAG_SUBREG) { if (!((reg->flags | known_reg->flags) & BPF_FLAG_ADD_CONST)) { - { + if (reg->flags & BPF_FLAG_SUBREG_SEXT) { + reconstruct_sext32(reg, known_reg); + } else { u32 saved_id =3D reg->id; - u8 saved_subreg =3D reg->flags & BPF_FLAG_SUBREG_ZEXT; + u8 saved_subreg =3D reg->flags & BPF_FLAG_SUBREG; =20 /* * reg =3D zext32(known_reg): its low 32 bits come from @@ -16008,7 +16098,7 @@ static void sync_linked_regs(struct bpf_verifier_en= v *env, struct bpf_verifier_s */ *reg =3D *known_reg; reg->id =3D saved_id; - reg->flags =3D (reg->flags & ~BPF_FLAG_SUBREG_ZEXT) | saved_subreg; + reg->flags =3D (reg->flags & ~BPF_FLAG_SUBREG) | saved_subreg; zext_32_to_64(reg); reg_bounds_sync(reg); } @@ -16024,7 +16114,7 @@ static void sync_linked_regs(struct bpf_verifier_en= v *env, struct bpf_verifier_s * copying known_reg's low-32-only state into a full register would * be unsound, so leave reg unchanged. */ - if (known_reg->flags & BPF_FLAG_SUBREG_ZEXT) + if (known_reg->flags & BPF_FLAG_SUBREG) continue; /* * Skip mixed 32/64-bit links: the delta relationship doesn't --=20 2.53.0-Meta From nobody Mon Sep 28 23:51:14 2026 Received: from mta1.migadu.com (out-172.mta1.migadu.com [95.215.58.172]) (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 7CCA23EBF16 for ; Fri, 14 Aug 2026 23:20:25 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=95.215.58.172 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786749632; cv=none; b=fDa87fvS62F/sI8aeioMur6SNxRxcIq2hPTco/cMTzeBQubgysGNWZwj2l9ncE/pYShuhVbEJiTLi0/VbAeaU7jGXCN1rLKCURjJd9M3GL1p72T8Gr2rV+frv11mrdnTrT4CHUP/2tvNCdJbk3JjdOWRnaoM3GiChPw3KOOyETg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1786749632; c=relaxed/simple; bh=GoZ8TTax1JgQYYIBb8FdR0AkUUyzp+AN9V/No5qI2XU=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=eQ3HF4dcuje06yiOXr0efUcJK2Z577Ga7OradmM5CbC9mAEnkyHIaEKIZN70hrI52MtlAZSpWpauxMqZr+dFuCE+hbaPZ1rEOlgPCUjyfj4kAt82gze9Dw6ci/eo8w+VmtfIqlPlsJK4Jy1t+ieCp7TNQUkVAgc3uGIAuAHvC8M= 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=fAhADP9w; arc=none smtp.client-ip=95.215.58.172 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="fAhADP9w" X-Envelope-To: linux-kernel@vger.kernel.org DKIM-Signature: a=rsa-sha256; bh=GoZ8TTax1JgQYYIBb8FdR0AkUUyzp+AN9V/No5qI2XU=; c=simple/simple; d=linux.dev; h=from:to:subject:date:message-id:mime-version:content-type; s=key1; t=1786749621; v=1; x=1787354421; b=fAhADP9wFoLqIMpVvcQ4q44t7CvqWKtRhDYO1UQE88O5Umiqo6CO5GMuB8ta9NgbwzLrS2+z ToNPgUR75My8hSEq62b0+73cTq6/QT5DrlgIReYGHQvdQ8Kw1PeeMmfMh2BH7C6RnyUbNPOPr0q nl+A0E8GhnaJB2NcGBd8H3Co= X-Envelope-To: linux-kernel@vger.kernel.org Received: from gvineet-fedora-PF5JGVFY.thefacebook.com (2620:10d:c090:500::6:7be4) by smtp.migadu.com with ESMTPS id f0777d4a1a44ec89; Fri, 14 Aug 2026 23:20:21 +0000 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: [RFC bpf-next 6/6] selftests/bpf: cover 32-bit sign-extension low-32 links Date: Fri, 14 Aug 2026 16:19:45 -0700 Message-ID: <20260814231945.3884596-7-vineet.gupta@linux.dev> X-Mailer: git-send-email 2.55.0 In-Reply-To: <20260814231945.3884596-1-vineet.gupta@linux.dev> References: <20260814231945.3884596-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" Tests for the BPF_FLAG_SUBREG_SEXT link, mostly built with the div-by-zero-guard idiom: the div is unreachable iff the verifier deduces the sign-extended register is 0, so a missed deduction turns __success into a "div by zero" rejection. Deduction through the link: - sext_linked_low_narrow_to_zero, sext_linked_separate_dest_narrow_to_zero: in-place and dst !=3D src sign extension narrowed to 0 via the source's = low 32 bits. The separate-dest case runs with BPF_F_TEST_STATE_FREQ so the link has to survive state cleaning. - sext_narrow_{branch_on_source,copied_back,inplace_pre_copy,spill_fill}: variants derived from real "R0 ... should have been in [0, 1]" exit rejections -- branch on source vs dest, copy-back, spill/fill across a call. - sext_resext_preserves_range: a redundant re-sext of a value clamped to [-4095, 0] must keep the tight range (the errno-or-zero return pattern). In-loop behaviour: - sext_in_loop_converges: convergence regression test, reproducing the bytecode bpf-gcc emits for a cond_break loop -- a counter incremented wi= th an ALU32 add (zero-extending the high half) then sign-extended in place every iteration. Forming the link refreshes the linked scalar id and BPF_FLAG_SUBREG_SEXT each iteration, so the loop-carried state never repeats. It converges only because regsafe() demands a matching low-32 link just when the old state already has one: ~9 insns with that, versus a load failure at 1,000,001 insns without. - sext_in_loop_separate_dest_index: the companion case, a fresh in-loop te= mp (a bounds-checked array index) that is dead across the back-edge. The li= nk is formed here too -- there is no liveness or loop-carried exclusion, the gate is just (sz =3D=3D 4) -- but because the temp is not loop-carried t= he link costs nothing in convergence and simply buys precision: the narrowing reaches it. Interaction with the zero-extending link: - zext_mov_from_sext_src_zero_extends: a 32-bit zero-extending mov (w2 =3D= w1) whose source is sign-extended (r1 =3D (s32)r6) must still zero-extend, i= .e. its link must be BPF_FLAG_SUBREG_ZEXT and must not inherit BPF_FLAG_SUBREG_SEXT from the source. Otherwise sync_linked_regs() rebui= lds the destination with reconstruct_sext32() on a later low-32 narrowing, computing a negative value for what is really a large positive zero-extended one. After "if w6 s>=3D 0" falls through, r6's low 32 bits= have bit 31 set, so the zero-extended r2 is strictly positive and the guarded div is reachable only on a mis-reconstruction. Two more cases the earlier tests did not reach: - sext_mov_keeps_add_const_src: mirror of zext_mov_keeps_add_const_src. A sext whose source carries an ADD_CONST delta must not destroy that link. Fails without the ADD_CONST source exclusion in the previous patch. - sext_dest_driven_does_not_narrow_base: narrows the LINKED register and requires the wide base is NOT narrowed, i.e. the "known_reg is subreg-linked" continue in sync_linked_regs(). A __failure test -- the div must stay reachable. Companion to the zero-extend version added by the zero-extend selftest patch. All are written in asm so the bytecode is identical regardless of the host BPF compiler. Signed-off-by: Vineet Gupta --- .../bpf/progs/verifier_linked_scalars.c | 412 ++++++++++++++++++ 1 file changed, 412 insertions(+) diff --git a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c b/= tools/testing/selftests/bpf/progs/verifier_linked_scalars.c index 2cc6f9e45aff..ff71e168d4cc 100644 --- a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c +++ b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c @@ -826,4 +826,416 @@ l_out_%=3D: \ : __clobber_all); } =20 +/* + * The tests below use the cpuv4 32-bit sign extension (r0 =3D (s32)r0), s= o they + * need a compiler that can emit it and a JIT that can run it. Same gate as + * verifier_movsx.c, except the compiler clause also accepts bpf-gcc, which + * does not define __clang_major__ but does define __BPF_FEATURE_MOVSX. + * + * The tests above do not need cpuv4, so the guard starts here rather than + * covering the whole file. + */ +#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)) + +/* + * Sign-extension linked-register tracking, in-place narrow-to-zero. + * + * r1 =3D r0 ties r0,r1 with a shared id. r0 =3D (s32)r0 sign-extends r0's= low 32 + * bits; the helper return is a full 64-bit unknown so the sign bit isn't + * provably 0, and r0 keeps a BPF_FLAG_SUBREG_SEXT link to r1. On the w1 = =3D=3D 0 + * fall-through, r1's low 32 bits are 0; r0's low 32 bits equal r1's and r= 0's + * upper bits are the sign-extension of that (0) -- so r0 =3D=3D 0. + * + * The guarded div-by-zero is unreachable iff the verifier deduces r0 =3D= =3D 0. + */ +SEC("socket") +__success +__naked void sext_linked_low_narrow_to_zero(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r1 =3D r0; /* r1 =3D=3D r0, shared id */ \ + r0 =3D (s32)r0; /* r0 =3D sext32(r0) */ \ + if w1 !=3D 0 goto l0_%=3D; /* fall-through: w1 =3D=3D 0 */ \ + /* want deduced here: r0 =3D=3D 0 */ \ + if r0 =3D=3D 0 goto l0_%=3D; /* always taken iff r0=3D=3D0 known */ \ + r0 /=3D 0; /* unreachable iff r0=3D=3D0 deduced */ \ +l0_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * Separate-dest sign-extension: r3 =3D (s32)r2 (dst !=3D src). r2,r3 shar= e a base + * id (r3 with BPF_FLAG_SUBREG_SEXT). On the w2 =3D=3D 0 fall-through, r2'= s low 32 bits are + * 0, so r3 =3D sext32(0) =3D 0 and the guarded div-by-zero is unreachable. + * + * Runs with BPF_F_TEST_STATE_FREQ to force checkpointing between the sext= and + * the branch: the sext linkage (BPF_FLAG_SUBREG_SEXT) must survive state + * cleaning so sync_linked_regs() can still reconstruct r3. bpf_clear_sing= ular_ids() + * strips the link flags when counting base ids; otherwise r3's compound i= d looks + * singular and gets cleared, and r3 stays wide. + */ +SEC("socket") +__success +__flag(BPF_F_TEST_STATE_FREQ) +__naked void sext_linked_separate_dest_narrow_to_zero(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r2 =3D r0; /* r2,(r0) linked, id N */ \ + r3 =3D (s32)r2; /* r3 =3D sext32(r2): SEXT link base N */ \ + if w2 !=3D 0 goto l0_%=3D; /* fall-through: w2 =3D=3D 0 */ \ + /* want deduced here: r3 =3D=3D 0 */ \ + if r3 =3D=3D 0 goto l0_%=3D; /* always taken iff r3=3D=3D0 known */ \ + r0 /=3D 0; /* unreachable iff r3=3D=3D0 deduced */ \ +l0_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * Coverage derived from real "R0 ... should have been in [0, 1]" exit + * rejections. Each sign-extends a value, then a branch proves its low 32 = bits + * are 0 so the sext result must be 0. Expressed with the div-by-zero idio= m (same + * deduced range the return-code check reads): the div is unreachable iff = the + * verifier deduces the sext register is 0. + */ + +/* 1: branch on the SOURCE reg; separate dest (value stands in for a u32 l= oad). */ +SEC("socket") +__success +__naked void sext_narrow_branch_on_source(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r2 =3D r0; /* r2 =3D value (proxy for u32 load) */ \ + r0 =3D (s32)r2; /* r0 =3D sext32(r2) */ \ + if w2 !=3D 0 goto l0_%=3D; /* w2 !=3D 0: r0 unknown, skip */ \ + if r0 =3D=3D 0 goto l0_%=3D; /* w2 =3D=3D 0: r0 must be 0 */ \ + r0 /=3D 0; \ +l0_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* 2: sext into r7, prove via w0, then copy r7 back into r0. */ +SEC("socket") +__success +__naked void sext_narrow_copied_back(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r7 =3D (s32)r0; /* r7 =3D sext32(r0) */ \ + if w0 !=3D 0 goto l0_%=3D; /* w0 !=3D 0: skip */ \ + r0 =3D r7; /* w0 =3D=3D 0: r0 =3D r7 (must be 0) */ \ + if r0 =3D=3D 0 goto l0_%=3D; \ + r0 /=3D 0; \ +l0_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* 3: in-place sext; branch on the pre-sext copy r1 (=3D=3D direction). */ +SEC("socket") +__success +__naked void sext_narrow_inplace_pre_copy(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r1 =3D r0; /* pre-sext copy, linked */ \ + r0 =3D (s32)r0; /* in-place sext32 */ \ + if w1 =3D=3D 0 goto l_chk_%=3D;/* w1 =3D=3D 0: r0 must be 0 */ \ + goto l0_%=3D; /* w1 !=3D 0: nothing to check */ \ +l_chk_%=3D: \ + if r0 =3D=3D 0 goto l0_%=3D; \ + r0 /=3D 0; \ +l0_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* 4: sext, prove via w0, spill to stack across a call, reload, use. */ +SEC("socket") +__success +__naked void sext_narrow_spill_fill(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r9 =3D (s32)r0; /* r9 =3D sext32(r0) */ \ + if w0 !=3D 0 goto l0_%=3D; /* w0 !=3D 0: skip */ \ + /* w0 =3D=3D 0: r9 must be 0 */ \ + *(u64 *)(r10 - 8) =3D r9; /* spill r9 */ \ + call %[bpf_get_prandom_u32];/* clobbers r0-r5 */ \ + r5 =3D *(u64 *)(r10 - 8); /* reload -> must be 0 */ \ + if r5 =3D=3D 0 goto l0_%=3D; \ + r0 /=3D 0; \ +l0_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * A redundant 32-bit sign-extension of an already-narrowed value must pre= serve + * the range. This is the errno-or-zero return pattern (set_if_not_errno_o= r_zero() + * followed by "return ret" on an int): the value is clamped to [-4095, 0]= and + * then sign-extended again, e.g. verify_pkcs7_sig / many lsm.s progs under + * bpf-gcc. coerce_reg_to_size_sx() bails to the full [S32_MIN, S32_MAX] r= ange + * when the range straddles the sign boundary (smin<0, smax>=3D0), so with= out the + * sext-self reconstruction the final "r0 =3D (s32)r0" widens [-4095, 0] b= ack to + * the full range and the program is rejected. Knowing the high half is the + * sign-extension of the low 32 bits lets the verifier rebuild the tight r= ange. + */ +SEC("socket") +__success +__naked void sext_resext_preserves_range(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r0 =3D (s32)r0; /* r0 =3D [S32_MIN, S32_MAX] */ \ + if r0 s> 0 goto l_out_%=3D; /* r0 <=3D 0 */ \ + if r0 s< -4095 goto l_out_%=3D; /* r0 in [-4095, 0] */ \ + r0 =3D (s32)r0; /* redundant re-sext (pkcs7 pattern) */ \ + if r0 s>=3D -4095 goto l_lo_ok_%=3D;/* must hold if range kept */ \ + r0 /=3D 0; /* reached only if lower bound lost */ \ +l_lo_ok_%=3D: \ + if r0 s<=3D 0 goto l_out_%=3D; /* must hold if range kept */ \ + r0 /=3D 0; /* reached only if upper bound lost */ \ +l_out_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * A 32-bit sign-extension INSIDE a loop must verify and converge. This is= the + * bytecode pattern bpf-gcc emits for a cond_break loop (see cond_break4):= a + * counter is incremented with an ALU32 add (which zero-extends the high h= alf) + * and then sign-extended in place every iteration. + * + * The verifier links dst<->src on a sign-extension. Doing that for a sext= on a + * register carried across the loop back-edge mints/refreshes the linked s= calar + * id and its BPF_FLAG_SUBREG_SEXT metadata each iteration; combined with = the + * ALU32 add's BPF_FLAG_ADD_CONST delta the loop-carried state never repea= ts, so state + * pruning can't converge and verification runs to the 1M instruction limi= t. + * + * The regsafe() guard on the low-32 link flags is what prevents this: it = only demands a + * match when the OLD state already carries a link (rold->id), so a regist= er that + * first picks up a sext link inside the loop can still match its pre-loop= state. + * Without that guard the loop-carried r2 never matches and the load fails= at + * 1,000,001 insns, i.e. this __success flips to a load failure -- so this= is the + * regression test for it. (See sext_in_loop_separate_dest_index for the + * companion case, a fresh in-loop temp that keeps its link for precision.) + * + * The pattern is written in asm so the bytecode is identical regardless o= f the + * host BPF compiler. + */ +SEC("socket") +__success +__naked void sext_in_loop_converges(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r2 =3D r0; /* r2 =3D 64-bit unknown (helper ret) */ \ +l_body_%=3D: \ + .byte 0xe5; /* may_goto l_exit (loop bound) */ \ + .byte 0; \ + .short 3; \ + .long 0; \ + w2 +=3D 1; /* ALU32 add: low +=3D 1, high =3D 0 */ \ + r2 =3D (s32)r2; /* in-place in-loop sign-extend */ \ + goto l_body_%=3D; \ +l_exit_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * A separate-destination 32-bit sign extension INSIDE a loop keeps its lo= w-32 + * link, so a later bounds check on the source narrows the sign-extended + * destination too. This is the bytecode a bpf-gcc build emits for array i= ndexing + * in a bpf_for loop -- a fresh 32-bit index load, a separate "r1 =3D (s32= )r0", + * then a bounds check on the index (verifier_global_subprogs' syscall_arr= ay_bpf_for). + * + * Both in-loop cases form the link -- subreg_link is just (sz =3D=3D 4), = with no + * liveness or loop-carried exclusion. What differs is what the link buys.= Here + * the destination is a fresh temp, dead across the back-edge, so the link= is + * pure precision: "if w0 > 99" narrows r1 to [0, 99] and the guarded + * div-by-zero is unreachable. In sext_in_loop_converges the target is the + * loop-carried counter, so the link is re-formed every iteration and the + * question is convergence instead -- answered by the regsafe() rold->id g= uard, + * not by declining to link. + * + * Written in asm so the bytecode is identical regardless of the host BPF + * compiler. + */ +SEC("socket") +__success +__naked void sext_in_loop_separate_dest_index(void) +{ + asm volatile (" \ +l_body_%=3D: \ + .byte 0xe5; /* may_goto l_exit (loop bound) */ \ + .byte 0; \ + .short 7; \ + .long 0; \ + call %[bpf_get_prandom_u32];/* r0 =3D fresh u32 each iter */ \ + r1 =3D (s32)r0; /* in-loop separate-dest sext */ \ + if w0 > 0x63 goto l_body_%=3D;/* fall-through: w0 <=3D 99 */ \ + /* want r1 =3D sext32(r0 low) =3D=3D [0, 99] here (needs the link) */ \ + if r1 > 0x63 goto l_err_%=3D;/* taken unless r1 narrowed */ \ + goto l_body_%=3D; \ +l_err_%=3D: \ + r0 /=3D 0; /* reachable iff r1 not narrowed */ \ + goto l_body_%=3D; \ +l_exit_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * A 32-bit zero-extending mov (w2 =3D w1) whose SOURCE is a sign-extended= register + * must still zero-extend: dst's high bits are 0, not the sign-extension o= f the + * low field. Regression test for the zext link clearing BPF_FLAG_SUBREG_S= EXT (otherwise + * dst would inherit SUBREG_SEXT from the sext'd source, and sync_linked_r= egs() + * would later rebuild it with reconstruct_sext32() -- computing a negativ= e value + * for what is actually a large positive zero-extended one). + * + * r1 =3D (s32)r6 makes r1 a sext-linked wide source; w2 =3D w1 forms the = zext link. + * After "if w6 s>=3D 0" falls through, r6's low 32 bits have bit 31 set, = so the + * zero-extended r2 must be in [0x80000000, 0xffffffff]. Two guards assert= that + * whole range, so the test needs the feature present, not merely the abse= nce of + * the sext-leak bug: "r2 s< 0" catches the leak (r2 rebuilt negative), and + * "w2 s>=3D 0" catches the low-32 link being absent entirely (r2 not narr= owed to + * the high half, so bit 31 is not known set). Either makes the div reacha= ble. + */ +SEC("socket") +__success +__naked void zext_mov_from_sext_src_zero_extends(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; /* r6 low =3D unknown u32 (callee-saved) */ \ + call %[bpf_get_prandom_u32]; \ + r0 <<=3D 32; \ + r6 |=3D r0; /* r6 =3D full 64-bit unknown (width 64) */ \ + r1 =3D (s32)r6; /* r1 =3D sext32(r6 low): SUBREG_SEXT, wide */ \ + w2 =3D w1; /* zext mov from sext-linked wide src */ \ + if w6 s>=3D 0 goto l_out_%=3D;/* fall-through: r6 low has bit 31 set */ \ + /* r2 =3D zext32(r6 low) must be in [0x80000000, 0xffffffff]: */ \ + if r2 s< 0 goto l_err_%=3D;/* sext leak: r2 wrongly negative */ \ + if w2 s>=3D 0 goto l_err_%=3D;/* link absent: r2 low bit 31 not known set= */ \ + goto l_out_%=3D; \ +l_err_%=3D: \ + r0 /=3D 0; /* r2 not proven in [0x80000000, 0xffffffff] */ \ +l_out_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * Mirror of zext_mov_keeps_add_const_src for the sign-extending mov: a se= xt + * whose source carries an ADD_CONST delta must not destroy that link. + * + * Forming a low-32 link calls assign_scalar_id_before_mov(), which clears= an + * ADD_CONST src, so the sext arm excludes such a source exactly as the ze= xt + * arm does. Without that exclusion r5 loses its base+delta relationship t= o r6 + * here, "if r6 > 10" no longer narrows r5, and the guarded div becomes + * reachable. + */ +SEC("socket") +__success +__naked void sext_mov_keeps_add_const_src(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; /* r6 low =3D unknown u32 */ \ + call %[bpf_get_prandom_u32]; \ + r0 <<=3D 32; \ + r6 |=3D r0; /* r6 =3D full 64-bit unknown (base) */ \ + r5 =3D r6; /* r5, r6 linked (shared id) */ \ + r5 +=3D 3; /* r5 =3D base + 3: ADD_CONST, still wide */ \ + r7 =3D (s32)r5; /* 32-bit sext mov, ADD_CONST src */ \ + if r6 > 10 goto l_out_%=3D;/* r6 in [0, 10] */ \ + /* r5 =3D r6 + 3 must be in [3, 13] here (needs the kept link) */ \ + if r5 > 13 goto l_err_%=3D;/* taken only if r5 not narrowed */ \ + goto l_out_%=3D; \ +l_err_%=3D: \ + r0 /=3D 0; /* reachable iff r5's link was cleared */ \ +l_out_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +/* + * Dest-driven direction, sign-extend flavour: narrowing the LINKED regist= er + * must not narrow the wide base. + * + * r7 =3D (s32)r6 shares only r6's low 32 bits. Learning r7 =3D=3D 0 says = nothing + * about r6's high half, so sync_linked_regs() must leave r6 alone -- that= is + * the "known_reg is subreg-linked" continue. If it ever propagated, r6 wo= uld + * be known 0 here and the div would be treated as unreachable, so the pro= gram + * must be REJECTED. + */ +SEC("socket") +__failure __msg("div by zero") +__flag(BPF_F_TEST_STATE_FREQ) +__naked void sext_dest_driven_does_not_narrow_base(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r6 =3D r0; /* r6 low =3D unknown u32 */ \ + call %[bpf_get_prandom_u32]; \ + r0 <<=3D 32; \ + r6 |=3D r0; /* r6 =3D full 64-bit unknown (base) */ \ + r7 =3D (s32)r6; /* low-32 SEXT link */ \ + if r7 !=3D 0 goto l_out_%=3D;/* r7 =3D=3D 0: low 32 bits are 0 */ \ + if r6 !=3D 0 goto l_out_%=3D;/* r6 may still have high bits set */ \ + r0 /=3D 0; /* must stay reachable */ \ +l_out_%=3D: \ + r0 =3D 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + +#endif /* cpuv4 sign extension */ + char _license[] SEC("license") =3D "GPL"; --=20 2.53.0-Meta