vdrn
May 16, 2025, 9:05am
1
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
vdrn
May 16, 2025, 9:24am
3
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
vdrn
May 16, 2025, 1:17pm
5
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
alice
May 18, 2025, 5:56am
7
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
alice
May 18, 2025, 10:32am
9
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
luca3s
May 18, 2025, 11:06am
10
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
kornel
May 20, 2025, 11:34pm
11
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