Continuing the discussion from Unsoundness in `Pin`, I'd like to take a bit of time to discuss CoerceUnsized
. I'm still having trouble putting my finger on a crisp articulation of where the blame lies for the soundness failure.
The problem
We have a CoerceUnsized
impl for Pin
that allows you to translate (for example) Pin<&i32>
to Pin<&dyn Debug>
. This is interesting because the pinned target type changes from i32
from dyn Debug
, and the former is Unpin
while the latter is not. More generally, this works for any sort of Pin<Pointer<T>>
to Pin<Pointer<Q>>
type, so long as Pointer<T>: CoerceUnsized<Pointer<Q>>
.
CoerceUnsized
is a bit of a funny trait. It has some built-in, compiler-enforced conditions that are meant to guarantee the conversion is safe. For example, you can convert a Rc<i32
> to Rc<dyn Debug>
because i32: Debug
and because the Rc
definition directly stores a *mut RcBox<i32>
-- in other words, the i32
data is found through exactly one layer of indirection. This means we can convert that pointer to a *mut RcBox<dyn Debug>
, which is a fat pointer and hence 2-words wide. I'm not sure where the best write-up of these rules exists, but we should fix that.
Regardless, certainly the intent of CoerceUnsized
was that Pointer<P>: CoerceUnsized<Pointer<Q>>
implies that you are returning the same underlying data when you return the result, but with some of the static type erased into dynamic form.
Therefore, if you use Pin::new(value)
to create a pinned version of some T: Unpin
, it's ok if you coerce it to a pinned dyn
value, because the underlying value is still of some Unpin
type. I'm not entirely sure that this follows, but it's the intuition we are going for I think.
The problem is that nothing guarantees that the Pointer
will return the same data before and after coercion. @comex demonstrated that right now you could have two distinct Deref
impls, for example. One of them returns some constant &()
, but the other applies for a target of dyn Future
and it substitutes a different value which is not (in fact) Unpin
.
(I was toying with other versions that would be problematic, but I didn't find one yet.)
One angle on the problem
One way to look at the problem is to blame Pin::new
. It permits one, after all, to construct a Pin<P>
knowing only that P
derefs to some T: Unpin
. But -- because of the CoerceUnsized
impl for Pin
, constructing a Pin<P>
also means you have (implicitly) constructed a Pin<Q>
for all types where P: CoerceUnsized<Q>
. So really we have to prove that all of those types will be respect the Pin
invariant -- and, in this case, we cannot, which is why we have a problem.
(The Pin
invariant being the one that @RalfJung expressed here. In short:
- If the type
P: Deref
, then it derefs to a legal pinned value of typeP::Target
- etc
The problem here is that while the original Pointer<T>
meets those conditions, the Pointer<U>
that we can coerce to does not -- it implements P: DerefMut
and the result is not a safe mutable pinned reference (right?). Or something like that.
The problem of course is that Pin::new
can't possible prove those conditions on its own. It needs something stronger -- either it needs some more where clauses, or else we need some new conditions somewhere.
What I think the "right fix" looks like
This brings us to why @withoutboats is arguing that we should make the CoerceUnsized
trait unsafe. The idea is that it should imply a stronger invariant than it currently does, so that Pin::new
can say something stronger about the types that P
may be coerced to. Right now all we basically know is something like "the layout can be coerced into an equivalent layout that includes a fat pointer", but that doesn't actually tell us anything about how the Deref
and other impls will behave in this new type.
I think what might seem reasonable would be something like: implementing CoerceUnsized
implies that Deref
will return a reference to the same value both before and after coercion, and the metadata on that reference will correspond to the static type that was erased.
So, for example, when we coerce our Pin<Pointer<i32>>
to a Pin<Pointer<dyn Debug>>
, we're still going to deref to the same underlying i32
value.
I'm not really sure if this is strong enough to for Pin::new
, but that kind of circles back to why it is ok to coerce to a Pin<Pointer<dyn Debug>>
in the first place, presumably?
Anyway, that's how I currently understand what's going on.