mark_reg_stack_read() currently treats a variable-offset stack read as
known-zero only when every byte in the read range has STACK_ZERO type.
This loses precision for stack bytes that are represented as STACK_SPILL
but come from a spilled scalar const zero.

Fixed-offset stack reads already preserve such partial reads from a
spilled scalar zero as const zero. Variable-offset stack writes also keep
a spilled scalar zero intact when a zero write overlaps it. The read side
is therefore inconsistent and can reject otherwise valid programs: a byte
read from a spilled zero becomes an unknown u8 and may then make a
stack access through that value appear out of bounds.

Treat STACK_SPILL bytes backed by a spilled scalar const zero as zero
bytes in mark_reg_stack_read(). When such a spilled scalar is used to
prove the destination register is const zero, mark every contributing
source stack slot precise before state pruning can use an explored
zero-spill path for a later non-zero spill path.

This has to be done eagerly for variable-offset loads. Fixed-offset stack
fills can record one source stack slot in the jump history and propagate
destination precision back to that slot later, but a variable-offset load
may source bytes from multiple stack slots. Seed precision backtracking
with every zero-spill slot that contributes to the const-zero
classification instead.

Add verifier selftests for both sides: accepted programs that read a byte
through a variable stack offset from spilled zero scalars, including a
multi-slot range, and a rejected program that would be accepted unsafely
by a naive implementation that promotes spilled zero bytes to const zero
without tracking the source spilled slot precisely. Use
BPF_F_TEST_STATE_FREQ for the rejected test so it does not depend on the
current verifier checkpoint heuristic thresholds.

Tested:
  ./test_progs -t verifier_var_off

Assisted-by: opencode:gpt-5.5
Signed-off-by: Woojin Ji <[email protected]>
---
 kernel/bpf/verifier.c                              | 52 ++++++++++---
 .../testing/selftests/bpf/progs/verifier_var_off.c | 88 ++++++++++++++++++++++
 2 files changed, 130 insertions(+), 10 deletions(-)

diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 7fb88e1cd..c4b89fbd9 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -4058,14 +4058,21 @@ static int check_stack_write_var_off(struct 
bpf_verifier_env *env,
  * SCALAR. This function does not deal with register filling; the caller must
  * ensure that all spilled registers in the stack range have been marked as
  * read.
+ *
+ * If the const-zero classification depends on spilled scalar zeroes, mark the
+ * contributing stack slots precise so pruning cannot reuse a zero-spill state
+ * for a later path containing a different spilled scalar.
+ *
+ * Returns an error if precision backtracking fails.
  */
-static void mark_reg_stack_read(struct bpf_verifier_env *env,
-                               /* func where src register points to */
-                               struct bpf_func_state *ptr_state,
-                               int min_off, int max_off, int dst_regno)
+static int mark_reg_stack_read(struct bpf_verifier_env *env,
+                              /* func where src register points to */
+                              struct bpf_func_state *ptr_state,
+                              int min_off, int max_off, int dst_regno)
 {
        struct bpf_verifier_state *vstate = env->cur_state;
        struct bpf_func_state *state = vstate->frame[vstate->curframe];
+       u64 zero_spill_mask = 0;
        int i, slot, spi;
        u8 *stype;
        int zeros = 0;
@@ -4075,19 +4082,38 @@ static void mark_reg_stack_read(struct bpf_verifier_env 
*env,
                spi = slot / BPF_REG_SIZE;
                mark_stack_slot_scratched(env, spi);
                stype = ptr_state->stack[spi].slot_type;
-               if (stype[slot % BPF_REG_SIZE] != STACK_ZERO)
-                       break;
-               zeros++;
+               if (stype[slot % BPF_REG_SIZE] == STACK_ZERO) {
+                       zeros++;
+                       continue;
+               }
+               if (stype[slot % BPF_REG_SIZE] == STACK_SPILL &&
+                   bpf_is_spilled_scalar_reg(&ptr_state->stack[spi]) &&
+                   tnum_is_const(ptr_state->stack[spi].spilled_ptr.var_off) &&
+                   ptr_state->stack[spi].spilled_ptr.var_off.value == 0) {
+                       zero_spill_mask |= 1ull << spi;
+                       zeros++;
+                       continue;
+               }
+               break;
        }
        if (zeros == max_off - min_off) {
                /* Any access_size read into register is zero extended,
                 * so the whole register == const_zero.
                 */
                __mark_reg_const_zero(env, &state->regs[dst_regno]);
+               if (zero_spill_mask) {
+                       for (spi = 0; spi < MAX_BPF_STACK / BPF_REG_SIZE; 
spi++) {
+                               if (zero_spill_mask & (1ull << spi))
+                                       bpf_bt_set_frame_slot(&env->bt, 
ptr_state->frameno, spi);
+                       }
+                       return mark_chain_precision_batch(env, env->cur_state);
+               }
        } else {
                /* have read misc data from the stack */
                mark_reg_unknown(env, state->regs, dst_regno);
        }
+
+       return 0;
 }
 
 /* Read the stack at 'off' and put the results into the register indicated by
@@ -4109,6 +4135,7 @@ static int check_stack_read_fixed_off(struct 
bpf_verifier_env *env,
        int i, slot = -off - 1, spi = slot / BPF_REG_SIZE;
        struct bpf_reg_state *reg;
        u8 *stype, type;
+       int err;
        int insn_flags = insn_stack_access_flags(reg_state->frameno, spi);
 
        stype = reg_state->stack[spi].slot_type;
@@ -4235,8 +4262,11 @@ static int check_stack_read_fixed_off(struct 
bpf_verifier_env *env,
                        }
                        return -EACCES;
                }
-               if (dst_regno >= 0)
-                       mark_reg_stack_read(env, reg_state, off, off + size, 
dst_regno);
+               if (dst_regno >= 0) {
+                       err = mark_reg_stack_read(env, reg_state, off, off + 
size, dst_regno);
+                       if (err)
+                               return err;
+               }
                insn_flags = 0; /* we are not restoring spilled register */
        }
        if (insn_flags)
