bpf: Prevent invalid u32 bounds in __reg32_deduce_bounds()
PATCH 1/1 bpf: Prevent invalid u32 bounds in __reg32_deduce_bounds()
When refining register bounds, __reg32_deduce_bounds can derive u32 min/max from 64-bit or s32 bounds and assign them with max_t/min_t.
If the existing u32 range and the derived range do not overlap (e.g., u64 says [0, 1] while u32 was [2, 2] from an earlier path), the intersection is empty and the new u32_min_value can end up greater than u32_max_value, triggering the following warning:
verifier bug: REG INVARIANTS VIOLATION (false_reg1): range bounds violation
u64=[0x0, 0x1] s64=[0x0, 0x1] u32=[0x3, 0x1] s32=[0x0, 0x1] var_off=(0x0, 0x1)
WARNING: kernel/bpf/verifier.c:2742 at reg_bounds_sanity_check
Call Trace:
reg_bounds_sanity_check+0xbc/0x1e0
reg_set_min_max+0x1a2/0x1f0
check_cond_jmp_op+0x5d2/0x1980
do_check_common+0x2b0f/0x3410
do_check_subprogs+0xcd/0x180
bpf_check+0x33fe/0x3850
bpf_prog_load+0x7d7/0xee0
__sys_bpf+0xea2/0x2e30This was triggered by the scx CI while loading the scx_layered sched_ext scheduler [1].
Fix by only applying the derived u32 bounds when the resulting range is valid (u32_min <= u32_max).
[1] https://github.com/sched-ext/scx/pull/3349
--- kernel/bpf/verifier.c | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-)
kernel/bpf/verifier.c +21 -6
@@ -2424,8 +2424,13 @@ static void __reg32_deduce_bounds(struct bpf_reg_state *reg)/* u64 to u32 casting preserves validity of low 32 bits as * a range, if upper 32 bits are the same */ - reg->u32_min_value =max_t (u32, reg->u32_min_value, (u32 )reg->umin_value); - reg->u32_max_value =min_t (u32, reg->u32_max_value, (u32 )reg->umax_value); +u32 u32_min =max_t (u32, reg->u32_min_value, (u32 )reg->umin_value); +u32 u32_max =min_t (u32, reg->u32_max_value, (u32 )reg->umax_value); + +if (u32_min <= u32_max) { + reg->u32_min_value = u32_min; + reg->u32_max_value = u32_max; + }if ((s32 )reg->umin_value <= (s32 )reg->umax_value) { reg->s32_min_value =max_t (s32, reg->s32_min_value, (s32 )reg->umin_value);
@@ -2435,8 +2440,13 @@ static void __reg32_deduce_bounds(struct bpf_reg_state *reg)if ((reg->smin_value >>32 ) == (reg->smax_value >>32 )) {/* low 32 bits should form a proper u32 range */ if ((u32 )reg->smin_value <= (u32 )reg->smax_value) { - reg->u32_min_value =max_t (u32, reg->u32_min_value, (u32 )reg->smin_value); - reg->u32_max_value =min_t (u32, reg->u32_max_value, (u32 )reg->smax_value); +u32 u32_min =max_t (u32, reg->u32_min_value, (u32 )reg->smin_value); +u32 u32_max =min_t (u32, reg->u32_max_value, (u32 )reg->smax_value); + +if (u32_min <= u32_max) { + reg->u32_min_value = u32_min; + reg->u32_max_value = u32_max; + } }/* low 32 bits should form a proper s32 range */ if ((s32 )reg->smin_value <= (s32 )reg->smax_value) {
@@ -2479,8 +2489,13 @@ static void __reg32_deduce_bounds(struct bpf_reg_state *reg)* -3 s<= x s<= -1 implies 0xf...fd u<= x u<= 0xf...ff. */ if ((u32 )reg->s32_min_value <= (u32 )reg->s32_max_value) { - reg->u32_min_value =max_t (u32, reg->s32_min_value, reg->u32_min_value); - reg->u32_max_value =min_t (u32, reg->s32_max_value, reg->u32_max_value); +u32 u32_min =max_t (u32, reg->s32_min_value, reg->u32_min_value); +u32 u32_max =min_t (u32, reg->s32_max_value, reg->u32_max_value); + +if (u32_min <= u32_max) { + reg->u32_min_value = u32_min; + reg->u32_max_value = u32_max; + } } }
Signature · 1 line
2.53.0
Eduard Zingerman
maintainerQuoted 24 lines · Andrea Righi
When refining register bounds,
__reg32_deduce_boundscan derive u32 min/max from 64-bit or s32 bounds and assign them with max_t/min_t.If the existing u32 range and the derived range do not overlap (e.g., u64 says [0, 1] while u32 was [2, 2] from an earlier path), the intersection is empty and the new
u32_min_valuecan end up greater thanu32_max_value, triggering the following warning:verifier bug: REG INVARIANTS VIOLATION (
false_reg1): range bounds violationu64=[0x0, 0x1] s64=[0x0, 0x1] u32=[0x3, 0x1] s32=[0x0, 0x1] var_off=(0x0, 0x1) WARNING: kernel/bpf/verifier.c:2742 at reg_bounds_sanity_check Call Trace: reg_bounds_sanity_check+0xbc/0x1e0 reg_set_min_max+0x1a2/0x1f0 check_cond_jmp_op+0x5d2/0x1980 do_check_common+0x2b0f/0x3410 do_check_subprogs+0xcd/0x180 bpf_check+0x33fe/0x3850 bpf_prog_load+0x7d7/0xee0 __sys_bpf+0xea2/0x2e30This was triggered by the scx CI while loading the
scx_layeredsched_extscheduler [1].
Quoted 8 lines
Fix by only applying the derived u32 bounds when the resulting range is valid (
u32_min<=u32_max).[1] https://github.com/sched-ext/scx/pull/3349
Fixes c1efab6468fd5 ("bpf: derive subreg bounds from full bounds when upper 32 bits are constant")Signed-off-by Andrea Righi <[email protected]>---
Hi Andrea,
Sorry for delayed response.
Currently the assumption is that all ranges/tnum tracked as a part of a register state are valid, and I don't think we'd like to sidestep this assumption. Eventually is_scalar_branch_taken() and adjust_scalar_min_max_vals() should be unified to avoid possibility for mismatch between is_scalar_branch_taken() predictions (when it assumes that some branch can be taken by failing to predict jump) and adjust_scalar_min_max_vals() adjustments (when it infers that in fact register state is empty for the branch).
As it turns out, the problem you report is a bit easier to fix. After minimizing the failing file with Emil's help I derived the following test case that captures the problem:
SEC("socket")__success__flag(BPF_F_TEST_REG_INVARIANTS)__naked void signed_unsined_intersection32(void *ctx)
{
asm volatile(" \
call %[bpf_get_prandom_u32]; \
w0 &= 0xffffffff; \
if w0 < 0x3 goto 1f; \ /* on fall-through u32 range [3..U32_MAX] */
if w0 s> 0x1 goto 1f; \ /* on fall-through s32 range [S32_MIN..1] *
if w0 s< 0x0 goto 1f; \ /* the above ranges can be narrowed to [S32_MIN..-1] */
r10 = 0; \ /* meaning that condition can be predicted, */
1: exit; \ /* but verifier is not smart enough at the moment. */
" :
: __imm(bpf_get_prandom_u32)
: __clobber_all);
}This branch has verifier modifications making it smart enough to process this case: https://github.com/eddyz87/bpf/tree/cnum-sync-bounds . With some additional verification: https://github.com/eddyz87/cnum-verif . I'm working towards submitting the fix, need to decide on what to do with reg_bounds.c, as this test is now not smart enough.
Thanks,
Eduard
[...]
Hi Eduard,
Quoted 69 lines · Eduard Zingerman
Quoted 24 lines · Andrea Righi
When refining register bounds,
__reg32_deduce_boundscan derive u32 min/max from 64-bit or s32 bounds and assign them with max_t/min_t.If the existing u32 range and the derived range do not overlap (e.g., u64 says [0, 1] while u32 was [2, 2] from an earlier path), the intersection is empty and the new
u32_min_valuecan end up greater thanu32_max_value, triggering the following warning:verifier bug: REG INVARIANTS VIOLATION (
false_reg1): range bounds violationu64=[0x0, 0x1] s64=[0x0, 0x1] u32=[0x3, 0x1] s32=[0x0, 0x1] var_off=(0x0, 0x1) WARNING: kernel/bpf/verifier.c:2742 at reg_bounds_sanity_check Call Trace: reg_bounds_sanity_check+0xbc/0x1e0 reg_set_min_max+0x1a2/0x1f0 check_cond_jmp_op+0x5d2/0x1980 do_check_common+0x2b0f/0x3410 do_check_subprogs+0xcd/0x180 bpf_check+0x33fe/0x3850 bpf_prog_load+0x7d7/0xee0 __sys_bpf+0xea2/0x2e30This was triggered by the scx CI while loading the
scx_layeredsched_extscheduler [1].Quoted 8 lines
Fix by only applying the derived u32 bounds when the resulting range is valid (
u32_min<=u32_max).[1] https://github.com/sched-ext/scx/pull/3349
Fixes c1efab6468fd5 ("bpf: derive subreg bounds from full bounds when upper 32 bits are constant")Signed-off-by Andrea Righi <[email protected]>---
Hi Andrea,
Sorry for delayed response.
Currently the assumption is that all ranges/tnum tracked as a part of a register state are valid, and I don't think we'd like to sidestep this assumption. Eventually
is_scalar_branch_taken()andadjust_scalar_min_max_vals()should be unified to avoid possibility for mismatch betweenis_scalar_branch_taken()predictions (when it assumes that some branch can be taken by failing to predict jump) andadjust_scalar_min_max_vals()adjustments (when it infers that in fact register state is empty for the branch).As it turns out, the problem you report is a bit easier to fix. After minimizing the failing file with Emil's help I derived the following test case that captures the problem:
SEC("socket")__success__flag(BPF_F_TEST_REG_INVARIANTS)__nakedvoidsigned_unsined_intersection32(void *ctx){ asm volatile(" \ call %[bpf_get_prandom_u32]; \ w0 &= 0xffffffff; \ if w0 < 0x3 goto 1f; \ /* on fall-through u32 range [3..U32_MAX] */ if w0 s> 0x1 goto 1f; \ /* on fall-through s32 range [S32_MIN..1] * if w0 s< 0x0 goto 1f; \ /* the above ranges can be narrowed to [S32_MIN..-1] */ r10 = 0; \ /* meaning that condition can be predicted, */ 1: exit; \ /* but verifier is not smart enough at the moment. */ " : : __imm(bpf_get_prandom_u32) : __clobber_all); }
Quoted 5 lines
This branch has verifier modifications making it smart enough to process this case: https://github.com/eddyz87/bpf/tree/cnum-sync-bounds . With some additional verification: https://github.com/eddyz87/cnum-verif . I'm working towards submitting the fix, need to decide on what to do with
reg_bounds.c, as this test is now not smart enough.
I see, yeah, my approach is more of a defensive fix, the right architectural fix is to make the verifier smart enough to never produce this inconsistent state, which is what your branch is doing IIUC. So feel free to ignore this patch once you land the proper fix.
Thanks,
-Andrea