From 0c1f5e400af3a929aa9a962427c312cb8f44f024 Mon Sep 17 00:00:00 2001 From: Dimitar Kanaliev Date: Tue, 25 Nov 2025 14:56:32 +0200 Subject: [PATCH 1/3] bpf: Introduce tnum_scast as a tnum native sign extension helper This patch introduces a new helper function - tnum_scast(), which sign-extends a tnum from a smaller integer size to the full 64-bit bpf register range. This is achieved by utilizing the native sign-extension behavior of signed 64-bit integers. By casting the value and mask to s64, shifting left to align the target sign bit with the 64-bit MSB, and then performing an arithmetic right shift, the sign bit is automatically propagated to the upper bits. For the mask, this works because if the sign bit is unknown (1), the arithmetic shift propagates 1s (making upper bits unknonw). If known (0), it propagates 0s (making upper bits known). a) When the sign bit is known: Assume a tnum with value = 0xFF, mask = 0x00, size = 1, which corresponds to an 8-bit subregister of value 0xFF (-1 in 8 bits). s = 64 - 8 = 56 value = ((s64)0xFF << 56) >> 56; // 0xFF...FF (-1) mask = ((s64)0x00 << 56) >> 56; // 0x00...00 Because the sign bit is known to be 1, we sign-extend with 1s. The resulting tnum is (0xFFFFFFFFFFFFFFFF, 0x0000000000000000). b) When the sign bit is unknown: Assume a tnum with value = 0x7F, mask = 0x80, size = 1. s = 56 value = ((s64)0x7F << 56) >> 56; // 0x00...7F mask = ((s64)0x80 << 56) >> 56; // 0xFF...80 The lower 8 bits can be 0x7F or 0xFF. The mask sign bit was 1 (unknown), so the arithmetic shift propagated 1s, making all higher 56 bits unknown. In 64-bit form, this tnum correctly represents the range from 0x000000000000007F (+127) to 0xFFFFFFFFFFFFFFFF (-1). Signed-off-by: Dimitar Kanaliev --- include/linux/tnum.h | 3 +++ kernel/bpf/tnum.c | 13 +++++++++++++ 2 files changed, 16 insertions(+) diff --git a/include/linux/tnum.h b/include/linux/tnum.h index c52b862dad45b..ed18ee1148b67 100644 --- a/include/linux/tnum.h +++ b/include/linux/tnum.h @@ -63,6 +63,9 @@ struct tnum tnum_union(struct tnum t1, struct tnum t2); /* Return @a with all but the lowest @size bytes cleared */ struct tnum tnum_cast(struct tnum a, u8 size); +/* Return @a sign-extended from @size bytes */ +struct tnum tnum_scast(struct tnum a, u8 size); + /* Returns true if @a is a known constant */ static inline bool tnum_is_const(struct tnum a) { diff --git a/kernel/bpf/tnum.c b/kernel/bpf/tnum.c index f8e70e9c3998d..eabcec2ebc26b 100644 --- a/kernel/bpf/tnum.c +++ b/kernel/bpf/tnum.c @@ -199,6 +199,19 @@ struct tnum tnum_cast(struct tnum a, u8 size) return a; } +struct tnum tnum_scast(struct tnum a, u8 size) +{ + u8 s = 64 - size * 8; + u64 value, mask; + + if (size >= 8) + return a; + + value = ((s64)a.value << s) >> s; + mask = ((s64)a.mask << s) >> s; + return TNUM(value, mask); +} + bool tnum_is_aligned(struct tnum a, u64 size) { if (!size) From a137b49d9a0365286b39097359c2de95957c302c Mon Sep 17 00:00:00 2001 From: Dimitar Kanaliev Date: Tue, 25 Nov 2025 14:56:33 +0200 Subject: [PATCH 2/3] bpf: verifier: Simplify register sign extension with tnum_scast This patch refactors the verifier's sign-extension logic for narrow register values to use the new tnum_scast helper. Previously, coerce_reg_to_size_sx and coerce_subreg_to_size_sx employed manual logic to determine bounds, sometimes falling back to loose ranges when sign bits were uncertain. We simplify said logic by delegating the bounds calculation to tnum_scast + the existing bounds synchronization logic: 1. The register's tnum is updated via tnum_scast() 2. The signed bounds (smin/smax) are reset to the maximum theoretical range for the target size. 3. The unsigned bounds are reset to the full register width. 4. __update_reg_bounds() is called. By invoking __update_reg_bounds(), the verifier automatically calculates the intersection between the theoretical signed range and the bitwise info in reg->var_off. This ensures bounds are as tight as possible without requiring custom logic in the coercion functions. This commit also removes set_sext64_default_val() and set_sext32_default_val() as they are no longer used. Signed-off-by: Dimitar Kanaliev --- kernel/bpf/verifier.c | 150 +++++++++--------------------------------- 1 file changed, 30 insertions(+), 120 deletions(-) diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 766695491bc5b..c9a6bf85b4add 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c @@ -6876,147 +6876,57 @@ static void coerce_reg_to_size(struct bpf_reg_state *reg, int size) reg_bounds_sync(reg); } -static void set_sext64_default_val(struct bpf_reg_state *reg, int size) -{ - if (size == 1) { - reg->smin_value = reg->s32_min_value = S8_MIN; - reg->smax_value = reg->s32_max_value = S8_MAX; - } else if (size == 2) { - reg->smin_value = reg->s32_min_value = S16_MIN; - reg->smax_value = reg->s32_max_value = S16_MAX; - } else { - /* size == 4 */ - reg->smin_value = reg->s32_min_value = S32_MIN; - reg->smax_value = reg->s32_max_value = S32_MAX; - } - reg->umin_value = reg->u32_min_value = 0; - reg->umax_value = U64_MAX; - reg->u32_max_value = U32_MAX; - reg->var_off = tnum_unknown; -} - static void coerce_reg_to_size_sx(struct bpf_reg_state *reg, int size) { - s64 init_s64_max, init_s64_min, s64_max, s64_min, u64_cval; - u64 top_smax_value, top_smin_value; - u64 num_bits = size * 8; + s64 smin_value, smax_value; - if (tnum_is_const(reg->var_off)) { - u64_cval = reg->var_off.value; - if (size == 1) - reg->var_off = tnum_const((s8)u64_cval); - else if (size == 2) - reg->var_off = tnum_const((s16)u64_cval); - else - /* size == 4 */ - reg->var_off = tnum_const((s32)u64_cval); - - u64_cval = reg->var_off.value; - reg->smax_value = reg->smin_value = u64_cval; - reg->umax_value = reg->umin_value = u64_cval; - reg->s32_max_value = reg->s32_min_value = u64_cval; - reg->u32_max_value = reg->u32_min_value = u64_cval; + if (size >= 8) return; - } - top_smax_value = ((u64)reg->smax_value >> num_bits) << num_bits; - top_smin_value = ((u64)reg->smin_value >> num_bits) << num_bits; + reg->var_off = tnum_scast(reg->var_off, size); - if (top_smax_value != top_smin_value) - goto out; + smin_value = -(1LL << (size * 8 - 1)); + smax_value = (1LL << (size * 8 - 1)) - 1; - /* find the s64_min and s64_min after sign extension */ - if (size == 1) { - init_s64_max = (s8)reg->smax_value; - init_s64_min = (s8)reg->smin_value; - } else if (size == 2) { - init_s64_max = (s16)reg->smax_value; - init_s64_min = (s16)reg->smin_value; - } else { - init_s64_max = (s32)reg->smax_value; - init_s64_min = (s32)reg->smin_value; - } - - s64_max = max(init_s64_max, init_s64_min); - s64_min = min(init_s64_max, init_s64_min); + reg->smin_value = smin_value; + reg->smax_value = smax_value; - /* both of s64_max/s64_min positive or negative */ - if ((s64_max >= 0) == (s64_min >= 0)) { - reg->s32_min_value = reg->smin_value = s64_min; - reg->s32_max_value = reg->smax_value = s64_max; - reg->u32_min_value = reg->umin_value = s64_min; - reg->u32_max_value = reg->umax_value = s64_max; - reg->var_off = tnum_range(s64_min, s64_max); - return; - } + reg->s32_min_value = (s32)smin_value; + reg->s32_max_value = (s32)smax_value; -out: - set_sext64_default_val(reg, size); -} - -static void set_sext32_default_val(struct bpf_reg_state *reg, int size) -{ - if (size == 1) { - reg->s32_min_value = S8_MIN; - reg->s32_max_value = S8_MAX; - } else { - /* size == 2 */ - reg->s32_min_value = S16_MIN; - reg->s32_max_value = S16_MAX; - } + reg->umin_value = 0; + reg->umax_value = U64_MAX; reg->u32_min_value = 0; reg->u32_max_value = U32_MAX; - reg->var_off = tnum_subreg(tnum_unknown); + + __update_reg_bounds(reg); } static void coerce_subreg_to_size_sx(struct bpf_reg_state *reg, int size) { - s32 init_s32_max, init_s32_min, s32_max, s32_min, u32_val; - u32 top_smax_value, top_smin_value; - u32 num_bits = size * 8; - - if (tnum_is_const(reg->var_off)) { - u32_val = reg->var_off.value; - if (size == 1) - reg->var_off = tnum_const((s8)u32_val); - else - reg->var_off = tnum_const((s16)u32_val); + s32 smin_value, smax_value; - u32_val = reg->var_off.value; - reg->s32_min_value = reg->s32_max_value = u32_val; - reg->u32_min_value = reg->u32_max_value = u32_val; + if (size >= 4) return; - } - top_smax_value = ((u32)reg->s32_max_value >> num_bits) << num_bits; - top_smin_value = ((u32)reg->s32_min_value >> num_bits) << num_bits; + reg->var_off = tnum_subreg(tnum_scast(reg->var_off, size)); - if (top_smax_value != top_smin_value) - goto out; + smin_value = -(1 << (size * 8 - 1)); + smax_value = (1 << (size * 8 - 1)) - 1; - /* find the s32_min and s32_min after sign extension */ - if (size == 1) { - init_s32_max = (s8)reg->s32_max_value; - init_s32_min = (s8)reg->s32_min_value; - } else { - /* size == 2 */ - init_s32_max = (s16)reg->s32_max_value; - init_s32_min = (s16)reg->s32_min_value; - } - s32_max = max(init_s32_max, init_s32_min); - s32_min = min(init_s32_max, init_s32_min); - - if ((s32_min >= 0) == (s32_max >= 0)) { - reg->s32_min_value = s32_min; - reg->s32_max_value = s32_max; - reg->u32_min_value = (u32)s32_min; - reg->u32_max_value = (u32)s32_max; - reg->var_off = tnum_subreg(tnum_range(s32_min, s32_max)); - return; - } + reg->s32_min_value = smin_value; + reg->s32_max_value = smax_value; -out: - set_sext32_default_val(reg, size); + reg->u32_min_value = 0; + reg->u32_max_value = U32_MAX; + + __update_reg32_bounds(reg); + + reg->umin_value = reg->u32_min_value; + reg->umax_value = reg->u32_max_value; + + reg->smin_value = reg->umin_value; + reg->smax_value = reg->umax_value; } static bool bpf_map_is_rdonly(const struct bpf_map *map) From 87eb5bc8c1afa7e49814a8b316e789c3bd6dff0c Mon Sep 17 00:00:00 2001 From: Dimitar Kanaliev Date: Tue, 25 Nov 2025 14:56:34 +0200 Subject: [PATCH 3/3] selftests/bpf: Add verifier bounds checks for sign extension This patch adds a new test cases to validate the improved register bounds tracking logic. We perform the sequence: call bpf_get_prandom_u32; r1 &= 0x100; r1 = (s8)r1; After the bitwise AND, `r1` is either 0 or 256 (0x100). If 0: The lower 8 bits are 0. If 256: The bit at index 8 is set, but the lower 8 bits are 0. Since the cast to s8 only considers bits 0-7, the set bit at index 8 is truncated. In both cases, the sign bit (bit 7) is 0, so the result is exactly 0. With the coercion logic before this series: 1: (bf) r1 = r0 ; R0=scalar(id=1) R1=scalar(id=1) 2: (57) r1 &= 256 ; R1=scalar(...,var_off=(0x0; 0x100)) 3: (bf) r1 = (s8)r1 ; R1=scalar(smin=smin32=-128,smax=smax32=127) With our changes: 1: (bf) r1 = r0 ; R0=scalar(id=1) R1=scalar(id=1) 2: (57) r1 &= 256 ; R1=scalar(...,var_off=(0x0; 0x100)) 3: (bf) r1 = (s8)r1 ; R1=0 Signed-off-by: Dimitar Kanaliev --- .../selftests/bpf/progs/verifier_movsx.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tools/testing/selftests/bpf/progs/verifier_movsx.c b/tools/testing/selftests/bpf/progs/verifier_movsx.c index a4d8814eb5edc..df7ad41af172e 100644 --- a/tools/testing/selftests/bpf/progs/verifier_movsx.c +++ b/tools/testing/selftests/bpf/progs/verifier_movsx.c @@ -339,6 +339,25 @@ label_%=: \ : __clobber_all); } +SEC("socket") +__description("MOV64SX, S8, upper bits truncation") +__log_level(2) +__msg("R1={{P?}}0") +__success __success_unpriv __retval(0) +__naked void mov64sx_s8_truncated_range(void) +{ + asm volatile (" \ + call %[bpf_get_prandom_u32]; \ + r1 = r0; \ + r1 &= 0x100; \ + r1 = (s8)r1; \ + r0 = 0; \ + exit; \ +" : + : __imm(bpf_get_prandom_u32) + : __clobber_all); +} + #else SEC("socket")