From 6463ad908139836c7528ba47079a7f36e8067f20 Mon Sep 17 00:00:00 2001 From: Ralf Jung Date: Mon, 19 Jan 2026 11:02:16 +0100 Subject: [PATCH 1/6] const-eval: explain the final-value-byte-provenance restriction --- src/const_eval.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/const_eval.md b/src/const_eval.md index 779b00354e..295be5c1ae 100644 --- a/src/const_eval.md +++ b/src/const_eval.md @@ -232,6 +232,12 @@ r[const-eval.const-expr.loop] r[const-eval.const-expr.if-match] * [if] and [match] expressions. +r[const-eval.const-expr.final-value-provenance] +The representation of the final value of a constant or static initializer must only contain 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. + +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. +As a quality-of-implementation concern, the compiler should only actually fail if the initializer copies or overwrites parts of a pointer and that memory ends up in the final value. + r[const-eval.const-context] ## Const context [const context]: #const-context From 4c2c96d218c06a9a21f50f647212d04a5e7b7858 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 20 Jan 2026 21:15:34 +0000 Subject: [PATCH 2/6] Revise text on initializer provenance restriction The text here is describing a restriction on the final value of a constant or static initializer. We could put this in the chapters for constant and static items, but then we'd have to duplicate it. At the same time, it doesn't make sense to be in the section for constant expressions since this is not a restriction on those expressions. Let's solve this, for now, by keeping it in the chapter on constant evaluation but putting it in a new "constant initializers" section. We'll move the second paragraph, which states what a compiler should do as a "quality-of-implementation" matter, into an admonition, and we'll reword this a bit to talk about what `rustc` does (but does not guarantee) to match the style we use for this in other places. We also add links to the appropriate chapters and sections for constant and static initializers. --- src/const_eval.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/const_eval.md b/src/const_eval.md index 295be5c1ae..1aaec8c891 100644 --- a/src/const_eval.md +++ b/src/const_eval.md @@ -232,11 +232,13 @@ r[const-eval.const-expr.loop] r[const-eval.const-expr.if-match] * [if] and [match] expressions. +## Const initializers + r[const-eval.const-expr.final-value-provenance] -The representation of the final value of a constant or static initializer must only contain 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. +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. -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. -As a quality-of-implementation concern, the compiler should only actually fail if the initializer copies or overwrites parts of a pointer and that memory ends up in the final value. +> [!NOTE] +> 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. r[const-eval.const-context] ## Const context @@ -313,6 +315,7 @@ The types of a const function's parameters and return type are restricted to tho [const generic argument]: items/generics.md#const-generics [const generic parameters]: items/generics.md#const-generics [constant expressions]: #constant-expressions +[constant initializer]: items.const [constants]: items/constant-items.md [Const parameters]: items/generics.md [dereference expression]: expr.deref @@ -342,6 +345,7 @@ The types of a const function's parameters and return type are restricted to tho [promoted]: destructors.md#constant-promotion [range expressions]: expressions/range-expr.md [slice]: types/slice.md +[static initializer]: items.static.init [statics]: items/static-items.md [Struct expressions]: expressions/struct-expr.md [temporary lifetime extension]: destructors.scope.lifetime-extension From c9ea50078336dbb05b9b90cc4a5100e81a7f484f Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 20 Jan 2026 21:59:28 +0000 Subject: [PATCH 3/6] 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 --- src/const_eval.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/const_eval.md b/src/const_eval.md index 1aaec8c891..d76fe70fad 100644 --- a/src/const_eval.md +++ b/src/const_eval.md @@ -237,8 +237,64 @@ r[const-eval.const-expr.if-match] r[const-eval.const-expr.final-value-provenance] 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. +```rust,compile_fail +# use core::mem::MaybeUninit; +# +#[repr(C)] +struct Pair { + x: u128, + y: MaybeUninit, + // 8 bytes of padding at offset 24. +} + +const C: Pair = unsafe { +// ^^^^^^^ ERROR: Partial pointer in final value of constant. + let mut m = MaybeUninit::::uninit(); + // Store pointer that extends half-way into trailing padding. + m.as_mut_ptr().byte_add(20).cast::<&u8>().write_unaligned(&0); + // Initialize fields. + (*m.as_mut_ptr()).x = 0; + (*m.as_mut_ptr()).y = MaybeUninit::new(0); + // Now `m` contains a pointer fragment in the padding. + m.assume_init() +}; +``` + +> [!NOTE] +> Manually initializing (e.g., zeroing) the padding bytes ensures the final value is accepted: +> +> ```rust +> # use std::mem::MaybeUninit; +> # #[repr(C)] +> # struct Pair { +> # x: u128, +> # y: MaybeUninit, +> # } +> const C: Pair = unsafe { +> let mut m = MaybeUninit::::uninit(); +> m.as_mut_ptr().byte_add(20).cast::<&u8>().write_unaligned(&0); +> // Explicitly zero the padding. +> m.as_mut_ptr().byte_add(24).cast::().write_unaligned(0); +> // As above. +> (*m.as_mut_ptr()).x = 0; +> (*m.as_mut_ptr()).y = MaybeUninit::new(0); +> m.assume_init() +> }; +> ``` + > [!NOTE] > 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. +> +> E.g., `rustc` currently accepts this, even though the padding bytes are uninitialized: +> +> ```rust +> # #[repr(C)] +> # struct Pair { x: u128, y: u64 } +> // The padding bytes are uninitialized. +> const ALLOWED: Pair = Pair { x: 0, y: 0 }; +> ``` +> +> 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). Because the semantics of typed copies are not yet fully specified in Rust --- and to preserve the ability to change how they work in the future in constant evaluation (for example, to always set the padding bytes to uninitialized) --- the language allows the compiler to reject any initializer with an uninitialized padding byte. Since the compiler cannot currently guarantee that an uninitialized byte does not contain a pointer fragment without a full model of typed copies, this allowance is necessary to avoid relying on underspecified details of the language. r[const-eval.const-context] ## Const context From efaf9f24149bec84d01125744f8f2077953c96fd Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 27 Jan 2026 22:48:32 +0000 Subject: [PATCH 4/6] Add note explaining whole-pointer order requirement Let's add an admonition explaining that bytes with provenance must form a complete pointer in the correct order. --- src/const_eval.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/const_eval.md b/src/const_eval.md index d76fe70fad..6a480580d3 100644 --- a/src/const_eval.md +++ b/src/const_eval.md @@ -260,6 +260,11 @@ const C: Pair = unsafe { }; ``` +> [!NOTE] +> The bytes with provenance must form a complete pointer in the correct order. In the example above, the pointer is written at offset 20, but it requires (on 64-bit platforms) 8 bytes. Four of those bytes fit in the `y` field; the rest extend into the padding at offset 24. When the fields are initialized, the `y` bytes get overwritten, leaving only a partial pointer (4 bytes) in the padding. These 4 bytes have provenance but don't form a complete pointer, causing compilation to fail. +> +> This restriction ensures that any bytes with provenance in the final value represent complete, valid pointers. The compiler cannot support pointer fragments because it would be unable to reason about them at compile time. + > [!NOTE] > Manually initializing (e.g., zeroing) the padding bytes ensures the final value is accepted: > From 94920de962cb0f29ee32c6aabe22b109a5b4c113 Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 27 Jan 2026 23:14:26 +0000 Subject: [PATCH 5/6] Add example of reversed pointer bytes being rejected Let's add an example demonstrating that reversing the order of pointer bytes causes compilation to fail, even though all bytes are present. The compiler tracks the position of each byte within its original pointer and only accepts pointers when reassembled in the correct order. This example copies a pointer byte-by-byte in reverse order into the padding of a struct, which fails because the fragment indices don't match up to form a valid pointer. Context: rust-lang/rust#144081 --- src/const_eval.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/const_eval.md b/src/const_eval.md index 6a480580d3..969ceedeeb 100644 --- a/src/const_eval.md +++ b/src/const_eval.md @@ -265,6 +265,35 @@ const C: Pair = unsafe { > > This restriction ensures that any bytes with provenance in the final value represent complete, valid pointers. The compiler cannot support pointer fragments because it would be unable to reason about them at compile time. +> [!NOTE] +> +> Reversing the order of the pointer bytes also causes compilation to fail, even though all bytes are present: +> +> ```rust,compile_fail +> # use std::mem::MaybeUninit; +> # #[repr(C)] +> # struct Pair { +> # x: u128, +> # y: MaybeUninit, +> # } +> const C: Pair = unsafe { +> // ^^^^^^^ ERROR: Partial pointer in final value of constant. +> let mut m = MaybeUninit::::uninit(); +> let ptr: *const u8 = &0; +> let ptr_bytes = &ptr as *const _ as *const MaybeUninit; +> // Write pointer bytes in reverse order into the padding. +> let dst = m.as_mut_ptr().cast::>().add(24); +> let mut i = 0; +> while i < 8 { +> dst.add(i).write(ptr_bytes.add(7 - i).read()); +> i += 1; +> } +> (*m.as_mut_ptr()).x = 0; +> (*m.as_mut_ptr()).y = MaybeUninit::new(0); +> m.assume_init() +> }; +> ``` + > [!NOTE] > Manually initializing (e.g., zeroing) the padding bytes ensures the final value is accepted: > From fde73c79766f894f33ae8f1416be85da58efbcda Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Tue, 27 Jan 2026 23:16:59 +0000 Subject: [PATCH 6/6] Add normative mention about ordering pointer bytes In const eval, the bytes with provenance that become part of the final value must form a whole-pointer group with the bytes in the correct order. We have admonitions about this; let's also add disambiguating text to the normative language. --- src/const_eval.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/const_eval.md b/src/const_eval.md index 969ceedeeb..fd376e1a8a 100644 --- a/src/const_eval.md +++ b/src/const_eval.md @@ -235,7 +235,7 @@ r[const-eval.const-expr.if-match] ## Const initializers r[const-eval.const-expr.final-value-provenance] -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. +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 correctly-ordered bytes that form an entire pointer, compilation will fail. ```rust,compile_fail # use core::mem::MaybeUninit;