This Zulip thread led me down an interesting rabbit hole... here's some interesting async code that causes a use-after-free:
$ ./miri run async2.rs --edition 2021 -Zmiri-disable-stacked-borrows -Zmiri-disable-validation
error: Undefined Behavior: pointer to alloc872 was dereferenced after this allocation got freed
--> async2.rs:39:15
|
39 | let _ = { *problematic_variable };
| ^^^^^^^^^^^^^^^^^^^^^ pointer to alloc872 was dereferenced after this allocation got freed
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: BACKTRACE:
= note: inside closure at async2.rs:39:15
= note: inside `<std::future::from_generator::GenFuture<[static generator@async2.rs:32:20: 40:2]> as std::future::Future>::poll` at /home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/core/src/future/mod.rs:91:19
note: inside closure at async2.rs:14:9
--> async2.rs:14:9
|
14 | unsafe { Pin::new_unchecked(&mut future) }.poll(cx)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: inside `<std::future::PollFn<[closure@async2.rs:13:47: 13:56]> as std::future::Future>::poll` at /home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/core/src/future/poll_fn.rs:61:9
= note: inside `<std::boxed::Box<std::future::PollFn<[closure@async2.rs:13:47: 13:56]>> as std::future::Future>::poll` at /home/r/.rustup/toolchains/miri/lib/rustlib/src/rust/library/alloc/src/boxed.rs:2074:9
note: inside `main` at async2.rs:29:13
--> async2.rs:29:13
|
29 | let _ = Pin::new(&mut pinned2).as_mut().poll(&mut cx);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The code does contain an unsafe
block, but that block looks fine at first sight:
let mut future = trouble();
let mut pinned = Box::pin(future::poll_fn(move |cx| {
unsafe { Pin::new_unchecked(&mut future) }.poll(cx)
}));
future
is not moved, so we can just re-create a pinned reference to it each time.
Except, unfortunately, later the future is being moved. This is possible because PollFn<F>
is always Unpin
, even if F: !Unpin
. And the move
in the closure means that future
actually lives inside the closure environment, so when we move the PollFn
, we invalidate a self-referential generator.
This causes trouble even without actually moving the PollFn
-- the example in this Miri bug report violates aliasing rules, since as part of the PollFn
wrapper, mutable references are being created, and due to PollFn: Unpin
, those are truly unique mutable references, thus the fact that they alias with the self-referential generator causes Stacked Borrows UB.
I have not found a way to cause UB without unsafe
, so there is strictly speaking not a bug here -- but I feel like this is a big footgun, so at least the poll_fn
docs should be quite upfront about the fact that PollFn
is always movable, so you better be careful about what you do in that closure. Maybe it would have been better to not make PollFn
unconditionally Unpin
; I think the regular auto trait impl for Unpin
would fix the aliasing violation (since it would propagate the aliasing rule exemption we have for self-referential generators) and would also stop the use-after-free example from compiling.
In fact maybe Unpin
should have been an unsafe trait after all...