Do reference accesses really invalidate pointers?

The Rust documentation for std::ptr has this rule:

  • The result of casting a reference to a pointer is valid for as long as the underlying object is live and no reference (just raw pointers) is used to access the same memory. That is, reference and pointer accesses cannot be interleaved.

I agree that pointers invalidate references. That is, if you use a pointer to access data while a reference to that data exists, sure, that's UB. But references invalidate pointers?

  1. Is that right?

  2. Why is that rule necessary? It seems like it makes programming with pointers super dangerous and error-prone, especially since the "casting" is done implicitly by the compiler, e.g. if you pass a reference to a pointer argument.

  3. Does this mean that passing a pointer to this function invalidates the pointer?

unsafe fn initialize(p: *mut Point) {
    let pt = &mut *p;
    pt.x = 0;
    pt.y = 0;
}

My simple minded and vague answer is that the compiler (via LLVM) is allowed to do all sorts of radical optimizations with references that are not allowed with pointers. So mixing them in any form, for the same variable, is going to be dangerous. Having a rule that prohibits this is a really good idea.

Your code doesn't contain any "casting a reference to a pointer", so that paragraph doesn't literally apply, but also, I suspect the original text means to say that you can't interleave use of a particular mutable reference and pointers derived from that reference.

Certainly re-borrows from reference to pointer and back have to be sound, or many std data structures such as Vec would be unsound.

2 Likes

Here is some code that does both reference-to-pointer casts and pointer-to-reference casts, and lots of mutation with each. Miri agrees with me that it contains no UB, and I hope most Rust programmers with unsafe experience will agree with me that this is all reasonable code:

fn main() {
    let mut x = 1;
    foo(&mut x);
}
fn foo(x: &mut u8) {
    *x += 1;
    unsafe { bar(x) };
    *x += 1;
}
unsafe fn bar(x: *mut u8) {
    unsafe {
        *x += 1;
        baz(&mut *x);
        *x += 1;
    }
}
pub fn baz(x: &mut u8) {
    *x += 1;
}

The above code (specifically foo) does coerce a reference to a pointer, use the pointer, and then use the reference, but it is sound. I think that what documentation means, but expresses poorly, is that you must not do the following:

fn main() {
    let mut x = 1;
    bad(&mut x);
}
fn bad(x: &mut u8) {
    let p: *mut u8 = x;
    *x += 1;
    unsafe { *p += 1; }; // UB because we used the `x` that we coerced from
}

I think that by “interleaving”, they meant a sequence of operations that cannot be described as strictly nested (each reborrow or coercion being dropped (or more precisely, no longer used) before its origin is used again), the way the first program sticks to.

4 Likes

Great point. Thank you! I felt like I must be taking crazy pills.

I should submit a PR, but I don't feel like I know what the text should say.

References do not invalidite the pointer it was created from.

1 Like

Based on @kpreid's response (which seems right!) even changing the word "interleaved" to "overlapping" would help. And to add his words:

... That is, reference and pointer accesses must be non-overlapping, by which we mean each reborrow or coercion is dropped (or more precisely, no longer used) before its origin is used again.

I made a little example where Miri is not happy:

fn main()
{
    let mut x = 0;
    let r = &mut x;
    let p : * mut i32 = r;
    *r = 4;
    unsafe{ *p = 5; }
    println!("r={}", r)
}
7 |     unsafe{ *p = 5; }
  |             ^^^^^^
  |             |
  |             attempting a write access using <2383> at alloc1160[0x0], but that tag does not exist in the borrow stack for this location
  |             this error occurs as part of an access at alloc1160[0x0..0x4]