Re: [RFC bpf-next] bpf, verifier: improve signed ranges inference for BPF_AND
From: Shung-Hsi Yu <hidden>
Date: 2024-07-16 15:10:16
Also in:
bpf
On Tue, Jul 16, 2024 at 10:52:26PM GMT, Shung-Hsi Yu wrote:
This commit teach the BPF verifier how to infer signed ranges directly from signed ranges of the operands to prevent verifier rejection
...
quoted hunk ↗ jump to hunk
--- kernel/bpf/verifier.c | 62 +++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 20 deletions(-)diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c index 8da132a1ef28..6d4cdf30cd76 100644 --- a/kernel/bpf/verifier.c +++ b/kernel/bpf/verifier.c@@ -13466,6 +13466,39 @@ static void scalar_min_max_mul(struct bpf_reg_state *dst_reg, } } +/* Clears all trailing bits after the most significant unset bit. + * + * Used for estimating the minimum possible value after BPF_AND. This + * effectively rounds a negative value down to a negative power-of-2 value + * (except for -1, which just return -1) and returning 0 for non-negative + * values. E.g. masked32_negative(0xff0ff0ff) == 0xff000000.
s/masked32_negative/negative32_bit_floor/
quoted hunk ↗ jump to hunk
+ */ +static inline s32 negative32_bit_floor(s32 v) +{ + /* XXX: per C standard section 6.5.7 right shift of signed negative + * value is implementation-defined. Should unsigned type be used here + * instead? + */ + v &= v >> 1; + v &= v >> 2; + v &= v >> 4; + v &= v >> 8; + v &= v >> 16; + return v; +} + +/* Same as negative32_bit_floor() above, but for 64-bit signed value */ +static inline s64 negative_bit_floor(s64 v) +{ + v &= v >> 1; + v &= v >> 2; + v &= v >> 4; + v &= v >> 8; + v &= v >> 16; + v &= v >> 32; + return v; +} + static void scalar32_min_max_and(struct bpf_reg_state *dst_reg, struct bpf_reg_state *src_reg) {@@ -13485,16 +13518,10 @@ static void scalar32_min_max_and(struct bpf_reg_state *dst_reg, dst_reg->u32_min_value = var32_off.value; dst_reg->u32_max_value = min(dst_reg->u32_max_value, umax_val); - /* Safe to set s32 bounds by casting u32 result into s32 when u32 - * doesn't cross sign boundary. Otherwise set s32 bounds to unbounded. - */ - if ((s32)dst_reg->u32_min_value <= (s32)dst_reg->u32_max_value) { - dst_reg->s32_min_value = dst_reg->u32_min_value; - dst_reg->s32_max_value = dst_reg->u32_max_value; - } else { - dst_reg->s32_min_value = S32_MIN; - dst_reg->s32_max_value = S32_MAX; - } + /* Rough estimate tuned for [-1, 0] & -CONSTANT cases. */ + dst_reg->s32_min_value = negative32_bit_floor(min(dst_reg->s32_min_value, + src_reg->s32_min_value)); + dst_reg->s32_max_value = max(dst_reg->s32_max_value, src_reg->s32_max_value); } static void scalar_min_max_and(struct bpf_reg_state *dst_reg,@@ -13515,16 +13542,11 @@ static void scalar_min_max_and(struct bpf_reg_state *dst_reg, dst_reg->umin_value = dst_reg->var_off.value; dst_reg->umax_value = min(dst_reg->umax_value, umax_val); - /* Safe to set s64 bounds by casting u64 result into s64 when u64 - * doesn't cross sign boundary. Otherwise set s64 bounds to unbounded. - */ - if ((s64)dst_reg->umin_value <= (s64)dst_reg->umax_value) { - dst_reg->smin_value = dst_reg->umin_value; - dst_reg->smax_value = dst_reg->umax_value; - } else { - dst_reg->smin_value = S64_MIN; - dst_reg->smax_value = S64_MAX; - } + /* Rough estimate tuned for [-1, 0] & -CONSTANT cases. */ + dst_reg->smin_value = negative_bit_floor(min(dst_reg->smin_value, + src_reg->smin_value)); + dst_reg->smax_value = max(dst_reg->smax_value, src_reg->smax_value); + /* We may learn something more from the var_off */ __update_reg_bounds(dst_reg); }
Checked that this passes BPF CI[0] (except s390x-gcc/test_verifier, which seems stucked), and verified the logic with z3 (see attached Python script, adapted from [1]); so it seems to work. Will try running tools/testing/selftests/bpf/prog_tests/reg_bounds.c against it next. 0: https://github.com/kernel-patches/bpf/actions/runs/9958322024 1: https://github.com/bpfverif/tnums-cgo22/blob/main/verification/tnum.py