rustundefined-behaviorunsafe

Ensure that a mutable reference is dead at a given point in a function, to avoid UB


I'm writing unsafe code. It looks something like this:

fn yield_value(value: &mut T) {
  let value_ptr = std::ptr::from_mut(value);
  SOME_ATOMIC_PTR.store(value_ptr, Ordering::Release);
  // [...other stuff]
  park_thread_until_we_re_sure_no_references_to_value_can_exist_anymore();
}

As we all know, &mut references to overlapping memory cannot coexist. I'm worried about the fact that value is still in scope by the time we release its pointer to another thread, which may re-create a reference from it. I know that Rust's lifetime length inference is quite complex, and I don't know if the spec gives me any guarantee that value won't be alive past its last use. I could be missing an obvious solution but I'm not sure how to proceed.

I would like to make sure that value is considered "no longer alive" before the atomic release, to avoid any possible overlap.


Solution

  • I believe this is sound, but I am not an expert on Stacked Borrows/Tree Borrows.

    Liveness is not the problem here. Liveness is a compile-time concept, while modeling unsafe code is inherently a runtime thing. Validity of references is not determine by their variables' liveness: all present and probably future as well models for aliasing consider the reference only when it is accessed (in a general sense; e.g. any reborrow will cause a retag, including a function call).

    There is a potential problem here though, and that is protectors. In rough outline, to justify many optimizations we declare that references passed to functions (including in nested fields, but not including raw pointers) are protected and need to live until the exit from the function.

    I believe that to not be a problem, though, because protectors don't retag the reference at the exit from the function; that is, they are not like write or reads that make sure this reference (more precisely: tag) is the top reference, causing any reference derived from it to be invalidated, they just check the reference is still on the stack/tree, which is the case here as a reference derived from it is live.

    Modeling this in Miri also succeeds executing, which further shows this is sound code.

    The code for Miri:

    use std::sync::atomic::{AtomicPtr, Ordering};
    
    static PTR: AtomicPtr<i32> = AtomicPtr::new(std::ptr::null_mut());
    
    fn foo(r: &mut i32) {
        *r = 789;
        PTR.store(std::ptr::from_mut(r), Ordering::Relaxed);
    }
    
    fn main() {
        let mut v = 123;
        foo(&mut v);
        let p = PTR.load(Ordering::Relaxed);
        unsafe { *p = 456 };
    }