From: Vineet Gupta <vineet.gupta@linux.dev>
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 <vineet.gupta@linux.dev>
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 [thread overview]
Message-ID: <20260814231945.3884596-6-vineet.gupta@linux.dev> (raw)
In-Reply-To: <20260814231945.3884596-1-vineet.gupta@linux.dev>
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 = (s32)r0) preserves the source's low 32 bits and
sets the high bits to their sign-extension. When the sign bit is not provably
zero the verifier clears the destination's scalar id, so a later narrowing of
the low 32 bits (e.g. "if w1 == 0") never reaches the sign-extended register 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=v4 builds.
1. callback exit-code checks that reject "R0 ... should have been in [0, 1]".
| 0: (61) r2 = *(u32 *)(r1 +24)
| 1: (bf) r0 = (s32)r2
| 2: (56) if w2 != 0x0 goto pc+10 ; R2=0 (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 widens
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=scalar(id=7,smin=smin32=-4095,smax=0x7fffffff)
| 70: (d5) if r0 s<= 0x0 goto pc+1 72:
| 72: (bf) r0 = (s32)r0 ; R0=scalar(smin=0xffffffff80000000,smax=0x7fffffff)
| 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 complex".
Solution
========
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_SEXT
so its high bits are known to be the sign-extension of the low field. Only
32-bit sign extension is tracked -- (s8)/(s16) do not form a link (not seen
in codegen so far) -- so the flag alone carries the width and no extra field
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 bits,
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 = (s32)r0 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) != (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 == 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 <vineet.gupta@linux.dev>
---
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 = (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, struct 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, struct 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 == 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) != (rcur->flags & BPF_FLAG_SUBREG_ZEXT))
+ (rold->flags & BPF_FLAG_SUBREG) != (rcur->flags & BPF_FLAG_SUBREG))
return false;
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_verifier_env *env,
return 0;
}
+static void reconstruct_sext32(struct bpf_reg_state *reg, struct bpf_reg_state *src);
+
/* check validity of 32-bit and 64-bit arithmetic operations */
static int check_alu_op(struct bpf_verifier_env *env, struct bpf_insn *insn)
{
@@ -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 == SCALAR_VALUE) {
+ int sz = insn->off >> 3;
bool no_sext;
+ bool subreg_link;
no_sext = reg_umax(src_reg) < (1ULL << (insn->off - 1));
- if (no_sext)
+ /*
+ * When no_sext, dst == 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 = (s32)r0 is the case this is
+ * here for.
+ */
+ subreg_link = (sz == 4) &&
+ !(src_reg->flags & BPF_FLAG_ADD_CONST);
+
+ if (no_sext || subreg_link)
assign_scalar_id_before_mov(env, src_reg);
*dst_reg = *src_reg;
- if (!no_sext)
- clear_scalar_id(dst_reg);
- coerce_reg_to_size_sx(dst_reg, insn->off >> 3);
+ if (!no_sext) {
+ if (subreg_link && src_reg->id) {
+ /* ->id already copied above */
+ dst_reg->flags = (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 == 4, not on the flag alone: an (s8)/(s16)
+ * mov whose src is already SEXT-linked copies the flag
+ * across in the *dst_reg = *src_reg above, and a 32-bit
+ * reconstruction must not run for a narrower operation.
+ */
+ if (sz == 4 && (dst_reg->flags & BPF_FLAG_SUBREG_SEXT)) {
+ struct bpf_reg_state sext_src = *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 *env, struct bpf_insn *insn)
if (!is_src_reg_u32) {
if (wide_subreg_link && src_reg->id) {
/* ->id already copied above */
- dst_reg->flags |= 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 = (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_verifier_env *env,
}
}
+/*
+ * Set @reg to the sign-extension of the low 32 bits currently held by @src.
+ * A BPF_FLAG_SUBREG_SEXT-linked register came from a 32-bit sign
+ * extension (r0 = (s32)r0): it shares @src's low 32 bits and its high bits 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 must
+ * ensure no ADD_CONST delta is involved (see sync_linked_regs()).
+ */
+static void reconstruct_sext32(struct bpf_reg_state *reg, struct bpf_reg_state *src)
+{
+ s32 s32min = reg_s32_min(src);
+ s32 s32max = reg_s32_max(src);
+
+ if (s32min == 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 = 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 == 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 = reg->id;
- u8 saved_subreg = reg->flags & BPF_FLAG_SUBREG_ZEXT;
+ u8 saved_subreg = reg->flags & BPF_FLAG_SUBREG;
/*
* reg = zext32(known_reg): its low 32 bits come from
@@ -16008,7 +16098,7 @@ static void sync_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier_s
*/
*reg = *known_reg;
reg->id = saved_id;
- reg->flags = (reg->flags & ~BPF_FLAG_SUBREG_ZEXT) | saved_subreg;
+ reg->flags = (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_env *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
--
2.53.0-Meta
next prev parent reply other threads:[~2026-08-14 23:20 UTC|newest]
Thread overview: 7+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-14 23:19 [RFC bpf-next 0/6] bpf: track scalar equality across the low 32 bits Vineet Gupta
2026-08-14 23:19 ` [RFC bpf-next 1/6] bpf: turn bpf_reg_state->precise into a flags field [NFC] Vineet Gupta
2026-08-14 23:19 ` [RFC bpf-next 2/6] bpf: move the linked-scalar flags into bpf_reg_state->flags [NFC] Vineet Gupta
2026-08-14 23:19 ` [RFC bpf-next 3/6] bpf: support low-32 subreg scalar linking for zero-extending movs Vineet Gupta
2026-08-14 23:19 ` [RFC bpf-next 4/6] selftests/bpf: cover low-32 subreg-equal link " Vineet Gupta
2026-08-14 23:19 ` Vineet Gupta [this message]
2026-08-14 23:19 ` [RFC bpf-next 6/6] selftests/bpf: cover 32-bit sign-extension low-32 links Vineet Gupta
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260814231945.3884596-6-vineet.gupta@linux.dev \
--to=vineet.gupta@linux.dev \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=emil@etsalapatis.com \
--cc=ihor.solodrai@linux.dev \
--cc=john.fastabend@gmail.com \
--cc=jolsa@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=martin.lau@linux.dev \
--cc=memxor@gmail.com \
--cc=shuah@kernel.org \
--cc=song@kernel.org \
--cc=yonghong.song@linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox