Skip to content

Commit cfad08e

Browse files
committed
Describe pointer fragment restriction in const final values
Let's add examples and explanatory notes to clarify the restriction that the representation of the final value of a constant or static initializer must only contain bytes with provenance in whole-pointer groups. We'll add a `compile_fail` example demonstrating how storing a pointer that extends into padding creates pointer fragments in the final value, causing compilation to fail and show to work around this by explicitly zeroing the padding bytes. Let's extend the existing note about uninitialized padding bytes to provide deeper intuition about this restriction and explain how constant evaluation makes the details of typed copies observable (whether field-by-field or memory-block), how these details are not yet fully specified in Rust, and why the compiler must be allowed to reject initializers with uninitialized padding bytes to preserve future flexibility (such as always setting padding to uninitialized). Context: - rust-lang/rust#148470 - rust-lang/rust#148967
1 parent c461766 commit cfad08e

1 file changed

Lines changed: 57 additions & 0 deletions

File tree

src/const_eval.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,65 @@ r[const-eval.const-expr.if-match]
237237
r[const-eval.const-expr.final-value-provenance]
238238
The representation of the final value of a [constant][constant initializer] or [static initializer] must only contain bytes with provenance in whole-pointer groups. If a byte has provenance but is not part of an adjacent group of bytes that form an entire pointer, compilation will fail.
239239

240+
```rust,compile_fail
241+
# use core::mem::MaybeUninit;
242+
#
243+
#[repr(C, align(32))]
244+
struct Pair {
245+
x: u128, // Offset 0, 16 bytes.
246+
y: MaybeUninit<u64>, // Offset 16, 8 bytes.
247+
// Offset 24, 8 bytes of padding.
248+
}
249+
250+
const C: Pair = unsafe {
251+
// ^^^^^^^ ERROR: Partial pointer in final value of constant.
252+
let mut m = MaybeUninit::<Pair>::uninit();
253+
// Store pointer that extends half-way into trailing padding.
254+
m.as_mut_ptr().byte_add(20).cast::<&u8>().write_unaligned(&0);
255+
// Initialize fields.
256+
(*m.as_mut_ptr()).x = 0;
257+
(*m.as_mut_ptr()).y = MaybeUninit::new(0);
258+
// Now `m` contains a pointer fragment in the padding.
259+
m.assume_init()
260+
};
261+
```
262+
263+
> [!NOTE]
264+
> Manually initializing (e.g., zeroing) the padding bytes ensures the final value is accepted:
265+
>
266+
> ```rust
267+
> # use std::mem::MaybeUninit;
268+
> # #[repr(C, align(32))]
269+
> # struct Pair {
270+
> # x: u128, // Offset 0, 16 bytes.
271+
> # y: MaybeUninit<u64>, // Offset 16, 8 bytes.
272+
> # // Offset 24, 8 bytes of padding.
273+
> # }
274+
> const C: Pair = unsafe {
275+
> let mut m = MaybeUninit::<Pair>::uninit();
276+
> m.as_mut_ptr().byte_add(20).cast::<&u8>().write_unaligned(&0);
277+
> // Explicitly zero the padding.
278+
> m.as_mut_ptr().byte_add(24).cast::<u64>().write_unaligned(0);
279+
> // As above.
280+
> (*m.as_mut_ptr()).x = 0;
281+
> (*m.as_mut_ptr()).y = MaybeUninit::new(0);
282+
> m.assume_init()
283+
> };
284+
> ```
285+
240286
> [!NOTE]
241287
> If a byte in the representation of the final value is uninitialized, then it *may* end up having provenance, which can cause compilation to fail. `rustc` tries (but does not guarantee) to only actually fail if the initializer copies or overwrites parts of a pointer and those bytes appear in the final value.
288+
>
289+
> E.g., `rustc` currently accepts this, even though the padding bytes are uninitialized:
290+
>
291+
> ```rust
292+
> # #[repr(C)]
293+
> # struct Pair { x: u128, y: u64 }
294+
> // The padding bytes are uninitialized.
295+
> const ALLOWED: Pair = Pair { x: 0, y: 0 };
296+
> ```
297+
>
298+
> Constant evaluation makes the details of typed copies observable: depending on whether a copy is performed field-by-field or as a memory-block copy, provenance in padding bytes might be discarded or preserved (both in the source and in the destination). The language allows the compiler to reject any initializer with an uninitialized padding byte to preserve implementation flexibility (e.g., the compiler may in the future always set padding bytes to uninitialized). Determining whether an uninitialized byte contains a pointer fragment would require tracking that is not currently performed.
242299
243300
r[const-eval.const-context]
244301
## Const context

0 commit comments

Comments
 (0)