|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: Const Eval Safety Rules |
| 4 | +author: Felix Klock |
| 5 | +description: "Various ways const-eval can change between Rust versions" |
| 6 | +team: The Compiler Team <https://www.rust-lang.org/governance/teams/compiler> |
| 7 | +--- |
| 8 | + |
| 9 | +In a recent Rust issue ([#99923][]), a developer noted that the upcoming |
| 10 | +1.64-beta version of Rust had started signalling errors on their crate, |
| 11 | +[`icu4x`][icu4x]. The `icu4x` crate makes heavy use of *const-eval*: Rust code |
| 12 | +that is run at compile-time but produces values that may end up embedded in the |
| 13 | +final object code that executes at runtime. |
| 14 | + |
| 15 | +<!-- |
| 16 | +
|
| 17 | +(This is distinct from procedural macros, which are Rust code that runs at |
| 18 | +compile-time to manipulate *program syntax*; syntactic values are not usually |
| 19 | +embedded into the final object code.) |
| 20 | +
|
| 21 | +--> |
| 22 | + |
| 23 | +[#99923]: https://github.com/rust-lang/rust/issues/99923 |
| 24 | + |
| 25 | +[icu4x]: https://github.com/unicode-org/icu4x |
| 26 | + |
| 27 | +## A new diagnostic to watch for |
| 28 | + |
| 29 | +The problem, reduced over the course of the [comment thread of #99923][repro |
| 30 | +comment], is that certain static initialization expressions (see below) are |
| 31 | +defined as having undefined behavior (UB) *at compile time* ([playground][repro |
| 32 | +playground]): |
| 33 | + |
| 34 | +[repro comment]: https://github.com/rust-lang/rust/issues/99923#issuecomment-1200284482 |
| 35 | + |
| 36 | +[repro playground]: https://play.rust-lang.org/?version=beta&mode=debug&edition=2021&gist=67a917fc4f2a4bf2eb72aebf8dad0fe9 |
| 37 | + |
| 38 | +```rust |
| 39 | +pub static FOO: () = unsafe { |
| 40 | + let illegal_ptr2int: usize = std::mem::transmute(&()); |
| 41 | + let _copy = illegal_ptr2int; |
| 42 | +}; |
| 43 | +``` |
| 44 | + |
| 45 | +(Many thanks to `@eddyb` for the minimal reproduction!) |
| 46 | + |
| 47 | +The code above was accepted by Rust versions 1.63 and earlier, but in the Rust |
| 48 | +1.64-beta, it now causes a compile time error with the following message: |
| 49 | + |
| 50 | +``` |
| 51 | +error[E0080]: could not evaluate static initializer |
| 52 | + --> demo.rs:3:17 |
| 53 | + | |
| 54 | +3 | let _copy = illegal_ptr2int; |
| 55 | + | ^^^^^^^^^^^^^^^ unable to turn pointer into raw bytes |
| 56 | + | |
| 57 | + = help: this code performed an operation that depends on the underlying bytes representing a pointer |
| 58 | + = help: the absolute address of a pointer is not known at compile-time, so such operations are not supported |
| 59 | +``` |
| 60 | + |
| 61 | +As the message says, this operation is not supported: the `transmute` is trying |
| 62 | +to above is trying to reinterpret the memory address `&()` as an integer of type |
| 63 | +`usize`. The compiler cannot predict what memory address the `()` would be |
| 64 | +associated with at execution time, so it refuses to allow that reinterpretation. |
| 65 | + |
| 66 | +## What is new here |
| 67 | + |
| 68 | +You might be thinking: "it *used to be* accepted; therefore, there must be some |
| 69 | +value for the memory address that the previous version of the compiler was using |
| 70 | +here." |
| 71 | + |
| 72 | +But such reasoning would be based on an imprecise view of what the Rust compiler |
| 73 | +was doing here. |
| 74 | + |
| 75 | +The const-eval machinery of the Rust compiler is built upon the MIR-interpreter |
| 76 | +[Miri][], which uses an *abstract model* of a hypothetical machine as the |
| 77 | +foundation for evaluating such expressions. This abstract model doesn't have to |
| 78 | +represent memory addresses as mere integers; in fact, to support Miri's |
| 79 | +fine-grained checking for UB, it uses a much richer datatype for |
| 80 | +the values that are held in the abstract memory store. |
| 81 | + |
| 82 | +[Miri]: https://github.com/rust-lang/miri#readme |
| 83 | + |
| 84 | +The details of Miri's value representation do not matter too much for our |
| 85 | +discussion here. We merely note that earlier versions of the compiler silently |
| 86 | +accepted expressions that *seemed to* transmute memory addresses into integers, |
| 87 | +copied them around, and then transmuted them back into addresses; but that was |
| 88 | +not what was acutally happening under the hood. Instead, what was happening was |
| 89 | +that the Miri values were passed around blindly (after all, the whole point of |
| 90 | +transmute is that it does no transformation on its input value, so it is a no-op |
| 91 | +in terms of its operational semantics). |
| 92 | + |
| 93 | +The fact that it was passing a memory address into a context where you would |
| 94 | +expect there to always be an integer value would only be caught, if at all, at |
| 95 | +some later point. |
| 96 | + |
| 97 | +For example, the const-eval machinery rejects code that attempts to embed the |
| 98 | +transmuted pointer into a value that could be used by runtime code, like so ([playpen][embed play]): |
| 99 | + |
| 100 | +[embed play]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=48456e8bd028c6aa5c80a1962d7e4fb8 |
| 101 | + |
| 102 | +```rust |
| 103 | +pub static FOO: usize = unsafe { |
| 104 | + let illegal_ptr2int: usize = std::mem::transmute(&()); |
| 105 | + illegal_ptr2int |
| 106 | +}; |
| 107 | +``` |
| 108 | + |
| 109 | +Likewise, it rejects code that attempts to *perform arithmetic* on that |
| 110 | +non-integer value, like so ([playground][arith play]): |
| 111 | + |
| 112 | +[arith play]: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=74a35dd6ff93c86bd38c1a0006f2fc41 |
| 113 | + |
| 114 | +```rust |
| 115 | +pub static FOO: () = unsafe { |
| 116 | + let illegal_ptr2int: usize = std::mem::transmute(&()); |
| 117 | + let _incremented = illegal_ptr2int + 1; |
| 118 | +}; |
| 119 | +``` |
| 120 | + |
| 121 | +Both of the latter two variants are rejected in stable Rust, and have been for |
| 122 | +as long as Rust has accepted pointer-to-integer conversions in static |
| 123 | +initializers (see e.g. Rust 1.52). |
| 124 | + |
| 125 | +## More similar than different |
| 126 | + |
| 127 | +In fact, *all* of the examples provided above are exhibiting *undefined |
| 128 | +behavior* according to the semantics of Rust's const-eval system. |
| 129 | + |
| 130 | +The first example with `_copy` was accepted in Rust versions 1.46 through 1.63 |
| 131 | +because of Miri implementation artifacts. Miri puts considerable effort into |
| 132 | +detecting UB, but does not catch all instances of it. Furthermore, by default, |
| 133 | +Miri's detection can be delayed to a point far after where the actual |
| 134 | +problematic expression is found. |
| 135 | + |
| 136 | +But with nightly Rust, we can opt into extra checks for UB that Miri provides, |
| 137 | +by passing the unstable flag `-Z extra-const-ub-checks`. If we do that, then for |
| 138 | +*all* of the above examples we get the same result: |
| 139 | + |
| 140 | +``` |
| 141 | +error[E0080]: could not evaluate static initializer |
| 142 | + --> demo.rs:2:34 |
| 143 | + | |
| 144 | +2 | let illegal_ptr2int: usize = std::mem::transmute(&()); |
| 145 | + | ^^^^^^^^^^^^^^^^^^^^^^^^ unable to turn pointer into raw bytes |
| 146 | + | |
| 147 | + = help: this code performed an operation that depends on the underlying bytes representing a pointer |
| 148 | + = help: the absolute address of a pointer is not known at compile-time, so such operations are not supported |
| 149 | +``` |
| 150 | + |
| 151 | +The earlier examples had diagnostic output that put the blame in a misleading |
| 152 | +place. With the more precise checking `-Z extra-const-ub-checks` enabled, the |
| 153 | +compiler highlights the expression where we can first witness UB: the original |
| 154 | +transmute itself! (Which was stated at the outset of this post; here we are just |
| 155 | +pointing out that these tools can pinpoint the injection point more precisely.) |
| 156 | + |
| 157 | +## Change is hard |
| 158 | + |
| 159 | +You might well be wondering at this point: "Wait, when *is* it okay to transmute |
| 160 | +a pointer to a `usize` during const evaluation?" And the answer is: "Never. It |
| 161 | +has always been undefined behavior, ever since const-eval added support for |
| 162 | +`transmute` and `union`." You can read more about this in the |
| 163 | +`const_fn_`{`transmute`,`union`} [stabilization report][cftu report], |
| 164 | +specifically the subsection entitled "Pointer-integer-transmutes". |
| 165 | + |
| 166 | +[cftu report]: https://github.com/rust-lang/rust/pull/85769#issuecomment-854363720 |
| 167 | + |
| 168 | +Thus, we can see that the classification of this as UB during const evaluation |
| 169 | +is not a new thing at all. The only change here was that Miri had some internal |
| 170 | +changes that made it start detecting the UB rather than silently ignoring it. |
| 171 | + |
| 172 | +So we see that the Rust compiler has a shifting notion of what UB it will |
| 173 | +explicitly catch. We anticipated this: RFC 3016, "const UB", explicitly |
| 174 | +[says](https://github.com/rust-lang/rfcs/blob/master/text/3016-const-ub.md#guide-level-explanation): |
| 175 | + |
| 176 | +> [...] there is no guarantee that UB is reliably detected during CTFE. This can |
| 177 | +> change from compiler version to compiler version: CTFE code that causes UB |
| 178 | +> could build fine with one compiler and fail to build with another. (This is in |
| 179 | +> accordance with the general policy that unsound code is not subject to |
| 180 | +> stability guarantees.) |
| 181 | +
|
| 182 | +Having said that: So much of Rust's success has been built around the trust that |
| 183 | +we have earned with our community. Yes, the project has always reserved the |
| 184 | +right to make breaking changes when resolving soundness bugs; but we have also |
| 185 | +strived to avoid such breakage *whenever feasible*, via things like |
| 186 | +[future-incompatible lints][future-incompat]. |
| 187 | + |
| 188 | +[future-incompat]: https://doc.rust-lang.org/rustc/lints/index.html#future-incompatible-lints |
| 189 | + |
| 190 | +Today, with our current const-eval architecture layered atop Miri, it is not |
| 191 | +feasible to ensure that changes such as the [one that injected][PR #97684] issue |
| 192 | +[#99923][] go through a future-incompat warning cycle. Maybe that is an okay |
| 193 | +state of affairs; it is hard to tell if we will encounter other cases similar to |
| 194 | +this in the future as Miri and const-eval continue to evolve. |
| 195 | + |
| 196 | +[PR #97684]: https://github.com/rust-lang/rust/pull/97684 |
| 197 | + |
| 198 | +[stability post]: https://blog.rust-lang.org/2014/10/30/Stability.html |
| 199 | + |
| 200 | +The compiler team plans to keep our eye on issues in this space. If we see |
| 201 | +evidence that these kinds of changes do cause breakage to a non-trivial number |
| 202 | +of crates, then we will investigate further how we might smooth the transition |
| 203 | +path between compiler releases. However, we need to balance any such goal |
| 204 | +against the fact that Miri has very a limited set of developers: the researchers |
| 205 | +determining how to define the semantics of unsafe languages like Rust. We do not |
| 206 | +want to slow their work down! |
| 207 | + |
| 208 | +## What you can do for safety's sake |
| 209 | + |
| 210 | +If you observe the `could not evaluate static initializer` message on your crate |
| 211 | +atop Rust 1.64, and it was compiling with previous versions of Rust, we want you |
| 212 | +to let us know: [file an issue][]! |
| 213 | + |
| 214 | +<!-- |
| 215 | +
|
| 216 | +(Of course we always want to hear about such cases where a crate regresses |
| 217 | +between Rust releases; this is just a case that was particularly subtle for us |
| 218 | +to tease apart within the project community itself.) |
| 219 | +
|
| 220 | +--> |
| 221 | + |
| 222 | +If you can test compiling your crate atop the 1.64-beta before the stable |
| 223 | +release goes out on September 22nd, all the better! One easy way to try the beta |
| 224 | +is to use [rustup's override shortand][rustup] for it. |
| 225 | + |
| 226 | +[rustup]: https://rust-lang.github.io/rustup/overrides.html#toolchain-override-shorthand |
| 227 | + |
| 228 | +[file an issue]: https://github.com/rust-lang/rust/issues/new/choose |
| 229 | + |
| 230 | +As Rust's const-eval evolves, we may see another case like this arise again. If |
| 231 | +you want to defend against future instances of const-eval UB, we recommend that |
| 232 | +you set up a continuous integration service to invoke the nightly `rustc` with |
| 233 | +the unstable `-Z extra-const-ub-checks` flag on your code. |
| 234 | + |
| 235 | +## Want to help? |
| 236 | + |
| 237 | +As you might imagine, a lot of us are pretty interested in questions such as |
| 238 | +"what should be undefined behavior?" |
| 239 | + |
| 240 | +See for example Ralf Jung's excellent blog series on why pointers are |
| 241 | +complicated (parts [I][ralf1], [II][ralf2], [III][ralf3]), which contain some of |
| 242 | +the details elided above about Miri's representation, and spell out reasons why |
| 243 | +you might want to be concerned about pointer-to-usize transmutes even *outside* |
| 244 | +of const-eval. |
| 245 | + |
| 246 | +If you are interested in trying to help us figure out answers to those kinds of |
| 247 | +questions, please join us in the [unsafe code guidelines zulip][ucg zulip]. |
| 248 | + |
| 249 | +[ralf1]: https://www.ralfj.de/blog/2018/07/24/pointers-and-bytes.html |
| 250 | +[ralf2]: https://www.ralfj.de/blog/2020/12/14/provenance.html |
| 251 | +[ralf3]: https://www.ralfj.de/blog/2022/04/11/provenance-exposed.html |
| 252 | +[ucg zulip]: https://rust-lang.zulipchat.com/#narrow/stream/136281-t-lang.2Fwg-unsafe-code-guidelines |
| 253 | + |
| 254 | +If you are interested in learning more about Miri, or contributing to it, you |
| 255 | +can say Hello in the [miri zulip][]. |
| 256 | + |
| 257 | +[miri zulip]: https://rust-lang.zulipchat.com/#narrow/stream/269128-miri |
| 258 | + |
| 259 | + |
| 260 | +## Conclusion |
| 261 | + |
| 262 | +The compiler team is hoping that issue [#99923][] is an exceptional fluke and |
| 263 | +that the 1.64 stable release will not encounter any other surprises related to |
| 264 | +this change to the const-eval machinery. |
| 265 | + |
| 266 | +But fluke or not, the issue provided excellent motivation to spend some time |
| 267 | +exploring facets of Rust's const-eval architecture, and the Miri interpreter |
| 268 | +that underlies it. We hope you enjoyed reading this as much as we did writing |
| 269 | +it. |
0 commit comments