Skip to content

unsoundness relating to WF requirements on trait object types #44454

Description

@nikomatsakis

So @scalexm pointed me at this curious example:

trait Animal<X>: 'static { }

fn foo<Y, X>() where Y: Animal<X> + ?Sized {
    // `Y` implements `Animal<X>` so `Y` is 'static.
    baz::<Y>()
}

fn bar<'a>(arg: &'a i32) {
    foo::<Animal<&'a i32>, &'a i32>()
    
    // If you uncomment this line, you will get an error.
    //baz::<Animal<&'a i32>>()
}

fn baz<T: 'static + ?Sized>() {
}

fn main() {
    let a = 5;
    bar(&a);
}

There's a lot going on here, but the key point is that, in the call to foo::<Animal<&'a i32>, &'a i32>, we assume that Animal<&'a i32>: Animal<&'a i32>. This is reasonable, since it's a trait object type -- however, it is a malformed trait object type, and hence one where no impl could ever exist (i.e., we could never make an instance of this type). Interestingly, we are not smart enough to extend this to the case where the 'static bound is applied directly (hence the commented out call to baz::<Animal<&'a i32>>.

At the very least, this is inconsistent behavior, but I feel like there is an unsoundness lurking here. What makes me nervous is that we say that a projection type <P0 as Trait<P1...Pn>>::Foo outlives 'a if Pi: 'a for all i. I am worried that you could use an impl like this:

trait Projector { type Foo; }

impl<X> Projector for Animal<X> {
  type Foo = X;
}

to prove then that <Animal<&'a i32> as Projector>::Foo outlives 'static (say). I have not yet quite managed to weaponize this, but I got pretty close -- that's a gist in which we invoke a function is_static<U: 'static>(u: U) where U will be of type &'a i32 when monomophized. If rustc were as smart as chalk, I suspect we could easily craft a transmute of some kind to &'static i32. There may be a way to do it within current rustc too.

I'm not sure what's the best fix here. I suspect we should require that trait object types meet the WF requirements declared on the trait; and yet, I think the reason we did not is because we don't know the Self type, which means we can't fully validate that. =) This also feels a mite inconsistent with implied bounds. (Perhaps most likely is that we could tweak the WF rules for trait objects to specifically target lifetime bounds, but I have to think about that, feels .. hacky?)

We could tweak the notion of what type parameters are constrained -- or at least which may appear in a projection. e.g., we could disallow that impl of Projector for Animal<X>, because X would not be considered "sufficiently" constrained to appear in type Foo = .... Backwards incompatible, obviously, and feels unfortunate.


This causes actual unsoundness, as discovered by @scalexm in #44454 (comment);

use std::any::Any;

trait Animal<X>: 'static { }

trait Projector {
  type Foo;
}

impl<X> Projector for dyn Animal<X> {
  type Foo = X;
}

fn make_static<'a, T>(t: &'a T) -> &'static T {
    let x: <dyn Animal<&'a T> as Projector>::Foo = t;
    let any = generic::<dyn Animal<&'a T>, &'a T>(x);
    any.downcast_ref::<&'static T>().unwrap()
}

fn generic<T: Projector + Animal<U> + ?Sized, U>(x: <T as Projector>::Foo)
    -> Box<dyn Any>
{
    make_static_any(x)
}

fn make_static_any<U: 'static>(u: U) -> Box<dyn Any> {
    Box::new(u)
}

fn main() {
    let a = make_static(&"salut".to_string());
    println!("{}", *a);
}

cc @arielb1 @aturon

