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.
We have a
CoerceUnsized impl for
Pin that allows you to translate (for example)
Pin<&dyn Debug>. This is interesting because the pinned target type changes from
dyn Debug, and the former is
Unpin while the latter is not. More generally, this works for any sort of
Pin<Pointer<Q>> type, so long as
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<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)
(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.
- If the type
P: Deref, then it derefs to a legal pinned value of type
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
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.