@@ -4291,7 +4321,9 @@ static int check_stack_read_var_off(struct 
bpf_verifier_env *env,
 
        min_off = reg->smin_value + off;
        max_off = reg->smax_value + off;
-       mark_reg_stack_read(env, ptr_state, min_off, max_off + size, dst_regno);
+       err = mark_reg_stack_read(env, ptr_state, min_off, max_off + size, 
dst_regno);
+       if (err)
+               return err;
        check_fastcall_stack_contract(env, ptr_state, env->insn_idx, min_off);
        return 0;
 }
diff --git a/tools/testing/selftests/bpf/progs/verifier_var_off.c 
b/tools/testing/selftests/bpf/progs/verifier_var_off.c
index f345466bc..2d4878270 100644
--- a/tools/testing/selftests/bpf/progs/verifier_var_off.c
+++ b/tools/testing/selftests/bpf/progs/verifier_var_off.c
@@ -59,6 +59,94 @@ __naked void stack_read_priv_vs_unpriv(void)
 "      ::: __clobber_all);
 }
 
+SEC("cgroup/skb")
+__description("variable-offset stack read preserves spilled zero")
+__success
+__failure_unpriv __msg_unpriv("R2 variable stack access prohibited for !root")
+__retval(0)
+__naked void stack_read_var_off_preserves_spilled_zero(void)
+{
+       asm volatile (
+       "r0 = 0; "
+       " *(u64 *)(r10 - 8) = r0; "
+       "r2 = *(u32 *)(r1 + 0); "
+       "r2 &= 7; "
+       "r2 -= 8; "
+       "r2 += r10; "
+       "r3 = *(u8 *)(r2 + 0); "
+       "r1 = r10; "
+       "r1 += -1; "
+       "r1 += r3; "
+       " *(u8 *)(r1 + 0) = r3; "
+       "r0 = 0; "
+       "exit; "
+       ::
+       : __clobber_all);
+}
+
+SEC("cgroup/skb")
+__description("variable-offset stack read preserves spilled zero across slots")
+__success
+__failure_unpriv __msg_unpriv("R2 variable stack access prohibited for !root")
+__retval(0)
+__naked void stack_read_var_off_preserves_spilled_zero_across_slots(void)
+{
+       asm volatile (
+       "r0 = 0; "
+       " *(u64 *)(r10 - 8) = r0; "
+       " *(u64 *)(r10 - 16) = r0; "
+       "r2 = *(u32 *)(r1 + 0); "
+       "r2 &= 15; "
+       "r2 -= 16; "
+       "r2 += r10; "
+       "r3 = *(u8 *)(r2 + 0); "
+       "r1 = r10; "
+       "r1 += -1; "
+       "r1 += r3; "
+       " *(u8 *)(r1 + 0) = r3; "
+       "r0 = 0; "
+       "exit; "
+       ::
+       : __clobber_all);
+}
+
+SEC("cgroup/skb")
+__description("variable-offset stack read tracks spilled zero precisely")
+__failure
+__flag(BPF_F_TEST_STATE_FREQ)
+__msg("invalid variable-offset write to stack R1")
+__failure_unpriv __msg_unpriv("R2 variable stack access prohibited for !root")
+__naked void stack_read_var_off_tracks_spilled_zero_precisely(void)
+{
+       asm volatile (
+       "r6 = *(u32 *)(r1 + 0); "
+       "r6 &= 1; "
+       "r0 = 0; "
+       "if r6 != 0 goto "
+       "+"
+       "2; "
+       " *(u64 *)(r10 - 8) = r0; "
+       "goto "
+       "+"
+       "2; "
+       "r0 = 1; "
+       " *(u64 *)(r10 - 8) = r0; "
+       "r0 = 0; "
+       "r2 = *(u32 *)(r1 + 4); "
+       "r2 &= 7; "
+       "r2 -= 8; "
+       "r2 += r10; "
+       "r3 = *(u8 *)(r2 + 0); "
+       "r1 = r10; "
+       "r1 += -1; "
+       "r1 += r3; "
+       " *(u8 *)(r1 + 0) = 0; "
+       "r0 = 0; "
+       "exit; "
+       ::
+       : __clobber_all);
+}
+
 SEC("cgroup/skb")
 __description("variable-offset stack read, uninitialized")
 __success

---
base-commit: ddd664bbff63e09e7a7f9acae9c43605d4cf185f
change-id: 20260610-bpf-stack-var-off-zero-v1-34ad1bc3b533

Best regards,
--  
Woojin Ji <[email protected]>


Reply via email to