Skip to content

Commit 4a4cbce

Browse files
authored
Introduce loops, unwinding, and bounds tutorial (#494)
* Introduce loops, unwinding, and bounds tutorial * address feedback * typo
1 parent 5cb3d7c commit 4a4cbce

File tree

4 files changed

+183
-1
lines changed

4 files changed

+183
-1
lines changed

rmc-docs/src/SUMMARY.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@
1111
- [RMC tutorial](./rmc-tutorial.md)
1212
- [First steps with RMC](./tutorial-first-steps.md)
1313
- [Failures that RMC can spot](./tutorial-kinds-of-failure.md)
14-
- [Loops, unwinding, and bounds]()
14+
- [Loops, unwinding, and bounds](./tutorial-loops-unwinding.md)
1515

1616
- [RMC developer documentation]()
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Loops, unwinding, and bounds
2+
3+
Consider code like this:
4+
5+
```rust
6+
{{#include tutorial/loops-unwinding/src/lib.rs:code}}
7+
```
8+
9+
This code has an off-by-one error that only occurs on the last iteration of the loop (when called with an input that will trigger it).
10+
We can try to find this bug with a proof harness like this:
11+
12+
```rust
13+
{{#include tutorial/loops-unwinding/src/lib.rs:rmc}}
14+
```
15+
16+
When we run RMC on this, we run into an unfortunate result: non-termination.
17+
This non-termination is caused by the model checker trying to unroll the loop an unbounded number of times.
18+
19+
> **NOTE:** Presently, [due to a bug](https://github.com/model-checking/rmc/issues/493), this is especially bad: we don't see any output at all.
20+
> You are supposed to see some log lines that might give some clue that an infinite loop is occurring.
21+
> If RMC doesn't terminate, it's almost always the problem that this section is covering, however.
22+
23+
To verify programs like this, we really need to do two things:
24+
25+
1. Create an upper bound on the size of the problem.
26+
We've actually already done part of this: our proof harness seems to be trying to set an upper limit of 10.
27+
2. Tell RMC about this limit, if it's not able to figure it out on its own.
28+
29+
> **NOTE:** In the future, RMC may eventually support specifying _loop invariants_, which allow us to do away with fixed upper bounds like this.
30+
> That support is not ready yet, however.
31+
32+
Bounding proofs like this means we may no longer be proving as much as we originally hoped.
33+
Who's to say, if we prove everything works up to size 10, that there isn't a novel bug lurking, expressible only with problems of size 11+?
34+
But, let's get back to the practical issue at hand.
35+
36+
We can "make progress" in our work by giving RMC a global bound on the problem size using the `--unwind <bound>` flag.
37+
This flag puts a fixed upper bound on loop unrolling.
38+
RMC will automatically generate verification conditions that help us understand if that bound isn't enough.
39+
Let's start with the "sledge hammer" by dropping all the way down to size 1:
40+
41+
```
42+
# rmc src/lib.rs --cbmc-args --unwind 1
43+
[.unwind.0] unwinding assertion loop 0: FAILURE
44+
VERIFICATION FAILED
45+
```
46+
47+
> **NOTE:** `--unwind` is a flag to the underlying model checker, CBMC, and so it needs to appear after `--cbmc-args`.
48+
> This flag `--cbmc-args` "switches modes" in the command line from RMC flags to CBMC flags, so we place all RMC flags and arguments before it.
49+
50+
This output is showing us two things:
51+
52+
1. RMC tells us we haven't unwound enough. This is the failure of the "unwinding assertion."
53+
2. We aren't seeing other failures if we only unroll the loop once.
54+
The execution can't progress far enough to reveal the bug we're interested in (which actually only happens in the last iteration of the loop).
55+
56+
Doing an initial `--unwind 1` is generally enough to force termination, but often too little to do any practical verification.
57+
58+
We were clearly aiming at a size limit of 10 in our proof harness, so let's try a few things here:
59+
60+
```
61+
# rmc src/lib.rs --cbmc-args --unwind 10 | grep FAIL
62+
[.unwind.0] unwinding assertion loop 0: FAILURE
63+
VERIFICATION FAILED
64+
```
65+
66+
A bound of 10 still isn't enough because we generally need to unwind one greater than the number of executed loop iterations:
67+
68+
```
69+
# rmc src/lib.rs --cbmc-args --unwind 11 | grep FAIL
70+
[initialize_prefix.unwind.0] line 11 unwinding assertion loop 0: FAILURE
71+
[initialize_prefix.assertion.2] line 12 index out of bounds: the length is move _20 but the index is _19: FAILURE
72+
[initialize_prefix.pointer_dereference.5] line 12 dereference failure: pointer outside object bounds in buffer.data[var_19]: FAILURE
73+
VERIFICATION FAILED
74+
```
75+
76+
We're still not seeing the unwinding assertion failure go away!
77+
This is because our error is really an off by one problem, we loop one too many times, so let's add one more:
78+
79+
```
80+
# rmc src/lib.rs --cbmc-args --unwind 12 | grep FAIL
81+
[initialize_prefix.assertion.2] line 12 index out of bounds: the length is move _20 but the index is _19: FAILURE
82+
[initialize_prefix.pointer_dereference.5] line 12 dereference failure: pointer outside object bounds in buffer.data[var_19]: FAILURE
83+
VERIFICATION FAILED
84+
```
85+
86+
RMC is now sure we've unwound the loop enough to verify our proof harness, and now we're seeing just the bound checking failures from the off by one error.
87+
88+
1. Exercise: Fix the off-by-one bounds error and get RMC to verify successfully.
89+
2. Exercise: After fixing the error, `--unwind 11` works. Why?
90+
91+
## Customizing individual loop bounds
92+
93+
Setting `--unwind` globally affects every loop.
94+
Once you know which loop is the culprit, it can sometimes be helpful to provide specific bounds on specific loops.
95+
96+
In the general case, specifying just the highest bound globally for all loops shouldn't cause any problems, except that the solver may take more time because _all_ loops will be unwound to the specified bound.
97+
98+
1. Exercise: Try increasing the unwind bound on the code from the previous section and then time how long solving takes.
99+
For example, we see 0.5s at unwinding 12, and 3s at unwinding 100.
100+
101+
> **NOTE:** RMC does not yet support annotating code with unwinding bounds.
102+
> What follows is a hacky way to make things happen, if you need it.
103+
104+
In situations where you need to optimize solving time better, specific bounds for specific loops can be provided on the command line.
105+
106+
```
107+
# rmc src/lib.rs --cbmc-args --show-loops
108+
[...]
109+
Loop _RNvCs6JP7pnlEvdt_3lib17initialize_prefix.0:
110+
file ./src/lib.rs line 11 column 5 function initialize_prefix
111+
112+
Loop _RNvMs8_NtNtCswN0xKFrR8r_4core3ops5rangeINtB5_14RangeInclusivejE8is_emptyCs6JP7pnlEvdt_3lib.0:
113+
file $RUST/library/core/src/ops/range.rs line 540 column 9 function std::ops::RangeInclusive::<Idx>::is_empty
114+
115+
Loop gen-repeat<[u8; 10]::16806744624734428132>.0:
116+
```
117+
118+
This command shows us the mangled names of the loops involved.
119+
Then we can specify the bound for specific loops by name, from the command line:
120+
121+
```
122+
rmc src/lib.rs --cbmc-args --unwindset _RNvCs6JP7pnlEvdt_3lib17initialize_prefix.0:12
123+
```
124+
125+
The general format of the `--unwindset` option is: `label_1:bound_1,label_2:bound_1,...`.
126+
The label is revealed by the output of `--show-loops` as we saw above.
127+
128+
## Summary
129+
130+
In this section:
131+
132+
1. We saw RMC fail to terminate.
133+
2. We saw how `--unwind 1` can "sledgehammer" RMC into terminating, possibly with additional and/or missing failures.
134+
3. We saw how "unwinding assertions" can warn us that we've set the unwinding limit too low.
135+
4. We saw how to put a practical bound on problem size in our proof harness.
136+
5. We saw how to pick an unwinding size large enough to successfully verify that bounded proof.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0 OR MIT
3+
[package]
4+
name = "loops-unwinding"
5+
version = "0.1.0"
6+
edition = "2018"
7+
8+
[dependencies]
9+
10+
[workspace]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
// ANCHOR: code
5+
fn initialize_prefix(length: usize, buffer: &mut [u8]) {
6+
// Let's just ignore invalid calls
7+
if length > buffer.len() {
8+
return;
9+
}
10+
11+
for i in 0..=length {
12+
buffer[i] = 0;
13+
}
14+
}
15+
// ANCHOR_END: code
16+
17+
fn __nondet<T>() -> T {
18+
unimplemented!()
19+
}
20+
fn __VERIFIER_assume(cond: bool) {
21+
unimplemented!()
22+
}
23+
24+
// ANCHOR: rmc
25+
#[cfg(rmc)]
26+
#[no_mangle]
27+
fn main() {
28+
const LIMIT: usize = 10;
29+
let mut buffer: [u8; LIMIT] = [1; LIMIT];
30+
31+
let length = __nondet();
32+
__VERIFIER_assume(length <= LIMIT);
33+
34+
initialize_prefix(length, &mut buffer);
35+
}
36+
// ANCHOR_END: rmc

0 commit comments

Comments
 (0)