]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
bpf: Simplify bounds refinement from s32
authorPaul Chaignon <paul.chaignon@gmail.com>
Thu, 24 Jul 2025 17:42:52 +0000 (19:42 +0200)
committerDaniel Borkmann <daniel@iogearbox.net>
Sun, 27 Jul 2025 17:23:29 +0000 (19:23 +0200)
During the bounds refinement, we improve the precision of various ranges
by looking at other ranges. Among others, we improve the following in
this order (other things happen between 1 and 2):

  1. Improve u32 from s32 in __reg32_deduce_bounds.
  2. Improve s/u64 from u32 in __reg_deduce_mixed_bounds.
  3. Improve s/u64 from s32 in __reg_deduce_mixed_bounds.

In particular, if the s32 range forms a valid u32 range, we will use it
to improve the u32 range in __reg32_deduce_bounds. In
__reg_deduce_mixed_bounds, under the same condition, we will use the s32
range to improve the s/u64 ranges.

If at (1) we were able to learn from s32 to improve u32, we'll then be
able to use that in (2) to improve s/u64. Hence, as (3) happens under
the same precondition as (1), it won't improve s/u64 ranges further than
(1)+(2) did. Thus, we can get rid of (3).

In addition to the extensive suite of selftests for bounds refinement,
this patch was also tested with the Agni formal verification tool [1].

Additionally, Eduard mentioned:

  The argument appears to be as follows:

  Under precondition `(u32)reg->s32_min <= (u32)reg->s32_max`
  __reg32_deduce_bounds produces:

    reg->u32_min = max_t(u32, reg->s32_min, reg->u32_min);
    reg->u32_max = min_t(u32, reg->s32_max, reg->u32_max);

  And then first part of __reg_deduce_mixed_bounds assigns:

    a. reg->umin umax= (reg->umin & ~0xffffffffULL) | max_t(u32, reg->s32_min, reg->u32_min);
    b. reg->umax umin= (reg->umax & ~0xffffffffULL) | min_t(u32, reg->s32_max, reg->u32_max);

  And then second part of __reg_deduce_mixed_bounds assigns:

    c. reg->umin umax= (reg->umin & ~0xffffffffULL) | (u32)reg->s32_min;
    d. reg->umax umin= (reg->umax & ~0xffffffffULL) | (u32)reg->s32_max;

  But assignment (c) is a noop because:

     max_t(u32, reg->s32_min, reg->u32_min) >= (u32)reg->s32_min

  Hence RHS(a) >= RHS(c) and umin= does nothing.

  Also assignment (d) is a noop because:

    min_t(u32, reg->s32_max, reg->u32_max) <= (u32)reg->s32_max

  Hence RHS(b) <= RHS(d) and umin= does nothing.

  Plus the same reasoning for the part dealing with reg->s{min,max}_value:

    e. reg->smin_value smax= (reg->smin_value & ~0xffffffffULL) | max_t(u32, reg->s32_min_value, reg->u32_min_value);
    f. reg->smax_value smin= (reg->smax_value & ~0xffffffffULL) | min_t(u32, reg->s32_max_value, reg->u32_max_value);

      vs

    g. reg->smin_value smax= (reg->smin_value & ~0xffffffffULL) | (u32)reg->s32_min_value;
    h. reg->smax_value smin= (reg->smax_value & ~0xffffffffULL) | (u32)reg->s32_max_value;

      RHS(e) >= RHS(g) and RHS(f) <= RHS(h), hence smax=,smin= do nothing.

  This appears to be correct.

Also, Shung-Hsi:

  Beside going through the reasoning, I also played with CBMC a bit to
  double check that as far as a single run of __reg_deduce_bounds() is
  concerned (and that the register state matches certain handwavy
  expectations), the change indeed still preserve the original behavior.

Signed-off-by: Paul Chaignon <paul.chaignon@gmail.com>
Signed-off-by: Daniel Borkmann <daniel@iogearbox.net>
Reviewed-by: Shung-Hsi Yu <shung-hsi.yu@suse.com>
Acked-by: Eduard Zingerman <eddyz87@gmail.com>
Link: https://github.com/bpfverif/agni
Link: https://lore.kernel.org/bpf/aIJwnFnFyUjNsCNa@mail.gmail.com
kernel/bpf/verifier.c

index e2fcea860755cdecbdbcaa72c20e1936bcce8a46..d218516c3b33efed4190e5b8ccde1dc73f33cc68 100644 (file)
@@ -2554,20 +2554,6 @@ static void __reg_deduce_mixed_bounds(struct bpf_reg_state *reg)
        reg->smin_value = max_t(s64, reg->smin_value, new_smin);
        reg->smax_value = min_t(s64, reg->smax_value, new_smax);
 
-       /* if s32 can be treated as valid u32 range, we can use it as well */
-       if ((u32)reg->s32_min_value <= (u32)reg->s32_max_value) {
-               /* s32 -> u64 tightening */
-               new_umin = (reg->umin_value & ~0xffffffffULL) | (u32)reg->s32_min_value;
-               new_umax = (reg->umax_value & ~0xffffffffULL) | (u32)reg->s32_max_value;
-               reg->umin_value = max_t(u64, reg->umin_value, new_umin);
-               reg->umax_value = min_t(u64, reg->umax_value, new_umax);
-               /* s32 -> s64 tightening */
-               new_smin = (reg->smin_value & ~0xffffffffULL) | (u32)reg->s32_min_value;
-               new_smax = (reg->smax_value & ~0xffffffffULL) | (u32)reg->s32_max_value;
-               reg->smin_value = max_t(s64, reg->smin_value, new_smin);
-               reg->smax_value = min_t(s64, reg->smax_value, new_smax);
-       }
-
        /* Here we would like to handle a special case after sign extending load,
         * when upper bits for a 64-bit range are all 1s or all 0s.
         *