-
-
Notifications
You must be signed in to change notification settings - Fork 14.5k
Description
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_uncheckedmethod 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, andDropimplementations ofPtr, if they exist. Most importantly, they must not move out of theirselfarguments:Pin::as_mutandPin::as_refwill callDerefMut::deref_mutandDeref::derefon the pointer typePtrand 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 PointerThis 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 whenSelfis 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 isBox, notSomeTypeOfFuture.This is important because there are certain operations that are not safe to perform on a pinned value in general, but
Pinimplements 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
Pinmay 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 Tto the pinned value. This is dangerous because it can be used to move the value (e.g. withmem::swap).- Creating a
&Pto the pointer type. This is dangerous because reference counted pointer types assume that if one refcounted pointer is wrapped inPin, then they all are. For example, obtaining an&Arc<T>from aPin<Arc<T>>is a problem because you can clone theArcand eventually callArc::get_mutand 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
DerefMutorDrop,
then methods such asPin::as_mut,Pin::set, or the destructor of
Pin<_>, will pass a&mut TtoDerefMut::deref_mutorDrop::dropon
the pointer type. The implementation ofDerefMutorDropon the
pointer type must not abuse this&mut Tvalue 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 byDeref/DerefMutdoes 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 ofDerefor
DerefMutto 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 ifderef/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 implementsCloneby:
- Create a
&Pto the pointer type.- Call
P::clonewith the&Pvalue.- Call
Pin::new_uncheckedon the return value ofP::clone.As explained previously, both operation 1 and 3 are dangerous. If this
pointer type implementsClone, then it must not abuse the&Ppassed to
clone, and if the original pointer referenced a pinned value, it must
return something that is safe to pass toPin::new_unchecked.Formatting traits
The
Pintype implementsfmt::Debug,fmt::Display, and
fmt::Pointerby calling thefmtmethod of said trait on the pointer
type, which involves the unsafe operation of creating a&Preference.
If the pointer type implements any of the formatting traits, then the
implementation of said formatting trait must not use the&Preference 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.