From: Eduard Zingerman <eddyz87@gmail.com>
To: Andrii Nakryiko <andrii.nakryiko@gmail.com>
Cc: Alexei Starovoitov <alexei.starovoitov@gmail.com>,
Andrew Werner <awerner32@gmail.com>, bpf <bpf@vger.kernel.org>,
Andrei Matei <andreimatei1@gmail.com>,
Tamir Duberstein <tamird@gmail.com>,
Joanne Koong <joannelkoong@gmail.com>,
kernel-team@dataexmachina.dev, Song Liu <song@kernel.org>,
Kumar Kartikeya Dwivedi <memxor@gmail.com>
Subject: Re: [BUG] verifier escape with iteration helpers (bpf_loop, ...)
Date: Mon, 02 Oct 2023 06:26:04 +0300 [thread overview]
Message-ID: <a60a991079f633955e1e6fcebc4d040c06fea408.camel@gmail.com> (raw)
In-Reply-To: <CAEf4BzYNpL7OVqCfDCoPfrcJ3pkZo77GS7000pRfGghQf1kn2Q@mail.gmail.com>
On Thu, 2023-09-28 at 11:30 -0700, Andrii Nakryiko wrote:
[...]
> > The way I read it the following algorithm should suffice:
> > - add a field bpf_verifier_env::iter_head similar to 'head' but for
> > postponed looping states;
> > - add functions push_iter_stack(), pop_iter_stack() similar to
> > push_stack() and pop_stack();
>
> I don't like the suggested naming, it's too iter-centric, and it's
> actually a queue, not a stack, etc. But that's something to figure out
> later.
After spending some time I think I figured out an example that shows
that postponed looping states do not form a queue. Please bear with
me, as it is quite large:
1. j = iter_new(); // fp[-16]
2. a = 0; // r6
3. b = 0; // r7
4. c = -24; // r8
5. while (iter_next(j)) {
6. i = iter_new();
7. a = 0;
8. b = 0;
9. while (iter_next(i)) {
10. if (a == 1) {
11. a = 0;
12. b = 1;
13. } else if (a == 0) {
14. a = 1;
15. if (random() == 42)
16. continue;
17. if (b == 1) {
18. *(r10 + c) = 7;
19. iter_destroy(i);
20. iter_destroy(j);
21. return;
22. }
23. }
24. }
25. iter_destroy(i);
26. a = 0;
27. b = 0;
28. c = -25;
29. }
30. iter_destroy(j);
31. return;
This example is unsafe, because on second iteration of loop 5-29 the
value of 'c' is -25 (assignment at 28) and 'c' is used for stack
access at 18, offset of -25 is misaligned.
The basic idea is to:
(a) hide the fact that (c) is precise behind a postponed state;
(b) hide the unsafe (c) value behind a postponed state.
The (b) is achieved on first iteration of the loop 5-29:
enter 5 in state with c=-24 not precise, 'i' is initially considered
drained thus verification path is 5-9,25-29,5. Upon second visit to 5
the state is postponed because c=-24 is considered equal to c=-25
(both are not precise).
State at first visit of 5:
R0=scalar() R1_rw=fp-16 R6=0 R7=0 R8=-24 R10=fp0
fp-16_r=iter_num(ref_id=1,state=active,depth=0) refs=1
State at second visit of 5 (postponed, let's call it P1):
R1_w=fp-16 R6=0 R7=0 R8=-25 R10=fp0
fp-16=iter_num(ref_id=1,state=active,depth=1) refs=1
The (a) is achieved by a series of jumps in the inner loop 9-24:
- first visit of 9 is in state {a=0,b=0,c=XX}
(here XX is a value of C and it is either -24 or -25, depending on
outer loop iteration number);
- second visit of 9 is in state {a=1P,b=0,c=XX}
(path 9,14-16,9; at this point 'a' is marked precise because of prediction at 13);
- third visit of 9 is in state {a=0P,b=1,c=XX}
(path 9-12,9; 'b' is not yet marked as precise and thus this state
is equal to the first one and is postponed).
- after this verifier visits 17, predicts condition as false and marks
'b' as precise, but {a=1P,b=1,c=XX} is already postponed.
E.g. the following state is postponed at first iteration, let's call it P2:
R0=... R1_w=fp-8 R6=0 R7=1 R8=-24 R10=fp0
fp-8=iter_num(ref_id=3,state=active,depth=2)
fp-16=iter_num(ref_id=1,state=active,depth=1) refs=1,3
If verification were to proceed from state P2 {a=0P,b=1P,c=XX} at 9
the path would be 9,13-14,17-21 and 'c' would be marked as precise.
But if we process postponed states as a queue P1 would be processed
first, 'c' would not have a precision mark yet and P1 would be
discarded => P2 with R8=-25 would never be investigated.
I converted this test case to assembly [2] and tried with a
prototype [1] to make sure that I don't miss anything.
---
In general, states form a tree during verification and we want to stop
the tree growth when we see a would be back-edge to a similar state.
Consider the following state graph:
.---> S1 <---. Here there is a chain of derived states:
| | | S1 -> S2 -> S3
| v |
| S2 -> S2' And there are potentially looping states:
| | - S2' (derived from S2, states_equal to S1)
| v - S3' (derived from S3, states_equal to S1)
S3' <- S3
If we find that S2' is no longer states_equal to S1 it might add a
derivation that would eventually propagate read or precision mark to
some register in S1. Same is true for S3'. Thus there is no clear
order in which S2' and S3' should be processed.
It is possible to imagine more complex state graphs. Unfortunately,
preparing real BPF examples for this graphs is very time consuming
(at-least for me).
Hence, I think that a fixed point computation is necessary as I write
in a sibling email to Alexei:
- Process states from env->head while those are available, in case if
potential looping state (is_states_equal()) is reached put it to a
separate queue.
- Once all env->head states are processed the only source for new read
and precision marks is in postponed looping states, some of which
might not be is_states_equal() anymore. Submit each such state for
verification until fixed point is reached (repeating steps for
env->head processing).
The work-in-progress implementation for this algorithm is in [1].
---
While working on this I found an unexpected interaction with infinite
loop detection for the following test case:
// any branch
if (random() > 42)
a = 0;
else
a = 1;
// iterator loop
i = iter_new()
while (iter_next(i))
// do something
When first branch of 'if' is processed a looping state for 'while' is
added to a postponed states queue. Then second branch of 'if' is
processed and reaches 'iter_new()', at which point infinite loop
detection logic sounds an alarm:
if (states_maybe_looping(&sl->state, cur) &&
states_equal(env, &sl->state, cur) &&
!iter_active_depths_differ(&sl->state, cur) && false) {
verbose_linfo(env, insn_idx, "; ");
verbose(env, "infinite loop detected at insn %d\n", insn_idx);
return -EINVAL;
}
The state with iter_new() is a parent for a looping state thus has
it's .branches counter as non-zero, and iterator depth is 0 for both
hands of the 'if'.
Adding such states to the looping states queue is not safe, as there
is no guarantee this arbitrary state would terminate (opposed to
iter_next() for which we are guaranteed to have 'drained' branch).
Thus, the only fix that I see now is to disable infinite loop
detection if there are active iterators.
(Might do some trick with remembering this state are printing it if
instructions limit counter is reached, but I'm not sure if added
complexity is worth it, e.g. what to do if there are several such
states?)
---
[1] https://github.com/eddyz87/bpf/tree/iters-bug-delayed-traversal
[2] https://github.com/eddyz87/bpf/blob/iters-bug-delayed-traversal/tools/testing/selftests/bpf/progs/iters.c#L860
Thanks,
Eduard
next prev parent reply other threads:[~2023-10-02 3:26 UTC|newest]
Thread overview: 52+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-07-07 14:04 [BUG] verifier escape with iteration helpers (bpf_loop, ...) Andrew Werner
2023-07-07 16:44 ` Alexei Starovoitov
2023-07-07 18:08 ` Andrii Nakryiko
2023-07-07 18:21 ` Andrew Werner
2023-09-17 21:37 ` Eduard Zingerman
2023-09-17 22:09 ` Kumar Kartikeya Dwivedi
2023-09-18 13:06 ` Eduard Zingerman
2023-09-19 16:28 ` Eduard Zingerman
2023-09-19 23:02 ` Andrii Nakryiko
2023-09-20 0:19 ` Eduard Zingerman
2023-09-20 16:20 ` Eduard Zingerman
2023-09-20 16:57 ` Andrii Nakryiko
2023-09-21 9:14 ` Alexei Starovoitov
2023-09-21 11:03 ` Eduard Zingerman
2023-09-21 12:56 ` Alexei Starovoitov
2023-09-21 16:23 ` Eduard Zingerman
2023-09-21 16:35 ` Andrii Nakryiko
2023-09-21 18:16 ` Eduard Zingerman
2023-09-22 1:01 ` Eduard Zingerman
2023-09-22 2:48 ` Andrii Nakryiko
2023-09-22 18:36 ` Eduard Zingerman
2023-09-22 20:52 ` Andrii Nakryiko
2023-09-25 1:01 ` Eduard Zingerman
2023-09-26 0:33 ` Andrii Nakryiko
2023-09-26 15:55 ` Eduard Zingerman
2023-09-26 16:25 ` Andrii Nakryiko
2023-09-28 1:09 ` Eduard Zingerman
2023-09-28 18:30 ` Andrii Nakryiko
2023-10-02 3:26 ` Eduard Zingerman [this message]
2023-09-30 0:41 ` Alexei Starovoitov
2023-10-02 1:40 ` Eduard Zingerman
2023-10-02 16:29 ` Alexei Starovoitov
2023-10-02 17:18 ` Eduard Zingerman
2023-10-03 0:05 ` Alexei Starovoitov
2023-10-03 2:00 ` Alexei Starovoitov
2023-10-03 15:33 ` Eduard Zingerman
2023-10-03 16:07 ` Alexei Starovoitov
2023-10-03 18:50 ` Alexei Starovoitov
2023-10-03 21:52 ` Eduard Zingerman
2023-10-03 22:03 ` Eduard Zingerman
2023-10-03 23:08 ` Alexei Starovoitov
2023-10-03 23:14 ` Eduard Zingerman
2023-10-04 0:22 ` Andrii Nakryiko
2023-10-04 1:05 ` Eduard Zingerman
2023-10-04 2:57 ` Alexei Starovoitov
2023-10-04 5:50 ` Alexei Starovoitov
2023-10-04 9:49 ` Eduard Zingerman
2023-10-04 11:52 ` Eduard Zingerman
2023-09-19 23:14 ` Andrii Nakryiko
2023-09-20 0:06 ` Eduard Zingerman
2023-09-20 16:37 ` Andrii Nakryiko
2023-09-20 17:13 ` Eduard Zingerman
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=a60a991079f633955e1e6fcebc4d040c06fea408.camel@gmail.com \
--to=eddyz87@gmail.com \
--cc=alexei.starovoitov@gmail.com \
--cc=andreimatei1@gmail.com \
--cc=andrii.nakryiko@gmail.com \
--cc=awerner32@gmail.com \
--cc=bpf@vger.kernel.org \
--cc=joannelkoong@gmail.com \
--cc=kernel-team@dataexmachina.dev \
--cc=memxor@gmail.com \
--cc=song@kernel.org \
--cc=tamird@gmail.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