Skip to content

NLL: unsound verification of higher ranked outlives bounds #98095

Closed

Description

@aliemjay

This code now passes under under NLL after #95565: (playground)

fn assert_static<T>() where for<'a> T: 'a, {}

// `test` passes without requiring `T: 'static`
fn test<T>() { assert_static::<T>(); }

assert_static should be able to infer that T: 'static because, in the language of Chalk, I believe the bound for<'a> T: 'a should be lowered to forall<'a> { Outlives(T: 'a) :- WellFormed(T). } and because the well-formedness of T is assumed to be true, we can instantiate 'a to any lifetime including 'static.

@rustbot label T-compiler T-types regression-from-stable-to-nightly C-bug

Activity

  1. added
    C-bugCategory: This is a bug.
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    T-typesRelevant to the types team, which will review and decide on the PR/issue.
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Jun 14, 2022
  2. steffahn commented on Jun 14, 2022

    @steffahn
    Member

    Here’s a way to exploit this behavior for unsoundness:

    use std::any::Any;
    
    trait Lt<'a>: 'a {}
    impl<'a, T: ?Sized + 'a> Lt<'a> for T {}
    
    trait Tr {}
    impl<T: ?Sized> Tr for T {}
    
    fn test<T, R: Any>(x: T) -> R {
        // works despite non-'static
        step1::<T, R>(x)
    }
    
    // remark: starting in `test` with `step2` directly works, too.
    fn step1<T, R: Any>(x: T) -> R
    where
        for<'a> T: 'a,
    {
        step2::<T, R>(x)
    }
    
    fn step2<T, R: Any>(x: T) -> R
    where
        for<'a> T: Lt<'a>,
    {
        step3::<T, R>(x)
    }
    
    fn step3<T, R: Any>(x: T) -> R
    where
        T: Lt<'static>,
    {
        step4::<T, R>(x)
    }
    
    fn step4<T, R: Any>(x: T) -> R
    where
        T: 'static,
    {
        *(Box::new(x) as Box<dyn Any>).downcast::<R>().unwrap()
    }
    
    fn main() {
        let x = String::from("Hello World");
        let r: &'static str = test(&x[..]);
        drop(x);
        println!("{r}");
    }
    ����0V���
    

    (playground)

    @rustbot label I-unsound


    Interestingly, not only calling step1 or step2 from test fails on stable, but calling step2 from step1 doesn’t work on stable either, so maybe a for<'a> T: 'a constraint on its own is actually kinda useless at the moment?

  3. rustbot commented on Jun 14, 2022

    @rustbot
  4. added
    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
    on Jun 14, 2022
  5. oli-obk commented on Jun 14, 2022

    @oli-obk
    Contributor

    but calling step2 from step1 doesn’t work on stable either, so maybe a for<'a> T: 'a constraint on its own is actually kinda useless at the moment?

    Yea fn foo<T>() where for<'a> T: 'a { foo::<T>() } fails to compile on stable right now, because higher kinded outlives bounds don't satisfy themselves

  6. jackh726 commented on Jun 14, 2022

    @jackh726
    Member

    This is confirmed to be directly because of NLL and not something else.

    I'm going to nominate for compiler team, since this is pretty significant. We'll continue to discuss within types team if there is a reasonable fix here, but I want this to be on people's radar since this might end up being a NLL stabilization revert in the short-term.

  7. lqd commented on Jun 14, 2022

    @lqd
    Member

    This is used to be rejected by NLLs, and started being accepted in nightly-2020-07-28 (commit range).

    List of merged PRs for that nightly

    Out of those, #73503 looks like the most likely candidate, cc PR author @lcnr and reviewer @nikomatsakis.

  8. apiraino commented on Jun 14, 2022

    @apiraino
    Contributor

    WG-prioritization assigning priority (Zulip discussion).

    @rustbot label -I-prioritize +P-critical

  9. added and removed
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Jun 14, 2022
  10. nikomatsakis commented on Jun 14, 2022

    @nikomatsakis
    Contributor

    Discussed with @oli-obk and @lcnr -- we believe the problem is that the outlives code isn't handling placeholder regions correctly... they are in this fallthrough

    but they should be handled more like parameters, except that (at least at present) we can only bound them by 'static.

    I might be able to make this fix today else could mentor someone potentially (ping me on Zulip).

  11. compiler-errors commented on Jun 14, 2022

    @compiler-errors
    Contributor

    I have a somewhat clear understanding for why this happens. Specifically, consider the two cases:

    fn outlives_forall<T>() where for<'u> T: 'u {}
    
    fn test1<S>() { outlives_forall::<S>(); }
    
    struct Value<'a>(&'a ());
    fn test2<'a>() { outlives_forall::<Value<'a>>(); }
    

    On stable, both test1 and test2 fail, because the migrate borrowck mode is still enabled. On nightly, just test2 fails.

    Why does test2 fail?

    When we call outlives_forall::<Value<'a>> in test2, we get back a TypeOutlives constraint that looks like Value<'a>: RePlaceholder(U1, 'u). We walk the type and gather its outlives components, being [ReFree('a)]. So we register ReFree('a): RePlaceholder(U1, 'u).

    This part is a bit more fuzzy, but I think the outlives constraint establishes an edge on the SCC graph, that causes us to call incompatible_universe(), forcing the placeholder region to be inferred as 'static, and leading to the error.

    Why does test1 not fail?

    When we call outlives_forall::<S> in test1, we get an outlives constraint that instead looks like S: RePlaceholder(U1, 'u). We walk the type and gather its outlives components, being [U]. This means we don't register a region-outlives relationship, but instead delay this relationship as a type test. That type test looks like:

    TypeTest { 
      generic_kind: T/#0, 
      lower_bound: '_#3r, 
      locations: Single(bb0[1]), 
      verify_bound: AnyBound([OutlivedBy('_#1r)]) 
    }
    

    where '_#3r is the placeholder and '_#1r is the region vid associated with the ReFree lifetime of the type parameter. What's notable is that this is basically equivalent to '_#1r: '_#3r, except for the fact that we don't use this when we're solving regions, and instead delay it as a test to be performed afterwards. This means we never call incompatible_universe, meaning we never shorten(?) the placeholder region to 'static...


    What @aliemjay and @nikomatsakis independently found is that a second bug exists in eval_outlives that Niko fixed in #98109, one that only shows up in type tests as above. That is fixed with the PR, but it manifests other bugs :^)

  12. nikomatsakis commented on Jun 14, 2022

    @nikomatsakis
    Contributor

    So @compiler-errors and I did a deep dive here. What we found is that:

    • There is a bug in the NLL type check (which @compiler-errors described above) that is causing it to be unsound.
    • However, fixing that bug exposes a (pre-existing) bug around how we handle for<'a> T: 'a bounds, particularly with projections, causing regressions.
    • The good news is that I think fixing this bug will benefit @oli-obk and others. (I'll describe it in detail later.) The bad news is that a proper fix is non-trivial, so for the short term it may be better to revert NLL stabilization. I'll have to check with @jackh726 about how difficult that will be.
  13. nikomatsakis commented on Jun 15, 2022

    @nikomatsakis
    Contributor

    Fix is in #98109, this includes the fix for both this bug and the bug that the fix revealed :)

  14. added a commit that references this issue on Jun 24, 2022
    d017d59
  15. removed
    I-types-nominatedNominated for discussion during a types team meeting.
    on Jul 29, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

C-bugCategory: This is a bug.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-criticalCritical priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-typesRelevant to the types team, which will review and decide on the PR/issue.regression-from-stable-to-nightlyPerformance or correctness regression from stable to nightly.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions