Skip to content

Commit 191a3f7

Browse files
Create RFC for MIR linker (rust-lang#1600)
Co-authored-by: Zyad Hassan <[email protected]>
1 parent 61c3da7 commit 191a3f7

File tree

3 files changed

+257
-0
lines changed

3 files changed

+257
-0
lines changed

rfc/book.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ src = "src"
1212
site-url = "/kani/rfc/"
1313
git-repository-url = "https://github.com/model-checking/kani"
1414
edit-url-template = "https://github.com/model-checking/kani/edit/main/rfc/{path}"
15+
no-section-label = true
1516

1617
[output.html.playground]
1718
runnable = false

rfc/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@
66

77
# Kani RFCs
88

9+
- [0001-mir-linker](rfcs/0001-mir-linker.md)
910

rfc/src/rfcs/0001-mir-linker.md

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
- **Feature Name:** MIR Linker (mir_linker)
2+
- **RFC Tracking Issue**: <https://github.com/model-checking/kani/issues/1588>
3+
- **RFC PR:** <https://github.com/model-checking/kani/pull/1600>
4+
- **Status:** Under Review
5+
- **Version:** 0
6+
7+
-------------------
8+
9+
## Summary
10+
11+
Fix linking issues with the rust standard library in a scalable manner by only generating goto-program for
12+
code that is reachable from the user harnesses.
13+
14+
## User Impact
15+
16+
The main goal of this RFC is to enable Kani users to link against all supported constructs from the `std` library.
17+
Currently, Kani will only link to items that are either generic or have an inline annotation.
18+
19+
The approach introduced in this RFC will have the following secondary benefits.
20+
- Reduce spurious warnings about unsupported features for cases where the feature is not reachable from any harness.
21+
- In the verification mode, we will likely see a reduction on the compilation time and memory consumption
22+
by pruning the inputs of symtab2gb and goto-instrument.
23+
- Compared to linking against the standard library goto-models that take more than 5 GB.
24+
- In a potential assessment mode, only analyze code that is reachable from all public items in the target crate.
25+
26+
One downside is that if we decide to include a pre-compiled version of the std, our release bundle will double in size
27+
(See [Rational and Alternatives](0001-mir-linker.md#rational-and-alternatives)
28+
for more information on the size overhead).
29+
This will negatively impact the time taken to set up Kani
30+
(triggered by either the first time a user invokes `kani | cargo-kani` , or explicit invoke the subcommand `setup`).
31+
32+
## User Experience
33+
34+
Once this RFC has been stabilized users shall use Kani in the same manner as they have been today.
35+
Until then, we wil add an unstable option `--mir-linker` to enable the cross-crate reachability analysis
36+
and the generation of the goto-program only when compiling the target crate.
37+
38+
Kani setup will likely take longer and more disk space as mentioned in the section above.
39+
This change will not be guarded by `--mir-linker` option above.
40+
41+
## Detailed Design
42+
43+
In a nutshell, we will no longer generate a goto-program for every crate we compile.
44+
Instead, we will generate the MIR for every crate, and we will generate only one goto-program.
45+
This model will only include items reachable from the target crate's harnesses.
46+
47+
The current system flow for a crate verification is the following (Kani here represents either `kani | cargo-kani`
48+
executable):
49+
50+
1. Kani compiles the user crate as well as all its dependencies.
51+
For every crate compiled, `kani-compiler` will generate a goto-program.
52+
This model includes everything reachable from the crate's public functions.
53+
2. After that, Kani links all models together by invoking `goto-cc`.
54+
This step will also link against Kani's `C` library.
55+
3. For every harness, Kani invokes `goto-instrument` to prune the linked model to only include items reachable from the given harness.
56+
4. Finally, Kani instruments and verify each harness model via `goto-instrument` and `cbmc` calls.
57+
58+
After this RFC, the system flow would be slightly different:
59+
60+
1. Kani compiles the user crate dependencies up to the MIR translation.
61+
I.e., for every crate compiled, `kani-compiler` will generate an artifact that includes the MIR representation
62+
of all items in the crate.
63+
2. Kani will generate the goto-program only while compiling the target user crate.
64+
It will generate one goto-program that includes all items reachable from any harness in the target crate.
65+
3. `goto-cc` will still be invoked to link the generated model against Kani's `C` library.
66+
4. Steps #3 and #4 above will be performed without any change.
67+
68+
This feature will require three main changes to Kani which are detailed in the sub-sections below.
69+
70+
### Kani's Sysroot
71+
72+
Kani currently uses `rustup` sysroot to gather information from the standard library constructs.
73+
The artifacts from this `sysroot` include the MIR for generic items as well as for items that may be included in
74+
a crate compilation (e.g.: functions marked with `#[inline]` annotation).
75+
The artifacts do not include the MIR for items that have already been compiled to the `std` shared library.
76+
This leaves a gap that cannot be filled by the `kani-compiler`;
77+
thus, we are unable to translate these items into goto-program.
78+
79+
In order to fulfill this gap, we must compile the standard library from scratch.
80+
This RFC proposes a similar method to what [`MIRI`](https://github.com/rust-lang/miri) implements.
81+
We will generate our own sysroot using the `-Z always-encode-mir` compilation flag.
82+
This sysroot will be pre-compiled and included in our release bundle.
83+
84+
We will compile `kani`'s libraries (`kani` and `std`) also with `-Z always-encode-mir`
85+
and with the new sysroot.
86+
87+
88+
### Cross-Crate Reachability Analysis
89+
90+
`kani-compiler` will include a new `reachability` module to traverse over the local and external MIR items.
91+
This module will `monomorphize` all generic code as it's performing the traversal.
92+
93+
The traversal logic will be customizable allowing different starting points to be used.
94+
The two options to be included in this RFC is starting from all local harnesses
95+
(tagged with `#[kani::proof]`) and all public functions in the local crate.
96+
97+
The `kani-compiler` behavior will be customizable via a new flag:
98+
99+
```
100+
--reachability=[ harnesses | pub_fns | none | legacy ]
101+
```
102+
103+
where:
104+
105+
- `harnesses`: Use the local harnesses as the starting points for the reachability analysis.
106+
- `pub_fns`: Use the public local functions as the starting points for the reachability analysis.
107+
- `none`: This will be the default value if `--reachability` flag is not provided. It will skip
108+
reachability analysis. No goto-program will be generated.
109+
This will be used to compile dependencies up to the MIR level.
110+
`kani-compiler` will still generate artifacts with the crate's MIR.
111+
- `legacy`: Keep `kani-compiler` current behavior by using
112+
`rustc_monomorphizer::collect_and_partition_mono_items()` which respects the crate boundary.
113+
This will generate a goto-program for each crate compiled by `kani-compiler`, and it will still have the same
114+
`std` linking issues.
115+
*This option will be removed as part of the `rfc` stabilization.*
116+
117+
These flags will not be exposed to the final user.
118+
They will only be used for the communication between `kani-driver` and `kani-compiler`.
119+
120+
### Dependencies vs Target Crate Compilation
121+
122+
The flags described in the section above will be used by `kani-driver` to implement the new system flow.
123+
For that, we propose the following mechanism:
124+
125+
- For standalone `kani`, we will pass the option `--reachability=harnesses` to `kani-compiler`.
126+
- For `cargo-kani`, we will replace
127+
```
128+
cargo build <FLAGS>
129+
```
130+
131+
with:
132+
133+
```
134+
cargo rustc <FLAGS> -- --reachability=harnesses
135+
```
136+
137+
to build everything.
138+
This command will compile all dependencies without the `--reachability` argument, and it will only pass `harnesses`
139+
value to the compiler when compiling the target crate.
140+
141+
## Rational and Alternatives
142+
143+
Not doing anything is not an alternative, since this fixes a major gap in Kani's usability.
144+
145+
### Benefits
146+
147+
- The MIR linker will allow us to fix the linking issues with Rust's standard library.
148+
- Once stabilized, the MIR linker will be transparent to the user.
149+
- It will enable more powerful and precise static analysis to `kani-compiler`.
150+
- It won't require any changes to our dependencies.
151+
- This will fix the harnesses' dependency on the`#[no_mangle]` annotation
152+
([Issue-661](https://github.com/model-checking/kani/issues/661)).
153+
154+
### Risks
155+
156+
Failures in the linking stage would not impact the tool soundness. I anticipate the following failure scenarios:
157+
- ICE (Internal compiler error): Some logic is incorrectly implemented and the linking stage crashes.
158+
Although this is a bad experience for the user, this will not impact the verification result.
159+
- Missing items: This would either result in ICE during code generation or a verification failure if the missing
160+
item is reachable.
161+
- Extra items: This shouldn't impact the verification results, and they should be pruned by CBMC's reachability
162+
analysis.
163+
This is already the case today. In extreme cases, this could include a symbol that we cannot compile and cause an ICE.
164+
165+
The new reachability code would be highly dependent on the `rustc` unstable APIs, which could increase
166+
the cost of the upstream synchronization.
167+
That said, the APIs that would be required are already used today.
168+
169+
Finally, this implementation relies on a few unstable options from `cargo` and `rustc`.
170+
These APIs are used by other tools such as MIRI, so we don't see a high risk that they would be removed.
171+
172+
### Alternatives
173+
174+
The other options explored were:
175+
1. Pre-compile the standard library, and the kani library, and ship the generated `*symtab.json` files.
176+
2. Pre-compile the standard library, and the kani library, convert the standard library and dependencies to goto-program
177+
(via`symtab2gb`) and link them into one single goto-program.
178+
Ship the generated model.
179+
180+
Both would still require shipping the compiler metadata (via `rlib` or `rmeta`) for the kani library, its
181+
dependencies, and `kani_macro.so`.
182+
183+
Both alternatives are very similar. They only differ on the artifact that would be shipped.
184+
They require generating and shipping a custom `sysroot`;
185+
however, there is no need to implement the reachability algorithm.
186+
187+
We implemented a prototype for the MIR linker and one for the alternatives.
188+
Both prototypes generate the sysroot as part of the `cargo kani` flow.
189+
190+
We performed a small experiment (on a `c5.4xlarge` ec2 instance running Ubuntu 20.04) to assess the options.
191+
192+
For this experiment, we used the following harness:
193+
```rust
194+
#[kani::proof]
195+
#[kani::unwind(4)]
196+
pub fn check_format() {
197+
assert!("2".parse::<u32>().unwrap() == 2);
198+
}
199+
```
200+
The experiment showed that the MIR linker approach is much more efficient.
201+
202+
See the table bellow for the breakdown of time (in seconds) taken for each major step of
203+
the harness verification:
204+
205+
206+
| Stage | MIR Linker | Alternative |
207+
----------------------------|------------|-------------|
208+
| compilation | 22.2s | 64.7s |
209+
| goto-program generation | 2.4s | 90.7s |
210+
| goto-program linking | 0.8s | 33.2s |
211+
| code instrumentation | 0.8s | 33.1 |
212+
| verification | 0.5s | 8.5s |
213+
214+
It is possible that `goto-cc` time can be improved, but this would also require further experimentation and
215+
expertise that we don't have today.
216+
217+
Every option would require a custom sysroot to either be built or shipped with Kani.
218+
The table below shows the size of the sysroot files for the alternative #2
219+
(goto-program files) vs compiler artifacts (`*.rmeta` files)
220+
files with `-Z always-encode-mir` for `x86_64-unknown-linux-gnu` (on Ubuntu 18.04).
221+
222+
| File Type | Raw size | Compressed size |
223+
|----------------|----------|-----------------|
224+
| `symtab.json` | 950M | 26M |
225+
| `symtab.out` | 84M | 24M |
226+
| `*.rmeta` | 92M | 25M |
227+
228+
These results were obtained by looking at the artifacts generated during the same experiment.
229+
230+
## Open questions
231+
232+
- Should we build or download the sysroot during `kani setup`?
233+
- What's the best way to enable support to run Kani in the entire `workspace`?
234+
- One possibility is to run `cargo rustc` per package.
235+
- Should we codegen all static items no matter what? Static objects can only be initialized via constant function.
236+
Thus, it shouldn't have any side effect.
237+
That relies on all constant initializers being evaluated during compilation.
238+
- What's the best way to handle `cargo kani --tests`?
239+
We still want to restrict the reachability and codegen to the last compilation step.
240+
Possible solutions that we considered so far:
241+
1. Add a new value to `--reachability`, maybe "on-tests" or something like that.
242+
This would behave as either "harness" or "none" depending whether RUSTFLAGS has `--tests`.
243+
2. Change how we handle not having `--reachability` argument to also take into account the existence of `--tests`.
244+
245+
246+
## Future possibilities
247+
248+
- Split the goto-program into two or more items to optimize compilation result caching.
249+
- Dependencies: One model will include items from all the crate dependencies.
250+
This model will likely be more stable and require fewer updates.
251+
- Target crate: The model for all items in the target crate.
252+
- Do the analysis per-harness. This might be adequate once we have a mechanism to cache translations.
253+
- Add an option to include external functions to the analysis starting point in order to enable verification when
254+
calls are made from `C` to `rust`.
255+
- Contribute the reachability analysis code back to upstream.

0 commit comments

Comments
 (0)