Skip to content

Large MaybeUninit::assume_init values no longer construct in place: 8 kB stack temporary + memcpy (regression in nightly-2026-06-26, caused by #158345) #159454

Description

@driveraid

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 =
    f28ac764c36004fa6a6e098d15b4016a838c13c6 is good, nightly-2026-06-26 =
    bd08c9e71874a81670fe3938dbf85148e42c2b96 is bad).
  • Confirmed cause: reverting the one-line change from Use transmute_neo in assume_init #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,alloc restores the original
    codegen (8936 B frame → 712 B).
  • -Zmir-enable-passes=-GVN on the bad nightly also restores the good codegen,
    both on the minimal repro and on the full firmware.
    -Zmir-enable-passes=-DestinationPropagation does not help, so this is a
    different trigger than the one diagnosed in Stack slots are not being reused due to missing LLVM IR lifetime annotations. #157676, though plausibly the
    same underlying missing-lifetime/value-numbering weakness: GVN now sees
    through the MIR-level Transmute of 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 in rustc 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.A-mir-optArea: MIR optimizationsC-optimizationCategory: An issue highlighting optimization opportunities or PRs implementing suchT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.regression-from-stable-to-betaPerformance or correctness regression from stable to beta.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions