What is source of provenance for locals of async blocks?

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.

2 Likes

Got it! So, projection of &mut Pin<&mut impl !Unpin> to &mut impl !Unpin is magic in pretty much the same way as &Cell<impl Sized> to &mut impl Sized?

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.

The documentation used to be written this way, but that was a conservative choice in documentation and never how the compiler or abstract machine actually worked. As of the resolution of Can a pointer obtained by casting `&UnsafeCell<T>` to `*mut T` be written to? · Issue #281 · rust-lang/unsafe-code-guidelines · GitHub and allow accessing the contents of UnsafeCell without going through get- #159730, it has been settled that the thing which UnsafeCell does is prevent &UnsafeCell<T> from implying that the memory is not writable; there is no special power in UnsafeCell’s methods.

1 Like

That's interesting.

To verify.

struct Foo {
    a: core::pin::UnsafePinned<u8>,
    b: u8,
    c: core::cell::UnsafeCell<u8>,
}

&Foo has permissions

  • shared read only: a, b
  • shared read/write: a,c

and &mut Foo has permissions

  • shared read/write: a
  • exclusive read/write: b,c

Is that about right?

While we're at that, is padding readable? writable through &T?

It's obviously writable through &mut T as long as std::mem::swap does an untyped copy.

There's nothing special about padding. Padding inside an UnsafeCell is writable, padding outside only readable.

1 Like

Yes, that’s right as far as I know.

1 Like