From: Vineet Gupta <vineet.gupta@linux.dev>
To: sashiko-reviews@lists.linux.dev
Cc: bpf@vger.kernel.org
Subject: Re: [PATCH bpf-next v3 06/12] bpf: Track low-32 scalar equality across sign-extending movs
Date: Mon, 26 Oct 2026 13:46:45 -0700 [thread overview]
Message-ID: <8653c48c-8bdb-40be-adf7-8ea2db8f4298@linux.dev> (raw)
In-Reply-To: <sashiko-outbox-179390@kernel.org>
On 10/20/26 9:57 AM, sashiko-bot@kernel.org wrote:
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [Medium] bpf: empty range after reg_bounds_sync() in sync_linked_regs()
> --
>
> commit ad01f18c15eb8cfa9a97b984758db9dd254aace2
> Author: Vineet Gupta <vineet.gupta@linux.dev>
>
> bpf: Track low-32 scalar equality across sign-extending movs
>
> This commit adds BPF_REG_LINK_SEXT to track low-32 scalar equality across
> 32-bit sign-extending moves, using sext_32_to_64() and tnum_sext() to
> propagate bounds in sync_linked_regs().
>
>> diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
>> index 6afd718ede647..4621b476b206e 100644
>> --- a/kernel/bpf/verifier.c
>> +++ b/kernel/bpf/verifier.c
> [ ... ]
>> @@ -6090,6 +6090,25 @@ static void zext_32_to_64(struct bpf_reg_state *reg)
>> reg_set_urange64(reg, reg_u32_min(reg), reg_u32_max(reg));
>> }
>>
>> +/*
>> + * The sign-extending counterpart. Signed bounds carry over directly because
>> + * sign extension is monotonic over the signed 32-bit range.
>> + */
>> +static void sext_32_to_64(struct bpf_reg_state *reg)
>> +{
>> + /*
>> + * See comment at reg_step_check_unsigned(). Sign extension subtracts
>> + * 2^32 from every value with bit 31 set, which preserves a residue
>> + * mod step only when step divides 2^32, so the line cannot be kept.
>> + * The zero-extending twin calls reg_step_check_unsigned(reg, 32) for
>> + * the same reason; the unsigned form does not apply here because the
>> + * result is not the low 32 bits but the low 32 bits moved.
>> + */
>> + reg_step_reset(reg);
>> + reg->var_off = tnum_sext(reg->var_off, 4);
>> + reg_set_srange64(reg, reg_s32_min(reg), reg_s32_max(reg));
>> +}
> [ ... ]
>> @@ -18795,9 +18843,26 @@ static void sync_linked_regs(struct bpf_verifier_env *env, struct bpf_verifier_s
>> scalar32_min_max_add(reg, &fake_reg);
>> scalar_min_max_add(reg, &fake_reg);
>> reg->var_off = tnum_add(reg->var_off, fake_reg.var_off);
>> - if (reg->link_kind == BPF_REG_LINK_ZEXT ||
>> - known_reg->link_kind == BPF_REG_LINK_ZEXT)
>> + if (reg->link_kind == BPF_REG_LINK_SEXT) {
>> + sext_32_to_64(reg);
>> + } else if (reg->link_kind == BPF_REG_LINK_ZEXT ||
>> + known_reg->link_kind == BPF_REG_LINK_ZEXT ||
>> + known_reg->link_kind == BPF_REG_LINK_SEXT) {
>> + /*
>> + * Either @reg is itself the zero extension, or
>> + * @reg has no 32-bit kind and @known_reg does
>> + * -- the reverse direction, where the guard
>> + * above has already proved @reg fits 32 bits.
>> + * Then @reg is @known_reg's low half with
>> + * nothing above it, whichever way @known_reg
>> + * filled its own high half, so the zero
>> + * extension is the reconstruction for both
>> + * kinds. BPF_REG_LINK_FULL is not here on
>> + * purpose: it relates all 64 bits and must not
>> + * be truncated.
>> + */
>> zext_32_to_64(reg);
>> + }
>> /*
>> * Intersect rather than overwrite. A BPF_REG_LINK_ZEXT
>> * link over a wide base does not propagate back, so the
> [Severity: Medium]
> Can sync_linked_regs() leave reg with CNUM64_EMPTY and CNUM32_EMPTY after
> reg_bounds_sync(), triggering a register invariants violation on the next
> instruction that inspects the register?
Yes and actually two things:
- tnum_intersect() has no tnum_overlap() precheck, so two disagreeing
known bits become a known 1 rather than a detected contradiction.
- range_bounds_violation() tests only cnum32/64_is_empty, and it runs
*before* reg_bounds_sync(). A contradiction that only materialises
inside the sync through __update_reg_bounds() or
deduce_bounds_32_from_64() is therefore not caught by the restore that
4/12 added.
So the guard added in 4/12 is narrower than the problem it is named for.
The reproducer in your 4/12 report does not fire, though, and the log
says why. Built as a socket filter on a patch-10 kernel it verifies
clean, and the walk goes 0..7 then straight to 10:
5: R7=scalar(id=1+0,...,var_off=(0x4; 0x1))
6: (45) if r6 & 0x2 goto pc+1
6: R6=scalar(id=1,...,var_off=(0x4; 0xffffffff00000001))
7: (05) goto pc+2
10: (b7) r0 = 0
The "r9 = 0; r7 %= r9" pair at 8 and 9 is never walked. r6's var_off has
bit 1 known zero: value 0x4, and bit 1 is not in the mask, so "if r6 &
2" is resolved statically and the contradicting path is pruned.
r6 knows that because of 8/12 itself: its reverse arm pinned the base's
low half from r7, which is why R6 carries smin32/umin32=4,
smax32/umax32=5 and the low var_off (0x4; 0x1) by instruction 6. The
same issue hid the original 4/12 bug, 8/12 keeps masking things rather
than fixing them.
Rather than keep on chasing which reproducers survives patch 8's
masking, v4 does what you suggested at the end of the 4/12 report: ask
tnum_overlap() before tnum_intersect(), and check
range_bounds_violation() after reg_bounds_sync() as well as before. Both
arms now go through one helper:
static void sync_bounds_or_revert(struct bpf_reg_state *reg,
const struct bpf_reg_state *prev)
{
if (range_bounds_violation(reg)) {
*reg = *prev;
return;
}
reg_bounds_sync(reg);
if (range_bounds_violation(reg))
*reg = *prev;
}
Thx,
-Vineet
> At the end of the differencing block in sync_linked_regs():
>
> cnum64_intersect_with(®->r64, prev.r64);
> cnum32_intersect_with(®->r32, prev.r32);
> reg->var_off = tnum_intersect(reg->var_off, prev.var_off);
> if (range_bounds_violation(reg))
> *reg = prev;
> reg_bounds_sync(reg);
>
> range_bounds_violation(reg) is only checked before reg_bounds_sync(reg),
> and tnum_intersect() is called without checking tnum_overlap(). When a
> contradiction occurs across domains (between r64, r32, and var_off) rather
> than within r64 or r32 individually, the pre-sync range_bounds_violation()
> check passes, and reg_bounds_sync(reg) then turns the cross-domain
> disjointness into CNUM64_EMPTY / CNUM32_EMPTY without restoring prev.
>
> This can be triggered in two ways with BPF_REG_LINK_SEXT:
>
> 1. Cross-domain contradiction between r64 and r32 via sext_32_to_64():
>
> Suppose r6 is a 64-bit unknown scalar and r7 = (s32)r6 links r7 to r6
> with BPF_REG_LINK_SEXT.
>
> - Narrow r7 with signed 64-bit jumps to [-0x7ffffff0, 0x7ffffff0], so
> r7.r64 is [-0x7ffffff0, 0x7ffffff0] and r7.r32 is the arc
> [0x80000010, 0x7ffffff0].
> - Execute if w6 < 0x7ffffff0 goto out. Because [0x7ffffff0, 0xffffffff]
> and [0x80000010, 0x7ffffff0] overlap in two disjoint arcs,
> cnum32_intersect() over-approximates by keeping the smaller arc
> [0x7ffffff0, 0xffffffff] for r6.r32 and r7.r32, while r7.r64 remains
> [-0x7ffffff0, 0x7ffffff0].
> - Execute if w6 != w8 goto out where w8 has r32 = [0x7ffffff8, 0x80000007].
> - In sync_linked_regs() for r7, reg->r32 is [0x7ffffff8, 0x80000007],
> which straddles S32_MAX / S32_MIN. sext_32_to_64(reg) sets reg->r64 to
> [S32_MIN, S32_MAX].
> - Intersecting reg->r64 with prev.r64 gives [-0x7ffffff0, 0x7ffffff0]
> (non-empty) and intersecting reg->r32 with prev.r32 gives
> [0x7ffffff8, 0x80000007] (non-empty).
> - range_bounds_violation(reg) is false, so *reg = prev is skipped.
> - reg_bounds_sync(reg) calls deduce_bounds_32_from_64(reg), which
> intersects [0x7ffffff8, 0x80000007] with [0x80000010, 0x7ffffff0] and
> leaves reg->r32 = CNUM32_EMPTY and reg->r64 = CNUM64_EMPTY.
>
> 2. Contradiction between var_off and r64/r32 via tnum_intersect():
>
> Suppose r6 is a 64-bit unknown scalar, r7 = (s32)r6, and r7 is narrowed
> to [-4, 3].
>
> - Execute if w6 & 8 goto out. On the fallthrough branch,
> regs_refine_cond_op() calls __mark_reg_unbounded(&r6) and clears bit 3
> in var_off; sync_linked_regs() narrows r7 to [0, 3] with
> var_off = TNUM(0, 3).
> - Execute if w6 & 4 goto target. On the taken branch, r6 gets low-32
> var_off = TNUM(4, 0xfffffff3) and r32 = [-0x7ffffffc, 0x7ffffff7].
> - In sync_linked_regs() on r7, cnum64_intersect_with() and
> cnum32_intersect_with() both produce [0, 3] (non-empty), while
> tnum_intersect() without a tnum_overlap() check combines
> TNUM(4, 0xfffffffffffffff3) and TNUM(0, 3) into TNUM(4, 3) ([4, 7]).
> - range_bounds_violation(reg) is false before reg_bounds_sync(reg), and
> __update_reg_bounds() inside reg_bounds_sync(reg) then intersects
> [0, 3] with [4, 7], leaving r7 with CNUM32_EMPTY and CNUM64_EMPTY.
>
> Should sync_linked_regs() check !tnum_overlap(reg->var_off, prev.var_off)
> and also check range_bounds_violation(reg) after reg_bounds_sync(reg) before
> committing the updated state?
>
>> /*
>> * Intersect rather than overwrite. A BPF_REG_LINK_ZEXT
>> * link over a wide base does not propagate back, so the
> [ ... ]
>
next prev parent reply other threads:[~2026-10-26 20:46 UTC|newest]
Thread overview: 35+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-20 16:18 [PATCH bpf-next v3 00/12] bpf: track scalar equality across the low 32 bits Vineet Gupta
2026-10-20 16:18 ` [PATCH bpf-next v3 01/12] bpf: Keep the 32-bit range across a sign extension that cannot change it Vineet Gupta
2026-10-20 16:18 ` [PATCH bpf-next v3 02/12] selftests/bpf: Cover sign extensions that cannot change the range Vineet Gupta
2026-10-20 16:18 ` [PATCH bpf-next v3 03/12] bpf: Prepare linked scalars for relations narrower than 64 bits Vineet Gupta
2026-10-20 17:08 ` bot+bpf-ci
2026-10-26 22:52 ` Vineet Gupta
2026-10-20 16:18 ` [PATCH bpf-next v3 04/12] bpf: Track low-32 scalar equality across zero-extending movs Vineet Gupta
2026-10-20 16:39 ` sashiko-bot
2026-10-26 23:06 ` Vineet Gupta
2026-10-20 17:09 ` bot+bpf-ci
2026-10-26 20:30 ` Vineet Gupta
2026-10-20 16:19 ` [PATCH bpf-next v3 05/12] selftests/bpf: Cover the low-32 link for " Vineet Gupta
2026-10-20 16:53 ` bot+bpf-ci
2026-10-26 18:36 ` Vineet Gupta
2026-10-20 16:19 ` [PATCH bpf-next v3 06/12] bpf: Track low-32 scalar equality across sign-extending movs Vineet Gupta
2026-10-20 16:57 ` sashiko-bot
2026-10-26 20:46 ` Vineet Gupta [this message]
2026-10-20 16:19 ` [PATCH bpf-next v3 07/12] selftests/bpf: Cover the low-32 link for " Vineet Gupta
2026-10-20 16:19 ` [PATCH bpf-next v3 08/12] bpf: Recover the low 32 bits on the reverse link direction Vineet Gupta
2026-10-20 16:53 ` bot+bpf-ci
2026-10-26 19:57 ` Vineet Gupta
2026-10-20 17:20 ` sashiko-bot
2026-10-26 21:28 ` Vineet Gupta
2026-10-20 16:19 ` [PATCH bpf-next v3 09/12] bpf: Track low-32 scalar equality across narrowing stack fills Vineet Gupta
2026-10-20 16:19 ` [PATCH bpf-next v3 10/12] selftests/bpf: Cover the low-32 link for " Vineet Gupta
2026-10-20 16:29 ` sashiko-bot
2026-10-20 17:15 ` Vineet Gupta
2026-10-20 16:52 ` bot+bpf-ci
2026-10-26 20:21 ` Vineet Gupta
2026-10-20 16:19 ` [PATCH bpf-next v3 11/12] [do not merge] bpf: Track low-32 scalar equality across narrowing stack spills Vineet Gupta
2026-10-20 16:42 ` sashiko-bot
2026-10-26 21:51 ` Vineet Gupta
2026-10-20 16:19 ` [PATCH bpf-next v3 12/12] [do not merge] selftests/bpf: Cover the low-32 link for " Vineet Gupta
2026-10-20 16:32 ` sashiko-bot
2026-10-26 22:01 ` 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=8653c48c-8bdb-40be-adf7-8ea2db8f4298@linux.dev \
--to=vineet.gupta@linux.dev \
--cc=bpf@vger.kernel.org \
--cc=sashiko-reviews@lists.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