On Fri, Jan 06, 2023 at 05:48:06AM IST, Joanne Koong wrote: > On Sun, Jan 1, 2023 at 12:34 AM Kumar Kartikeya Dwivedi > <memxor@xxxxxxxxx> wrote: > > > > 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 > > 8addbfc7b308 ("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: 97e03f521050 ("bpf: Add verifier support for dynptrs") > > Signed-off-by: Kumar Kartikeya Dwivedi <memxor@xxxxxxxxx> > > --- > > kernel/bpf/verifier.c | 73 +++++++++++++++++++++++++++++++++++++++++++ > > 1 file changed, 73 insertions(+) > > > > diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c > > index 4a25375ebb0d..f7248235e119 100644 > > --- a/kernel/bpf/verifier.c > > +++ b/kernel/bpf/verifier.c > > @@ -781,6 +781,9 @@ static int mark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_reg_ > > state->stack[spi - 1].spilled_ptr.ref_obj_id = id; > > } > > > > + state->stack[spi].spilled_ptr.live |= REG_LIVE_WRITTEN; > > + state->stack[spi - 1].spilled_ptr.live |= REG_LIVE_WRITTEN; > > + > > return 0; > > } > > > > @@ -805,6 +808,26 @@ static int unmark_stack_slots_dynptr(struct bpf_verifier_env *env, struct bpf_re > > > > __mark_reg_not_init(env, &state->stack[spi].spilled_ptr); > > __mark_reg_not_init(env, &state->stack[spi - 1].spilled_ptr); > > + > > + /* Why do we need to set REG_LIVE_WRITTEN for STACK_INVALID slot? > > + * > > + * While we don't allow reading STACK_INVALID, it is still possible to > > + * do <8 byte writes marking some but not all slots as STACK_MISC. Then, > > + * helpers or insns can do partial read of that part without failing, > > + * but check_stack_range_initialized, check_stack_read_var_off, and > > + * check_stack_read_fixed_off will do mark_reg_read for all 8-bytes of > > + * the slot conservatively. Hence we need to screen off those liveness > > + * marking walks. > > + * > > + * This was not a problem before because STACK_INVALID is only set by > > + * default, or in clean_live_states after REG_LIVE_DONE, not randomly > > + * during verifier state exploration. Hence, for this case parentage > > Where does it get set randomly during verifier state exploration for this case? > When unmarking dynptr slots, we set STACK_INVALID. There are no other instances where a slot is marked STACK_INVALID while verifier is doing symbolic execution. It is either set by default or after checkpointed state won't be receiving liveness marks anymore (in clean_live_states). > > + * chain will still be live, while earlier reg->parent was NULL, so we > > What does "live" in "parentage chain will still be live" here mean? It just means it will probably have a non-NULL reg->parent (which mark_reg_read will follow), by default when STACK_INVALID slot is written to the spilled_ptr will have reg->parent as NULL. > what does "earlier" in "earlier reg->parent" refer to here, and why > was the earlier reg->parent NULL? > Earlier refers to the default case when STACK_INVALID was not being set while unmarking dynptr slots. So any mark_reg_read on spilled_ptr didn't propagate any read marks. Now it can happen, so to make state pruning less conservative we need to be more careful in places where REG_LIVE_WRITTEN needs to be set to stop those register parent chain walks in mark_reg_read. > > [...]