Skip to content

Commit cd87839

Browse files
authored
Revise 'Kind of Failure' tutorial (rust-lang#1337)
1 parent d84b123 commit cd87839

File tree

1 file changed

+81
-50
lines changed

1 file changed

+81
-50
lines changed

docs/src/tutorial-kinds-of-failure.md

Lines changed: 81 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Failures that Kani can spot
22

3-
In the [last section](./tutorial-first-steps.md) we saw Kani spot two major kinds of failures: Assertions and panics.
3+
In the [last section](./tutorial-first-steps.md), we saw Kani spot two major kinds of failures: assertions and panics.
44
If the proof harness allows some program execution that results in a panic, then Kani will report that as a failure.
5-
In addition, we saw very briefly a couple of other kinds of failures: Null pointer dereferences and overflows.
5+
In addition, we saw (very briefly) a couple of other kinds of failures: null pointer dereferences and overflows.
66
In this section, we're going to expand on these additional checks, to give you an idea of what other problems Kani will find.
77

88
## Bounds checking and pointers
@@ -20,7 +20,7 @@ We can again write a simple property test against this code:
2020
{{#include tutorial/kinds-of-failure/src/bounds_check.rs:proptest}}
2121
```
2222

23-
This property test will immediately find the failing case because of this dynamic check.
23+
This property test will immediately find a failing case, thanks to Rust's built-in bounds checking.
2424

2525
But what if we change this function to use unsafe Rust?
2626

@@ -36,81 +36,96 @@ Now the error becomes invisible to this test:
3636
test bounds_check::tests::doesnt_crash ... ok
3737
```
3838

39-
But we're able to write a harness for this unsafe code:
39+
The property test still causes an out-of-bounds access, but this undefined behavior does not necessarily cause an immediate crash.
40+
(This is part of why undefined behavior is so difficult to debug.)
41+
Through the use of unsafe code, we removed the runtime check for an out of bounds access.
42+
It just turned out that none of the randomly generated tests triggered behavior that actually crashed.
43+
But if we write a Kani proof harness:
4044

4145
```rust
4246
{{#include tutorial/kinds-of-failure/src/bounds_check.rs:kani}}
4347
```
4448

45-
If we run it with Kani:
49+
And run this proof with:
50+
4651
```bash
47-
kani src/bounds_check.rs --harness bound_check
52+
cargo kani --harness bound_check
4853
```
4954

50-
One of the checks fails. Also, notice there were many checks in the verification output.
51-
This is a result of using the standard library `Vec` implementation, which means our harness actually used quite a bit of code, short as it looks.
52-
Kani is inserting a lot more checks than appear as asserts in our code, so the output can be large.
53-
Let's narrow that output down a bit:
55+
We still see a failure from Kani, even without Rust's runtime bounds checking.
56+
57+
> Also, notice there were many checks in the verification output.
58+
> (At time of writing, 351.)
59+
> This is a result of using the standard library `Vec` implementation, which means our harness actually used quite a bit of code, short as it looks.
60+
> Kani is inserting a lot more checks than appear as asserts in our code, so the output can be large.
61+
62+
We get the following summary at the end:
5463

5564
```
56-
# kani src/bounds_check.rs --harness bound_check | grep Failed
65+
SUMMARY:
66+
** 1 of 351 failed
5767
Failed Checks: dereference failure: pointer outside object bounds
68+
File: "./src/bounds_check.rs", line 11, in bounds_check::get_wrapped
69+
70+
VERIFICATION:- FAILED
5871
```
5972

6073
Notice that, for Kani, this has gone from a simple bounds-checking problem to a pointer-checking problem.
6174
Kani will check operations on pointers to ensure they're not potentially invalid memory accesses.
62-
Any unsafe code that manipulates pointers will, as we see here, raise failures if its behavior is actually unsafe.
75+
Any unsafe code that manipulates pointers will, as we see here, raise failures if its behavior is actually a problem.
6376

6477
Consider trying a few more small exercises with this example:
6578

66-
1. Exercise: Switch back to the normal/safe indexing operation and re-try Kani. What changes compared to the unsafe operation and why?
79+
1. Exercise: Switch back to the normal/safe indexing operation and re-try Kani.
80+
How does Kani's output change, compared to the unsafe operation?
6781
(Try predicting the answer, then seeing if you got it right.)
6882
2. Exercise: [Remember how to get a trace from Kani?](./tutorial-first-steps.md#getting-a-trace) Find out what inputs it failed on.
6983
3. Exercise: Fix the error, run Kani, and see a successful verification.
70-
4. Exercise: Try switching back to the unsafe code (now with the error fixed) and re-run Kani. It should still successfully verify.
84+
4. Exercise: Try switching back to the unsafe code (now with the error fixed) and re-run Kani. Does it still verify successfully?
7185

7286
<details>
7387
<summary>Click to see explanation for exercise 1</summary>
7488

7589
Having switched back to the safe indexing operation, Kani reports two failures:
7690

7791
```
78-
# kani src/bounds_check.rs --harness bound_check | grep Failed
92+
SUMMARY:
93+
** 2 of 350 failed
7994
Failed Checks: index out of bounds: the length is less than or equal to the given index
95+
File: "./src/bounds_check.rs", line 11, in bounds_check::get_wrapped
8096
Failed Checks: dereference failure: pointer outside object bounds
97+
File: "./src/bounds_check.rs", line 11, in bounds_check::get_wrapped
98+
99+
VERIFICATION:- FAILED
81100
```
82101

83-
The first is Rust's implicit assertion for the safe indexing operation.
102+
The first is Rust's runtime bounds checking for the safe indexing operation.
84103
The second is Kani's check to ensure the pointer operation is actually safe.
85-
This pattern (two checks for similar issues in safe Rust code) is common, and we'll see it again in the next section.
104+
This pattern (two checks for similar issues in safe Rust code) is common to see, and we'll see it again in the next section.
105+
106+
> **NOTE**: While Kani will always be checking for both properties, [in the future the output here may change](https://github.com/model-checking/kani/issues/1349).
107+
> You might have noticed that the bad pointer dereference can't happen, since the bounds check would panic first.
108+
> In the future, Kani's output may report only the bounds checking failure in this example.
86109
87110
</details>
88111

89112
<details>
90113
<summary>Click to see explanation for exercise 2</summary>
91114

92-
Having run `kani --visualize` and clicked on one of the failures to see a trace, there are three things to immediately notice:
115+
Having run `cargo kani --harness bound_check --visualize` and clicked on one of the failures to see a trace, there are three things to immediately notice:
93116

94117
1. This trace is huge. Because the standard library `Vec` is involved, there's a lot going on.
95118
2. The top of the trace file contains some "trace navigation tips" that might be helpful in navigating the trace.
96119
3. There's a lot of generated code and it's really hard to just read the trace itself.
97120

98-
To navigate this trace to find the information you need, we recommend searching for things you expect to be somewhere in the trace:
121+
To navigate this trace to find the information you need, we again recommend searching for things you expect to be somewhere in the trace:
99122

100-
1. Search the document for `kani::any` or `<variable_of_interest> =` such as `size =`.
123+
1. Search the page for `kani::any` or `<variable_of_interest> =` such as `size =` or `let size`.
101124
We can use this to find out what example values lead to a problem.
102125
In this case, where we just have a couple of `kani::any` values in our proof harness, we can learn a lot just by seeing what these are.
103126
In this trace we find (and the values you get may be different):
104127

105128
```
106-
Step 23: Function bound_check, File src/bounds_check.rs, Line 43
107-
let size: usize = kani::any();
108-
size = 0ul
109-
110-
Step 27: Function bound_check, File src/bounds_check.rs, Line 45
111-
let index: usize = kani::any();
112-
index = 0ul
113-
114129
Step 36: Function bound_check, File src/bounds_check.rs, Line 43
115130
let size: usize = kani::any();
116131
size = 2464ul
@@ -120,13 +135,13 @@ let index: usize = kani::any();
120135
index = 2463ul
121136
```
122137

123-
Try not to be fooled by the first assignments: we're seeing zero-initialization there.
124-
Their values are overwritten by the later assignments.
125138
You may see different values here, as it depends on the solver's behavior.
126139

127-
2. Try searching for "failure:". This will be near the end of the document.
128-
Now you can try reverse-searching for assignments to the variables involved.
129-
For example, search upwards from the failure for `i =`.
140+
2. Try searching for `failure:`. This will be near the end of the page.
141+
You can now search upwards from a failure to see what values certain variables had.
142+
Sometimes it can be helpful to change the source code to add intermediate variables, so their value is visible in the trace.
143+
For instance, you might want to compute the index before indexing into the array.
144+
That way you'd see in the trace exactly what value is being used.
130145

131146
These two techniques should help you find both the nondeterministic inputs, and the values that were involved in the failing assertion.
132147

@@ -145,9 +160,10 @@ fn get_wrapped(i: usize, a: &[u32]) -> u32 {
145160
We've corrected the out-of-bounds access, but now we've omitted the "base case": what to return on an empty list.
146161
Kani will spot this not as a bound error, but as a mathematical error: on an empty list the modulus operator (`%`) will cause a division by zero.
147162

148-
1. Exercise: Try to run Kani on the above, to see what this kind of failure looks like.
163+
1. Exercise: Try to run Kani on this version of `get_wrapped`, to see what this kind of failure looks like.
149164

150-
Rust also performs runtime safety checks for integer overflows, much like it does for bounds checks.
165+
Rust can also perform runtime safety checks for integer overflows, much like it does for bounds checks.
166+
([Though Rust disables this by default in `--release` mode, it can be re-enabled.](https://doc.rust-lang.org/reference/expressions/operator-expr.html#overflow))
151167
Consider this code (available [here](https://github.com/model-checking/kani/blob/main/docs/src/tutorial/kinds-of-failure/src/overflow.rs)):
152168

153169
```rust
@@ -159,28 +175,33 @@ Kani will find these failures as well.
159175
Here's the output from Kani:
160176

161177
```
162-
# kani src/overflow.rs --harness add_overflow
163-
[...]
164-
RESULTS:
165-
Check 1: simple_addition.assertion.1
166-
- Status: FAILURE
167-
- Description: "attempt to add with overflow"
178+
# cargo kani --harness add_overflow
168179
[...]
180+
SUMMARY:
181+
** 1 of 2 failed
182+
Failed Checks: attempt to add with overflow
183+
File: "./src/overflow.rs", line 7, in overflow::simple_addition
184+
169185
VERIFICATION:- FAILED
170186
```
171187

172188
This issue can be fixed using Rust's alternative mathematical functions with explicit overflow behavior.
173-
For instance, instead of `a + b` write `a.wrapping_add(b)`.
189+
For instance, if the wrapping behavior is intended, you can write `a.wrapping_add(b)` instead of `a + b`.
190+
Kani will then report no issues.
174191

175192
### Exercise: Classic overflow failure
176193

177-
One of the classic subtle bugs that persisted in many implementations for a very long time is finding the midpoint in quick sort.
194+
A classic example of a subtle bug that persisted in many implementations for a very long time is "finding the midpoint" in quick sort.
178195
This often naively looks like this (code available [here](https://github.com/model-checking/kani/blob/main/docs/src/tutorial/kinds-of-failure/src/overflow_quicksort.rs)):
179196

180197
```rust
181198
{{#include tutorial/kinds-of-failure/src/overflow_quicksort.rs:code}}
182199
```
183200

201+
```
202+
cargo kani --harness midpoint_overflow
203+
```
204+
184205
Kani immediately spots the bug in the above code.
185206

186207
1. Exercise: Fix this function so it no longer overflows.
@@ -198,24 +219,34 @@ return low + (high - low) / 2;
198219
```
199220

200221
But if you naively try this (try it!), you'll find a new underflow error: `high - low` might result in a negative number, but has type `u32`.
201-
Hence, the need to add an assumption that would make that impossible.
202-
(Adding an assumption, though, means there's a new way to "use it wrong." Perhaps we'd like to avoid that!)
222+
Hence, the need to add the assumption we suggested above, to make that impossible.
223+
(Adding an assumption, though, means there's a new way to "use it wrong." Perhaps we'd like to avoid that! Can you avoid the assumption?)
203224

204225
After that, you might wonder how to "prove your new implementation correct."
205226
After all, what does "correct" even mean?
206227
Often we're using a good approximation of correct, such as the equivalence of two implementations (often one much "simpler" than the other somehow).
207-
Here's one possible assertion to make that obvious:
228+
Here's one possible assertion we could write in the proof harness:
208229

209230
```rust
210231
assert!(result as u64 == (a as u64 + b as u64) / 2);
211232
```
212233

213-
Since this implementation is just the original one, but cast to a wider unsigned integer type, it should have the same result but without overflowing.
214-
When Kani tells us both of these methods yield the same exact result, that gives us additional confidence that we haven't overlooked something.
234+
You might have even come up with this approach to avoiding the overflow issue in the first place!
235+
Having two different implementations, using different approaches, but proven to yield the same results, gives us greater confidence that we compute the correct result.
215236

216237
</details>
217238

218239
## Failures that Kani cannot spot
219240

220-
Check out [Limitations](./limitations.md) for information on the checks that
221-
Kani doesn't perform.
241+
Check out [Limitations](./limitations.md) for information on the checks that Kani does not perform.
242+
Notably, Kani is not prioritizing all Rust-specific notions of undefined behavior.
243+
244+
## Summary
245+
246+
In this section:
247+
248+
1. We saw Kani spot out-of-bounds accesses.
249+
2. We saw Kani spot actually-unsafe dereferencing of a raw pointer to invalid memory.
250+
3. We got more experience reading the traces that Kani generates, to debug a failing proof harness.
251+
3. We saw Kani spot a division by zero error and an overflowing addition.
252+
5. As an exercise, we tried proving an assertion (finding the midpoint) that was not completely trivial.

0 commit comments

Comments
 (0)