Safety of `Vec::<MaybeUninit<T>>` plus `.set_len()`

The following code is unsound [0, 1].

let mut my_vec = Vec::<u8>::with_capacity(1024);
unsafe { my_vec.set_len(1024) }; // UB!
my_vec.iter_mut().for_each(|b| *b = 0);

What about using MaybeUninit in combination with Vec::set_len()?

let mut my_vec = Vec::<MaybeUninit<u8>>::with_capacity(1024);
unsafe { my_vec.set_len(1024) }; // UB?
my_vec.iter_mut().for_each(|b| *b = MaybeUninit::new(0));

Is an uninitialized MaybeUninit different from a MaybeUninit { uninit: () }?

The point of the MaybeUninit type is exactly that even an uninitialized instance of it is valid. I believe your second snippet should be sound. Vec even has a method that returns the uninitialized ("spare") suffix of the buffer as a slice-of-maybe-uninit.

Speaking of which, you should probably use this method instead of re-implementing it manually. I would rather write your code as

    let mut my_vec = Vec::<u8>::with_capacity(1024);
    my_vec.spare_capacity_mut().fill(MaybeUninit::new(0));
    unsafe { my_vec.set_len(my_vec.capacity()); }

It isn't.

3 Likes

Thanks for the pointers and the code – this is what I was looking for but didn't know that I was. :heart:

It isn't.

I'm not sure I'm convinced by that. Sure, calling MaybeUninit::uninit() is the same as MaybeUninit { uninit: () }, but I guess I'm wondering whether there is yet another form of uninitialized memory. However, I'm not totally sure the question even makes sense. :person_shrugging:

There is only one form of uninitialized memory; the underpinning of uninitialized memory from a compiler perspective is that, because it's never been written, reads from it can be assumed to take whatever value is most useful for the compiler at this moment.

1 Like

It does make sense, but thankfully – as far as I know – uninitialized memory is not hierarchical in Rust. This is because MaybeUninit is simply a marker (a special-case or compiler magic, if you like) that tells the compiler "this chunk of memory here may be uninitialized". That's the same kind of uninitialized whether or not you get it from a literal call to the uninit() "constructor". MaybeUninit cuts the whole discussion short by essentially declaring that even an uninitialized MaybeUninit can, in fact, be considered initialized (for its own, useless-by-itself, type – emphatically not for the wrapped type).

3 Likes

It's easiest (YMMV) to think of uninit as if every byte in memory had a magic 257th unique bit pattern. This extra bit pattern never occurs in any valid value of any type, and thus (in this way of thinking) not every byte is in fact a valid u8 because a u8 only has 256 valid values. The only exception to this is MaybeUninit – a MaybeUninit<u8> does in fact have 257 valid values and thus you can just pick any byte in the program's memory and say it's a MaybeUninit and that's sound.

2 Likes

that's not quite true, padding bytes can be uninit, so (u16, u8) has 3 must-be-initialized bytes and 1 byte that's often uninit.

3 Likes

Or, if you're into your physical accuracy, think of each bit as having values other than just 0 and 1; VHDL std_logic has 9 different values to model a digital bit inside a chip, for example.

We can only work reliably with 0s and 1s in software, but reality is not that kind.

1 Like

I just realized that the suggested fixes for the clippy lint uninit_vec, which is triggered by the OP's first snippet, include both the OP's second snippet and the accepted answer's Vec::spare_capacity_mut().

Putting this here in case anyone stumbling across this topic in the future should not be convinced by the discussion thus far. :slightly_smiling_face:

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.