Is the UnsafePinned RFC wrong about being able to return `&mut T` from `get_mut_unchecked`?

I find the UnsafePinned RFC a little cryptic; it's clearly written for experts who are already well steeped in the design space and the dynamic analysis tools for aliasing rules, and doesn't really explain its assumptions. Here is one thing I'm confused about that I'm hoping someone here can explain; I can't figure it out, and it kind of seems like the RFC is wrong.


I think I understand the overall point here: if a struct S contains an UnsafePinned<T> field then the compiler should be prevented from assuming &mut S is an exclusive reference to all of the memory in S, or that &S prevents modification. It allows for all of the following:

  • Two &mut UnsafePinned<T> references to exist at the same time for the same field within S.[1]

  • The contents of the T field to change, even when somebody holds an &mut S, through a pointer not derived from that reference.

  • The contents of the T field to change even when somebody holds an &S or an &UnsafePinned<T>.

Okay, so far so good; it's an extension of UnsafeCell. But then the RFC also says this in an off-hand comment:

We could even soundly make get_mut_unchecked return an &mut T, given that the safety invariant is not affected by UnsafePinned. But that would probably not be useful and only cause confusion.

Now this part doesn't make sense to me. All of the properties above are about references to the UnsafePinned itself, or to structs that contain it—things where the compiler can see that there is an UnsafePinned involved. But this side note seems to be totally the opposite: it's saying that you could soundly create two &mut T, where T doesn't necessarily contain UnsafePinned.

Surely this can't actually work, right? It would allow you two create two exclusive references, presumably with noalias set because T isn't special, at the same time. You could smuggle those to a module where UnsafePinned is nowhere to be seen and cause chaos, right?


As far as I can tell, the only sound ways to use UnsafePinned<T> are:

  • Always access the T via raw pointer, being careful not to introduce data races.
  • Access the T via normal references obtained via dereferencing the result of the raw pointer APIs, using some higher-level argument about mutual exclusion to ensure that the usual aliasing rules are obeyed.

Is that not correct?


  1. The RFC also shows a duplicate function that allows two &mut S references to exist that it says doesn't cause immediate UB, although this seems wrong to me. Surely this isn't intended to be viral to parent structs. What about the other fields of S? Maybe this is related to my main question. ↩︎

disclaimer: I'm not expert on this subject, I just happen to be pondering over UnsafePinned recently for a project. my opionion might be completely wrong. please correct me if you find any.

my interpretation is, this statement is only true if creating aliasing &mut UnsafePinned (in other words, duplicating &mut UnsafePinned) were sound, but it isn't.

the side note says it would be sound if we made get_mut_unchecked return &mut T. but this alone doesn't give you two aliasing &mut T from a single &mut UnsafePinned, if you want two &mut T, you still need two aliasing &mut UnsafePinned<T>, which you CANNOT get soundly.

it's the same reasoning for the duplicate() example: even if duplicating &mut UnsafePinned is not immediate UB, using them incorrectly can violate the safety invariant. the rfc uses mem::swap() as one example of "incorrect" usage to cause UB, your example of deriving two aliasing &mut T is just another "incorrect" usage that violates the safety invariant.

that said, although the rfc is accepted, what end up being implemented might not reflect precisely what's being said in the rfc, and the rfc might also be revised or superseded. that's what the tracking issue is all about, and there are still many unresolved questions remaining.

Thanks. You're right that the two are connected via borrowing; I hadn't thought about that. But it seems fairly clear to me that the RFC is intending to say &mut UnsafePinned<T> can be duplicated:

  • The emphasis on "you do not control" here, implying it's okay when you do control it:

    However, what is not analogous is that &mut S, when handed to safe code you do not control, must still be unique pointers! This is because of methods like mem::swap that can still assume that their two arguments are non-overlapping. (mem::swap on two &mut UnsafePinned<i32> may soundly assume that they do not alias.) […] You can write your own code handling &mut S and as long as that code is careful not to make use of this memory in the wrong way, potential aliasing is fine.

  • The fact that it says duplicate doesn't cause immediate UB, despite it having this signature implies the stronger statement that it's okay to even duplicate references to structs containing UnsafePinned (this seems wrong!) as long as you don't access those references in a problematic way:

    fn duplicate<'a>(s: &'a mut S) -> (&'a mut S, &'a mut S)
    

I think I agree with you that this basically makes no sense, from both angles:

  • If you wind up with two &mut T then all bets are off, because the compiler can't see those should be noalias because T may be UnsafeUnpin, even if UnsafePinned<T> is not.

  • If you wind up with two &mut S then presumably you can get into the same situation by projecting that exclusive reference to one of the other fields of S, which may also not be UnsafeUnpin.

This is what I mean about the RFC being cryptic. It really ought to explain this in more detail.

UnsafePinned prevents this. What it doesn't prevent is other Rust code making assumptions on what can be done on &mut references (e.g. mem::swap). For this reason exposing the &mut references to safe code you don't control is not sound. But just having the two &mut references is not UB until you do something bad with it.

Here the reasoning is that having it return a &mut reference might make it look like that it's fine to share that reference, but it is not. The safety requirements for duplicating the &mut (or Pin<&mut>) reference also affect what you can do with the return value of get_mut_unchecked, but since this is "far" from the duplication people might forget that. Returning a raw pointer here forces another use of unsafe that reminds you about that, but is not strictly necessary due to being just a reminder.

all that matters to the compiler is wether a type is UnsafeUnpin or not, not even specifically where any UnsafePinned might be.

in the folowing i will call T any UnsafeUnpin type and S any !UnsafeUnpin type.

for your first point,
given any type, the compiler always knows wether it is an S or a T, and so it is able to decide wether the references to it should be noalias or not.

for your second point,
if you have two &mut S, then anything you may do with one of them may cause UB, so you need to be very careful. having them is sound, it does not cause UB by itself, but it is not safe.
projecting both reference to the same field field of type T is unsound, it is immediate language UB.

this function :

pub fn get_mut_unchecked(&mut self) -> &mut Inner

is a safe and sound function for the exact same reason mem::swap is.
when you created two &mut Ss, you needed unsafe to do it, and part of the contract of that unsafe is that you wouldn't use them to create aliasing &mut Ts

What the RFC does is make it so that when the target type contains an UnsafePinned, mutable references to that target type behave like raw pointers aliasing wise. Reborrowing and other similar operations do not create a sub-tag in the borrow tree, so the entire borrow tree is just a single tag shared by all of the references. This means that all such mutable references are indistinguishable from an aliasing perspective. Or in other words, from the aliasing model's perspective they are the same mutable reference. According to the model, you don't have multiple mutable references at all! It's just the same single mutable reference that "exists in multiple places".

When you create a &mut F to some field inside the cell, what happens is that since F does not contain an UnsafePinned, then the creation of that mutable reference creates a child node in the borrow tree. The new node is a child of the super node shared by all &mut UnsafePinned<T> references. Or in other words, the &mut F is considered derived from all of the many mutable references you may have to the UnsafePinned. Mutable references are allowed to co-exist with the mutable reference they are derived from, so there is no problem. Even if you actually have multiple of them, the &mut F is derived from all of them, and is therefore allowed to alias with them.


Or a less precise version that may be easier to use in practice:

One way to think of it is that the &mut UnsafePinned<S> pointer does not "count" when it comes to exclusivity. You can have one pointer to the field together with a pointer to the struct because the struct pointer doesn't count. You can have more than one pointer to the struct, because none of them count. But if you have two pointers to the same field, then you have two pointers that count, and now it's no longer exclusive anymore.

I don't believe that this is allowed. From the RFC &UnsafePinned<T> is not special

UnsafePinned: disables aliasing (and affects but does not fully disable dereferenceable) behind mutable refs, i.e. &mut UnsafePinned<T> is special. UnsafePinned<&mut T> (by-val, fully owned) is not special at all and basically like &mut T; &UnsafePinned<T> is also not special.

And on the docs of UnsafePinned::get

Note that &UnsafePinned<T> is read-only if &T is read-only.


Edit:

I dug into this a bit more and it looks like things have changed since I last saw it

UnsafePinned does now subsume UnsafeCell

Decided here: Decide what to do about `UnsafePinned` and safe `Pin::as_ref` · Issue #137750 · rust-lang/rust · GitHub

And in docs: UnsafePinned in std::pin - Rust

This also subsumes the effects of UnsafeCell, i.e., &UnsafePinned<T> may point to data that is being mutated.

It may be useful as an analogy to note that UnsafeCell has a very similar effect: it makes it so that reborrows of shared references do not create sub-tags in the borrow tree, meaning that all shared references to the UnsafeCell have the same tag and are indistinguishable under the aliasing model.

So in that sense the two cells are very similar. One prevents creation of sub-tags for &T. The other prevents creation of sub-tags for &mut T.

Of course UnsafeCell also has some further effect on top of this so that &T does not imply immutable.