For user-defined smart pointer types P that deref to an !Unpin type, the API of Pin exposes exactly three different ways of creating a Pin<P> value without already having a Pin<P> value:
- Calling
Pin::new_unchecked.
- Performing an unsizing coercion.
- Performing a subtyping coercion.
This issue is about methods 1 and 2 only (see #134407 on method 3). Methods 1 and 2 are both gated behind use of unsafe:
- The
Pin::new_unchecked method itself is unsafe.
- Unsizing coercions are only possible with pointers that implement the unsafe trait
PinCoerceUnsized.
However, the safety requirements listed about the pointer type are insufficient.
Pin::new_unchecked (affects stable)
The safety requirements of Pin::new_unchecked says a lot of stuff, but with one exception, everything it says talks about specifically the value behind the pointer, and not the pointer itself. The only thing it says about the pointer is this:
By using this method, you are also making a promise about the Deref, DerefMut, and Drop implementations of Ptr, if they exist. Most importantly, they must not move out of their self arguments: Pin::as_mut and Pin::as_ref will call DerefMut::deref_mut and Deref::deref on the pointer type Ptr and expect these methods to uphold the pinning invariants.
However, there are safe traits other than Deref, DerefMut, and Drop we need to be careful about. Here is an example illustrating why Clone is also important:
use std::ops::Deref;
use std::sync::Arc;
use std::pin::Pin;
use std::marker::PhantomPinned;
struct Malicious<T> {
inner: Arc<T>,
extra: Arc<T>,
}
impl<T> Deref for Malicious<T> {
type Target = T;
fn deref(&self) -> &T {
&self.inner
}
}
impl<T> Clone for Malicious<T> {
fn clone(&self) -> Malicious<T> {
Malicious {
// notice extra is moved to inner
inner: self.extra.clone(),
extra: self.extra.clone(),
}
}
}
fn main() {
// we consider arc1 pinned
let arc1 = Arc::new(PhantomPinned);
// we do not consider arc2 pinned
let mut arc2 = Arc::new(PhantomPinned);
let m: Malicious<PhantomPinned> = Malicious {
inner: arc1,
extra: arc2.clone(),
};
// SAFETY: `arc1` will not be moved out of again
let m_pinned = unsafe { Pin::new_unchecked(m) };
let m2 = m_pinned.clone();
let m2_ref: Pin<&PhantomPinned> = m2.as_ref();
// somehow we obtained a pinned reference to arc2
assert!(core::ptr::eq(&*m2_ref, &*arc2));
}
PinCoerceUnsized (unstable only)
The same applies to the PinCoerceUnsized. It makes various requirements about the Deref and DerefMut traits, but it says nothing about Clone.
#![feature(pin_coerce_unsized_trait, derive_coerce_pointee)]
use std::marker::CoercePointee;
use std::sync::Mutex;
use std::ops::Deref;
use std::sync::Arc;
use std::pin::{Pin, PinCoerceUnsized};
use std::marker::PhantomPinned;
static EXTRA: Mutex<Option<Arc<PhantomPinned>>> = Mutex::new(None);
#[derive(CoercePointee)]
#[repr(transparent)]
struct Malicious<T: ?Sized> {
inner: Arc<T>,
}
// SAFETY: A given value of `Malicious<T>` never changes which object it
// points at.
unsafe impl<T: ?Sized> PinCoerceUnsized for Malicious<T> {}
impl<T: ?Sized> Deref for Malicious<T> {
type Target = T;
fn deref(&self) -> &T {
&self.inner
}
}
impl Clone for Malicious<dyn MyTrait> {
fn clone(&self) -> Malicious<dyn MyTrait> {
Malicious {
inner: EXTRA.lock().unwrap().clone().unwrap(),
}
}
}
trait MyTrait {
fn to_phantom_pinned(self: Pin<&Self>) -> Option<Pin<&PhantomPinned>>;
}
impl MyTrait for PhantomPinned {
fn to_phantom_pinned(self: Pin<&Self>) -> Option<Pin<&PhantomPinned>> {
Some(self)
}
}
impl MyTrait for i32 {
fn to_phantom_pinned(self: Pin<&Self>) -> Option<Pin<&PhantomPinned>> {
None
}
}
fn main() {
*EXTRA.lock().unwrap() = Some(Arc::new(PhantomPinned));
let foo = Pin::new(Malicious { inner: Arc::new(42) });
let bar: Pin<Malicious<dyn MyTrait>> = foo;
let baz = bar.clone();
let abc: Pin<&PhantomPinned> = baz.as_ref().to_phantom_pinned().unwrap();
let extra = EXTRA.lock().unwrap().take().unwrap();
// somehow we obtained a pinned reference to extra
assert!(core::ptr::eq(&*abc, &*extra));
}
Everything in the safety requirements of PinCoerceUnsized talks about the fact that an individual value must not change its identity, and that is true for Malicious.
Not just the Clone trait
There are a bunch of other traits that can potentially be abused in this manner. For example, the implementations of fmt::Debug, fmt::Display, and fmt::Pointer are all called with an &P reference, and creating such a reference is unsound.
Potential solutions
Reword both safety requirements
We can of course reword both Pin::new_unchecked and PinCoerceUnsized to comprehensively list all traits you must not implement maliciously.
Implement dangerous traits in terms of new traits
We could introduce traits like these:
trait PinClone {
fn clone(self: &Pin<Self>) -> Pin<Self>;
}
impl<Ptr: PinClone> Clone for Pin<Ptr> { ... }
impl PinDebug {
fn fmt(self: &Pin<Self>, f: &mut Formatter<'_>) -> Result<(), Error>;
}
impl<Ptr: PinDebug> Debug for Pin<Ptr> { ... }
// similarly for Display and Pointer
This would be in addition to the existing unsafe trait PinCoerceUnsized.
The main disadvantage of this is that it's a breaking change.
Expand the scope of PinCoerceUnsized
We already have an unsafe trait guarding unsizing coercions called PinCoerceUnsized. We could expand the scope of this trait. For example, we could rename PinCoerceUnsized to PinSafe and rewrite its documentation to also make requirements about Clone and any other trait like this. Then, we can reword Pin::new_unchecked to require that the pointer type satisfies the safety requirements of PinSafe (we cannot require that it implements PinSafe for backwards compat, but we can require that it could implement it).
Perhaps in a future edition, Pin::new_unchecked could require P: PinSafe to simplify what is listed in its safety requirements.
Click to view sample documentation for PinSafe
Trait indicating that this pointer type is safe to use with Pin.
By implementing this unsafe trait, you are asserting that the API of Pin<_> is sound when Self is the pointer type.
This trait has to do with the pointer type, and should not be confused with the pointee type. For example, given the type Pin<Box<SomeTypeOfFuture>>, the type that must be pin safe is Box, not SomeTypeOfFuture.
This is important because there are certain operations that are not safe to perform on a pinned value in general, but Pin implements various traits by performing those dangerous operations and passing the resulting dangerous values to safe trait implementations on the pointer type. A pointer is considered pin safe if the implementations of these safe traits does not abuse these dangerous values.
A few examples of dangerous operations that Pin may perform when it calls into trait implementations of the pointer type can be found below:
- Calling
Pin::new_unchecked. This is dangerous because it assumes the pointed-to value is pinned.
- Creating a
&mut T to the pinned value. This is dangerous because it can be used to move the value (e.g. with mem::swap).
- Creating a
&P to the pointer type. This is dangerous because reference counted pointer types assume that if one refcounted pointer is wrapped in Pin, then they all are. For example, obtaining an &Arc<T> from a Pin<Arc<T>> is a problem because you can clone the Arc and eventually call Arc::get_mut and move the pinned value.
The below sections explains what a pointer type must satisfy to be pin safe.
No moving out
This pointer type must not provide any API that lets the end-user move the
pointed-to value unless it is !Unpin.
As an example, if this pointer type implements DerefMut or Drop,
then methods such as Pin::as_mut, Pin::set, or the destructor of
Pin<_>, will pass a &mut T to DerefMut::deref_mut or Drop::drop on
the pointer type. The implementation of DerefMut or Drop on the
pointer type must not abuse this &mut T value to move the value.
Object identity
A pin safe pointer type will keep pointing at the same value. That is, the
address and concrete type returned by Deref/DerefMut does not
change.
For example, if you have two values A and B of the same type where only A is
pinned, then it would be illegal for the implementation of Deref or
DerefMut to first return a reference to A, and then later return a
reference to B.
This applies even if the Pin<P> value is moved, or if deref/deref_mut
was called, or if an unsizing coercion was applied to the pointer, or if the
pointer was used with dynamic dispatch. Or any other API the pointer type
may choose to provide.
Clone
The Pin<P> type implements Clone by:
- Create a
&P to the pointer type.
- Call
P::clone with the &P value.
- Call
Pin::new_unchecked on the return value of P::clone.
As explained previously, both operation 1 and 3 are dangerous. If this
pointer type implements Clone, then it must not abuse the &P passed to
clone, and if the original pointer referenced a pinned value, it must
return something that is safe to pass to Pin::new_unchecked.
Formatting traits
The Pin type implements fmt::Debug, fmt::Display, and
fmt::Pointer by calling the fmt method of said trait on the pointer
type, which involves the unsafe operation of creating a &P reference.
If the pointer type implements any of the formatting traits, then the
implementation of said formatting trait must not use the &P reference in a
way that is unsound.
pub unsafe trait PinSafe {}
I personally prefer the last solution because it allows for deduplication. We do not need to list the same requirements twice: both for Pin::new_unchecked and PinCoerceUnsized.
For user-defined smart pointer types
Pthat deref to an!Unpintype, the API ofPinexposes exactly three different ways of creating aPin<P>value without already having aPin<P>value:Pin::new_unchecked.This issue is about methods 1 and 2 only (see #134407 on method 3). Methods 1 and 2 are both gated behind use of unsafe:
Pin::new_uncheckedmethod itself is unsafe.PinCoerceUnsized.However, the safety requirements listed about the pointer type are insufficient.
Pin::new_unchecked (affects stable)
The safety requirements of
Pin::new_uncheckedsays a lot of stuff, but with one exception, everything it says talks about specifically the value behind the pointer, and not the pointer itself. The only thing it says about the pointer is this:However, there are safe traits other than
Deref,DerefMut, andDropwe need to be careful about. Here is an example illustrating whyCloneis also important:PinCoerceUnsized (unstable only)
The same applies to the
PinCoerceUnsized. It makes various requirements about theDerefandDerefMuttraits, but it says nothing aboutClone.Everything in the safety requirements of
PinCoerceUnsizedtalks about the fact that an individual value must not change its identity, and that is true forMalicious.Not just the Clone trait
There are a bunch of other traits that can potentially be abused in this manner. For example, the implementations of
fmt::Debug,fmt::Display, andfmt::Pointerare all called with an&Preference, and creating such a reference is unsound.Potential solutions
Reword both safety requirements
We can of course reword both
Pin::new_uncheckedandPinCoerceUnsizedto comprehensively list all traits you must not implement maliciously.Implement dangerous traits in terms of new traits
We could introduce traits like these:
This would be in addition to the existing unsafe trait
PinCoerceUnsized.The main disadvantage of this is that it's a breaking change.
Expand the scope of PinCoerceUnsized
We already have an unsafe trait guarding unsizing coercions called
PinCoerceUnsized. We could expand the scope of this trait. For example, we could renamePinCoerceUnsizedtoPinSafeand rewrite its documentation to also make requirements aboutCloneand any other trait like this. Then, we can rewordPin::new_uncheckedto require that the pointer type satisfies the safety requirements ofPinSafe(we cannot require that it implementsPinSafefor backwards compat, but we can require that it could implement it).Perhaps in a future edition,
Pin::new_uncheckedcould requireP: PinSafeto simplify what is listed in its safety requirements.Click to view sample documentation for PinSafe
I personally prefer the last solution because it allows for deduplication. We do not need to list the same requirements twice: both for
Pin::new_uncheckedandPinCoerceUnsized.