Skip to content

uninit_vector should return Vec<MaybeUninit<T>> #396

@plafer

Description

@plafer

The problem

The current implementation of uninit_vector() causes undefined behavior, and is especially dangerous to use with types that manage resources (and thus implement Drop) such as Box or Arc.

This is due to how the API forces you to initialize the vector:

let v: Vec<Box<T>> = uninit_vector(10);

for idx in 0..10 {
  // Rust thinks that `v` is initialized, and so drops the previous value stored at `v[idx]`
  // which in our case is uninitialized, and hence causes undefined behavior.
  v[idx] = Box::new(T);
}

You can test this for yourself with the following snippet

pub struct MyStruct(u32);

impl Drop for MyStruct {
    fn drop(&mut self) {
        std::println!("MyStruct is being dropped, with value {}", self.0);
    }
}

// Output:
// before assignment
// MyStruct is being dropped, with value <undefined; depends on your system/environment>
// after assignment
// MyStruct is being dropped, with value 42
fn main() {
    let mut v: Vec<MyStruct> = unsafe { uninit_vector(1) };

    std::println!("before assignment");
    v[0] = MyStruct(42);
    std::println!("after assignment");
}

The first call the MyStruct::drop() is due to the assignment dropping the former (uninitialized) value in the vector; the second call happens at the end of main() when v is dropped (and all values in the vector).

We probably never noticed anything bad happening before, since the main use of uninit_vector is with Field types that don't implement Drop. However, using it with any type still causes undefined behavior and we should move away from it.

The solution

The solution is to use MaybeUninit which was designed for this use case. So we should mark uninit_vector as deprecated and provide a new

pub unsafe fn uninit_vector2<T>(length: usize) -> Vec<core::mem::MaybeUninit<T>> {
    let mut vector = Vec::with_capacity(length);
    vector.set_len(length);
    vector
}

Then callers use MaybeUninit::write() to set the value (which very importantly does not call drop() on the old value as the assignment operator does). The previous broken example now becomes

// Output:
// before assignment
// after assignment
// MyStruct is being dropped, with value 42
fn main() {
    let v: Vec<MyStruct> = {
        let mut v: Vec<MaybeUninit<MyStruct>> = unsafe { uninit_vector2(1) };

        std::println!("before assignment");
        v[0].write(MyStruct(42));
        std::println!("after assignment");

        // SAFETY: We have initialized all values in the vector, so it is safe to transmute.
        unsafe { core::mem::transmute(v) }
    };
}

Note that using transmute() is the documented way of doing this.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions