From: Kumar Kartikeya Dwivedi <memxor@gmail.com>
To: Joanne Koong <joannelkoong@gmail.com>
Cc: bpf@vger.kernel.org, Alexei Starovoitov <ast@kernel.org>,
Andrii Nakryiko <andrii@kernel.org>,
Daniel Borkmann <daniel@iogearbox.net>,
Martin KaFai Lau <martin.lau@kernel.org>,
David Vernet <void@manifault.com>,
Eduard Zingerman <eddyz87@gmail.com>
Subject: Re: [PATCH bpf-next v1 1/8] bpf: Fix state pruning for STACK_DYNPTR stack slots
Date: Mon, 9 Jan 2023 16:47:05 +0530 [thread overview]
Message-ID: <20230109111705.g5s2pdnp4xw2kmbb@apollo> (raw)
In-Reply-To: <CAJnrk1b2YaftHpKJCjcGXBScgp1z9Ff4vFqTuZfDGeNF346cJQ@mail.gmail.com>
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@gmail.com> 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@gmail.com>
> > ---
> > 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.
> > [...]
next prev parent reply other threads:[~2023-01-09 11:18 UTC|newest]
Thread overview: 38+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-01-01 8:33 [PATCH bpf-next v1 0/8] Dynptr fixes Kumar Kartikeya Dwivedi
2023-01-01 8:33 ` [PATCH bpf-next v1 1/8] bpf: Fix state pruning for STACK_DYNPTR stack slots Kumar Kartikeya Dwivedi
2023-01-02 19:28 ` Eduard Zingerman
2023-01-09 10:59 ` Kumar Kartikeya Dwivedi
2023-01-04 22:24 ` Andrii Nakryiko
2023-01-09 11:05 ` Kumar Kartikeya Dwivedi
2023-01-12 0:47 ` Andrii Nakryiko
2023-01-06 0:18 ` Joanne Koong
2023-01-09 11:17 ` Kumar Kartikeya Dwivedi [this message]
2023-01-01 8:33 ` [PATCH bpf-next v1 2/8] bpf: Fix missing var_off check for ARG_PTR_TO_DYNPTR Kumar Kartikeya Dwivedi
2023-01-04 22:32 ` Andrii Nakryiko
2023-01-09 11:18 ` Kumar Kartikeya Dwivedi
2023-01-06 0:57 ` Joanne Koong
2023-01-06 17:56 ` Joanne Koong
2023-01-09 11:21 ` Kumar Kartikeya Dwivedi
2023-01-01 8:33 ` [PATCH bpf-next v1 3/8] bpf: Fix partial dynptr stack slot reads/writes Kumar Kartikeya Dwivedi
2023-01-04 22:42 ` Andrii Nakryiko
2023-01-09 11:26 ` Kumar Kartikeya Dwivedi
2023-01-05 3:06 ` Alexei Starovoitov
2023-01-09 11:52 ` Kumar Kartikeya Dwivedi
2023-01-10 2:19 ` Alexei Starovoitov
2023-01-06 19:16 ` Joanne Koong
2023-01-06 19:31 ` Joanne Koong
2023-01-09 11:30 ` Kumar Kartikeya Dwivedi
2023-01-12 18:51 ` Joanne Koong
2023-01-01 8:33 ` [PATCH bpf-next v1 4/8] bpf: Allow reinitializing unreferenced dynptr stack slots Kumar Kartikeya Dwivedi
2023-01-04 22:44 ` Andrii Nakryiko
2023-01-06 19:33 ` Joanne Koong
2023-01-09 11:40 ` Kumar Kartikeya Dwivedi
2023-01-01 8:33 ` [PATCH bpf-next v1 5/8] selftests/bpf: Add dynptr pruning tests Kumar Kartikeya Dwivedi
2023-01-04 22:49 ` Andrii Nakryiko
2023-01-09 11:44 ` Kumar Kartikeya Dwivedi
2023-01-01 8:34 ` [PATCH bpf-next v1 6/8] selftests/bpf: Add dynptr var_off tests Kumar Kartikeya Dwivedi
2023-01-01 8:34 ` [PATCH bpf-next v1 7/8] selftests/bpf: Add dynptr partial slot overwrite tests Kumar Kartikeya Dwivedi
2023-01-01 8:34 ` [PATCH bpf-next v1 8/8] selftests/bpf: Add dynptr helper tests Kumar Kartikeya Dwivedi
2023-01-04 22:51 ` [PATCH bpf-next v1 0/8] Dynptr fixes Andrii Nakryiko
2023-01-12 1:08 ` Kumar Kartikeya Dwivedi
2023-01-13 22:31 ` Andrii Nakryiko
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20230109111705.g5s2pdnp4xw2kmbb@apollo \
--to=memxor@gmail.com \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=joannelkoong@gmail.com \
--cc=martin.lau@kernel.org \
--cc=void@manifault.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox