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

Implementation of BufMut for &mut [u8] is unsound #328

Closed
Kixunil opened this issue Nov 27, 2019 · 19 comments
Closed

Implementation of BufMut for &mut [u8] is unsound #328

Kixunil opened this issue Nov 27, 2019 · 19 comments
Labels
Milestone

Comments

@Kixunil
Copy link

Kixunil commented Nov 27, 2019

Example how safe code can cause UB using BufMut:

let mut buf = [42];

BufMut::bytes_mut(&mut buf)[0] = MaybeUninit::uninit();

println!("Reading this value is UB: {}", buf[0]);

As discussed at rust-lang/rust#66699 casting &mut [T] to &mut MaybeUninit<T> isn't safe. Thus bytes_mut must return a newtype that prevents overwriting value with invalid values. See how I addressed it in my crate possibly_uninit.

It'd be the best to share common newtype slice implementation to not fragment the ecosystem. You can expect full support from me when it comes to using my crate as a dependency. That includes making you maintainers, if you're interested.

@seanmonstar
Copy link
Member

I'm amazed and yet not surprised. Thanks for pointing this out! It does seem like a tragedy that MaybeUninit::uninit() is safe to call.

We may end up needing newtype in bytes as you mentioned. I need to think about this more.

@carllerche
Copy link
Member

As sean pointed out, it seems that the source of UB is due to MaybeUninit::uninit() being safe to call. The fix would be for bytes to define its own UninitBuf type that only exposes *mut u8 and usize (len).

@carllerche
Copy link
Member

While we should obviously fix this, I don't think it merits an emergency patch. We probably can wait until 0.6 (in a few months). This doesn't seem worse than the state of 0.4.

@Kixunil
Copy link
Author

Kixunil commented Nov 27, 2019

Yes, if MaybeUninit::uninit() was unsafe to call, then it wouldn't cause unsoundness. Since MaybeUninit was design to allow working with uninitialized memory safely, I think it's correct to make uninit() safe and all other code should ensure correctness. That being said, I suspect that not providing these abstractions in core when stabilizing MaybeUninit was a footgun.

What do you think about my approach of creating special slice type? Would just using it and depending on possibly_uninit be sufficient? Would you like some changes to it? Or would you like to avoid dependency?

I agree it's probably not emergency situation, maybe it should be documented, though.

@carllerche
Copy link
Member

Something like your slice is probably better. I would rather avoid adding a dependency on a third party crate as we are pushing hard for 1.0.

@Kixunil
Copy link
Author

Kixunil commented Nov 27, 2019

Hmm, I see the good reason to avoid it because of pushing for 1.0. Would separating only the type itself into a separate crate help? Such crate could stabilize very quickly (Instantly?) and my crate would just depend on it too. Alternatively, it could be implemented in bytes directly, but I don't think it'd be nice to require people who don't want to do anything with bytes to depend on bytes just to get MaybeUninitSlice.

I think that this separate crate should be called core. Eventually. :)

@seanmonstar
Copy link
Member

cc @RalfJung

@RalfJung
Copy link
Contributor

RalfJung commented Nov 28, 2019

Good catch!

It does seem like a tragedy that MaybeUninit::uninit() is safe to call.

Strongly disagreed. MaybeUninit was designed for a particular purpose, and for that purpose it makes no sense to make uninit() unsafe.

This looks to me like someone made the mistake of realizing that MaybeUninit<T> is like a supertype of T, and thus &mut MaybeUninit<T> also would be a supertype of &mut T -- but &mut T is invariant, not covariant, so that is not correct. Mistakes happen, and that's okay, but I wouldn't blame MaybeUninit's API for this.

That being said, I suspect that not providing these abstractions in core when stabilizing MaybeUninit was a footgun.

MaybeUninit was never intended to be used for what it is being used as here, and that use-case was also not even mentioned during the many, many months of pre-stabilization discussion.

@Kixunil
Copy link
Author

Kixunil commented Nov 28, 2019

Fair, do you think the apparent popularity justifies including some standardized types into core to prevent future accidents? It could be very conservative for starters and expanded later.

@RalfJung
Copy link
Contributor

RalfJung commented Nov 28, 2019

the apparent popularity

I don't see one instance of this bug as an indication of popularity. ;) An API that both is geared towards in-place initialization (and thus handles references to MaybeUninit<T>) and allows safe construction for borrowed already initialized data is a somewhat odd thing.

One could just as well argue that whatever safely turns an &mut [T] into a BufMut is wrong. I think I understand the design decisions that lead here, but I don't see any reason to expect this to happen frequently.


In fact, I think just making MaybeUninit::uninit() unsafe does not fix the soundness hole (assuming that there is a way to construct a BufMut for uninitialized memory, which is the entire point):

fn with_uninit_bufmut(buf1: impl BufMut) {
  let mut buf2 = [42];
  BufMut::bytes_mut(&mut buf2)[0] = buf1.bytes_mut()[0];
  // If `buf1` is uninitialized, now so is `buf2`.
}

@Kixunil
Copy link
Author

Kixunil commented Nov 28, 2019

I meant popularity of this crate and possibly pattern. But that may still be overstatement.

I think the whole thing is stemming from the fact that we didn't have a nice way of working with uninitialized memory from the beginning, so people were using &mut [u8] instead (see std::io::Read which unfortunately is a stable API), so now people are trying to make stuff backwards compatible.

If the primitives for working with uninit memory were in Rust from the beginning, I don't think we would be having this discussion.

Good point about uninit() being safe.

@RalfJung
Copy link
Contributor

Good point about uninit() being safe.

Given that, is there a proposed fixed API? My counterexample will work for any &mut [Wrap<T>] where Wrap<u8> is Copy. It's not enough to have a slice-of-newtype, you really need a newtyped slice that somehow disallows this. Is there a concrete proposal for that?

@Kixunil
Copy link
Author

Kixunil commented Nov 28, 2019

Well, I proposed to cut out MaybeUninitSlice (obviously, renaming it as you recommended) from my crate into a separate one, which can be stabilized quickly, then return that thing instead of &mut [MaybeUninit<u8>]. I don't know if there's any better way.

@RalfJung
Copy link
Contributor

Ah I see. Yes that API looks good on a cursory glance.

@carllerche
Copy link
Member

I would say either something should exist in std or bytes will define something like MaybeUninitSlice specifically for [u8] (maybe UninitBuf or something). If there is going to be a standard that all crates use, it should be in std.

@Kixunil
Copy link
Author

Kixunil commented Nov 28, 2019

You mean core, right? There's no reason it needs OS or allocator. :)

@carllerche
Copy link
Member

Sure. I use std / core interchangeably to mean "what rustc ships with" :)

@benma
Copy link

benma commented Apr 3, 2020

Imho this bug warrants a quick documentation update if a fix is taking longer. I was about to use the &mut [u8] implementation of BufMut and almost missed that it's not safe to use.

carllerche added a commit that referenced this issue Oct 19, 2020
The way BufMut uses MaybeUninit can lead to unsoundness. This replaces
MaybeUnit with a type owned by bytes so we can ensure the usage patterns
are sound.

Refs: #328
@carllerche
Copy link
Member

Fixed by #433

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants