- Notifications
You must be signed in to change notification settings - Fork 13.8k
Description
As context, this entirely safe code is reported as UB by Miri under both Stacked Borrows and Tree Borrows:
use core::{ops::Deref, pin::{pin, Pin}, task::{Context, Poll, Waker}}; fn main() { let mut f = pin!(async move { let x = &mut 0u8; core::future::poll_fn(move |_| { *x = 1; //~^ ERROR write access is forbidden Poll::<()>::Pending }).await }); let mut cx = Context::from_waker(&Waker::noop()); assert_eq!(f.as_mut().poll(&mut cx), Poll::Pending); _ = <Pin<&mut _> as Deref>::deref(&f); assert_eq!(f.as_mut().poll(&mut cx), Poll::Pending); }
Output
Under TB:error: Undefined Behavior: write access through <1666> at alloc969[0x8] is forbidden --> src/main.rs:7:13 | 7 | *x = 1; | ^^^^^^ write access through <1666> at alloc969[0x8] is forbidden | = help: this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental = help: the accessed tag <1666> has state Frozen which forbids this child write access help: the accessed tag <1666> was created here, in the initial state Reserved --> src/main.rs:6:9 | 6 | / core::future::poll_fn(move |_| { 7 | | *x = 1; 8 | | Poll::<()>::Pending 9 | | }).await | |________________^ help: the accessed tag <1666> later transitioned to Active due to a child write access at offsets [0x8..0x9] --> src/main.rs:7:13 | 7 | *x = 1; | ^^^^^^ = help: this transition corresponds to the first write to a 2-phase borrowed mutable reference help: the accessed tag <1666> later transitioned to Frozen due to a reborrow (acting as a foreign read access) at offsets [0x0..0x10] --> src/main.rs:13:9 | 13 | _ = <Pin<&mut _> as Deref>::deref(&f); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ = help: this transition corresponds to a loss of write permissions = note: BACKTRACE (of the first span): = note: inside closure at src/main.rs:7:13: 7:19 --> src/main.rs:9:12 | 9 | }).await | ^^^^^ note: inside `main` --> src/main.rs:14:16 | 14 | assert_eq!(f.as_mut().poll(&mut cx), Poll::Pending); | ^^^^^^^^^^^^^^^^^^^^^^^^ note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace error: aborting due to 1 previous error
This isn't a bug in Miri really. It's a bug... elsewhere. Part of that is described by:
But at the same time, this is relevant to the post-RFC work on UnsafePinned
:
The idea of UnsafePinned
is that we use it to wrap the fields subject to self-borrows so that given a mutable reference to such fields, the compiler (and our model) won't assume that a foreign write can't occur.
But the RFC suggested that while &mut UnsafePinned<T>
was special, &UnsafePinned<T>
wasn't, and would therefore act like &T
. This is where we've hit problems due to safe Pin::deref
, as @RalfJung has described:
- Tracking Issue for RFC 3467: UnsafePinned #125735 (comment)
- Initial
UnsafePinned
implementation [Part 1: Libs] #137043 (comment) - !Send Future violates stacked borrow rules miri#3796 (comment)
In the above example, if the compiler were to insert:
let x: &mut UnsafePinned<0u8> = ..;
Then we still end up having a problem, under the RFC's model and our current model behavior, which is (quoting @RalfJung):
when a shared reference gets created, do a read of everything the reference points to (except
UnsafeCell
) to ensure things are in a sane state
What happens in the example is that:
- We create and store a mutable self-reference to the
u8
owned by the future. - We activate that self-reference with a write, marking it
Unique
under TB. - Then, due to the
deref
creating a reference to the future and our behavior to treat that as a read of the entire future, a foreign read occurs, marking the mutable reference asFrozen
. - Next time we try to write, we get UB.
About this, @RalfJung has said:
In the RFC we had the question whether
UnsafePinned
should also act like anUnsafeCell
. I am now fairly sure that the answer is "yes, it must". The reason is this: due toPin::deref
being safe, it is at any moment possible to create a shared reference to a future that is halted at a suspension point. If there is noUnsafeCell
, this acts like a read of the entire future data, and that read conflicts with mutable references that the future may hold to its own data. This is not a necessary property of aliasing with mutable reference, but it is a necessary consequence of the decision thatPin::deref
should be safe.
And also that:
One could imagine aliasing models that don't do such a read, though then
dereferenceable
becomes more questionable and I don't know how this would impact optimizations.
Anyway, it seems we should have a canonical issue to use to decide about how to proceed about this and how either our model or RFC 3467 would need to be adjusted before the stabilization of UnsafePinned
, so here is that issue.
2025-04-26:
As a slight refinement here, note that we actually don't even need deref
, as Pin::as_ref
is enough.
use core::{ pin::{pin, Pin}, task::{Context, Poll, Waker}, }; fn main() { let mut f = pin!(async move { let x = &mut 0u8; core::future::poll_fn(move |_| { *x = 1; //~^ ERROR write access is forbidden Poll::<()>::Pending }) .await }); let mut cx = Context::from_waker(&Waker::noop()); assert_eq!(f.as_mut().poll(&mut cx), Poll::Pending); let _: Pin<&_> = f.as_ref(); // Or: `f.as_mut().into_ref()`. assert_eq!(f.as_mut().poll(&mut cx), Poll::Pending); }
cc @RalfJung @WaffleLapkin @rust-lang/opsem @rust-lang/lang
@rustbot labels +T-lang +T-opsem +C-discussion +F-unsafe_pinned