Re: [PATCH bpf-next v2 10/13] selftests/bpf: cover the low-32 link for narrowing stack fills
From: bot+bpf-ci
Date: Thu Sep 10 2026 - 14:01:22 EST
> diff --git a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c
> index 9d060d8b0c1f..e8a44e7579c8 100644
> --- a/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c
> +++ b/tools/testing/selftests/bpf/progs/verifier_linked_scalars.c
[ ... ]
> @@ -966,6 +966,88 @@ __naked void zext_mov_breaks_add_const_src(void)
> : __clobber_all);
> }
>
> +/*
> + * A narrowing fill keeps only the slot's low 32 bits, so a later narrowing of
> + * the spilled value must still reach the filled register. Same relation as a
> + * 32-bit mov from a wide source, with the stack slot as the base.
> + */
> +SEC("socket")
> +__success
> +__naked void zext_fill_narrow_from_wide_spill(void)
> +{
> + asm volatile (" \
> + call %[bpf_get_prandom_u32]; \
> + r6 = r0; \
> + call %[bpf_get_prandom_u32]; \
> + r0 <<= 32; \
> + r6 |= r0; /* r6 = full 64-bit unknown */ \
> + *(u64 *)(r10 - 8) = r6; /* slot linked to r6 */ \
> + r2 = *(u32 *)(r10 - 8); /* narrowing fill, forms the link */ \
> + if w6 != 0 goto 1f; /* narrows r6, propagates to r2 */ \
> + if r2 == 0 goto 1f; \
> + r0 /= 0; \
> +1: \
> + r0 = 0; \
> + exit; \
> +" :
> + : __imm(bpf_get_prandom_u32)
> + : __clobber_all);
> +}
> +
> +/*
> + * A full-width fill of a wide slot is a plain 64-bit equality, not a low-32
> + * link, so it must keep propagating exactly as before.
> + */
> +SEC("socket")
> +__success
> +__naked void zext_fill_full_width_keeps_full_link(void)
> +{
> + asm volatile (" \
> + call %[bpf_get_prandom_u32]; \
> + r6 = r0; \
> + call %[bpf_get_prandom_u32]; \
> + r0 <<= 32; \
> + r6 |= r0; \
> + *(u64 *)(r10 - 8) = r6; \
> + r2 = *(u64 *)(r10 - 8); /* no narrowing */ \
> + if r6 != 0 goto 1f; \
> + if r2 == 0 goto 1f; \
> + r0 /= 0; \
> +1: \
> + r0 = 0; \
> + exit; \
> +" :
> + : __imm(bpf_get_prandom_u32)
> + : __clobber_all);
> +}
> +
> +/*
> + * A sub-word fill is below the low-32 model, so no link is formed and the
> + * relation is dropped as before.
> + */
> +SEC("socket")
> +__failure __msg("div by zero")
> +__naked void zext_fill_byte_forms_no_link(void)
> +{
> + asm volatile (" \
> + call %[bpf_get_prandom_u32]; \
> + r6 = r0; \
> + call %[bpf_get_prandom_u32]; \
> + r0 <<= 32; \
> + r6 |= r0; \
> + *(u64 *)(r10 - 8) = r6; \
> + r2 = *(u8 *)(r10 - 8); /* 1-byte fill: no link */ \
> + if w6 != 0 goto 1f; \
> + if r2 == 0 goto 1f; /* not deduced */ \
> + r0 /= 0; \
> +1: \
> + r0 = 0; \
> + exit; \
> +" :
> + : __imm(bpf_get_prandom_u32)
> + : __clobber_all);
> +}
> +
> #ifdef CAN_USE_MOVSX
Should the guard here be checking for LDSX support instead? The two
programs added below (sext_fill_narrow_from_wide_spill and
fill_kinds_reach_different_values) both use BPF_LDX | BPF_MEMSX
instructions (`r2 = *(s32 *)(r10 - 8)`), not MOVSX.
Other LDSX users in the suite gate on the LDSX feature - for instance,
verifier_ldsx.c checks:
(__clang_major__ >= 18 || defined(__BPF_FEATURE_LDSX))
The two macros happen to align for clang >= 18 and gcc-bpf -mcpu=v4, but
a compiler advertising MOVSX without LDSX would fail to build these
programs rather than skipping them.
[ ... ]
> @@ -1129,6 +1211,68 @@ __naked void sext_kinds_reach_different_values(void)
> : __clobber_all);
> }
>
> +/*
> + * The sign-extending counterpart: the filled register is the sign extension of
> + * the slot's low 32 bits, so a narrowing of those bits arrives sign-extended.
> + */
> +SEC("socket")
> +__success
> +__naked void sext_fill_narrow_from_wide_spill(void)
> +{
> + asm volatile (" \
> + call %[bpf_get_prandom_u32]; \
> + r6 = r0; \
> + call %[bpf_get_prandom_u32]; \
> + r0 <<= 32; \
> + r6 |= r0; /* r6 = full 64-bit unknown */ \
> + *(u64 *)(r10 - 8) = r6; /* slot linked to r6 */ \
> + r2 = *(s32 *)(r10 - 8); /* narrowing sx fill */ \
> + if w6 != -1 goto 1f; /* narrows r6, propagates to r2 */ \
> + if r2 == -1 goto 1f; /* sign-extended, not 0xffffffff */ \
> + r0 /= 0; \
> +1: \
> + r0 = 0; \
> + exit; \
> +" :
> + : __imm(bpf_get_prandom_u32)
> + : __clobber_all);
> +}
> +
> +/*
> + * The same, for the two fill kinds off one slot.
> + */
> +SEC("socket")
> +__failure __msg("div by zero")
> +__flag(BPF_F_TEST_STATE_FREQ)
> +__naked void fill_kinds_reach_different_values(void)
> +{
> + asm volatile (" \
> + call %[bpf_get_prandom_u32]; \
> + r6 = r0; \
> + r6 &= 1; \
> + call %[bpf_get_prandom_u32]; \
> + r8 = r0; \
> + call %[bpf_get_prandom_u32]; \
> + r0 <<= 32; \
> + r8 |= r0; \
> + *(u64 *)(r10 - 8) = r8; \
> + if r6 >= 1 goto 2f; \
> + r2 = *(s32 *)(r10 - 8); /* sign-extending fill */ \
> + goto 1f; \
> +2: \
> + r2 = *(u32 *)(r10 - 8); /* zero-extending fill */ \
> +1: \
> + if w8 != -1 goto 3f; \
> + if r2 == -1 goto 3f; /* only the sign-extending path */ \
> + r0 /= 0; \
> +3: \
> + r0 = 0; \
> + exit; \
> +" :
> + : __imm(bpf_get_prandom_u32)
> + : __clobber_all);
> +}
> +
> #endif /* CAN_USE_MOVSX */
>
> char _license[] SEC("license") = "GPL";
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/34506184282