Skip to content

Incoherent (?) Lifetime HRTB on associated type results in unsoundness in stable, safe code #141713

Open
@maxdexh

Description

@maxdexh

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. Many earlier versions reject construct_implementor, but most just 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-associated-itemsArea: Associated items (types, constants & functions)A-closuresArea: Closures (`|…| { … }`)A-type-systemArea: Type systemC-bugCategory: This is a bug.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-highHigh priorityT-typesRelevant to the types team, which will review and decide on the PR/issue.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.

    Type

    No type

    Projects

    Status

    unknown

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions