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