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.