Why is it unsafe to pin a shared reference?

I understand why it is unsafe to pin &mut T, because the target could create and store a reference to itself, then after the pin is dropped, you could mem::swap with another reference. However, given a reference &T to a type which is !Unpin, there is no way to move anything out of it or make it self-referential, except via interior mutability or static references (which would still require unsafe code in order to actually write the information to T). Why is Pin::new unsafe for Pin<&T> if T: !Unpin? What is an example of how this could cause unsafety? I've looked and looked but I can't find anything.

For example, let's say I construct a Pin<&T> which borrows from a Box<T>. I perform some mystery operation with the pin. Now, I first have to drop the pin and the pin's reference (as well as all other references to T) before I can get &mut T from the box. Therefore, the mystery operation that is given Pin<&T> must somehow require T to stay pinned even after the pin is dropped. How is this possible, and in what situation would this be the case? The only way I can think of is using interior mutability to store *const T inside T, but then wouldn't it require unsafe code to read from that pointer later?

let mut not_unpin = get_not_unpin();
{
    let not_unpin_ref = &not_unpin;
    with_pinned(Pin::new(not_unpin_ref);
}
mem::swap(&mut not_unpin, get_another_not_unpin());

edit: Added not_ prefix everywhere. Honestly I don't like the double negative in !Unpin :stuck_out_tongue:

3 Likes

Thanks for your answer. What is with_pinned here? Is it fn with_pinned(p: Pin<&T>)? If so, then how would calling mem::swap be unsafe? Is the problem that with_pinned stores a a pointer (*const T) in T using interior mutability, then later reads from that pointer with unsafe code? Also do you mean "not unpin" because if everything here implemented Unpin then there would be no problem.

If something is stored under Pin<&T>, other code is allowed to assume the T will never move - that's the main point of Pin. The combination of that assumption and mem::swap is unsound because mem::swap moves the underlying pinned item.

2 Likes

Pin requires that the data behind the pointer not be moved or otherwise invalidated until it is dropped, in other words, for the entire lifetime of the data.

While it is guaranteed by the borrow checker that during the lifetime of a Pin<&T>, the instance of T doesn't get moved, the compiler can't guarantee that it won't get moved after that pinned reference goes out of scope.

Therefore Pin<&T>::new_unchecked has the safety requirement that between when the pinned reference goes out of scope and when the instance of T gets dropped, the instance of T does not get moved or invalidated.

If we allowed developers to create Pin<&T> from a safe function, they could violate this invariant, therefore to do so would be unsound; as demonstrated by @Hyeonu

4 Likes

In that case, what exactly is the purpose of Pin<&T>? Does it require interior mutability in order to do anything useful? Or, for example, since Pin::as_ref() returns Pin<&T>; what is a real-world usage of Pin::as_ref()?

EDIT: In other words, why would a function ever need to take Pin<&T> instead of &T? If there were no possible reason, then it would make sense that Pin::new would be unnecessary for Pin<&T>, but Pin::as_ref() exists so there must be a reason?

The pointer type and the guarantees of Pin are orthogonal. Writing Pin<&T> allows a type to both rely on it never being moved until dropped, and additional allows multiple such reference to exist at the same time. The latter would not be possible with Pin<&mut T> which requires all the uniqueness guarantees of standard mutable references.

1 Like

Thank you, I believe I understand the guarantees of Pin that the address will never move. Part of my question is: what is a real-life use case for Pin<&T>? I really can’t think of any, but Pin::as_ref() exists, so there must be some use for it? The reason I am wondering this is because I wonder if I misunderstood something, since the way I understand it means that Pin<&T> is useless, but there exists a method to create it, so I am trying to figure out if this is because I misunderstood, there is a use case I can’t think of (or uses interior mutability with a pointer, which I imagine is not the reason for adding the method), or where this discrepancy comes from.

Edit: I want to clarify, part of my question is, what is the benefit of Pin<&T> over just &T? What is the benefit of a guarantee that a shared reference is pinned? Creating self-referential or intrusive structures (AFAIK the main uses for Pin) need Pin<&mut T> to do anything useful, so why would you ever need Pin<&T> rather than just &T? (Keeping in mind that you can always convert Pin<&T> to &T via deref; and also why is the reverse of that conversion not always safe, or is it?)

If you have a Pin<&T> you know that value will not move until it is dropped, so if you have information about when it is dropped, you can take a raw pointer to it and store it somewhere and rely on it remaining valid.

2 Likes

Why do you think that? There's nothing stopping T from containing a Cell<*const Self> that would allow it to use self-referential properties even through a shared, pinned reference.

2 Likes

The conversion Pin<&T> to &T converts one type to another with strictly less guarantees. It's safe because the shared reference prevents the caller from arbitrarily moving the T. Keep in mind that Pin is a shallow property: Having a Pin<&T> does not trivially allow creating a pin to any of the fields of T. In particular, and referring to my example above, a Pin<&Cell<T>> would not allow safely creating a Pin<&T>. Even though you could cast the former to &Cell<T>, replace the T and thus move it, this can not violate any pinning properties of T since there was no Pin<&T> in the first place.

2 Likes

Note that Pin::<&'static [mut] _>::new_unchecked is always sound to call, thanks to the 'static, so uniqueness, as @HeroicKatora pointed out, is not really the issue with Pin.

Pin<impl Deref[Mut]<Target = T>> has two purposes:

  1. Unless T : Unpin to enable opting out of all of Pin guarantees, Pin will restrict mutable access to the pointee, mainly to avoid mem::swap & co. (since these things enable moving stuff without running its destructor). In practice, mutable access is provided per se by exclusive access (&mut), so there is indeed a special interaction going on with Pin<impl DerefMut<Target = impl !Unpin>>, whereby you lose all the DerefMut non-unsafe API and are restricted to, either Deref-only APIs, or using unsafe to discharge the compiler prom preventing unsoundness, and carry with that responsibility yourself (you can use an obtained &mut (impl !Unpin) soundly, provided that you everytime run the destructor before "(re)moving" the value.

    But mutation can also use shared / aliased / interior mutability, to enable things like, for instance, RefCell::swap; so Pin interaction can not be reduced to &mut references. For this example, removing the "problematic" API of RefCell to only allow the aformentioned "(re)moving with drop calls" enables using it with Pin while also providing Pinning projections: c.f., ::pin_cell

  2. Until the pointee T is actually dropped, it shall remain where the Pin-ned pointer witnessed it, even beyond the lifetime of that pointer.

    This is why Pin::<&'any [mut] T>::new_unchecked is unsafe: the lifetime 'any could be arbitrarily small, as in @Hyeonu's great counter-example, meaning that the Rust compiler no longer checks the invariants w.r.t. the pointee.

I have made an actual PoC based on @Hyeonu's theoretical counter-example:

  • I am using an exclusive (&mut) reference for convenience, but you will notice that the issue of the code does not lie in the mut-ness of the reference, but rather on the lack of destructor call, given the short lifetime of the pinned reference.
/// The following code causes Undefined Behavior (Run with MIRI to confirm)
fn main ()
{
    use some_lib::{BackPtr, SmartInteger};

    let back_ptr = BackPtr::new();
    {
        let mut smart_integer = SmartInteger::new(42);
        {
            let at_smart_integer = unsafe {
                Pin::new_unchecked(&mut smart_integer)
            };
            at_smart_integer.register_self_to(&back_ptr);
            assert_eq!(back_ptr.deref_read(), Some(42)); // OK
        }
        mem::forget(smart_integer); // swap isn't even needed, forget suffices
    }
    dbg!(back_ptr.deref_read()); // UAF
}
  • Playground

  • Since someone having ownership of the value after the lifetime of the pinned reference can thus cause unsoundness without using unsafe, it means that the unsoundess must lie somewhere else: in the Pin::new_unchecked call.

    • Note that this sentence does not assume the uniqueness or lack thereof of the pinned reference, so there is no difference between &mut and &.

      • If anyone is skeptical w.r.t this point, I can rework the example to cause unsoundness with no &mut nor interior mutability whatsoever (except for BackPtr itself of course).
3 Likes

Thank you all for your help, I think I've figured it out! @Yandros That playground was especially helpful!

Now I can see why Pin::new(impl !Unpin) is unsafe, because for example you can call mem::forget() on a value on the stack which will deallocate the memory without dropping the target, which breaks the Drop guarantee. This is safe for Box<T> because calling mem::forget() won't actually deallocate the memory, it would be the same as calling Box::leak(). Also I can see how this would easily be adapted if SmartInteger::register_self_to took Pin<&Self> instead of Pin<&mut Self>, and then there would be a clear benefit to using a shared reference: you could alias the pin. I was getting caught up on the fact that you would need interior mutability to create a self-referential struct (which seemed like a bad idea), but using interior mutability makes a lot of sense if you want to store a reference to the target outside of the target and take advantage of the Drop guarantee. Now I can see why it would be useful to call Pin::as_ref() or Pin::into_ref().

1 Like

"Once you have eliminated the impossible, whatever remains, no matter how improbable [it seems to you right now], must be the truth"

6 Likes

:smile:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.