Can `Arc::is_unique` return `true` in this example?

Consider this example:

#![feature(arc_is_unique)]
use std::sync::{
    Arc,
    atomic::{AtomicBool, AtomicPtr, Ordering},
};
use std::thread;
fn main() {
    let mut is_unique = false;
    let arc = Arc::new(1); // #0
    let ptr = AtomicPtr::new(std::ptr::null_mut());
    let flag = AtomicBool::new(false);
    let ptr_rf = &ptr;
    let flag_rf = &flag;
    thread::scope(|s| {
        // thread 1
        s.spawn(move || {
            // clone() behaves like `strong.fetch_add(1,Relaxed)`
            let arc2 = arc.clone(); // #1
            ptr_rf.store(Arc::into_raw(arc2) as *mut i32, Ordering::Relaxed); // #2
            while !flag_rf.load(Ordering::Acquire) {} // #3
        });
        // thread 2
        s.spawn(|| {
            let raw_arc = loop {
                let val = ptr.load(Ordering::Relaxed) as *const i32; // #4
                if !val.is_null() {
                    break val;
                }
            };
            let arc = unsafe { Arc::from_raw(raw_arc as *const _) };
            is_unique = Arc::is_unique(&arc); // #5
            flag.store(true, Ordering::Release); // #6
        });
    });
    println!("{}", is_unique);
}

In this example, #0 creates the control block and initializes strong and weak to 1, respectively; these modifications all happens-before the beginning of the two threads. The clone() at #1, as explained in the comment, is a modification that increases strong by one.

Since #2 and #4 both use Relaxed memory ordering, the modification at #1 is unordered with #5; In other words, #1 doesn't happen before #5, the modification at #1 is not guaranteed to be visible to #5. According to the write-read coherence rule:

If a side effect X on an atomic object M happens before a value computation B of M, then the evaluation B takes its value from X or from a side effect Y that follows X in the modification order of M.

Such a side effect X in this example is the modification to strong at #0; this means the load to strong within Arc::is_unique is valid to read the initial value 1.

#6 synchronizing with #3 ensures that the load to strong within Arc::is_unique cannot read the modification of the drop of Arc in thread 1, which could be 1. This excludes the case that Arc::is_unqiue returns true because it reads that value.

So, is is_unique in this example possible to be true?

PS:

The implementation of Arc::is_unique is approximately as follows:

    pub fn is_unique(this: &Self) -> bool {
        // lock the weak pointer count if we appear to be the sole weak pointer
        // holder.
        //
        // The acquire label here ensures a happens-before relationship with any
        // writes to `strong` (in particular in `Weak::upgrade`) prior to decrements
        // of the `weak` count (via `Weak::drop`, which uses release). If the upgraded
        // weak ref was never dropped, the CAS here will fail so we do not care to synchronize.
        if this.inner().weak.compare_exchange(1, usize::MAX, Acquire, Relaxed).is_ok() {
            // This needs to be an `Acquire` to synchronize with the decrement of the `strong`
            // counter in `drop` -- the only access that happens when any but the last reference
            // is being dropped.
            let unique = this.inner().strong.load(Acquire) == 1;

            // The release write here synchronizes with a read in `downgrade`,
            // effectively preventing the above read of `strong` from happening
            // after the write.
            this.inner().weak.store(1, Release); // release the lock
            unique
        } else {
            false
        }
    }

I believe the answer is yes, it is possible for is_unique to be true in this example.

the key insight is the two store operations in thead 1, one for the strong count, one for the atomic pointer, can be observed in different order from the perspective of a different thread, because the two memory transaction writes to different memory locations, and both have the Relaxed order.

this would probably not happen on x86 architecture, but can actually happen on weak ordered hardware, not just a theoretical possibility, I think.

You can run this locally with

MIRIFLAGS="-Zmiri-many-seeds" cargo miri run

and see many runs returning true :wink:


Edit: similarly, doing multiple iterations of this will also show it on the playground

(given the threads live in a scope, there is no concurrency interaction to be expected between different iterations)


Edit2: Also, of course the underlying reasons why this “fails” to begin with is that the operations of Arc are implemented based on the assumption that any transfer of ownership of an Arc – or transfer of a borrow of an Arc (via immutble or mutable reference) – to another thread, is itself synchronized through different mechanisms.

While this code example does work around some of the other issues that such a transfer of a pointer without synchronization would often entail, such as the ability to access a memory location before it’s initialized, or after it’s freed (by making the initialization happen in the main thread; and by synchronizing the drop operations[1]).

Things that can also create problems include e.g.: mutating the Arc’s content while it’s still uniquely referenced by thread 1 (even just this is enough to make miri always reject the whole thing):

-   let arc = Arc::new(1); // #0
+   let mut arc = Arc::new(1); // #0
    let ptr = AtomicPtr::new(std::ptr::null_mut());
    let flag = AtomicBool::new(false);
    let ptr_rf = &ptr;
    let flag_rf = &flag;
    thread::scope(|s| {
        // thread 1
        s.spawn(move || {
+           Arc::get_mut(&mut arc);
            // clone() behaves like `strong.fetch_add(1,Relaxed)`
            let arc2 = arc.clone(); // #1

  1. though actually, I don’t think this particular step makes too much of a difference here – actually it seems more to only exists such that any is_unique == true result is obviously “unsound”, never possibly just a case of “well the other thread had already dropped it” ↩︎

MIRIFLAGS="-Zmiri-many-seeds" cargo miri run

I didn't know this approach. Can this approach be directly used on the playground?

by synchronizing the drop operations

The purpose is only to make the load of strong within Arc::is_unique either read the modification at #1 or the earlier modification(i.e., the initial value 1) in the modification order of strong.

I think the read-only access to arc in thread 2 is safe because the existence of a drop of arc in thread 2 ensures that the access happens-before the deallocation of the resource, regardless of the order in which the drop in the two threads happens.

I realized this is likely the case while writing the sentence (see the footnote I had already added :sweat_smile:)

I think you're probably correct about this.


I don't know of a supported direct way. Indirectly though - well - you could just call miri with std::process::Command I guess…


Edit: IDK why it seems to want to re-compile some of the dependencies, and it seems like it wants me to hit the run button twice before actually working but otherwise… this seems to work ^^

Edit2: Ah! Probably RAM usage limits, -j 1 makes it work first try!

[Run these with the normal “Run” button, not through selecting miri in the tools section.]

Thanks. I find that the result true is rarely reproducible in run mode; I cannot see true in run mode. However, this can be merely reproduced in the common miri mode.