rustunsafe

Rust aliasing: do references to the parent struct count as references to its fields?


Below is a definition for a struct that maintains an integer with a dumb operation on it: call into C++ to spawn a thread that will call back into Rust and increment the value, and in the meantime also increment it on this thread. The struct accepts the reference to itself as a mutable pinned reference (simulating e.g. the way Future::poll works). It hands off a const pointer to its embedded Mutex<u64> to C++, and when C++ calls back it converts that back into a shared reference to the Mutex<u64>.

use std::ffi::c_void;
use std::pin::Pin;
use std::sync::Mutex;

struct S {
    val: Mutex<u64>,
    _marker: std::marker::PhantomPinned,
}

impl S {
    // Increment the value twice, in a really inefficient way and without
    // waiting for both increments to take effect.
    pub fn increment_twice(self: Pin<&mut Self>) {
        let self_ref: &mut Self = unsafe { Pin::into_inner_unchecked(self) };

        // Increment once in the background with C++.
        unsafe {
            start_thread_with_cpp(
                cast_and_increment,
                &self_ref.val as *const Mutex<u64> as *mut c_void,
            );
        }

        // And another time here on this thread.
        let mut val = self_ref.val.lock().unwrap();
        *val += 1;
    }
}

impl Drop for S {
    fn drop(&mut self) {
        // Assume that we somehow wait for all outstanding calls to finish here,
        // i.e. there are no dangling pointers.
        todo!();
    }
}

unsafe extern "C" {

    // Use C++ to spawn a thread that will eventually call the supplied function
    // with the supplied argument.
    fn start_thread_with_cpp(f: unsafe extern "C" fn(*mut c_void), p: *mut c_void);

}

// Cast back to *const  and increment.
extern "C" fn cast_and_increment(p: *mut c_void) {
    let val: &Mutex<u64> = unsafe { &*(p as *const Mutex<u64>) };
    let mut val = val.lock().unwrap();
    *val += 1;
}

As far as I'm aware, this code is more or less correct in the sense that it doesn't have any data races. The mutex does its own internal synchronization, so it's totally fine to access it concurrently from two threads.

But my question is: are there any aliasing violations here? We only ever create and hand off shared references to the mutex, but of course it's within the S and we do have a mutable reference to that struct. Does this count as a mutable reference to the mutex for the purposes of the aliasing rules?

Follow-up questions:


Solution

  • This looks to be a specific instance of a long-standing problem in Rust that others have encountered as well. Some examples of other places it has come up:

    As you can see from the last two links, it seems that it's been known for years that much of the Rust async ecosystem relies on similar code that is technically unsound.

    Does this example have undefined behavior?

    Maybe? Rust's aliasing rules are annoyingly but intentionally vaguely defined, so it sort of depends on who you ask:

    What real world problems could this cause?

    One easy one is that tools might reject it. UB detection tools like Miri or BorrowSanitizer might get upset about it (I haven't checked). Depending on your definition this may or may not count as a real world problem, since it would make it harder to use the tools to find other problems in a codebase that included this code.

    More importantly but also more speculatively: it's possible that the compiler assuming memory isn't aliased could cause it to use that memory for temporary space in some optimization. For example it might spill registers there and then restore a legal value later. To be clear I haven't actually observed this. Perhaps I couldn't—it sounds like llvm's noalias attribute isn't consistently used by Rust to exploit the aliasing rules, despite their promise for opitmizations.

    How could we structure it differently?

    From the links above, it sounds like there is actually no way to do this correctly in stable Rust, and lots of code is technically unsound because of it. The leading proposal for fixing it seems to be UnsafePinned, which is described well by its RFC.