Open arielb1 opened 5 years ago
@arielb1 Interesting. I guess we would need a deeper check, something along the lines of:
impl<T> Object for SomeType<T> where WC {
/* ... */
}
// If the following goal has a non-empty set of solutions, reject the impl.
exists<T> {
exists<U> {
Unify(dyn Object<Output = U>, SomeType<T>),
dyn Object<Output = U>: WC
}
}
cc @nikomatsakis
Sure.
What makes this non-trivial is auto-traits (dyn Object + Send
is a distinct wrt. unification from dyn Object
), and to some extent binders (unless you ignore them in unification). i.e., supposing the impl you have is
impl<T> Object<'a> for SomeType<T> where WC
Then the "ideal" lowering would be something of this format:
// If the following goal has a non-empty set of solutions, reject the impl.
exists<T: type, 'a('x): lifetime -> lifetime, U('x): lifetime -> type, AT: list of auto-traits> [
Unify(dyn for<'s> Object<'a('s), Output = U('s)> + AT, SomeType<T>),
dyn for<'s> Object<'a('s), Output = U('s)> + AT: WC
}
If you ignore binders in unification (i.e., you consider for<'a> Object<'a, Output=&'a T>
to be the same as Object<'x, Output=&'y T>
in coherence), then you don't have to deal with the HRTB problem, but you still have to deal with auto-traits:
// If the following goal has a non-empty set of solutions, reject the impl.
exists<T: type, 'a: lifetime, U: type, AT: list of auto-traits> [
Unify(dyn Object<'a, Output = U> + AT, SomeType<T>),
dyn Object<'a, Output = U> + AT: WC
}
While I don't think this is insurmountable, it is fairly annoying and places some restrictions on how you encode auto-traits.
I'm interested in working on this.
It turns out that only one trait is necessary to reproduce this: (playground)
trait Object {
type Output;
}
impl<T: ?Sized> Object for T {
type Output = &'static u64;
}
fn foo<'a, T: ?Sized>(x: <T as Object>::Output) -> &'a u64 {
x
}
fn transmute_lifetime<'a, 'b>(x: &'a u64) -> &'b u64 {
foo::<dyn Object<Output=&'a u64>>(x)
}
// And yes this is a genuine `transmute_lifetime`!
fn get_dangling<'a>() -> &'a u64 {
let x = 0;
transmute_lifetime(&x)
}
fn main() {
let r = get_dangling();
println!("{}", r);
}
This seems quite bad, as simply writing a blanket impl is enough to expose the issue.
One way to fix this issue would be the following:
If a trait has any associated items, and a blanket impl exists for it, that trait cannot be object-safe.
This is pretty extreme - however, the only other solution I can think of would be to deny all blanket impls of a trait with associated items - which seems even worse, given that the trait might not have even been object-safe to begin with.
Unfortunately, it gets worse. Here's an implementation of mem::transmute
in safe Rust:
trait Object<U> {
type Output;
}
impl<T: ?Sized, U> Object<U> for T {
type Output = U;
}
fn foo<T: ?Sized, U>(x: <T as Object<U>>::Output) -> U {
x
}
fn transmute<T, U>(x: T) -> U {
foo::<dyn Object<U, Output = T>, U>(x)
}
I think blame should be assigned to not normalizing <T as Object<U>>::Output
to U
in fn foo
. If that happened, foo::<dyn Object<U, Output = T>, U>(x)
would not type-check.
I checked the code above with godbolt and the problem has existed since Rust 1.0.0.
The first comments in the thread seem to indicate that the problem is instead with the blanket impl itself being accepted by the compiler.
If I understood correctly, @Centril's impl above can be applied to dyn Object<U, Output=T>
for any U
and T
, and defines Output=U
. However, a trait object like dyn Object<U, Output=T>
already provides an implicit impl with the same input types, but a different output type, which is incoherent.
Seems like a good idea to discuss in language and compiler team, nominating.
Notably, in my reproducer above, if you change fn foo
to:
fn foo<T: ?Sized + Object<U>, U>(x: <T as Object<U>>::Output) -> U {
x
}
then you will get:
error[E0308]: mismatched types
--> src/lib.rs:10:5
|
9 | fn foo<T: ?Sized + Object<U>, U>(x: <T as Object<U>>::Output) -> U {
| - expected `U` because of return type
10 | x
| ^ expected type parameter, found associated type
|
= note: expected type `U`
found type `<T as Object<U>>::Output`
Is that not just due to where-clauses having precedence over impls? AFAIK trait selection will prefer the Object<U>
impl in the bounds, which doesn't have an assoc. type specified, so the compiler can't normalize it.
I made a first attempt at a fix here: https://github.com/Aaron1011/rust/tree/fix/trait-obj-coherence
My idea was to extend the obligations generated by ty::wf::obligations
to include projection predicates of the form:
<dyn MyTrait<type Assoc=T> as MyTrait>::Assoc = T
.
This is combeind with an extra flag in SelectionContext
to force impl candidates to be chosen over object candidates. In the case of an incoherent blanket impl (e.g. any of the reproduces in this thread), <dyn MyTrait<type Assoc=T> as MyTrait>::Assoc
should project to the type specified in the blanket impl. This will fail to unify with T
, correctly causing a compilation error.
Unfortunately, I wasn't able to get this working, due to how the well-formed predicate generation is integrated with the rest of the compiler. I would need to significantly refactor FulfillmentContext
, SelectionContext, and/or
Predicatein order to keep track of the extra flag that needs to be passed to
SelectionContext`.
I'm hoping that someone can come up with a better approach. However, any solution will need to deal with the fact that SelectionContext
masks attempts to detect the incoherence by discarding the impl candidate in favor of the object candidate.
This is interesting! The fact that foo
in @Centril's example even gets accepted shows that we aggressively exploit coherence during type-checking: because we saw an impl
that uses a certain associated type, we type-check assuming that that is the impl
that got used here. Unfortunately, as the example here shows, that is in conflict with the "implicit impl
" that we get for trait objects.
I wouldn't be sad if we would fix this by being less aggressive about exploiting coherence during type-checking -- that makes analysis much more complicated, and this example shows why. But there's likely already tons of code out there that exploits this. Another fix is to refuse to create trait objects that "contradict" the kind of coherence knowledge that the type checker might exploit, but that seems extremely fragile.
@Aaron1011 your plan seems to be to encode in the type of foo
the fact that we exploited coherence? However this "exploitation" happens in the body of the function, so what exactly gets exploited cannot be known yet when computing the well-formedness predicate for foo
. Thus I guess what you want to / have to do is to aggressively look for all things one might be able to exploit up-front, add them all to the well-formedness obligations, and then during type-checking the boy look only at the obligations and not exploit coherence any more (as doing so would introduce scary bugs if the two systems for getting such type equalities out of coherence would ever not agree).
Once again, exploiting assumptions implicitly instead of inferring them to an explicit form proves to be an issue. This reminds me that we still implicitly exploit WF everywhere instead of turning that into explicit assumptions...
triage: P-high due to unsoundness. Leaving nominated in hope of discussing today. Not assigning to anyone yet.
assigning to @nikomatsakis with expectation that they will delegate. (and removing nomination label)
Still nominated for t-lang.
@RalfJung
I am not sure that giving up on coherence is the right choice here. I think it is too easy to split the code into functions, such that no one function sees the coherence violation:
struct Data<T: ?Sized, U> where T: Object<U> {
data: <T as Object<U>>::Output
}
trait Object<U> {
type Output;
}
trait Mark {
type Output;
}
impl<T: ?Sized, U: Mark<Output=V>, V> Object<U> for T {
type Output = V;
}
fn iso_1<T, U>(data: T) -> Data<dyn Object<U, Output=T>, U> {
// in any coherence-less solution, this shouldn't "see" the
// blanket impl, as it might not apply (e.g., `Local` might
// be in a third-party crate).
Data { data }
}
fn iso_2<X: ?Sized, U, V>(data: Data<X, U>) -> V
where U: Mark<Output=V>
{
// similarly, this shouldn't "see" the trait-object impl.
data.data
}
fn transmute_m<T, U, V>(data: T) -> V
where U: Mark<Output=V>
{
// This function *also* shouldn't see the blanket impl - `Local`
// might be in a third-party crate.
iso_2::<dyn Object<U, Output=T>, U, V>(
iso_1::<T, U>(data)
)
}
// These 3 functions could be in a child crate
struct Local<T>(T);
impl<T> Mark for Local<T> {
type Output = T;
}
fn transmute<T, U>(x: T) -> U {
// and this function shouldn't see *anything* that looks like a
// problematic associated type.
transmute_m::<_, Local<_>, _>(x)
}
I think that conditioning object safety on an object type being coherent is a reasonable-enough way out of this.
If we want to be tricky, we might put the condition "directly" on the impl - i.e., have ObjectCandidate
not apply if an ImplCandidate
might "theoretically" apply to any substitution of this type. This would however require doing coherence-style negative reasoning during selection, which would be ugly.
@arielb1 your iso_2
exploits coherence, so "giving up on exploiting coherence" would not let that function type-check.
@RalfJung what's the specific way that makes iso_2
exploit coherence?
The wrong derivation that it makes is <X as Object<U>>::Output = V :- U: Mark<Output=V>
. I don't think we want to avoid making that sort of derivation "unconditionally" (or is it implicitly making some other derivation?).
It seems like a basic rule of Rust that we want to keep that it is possible to follow impls. e.g., with IntoIterator
, you really want typeck to be able to assume that <T as IntoIterator>::Item = U := T: Iterator, <T as Iterator>::Item = U
.
follow impls
That is exactly what I mean by "exploiting coherence": seeing an impl that applies and relying on the fact it is the only one that can ever apply.
Discussed at T-compiler meeting. Assigning to @centril to take point on seeing that progress is made on this issue, in some form.
We discussed this in depth at today's lang team meeting. We enumerated a few possible solutions to the problem.
Presently, if you have an object-safe trait Object
, the compiler 'auto-generates' an impl like
impl Object for dyn Object { }
However, we do not forbid you from making a potentially duplicate impl:
impl<T: ?Sized> Object for T { }
This in and of itself is not so harmful, but it causes a problem if you have associated types:
trait Object<U> {
type Output;
}
In this case, the compiler would "auto-generate" an impl like:
impl<A, B> Object<A> for dyn Object<A, Output = B> {
type Output = B;
}
However, the user could provide an impl that has a different value for the associated type:
impl<A, B> Object<B> for A
where
B: ?Sized,
{
type Output = B;
}
Now the problem is that different parts of the compiler could select different impls, resulting in distinct values for Output
. In @Centril's example (https://github.com/rust-lang/rust/issues/57893#issuecomment-500250283), there is a helper function foo
which only knows that it has some T: ?Sized
, so it uses the user's impl to resolve Output
:
fn foo<T: ?Sized, U>(x: <T as Object<U>>::Output) -> U {
// ^^^^^^^^^^^^^^^^^^^^^^^^^
// Resolves to `U`, so `foo` thinks it has an argument of
// type `U` -- therefore, it can be returned.
x
}
On the other hand, the transmute
function invokes foo
with the type dyn Object<U, Output = T>
:
fn transmute<T, U>(x: T) -> U {
foo::<dyn Object<U, Output = T>, U>(x)
// ^
// Output is normalized to `T`
}
This winds up resolving using the "auto-generated" impl, and hence Output
is normalized to T
-- therefore, we can give an x: T
, and we get back a U
. (Reading the source, I'm actually not entirely sure why this happens, I might have thought it would result in ambiguity... but in any case it clearly does.)
OK -- I was supposed to start with the solutions, and @Centril was supposed to write-up the problem, but I couldn't help myself. Sorry @Centril. I wanted to be sure I understood it. I'll do the solutions in the next comment.
We explored three possible solutions.
The most obvious is to "give up on coherence". Actually we didn't go into this in depth; @arielb1 felt that it cannot be made sound, and I'm inclined to agree. I think this Zulip chat contains more details.
The next solution was modifying the rules around object safety such that the trait Object
is not considered "object safe". The idea would be roughly that a trait is not "object safe" if there is some impl that could apply to the object. This was somewhat inspired by https://github.com/rust-lang/rust/issues/50781 -- which is a rather different, but also concerning, soundness hole around object types. (And one I have to revisit after writing these to see if we can somehow solve it in a similar way.)
In truth, we didn't dig too hard into this option. It would be "backwards incompatible" in a somewhat serious way, in that if someone has traits like
trait Foo {
fn method(&self) { }
}
impl<T: ?Sized> Foo for T {
}
today, the Foo
is "object safe" and hence one can have dyn Foo
without a problem. (If you invoke method
on a dyn Foo
, I believe it will dispatch through the vtable, because we'll see it at monomorphization time, but I'm not 100% sure.)
This would be a bit of a departure in the sense that none of the other object safety rules have to do with the set of impls. That said, I believe that @mikeyhew did a lot of refactoring as part of the custom self-type work to make that possible, so it could work out.
Still, it has some "strangeness" to it -- for example, it requires creating a dyn Foo
type so that we can test whether any impl applies to it -- but such a type is only well-formed if there is no such impl. (As noted in the next comment, we could probably do this check in a more syntactic way, actually.)
The final solution we considered was to suppress the "auto-generation" of the dyn Foo: Foo
impl if there exists some user-given impl that might apply to dyn Foo
. The idea then is that Foo
is object-safe but if you supply an overlapping impl, that's the impl we're going to use.
This has some interesting potential to offer. It might serve as a replacement for the where Self: Sized
hack that we use in types like Iterator
to permit a trait to be object-safe while still employing lots of complex methods. I'll look at that in the next comment.
My first thought was that we might model this (in chalk terms) by first constructing a program environment for the trait which does not involve the automatic rule. This could be done but feels like a bit of a pain. In particular, solving those impls might require lowering other traits, and we'd have to test those traits to see if they are object safe and hence get an automatic rule, which seems to require a kind of DAG between traits. Uncool.
In practice, however, the impl in question must be be implemented for some type parameter T
where T: ?Sized
. I don't think there would be any other way to match a dyn Foo
object type (at least, not without being generic over "trait bounds" or some such thing). So we might be able to do a simpler, more syntactic check?
The same seems to apply to option two.
One final note:
We discussed, somewhat speculatively, the idea that if we took the third option, where users can write their own impl of a trait for dyn Trait
, that it might enable an alternative to the where Self: Sized
hack. The idea would be that if you have a trait that contains non-object-safe methods, you can define it yourself for the dyn
type. This would be a non-trivial bit of design though and it's not necessarily a big win -- it's roughly the approach we opted against in creating object safe in the first place, though with the (nice!) advantage that the compiler will automatically create the dyn Foo
impl when it is easy to do so.
The basic idea would be that you can always create a dyn Foo
type for any trait, but you can't actually invoke non-object-safe methods through that type. This changes the MIR definition because, for now, MIR knows nothing about virtual dispatch -- that is effectively encapsulated into a "magic impl" that the compiler generates. But in this model, the MIR would have to be extended with some notion of a virtual call, such that when one invokes obj.foo()
on some variable obj: dyn Foo
, that is not (from MIR's perspective) invoking Foo::foo
but rather a virtual call. This lets you write the impl itself that bottoms out in virtual method dispatch.
OK, I'm taking a bit of time today to look into this. I want to start by implementing some version of the check proposed here:
The final solution we considered was to suppress the "auto-generation" of the
dyn Foo: Foo
impl if there exists some user-given impl that might apply to dyn Foo. The idea then is thatFoo
is object-safe but if you supply an overlapping impl, that's the impl we're going to use.
just to get an idea what kind of scenarios arise when we do this.
A few thoughts so far:
First off, I was wrong when I said that "the impl in question must be be implemented for some type parameter T
where T: ?Sized
". It seems clear that one could write (for example):
trait TraitA { type Item: ?Sized; }
trait TraitB<T> { }
impl<X: TraitA> TraitB<X> for X::Item { }
impl TraitA for () {
type Item = dyn TraitB<()>;
}
A more elaborate version of this can be found in this playground link.
So clearly the (semi-)syntactic check I was first envisioning is too simplistic. What we want is to see whether there is some impl of SomeTrait
whose self type could be dyn SomeTrait
(or equivalent to it). This can happen in basically three ways:
?Sized
?Sized
dyn SomeTrait
type (which we already check for)(Our current "eager" expansion of type alias makes the last point a non-issue, but it's something to keep in mind for the future.)
So I guess we should be able to implement a suitable check that looks for those things and, if found, "disables" the compiler-supplied dyn Foo: Foo
impl. I'll dig a bit more into what that actually means in my next comment.
(At first I thought cyclic traits could be a problem:
impl Trait1 for dyn Trait2 { }
impl Trait2 for dyn Trait1 { }
but actually I think I was just confused there. The key point here is that, for the type dyn Trait1
, there is only one impl of Trait1
that could apply (the compiler-supplied one). The same holds for dyn Trait2
.)
So say we do have a suitable test we can apply to a trait to "disable the compiler-supplied impl". What does this actually mean?
To start, it means that traits have three categories:
dyn Foo
is allowed, and compiler ensures that dyn Foo: Foo
dyn Foo
is allowed, but dyn Foo: Foo
may not holddyn Foo
is not allowedNow, RFC 2027 kind of merges the last two categories, which is nice. Unfortunately I've got to rebase PR https://github.com/rust-lang/rust/pull/57545 and get it landed, since poor @bovinebuddha kind of lost patience with me I think (and quite reasonably so). But I don't think that really needs to block this ultimately.
In very concrete terms, I think that the meaning of the "object safe degenerate" case is as simple as there being no "object candidate" in the select.rs (and project.rs) code. This would not prevent dyn Foo: Foo
from holding, but for that to hold, some other Foo
impl would have to exist.
Now one interesting issue is that the compiler present kind of hardcodes the fact that invoking Foo
functions on a dyn Foo
type routes to virtual dispatch. But I think ultimately this also comes from the object candidate, so actually removing that candidate should "just work".
Here are the high-level steps I think we need to take:
Let me log some tests.
<dyn Foo as Foo>::foo
call will get dispatched through the user's impl.impl dyn Trait for dyn Trait
now, but have it use these semantics. Maybe make such an example, just for fun.X: TraitC
is not true) when we need to prove dyn Foo: Foo
.@nikomatsakis Thanks for the investigation.
Let me log some tests.
We should also include @arielb1's various versions on the weaponized transmute which "giving up on coherence" did not address.
To start, it means that traits have three categories:
- object safe -- in which
dyn Foo
is allowed, and compiler ensures thatdyn Foo: Foo
- object safe, degenerate -- in which
dyn Foo
is allowed, butdyn Foo: Foo
may not hold- non-object-safe --
dyn Foo
is not allowedNow, RFC 2027 kind of merges the last two categories, which is nice. Unfortunately I've got to rebase PR #57545 and get it landed, since poor @bovinebuddha kind of lost patience with me I think (and quite reasonably so). But I don't think that really needs to block this ultimately.
It would be good to consider what implications this has for quantifying over traits (e.g. fn foo<trait B>(...)
and projecting to associated traits (e.g. MyType: Self::KeyBound
).
I considered something like struct Foo<dyn trait A>(Box<dyn trait A + Send>);
where dyn trait A
quantifies an object safe trait. However, with the degenerate case, you can no longer assume that dyn A: A
holds in a generic setting.
I believe you need to write where dyn A: A
to ensure that it does. That's unfortunate syntactic salt but it seems it would do the job so we would not give up on expressiveness. Alternatively, we can assert dyn A: A
implicitly and give up on expressiveness for degenerate cases. This where dyn A: A
could also serve as a WF assertion which might make dyn trait A
unnecessary as syntax.
@Centril
That all sounds about right. I think if we assume that we adopt RFC 2027, then dyn A
would always be a legal type, and hence it seems pretty reasonable to make dyn trait A
mean "some trait where dyn A: A
".
On a completely different but related note, I've long wished that we make "dyn-compatibility" more explicit -- something like dyn trait Foo { .. }
might be an explicit assertion that Foo is a fully dyn-compatible (aka "object safe").
Update: I've got a working prototype now that removes the object candidate, based on the model I described. It is able to bootstrap successfully but for some reason I do not yet understand it afils miserably at running tests, which all die with some obscure ICE while loading the stdlib. Investigating.
Update: I still don't really understand where those errors are coming from. However, I also added a warning to be emitted on every trait that has a "potentially overlapping impl". The bad news is that there are a reasonable number.
Of these, Any
is obviously the biggest concern -- it is only really useful in dyn Any
form. It's also kind of amazing that this overlap didn't occur to us before, given that example. (I do remember being vaguely concerned that the check in the compiler wasn't good enough, but I wish I had listened more to the concern; ah well!)
Heh, I wonder if my bug is actually caused by the Any
case! It must be. I think this code is failing:
The problem here is that downcast_ref
invokes type_id
-- it wants to be invoking type_id
through the vtable, to get the type-id of the underlying type. However, we are now suppressing the "vtable-based implementation" and so we are getting this implementation of type_id
instead:
In other words, we're getting the type-id for the type dyn Any
, not the hidden type. (This is what I meant by "uh this is obviously a coherence issue".) Therefore, our call to expect
fails.
I'm not really sure what the best solution is here, but one thought is that, in all of the above cases, the "vtable impl" could be considered a specialization of the overlapping, generic case.
We might therefore consider a different approach. Instead of skipping the generation of the dyn
impl, we might rather make it more official -- we could add it to the coherence check (presently, it is skipped, because we only consider impls that the user actually wrote). In that case, all the above impls would generate errors, but if we added default
to their functions, they would compile, and behave as before.
Moreover, the "weaponized" cases would all fail -- they would be invalid specializations unless default
were added to their type parameters, and in that case, the compiler would not assume that it can normalize based on those impls.
@nikomatsakis: What would be the migration path for any crates with traits similar to Borrow
or BorrowMut
? I'm concerned that a specialization-based approach would leave such crates with no path forward, other than waiting on the stabilization of specialization.
I was thinking about this more over the weekend. @Aaron1011, in answer to your question, it seems obvious we're going to have to manage the timing of our fix here pretty carefully. For one thing, we've talked about stabilizing some subset of specialization, and maybe we should try to execute on that before we make any hard errors here.
One thing that's worth pointing out -- the only thing that users need to be able to do is to declare individual items as default
(i.e., eligible to be specialized). They don't need to write the specializing impl themselves.
Leaving the timing question aside, I had some further thoughts.
First, we don't want to unilaterally introduce a impl Foo for dyn Foo
impl, clearly -- for example, this doesn't make sense for traits that are not object safe. So you could imagine some logic like:
impl Foo for dyn Foo
impl.However, this is kind of a mess. In particular, you may not have meant for your trait to be used as a dyn Trait
-- for example, I'm not sure we anticipated dyn Borrow
, although there are a few users -- but now you may be getting coherence errors due to that dyn Foo
impl that you never asked for.
This comes back to the original mistake (or so I view it now) of having traits implicitly become object safe if they can be. In retrospect, I think I would've preferred a design where users "opted in" to having a trait be dyn
-compatible, perhaps by writing
dyn trait Iterator { .. .}
or something like that. It would then be a hard-error if the trait were not dyn compatible. If we had that design, then it would be fairly natural that dyn trait Iterator
also introduces an impl of Iterator for dyn Iterator
. In fact, since we are moving to a world where dyn Iterator
is always a valid type, even if Iterator
is not dyn
-compatible, you can view dyn trait Iterator
as a kind of "convenient short-hand" where the compiler generates the impl for you, but in theory you could write it yourself (as we discussed in a lang-team meeting). This obviously ignores the fact that you don't have the primitives you need to write that impl (i.e., to do virtual dispatch), but that is something we could in principle correct.
Of course, requiring dyn trait
opt-in would not be a compatible change at this point. It could in principle be done across an Edition boundary, although the "warning" would be a bit tricky (in particular, we can only suggest adding dyn
to the trait if we see the trait used as a dyn
value locally, we can't know about all downstream uses).
One thing we could do is something like this:
dyn trait
declarations
dyn
compatible, but only if there is no overlapping impl (i.e., the check I implemented here)
dyn trait
declarationsNote this last point: this is not the logic that exists on the branch. My thought here is that the "drop the object candidate" logic has been shown to change the semantics of existing code (i.e., the Any
branch), so merely making such a change would be bad.
This change means that if people want to use dyn Foo
for some trait Foo
that has an overlapping impl, they have to explicitly declare the trait to be dyn trait Foo
, in which case the coherence rules apply.
(To be clear, I think this change requires an RFC -- but it may make sense to adapt my branch such that I can create a PR and test the impact.)
So I've been distracted here. I think that the crater results here appear to be promising, but there is still a bit more analysis needed. Those results show that relatively few craters are affected, but what I didn't figure out yet is what kind of fix is needed for those crates. In particular, part of the fix I've proposed leans on specialization, and it'd be good to know if those crates would require default
declarations in order to be compatible.
The answer is definitely yes for some of them, but maybe not all. It's worth doing the investigation. To do so, we basically want to look at the logs and see if there is indeed an impl.
So far I just took a quick look at the axiom 1.0 crate. It's actually raises an interesting point -- here are its crater logs. What we can see that there is a warning issues with the text "impl_potentially_overlapping_dyn_trait", indicating that we found an impl that potentially overlaps with the dyn ActorMessage
(per these rules). However, [that impl requires other traits, like Serialize
, and of course dyn ActorMessage: Serialize
would not hold. However, if we look to the future, we would have to consider the possibility of trait objects like dyn (ActorMessage + Serialize)
-- and such a trait object naturally could match against that impl. So I think that this is indeed a case where the correct impl would require a default
declaration, so that it can be overridden by the (compiler-supplied) impl for dyn ActorMessage
.
To track the progress of the investigation, I created a hackmd. I'm going to try and leave some notes in crates I've looked at, and if others want to pitch in, please feel free to do so!
We held a lang-team design meeting (minutes and video available here) where we discussed this issue in depth.
After the meeting, I realized that one simpler fix (at least for now) would be to revoke "dyn safety" if:
where Self: Sized
could be permitted)I've implemented this in PR #66037 and it is being tested on crater.
I just wanted to mention since its not highlighted in the notes that this intersects with #67562. The proposal here is to make the compiler treat the Any
impl as specializable by the compiler generated trait object impl. There are several variations on this proposal, but we should remember: we cannot allow end users to specialize Any
without making Any
an unsafe
trait, because the safety of the code in std::any
depends on the invariant that the TypeId
returned by Any
's method is the correct TypeId
for the erased type (currently upheld through by virtue of the sole unspecializable impl of Any
which covers all possible instantiations).
Any of these solutions are sufficient:
default(crate)
(downstream users cannot specialize)The first two are both mentioned in the document and it sounded like the lang team was in favor of both. My impression of #67562 is that the libs team would prefer not to see Any specializable by end users, and to keep its unsafety encapsulated in the any module.
I'm dumping here some notes from the lang team dropbox paper:
dyn Trait
not dyn safe
if:
Self: Sized
(not implemented)[TryStream](https://docs.rs/futures/0.3.1/futures/stream/trait.TryStream.html)
trait, which is used roughly like this: trait StreamX {
type Item;
}
trait TryStreamX: StreamX {
type Ok;
type Error;
}
impl<S, T, E> TryStreamX for S
where S: ?Sized + StreamX<Item = Result<T, E>>
{
type Ok = T;
type Error = E;
}
dyn TryStream
which is an error now
S
in the above impl might (in the future) become
dyn TryStreamX<Ok = A, Error = B> + StreamX<Item = Result<C, D>>
type Ok
would be A
from the default impl but C
from the impl abovedyn TryStream
, which is a bit of a shame
default
default
implicitly
TryStream
would be pretty severe here, I think
T: Stream<Item = Result<...>>
T::Ok
dyn TryStream
but you can do fn foo<T: TryStream<..>>()>
dyn Stream<Item = ..>
TryStream
example in a similar waydefault
, but this has potentially broader impactI'm un-tagging this as P-high. In practice, it is not actually P-high. We'd like to see someone work on this, but leaving it perpetually tagged P-high is not helping with that.
I got some more example code in #80783, which I closed as it’s a duplicate.
I guess they aren’t any worse (as in “more unsafe”) than the examples that we already have in this thread.
@nikomatsakis
- revisit the soundness hole and see if we can “weaponize” the
TryStream
example in a similar way
It surely can:
use futures::{Stream, TryStream};
fn transmute<B, A>(x: B) -> A {
foo::<dyn TryStream<Ok = B, Error = (), Item = Result<A, ()>>, A, B>(x)
}
fn foo<S: ?Sized, A, B>(x: <S as TryStream>::Ok) -> A
where
S: Stream<Item = Result<A, ()>>,
{
x
}
static X: u8 = 0;
fn main() {
let x = transmute::<&u8, &[u8; 1000000]>(&X);
println!("{:?}", x);
}
Edit:
Apparently, not even changing it to
pub trait TryStream:
Stream<Item = Result<<Self as TryStream>::Ok, <Self as TryStream>::Error>>
changes anything o.O (playground)
Apparently, not even changing it to
pub trait TryStream: Stream<Item = Result<<Self as TryStream>::Ok, <Self as TryStream>::Error>>
changes anything o.O
Which means that we can get unsafety without any dyn-overlapping implementation:
trait SuperTrait {
type A;
type B;
}
trait Trait: SuperTrait<A = <Self as SuperTrait>::B> {}
fn transmute<A, B>(x: A) -> B {
foo::<A, B, dyn Trait<A = A, B = B>>(x)
}
fn foo<A, B, T: ?Sized>(x: T::A) -> B
where
T: Trait<B = B>,
{
x
}
static X: u8 = 0;
fn main() {
let x = transmute::<&u8, &[u8; 1000000]>(&X);
println!("{:?}", x);
}
(in case this isn’t known yet, it might be worth opening another issue..)
From this comment, is there not a soundness issue already with Any
?
In particular, given T, U: Any + ?Sized
, you cannot assume that calling type_id
on values of those types will produce unique type IDs, because one of them might be dyn Any
. You cannot, as far as I can tell, actually get the type ID of dyn Any
using type_id
; you can only get it with TypeId::of
.
@bors labels +T-types +P-high
Sadly, though I'd prefer to pass it to others, the types team has to take ownership of this one. =)
@rustbot label +T-types +P-high
Comments
The check for manual
impl Object for Object
only makes sure there is no directimpl Object for dyn Object
- it does not consider such indirect impls. Therefore, you can write a blanketimpl<T: ?Sized> Object for T
that conflicts with the builtinimpl Object for dyn Object
.Reproducer
edit: minimal reproducer from https://github.com/rust-lang/rust/issues/57893#issuecomment-500250283
I had some difficulties with getting the standard "incoherence ICE" reproducer, because the object candidate supersedes the impl candidate in selection. So here's a "transmute_lifetime" reproducer.
Duplicates, see also