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: [PATCH bpf-next v2 00/13] bpf: track scalar equality across the low 32 bits
Date: Thu, 10 Sep 2026 22:16:22 +0530 [thread overview]
Message-ID: <20260910164635.459558-1-vineet.gupta@linux.dev> (raw)
The verifier's linked-scalar machinery tracks register equality only for
the full 64 bits, optionally with a constant delta. There is no way to
record "these two registers share just their low 32 bits", so a 32-bit
operation on a source with unknown high bits has to drop the relationship,
and a later narrowing of the source never reaches the destination:
The verifier's linked-scalar machinery tracks register equality only for
the full 64 bits, optionally with a constant delta. There is no way to
record "these two registers share just their low 32 bits", so a 32-bit
operation on a source with unknown high bits has to drop the relationship,
and a later narrowing of the source never reaches the destination:
r6 = ... /* full 64-bit unknown */
w7 = w6 /* 32-bit zero-extending mov */
if w6 != 0 goto ... /* not taken: r6's low 32 bits are 0 */
if w7 == 0 goto ... /* not deduced today */
The same gap exists for the 32-bit sign extension, which is what prompted
this, and -- as pointed out on the RFC -- for loads, which can zero- or
sign-extend the same way. This series covers all four, plus the spill that
feeds a narrowing fill.
Record a low-32 link instead of dropping the relation. On a later
narrowing, sync_linked_regs() rebuilds such a register from the base by
re-applying the extension the original operation used, rather than copying
it. The reverse direction is skipped: a low-32 register knows nothing about
a full register's high half.
There is an upstream test that documents the missing capability directly.
verifier_reg_equal's "w reg not equal if r reg upper32 bits not 0" asserts
that w3 = w2 does not let a later w2 < 9 bound r3, and that the program is
therefore rejected. That bound is sound, so the test becomes __success
here.
Layout
======
The kinds live in bitfields rather than in the top bits of ->id, as
suggested on 1/6:
enum bpf_add_const add_const:2;
enum bpf_subreg subreg:2;
bpf_reg_state stays 80 bytes; 20 bits of padding remain.
Patches
=======
1 move the kinds out of ->id [NFC]
2 restore the kind comparison in regs_exact() that 1 drops
3-4 zero-extending mov
5-6 keep the range across a sign extension that cannot change it
7-8 sign-extending mov
9-10 narrowing fill
11 record what a narrowing spill actually stores
12-13 narrowing spill
5 and 11 are precision fixes that stand on their own; they are ordered
ahead of the linking patches that build on them.
Testing
=======
clang and bpf-gcc 20260721, x86_64:
clang 129/2594 PASSED, 6 SKIPPED, 0/0 FAILED (-t verifier)
bpf-gcc no new failures against the base
A full test_progs run on both the base and the series gives the same 28
distinct failures, with identical per-test failure counts.
Every patch builds individually.
Changes since the RFC [1]
=========================
6 patches became 13. In aggregate:
- The kinds are bitfields rather than a byte of flags, so RFC 1/6 is gone
and RFC 2/6 shrank to patch 1.
- Two precision fixes were split out and ordered ahead of the linking
patches that build on them: coerce_reg_to_size_sx() no longer widens a
range it cannot change (5), and a narrowing spill records what it stored
rather than the wider source (11). Both stand alone.
- Loads are covered, as asked for on the cover letter: the narrowing fill
(9-10) and the narrowing spill that feeds it (12-13).
- regs_exact() regained a comparison the RFC lost when the kind left ->id
(2).
Four of those differ from what was reviewed and are worth describing in
some detail:
- The wide-source mov no longer excludes an ADD_CONST source. This was
suggested on 3/6 and it is the right call, but it has a cost:
forming the link clears the source's base+delta relationship, so
tracking that used to survive is lost. The two cannot both be kept -- a
link the sync path skips would be inert -- and the delta shape is common
in the codegen this targets. Patch 3 spells this out, and
zext_mov_breaks_add_const_src pins it.
- reconstruct_sext32() does NOT reuse coerce_reg_to_size_sx(), despite that
being the explicit ask on 5/6. The RFC's call-then-overwrite is gone --
coerce_reg_to_size_sx() is fixed in patch 5 and the mov site now has a
single sign-extension path. But the sync path cannot use it: it reads
smin/smax, which straddle after a 32-bit compare and collapse to the full
field range, so the link propagates nothing. It needs the base's 32-bit
range instead, exactly as zext_32_to_64() does for the zero-extending
side. Three tests failed on this before it was understood.
- The sign-extending self-mov is narrower than in the RFC. There,
r0 = (s32)r0 was never excluded, because reconstruct_sext32() ran at the
mov site to keep the range. Patch 5 now does that job, so the link is
formed only when r0 already carries an id -- otherwise there is nothing
to link to and minting one would leave the register describing itself.
- Patch 11 tightens the state recorded for every narrowing spill, including
programs with no links at all. It is separated out for that reason.
[1] 20260814231945.3884596-1-vineet.gupta@linux.dev
Two notes on the tests:
- The two sign-extension range tests live in verifier_movsx.c rather than
verifier_linked_scalars.c, contrary to the request on 4/6. They exercise
the range a sign-extending mov produces, not the ->id machinery, and that
file already gates on the cpuv4 support they need.
- Three tests that were named *_link_mismatch_blocks_pruning are now named
for what they actually check. Disabling the ->subreg comparison in
regsafe() does not make them fail: the states they compare differ in ids,
contents or ranges, so regsafe() keeps them apart for reasons of its own.
They verify an outcome, not that specific guard.
Still open
==========
- The kind comparison in regsafe() has no test that isolates it, like the
pre-existing add_const comparison. The shape it guards is old being a
superset of cur -- old SUBREG_ZEXT over [0, U32_MAX] against cur
SUBREG_SEXT over [0, 100] passes range_within(), and without the
comparison a later sync_linked_regs() would rebuild cur by the wrong
rule. Whenever the kinds differ in a way a program can construct directly
the ranges differ too, so the range checks reject first. Suggestions
welcome.
- The fill arm mints an id when spill_size == 4, but the clearing test does
not mention spill_size. A narrow value spilled at 8 bytes and filled at 4
therefore inherits a full 64-bit link without the minting gate running.
This predates the series; patch 9 builds on that arm without changing it.
Is it deliberate?
- regs_exact() loses the kind comparison for exactly one patch, between 1
and 2. They can be folded if the [NFC] label matters more than the bisect
step.
Vineet Gupta (13):
bpf: move linked-scalar flags out of bpf_reg_state->id [NFC]
bpf: compare linked-scalar kinds in regs_exact()
bpf: track low-32 scalar equality across zero-extending movs
selftests/bpf: cover the low-32 link for zero-extending movs
bpf: keep the range across a sign extension that cannot change it
selftests/bpf: cover sign extensions that cannot change the range
bpf: track low-32 scalar equality across sign-extending movs
selftests/bpf: cover the low-32 link for sign-extending movs
bpf: track low-32 scalar equality across narrowing stack fills
selftests/bpf: cover the low-32 link for narrowing stack fills
bpf: record what a narrowing spill actually stores
bpf: track low-32 scalar equality across narrowing stack spills
selftests/bpf: cover the low-32 link for narrowing stack spills
include/linux/bpf_verifier.h | 36 +-
include/linux/tnum.h | 3 +
kernel/bpf/log.c | 8 +-
kernel/bpf/states.c | 38 +-
kernel/bpf/tnum.c | 15 +
kernel/bpf/verifier.c | 282 ++++++--
tools/testing/selftests/bpf/progs/bpf_misc.h | 8 +
.../selftests/bpf/progs/verifier_bounds.c | 2 +-
.../bpf/progs/verifier_linked_scalars.c | 653 +++++++++++++++++-
.../selftests/bpf/progs/verifier_movsx.c | 51 ++
.../selftests/bpf/progs/verifier_reg_equal.c | 14 +-
.../selftests/bpf/progs/verifier_spill_fill.c | 10 +-
12 files changed, 995 insertions(+), 125 deletions(-)
--
2.53.0-Meta
next reply other threads:[~2026-09-10 16:46 UTC|newest]
Thread overview: 39+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-10 16:46 Vineet Gupta [this message]
2026-09-10 16:46 ` [PATCH bpf-next v2 01/13] bpf: move linked-scalar flags out of bpf_reg_state->id [NFC] Vineet Gupta
2026-09-10 17:00 ` sashiko-bot
2026-09-11 6:56 ` Vineet Gupta
2026-09-10 17:52 ` bot+bpf-ci
2026-09-12 18:50 ` Alexei Starovoitov
2026-09-10 16:46 ` [PATCH bpf-next v2 02/13] bpf: compare linked-scalar kinds in regs_exact() Vineet Gupta
2026-09-12 18:51 ` Alexei Starovoitov
2026-09-10 16:46 ` [PATCH bpf-next v2 03/13] bpf: track low-32 scalar equality across zero-extending movs Vineet Gupta
2026-09-10 17:52 ` bot+bpf-ci
2026-09-11 9:29 ` Vineet Gupta
2026-09-12 18:59 ` Alexei Starovoitov
2026-09-10 16:46 ` [PATCH bpf-next v2 04/13] selftests/bpf: cover the low-32 link for " Vineet Gupta
2026-09-10 16:46 ` [PATCH bpf-next v2 05/13] bpf: keep the range across a sign extension that cannot change it Vineet Gupta
2026-09-10 17:08 ` sashiko-bot
2026-09-10 17:52 ` bot+bpf-ci
2026-09-11 10:37 ` Vineet Gupta
2026-09-12 19:02 ` Alexei Starovoitov
2026-09-10 16:46 ` [PATCH bpf-next v2 06/13] selftests/bpf: cover sign extensions that cannot change the range Vineet Gupta
2026-09-10 16:46 ` [PATCH bpf-next v2 07/13] bpf: track low-32 scalar equality across sign-extending movs Vineet Gupta
2026-09-10 17:52 ` bot+bpf-ci
2026-09-11 10:00 ` Vineet Gupta
2026-09-12 19:09 ` Alexei Starovoitov
2026-09-10 16:46 ` [PATCH bpf-next v2 08/13] selftests/bpf: cover the low-32 link for " Vineet Gupta
2026-09-10 17:52 ` bot+bpf-ci
2026-09-11 8:00 ` Vineet Gupta
2026-09-10 16:46 ` [PATCH bpf-next v2 09/13] bpf: track low-32 scalar equality across narrowing stack fills Vineet Gupta
2026-09-10 17:04 ` sashiko-bot
2026-09-11 6:07 ` Vineet Gupta
2026-09-11 6:43 ` Vineet Gupta
2026-09-10 16:46 ` [PATCH bpf-next v2 10/13] selftests/bpf: cover the low-32 link for " Vineet Gupta
2026-09-10 17:00 ` sashiko-bot
2026-09-11 5:34 ` Vineet Gupta
2026-09-10 17:31 ` bot+bpf-ci
2026-09-11 5:07 ` Vineet Gupta
2026-09-10 16:46 ` [PATCH bpf-next v2 11/13] bpf: record what a narrowing spill actually stores Vineet Gupta
2026-09-10 16:46 ` [PATCH bpf-next v2 12/13] bpf: track low-32 scalar equality across narrowing stack spills Vineet Gupta
2026-09-10 17:05 ` sashiko-bot
2026-09-10 16:46 ` [PATCH bpf-next v2 13/13] selftests/bpf: cover the low-32 link for " 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=20260910164635.459558-1-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.