]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
bpf: Add range tracking for BPF_DIV and BPF_MOD
authorYazhou Tang <tangyazhou518@outlook.com>
Mon, 19 Jan 2026 08:54:57 +0000 (16:54 +0800)
committerAlexei Starovoitov <ast@kernel.org>
Wed, 21 Jan 2026 00:41:53 +0000 (16:41 -0800)
This patch implements range tracking (interval analysis) for BPF_DIV and
BPF_MOD operations when the divisor is a constant, covering both signed
and unsigned variants.

While LLVM typically optimizes integer division and modulo by constants
into multiplication and shift sequences, this optimization is less
effective for the BPF target when dealing with 64-bit arithmetic.

Currently, the verifier does not track bounds for scalar division or
modulo, treating the result as "unbounded". This leads to false positive
rejections for safe code patterns.

For example, the following code (compiled with -O2):

```c
int test(struct pt_regs *ctx) {
    char buffer[6] = {1};
    __u64 x = bpf_ktime_get_ns();
    __u64 res = x % sizeof(buffer);
    char value = buffer[res];
    bpf_printk("res = %llu, val = %d", res, value);
    return 0;
}
```

Generates a raw `BPF_MOD64` instruction:

```asm
;     __u64 res = x % sizeof(buffer);
       1: 97 00 00 00 06 00 00 00 r0 %= 0x6
;     char value = buffer[res];
       2: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0x0 ll
       4: 0f 01 00 00 00 00 00 00 r1 += r0
       5: 91 14 00 00 00 00 00 00 r4 = *(s8 *)(r1 + 0x0)
```

Without this patch, the verifier fails with "math between map_value
pointer and register with unbounded min value is not allowed" because
it cannot deduce that `r0` is within [0, 5].

According to the BPF instruction set[1], the instruction's offset field
(`insn->off`) is used to distinguish between signed (`off == 1`) and
unsigned division (`off == 0`). Moreover, we also follow the BPF division
and modulo runtime behavior (semantics) to handle special cases, such as
division by zero and signed division overflow.

- UDIV: dst = (src != 0) ? (dst / src) : 0
- SDIV: dst = (src == 0) ? 0 : ((src == -1 && dst == LLONG_MIN) ? LLONG_MIN : (dst / src))
- UMOD: dst = (src != 0) ? (dst % src) : dst
- SMOD: dst = (src == 0) ? dst : ((src == -1 && dst == LLONG_MIN) ? 0: (dst s% src))

Here is the overview of the changes made in this patch (See the code comments
for more details and examples):

1. For BPF_DIV: Firstly check whether the divisor is zero. If so, set the
   destination register to zero (matching runtime behavior).

   For non-zero constant divisors: goto `scalar(32)?_min_max_(u|s)div` functions.
   - General cases: compute the new range by dividing max_dividend and
     min_dividend by the constant divisor.
   - Overflow case (SIGNED_MIN / -1) in signed division: mark the result
     as unbounded if the dividend is not a single number.

2. For BPF_MOD: Firstly check whether the divisor is zero. If so, leave the
   destination register unchanged (matching runtime behavior).

   For non-zero constant divisors: goto `scalar(32)?_min_max_(u|s)mod` functions.
   - General case: For signed modulo, the result's sign matches the
     dividend's sign. And the result's absolute value is strictly bounded
     by `min(abs(dividend), abs(divisor) - 1)`.
     - Special care is taken when the divisor is SIGNED_MIN. By casting
       to unsigned before negation and subtracting 1, we avoid signed
       overflow and correctly calculate the maximum possible magnitude
       (`res_max_abs` in the code).
   - "Small dividend" case: If the dividend is already within the possible
     result range (e.g., [-2, 5] % 10), the operation is an identity
     function, and the destination register remains unchanged.

3. In `scalar(32)?_min_max_(u|s)(div|mod)` functions: After updating current
   range, reset other ranges and tnum to unbounded/unknown.

   e.g., in `scalar_min_max_sdiv`, signed 64-bit range is updated. Then reset
   unsigned 64-bit range and 32-bit range to unbounded, and tnum to unknown.

   Exception: in BPF_MOD's "small dividend" case, since the result remains
   unchanged, we do not reset other ranges/tnum.

4. Also updated existing selftests based on the expected BPF_DIV and
   BPF_MOD behavior.

