Sources of uninitialized memory in Rust

Here are the ways you can get uninitialized memory (undef in LLVM) that I am aware:

  • Padding between struct fields is considered uninitialized
  • MaybeUninit<T>
  • mem::uninitialized() (deprecated)
  • Padding between enum's hidden tag field and the variant actually contained

Are there any other circumstances where it comes up?

2 Likes
  • Calling the allocator returns a pointer to uninitialized memory.

Also, this is not another item for your list, but note that “between” is too specific; padding can exist at the end of a type due to alignment requirements (the size is always a multiple of the alignment).

6 Likes
  • The unused space in a union (which is how MaybeUninit works)
2 Likes

The unused capacity of a Vec. (a special case of calling the allocator)

2 Likes

Moving a non-Copy value leaves its old location uninitialized.

Writing to a pointer direct (*p = something) drops, and that Drop implementation may be handed uninitialized memory.

4 Likes

Calling a naughty foreign function

Wouldn't this be UB in the case of an explicit Drop impl?

Yeah, true, it's immediate UB because a &mut T is created.

1 Like

That is a special-case of padding.

Are you sure about that? I remember it being discussed whether it makes the memory uninitialized or just duplicates it. Better be safe, of course :slight_smile:

If it wasn't invalidated somehow, you could have aliasing, say.

The Nomicon says

If a value is moved out of a variable, that variable becomes logically uninitialized if the type of the value isn't Copy.

2 Likes

If you don't use the original value everything is fine. Of course you should not use it in case it invalidates something.

Here it is:

1 Like

The direct issue is

Looks like Miri will gain the ability to treat moves as deinit. There are some examples where it's probably not possible too though, or requires more nuance.

1 Like

Before MIRI can do it, we should ask whether it's invalid at all.

Given how moves work semantically, and the nomicon entry that @quinedot mentioned, I’ve always assumed that the compiler reuses the space of a moved-out stack variable for later variables¹.

In particular, it feels silly for a statement like this to not reuse the space:

let initial:T = T::new();
let modified:T = initial.method_that_takes_ownership();

This kind of optimization is only possible if accessing the original location after a move is UB, because the actual value stored there at any particular time is unpredictable to the programmer.


¹ I don’t know if the compiler actually has this kind of optimization

3 Likes

IIRC it doesn't; space is reserved for every variable that can possibly occur at the start of the function, except for variables which can be stored in registers. Notably, every branch of a switch takes up its own stack space, which I recall has caused stack-usage issues in functions with many println!s.

1 Like

It's valid after regular moves because it's an error to access them after, but I'm talking about things like ptr::read().

I was hoping a let with no initializer would allocate an undef value, but it looks like rustc has a MIR pass which trims variables that are never used before LLVM IR is emitted :disappointed:

fn main() {
    let x: u64;
}
2 Likes

They all get distinct space from rustc, but LLVM will try to merge them in the StackColoring pass.

2 Likes

ptr::read is not a move, because it leaves the original data alone.

(It has to, or things like ptr::reading the String in an Option<String> would break niche optimizations.)