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

Is that not the case with NonNull<u8>'s? Like can you have multiple NonNul<u8>'s soundly pointing to the same data? The standard library docs say that NonNull<u8> is a *mut T but non-zero and covariant".

I don't know what covariant means, but the ECS I'm contributing to, which is not my code, as far as I can tell, uses NonNull<u8>'s as pointers even to data which get must be read only and therefore gets casted to read only & references.

The data it references, though is in an UnsafeCell. That makes a difference doesn't it.

I didn't think of that. That's great. :+1:

Oh, that's even better. Thanks!

My (admittedly weak) understanding is that only &mut requires exclusive access, and *mut is unrestricted. If you use that *mut to produce an &'a mut, however, you must ensure that there are no reads or writes through any of the pointers while the 'a lifetime is valid.

2 Likes

Neither NonNull<T> nor *mut T is exclusive, so they can safely alias each other, or even a live &T or &mut T. The problem is how you get that pointer and whether there is or is not an interpretation of events where only one &mut T is live at a time.

I encourage you to read about the Stacked Borrows aliasing model if you want to have a deeper understanding of how aliasing relates to UB in Rust. This model is the closest thing we currently have to a definition of aliasing in Rust. It's a little more conservative than the C memory model (which is what LLVM uses) but it still lets you reason about the soundness of unsafe code that the compiler can't check.

This is accurate to my understanding. There's another concern, though: if you get the *mut by casting a &mut reference, that &mut reference now has to be valid and exclusive throughout the entire time between when you create the *mut and the last time you dereference the *mut. Copying the *mut is fine, even interleaving access between different *muts is allowable (as long as you maintain the rules of references internally) -- but all those *muts have to be derived from the same "loan" of the value that the original &mut took.

3 Likes

Yes, you can have multiple raw pointers pointing to the same data. It's just that you should create any extra raw pointers by copying the one you created initially, rather than casting the reference multiple times, because each time you touch the reference, the reference's rules trigger.

2 Likes

Ahhhh. That makes more sense.

So in my original example:

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);
}

@alice, you said dereferencing ptr1 inside the unsafe block would be UB, but if I did this it wouldn't be, right?

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

I think it's slightly more subtle than this. It's ok for the &mut reference to go out of scope before the last use of the raw pointer. The focus should be on what other references are used.

I prefer to think of it as "the raw pointer must not have been invalidated by a use of any other pointer/reference", plus a list of cases where using something invalidates other stuff. For example, any use of a mutable reference invalidates all other references that overlap with it, except for the reference you originally got the mutable reference from, and the reference you originally got that one from, and so on.

@zicklag Yes, your latter code block is perfectly fine.

1 Like

Agreed.

I think we're on the same page here, but just to check, do you agree that this code is sound and guaranteed to print 16?

let mut a = 10;
let p1 = {
    let ra = &mut a;
    ra as *mut _
}; // ra goes out of scope here but the borrow used to intialize it must be valid...
let p2 = p1;
unsafe {
    *p1 += 2;
    *p2 += 2;
    *p1 += 2;
    // ... until here, when it's no longer in use
}
println!("{}", a);
// Further use of `p1` or `p2` here would obviously be wrong

Each += 2 expression creates a new &mut reference, but none of them overlap, and p1 and p2 may be aliased since they are not &mut references.

That particular part of the code is what let p1 = &mut a as *mut _; desugars to (at least currently).

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

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.