• Kumar Kartikeya Dwivedi's avatar
    bpf: Fix state pruning for STACK_DYNPTR stack slots · d6fefa11
    Kumar Kartikeya Dwivedi authored
    The root of the problem is missing liveness marking for STACK_DYNPTR
    slots. This leads to all kinds of problems inside stacksafe.
    
    The verifier by default inside stacksafe ignores spilled_ptr in stack
    slots which do not have REG_LIVE_READ marks. Since this is being checked
    in the 'old' explored state, it must have already done clean_live_states
    for this old bpf_func_state. Hence, it won't be receiving any more
    liveness marks from to be explored insns (it has received REG_LIVE_DONE
    marking from liveness point of view).
    
    What this means is that verifier considers that it's safe to not compare
    the stack slot if was never read by children states. While liveness
    marks are usually propagated correctly following the parentage chain for
    spilled registers (SCALAR_VALUE and PTR_* types), the same is not the
    case for STACK_DYNPTR.
    
    clean_live_states hence simply rewrites these stack slots to the type
    STACK_INVALID since it sees no REG_LIVE_READ marks.
    
    The end result is that we will never see STACK_DYNPTR slots in explored
    state. Even if verifier was conservatively matching !REG_LIVE_READ
    slots, very next check continuing the stacksafe loop on seeing
    STACK_INVALID would again prevent further checks.
    
    Now as long as verifier stores an explored state which we can compare to
    when reaching a pruning point, we can abuse this bug to make verifier
    prune search for obviously unsafe paths using STACK_DYNPTR slots
    thinking they are never used hence safe.
    
    Doing this in unprivileged mode is a bit challenging. add_new_state is
    only set when seeing BPF_F_TEST_STATE_FREQ (which requires privileges)
    or when jmps_processed difference is >= 2 and insn_processed difference
    is >= 8. So coming up with the unprivileged case requires a little more
    work, but it is still totally possible. The test case being discussed
    below triggers the heuristic even in unprivileged mode.
    
    However, it no longer works since commit
    8addbfc7 ("bpf: Gate dynptr API behind CAP_BPF").
    
    Let's try to study the test step by step.
    
    Consider the following program (C style BPF ASM):
    
    0  r0 = 0;
    1  r6 = &ringbuf_map;
    3  r1 = r6;
    4  r2 = 8;
    5  r3 = 0;
    6  r4 = r10;
    7  r4 -= -16;
    8  call bpf_ringbuf_reserve_dynptr;
    9  if r0 == 0 goto pc+1;
    10 goto pc+1;
    11 *(r10 - 16) = 0xeB9F;
    12 r1 = r10;
    13 r1 -= -16;
    14 r2 = 0;
    15 call bpf_ringbuf_discard_dynptr;
    16 r0 = 0;
    17 exit;
    
    We know that insn 12 will be a pruning point, hence if we force
    add_new_state for it, it will first verify the following path as
    safe in straight line exploration:
    0 1 3 4 5 6 7 8 9 -> 10 -> (12) 13 14 15 16 17
    
    Then, when we arrive at insn 12 from the following path:
    0 1 3 4 5 6 7 8 9 -> 11 (12)
    
    We will find a state that has been verified as safe already at insn 12.
    Since register state is same at this point, regsafe will pass. Next, in
    stacksafe, for spi = 0 and spi = 1 (location of our dynptr) is skipped
    seeing !REG_LIVE_READ. The rest matches, so stacksafe returns true.
    Next, refsafe is also true as reference state is unchanged in both
    states.
    
    The states are considered equivalent and search is pruned.
    
    Hence, we are able to construct a dynptr with arbitrary contents and use
    the dynptr API to operate on this arbitrary pointer and arbitrary size +
    offset.
    
    To fix this, first define a mark_dynptr_read function that propagates
    liveness marks whenever a valid initialized dynptr is accessed by dynptr
    helpers. REG_LIVE_WRITTEN is marked whenever we initialize an
    uninitialized dynptr. This is done in mark_stack_slots_dynptr. It allows
    screening off mark_reg_read and not propagating marks upwards from that
    point.
    
    This ensures that we either set REG_LIVE_READ64 on both dynptr slots, or
    none, so clean_live_states either sets both slots to STACK_INVALID or
    none of them. This is the invariant the checks inside stacksafe rely on.
    
    Next, do a complete comparison of both stack slots whenever they have
    STACK_DYNPTR. Compare the dynptr type stored in the spilled_ptr, and
    also whether both form the same first_slot. Only then is the later path
    safe.
    
    Fixes: 97e03f52 ("bpf: Add verifier support for dynptrs")
    Acked-by: default avatarEduard Zingerman <eddyz87@gmail.com>
    Signed-off-by: default avatarKumar Kartikeya Dwivedi <memxor@gmail.com>
    Link: https://lore.kernel.org/r/20230121002241.2113993-2-memxor@gmail.comSigned-off-by: default avatarAlexei Starovoitov <ast@kernel.org>
    d6fefa11
verifier.c 497 KB