Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Document that casting &mut T to &mut MaybeUninit<T> is not safe #66699

Open
Shnatsel opened this issue Nov 24, 2019 · 20 comments
Open

Document that casting &mut T to &mut MaybeUninit<T> is not safe #66699

Shnatsel opened this issue Nov 24, 2019 · 20 comments
Labels
A-docs Area: documentation for any part of the project, including the compiler, standard library, and tools C-enhancement Category: An issue proposing an enhancement or a PR with one. T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@Shnatsel
Copy link
Member

Shnatsel commented Nov 24, 2019

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.

@jannickj
Copy link

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.

@JohnTitor JohnTitor added C-enhancement Category: An issue proposing an enhancement or a PR with one. A-docs Area: documentation for any part of the project, including the compiler, standard library, and tools labels Nov 24, 2019
@Kixunil
Copy link
Contributor

Kixunil commented Nov 24, 2019

@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.

@danielhenrymantilla
Copy link
Contributor

danielhenrymantilla commented Nov 24, 2019

@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!

@Kixunil
Copy link
Contributor

Kixunil commented Nov 25, 2019

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.

@Kixunil
Copy link
Contributor

Kixunil commented Nov 25, 2019

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

@Kixunil
Copy link
Contributor

Kixunil commented Nov 27, 2019

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.

@Kixunil
Copy link
Contributor

Kixunil commented Nov 27, 2019

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.

@RalfJung
Copy link
Member

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.

@RalfJung
Copy link
Member

@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 #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.

@Kixunil
Copy link
Contributor

Kixunil commented Nov 28, 2019

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.

@RalfJung
Copy link
Member

RalfJung commented Nov 28, 2019

&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.

@Kixunil
Copy link
Contributor

Kixunil commented Nov 28, 2019

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.

@jonas-schievink jonas-schievink added the T-lang Relevant to the language team, which will review and decide on the PR/issue. label Mar 6, 2020
WaffleLapkin added a commit to WaffleLapkin/arraylib that referenced this issue Apr 5, 2020
…om_init`

Well, that's weird. I've had a safety comment:
```rust
// `MaybeUninit<T>` is guaranteed to have the same ABI as `T`, so
// it's safe to cast `&mut [T]` to `&mut [MaybeUninit<T>]`
```
But it's wrong. `&mut T` and `T` isn't the same thing. While it's true that `T => MaybeUninit<T>`
or the same for an owned container (array, box, etc) should be fine, for an unique borrowed
container (`&mut _`, `&mut [_]`) it is definitely **not fine**, because the original owned value
remains `T`. Example of such a UB in safe code:

```rust
let mut a = ["string"];
<_>::from_init_mut(&mut a[..])[0] = MaybeUninit::uninit();
println!("{}", a); // segfault
```

You can also think of `MaybeUninit<T>` as a supertype of `T` and
then note that `&mut T` is invariant over `T`: https://doc.rust-lang.org/nomicon/subtyping.html#variance

The weirdest part of all of this is that I haven't tested those functions.
There aren't any tests. There aren't any test for safe function with unsafe in it!

I am ashamed...

A related issue in `rust-lang` repo, about documenting unsoundness
of `&mut T => &mut MaybeUninit<T>`: rust-lang/rust#66699
@danielhenrymantilla
Copy link
Contributor

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

@tesuji
Copy link
Contributor

tesuji commented Oct 17, 2020

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.

@Kixunil
Copy link
Contributor

Kixunil commented Oct 17, 2020

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.

@thomcc
Copy link
Member

thomcc commented Oct 30, 2020

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

@ojeda
Copy link
Contributor

ojeda commented Jan 19, 2021

Something cannot be sometimes unsound -- it is either unsound or not. Unsafe does not imply unsound or sound.

@thomcc
Copy link
Member

thomcc commented Jan 19, 2021

It's unsound if you write MaybeUninit::uninit to it, or expose it to safe code you don't control (that can do this trivially). It's sound otherwise.

@ojeda
Copy link
Contributor

ojeda commented Jan 19, 2021

That means it is unsound. It doesn't matter that sometimes there may not be code triggering the UB.

@Kixunil
Copy link
Contributor

Kixunil commented Jan 19, 2021

So the title should be "Document that casting &mut T to &mut MaybeUninit and exposing it to safe code is not sound"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-docs Area: documentation for any part of the project, including the compiler, standard library, and tools C-enhancement Category: An issue proposing an enhancement or a PR with one. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

10 participants