Activity

  1. added
    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
    T-langRelevant to the language team
    on Sep 9, 2017
  2. nikomatsakis commented on Sep 9, 2017

    @nikomatsakis
    ContributorAuthor

    Actually, thinking about this more, the problem is maybe not quite how I was thinking of it. That is, it seems like the problem lies in the definition of object safety. That is, if you imagine that you had to write the impl for Animal<X> by hand, it would look like:

    impl<X> Animal<X> for Animal<X> { ... }

    and this would fail the well-formedness checks because Animal<X>: 'static does not hold, at least not without relying on the impl we are writing =).

    The goal of the object safety rules are to ensure such an impl could be written.

    Maybe the rules of object safety have to be more conditional -- i.e., Animal<X> is object safe if X: 'static.

  3. scalexm commented on Sep 9, 2017

    @scalexm
    Contributor
  4. changed the title [-]*possible* unsoundness relating to WF requirements on trait types[/-] [+]*possible* unsoundness relating to WF requirements on trait object types[/+] on Sep 10, 2017
  5. changed the title [-]*possible* unsoundness relating to WF requirements on trait object types[/-] [+]unsoundness relating to WF requirements on trait object types[/+] on Sep 10, 2017
  6. arielb1 commented on Sep 10, 2017

    @arielb1
    Contributor

    This looks similar to #27675 - in both cases we have a trait object that is not WF.

    I think a reasonable way to solve this would be to have a trait object predicate depend on a WF(Self) predicate, so we basically have the rule

    'λ₀...'λₙ lifetimes
    τ₀...τₙ types
    Trait object-safe
    ~~WF(for<...> Trait<'λ₀...'λₙ, τ₀...τₙ>) - this part is new~~
    WF(for<...> Trait<'λ₀...'λₙ, τ₀...τₙ>: Trait<'λ₀...'λₙ, τ₀...τₙ>) - this part is new
    ----------------
    for<...> Trait<'λ₀...'λₙ, τ₀...τₙ>: for<...> Trait<'λ₀...'λₙ, τ₀...τₙ>
  7. arielb1 commented on Sep 11, 2017

    @arielb1
    Contributor

    The "this part is new" part is actually more subtle than I realized.

    Object types (aka dyn Trait<'λ₀...'λₙ, τ₀...τₙ>, to avoid confusion) are actually a funny way of writing

    exists<σ> (σ, for<...> WF(dyn Trait<'λ₀...'λₙ, τ₀...τₙ>: Trait<'λ₀...'λₙ, τ₀...τₙ>) → σ: Trait<'λ₀...'λₙ, τ₀...τₙ>)
    

    And the idea is that if the trait is object-safe, we can write a "compiler impl"

    impl<'λ₀...'λₙ, τ₀...τₙ> Traitl<'λ₀...'λₙ, τ₀...τₙ> for dyn Trait<'λ₀...'λₙ, τ₀...τₙ> {
        // ...
    }

    If we want to write such an impl, we need the where-clauses on the trait (both associated types and others) to hold. For supertraits, this follows by obvious induction. For other constraints, this is something that won't hold by itself.

    Therefore, we probably need to add these other predicates as conditions on the impl, and have

    impl<'λ₀...'λₙ, τ₀...τₙ> Trait<'λ₀...'λₙ, τ₀...τₙ> for dyn Trait<'λ₀...'λₙ, τ₀...τₙ>
        where WF(Trait<'λ₀...'λₙ, τ₀...τₙ>: dyn Trait<'λ₀...'λₙ, τ₀...τₙ>)
    {
        // ...
    }
  8. nikomatsakis commented on Sep 11, 2017

    @nikomatsakis
    ContributorAuthor

    I agree that it is the same as #27675. As we said on IRC, I think that the current fix is roughly as I described here -- in particular, when we check that dyn Trait<P...>: Trait<P...>, we need to also check that the where-clauses declared on Trait hold for dyn Trait<P...>. This subsumes some of the object safety checks (e.g. the search for where Self: Sized).

  9. bstrie commented on Sep 14, 2017

    @bstrie
    Contributor

    I agree that it is the same as #27675.

    Shall we close this as a dupe, then?

  10. nikomatsakis commented on Oct 19, 2017

    @nikomatsakis
    ContributorAuthor

    ping @arielb1 -- whatever happened with your branch here? I remember we had some discussions about cycles and don't recall how (if at all) they were resolved.

  11. 21 remaining items

  12. Noratrieb commented on Nov 30, 2022

    @Noratrieb
    Member

    searched nightlies: from nightly-2020-01-01 to nightly-2022-11-30
    regressed nightly: nightly-2022-08-10
    searched commit range: f03ce30...34a6cae
    regressed commit: 63e4312

    bisected with cargo-bisect-rustc v0.6.4

    Host triple: x86_64-unknown-linux-gnu
    Reproduce with:

    cargo bisect-rustc --access github --regress error

    so it was fixed by #99217

  13. lcnr commented on Nov 30, 2022

    @lcnr
    Contributor

    it looks like the original unsoundness was fixed before #99217, it seems like that stopped compiling in 1.61. @Nilstrieb can I get you to bisect this one as well?

    use std::any::Any;
    
    trait Animal<X>: 'static { }
    
    trait Projector {
      type Foo;
    }
    
    impl<X> Projector for dyn Animal<X> {
      type Foo = X;
    }
    
    fn make_static<'a, T>(t: &'a T) -> &'static T {
        let x: <dyn Animal<&'a T> as Projector>::Foo = t;
        let any = generic::<dyn Animal<&'a T>, &'a T>(x);
        any.downcast_ref::<&'static T>().unwrap()
    }
    
    fn generic<T: Projector + Animal<U> + ?Sized, U>(x: <T as Projector>::Foo)
        -> Box<dyn Any>
    {
        make_static_any(x)
    }
    
    fn make_static_any<U: 'static>(u: U) -> Box<dyn Any> {
        Box::new(u)
    }
    
    fn main() {
        let a = make_static(&"salut".to_string());
        println!("{}", *a);
    }
  14. albertlarsan68 commented on Dec 1, 2022

    @albertlarsan68
    Member

    I got Regression in nightly-2022-03-16, which maps to

    2022-03-14UTC: Auto merge of #94929 - flip1995:clippyup, r=Manishearth
    2022-03-14UTC: Auto merge of #94935 - matthiaskrgr:rollup-2o2kyz6, r=matthiaskrgr
    2022-03-15UTC: Auto merge of #92285 - compiler-errors:dyn-proj-bounds, r=nikomatsakis
    2022-03-15UTC: Auto merge of #94584 - pnkfelix:inject-use-suggestion-sites, r=ekuber
    2022-03-15UTC: Auto merge of #94938 - lnicola:rust-analyzer-2022-03-14, r=lnicola
    2022-03-15UTC: Auto merge of #94261 - michaelwoerister:debuginfo-types-refactor, r=wesleywiser
    2022-03-15UTC: Auto merge of #94928 - lcnr:inline-as_substs, r=michaelwoerister
    2022-03-15UTC: Auto merge of #94966 - matthiaskrgr:rollup-iqzswh3, r=matthiaskrgr
    2022-03-15UTC: Auto merge of #94973 - GuillaumeGomez:more-gui-tests, r=notriddle
    

    Unfortunately, there is no CI builds anymore for this range, and I do not have the power to bisect those commit myself now.

    Reproduce with cargo bisect-rustc --access=github --end 1.61.0 -- check

  15. oli-obk commented on Dec 1, 2022

    @oli-obk
    Contributor

    I'm guessing it was @compiler-errors' #92285

  16. removed
    E-needs-bisectionCall for participation: This issue needs bisection: https://github.com/rust-lang/cargo-bisect-rustc
    E-needs-mcveCall for participation: This issue has a repro, but needs a Minimal Complete and Verifiable Example
    on Dec 1, 2022
  17. lcnr commented on Dec 1, 2022

    @lcnr
    Contributor

    that's cool, looks like we only need the add ui tests and can then close this issue 🎉

  18. added a commit that references this issue on Dec 11, 2022
  19. added a commit that references this issue on Dec 11, 2022
  20. added a commit that references this issue on Dec 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-trait-systemArea: Trait systemA-type-systemArea: Type systemC-bugCategory: This is a bug.E-needs-testCall for participation: An issue has been fixed and does not reproduce, but no test has been added.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-mediumMedium priorityT-langRelevant to the language team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions