Mutating a heap-allocation through a shared reference to its container

Hey!

Take a look at the following code.

fn main() {
    let v: Vec<i32> = vec![0i32];
    
    // We're using `&v` here, and we're getting a `*const i32` back.
    let ptr_to_first = v.as_ptr() as *mut i32;

    unsafe {
        // Safety:
        //  `v` is owned by this function. `ptr_to_first` points into the
        //  vector's owned memory, ensuring we have unique access to it.
        *ptr_to_first = 10i32;
    }
    
    // Mutation did occur even though `v` is not marked as mutable
    // (i.e. `let v` and not `let mut v`).
    println!("{}", v[0]);
}

At first, I had assumed this code unsound because we're basically mutating the vector through a shared reference (v.as_ptr() takes &self).

However, according to what I understand of Stacked Borrows, this code should be sound.

For heap allocations, the stack of each freshly allocated memory location is Stack { borrows: vec![(Untagged: SharedReadWrite)] } , and the initial pointer to that memory has tag Untagged . (ref)

When we use v.as_ptr() to get a pointer to the allocation, the stack-memory that stores v is borrowed, but the actual heap location is left untouched. When the pointer to that location is used to write 10i32, the allocated memory is still a SharedReadWrite and not a SharedReadOnly like v.

When I run miri (see playground), it detects no undefined behaviour. Is this intended? Or more generally, can I rely on this (allocated memory acting like UnsafeCell)?

I'm probably misunderstanding something here, because Stacked Borrows isn't exactly easy to understand. If I do, please explain to me why this is, or is not, sound.

EDIT: Here is an example where this could be useful: playground

1 Like

It's true that, as Vec::as_ptr is currently implemented, your program is actually ok, however the documentation for Vec::as_ptr says the following:

The caller must also ensure that the memory the pointer (non-transitively) points to is never written to (except inside an UnsafeCell ) using this pointer or any pointer derived from it. If you need to mutate the contents of the slice, use as_mut_ptr .

So you are violating the documented safety requirements for as_ptr. This means that your program's correctness is relying on an implementation detail of as_ptr, and future changes to as_ptr or Rust's safety rules may change as_ptr in a way that makes your program exhibit UB without it being a breaking change.

4 Likes

Mmh. That's interesting.

So if I'm implementing a container myself, I should follow this principle of as_ptr and as_mut_ptr? Simply using a as_ptr(&self) -> *mut T would work today but might break later. Is that it?

You can make the guarantees you want in your own data structures, but returning *mut T from an as_ptr(&self) method would certainly be unidiomatic.

Understood. Thanks for the clarification!

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.