[1] https://www.kernel.org/doc/Documentation/bpf/standardization/instruction-set.rst

Co-developed-by: Shenghao Yuan <shenghaoyuan0928@163.com>
Signed-off-by: Shenghao Yuan <shenghaoyuan0928@163.com>
Co-developed-by: Tianci Cao <ziye@zju.edu.cn>
Signed-off-by: Tianci Cao <ziye@zju.edu.cn>
Signed-off-by: Yazhou Tang <tangyazhou518@outlook.com>
Tested-by: syzbot@syzkaller.appspotmail.com
Link: https://lore.kernel.org/r/20260119085458.182221-2-tangyazhou@zju.edu.cn
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
kernel/bpf/verifier.c
tools/testing/selftests/bpf/progs/verifier_value_illegal_alu.c
tools/testing/selftests/bpf/verifier/precise.c

index 91955661450515e2b1ec5464b68a4819607af26b..f11dc5366e5b49cf8506fc738ec258607a896760 100644 (file)
@@ -2349,6 +2349,18 @@ static void __mark_reg32_unbounded(struct bpf_reg_state *reg)
        reg->u32_max_value = U32_MAX;
 }
 
+static void reset_reg64_and_tnum(struct bpf_reg_state *reg)
+{
+       __mark_reg64_unbounded(reg);
+       reg->var_off = tnum_unknown;
+}
+
+static void reset_reg32_and_tnum(struct bpf_reg_state *reg)
+{
+       __mark_reg32_unbounded(reg);
+       reg->var_off = tnum_unknown;
+}
+
 static void __update_reg32_bounds(struct bpf_reg_state *reg)
 {
        struct tnum var32_off = tnum_subreg(reg->var_off);
@@ -15159,6 +15171,252 @@ static void scalar_min_max_mul(struct bpf_reg_state *dst_reg,
        }
 }
 
