|
1 |
| -# Const safety |
| 1 | +# Const safety |
| 2 | + |
| 3 | +The miri engine, which is used to execute code at compile time, can fail in |
| 4 | +four possible ways: |
| 5 | + |
| 6 | +* The program performs an unsupported operation (e.g., calling an unimplemented |
| 7 | + intrinsics, or doing an operation that would observe the integer address of a |
| 8 | + pointer). |
| 9 | +* The program causes undefined behavior (e.g., dereferencing an out-of-bounds |
| 10 | + pointer). |
| 11 | +* The program panics (e.g., a failed bounds check). |
| 12 | +* The program loops forever, and this is detected by the loop detector. Note |
| 13 | + that this detection happens on a best-effort basis only. |
| 14 | + |
| 15 | +Just like panics and non-termination are acceptable in safe run-time Rust code, |
| 16 | +we also consider these acceptable in safe compile-time Rust code. However, we |
| 17 | +would like to rule out the first two kinds of failures in safe code. Following |
| 18 | +the terminology in [this blog post], we call a program that does not fail in the |
| 19 | +first two ways *const safe*. |
| 20 | + |
| 21 | +[this blog post]: https://www.ralfj.de/blog/2018/07/19/const.html |
| 22 | + |
| 23 | +The goal of the const safety check, then, is to ensure that a program is const |
| 24 | +safe. What makes this tricky is that there are some operations that are safe as |
| 25 | +far as run-time Rust is concerned, but unsupported in the miri engine and hence |
| 26 | +not const safe (they fall in the first category of failures above). We call these operations *unconst*. The purpose |
| 27 | +of the following section is to explain this in more detail, before proceeding |
| 28 | +with the main definitions. |
| 29 | + |
| 30 | +## Miri background |
| 31 | + |
| 32 | +A very simple example of an unconst operation is |
| 33 | +```rust |
| 34 | +static S:i32 = 0; |
| 35 | +const BAD:bool = (&S as *const i32 as usize) % 16 == 0; |
| 36 | +``` |
| 37 | +The modulo operation here is not supported by the miri engine because evaluating |
| 38 | +it requires knowing the actual integer address of `S`. |
| 39 | + |
| 40 | +The way miri handles this is by treating pointer and integer values separately. |
| 41 | +The most primitive kind of value in miri is a `Scalar`, and a scalar is *either* |
| 42 | +a pointer (`Scalar::Ptr`) or a bunch of bits representing an integer |
| 43 | +(`Scalar::Bits`). Every value of a variable of primitive type is stored as a |
| 44 | +`Scalar`. In the code above, casting the pointer `&S` to `*const i32` and then |
| 45 | +to `usize` does not actually change the value -- we end up with a local variable |
| 46 | +of type `usize` whose value is a `Scalar::Ptr`. This is not a problem in |
| 47 | +itself, but then executing `%` on this *pointer value* is unsupported. |
| 48 | + |
| 49 | +However, it does not seem appropriate to blame the `%` operation above for this |
| 50 | +failure. `%` on "normal" `usize` values (`Scalar::Bits`) is perfectly fine, just using it on |
| 51 | +values computed from pointers is an issue. Essentially, `&i32 as *const i32 as |
| 52 | +usize` is a "safe" `usize` at run-time (meaning that applying safe operations to |
| 53 | +this `usize` cannot lead to misbehavior, following terminology [suggested here]) |
| 54 | +-- but the same value is *not* "safe" at compile-time, because we can cause a |
| 55 | +const safety violation by applying a safe operation (namely, `%`). |
| 56 | + |
| 57 | +[suggested here]: https://www.ralfj.de/blog/2018/08/22/two-kinds-of-invariants.html |
| 58 | + |
| 59 | +## Const safety check on values |
| 60 | + |
| 61 | +The result of any const computation (`const`, `static`, promoteds) is subject to |
| 62 | +a "sanity check" which enforces const safety. (A sanity check is already |
| 63 | +happening, but it is not exactly checking const safety currently.) Const safety |
| 64 | +is defined as follows: |
| 65 | + |
| 66 | +* Integer and floating point types are const-safe if they are a `Scalar::Bits`. |
| 67 | + This makes sure that we can run `%` and other operations without violating |
| 68 | + const safety. In particular, the value must *not* be uninitialized. |
| 69 | +* References are const-safe if they are `Scalar::Ptr` into allocated memory, and |
| 70 | + the data stored there is const-safe. (Technically, we would also like to |
| 71 | + require `&mut` to be unique and `&` to not be mutable unless there is an |
| 72 | + `UnsafeCell`, but it seems infeasible to check that.) For fat pointers, the |
| 73 | + length of a slice must be a valid `usize` and the vtable of a `dyn Trait` must |
| 74 | + be a valid vtable. |
| 75 | +* `bool` is const-safe if it is `Scalar::Bits` with a value of `0` or `1`. |
| 76 | +* `char` is const-safe if it is a valid unicode codepoint. |
| 77 | +* `()` is always const-safe. |
| 78 | +* `!` is never const-safe. |
| 79 | +* Tuples, structs, arrays and slices are const-safe if all their fields are |
| 80 | + const-safe. |
| 81 | +* Enums are const-safe if they have a valid discriminant and the fields of the |
| 82 | + active variant are const-safe. |
| 83 | +* Unions are always const-safe; the data does not matter. |
| 84 | +* `dyn Trait` is const-safe if the value is const-safe at the type indicated by |
| 85 | + the vtable. |
| 86 | +* Function pointers are const-safe if they point to an actual function. A |
| 87 | + `const fn` pointer (when/if we have those) must point to a `const fn`. |
| 88 | + |
| 89 | +For example: |
| 90 | +```rust |
| 91 | +static S: i32 = 0; |
| 92 | +const BAD: usize = &S as *const i32 as usize; |
| 93 | +``` |
| 94 | +Here, `S` is const-safe because `0` is a `Scalar::Bits`. However, `BAD` is *not* const-safe because it is a `Scalar::Ptr`. |
| 95 | + |
| 96 | +## Const safety check on code |
| 97 | + |
| 98 | +The purpose of the const safety check on code is to prohibit construction of |
| 99 | +non-const-safe values in safe code. We can allow *almost* all safe operations, |
| 100 | +except for unconst operations -- which are all related to raw pointers: |
| 101 | +Comparing raw pointers for (in)equality, converting them to integers, hashing |
| 102 | +them (including hashing references) and so on must be prohibited. Basically, we |
| 103 | +should not permit any raw pointer operations to begin with, and carefully |
| 104 | +evaluate any that we permit to make sure they are fully supported by miri and do |
| 105 | +not permit constructing non-const-safe values. |
| 106 | + |
| 107 | +There should also be a mechanism akin to `unsafe` blocks to opt-in to using |
| 108 | +unconst operations. At this point, it becomes the responsibility of the |
| 109 | +programmer to preserve const safety. In particular, a *safe* `const fn` must |
| 110 | +always execute const-safely when called with const-safe arguments, and produce a |
| 111 | +const-safe result. For example, the following function is const-safe (after |
| 112 | +some extensions of the miri engine that are already implemented in miri) even |
| 113 | +though it uses raw pointer operations: |
| 114 | +```rust |
| 115 | +const fn test_eq<T>(x: &T, y: &T) -> bool { |
| 116 | + x as *const _ == y as *const _ |
| 117 | +} |
| 118 | +``` |
| 119 | +On the other hand, the following function is *not* const-safe and hence it is considered a bug to mark it as such: |
| 120 | +``` |
| 121 | +const fn convert<T>(x: &T) -> usize { |
| 122 | + x as *const _ as usize |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | +## Open questions |
| 127 | + |
| 128 | +* Do we allow unconst operations in `unsafe` blocks, or do we have some other |
| 129 | + mechanism for opting in to them (like `unconst` blocks)? |
| 130 | + |
| 131 | +* How do we communicate that the rules for safe `const fn` using unsafe code are |
| 132 | + different than the ones for "runtime" functions? The good news here is that |
| 133 | + violating the rules, at worst, leads to a compile-time error in a dependency. |
| 134 | + No UB can arise. However, thanks to [promotion](promotion.md), compile-time |
| 135 | + errors can arise even if no `const` or `static` is involved. |
0 commit comments