Large `MaybeUninit::assume_init` values no longer construct in place: 8 kB stack temporary + memcpy (regression in nightly-2026-06-26, caused by #158345)
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
MaybeUninit::assume_init() on a large value used to compile to in-place
initialization of the destination. Since nightly-2026-06-26 it materializes the
full-size temporary on the stack and copies it, and also zero-fills bytes that
were never required to be initialized. On our embedded target this inflated one
function's stack frame from 720 B to 8928 B and caused a hardware stack-overflow
fault (STKOF) at boot.
Minimal repro
use std::mem::MaybeUninit;
use std::sync::atomic::AtomicUsize;
pub struct Queue {
buffer: [MaybeUninit<u64>; 1024],
write_idx: AtomicUsize,
read_idx: AtomicUsize,
}
impl Queue {
pub const fn new() -> Self {
Queue {
// The classic "array of MaybeUninit needs no initialization" idiom.
buffer: unsafe {
MaybeUninit::<[MaybeUninit<u64>; 1024]>::uninit().assume_init()
},
write_idx: AtomicUsize::new(0),
read_idx: AtomicUsize::new(0),
}
}
}
pub struct Bus {
ctrl: [u32; 32],
queue: Queue,
armed: bool,
}
impl Bus {
pub fn new() -> Self {
Bus {
ctrl: [0; 32],
queue: Queue::new(),
armed: false,
}
}
}
#[no_mangle]
pub fn init(slot: &'static mut MaybeUninit<Bus>) -> &'static mut Bus {
slot.write(Bus::new())
}
rustc -O --crate-type=lib --emit=asm repro.rs
nightly-2026-06-25 (good) — init writes the few initialized fields
directly into slot, no stack usage:
_init:
movi.2d v0, #0000000000000000
stp q0, q0, [x0, #96]
stp q0, q0, [x0, #64]
stp q0, q0, [x0, #32]
stp q0, q0, [x0]
str q0, [x0, #8320]
strb wzr, [x8, #16]
ret
nightly-2026-06-26 and later (bad) — an 8320-byte stack temporary plus a
memcpy (aarch64-apple-darwin shown; identical shape on
thumbv8m.main-none-eabihf, x86_64 analogous):
_init:
stp x20, x19, [sp, #-32]!
...
sub sp, sp, #1, lsl #12 ; 4096
str xzr, [sp]
sub sp, sp, #1, lsl #12 ; 4096 more (stack probing)
str xzr, [sp], #-128
bl _memcpy ; copy 8328 bytes into slot
...
A simpler variant (the Queue alone, slot.write(Queue::new())) shows a
related symptom: the good nightly emits a single str (only the atomics need
writing); the bad nightly emits bzero of all 8200 bytes — the transmuted
uninit buffer has become a materialized zero constant.
Bisection and cause
- Bisected to nightly-2026-06-26 (nightly-2026-06-25 =
f28ac764c36004fa6a6e098d15b4016a838c13c6is good, nightly-2026-06-26 =
bd08c9e71874a81670fe3938dbf85148e42c2b96is bad). - Confirmed cause: reverting the one-line change from #158345
(MaybeUninit::assume_init:(&raw const self.value).cast::<T>().read()→
transmute_neo(self)) in the nightly-2026-06-28 sysroot sources and
rebuilding our firmware with-Zbuild-std=core,allocrestores the original
codegen (8936 B frame → 712 B). -Zmir-enable-passes=-GVNon the bad nightly also restores the good codegen,
both on the minimal repro and on the full firmware.
-Zmir-enable-passes=-DestinationPropagationdoes not help, so this is a
different trigger than the one diagnosed in #157676, though plausibly the
same underlying missing-lifetime/value-numbering weakness: GVN now sees
through the MIR-levelTransmuteof the uninit value, and the result is a
materialized full-size temporary that nothing eliminates.
Real-world impact
Flight-controller firmware (RP2350, thumbv8m.main-none-eabihf, opt-level 3 +
fat LTO). A static SPSC queue ([MaybeUninit<T>; 1024] buffer, ~8 kB)
constructed with the uninit().assume_init() idiom and moved into a
static_cell::StaticCell inflated the main thread's stack frame from 720 B to
8928 B, overflowing its 12 kB stack at boot (ARMv8-M STKOF fault). The same
idiom is widespread in embedded code (heapless-style buffers, ring queues), so
this likely affects many no_std projects where stack is the scarcest
resource.
Workaround
Replacing the by-value transmute idiom with an inline-const array
initializer restores optimal codegen on the affected nightlies:
buffer: [const { MaybeUninit::uninit() }; 1024],
This avoids the large assume_init transmute entirely, so it sidesteps the
regression — but the huge amount of existing code using
MaybeUninit::<[MaybeUninit<T>; N]>::uninit().assume_init() (the idiom
documented in the MaybeUninit docs for years) silently regresses.
Version
- Last good:
rustc 1.98.0-nightly (f28ac764c 2026-06-23)(nightly-2026-06-25) - First bad: nightly-2026-06-26 (
bd08c9e71874a81670fe3938dbf85148e42c2b96),
still present inrustc 1.98.0-nightly (13f1859f2 2026-06-27)
(nightly-2026-06-28)
Related: #157676 (stack slots not reused due to missing LLVM lifetime
annotations), #158345 (cause).
@rustbot label: regression-from-stable-to-nightly, I-heavy, A-codegen, A-mir-opt
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the minimal repro in repro.rs and compare optimized assembly between the last good and first bad nightly. Read the #158345 change to MaybeUninit::assume_init and investigate the MIR GVN and Transmute behavior, using -Zmir-enable-passes=-GVN as a comparison. Done means the regression is covered and large uninitialized values no longer create a full-size stack temporary, memcpy, or unnecessary zero-fill.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- compilers, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100