Is transmuting `&mut [MaybeUinit<u8>]` to `&mut [u8]` an UB in my code?

I have the following code. Do I have any UB? If yes, what is the correct way on filling uninialized data from read?

(Playground)

use std::mem::MaybeUninit;
use std::io::{Cursor, Read};

// It can be a file or socket
fn some_reader() -> impl Read {
    Cursor::new(b"abcd")
}

fn main() {
    let mut reader = some_reader();
    let mut buf = Vec::with_capacity(16);

    {
        let read_buf_uninit: &mut [MaybeUninit<u8>] = buf.spare_capacity_mut();
        let read_buf: &mut [u8] = unsafe { std::mem::transmute(read_buf_uninit) };

        let len = reader.read(read_buf).unwrap();

        unsafe {
            buf.set_len(buf.len() + len);
        }
    }

    println!("{:?}", buf);
}

The situation depends on whether the type of reader is known.

If you allow the user to provide a reader and do this, then you definitely have a problem. The user could craft a reader that reads from the slice its supposed to write to. This would cause UB.

If you know that the reader is, say, a file, then you know that it wont read the uninitialized bytes. In this case, you are in slightly sketchy area, but mostly okay. It's the kind of thing where it's not fully decided whether it should be UB, but currently it is rather likely that they decide it isn't UB precisely to allow what you're doing.

That said, you really should use a pointer cast instead of transmute.

1 Like

Thanks a lot!

The only entirely non-“sketchy” approaches include the unstable APIs around Read::read_buf, and of course the approach to first zero-initialize the buffer. (The lack of better options [as far as I’m aware] is of course a large motivation why these [still unstable] APIs are being designed).

I'm a bit confused about this. Does pointer cast makes it safer? Or is it because in this case transmute is not really needed?

The layout of fat pointers to slices (which consist of a pointer part and a length part) isn’t entirely “stable” … at least not guaranteed to be stable … yet, so any transmute involving those is suboptimal. Even without this, certain transmutes of references can result in UB, e.g. casting between (non-fat) references, pointers and usize is possible, but transmuting between pointers and usize (and using the pointer afterwards) can result in UB. Subtleties like this are why using raw pointer casts instead of transmute is generally preferred if your goal is merely to change the type of the target of a reference. It’s considered the (very slightly) less unsafe approach.

(To name another aspect: E.g. - as far as I’m aware - it’s also slightly harder to go (accidentally) between immutable and mutable references this way. – On that note, I’m not even 100% certain whether you’re allowed to transmute e.g. &mut T into &T.)

1 Like

cool thanks

Yes, your code is unconditionally UB, despite @alice 's claims to the contrary. Note that something being Undefined Behaviour doesn't mean that a later version of Rust won't make it well-defined, perhaps depending on some compiler flags or other conditions. But in current Rust, the thing you're trying to do is undefined. The push to make it defined may never pan out and land in the compiler, or it could take many years, or end up in a similar but subtly different form.

Note that the core issue here isn't t-OpSem defining by fiat that this pattern is defined, or implementing it in Miri. The core issue is actually making sure that the compiler always handles this code correctly. I took a look at the current tests, and found nothing which would remotely guarantee the correctness of the code you wrote. Given that there are no tests for this case, I strongly doubt that anyone could guarantee that none of the passes in Rustc or LLVM will ever cause your code to miscompile.

What about this?

(Playground)

use std::io::{Cursor, Read};
use std::ptr;

// It can be a file or socket
fn some_reader() -> impl Read {
    Cursor::new([1, 2, 3, 4])
}

fn main() {
    let mut reader = some_reader();
    let mut buf: Vec<u8> = Vec::with_capacity(16);

    buf.push(0);

    {
        let read_buf = unsafe {
            let len = buf.len();
            let cap = buf.capacity();
            let p = buf.as_mut_ptr().add(len);
            &mut *ptr::slice_from_raw_parts_mut(p, cap - len)
        };

        let len = reader.read(read_buf).unwrap();

        unsafe {
            buf.set_len(buf.len() + len);
        }
    }

    assert_eq!(buf, [0, 1, 2, 3, 4]);
}

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.