types team 2024 roadmap

flowchart LR

    subgraph Edition2024
        RPITLifetimeCaptureRules["    \n\nowner: TC/errs"]
        WeakTypeAliases["Changing type aliases to be weak\n\nowner: (lang)"]
    end

    subgraph EOY2024
        NewSolverEverywhere["Use the new trait solver in all the places\n\nowner: lcnr"]
        NegativeImpls["Support negative impls in coherence\n\nowner: niko"]
        PoloniusStable["Location-sensitive polonius\n\nowner: lqd"]
    end

    subgraph HeatDeath
        UnlimiTAITed
        Coinduction
        Fix25860
        ImpliedBounds
        BeautifulGat
        PerfectDerive
        FixOrSettle41756["Address how reordering where clauses can change behavior"]
        ExistentialLifetimes["more usable RPIT via existential lifetimes"]
    end

    subgraph Early2024
        ModelCoherenceInFormality["Model coherence in formality\n\nowner: nikomatsakis"]
        NormalizeInOrphanCheck["Normalize in orphan check\n\nowner: lcnr"]
        NewSolverInCoherence["New solver in coherence\n\nowner: lcnr"]
        ImprovedOutlivesWithOpaqueBounds["`-> impl Trait + 'static` is ok even if it captures\n\nowner: errs"]
        MinTAITs["Minimal version of TAITs\n\nowner: oli, TC for prose, fallback to errs"]
        PoloniusPrototype["Prototype of Polonius\n\nowner: lqd"]
        DynUpcasting["Dyn upcasting\n\nowner: lcnr"]
    end

    Early2024 -.- Edition2024
    Edition2024 -.- EOY2024
    EOY2024 -.- HeatDeath

    NewSolverInCoherence --> NewSolverEverywhere
    NormalizeInOrphanCheck --> WeakTypeAliases
    MinTAITs --> RPITLifetimeCaptureRules
    MinTAITs --> UnlimiTAITed
    PoloniusPrototype --> PoloniusStable
    ImprovedOutlivesWithOpaqueBounds --> ExistentialLifetimes
    NewSolverEverywhere --> Coinduction
    NewSolverEverywhere --> UnlimiTAITed
    ModelCoherenceInFormality --> NewSolverInCoherence

    Coinduction --> ImpliedBounds
    ImpliedBounds --> BeautifulGat
    Coinduction --> Fix25860

    linkStyle 0 stroke-width:0px
    linkStyle 1 stroke-width:0px
    linkStyle 2 stroke-width:0px
    
  • New solver
    • coherence (early 2024)
    • possible additional partial stabilizations (by EOY 2024)
      • MIR! validation and/or MIR typeck
      • fn codegen_select_candidate
      • replace evaluate
      • item WF check
      • all but HIR typeck seems feasible
  • "Minimal version of TAITs" no edition interaction
    • Open question: what are the use cases we really want?
    • What are the restrictions we have to enforce to feel comfortable?
  • Weak type aliases for Edition 2024
    • need to be done mid-year
    • definitely requires edition change
    • requires: Normalize before orphan check so that we can impl on type aliases
    • main implementor: fmease
  • Polonius stable on nightly :)
  • a-mir-formality ??
    • what is the right best goal?
    • fuzzing?
  • Existential lifetimes ??

Not on roadmap:

  • Stabilize negative impls ?
  • GAT implied bounds / RTN on old solver??
  • Perfect derives / coinductive traits ??

unsoundness

https://github.com/rust-lang/rust/issues?q=is%3Aissue+is%3Aopen+label%3AI-unsound+label%3AT-types
Last year: https://hackmd.io/nNC_Z6nVSpW-ANkpJdSgpg?both (https://i.imgur.com/1zqc2pc.jpg)

Followup: Niko and Jack to put together an initial draft of unsoundess on a roadmap

niko digresses onto a question around outlives

This used to be true:

fn(&'a u32): 'static

But we wanted T::Item: 'x if T: 'x and

impl<'a> Trait for fn(&'a u32) {
    type Item = &'a u32;
}

But we could say that if a lifetime is used in contravariant position then it's not constrained.

I wish we could change the current behavior. If we could do this, doing it over an edition might be the way. It would be nice if T: 'static would always mean that T doesn't contain any data by reference.

This is related to:

Rust allows impl Fn(T<'a>) -> T<'b> to be : 'static, which is unsound #112905

Issue with TypeId:
fn(&'a u32): 'static means you can convert Box<fn(&'a u32)> to Box<dyn Any>, downcast to Box<fn(&'static u32)>.

playing with 112905

https://github.com/rust-lang/rust/issues/112905

The heart of 112905 is leverage the dyn trait loophole to convert a

type F<'a, 'b> = impl 'static + Fn(T<'a>) -> T<'b>;

F<'a, 'b> to F<'a, 'static>. Which lets you call the function and convert anything into 'static.

type F<'a, 'b> = impl 'static + Fn(T<'a>, &mut T<'b>);

Suppose we had an explicit notation:

type F<'a, 'b> = impl<'a, 'b> 'static + Fn(T<'a>, &mut T<'b>);

what if the type were

type G<'a, 'b> = impl<'a, 'b> 'static;

is that a soundness hole? It IS possible to have

mod boundary {
    type G<'a, 'b> = impl<'a, 'b> 'static;
    
    fn get<'a>(x: &'a u32) -> G<'a, 'a> {
        () // <-- is there a hidden type here that is unsound
    }
    
    fn take<'a, 'b>(g: G<'a, 'b>) {
        // ...given that caller may change 'a and 'b arbitrarily here
    }
}

fn caller<'x>(x: &'x u32) {
    let g0: boundary::G<'x, 'x> = get(x);
    let g1: boundary::G<'x, 'static> = dyn_trick(g0);
    boudary::take(g1); // but...so what??
}

fn dyn_trick<T, U>(t: T) -> U
where T: 'static, U: 'static, 
{
    let x: Box<dyn Any> = Box::new(t);
    *x.downcast::<U>().unwrap()
}

Another view of #112905 is that, in this code

#![feature(type_alias_impl_trait)]

struct T<'x>(&'x ());

type F<'a, 'b> = impl 'static + Fn(T<'a>) -> T<'b>;

fn helper<'a, 'b>(_: [&'b &'a (); 0]) -> F<'a, 'b> {
    // Why does this compile? Because of an implied bound
    // `where `'a: 'b` (which could well have been explicit);
    // that relationship is required to make the hidden type
    // well-formed, but we lose it when 
    |x: T<'a>| -> T<'b> { x } // this should *not* be `: 'static`
}
#![feature(type_alias_impl_trait)]

struct Foo<T: Ord> { t: T }

type FooAlias<T> = impl std::fmt::Debug;

fn foo<T: Ord>(t: T) -> FooAlias<T> {
    Foo::<T> { t }
}

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=dc541ebd1502a24cc3885a63ac8c83ce

Writing the implied where 'a: 'b bound correctly gives you this error:

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=4a1f612ca7b8c76b3b7795ea4242be14

message:

error[E0478]: lifetime bound not satisfied
 --> src/main.rs:9:18
  |
9 | type F<'a, 'b> = impl Any + Fn(T<'a>) -> T<'b>;
  |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
note: lifetime parameter instantiated with the lifetime `'a` as defined here
 --> src/main.rs:9:8
  |
9 | type F<'a, 'b> = impl Any + Fn(T<'a>) -> T<'b>;
  |        ^^
note: but lifetime parameter must outlive the lifetime `'b` as defined here
 --> src/main.rs:9:12
  |
9 | type F<'a, 'b> = impl Any + Fn(T<'a>) -> T<'b>;
  |            ^^

will not compile

#![forbid(unsafe_code)] // No `unsafe!`
#![feature(type_alias_impl_trait)]

use core::any::Any;

/// Anything covariant will do, for this demo.
type T<'lt> = &'lt str;

type F<'a, 'b> where 'a: 'b = impl Any + Fn(T<'a>) -> T<'b>;

fn helper<'a, 'b>(_: [&'b &'a (); 0]) -> F<'a, 'b>
where 'a: 'b { // <-- the change
    |x: T<'a>| -> T<'b> { x }
}

fn exploit<'a, 'b>(a: T<'a>) -> T<'b> {
    let f: F<'a, 'a> = helper([]);
    let any = Box::new(f) as Box<dyn Any>;

    let f: F<'a, 'static> = *any.downcast().unwrap();

    f(a)
}

fn main() {
    let r: T<'static> = {
        let local = String::from("...");
        exploit(&local)
    };
    // Since `r` now dangles, we can easily make the use-after-free
    // point to newly allocated memory!
    let _unrelated = String::from("UAF");
    dbg!(r); // may print `UAF`! Run with `miri` to see the UB.
}

In summary

NM: Not convinced this bug blocks semantic 'static.

NM: Am convinced that our implied bounds rules need work.

Fuzzing

Resources:

Select a repo