You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hey, nice job with this crate, and the flavor around it 😁
Alas, I'm afraid the ergonomic anonymous-binding-scope!()-macro API which you wished to use is unable to expose a fully sound non-unsafe API.
The culprit? Rust yield-generators / async {}.await coroutines.
Philosophical Explanation
Indeed, their sugar makes it so some locals are not "stack" locals, but rather, part of the coroutine state. This results in two related things:
As a yield / .await happens, it is possible to return from the current (actual poll()) function before the end of the function is executed, which includes code in charge of cleaning up this coroutine-captured state.
We're not even guaranteed that this coroutine-captured state will ever be cleaned up, as it can end up pinned in "static memory" (or rather, : 'static memory, such as the one produced by Box::leak())
Another way to highlight it is that this results in a very conceptually odd aspect of Rust:
any <'generic> lifetime parameter of an fn is deemed "bigger" than any local-to-the-fn-body lifetime, even when these fn-locally-owned instances end up in some Coroutine or Future state.
This is "generally"/in-the-typical-case sound because the returned Coroutine/Future type ends up 'generic-lifetime infected.
and yet local-to-the-fn-body lifetimes, such as in &'local T, when the T instance is captured as part of the Coroutine/Future state, is actually more like a Pin<&'local T>. The Pin-ness ensures that the drop of any locally-owned var will run before the coroutine's backing memory is repurposed ("the Drop guarantee of Pin").
⚠️ But the coroutine's state may actually never get repurposed, if it ends up pinned in 'static memory ⚠️
Warning
In practice this means that fn-locally-owned instances may actually never get dropped: being an anonymous binding, such as the Scope instance produced by the scope!() in the &Scope { … } RHS, or being a macro-hygiene-private binding (such as the one produced by pin!) does not prevent this⚠️
Exploit
use::core::{future, mem, task};asyncfnfoo(r:&str,out:&mutOption<::lien::Ref<str>>){let scope = ::lien::scope!();*out = Some(scope.lend(r));
future::pending().await}#[test]fnmain(){let r = {letmut out = None;let borrowee = String::from("...");let fut = foo(&borrowee,&mut out);poll_once_and_leak(fut);// drop(borrowee);
out.unwrap()};let _unrelated = String::from("UAF");// just for the `cargo t -- --nocapture` demodbg!(&*r);// on miri, UAF UB-detection panic, else, it probably prints `UAF`.unreachable!("this should either have failed to compile, or hanged before hitting this");}fnpoll_once_and_leak(fut:implFuture){
_ = Future::poll(// Leaked-in-`'static` `pin`ned data://// Could also have been written as:// - `Pin::static_mut(Box::leak(Box::new(fut)))`// - `Box::pin(fut).as_mut()` (followed by a `forget()`)
mem::ManuallyDrop::new(Box::pin(fut)).as_mut(),&mut task::Context::from_waker(task::Waker::noop()),);}
On miri:
$ cargo t --test exploit -q
running 1 test
main --- FAILED
failures:
---- main stdout ----
[tests/exploit.rs:20:5]&*r = "UAF"
thread 'main'(57885) panicked at tests/exploit.rs:22:5:
internal error: entered unreachable code: this should either have failed to compile, or hanged before hitting this
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
$ cargo +nightly miri t -q --test exploit
running 1 test
error:UndefinedBehavior: constructing invalid value of type&str: encountered a dangling reference(use-after-free)
--> /usr/local/rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/non_null.rs:441:18
|
441 | unsafe{&*self.as_ptr().cast_const()}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UndefinedBehavior occurred here
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused UndefinedBehavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: this is on thread `main`
= note: stack backtrace:0: std::ptr::NonNull::<str>::as_ref::<'_>
at /usr/local/rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ptr/non_null.rs:441:18:441:461: <lien::Ref<str>as std::ops::Deref>::deref
at src/lib.rs:496:26:496:432: main
at tests/exploit.rs:20:11:20:133:main::{closure#0}
at tests/exploit.rs:10:10:10:10
Solution
There is no way around getting rid of the macro API, alas, unless you found a way with a macro to assert you're in a place which forbids yields or .awaits. So you'll need to use a callback API for your Scope constructor:
Hey, nice job with this crate, and the flavor around it 😁
Alas, I'm afraid the ergonomic anonymous-binding-
scope!()-macro API which you wished to use is unable to expose a fully sound non-unsafeAPI.The culprit? Rust
yield-generators /async {}.awaitcoroutines.Philosophical Explanation
Indeed, their sugar makes it so some locals are not "stack" locals, but rather, part of the coroutine state. This results in two related things:
As a
yield/.awaithappens, it is possible toreturnfrom the current (actualpoll()) function before the end of the function is executed, which includes code in charge of cleaning up this coroutine-captured state.We're not even guaranteed that this coroutine-captured state will ever be cleaned up, as it can end up pinned in "
staticmemory" (or rather,: 'staticmemory, such as the one produced byBox::leak())Another way to highlight it is that this results in a very conceptually odd aspect of Rust:
any
<'generic>lifetime parameter of anfnis deemed "bigger" than any local-to-the-fn-body lifetime, even when thesefn-locally-owned instances end up in someCoroutineorFuturestate.Coroutine/Futuretype ends up'generic-lifetime infected.and yet local-to-the-
fn-body lifetimes, such as in&'local T, when theTinstance is captured as part of theCoroutine/Futurestate, is actually more like aPin<&'local T>. ThePin-ness ensures that the drop of any locally-owned var will run before the coroutine's backing memory is repurposed ("theDropguarantee ofPin").'staticmemoryWarning
In practice this means that⚠️
fn-locally-owned instances may actually never get dropped: being an anonymous binding, such as theScopeinstance produced by thescope!()in the&Scope { … }RHS, or being a macro-hygiene-private binding (such as the one produced bypin!) does not prevent thisExploit
On
miri:Solution
There is no way around getting rid of the macro API, alas, unless you found a way with a macro to assert you're in a place which forbids
yields or.awaits. So you'll need to use a callback API for yourScopeconstructor:#[doc(hidden)] pub __…-prefixed field names, I guess