From semantic perspective, what's the moment an undefined behavior of `&mut` noalias occurred in Rust?

Viewed 183

As Rust reference documention said

Breaking the pointer aliasing rules. &mut T and &T follow LLVM’s scoped noalias model, except if the &T contains an UnsafeCell.

It's really ambiguous.
I want to know that what's the exactly moment an undefined behavior of &mut noalias occurred in Rust.

Is it any of below, or something else?

  1. When defining two &mut that point to the same address?
  2. When two &mut that point to the same address exposed to rust?
  3. When perfrom any operation on a &mut that point to the same address of any other &mut?

For example, this code is observously UB:

unsafe {
    let mut x = 123usize;
    let a = (&mut x as *mut usize).as_mut().unwrap(); // created, but not accessed
    let b = (&mut x as *mut usize).as_mut().unwrap(); // created, accessed
    *b = 666;
    drop(a);
}

But what if I modify the code like this:

struct S<'a> {
    ref_x: &'a mut usize
}

fn main() {
    let mut x = 123;
    let s = S { ref_x: &mut x }; // like the `T` in `ManuallyDrop<T>`
    let taken = unsafe { std::ptr::read(&s as *const S) }; // like `ManuallyDrop<T>::take`
    // at thist ime, we have two `&mut x`
    *(taken.ref_x) = 666;
    drop(s);
    // UB or not?
}

Is the second version also UB?
The second version is totally the same implemention to std::mem::ManuallyDrop. If the second version is UB, is it a security bug of std::mem::ManuallyDrop<T>?

1 Answers

What the aliasing restriction is not

It is actually common to have multiple existing &mut T aliasing the same item.

The simplest example is:

fn main() {
   let mut i = 32;
   let j = &mut i;
   let k = &mut *j;

   *k = 3;

   println!("{}", i);
}

Note, though, that due to borrowing rules you cannot access the other aliases simultaneously.

If you look at the implementation of ManuallyDrop::take:

pub unsafe fn take(slot: &mut ManuallyDrop<T>) -> T {
    ptr::read(&slot.value)
}

You will note that there are no simultaneously accessible &mut T: calling the function re-borrows ManuallyDrop making slot the only accessible mutable reference.

Why is aliasing in Rust so ill-defined

It's really ambiguous. I want to know that what's the exactly moment an undefined behavior of &mut noalias occurred in Rust.

Tough luck, because as specified in the Nomicon:

Unfortunately, Rust hasn't actually defined its aliasing model.

The reason is that the language team wants to make sure that the definition they reach is both safe (demonstrably so), practical, and yet does not close the door to possible refinements. It's a tall order.

The Rust Unsafe Code Guidelines Working Group is still working on establishing the exact boundaries, and in particular Ralf Jung is working on an operational model for aliasing called Stacked Borrows.

Note: the Stacked Borrows model is implemented in MIRI, and therefore you can validate your code against the Stacked Borrows model simply by executing your code in MIRI. Of course Stacked Borrows is still experimental, so this doesn't guarantee anything.

What caution recommends

I personally subscribe to caution. Seeing as the exact model is unspecified, the rules are ever changing and therefore I recommend taking the stricter interpretation possible.

Thus, I interpret the no-aliasing rule of &mut T as:

At any point in the code, there shall not be two accessible references in scope which alias the same memory if one of them is &mut T.

That is, I consider that forming a &mut T to an instance T for which another &T or &mut T is in scope without invalidating the aliases (via borrowing) is ill-formed.

It may very well be overly cautious, but at least if the aliasing model ends up being more conservative than planned, my code will still be valid.

Related