MaybeUninit and Uninitialized Memory
MaybeUninit and Uninitialized Memory
Safe Rust assumes every value is valid for its type: a bool is only true or false, a reference is non-null and aligned, a Vec<T> upholds its length/capacity invariants. Sometimes you need storage before those invariants hold — out-pointers, element-by-element arrays, FFI buffers. The standard tool is std::mem::MaybeUninit<T>: a wrapper that tells the compiler “these bytes might not be initialized yet.”
Do not reach for the deprecated mem::uninitialized or casual mem::zeroed on arbitrary T. Prefer MaybeUninit, initialize deliberately, then assume_init only when every validity requirement of T is met.
Mental model
MaybeUninit::uninit() ← bytes may be garbage; Drop of T does NOT run
│
write / ptr::write ← initialize without dropping old (uninit) contents
│
assume_init / assume_init_drop
│
▼
T ← normal Drop and safe APIs apply
| API | Role |
|---|---|
MaybeUninit::uninit() |
Allocate storage; contents uninitialized |
MaybeUninit::new(v) |
Already-initialized wrapper |
MaybeUninit::zeroed() |
Fill with 0 bytes — only valid if all-zero is a valid T |
write(val) |
Initialize (overwrites without dropping previous) |
assume_init() |
Assert fully initialized; take ownership as T |
assume_init_drop() |
Drop in place when you do not want ownership |
as_mut_ptr() / &raw mut (*ptr).field |
Field-by-field init without creating &mut to uninit fields |
Dropping a MaybeUninit<T> never runs T’s destructor. If you initialized and then abandon the wrapper without assume_init / assume_init_drop, you leak resources (safe but usually undesirable).
Worked examples
Case 1: write then assume_init
use std::mem::MaybeUninit;
fn main() {
let mut slot = MaybeUninit::<Vec<u8>>::uninit();
slot.write(vec![1, 2, 3]);
let v = unsafe { slot.assume_init() };
assert_eq!(v, vec![1, 2, 3]);
println!("len={}", v.len());
}write returns &mut T to the now-initialized contents. Calling write twice skips dropping the first value — a leak if T owns heap memory. Prefer one write, or drop explicitly between overwrites when you must replace.
Case 2: Out-pointer style
use std::mem::MaybeUninit;
unsafe fn fill_vec(out: *mut Vec<i32>) {
// write does not drop old contents — correct for uninit destination
unsafe { out.write(vec![10, 20, 30]); }
}
fn main() {
let mut slot = MaybeUninit::uninit();
unsafe { fill_vec(slot.as_mut_ptr()); }
let v = unsafe { slot.assume_init() };
assert_eq!(v, vec![10, 20, 30]);
}The caller owns allocation strategy; the callee only initializes. After assume_init, Drop for Vec runs normally at end of scope.
Case 3: Array element-by-element
use std::mem::{self, MaybeUninit};
fn build_boxes() -> [Box<u32>; 4] {
let mut data: [MaybeUninit<Box<u32>>; 4] = [const { MaybeUninit::uninit() }; 4];
for (i, slot) in data.iter_mut().enumerate() {
slot.write(Box::new(i as u32));
}
// SAFETY: every element was written
unsafe { mem::transmute::<_, [Box<u32>; 4]>(data) }
}
fn main() {
let arr = build_boxes();
assert_eq!(*arr[3], 3);
println!("{:?}", arr.iter().map(|b| **b).collect::<Vec<_>>());
}Panic mid-loop leaks already-written Boxes (no destructor on remaining MaybeUninit), but does not invent UB from dropping uninitialized boxes. For partial init, track a length and call assume_init_drop on the live prefix in a cleanup path.
Case 4: Field-by-field struct init
use std::mem::MaybeUninit;
#[derive(Debug, PartialEq)]
struct Person {
name: String,
age: u32,
}
fn main() {
let person = {
let mut uninit = MaybeUninit::<Person>::uninit();
let ptr = uninit.as_mut_ptr();
// SAFETY: write through raw field pointers — do NOT create &mut to uninit fields
unsafe {
(&raw mut (*ptr).name).write(String::from("Ada"));
(&raw mut (*ptr).age).write(36);
uninit.assume_init()
}
};
assert_eq!(
person,
Person {
name: String::from("Ada"),
age: 36
}
);
println!("{person:?}");
}Using assume_init_mut() to “assign fields” before the struct is fully initialized is UB — that creates references to uninitialized memory. Prefer &raw mut / ptr::write as above. Padding bytes need not be initialized.
Case 5: zeroed pitfalls
use std::mem::MaybeUninit;
fn main() {
// OK: (u8, bool) can be all-zero
let pair = unsafe { MaybeUninit::<(u8, bool)>::zeroed().assume_init() };
assert_eq!(pair, (0, false));
// NOT OK: &T must be non-null — zeroed reference is instantaneous UB
// let bad: &i32 = unsafe { MaybeUninit::zeroed().assume_init() };
// NOT OK: many enums reject discriminant 0
// enum NonZero { A = 1, B = 2 }
// let bad = unsafe { MaybeUninit::<NonZero>::zeroed().assume_init() };
println!("pair={pair:?}");
}Rule of thumb: zeroed().assume_init() only for types where every bit pattern of all-zeros is a valid value (raw integers, bool, structs of such). Never for references, NonNull, most enums, or “looks like a Vec with ptr=0”.
Choosing the API
| Situation | Prefer |
|---|---|
| Single value, known initializer | MaybeUninit::new or plain T |
| Deferred init / out-pointer | uninit + write / ptr::write |
| Large array filled in a loop | [MaybeUninit<T>; N] + write each |
| Struct fields in sequence | &raw mut (*ptr).field + write |
| Need all-zero bit pattern and it is valid | zeroed with care |
Old code with mem::uninitialized |
Port to MaybeUninit |
Pitfalls
assume_inittoo early — immediate UB, even fori32/boolwhen bytes are still uninit.zeroedon references / invalid enums — classic footgun; all-zero is not “safe default” for everyT.- Double
writewithout drop — leaks the firstT. - Creating
&/&mutto uninitialized fields — use raw pointers /&raw mutuntil fully init. - Transmuting
&mut Tto&mut MaybeUninit<T>for safe code — lets safe code invent uninit views; unsound. - Assuming Miri-silence equals production-proof — still run tests under Miri for unsafe modules when you can.
Try this
- Run Case 1 and Case 3 under
cargo +nightly miri runif Miri is available. - Deliberately comment out one field write in Case 4 and observe that Miri / careful review catches the incomplete init (do not ship it).
- Replace a
mem::uninitializedcall in an old snippet withMaybeUninitand list whichassume_initproof you now owe. - Skim the Rustonomicon chapter on unchecked uninitialized memory and compare its array pattern to Case 3.