Soundness of casting `&mut T` to `&[MaybeUninit<u8>]`

Is this api sound:

use core::{mem::MaybeUninit, slice::from_raw_parts};

pub trait AsMaybeUninitSlice {
	fn as_uninit_slice(&mut self) -> &[MaybeUninit<u8>];
}
impl<T> AsMaybeUninitSlice for T {
	fn as_uninit_slice(&mut self) -> &[MaybeUninit<u8>] {
		let ptr = self as *const T as *const MaybeUninit<u8>;
		unsafe { from_raw_parts(ptr, size_of_val(self)) }
	}
}
impl<T> AsMaybeUninitSlice for [T] {
	fn as_uninit_slice(&mut self) -> &[MaybeUninit<u8>] {
		let ptr = self.as_ptr() as *const _;
		unsafe { from_raw_parts(ptr, size_of_val(self)) }
	}
}

Well, first of all, it's impossible due to conflicting implementations. Other than that, it seems to be sound, but I'm not really sure if it's useful.

1 Like

Implementations are not conflicting, though I agree that due to &mut self it's not the most convenient thing.

1 Like

Ah, sorry - forgot about the implicit : Sized bound in the first one.

2 Likes

Follow up question: Would adding T: core::marker::Freeze be sufficient to allow &self instead of &mut self:

#![feature(freeze)]
use core::{marker::Freeze, mem::MaybeUninit, slice::from_raw_parts};

pub trait AsMaybeUninitSlice {
	fn as_uninit(&self) -> &[MaybeUninit<u8>];
}
impl<T: Freeze> AsMaybeUninitSlice for T {
	fn as_uninit(&self) -> &[MaybeUninit<u8>] {
		let ptr = self as *const T as *const MaybeUninit<u8>;
		unsafe { from_raw_parts(ptr, size_of_val(self)) }
	}
}
impl<T: Freeze> AsMaybeUninitSlice for [T] {
	fn as_uninit(&self) -> &[MaybeUninit<u8>] {
		let ptr = self.as_ptr() as *const _;
		unsafe { from_raw_parts(ptr, size_of_val(self)) }
	}
}

I think it's fine.

It would have been unsafe if the slice was mutable, but an immutable slice can't de-init the content.

1 Like

Yes, immutable is fine. With mutable you have to be careful because writing invalid values to the slice is not allowed.

1 Like

Also as a minor item, remember that a typed write of T writes Uninit to all padding bytes. Only non-padding bytes are correct to .assume_init() on.

Edit: MaybeUninit::<u8>::assume_init() is the problem function.

2 Likes

Calling assume_init() on uninitialized padding is okay. The actual requirements of the function is that the bytes must be valid for the type being used. If the type has padding, then uninitialized is valid for those bytes, and therefore they may be uninitialized when you call assume_init().

1 Like

But this API returns a &[MaybeUninit<u8>] which doesn't have any padding bytes. So the uninit bytes from the orignial type get turned into u8 when calling .assume_init(), which is UB.
So that is where you would have to be careful with the API discussed here.

2 Likes

Converting &[MaybeUninit<u8>] to &[u8] is a separate problem from converting &mut [T] to &[MaybeUninit<u8>].

If T has padding bytes, then reinterpreting &[T] as &[MaybeUninit<u8>] is valid, but reinterpreting &[MaybeUninit<u8>] as &[u8] is not.

1 Like