I was looking on conditions which allow a reference to escape its block syntactically, as in the example below.
// Subscription adds the reference to static BTreeMap<usize, &'static mut u32>
// new_event() increments all subscribed values
fn sync_test() {
let mut events = 0;
{
let s = unsafe { Subscription::new(&mut events).unwrap() };
new_event();
new_event();
// _ = { &events }; <-- would invalidate &mut, making line below UB
new_event();
s.unsubscribe();
}
assert_eq!(events, 3);
}
I get the synchronous example, but not why the same works for async:
/// SAFETY: poll the future to completion.
async unsafe fn async_test() {
let mut events = 0;
{
let s = unsafe { Subscription::new(&mut events).unwrap() };
new_event();
yield_once().await; // forces to return Poll::Pending once
// now we get control again, and it is another call...
new_event();
new_event();
s.unsubscribe();
}
assert_eq!(events, 3);
}
One could think that provenance of &mut events has been renewed once we polled the async fn the second time. However, that reference could escape and be used while the future is inactive!
let mut fut = std::pin::pin!(async {
let mut events = 0;
{
let s = unsafe { Subscription::new(&mut events).unwrap() };
new_event();
yield_once().await;
new_event();
s.unsubscribe();
}
assert_eq!(events, 3);
});
and wrapping code can call new_event() between fut.poll's:
let Poll::Pending = fut.as_mut().poll(cx) else { ... };
_ = { &mut fut };
// ^^^ `fut`'s mutable borrow (used to poll it) shall end
new_event();
let Poll::Ready(()) = fut.as_mut().poll(cx) else { ... };
So, there are no borrows of fut, but a part of its stack memory is modified. Miri accepts it (the whole code included). What's the right model for the provenance here?
The memory of async blocks is not subject to aliasing restrictions (currently via a special rule for !Unpin types, in the future via a wrapper type called UnsafePinned), so accesses to it "from outside" make use of the provenance the outside pointer/reference was originally created with, and don't derive from, or conflict with, the &mut created for polling the async block.
No, currently, &mut impl !Unpin is magic, regardless of how it is obtained. But in the future, the magic will be in &mut UnsafePinned<T>. In both cases, the projection doesn’t matter.
There is no such magic. If you create an exclusive reference to the contents of a Cell<T>, the resulting &mut T reference will have the same aliasing rules as any other &mut T. Cell’s interior mutability depends on never creating such references.
Sorry, meant UnsafeCell here; the fact that UnsafeCell::as_mut_unchecked adds the write permission out of thin air.
I was about to say that &mut impl !Unpin gains a larger lifetime than the pinned reference it was constructed from (there was no &mut fut living long enough), thus the extra provenance is minted out of thin air, but then I got that provenance doesn't 1-to-1 correspond to lifetimes.