+static void scalar32_min_max_udiv(struct bpf_reg_state *dst_reg,
+                                 struct bpf_reg_state *src_reg)
+{
+       u32 *dst_umin = &dst_reg->u32_min_value;
+       u32 *dst_umax = &dst_reg->u32_max_value;
+       u32 src_val = src_reg->u32_min_value; /* non-zero, const divisor */
+
+       *dst_umin = *dst_umin / src_val;
+       *dst_umax = *dst_umax / src_val;
+
+       /* Reset other ranges/tnum to unbounded/unknown. */
+       dst_reg->s32_min_value = S32_MIN;
+       dst_reg->s32_max_value = S32_MAX;
+       reset_reg64_and_tnum(dst_reg);
+}
+
+static void scalar_min_max_udiv(struct bpf_reg_state *dst_reg,
+                               struct bpf_reg_state *src_reg)
+{
+       u64 *dst_umin = &dst_reg->umin_value;
+       u64 *dst_umax = &dst_reg->umax_value;
+       u64 src_val = src_reg->umin_value; /* non-zero, const divisor */
+
+       *dst_umin = div64_u64(*dst_umin, src_val);
+       *dst_umax = div64_u64(*dst_umax, src_val);
+
+       /* Reset other ranges/tnum to unbounded/unknown. */
+       dst_reg->smin_value = S64_MIN;
+       dst_reg->smax_value = S64_MAX;
+       reset_reg32_and_tnum(dst_reg);
+}
+
+static void scalar32_min_max_sdiv(struct bpf_reg_state *dst_reg,
+                                 struct bpf_reg_state *src_reg)
+{
+       s32 *dst_smin = &dst_reg->s32_min_value;
+       s32 *dst_smax = &dst_reg->s32_max_value;
+       s32 src_val = src_reg->s32_min_value; /* non-zero, const divisor */
+       s32 res1, res2;
+
+       /* BPF div specification: S32_MIN / -1 = S32_MIN */
+       if (*dst_smin == S32_MIN && src_val == -1) {
+               /*
+                * If the dividend range contains more than just S32_MIN,
+                * we cannot precisely track the result, so it becomes unbounded.
+                * e.g., [S32_MIN, S32_MIN+10]/(-1),
+                *     = {S32_MIN} U [-(S32_MIN+10), -(S32_MIN+1)]
+                *     = {S32_MIN} U [S32_MAX-9, S32_MAX] = [S32_MIN, S32_MAX]
+                * Otherwise (if dividend is exactly S32_MIN), result remains S32_MIN.
+                */
+               if (*dst_smax != S32_MIN) {
+                       *dst_smin = S32_MIN;
+                       *dst_smax = S32_MAX;
+               }
+               goto reset;
+       }
+
+       res1 = *dst_smin / src_val;
+       res2 = *dst_smax / src_val;
+       *dst_smin = min(res1, res2);
+       *dst_smax = max(res1, res2);
+
+reset:
+       /* Reset other ranges/tnum to unbounded/unknown. */
+       dst_reg->u32_min_value = 0;
+       dst_reg->u32_max_value = U32_MAX;
+       reset_reg64_and_tnum(dst_reg);
+}
+
+static void scalar_min_max_sdiv(struct bpf_reg_state *dst_reg,
+                               struct bpf_reg_state *src_reg)
+{
+       s64 *dst_smin = &dst_reg->smin_value;
+       s64 *dst_smax = &dst_reg->smax_value;
+       s64 src_val = src_reg->smin_value; /* non-zero, const divisor */
+       s64 res1, res2;
+
+       /* BPF div specification: S64_MIN / -1 = S64_MIN */
+       if (*dst_smin == S64_MIN && src_val == -1) {
+               /*
+                * If the dividend range contains more than just S64_MIN,
+                * we cannot precisely track the result, so it becomes unbounded.
+                * e.g., [S64_MIN, S64_MIN+10]/(-1),
+                *     = {S64_MIN} U [-(S64_MIN+10), -(S64_MIN+1)]
+                *     = {S64_MIN} U [S64_MAX-9, S64_MAX] = [S64_MIN, S64_MAX]
+                * Otherwise (if dividend is exactly S64_MIN), result remains S64_MIN.
+                */
+               if (*dst_smax != S64_MIN) {
+                       *dst_smin = S64_MIN;
+                       *dst_smax = S64_MAX;
+               }
+               goto reset;
+       }
+
+       res1 = div64_s64(*dst_smin, src_val);
+       res2 = div64_s64(*dst_smax, src_val);
+       *dst_smin = min(res1, res2);
+       *dst_smax = max(res1, res2);
+
+reset:
+       /* Reset other ranges/tnum to unbounded/unknown. */
+       dst_reg->umin_value = 0;
+       dst_reg->umax_value = U64_MAX;
+       reset_reg32_and_tnum(dst_reg);
+}
+
+static void scalar32_min_max_umod(struct bpf_reg_state *dst_reg,
+                                 struct bpf_reg_state *src_reg)
+{
+       u32 *dst_umin = &dst_reg->u32_min_value;
+       u32 *dst_umax = &dst_reg->u32_max_value;
+       u32 src_val = src_reg->u32_min_value; /* non-zero, const divisor */
+       u32 res_max = src_val - 1;
+
+       /*
+        * If dst_umax <= res_max, the result remains unchanged.
+        * e.g., [2, 5] % 10 = [2, 5].
+        */
+       if (*dst_umax <= res_max)
+               return;
+
+       *dst_umin = 0;
+       *dst_umax = min(*dst_umax, res_max);
+
+       /* Reset other ranges/tnum to unbounded/unknown. */
+       dst_reg->s32_min_value = S32_MIN;
+       dst_reg->s32_max_value = S32_MAX;
+       reset_reg64_and_tnum(dst_reg);
+}
+
+static void scalar_min_max_umod(struct bpf_reg_state *dst_reg,
+                               struct bpf_reg_state *src_reg)
+{
+       u64 *dst_umin = &dst_reg->umin_value;
+       u64 *dst_umax = &dst_reg->umax_value;
+       u64 src_val = src_reg->umin_value; /* non-zero, const divisor */
+       u64 res_max = src_val - 1;
+
+       /*
+        * If dst_umax <= res_max, the result remains unchanged.
+        * e.g., [2, 5] % 10 = [2, 5].
+        */
+       if (*dst_umax <= res_max)
+               return;
+
+       *dst_umin = 0;
+       *dst_umax = min(*dst_umax, res_max);
+
+       /* Reset other ranges/tnum to unbounded/unknown. */
+       dst_reg->smin_value = S64_MIN;
+       dst_reg->smax_value = S64_MAX;
+       reset_reg32_and_tnum(dst_reg);
+}
+
+static void scalar32_min_max_smod(struct bpf_reg_state *dst_reg,
+                                 struct bpf_reg_state *src_reg)
+{
+       s32 *dst_smin = &dst_reg->s32_min_value;
+       s32 *dst_smax = &dst_reg->s32_max_value;
+       s32 src_val = src_reg->s32_min_value; /* non-zero, const divisor */
+
+       /*
+        * Safe absolute value calculation:
+        * If src_val == S32_MIN (-2147483648), src_abs becomes 2147483648.
+        * Here use unsigned integer to avoid overflow.
+        */
+       u32 src_abs = (src_val > 0) ? (u32)src_val : -(u32)src_val;
+
+       /*
+        * Calculate the maximum possible absolute value of the result.
+        * Even if src_abs is 2147483648 (S32_MIN), subtracting 1 gives
+        * 2147483647 (S32_MAX), which fits perfectly in s32.
+        */
+       s32 res_max_abs = src_abs - 1;
+
+       /*
+        * If the dividend is already within the result range,
+        * the result remains unchanged. e.g., [-2, 5] % 10 = [-2, 5].
+        */
+       if (*dst_smin >= -res_max_abs && *dst_smax <= res_max_abs)
+               return;
+
+       /* General case: result has the same sign as the dividend. */
+       if (*dst_smin >= 0) {
+               *dst_smin = 0;
+               *dst_smax = min(*dst_smax, res_max_abs);
+       } else if (*dst_smax <= 0) {
+               *dst_smax = 0;
+               *dst_smin = max(*dst_smin, -res_max_abs);
+       } else {
+               *dst_smin = -res_max_abs;
+               *dst_smax = res_max_abs;
+       }
+
+       /* Reset other ranges/tnum to unbounded/unknown. */
+       dst_reg->u32_min_value = 0;
+       dst_reg->u32_max_value = U32_MAX;
+       reset_reg64_and_tnum(dst_reg);
+}
+
+static void scalar_min_max_smod(struct bpf_reg_state *dst_reg,
+                               struct bpf_reg_state *src_reg)
+{
+       s64 *dst_smin = &dst_reg->smin_value;
+       s64 *dst_smax = &dst_reg->smax_value;
+       s64 src_val = src_reg->smin_value; /* non-zero, const divisor */
+
+       /*
+        * Safe absolute value calculation:
+        * If src_val == S64_MIN (-2^63), src_abs becomes 2^63.
+        * Here use unsigned integer to avoid overflow.
+        */
+       u64 src_abs = (src_val > 0) ? (u64)src_val : -(u64)src_val;
+
+       /*
+        * Calculate the maximum possible absolute value of the result.
+        * Even if src_abs is 2^63 (S64_MIN), subtracting 1 gives
+        * 2^63 - 1 (S64_MAX), which fits perfectly in s64.
+        */
+       s64 res_max_abs = src_abs - 1;
+
+       /*
+        * If the dividend is already within the result range,
+        * the result remains unchanged. e.g., [-2, 5] % 10 = [-2, 5].
+        */
+       if (*dst_smin >= -res_max_abs && *dst_smax <= res_max_abs)
+               return;
+
+       /* General case: result has the same sign as the dividend. */
+       if (*dst_smin >= 0) {
+               *dst_smin = 0;
+               *dst_smax = min(*dst_smax, res_max_abs);
+       } else if (*dst_smax <= 0) {
+               *dst_smax = 0;
+               *dst_smin = max(*dst_smin, -res_max_abs);
+       } else {
+               *dst_smin = -res_max_abs;
+               *dst_smax = res_max_abs;
+       }
+
+       /* Reset other ranges/tnum to unbounded/unknown. */
+       dst_reg->umin_value = 0;
+       dst_reg->umax_value = U64_MAX;
+       reset_reg32_and_tnum(dst_reg);
+}
+
 static void scalar32_min_max_and(struct bpf_reg_state *dst_reg,
                                 struct bpf_reg_state *src_reg)
 {
@@ -15564,6 +15822,14 @@ static bool is_safe_to_compute_dst_reg_range(struct bpf_insn *insn,
        case BPF_MUL:
                return true;
 
+       /*
+        * Division and modulo operators range is only safe to compute when the
+        * divisor is a constant.
+        */
+       case BPF_DIV:
+       case BPF_MOD:
+               return src_is_const;
+
        /* Shift operators range is only computable if shift dimension operand
         * is a constant. Shifts greater than 31 or 63 are undefined. This
         * includes shifts by a negative number.
@@ -15616,6 +15882,7 @@ static int adjust_scalar_min_max_vals(struct bpf_verifier_env *env,
                                      struct bpf_reg_state src_reg)
 {
        u8 opcode = BPF_OP(insn->code);
+       s16 off = insn->off;
        bool alu32 = (BPF_CLASS(insn->code) != BPF_ALU64);
        int ret;
 
@@ -15667,6 +15934,38 @@ static int adjust_scalar_min_max_vals(struct bpf_verifier_env *env,
                scalar32_min_max_mul(dst_reg, &src_reg);
                scalar_min_max_mul(dst_reg, &src_reg);
                break;
+       case BPF_DIV:
+               /* BPF div specification: x / 0 = 0 */
+               if ((alu32 && src_reg.u32_min_value == 0) || (!alu32 && src_reg.umin_value == 0)) {
+                       ___mark_reg_known(dst_reg, 0);
+                       break;
+               }
+               if (alu32)
+                       if (off == 1)
+                               scalar32_min_max_sdiv(dst_reg, &src_reg);
+                       else
+                               scalar32_min_max_udiv(dst_reg, &src_reg);
+               else
+                       if (off == 1)
+                               scalar_min_max_sdiv(dst_reg, &src_reg);
+                       else
+                               scalar_min_max_udiv(dst_reg, &src_reg);
+               break;
+       case BPF_MOD:
+               /* BPF mod specification: x % 0 = x */
+               if ((alu32 && src_reg.u32_min_value == 0) || (!alu32 && src_reg.umin_value == 0))
+                       break;
+               if (alu32)
+                       if (off == 1)
+                               scalar32_min_max_smod(dst_reg, &src_reg);
+                       else
+                               scalar32_min_max_umod(dst_reg, &src_reg);
+               else
+                       if (off == 1)
+                               scalar_min_max_smod(dst_reg, &src_reg);
+                       else
+                               scalar_min_max_umod(dst_reg, &src_reg);
+               break;
        case BPF_AND:
                if (tnum_is_const(src_reg.var_off)) {
                        ret = maybe_fork_scalars(env, insn, dst_reg);
index 2129e4353fd97d4e1d444f382a315ff906b211f3..4d8273c258d510fef64d2d57252b8215d8a11c89 100644 (file)
@@ -173,14 +173,15 @@ __naked void flow_keys_illegal_variable_offset_alu(void)
        asm volatile("                                  \
        r6 = r1;                                        \
        r7 = *(u64*)(r6 + %[flow_keys_off]);            \
-       r8 = 8;                                         \
-       r8 /= 1;                                        \
+       call %[bpf_get_prandom_u32];                    \
+       r8 = r0;                                        \
        r8 &= 8;                                        \
        r7 += r8;                                       \
        r0 = *(u64*)(r7 + 0);                           \
        exit;                                           \
 "      :
-       : __imm_const(flow_keys_off, offsetof(struct __sk_buff, flow_keys))
+       : __imm_const(flow_keys_off, offsetof(struct __sk_buff, flow_keys)),
+         __imm(bpf_get_prandom_u32)
        : __clobber_all);
 }
 
index 59a020c3564742580bfb9a652d752473ce5593b2..061d98f6e9bb369e47f9612da02c4f82424d951d 100644 (file)
 {
        "precise: program doesn't prematurely prune branches",
        .insns = {
-               BPF_ALU64_IMM(BPF_MOV, BPF_REG_6, 0x400),
+               BPF_RAW_INSN(BPF_JMP | BPF_CALL, 0, 0, 0, BPF_FUNC_get_prandom_u32),
+               BPF_ALU64_REG(BPF_MOV, BPF_REG_6, BPF_REG_0),
                BPF_ALU64_IMM(BPF_MOV, BPF_REG_7, 0),
                BPF_ALU64_IMM(BPF_MOV, BPF_REG_8, 0),
                BPF_ALU64_IMM(BPF_MOV, BPF_REG_9, 0x80000000),
-               BPF_ALU64_IMM(BPF_MOD, BPF_REG_6, 0x401),
                BPF_JMP_IMM(BPF_JA, 0, 0, 0),
                BPF_JMP_REG(BPF_JLE, BPF_REG_6, BPF_REG_9, 2),
                BPF_ALU64_IMM(BPF_MOD, BPF_REG_6, 1),