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