Add subreg_eq_zext_mov_narrow: a wide-source "w7 = w6" followed by "if w6 == 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=" 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 all
narrow the base and check the linked register.
zext_dest_driven_does_not_narrow_base narrows the LINKED register instead and
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 <[email protected]>
---
 .../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/testing/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 u32 
bounds")
 __success __log_level(2)
-__msg("3: (1c) w3 -= w2 {{.*}} 
R3=scalar(smin=0,smax=umax=0xffffffff,var_off=(0x0; 0xffffffff))")
+/*
+ * w3 = 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 deterministic
+ * (raw asm, same bytecode in every flavour) so require it rather than making
+ * 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 -= w2 {{.*}} 
R3=scalar(id={{[0-9]+([+-][0-9]+)?}},smin=0,smax=umax=0xffffffff,var_off=(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_%=:                                                
        \
        : __clobber_all);
 }
 
+/*
+ * A 32-bit zero-extending mov (w7 = w6) from a source with unknown high bits
+ * shares only the low 32 bits (w7.lo == w6.lo, w7.hi == 0). A later 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 emits 
when it
+ * reuses "w0 = idx" for "return 0" on the idx==0 path of a callback.
+ */
+SEC("socket")
+__success
+__naked void subreg_eq_zext_mov_narrow(void)
+{
+       asm volatile ("                                         \
+       call %[bpf_get_prandom_u32];                            \
+       r6 = r0;                /* r6 = 64-bit unknown (helper ret is 
unbounded) */ \
+       call %[bpf_get_prandom_u32];                            \
+       r0 <<= 32;              /* r0 = unknown high bits */    \
+       r6 |= r0;               /* still 64-bit unknown; makes it explicit */ \
+       w7 = w6;                /* 32-bit zero-extend mov, wide src */  \
+       if w6 != 0 goto l_out_%=;       /* w6 low == 0 on fall-through */ \
+       /* w7 = zext32(w6 low) must be 0 here */                \
+       if w7 == 0 goto l_out_%=;       /* provably 0 iff linked */     \
+       r0 /= 0;                /* reached only if w7 not deduced 0 */  \
+l_out_%=:                                                      \
+       r0 = 0;                                                 \
+       exit;                                                   \
+"      :
+       : __imm(bpf_get_prandom_u32)
+       : __clobber_all);
+}
+
+/*
+ * A 32-bit zero-extending mov (w7 = w5) whose SOURCE is a wide 
ADD_CONST-linked
+ * register (r5 = base + K) must NOT disturb that source. Forming the low-32
+ * BPF_FLAG_SUBREG_ZEXT link on the destination would need 
assign_scalar_id_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). So 
for a
+ * wide ADD_CONST src the mov leaves the source's link intact and just clears 
the
+ * destination.
+ *
+ * Here r5 = r6 + 3 (ADD_CONST, wide). After the mov, narrowing the base r6 
must
+ * still reach r5 through the preserved link: r6 in [0, 10] => 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 = r0;                /* r6 low = unknown u32 */      \
+       call %[bpf_get_prandom_u32];                            \
+       r0 <<= 32;                                              \
+       r6 |= r0;               /* r6 = full 64-bit unknown (base) */ \
+       r5 = r6;                /* r5, r6 linked (shared id) */ \
+       r5 += 3;                /* r5 = base + 3: ADD_CONST, still wide */ \
+       w7 = w5;                /* 32-bit zext mov, wide ADD_CONST src */ \
+       if r6 > 10 goto l_out_%=;/* r6 in [0, 10] */            \
+       /* r5 = r6 + 3 must be in [3, 13] here (needs the kept link) */ \
+       if r5 > 13 goto l_err_%=;/* taken only if r5 not narrowed */ \
+       goto l_out_%=;                                          \
+l_err_%=:                                                      \
+       r0 /= 0;                /* reachable iff r5's link was cleared */ \
+l_out_%=:                                                      \
+       r0 = 0;                                                 \
+       exit;                                                   \
+"      :
+       : __imm(bpf_get_prandom_u32)
+       : __clobber_all);
+}
+
+/*
+ * Dest-driven direction, zero-extend flavour: narrowing the LINKED register
+ * must not narrow the wide base.
+ *
+ * w7 = w6 shares only r6's low 32 bits; r7's high half is zero, r6's is
+ * unknown. Learning r7 == 0 therefore says nothing about r6, and
+ * sync_linked_regs() must not copy r7's state onto it. Rejected iff the base
+ * 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-bit
+ * 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 = r0;                /* r6 low = unknown u32 */      \
+       call %[bpf_get_prandom_u32];                            \
+       r0 <<= 32;                                              \
+       r6 |= r0;               /* r6 = full 64-bit unknown (base) */ \
+       w7 = w6;                /* low-32 ZEXT link */          \
+       if r7 != 0 goto l_out_%=;/* r7 == 0: low 32 bits are 0 */ \
+       if r6 != 0 goto l_out_%=;/* r6 may still have high bits set */ \
+       r0 /= 0;                /* must stay reachable */       \
+l_out_%=:                                                      \
+       r0 = 0;                                                 \
+       exit;                                                   \
+"      :
+       : __imm(bpf_get_prandom_u32)
+       : __clobber_all);
+}
+
 char _license[] SEC("license") = "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_%=:      exit;                                           
\
 }
 
 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 not 0")
+__success
 __naked void subreg_equality_2(void)
 {
        asm volatile ("                                 \
        call %[bpf_ktime_get_ns];                       \
        r2 = r0;                                        \
-       /* Upper 4-bytes of r2 may not be 0, thus insn  \
-        * w3 = 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 = 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 = w2;                                        \
        if w2 < 9 goto l0_%=;                           \
        exit;                                           \
 l0_%=: if r3 < 9 goto l1_%=;                           \
-       /* r1 read is illegal at this point */          \
+       /* unreachable: r3 is provably < 9 */           \
        r0 -= r1;                                       \
 l1_%=: exit;                                           \
 "      :
-- 
2.53.0-Meta


Reply via email to