rust/unchecked-uninit.md
2015-07-06 18:36:16 -07:00

3.8 KiB

% Unchecked Uninitialized Memory

One interesting exception to this rule is working with arrays. Safe Rust doesn't permit you to partially initialize an array. When you initialize an array, you can either set every value to the same thing with let x = [val; N], or you can specify each member individually with let x = [val1, val2, val3]. Unfortunately this is pretty rigid, especially if you need to initialize your array in a more incremental or dynamic way.

Unsafe Rust gives us a powerful tool to handle this problem: mem::uninitialized. This function pretends to return a value when really it does nothing at all. Using it, we can convince Rust that we have initialized a variable, allowing us to do trickier things with conditional and incremental initialization.

Unfortunately, this opens us up to all kinds of problems. Assignment has a different meaning to Rust based on whether it believes that a variable is initialized or not. If it's uninitialized, then Rust will semantically just memcopy the bits over the uninitialized ones, and do nothing else. However if Rust believes a value to be initialized, it will try to Drop the old value! Since we've tricked Rust into believing that the value is initialized, we can no longer safely use normal assignment.

This is also a problem if you're working with a raw system allocator, which returns a pointer to uninitialized memory.

To handle this, we must use the ptr module. In particular, it provides three functions that allow us to assign bytes to a location in memory without evaluating the old value: write, copy, and copy_nonoverlapping.

  • ptr::write(ptr, val) takes a val and moves it into the address pointed to by ptr.
  • ptr::copy(src, dest, count) copies the bits that count T's would occupy from src to dest. (this is equivalent to memmove -- note that the argument order is reversed!)
  • ptr::copy_nonoverlapping(src, dest, count) does what copy does, but a little faster on the assumption that the two ranges of memory don't overlap. (this is equivalent to memcopy -- note that the argument order is reversed!)

It should go without saying that these functions, if misused, will cause serious havoc or just straight up Undefined Behaviour. The only things that these functions themselves require is that the locations you want to read and write are allocated. However the ways writing arbitrary bits to arbitrary locations of memory can break things are basically uncountable!

Putting this all together, we get the following:

fn main() {
	use std::mem;

	// size of the array is hard-coded but easy to change. This means we can't
	// use [a, b, c] syntax to initialize the array, though!
	const SIZE = 10;

	let x: [Box<u32>; SIZE];

	unsafe {
		// convince Rust that x is Totally Initialized
		x = mem::uninitialized();
		for i in 0..SIZE {
			// very carefully overwrite each index without reading it
			// NOTE: exception safety is not a concern; Box can't panic
			ptr::write(&mut x[i], Box::new(i));
		}
	}

	println!("{}", x);
}

It's worth noting that you don't need to worry about ptr::write-style shenanigans with types which don't implement Drop or contain Drop types, because Rust knows not to try to Drop them. Similarly you should be able to assign to fields of partially initialized structs directly if those fields don't contain any Drop types.

However when working with uninitialized memory you need to be ever-vigilant for Rust trying to Drop values you make like this before they're fully initialized. Every control path through that variable's scope must initialize the value before it ends, if has a destructor. This includes code panicking.

And that's about it for working with uninitialized memory! Basically nothing anywhere expects to be handed uninitialized memory, so if you're going to pass it around at all, be sure to be really careful.