Skip to content

unsoundness relating to WF requirements on trait object types #44454

Closed
@nikomatsakis

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

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