Rust: Document that casting &mut T to &mut MaybeUninit<T> is not safe

Created on 24 Nov 2019  路  16Comments  路  Source: rust-lang/rust

As discussed on Reddit, it turns out that casting &mut T to &mut MaybeUninit<T> is not a safe operation, since it allows UB in safe code.

Playground example demonstrating the UB: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=93dd41cf851bfc0de2233e0a83b4b778

It is probably a good idea to explicitly point that out in MaybeUninit docs.

C-enhancement T-doc T-lang

Most helpful comment

By the way, this issue is related to another interesting one: for &T -> &MaybeUninit<T> to be sound, we need to express that UnsafeCell and MaybeUninit do not commute! More info in the docs of ::uninit: https://docs.rs/uninit/0.4.0/uninit/out_ref/struct.Out.html#interior-mutability

All 16 comments

In the example you are already required to wrap the code in unsafe so I am curious why it's necessary to state explicitly for MaybeUninit<T>? transmute is unsafe for most types not just MaybeUninit.

@jannickj if you try to write a safe wrapper around MaybeUninit<T>, then the fact that T can be soundly transmuted into MaybeUninit<T> does not mean that you can transmute &mut T into &mut MaybeUninit<T> and expose the resulting reference to safe code. That's something easily overlooked.

FWIW, there's at least one crate in the wild that is unsound because of this mistake. Hey @danielhenrymantilla you probably want to fix it. I wrote you an e-mail regarding other stuff, no reply yet.

@Kixunil yes, thanks for the e-mail, I've been quite busy lately so I have not had much time to address this (or answer the e-mail 馃槈), but I'll try to have it fixed by tomorrow, with the previous versions being yanked (and the repo being published).


So, to summarize the issue, it turns out that &'a mut MaybeUninit<T> is far stronger than an OutRef<'a, T>, since it allows writing garbage. If we had an OutRef<'a, T> abstraction that only allowed to write a T into the pointee, then downgrading a &'a mut T to an OutRef<'a, T> should be sound, right? And I expect &'a mut MaybeUninit<T> -> OutRef<'a, T> to be sound as well.

image

Which, when mem::needs_drop::<T>().not(), means that OutRef<'_, T> has no downsides to write dava _w.r.t._ &'_ mut T


Thanks @Shnatsel and @Kixunil for reporting this!

Yes, I believe that's sound. Since I didn't get the answer to the e-mail I started creating a new crate from scratch that's addressing my needs. :) It's not published yet, but has bunch of other cool stuff. (Solves the same problem for slices, has tons of helper methods, iterator, safe cursor...) We can still cooperate if you want.

FYI, here's the crate: https://crates.io/crates/possibly_uninit I hope it will help prevent these kinds of bugs.

Interestingly BytesMut from bytes crate is also unsound. I fear that stabilizing MaybeUninit without having safe (or at least sound) abstractions for most common stuff was a major footgun.

How hard would it be to add some basic abstractions into core to avoid others screwing up? Does it require RFC? Maybe just something for slices and mutable references, not stabilizing too many methods yet.

To me this looks like someone mistakenly assumed &mut T to be covariant in T: in some sense, MaybeUninit<T> is a supertype of T, but that doesn't mean you can convert them below an &mut.

But, sure, if someone has a good suggestions for where in the MaybeUninit docs to put this, that seems fine.

@Kixunil Having a user-defined OutRef type seems like a reasonable approach. However, it looks like you are still using "uninit" terminology in your crate. I am particularly worried about this type. An "out ref" and a "reference to MybeUninit" are two fundamentally different concepts as this issue shows, so please be careful in picking terminology that conveys that difference! Your current names, I am afraid, will only make things worse. A "MaybeUninitSlice" should be just &mut [MaybeUninit<T>], by using the same term for the totally different concept of an OutSlice you might cause confusion.

How hard would it be to add some basic abstractions into core to avoid others screwing up? Does it require RFC? Maybe just something for slices and mutable references, not stabilizing too many methods yet.

See https://github.com/rust-lang/rust/issues/63569. However, what you are asking for is not "something for slices and mutable references to MaybeUninit"; what you are asking for is something entirely distinct, namely write-only references for initialization purposes.

Thanks for recommendation, good point, I will change it (that's why I called it "preview" :)). Is the name OutRef something standard and widely understood or just a suggestion? I didn't hear such thing before.

&out as complement to &mut has come up in discussions again and again over the years. This is related to in-place initialization / "placement new", which has a looooooong history in Rust. Swift has "in" and "inout" references; "out" seems like a natural name for this pattern.

However, note that there is AFAIK no way to implement safe &out as a library. The issue is that you need to somehow force a write to happen. (Well there might be tricks you can play with generative lifetimes but it's going to not be very ergonomic.) The BufMut issue is special because the safety of the problematic API relies on things already being initialized to begin with. So, I think there are very few cases where OutSlice actually makes an API safe. That type is not truly an out-reference as it doesn't enforce initialization. Hm.

OK, I will rename it to OutRef and OutSlice.

Yes, the lack of enforcing is something I noticed. It's the primary reason I added Cursor for slices and I was thinking about doing something similar for Sized types (call it Slot? IDK).

It is totally true that "OutSlice" is not very useful in itself as the code interacting with it will be mostly unsafe (it can be nicely seen in Cursor impls). However, I do think that preventing footguns in unsafe code still has a value. At least documentation value that could prevent mistakes like these.

By the way, this issue is related to another interesting one: for &T -> &MaybeUninit<T> to be sound, we need to express that UnsafeCell and MaybeUninit do not commute! More info in the docs of ::uninit: https://docs.rs/uninit/0.4.0/uninit/out_ref/struct.Out.html#interior-mutability

When I first saw the title, I was expecting that casting is &mut T as &mut MaybeUninit<T>.
Isn't it a transmute and not a cast ? I would like to change the issue title to reflect that.

Is &mut *(foo as *mut _ as *mut MaybeUninit<T>) considered transmute?
Anyway, the title should (also) s/safe/sound/. The fact it's unsafe is already obvious.

It's not always unsound, just sometimes. The usual way of describing this is unsafe.

Was this page helpful?
0 / 5 - 0 ratings