Help with miri UB

Inspired by a previous post I wrote the following code

use std::ptr;

#[repr(C)]
pub struct Array<T> {
    len: usize,
    ptr: *mut T,
}

impl<T: Copy> Array<T> {
    fn new(val: T, len: usize) -> Self {
        // This fails miri run.
        let mut vec = Vec::with_capacity(len);
        vec.fill(val);
        let data = vec.into_boxed_slice();

        // This passes miri run.
        // let data = vec![val; len].into_boxed_slice();

        Self {
            len: len as usize,
            ptr: Box::into_raw(data).cast(),
        }
    }
}

impl<T> Drop for Array<T> {
    fn drop(&mut self) {
        let &mut Self { len, ptr } = self;
        let slice = ptr::slice_from_raw_parts_mut(ptr, len);
        let a = unsafe { Box::from_raw(slice) };
        std::mem::drop(a);
    }
}

fn main() {
    let mut _o = Array::new(14, 3);
}

If I use the vec![] macro miri runs fine. If I use Vec::with_capacity() and Vec::fill() miri reports UB.

cargo +nightly miri run
Preparing a sysroot for Miri (target: x86_64-unknown-linux-gnu)... done
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `/home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo-miri runner target/miri/x86_64-unknown-linux-gnu/debug/rust_test`
error: Undefined Behavior: out-of-bounds pointer use: 0x4[noalloc] is a dangling pointer (it has no provenance)
    --> /home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:1007:9
     |
1007 |         Box(unsafe { Unique::new_unchecked(raw) }, alloc)
     |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ out-of-bounds pointer use: 0x4[noalloc] is a dangling pointer (it has no provenance)
     |
     = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
     = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
     = note: BACKTRACE:
     = note: inside `std::boxed::Box::<[i32]>::from_raw_in` at /home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:1007:9: 1007:58
     = note: inside `std::boxed::Box::<[i32]>::from_raw` at /home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/boxed.rs:951:18: 951:48
note: inside `<Array<i32> as std::ops::Drop>::drop`
    --> src/main.rs:30:26
     |
30   |         let a = unsafe { Box::from_raw(slice) };
     |                          ^^^^^^^^^^^^^^^^^^^^
     = note: inside `std::ptr::drop_in_place::<Array<i32>> - shim(Some(Array<i32>))` at /home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/mod.rs:497:1: 497:56
note: inside `main`
    --> src/main.rs:37:1
     |
37   | }
     | ^

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to previous error

Can you help me understand this? Are both wrong and one just doesn't trigger the problem in miri or is there actaually a difference between these two?

Oh, I see. Vec::fill() only fills len elements, not capacity.

Fill only writes vec.len() values, not vec.capacity().

Yes thank you, I'm an idiot :laughing:. I don't know why I sometimes don't get to the answer until I posted the question.

There's spare_capacity_mut() that you may find useful.

2 Likes

:duck:

4 Likes

I don't understand the purpose of that method. Why use it instead of simply pushing to the vector?

Well, it actually is a method on slice, not on Vec, and you can't push to slices.

I'd assume it allows (unsafely) achieving either better performance, since you don't need to update len as often, or it also allows greater flexibility, such as writing a bunch of new elements in different than linear (forward) order, so e. g. backwards, or in a more complicated order.

No, spare_capacity_mut is a method in Vec - slices font have a "spare capacity".

1 Like

I was talking about fill

I see. I think @SebastianJL, in the comment you answered to, was talking about spare_capacity_mut though :wink:

Yes I was. Sorry for being ambiguous.

Some things operate on &mut [MaybeUninit<T>] instead of &mut Vec<T>. This allows them to be compatible with stack allocations and various container types. E.g. the BorrowedBuf API.

2 Likes

Rust does not allow normal types like [u8] or T being uninitialized. So to have a slice, you first need to fill it with some value. If you're going to be writing something else to that slice, it's a wasted effort.

So Rust has a pattern for a "write-only" slices that can be uninitialized:

let mut vec = Vec::new();
// I prefer this over with_capacity, since it handles OOM
vec.try_reserve_exact(len)?; 

// extra [..len], because vec never promises its capacity is exact,
// even in the reserve_exact method!
let destination = &mut vec.spare_capacity_mut()[..len]; 

// Now you have a slice of `&mut [MaybeUninit<T>]`
write_elements_to(destination)?;
// or C/FFI style:
write_elements_to(destination.as_mut_ptr(), len)?;

vec.set_len(len); // assuming write_elements_to has done its job
vec
1 Like

Thanks for the concrete example.