Is it fine to have multiple mutable pointers to something if I don't dereference more than one?

Yes, I agree it is sound.

1 Like

Hmm. Because &mut T is neither Copy nor Clone, I expected the as-cast here to consume ra immediately. Some experimentation on the playground shows that it does continue to survive somehow, but I can’t figure out why.

I can’t think of any other operation that you can do to a non-Copy, non-Clone type that leaves the original in place. Is this a form of auto-reborrowng?


Edit: On reflection, there are a few others. At least, you can take a reference to the value or access its members directly.

That's my interpretation, but it's kind of moot in this case because there's nothing you could do with ra after casting it to test that theory that would not invalidate the later uses of p1 and p2. However since it's safe to cast test to *mut f32 twice in a row I think that reborrowing is the only interpretation that makes sense.

I'd recommend dropping p1 and p2 before using a, just to be on the safe side and no one getting the idea to look at the code on another day and think p1 and p2 could be used, again.

I put together a couple of variants on @trentj’s example to help me understand. My belief is that f2 is sound, but f1 is not; does that seem right?


Edit: I ran this through miri and got exactly the opposite answer to what I expected: It has no problem with f1, but the drop call in f2 invalidates the pointers. Commenting that line out makes f2 pass miri as well. (Playground)


fn f1<'a>(ra: &'a mut i32)->&'a mut i32 {
    let p1 = ra as *mut _;
    let p2 = p1;
    unsafe {
        *p1 += 2;
        *p2 += 2;
        *p1 += 2;
    };
    ra
}

fn f2<'a>(ra: &'a mut i32)->&'a mut i32 {
    let p1 = ra as *mut _;
    std::mem::drop(ra);
    let p2 = p1;
    unsafe {
        *p1 += 2;
        *p2 += 2;
        *p1 += 2;
        &mut *p2
    }
}

The drop call moves ra into the drop function, and moves are considered a use, thus it asserted exclusive access and invalidated p1.

2 Likes

f1 looks fine to me. p1 and p2 are used to create mutable references that are derived from ra, but they don't overlap each other and ra is not used again until after p1 and p2 are not used anymore.

f2 gives me some pause because I don't know whether passing ra to drop constitutes a "use" of ra. If it does, then f2 is unsound because the use of ra in drop invalidates its use to initialize p1. (Ok, I guess it does.)

My example might have mistakenly given the impression that ra going out of scope was important to the soundness of what followed. Sorry about that.

1 Like

That was a mistake on my part; here’s a fixed version of f2 that maintains my intended semantics (passes miri):

fn f2b<'a>(ra: &'a mut i32)->&'a mut i32 {
    let p1 = Some(ra).take().unwrap() as *mut _;
    let p2 = p1;
    unsafe {
        *p1 += 2;
        *p2 += 2;
        *p1 += 2;
        &mut *p2
    }
}

From stacked borrow's point of view, copying a raw pointer doesn't create a new one — it's just sorta the same raw pointer, so a mutable reference created from p2 has both p1 and p2 as "parents" in the sense I used earlier to describe which pointers were invalidated by mutable references.

FYI, a good way to know if something is sound is to try and write the pointer code only using Rust references; and seeing *mut T as &UnsafeCell<T> (aliased upgradable-to-unique-on-demand pointer):

  • impl<T : ?Sized> /* SomeExtensionTrait<T> for */ UnsafeCell<T> {
        /// Downgrade the uniqueness guarantee to only be applicable during
        /// the `'uniq` spans, instead of continually until the point of last usage.
        fn from_mut (it: &'_ mut T) -> &'_ UnsafeCell<T>
        {
            unsafe { ::core::mem::transmute(it) }
        }
    
        /// Safety: of all the `&UnsafeCell`s aliasing the pointee,
        /// none may be in an upgraded `&mut T` or even `&T` form
        /// for the duration of `'uniq`
        unsafe
        fn assume_unique<'uniq> (self: &'uniq UnsafeCell<T>)
          -> &'uniq mut T
        {
            &mut *self.get()
        }
    }
    

With this, we can rewrite:

fn main() {
    let test = &mut 77f32;
    
    {
        let ptr1 = test as *mut f32;
        let ptr2 = test as *mut f32;
        
        unsafe {
            *ptr2 = 32f32;
        };
    }
    
    assert_eq!(*test, 32f32);
}

as:

fn main() {
    let test = &mut 77f32;
    
    {
        let ptr1: &Cell<f32> = Cell::from_mut(test);
        let ptr2: &Cell<f32> = Cell::from_mut(test);
        // if ptr1 were used here, we'd get a borrock error.
        ptr2.set(32f32);
    }
    
    assert_eq!(*test, 32f32);
}
  • And we can see why using ptr1 would fail there

Whereas the ptr2 = ptr1 version would be:

fn main() {
    let test = &mut 77f32;
    
    {
        let ptr1: &Cell<f32> = Cell::from_mut(test);
        let ptr2: &Cell<f32> = ptr1; // OK, does not invalidate `ptr1`
        
        unsafe {
            ptr1.set(32f32); // OK
        };
    }
    
    assert_eq!(*test, 32f32);
}

We can now apply this logic to @2e71828's examples:

fn f1<'a> (ra: &'a mut i32) -> &'a mut i32 
{
    let p1: &'_ UnsafeCell<i32> = UnsafeCell::from_mut(ra);
    let p2 = p1; // OK
    unsafe {
        *p1.assume_unique() += 2; // `'uniq` one-line span for `p1`: OK
        *p2.assume_unique() += 2; // same for `p2`
        *p1.assume_unique() += 2; // same for `p1`
    }
    ra // OK: using `ra` invalidates `p1` and `p2`, but they are no longer usable anyways.
}

as well as:

fn f2<'a> (ra: &'a mut i32) ->&'a mut i32
{
    let p1: &'a UnsafeCell<i32> = UnsafeCell::from_mut(ra); // (re)borrow -----+
    std::mem::drop(ra); // Invalidates `p1`: (re)borrowed value dropped here   |
    let p2 = p1; // borrow used here <-----------------------------------------+
    unsafe {
        *p1 += 2;
        *p2 += 2;
        *p1 += 2;
        &mut *p2
    }
}

and

fn f2b<'a> (ra: &'a mut i32) -> &'a mut i32
{
    let p1: &'a UnsafeCell<i32> = UnsafeCell::from_mut(ra);
    let p2 = p1;
    unsafe {
        *p1.assume_unique() += 2; // OK
        *p2.assume_unique() += 2; // OK
        *p1.assume_unique() += 2; // OK
        p2.assume_unique() // `'uniq = 'a` here, OK because `p1` is never upgraded, and `ra` is never used.
    }
}

Aside

FWIW, I'm considering making a PR to suggest those two functions

  • (+ some name-bikesheddable unsafe fn assume_no_mut<'no_mut> (self: &'no_mut UnsafeCell<T>) -> &'no_mut T; that's the one I can't come up with a good name for it)

be added to UnsafeCell<T>, since they would improve the teachability of UnsafeCell<T>, interior mutability and Stacked Borrows model, and would also lead to less dangerous code (manually upgrading .get() is harder to read: it is harder to notice that the invariants to uphold are different for .assume_unique() than for .assume_no_mut() if the author is, instead, using &mut* ....get() and &* ...get() each time).

5 Likes

The good thing about this approach is that the compiler appears smart enough to optimize it out:

(Godbolt)

example::f2b:
        mov     rax, rdi
        add     dword ptr [rdi], 6
        ret
1 Like