Skip to content

Data race detected by Miri between Sender::take_waker() and Receiver::drop() #69

Description

@sandersaares

Miri detects a data race in oneshot.

error: Undefined Behavior: Data race detected between (1) non-atomic read on thread `unnamed-3` and (2) retag write of type `oneshot::Channel<()>` on thread `unnamed-2` at alloc724351
    -->
C:\Users\sasaares\.rustup\toolchains\nightly-x86_64-pc-windows-msvc\lib\rustlib\src\rust\library\alloc\src\boxed.rs:1260:9


     |

1260 |         Box(unsafe { Unique::new_unchecked(raw) }, alloc)

|         ok^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ (2) just happened here
     |
help: and (1) occurred earlier here

    --> C:\Source\oss\oneshot\src\lib.rs:1225:13
     |
1225 |             ptr::read(self.waker.get()).assume_init()
     |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
     = help: retags occur on all (re)borrows and as well as when references are copied or moved
     =
help: retags permit optimizations that insert speculative reads or writes
     = helpok: therefore from the perspective of data races, a retag has the same implications as a read or write
     = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior

That the race is between:

  • The ptr::read() in take_waker() called from Sender::send()
  • Receiver::drop() that drops the channel.

Precondition:

  • receiver observes UNPARKING state at least once

Because the UNPARKING state is exited via a Relaxed load, Miri considers that the Receiver::drop() that follows is racing with Sender::take_waker() - even though the latter was followed by an AcqRel state update, there was no Acquire that "closed the loop" on the receiver side.

The fix appears to be to add an Acquire fence when exiting the UNPARKING loop:

UNPARKING => loop {
    hint::spin_loop();
    // ORDERING: The load above has already synchronized with the write of the message.
    match channel.state.load(Relaxed) {
        MESSAGE => {
            // ORDERING: the sender has been dropped, so this update only
            // needs to be visible to us
            channel.state.store(DISCONNECTED, Relaxed);

            // synchronize with the take_waker() that resulted in us exiting the loop.
            fence(Acquire);

            // SAFETY: We observed the MESSAGE state
            break Poll::Ready(Ok(unsafe { channel.take_message() }));
        }
        DISCONNECTED => break Poll::Ready(Err(RecvError)),
        UNPARKING => (),
        _ => unreachable!(),
    }
},

Test that reproduces the issue:

#![cfg(feature = "std")]
#![cfg(feature = "async")]

use std::{
    future::Future,
    hint::spin_loop,
    pin::pin,
    task::{self, Poll, Waker},
};

#[test]
fn data_race() {
    // Depending on the random seed used by Miri, this will result in a data race being detected.
    // Use MIRIFLAGS="-Zmiri-many-seeds=0..512" to test with many different seeds.
    let (tx, rx) = oneshot::channel::<()>();

    let rx_thread = std::thread::spawn(move || {
        let mut rx = pin!(rx);

        let mut cx = task::Context::from_waker(Waker::noop());

        while let Poll::Pending = rx.as_mut().poll(&mut cx) {
            spin_loop();
        }
    });

    // It is important that we enter RECEIVING state first.
    // We start this thread later to increase the probability of that.
    let tx_thread = std::thread::spawn(move || {
        tx.send(()).unwrap();
    });

    tx_thread.join().unwrap();
    rx_thread.join().unwrap();
}

Steps to reproduce:

  • Set MIRIFLAGS="-Zmiri-many-seeds" to try with 64 different randomness seeds (result depends on Miri random choices). Use MIRIFLAGS="-Zmiri-many-seeds=0..512" to try with even more seeds if the time of day is not favorable for repro.
  • cargo +nightly miri test data_race

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions