rust-lang / rust-lang/rust

Lifetime HRTB on associated type results in unsoundness

Open
#141,713 18 comments 5 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-associated-items A-closures A-higher-ranked A-type-system C-bug I-ICE I-unsound P-high regression-from-stable-to-stable T-types
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

This code results in undefined behavior in safe code.

use std::any::Any;

// `for<'a> Producer<&'a T>` is equivalent to the (non-existent) `for<'a> FnOnce() -> &'a T`
pub trait Producer<T>: FnOnce() -> T {}
impl<T, F: FnOnce() -> T> Producer<T> for F {}

// What does `P::Output` even mean in this context? If this was `FnOnce(&()) -> &T`, using `Output`
// would result in "Cannot use the assiciated type of a trait with uninferred generic parameters",
// even when hiding behind an extra trait
fn write_incoherent_p2<T, P: for<'a> Producer<&'a T>>(
    weird: P::Output,
    out: &mut &'static dyn Any,
) {
    // `T` is not even `'static`, but `P::Output` seems to kind of 
    // resemble `for<'a> &'a T` (if that even means anything)
    *out = weird;
}

fn write_incoherent_p1<T, P: for<'a> Producer<&'a T>>(p: P, out: &mut &'static dyn Any) {
    // Producing and writing p() in one function doesn't work. Doing so requires T: 'static.
    // Adding T: 'static to all functions also makes this not work.
    // This fragility is why every line is its own function. 
    write_incoherent_p2::<T, P>(p(), out)
}

// Now we can trigger unsoundness by finding something that is `FnOnce() -> &'a T` for any `'a`
// `for<'a> FnOnce() -> &'a T` is basically just the signature of Box::leak
fn construct_implementor<T>(not_static: T, out: &mut &'static dyn Any) {
    write_incoherent_p1::<T, _>(|| Box::leak(Box::new(not_static)), out);
}

fn make_static_and_drop<T: 'static>(t: T) -> &'static T {
    let mut out: &'static dyn Any = &();
    construct_implementor::<&T>(&t, &mut out);
    *out.downcast_ref::<&T>().unwrap()
}

fn main() {
    println!("{:?}", make_static_and_drop(vec![vec![1]])); // use after free
}

The earliest affected version is 1.72 1.67.0. Many All earlier versions reject construct_implementor, but most just 1.70.0-1.72.0 ICE. Current nightly is also affected.

I am not entirely sure what is going on in this code; I was trying to create a function type that returns a reference of any requested lifetime (don't ask why), i.e. for<'a> Fn() -> &'a T. For some reason it works when hiding behind a blanket implementation, though it is very fragile. Taking Fn::Output of such a function type yields a type that seems to kind of resemble "for<'a> &'a T" and allows assignment to &'static dyn Any, even though T is not 'static.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by compiling the standalone reproducer with Rust 1.67.0 through 1.72.0 and current nightly, comparing acceptance, ICEs, and runtime behavior. Trace the associated-type and higher-ranked trait bound handling implicated by the example; the issue is done when the safe-code path no longer permits undefined behavior and regression coverage protects the affected cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.