Skip to content

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

Open
@danielhenrymantilla

Description

@danielhenrymantilla

This is similar to #84366, but I don't know if I would say it's exactly the same. For instance, the exploit involves no associated types (no Output of FnOnce at all), just the mere approach of:

  1. type F<'a, 'b> = impl Fn(T<'a>) -> T<'b> : 'static;
  2. dyn Any-erase it.
  3. downcast to a different F<'c, 'd> (e.g., 'a = 'b = 'c, and 'd = 'whatever_you_want).

Mainly, the -> T<'b> return could always become an &mut Option<T<'b>> out parameter (so as to have a -> () returning closure), so the return type of the closure not being : 'static is not really the issue; it's really about the closure being allowed to be : 'static despite any part of the closure API being non-'static.

In fact, before 1.66.0, we did have impl 'static + Fn(&'a ()) being 'a-infected (and thus non : 'static). While a very surprising property, it seems to be a more sound one that what we currently have.

The simplest possible exploit, with no unsafe(-but-sound) helper API (replaced by an implicit bound trick) requires:

  • T<'lt> to be covariant;
  • type_alias_impl_trait.

I'll start with that snippet nonetheless to get people familiarized with the context:

#![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> = impl 'static + Fn(T<'a>) -> T<'b>;

fn helper<'a, 'b>(_: [&'b &'a (); 0]) -> F<'a, 'b> {
    |x: T<'a>| -> T<'b> { x } // this should *not* be `: 'static`
}

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_or_else(|_| unreachable!());

    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.
}

Now, to avoid blaming implicit bounds and/or type_alias_impl_trait, here is a snippet not using either (which thus works independently of variance or lack thereof).

It does require unsafe to offer a sound API (it's the "witness types" / "witness lifetimes" pattern, wherein you can be dealing with a generic API with two potentially distinct generic parameters, but you have an instance of EqWitness<T, U> or EqWitness<'a, 'b>, with such instances only being constructible for <T, T> or <'a, 'a>.

/// Note: this is sound (modulo the `impl 'static`):
/// it's the "type witness" pattern (here, lifetime witness).
mod some_lib {
    use super::T; // e.g., `type T<'a> = Cell<&'a str>;`

    /// Invariant in `'a` and `'b` for soundness.
    pub struct LtEq<'a, 'b>(::core::marker::PhantomData<*mut Self>);

    impl<'a, 'b> LtEq<'a, 'b> {
        /// Invariant: these are the only actual instances of `LtEq` code may witness.
        pub fn new() -> LtEq<'a, 'a> {
            LtEq(<_>::default())
        }
    
        pub fn eq(&self) -> impl 'static + Fn(T<'a>) -> T<'b> {
            // this `impl Fn(T<'a>) -> T<'b>` is sound;
            let f = |a| unsafe { ::core::mem::transmute::<T<'a>, T<'b>>(a) };
            // what is *not* sound, is it being allowed to be `: 'static`
            f
        }
    }
}

With this tool/library at our disposal, we can then exploit it:

use core::{any::Any, cell::Cell};
use some_lib::LtEq;

/// Feel free to choose whatever you want, here.
type T<'lt> = Cell<&'lt str>;

/// I've explicitly lifetime annotated everything to make it clearer.
fn exploit<'a, 'b>(a: T<'a>) -> T<'b> {
    let f = LtEq::<'a, 'a>::new().eq();
    let any = Box::new(f) as Box<dyn Any>; // this should not compile: `T<'a> -> T<'a>` ought not to be `'static`

    let new_f = None.map(LtEq::<'a, 'b>::eq);

    fn downcast_a_to_type_of_new_f<F: 'static>(
        any: Box<dyn Any>,
        _: Option<F>,
    ) -> F {
        *any.downcast().unwrap_or_else(|_| unreachable!())
    }

    let f = downcast_a_to_type_of_new_f(any, new_f);

    f(a)
}

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

This happens since 1.66.0.

@rustbot modify labels: +I-unsound +regression-from-stable-to-stable

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-impl-traitArea: `impl Trait`. Universally / existentially quantified anonymous types with static dispatch.A-varianceArea: Variance (https://doc.rust-lang.org/nomicon/subtyping.html)C-bugCategory: This is a bug.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-highHigh priorityS-bug-has-testStatus: This bug is tracked inside the repo by a `known-bug` test.T-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