Skip to content

Commit 8075b0f

Browse files
committed
first draft of blog post re ctfe ub change observed in 1.64.
1 parent 9b340b7 commit 8075b0f

File tree

1 file changed

+269
-0
lines changed

1 file changed

+269
-0
lines changed
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
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

Comments
 (0)