To turn unsound interaction with Pin
's contract into a concrete dangling pointer, we need a type that relies on pinning guarantees – usually that’s going to be an async
-block Future
, because those are why Pin
was introduced in the first place.
For example, it should be unsound for an API to produce a Pin<&mut T>
reference, and then move the value anyway. (As long as the Pin<&mut T>
is exposed to the API user, and the type T
isn't owned by the API.)
use std::pin::Pin;
use std::ops::Deref;
fn my_new<Ptr: Deref>(pointer: Ptr) -> Pin<Ptr> {
unsafe { Pin::new_unchecked(pointer) }
}
fn unsound_pin_then_move<T>(x: T, expose_to: impl FnOnce(Pin<&mut T>)) {
let mut x_in_initial_place = x;
let r: Pin<&mut T> = my_new(&mut x_in_initial_place);
expose_to(r);
let x_in_different_place = x_in_initial_place;
}
For simplifying the exploitation, let's actually expose a Pin<&mut T>
to this new position, too (though that isn't necessary; e.g. a Drop
in the new place would also be unsound).
fn unsound_pin_then_move_then_pin<T>(x: T, mut expose_to: impl FnMut(Pin<&mut T>)) {
let mut x_in_initial_place = x;
let r: Pin<&mut T> = my_new(&mut x_in_initial_place);
expose_to(r);
let mut x_in_different_place = x_in_initial_place;
let r2: Pin<&mut T> = my_new(&mut x_in_different_place);
expose_to(r2);
}
Let's convert unsound_pin_then_move_then_pin
into a dangling reference. Here's an example future we could try to use.
async {
let my_array = [1, 2, 3, 4, 5];
let r = &my_array;
something().await;
println!("array through r: {r:?}");
}
these Futures in Rust rely on pinning. The future directly contains the array my_array
and the reference r
. If these are moved together at the .await
point, to a different place, r
still points to the original position.
We need something that actually yields at the await
:
enum YieldOnce {
Initial,
Yielded,
}
impl Future for YieldOnce {
type Output = ();
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<()> {
use YieldOnce::*;
match *self {
Initial => {
*self = Yielded;
Poll::Pending
}
Yielded => Poll::Ready(())
}
}
}
async fn something() {
YieldOnce::Initial.await
}
So we get this far:
fn main() {
let fut = async {
let my_array = [1, 2, 3, 4, 5];
let r = &my_array;
something().await;
println!("array through r: {r:?}");
};
unsound_pin_then_move_then_pin(fut, |pinned| {
// TODO
});
}
Now, we need to use the pinned
handle to move the future to the first await point, by polling once. To call Future::poll
, we need to construct a no-op Context
…
let w: Waker = todo!();
let cx = Context::from_waker(&w);
containing a no-op Waker
…
struct Noop;
impl Wake for Noop {
fn wake(self: Arc<Self>) {}
}
…
let w: Waker = Arc::new(Noop).into();
Finally, polling:
unsound_pin_then_move_then_pin(fut, |pinned| {
let _ = pinned.poll(&mut cx);
});
(playground)
array through r: [1, 2, 3, 4, 5]
OKAY… it isn’t completely broken yet. But already partially, let's add some debug printing:
async {
let my_array = [1, 2, 3, 4, 5];
let r = &my_array;
println!("{:p}, {:p}", &my_array, r);
something().await;
println!("{:p}, {:p}", &my_array, r);
println!("array through r: {r:?}");
}
0x7ffc936e4cf8, 0x7ffc936e4cf8
0x7ffc936e4d18, 0x7ffc936e4cf8
array through r: [1, 2, 3, 4, 5]
aha, so the array now is in a different spot than the reference that's supposed to point to it.
Really all that's left is to make this more clearly broken. A segfault would be nice. One way to start would be to do something more to the initial place. How about an Option
? Those tend to have fun usage of niches for None
that can break the previously contained value…
fn unsound_pin_then_move_then_pin<T>(x: T, mut expose_to: impl FnMut(Pin<&mut T>)) {
let mut x_in_initial_place = Some(x);
let r: Pin<&mut T> = my_new(x_in_initial_place.as_mut().unwrap());
expose_to(r);
let mut x_in_different_place = x_in_initial_place.take().unwrap();
let r2: Pin<&mut T> = my_new(&mut x_in_different_place);
expose_to(r2);
}
0x7ffc2ae55248, 0x7ffc2ae55248
0x7ffc2ae55268, 0x7ffc2ae55248
array through r: [-1154135295, 24654, 719671872, 32764, 719671856]
ah, already much more broken. And I’m not even sure how exactly it went wrong, haha!
(playground)
But I promised segfaults! Easy, we already have something where integers get messed up… what if these were pointers?
async {
let my_array = [Some(&42)];
let r = &my_array;
println!("{:p}, {:p}", &my_array, r);
println!("array through r (before await): {r:?}");
something().await;
println!("{:p}, {:p}", &my_array, r);
println!("array through r (after await): {r:?}");
}
0x7ffe58ff88d8, 0x7ffe58ff88d8
array through r (before await): [Some(42)]
0x7ffe58ff88f0, 0x7ffe58ff88d8
array through r (after await): [Some(415531848)]
promising, miri is unhappy for a long time already btw, but apparently the OS was still fine with this pointer access. Only one solution: Double indirection!
let my_array = [Some(&&42)];
0x7ffc6a80b138, 0x7ffc6a80b138
array through r (before await): [Some(42)]
0x7ffc6a80b150, 0x7ffc6a80b138
…
Exited with signal 11 (SIGSEGV): segmentation violation
…aaaaand BOOM
(playground)