Suspicious `transmute` in Tokio

I was digging around in the Tokio source code and noticed that ReadBuf::new uses transmute to convert from &mut [u8] -> &mut [MaybeUninit<u8>]1. This stood out to me, as it looks like it depends on the layouts of the wide pointers being this same, which (thanks to "Is the layout of a wide pointer stable?") I know is invalid.

I'm considering opening a PR to fix this, but wanted to make double-check really is invalid beforehand

1. Several other functions perform similar transmutes

13 Likes

Huh, I'm surprised I didn't catch this when we originally added ReadBuf to Tokio. It would certainly be more obviously correct to not use transmute here, which is itself enough reason to change it.

5 Likes

Also, would it have been better to open an issue instead?

PR made

4 Likes

Thanks!

1 Like

Though there may be a different way and better way to do it, I'm not sure the code in your first post is unsound. MaybeUninit is #[repr(transparent)] so I believe that the transmute is sound, given that your transmuting from one slide to another.

Yes, transmuting u8 into MaybeUninit<u8> is valid (or even theoretically [u8] into [MaybeUninit<u8>]), it's the reference that's the issue (&[u8] -> &[MaybeUninit<u8>]). The values referenced are valid, it's the (wide) reference themselves that have an unspecified layout and so are invalid.

Quoting @H2CO3 from "Is the layout of a wide pointer stable?":

1 Like

I agree that the wide pointer layout is unspecified, however I expect it to be the same for a given compiler version.

I don't expect transmute([ptr,len]) to give me a slice, but if U and T can be transmuted to one another and they have the same alignment I expect to be able to transmute &T to &U, is that not the case?

Although it is the case in practice, Rust does not guarantee that &[T] and &[U] store the pointer and length in the same order, even if T and U are of the same size and alignment.

9 Likes

TIL, thanks.

Is there any known scenario where a sufficiently smart compiler could take advantage of the order being allowed to differ?

I can think of some obscure micro-optimisations where field order might make a difference...

In the System V calling convention you'll pass the first 6 integer/pointer arguments via registers and the rest get pushed onto the stack. If you were passing a slice into a function such that the length field was passed via registers and the pointer was passed on the stack, and your function only uses the length field, then you will save a load instruction.

Another example is if you have an array of slices (&[&[T]]) and want to read all their length fields. In that case you've got to do some pointer arithmetic to get the correct byte offsets to each field, which will look like start + i*size_of::<&[T]>() + length_offset. If slice references were laid out such that the length comes first then length_offset would be zero and we could avoid a +1.

I'm sure we could come up with code where one order is more performant/convenient for &[T], while the other order is better for &[U].

That's me grasping at straws, though... Real code won't care about the layout of a slice reference, although the compiler does care about leaving it unspecified so it can fiddle with things later and not have people complain about ABI breaks.

1 Like

Sufficiently smart compiler may put fields commonly used together next to each others so they more likely be located within same cache line.

1 Like

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.