|
| 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. |
0 commit comments