Skip to content

Commit 147dc8f

Browse files
authored
Merge pull request #1028 from pnkfelix/ctfe-volatility
blog post re ctfe ub change observed in 1.64.
2 parents 0e9dd07 + d4d6575 commit 147dc8f

File tree

1 file changed

+327
-0
lines changed

1 file changed

+327
-0
lines changed

Diff for: posts/2022-09-15-const-eval-safety-rule-revision.md

+327
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
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

Comments
 (0)