Description
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:
type F<'a, 'b> = impl Fn(T<'a>) -> T<'b> : 'static;
dyn Any
-erase it.- 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.
}
@rustbot modify labels: +I-unsound +regression-from-stable-to-stable
Metadata
Metadata
Assignees
Labels
Type
Projects
Status