Error: E0506, what does reference invalidation mean?

The following code does not compile, with the following error error[E0506]: cannot assign to x because it is borrowed. The error documentation says x can't be assigned to a new value as it would invalidate the reference.

fn main() {
    let mut x = 12;
    let y = &mut x;  // y points to stack
    x = 13;               // Error: cannot assign to `x` because it is borrowed
    dbg!(y);
}

I do not understand how this is a problem. y points the stack address of x and assigning to x change just change its value (address of x remains the same). Then what does this error mean by invalidate the reference. Here reference still remains valid after the reassignment.

This become a problem if the reference directly points to the heap data (aka Smart pointers).

For example, below y points directly to the heap data. Upon reassignment to x, new heap is allocated and thus y now points to invalid memory.

fn main() {
    let mut x = "12".to_string();
    let y = &*x;                                 // y points to heap data
    x = "13".to_string();                  // Error: cannot assign to `x` because it is borrowed
    dbg!(y);
}

Pointers to the stack and to the heap (or any other memory segment) don't have different types. Consequently, it is not possible for the compiler to distinguish between the "obviously" unsafe and "obviously" safe code based on types, so it has to be conservative.

Also, it is actually not the case that the reason for unsoundness of shared mutability would be heap pointers. If you were to replicate the above snippet using a String, then the string object itself (as opposed to its backing buffer) would still be on the stack. Yet allowing the code to compile would be incorrect.

What matters is whether the type being mutated has a destructor, because shared mutability could then cause observation of destroyed values, which is wrong and unsound regardless of "the heap" or "the stack".

Accordingly, the kind of code you are trying to produce is an anti-pattern in Rust. Nevertheless, it is possible to achieve something similar by using constructs like Cell. The Cell type places a Copy bound on its type parameter, ensuring that it can't have a destructor.

Anyway, shared mutation of values is still a major source of confusing logical bugs, so it's best to avoid it either way, even if it is not immediately a source of undefined behavior.

3 Likes

Thank you for your reply.

How does String make it unsound? The reference will still point to the stack object.

Because you can obtain a pointer to the backing buffer via a pointer to the String, as you demonstrated in your own code.

Please confirm if my understanding is correct,

The below code which points to the buffer in heap is unsafe, as the reference can be invalidated if x is dropped.

However when writing let y = &x is still okay as it points to the stack.

As the compiler cannot determine just by looking at a type, if the variable lives in the stack or heap, it prevents all references with this rule.

There's nothing involved around heap or stack, Rust's borrow checker does not care where the data is. Rust just forbid any interleaved usage of mutable borrow. Rust do not distinguish "shallow" pointer or "deep" pointer.

A &mut T can assume that it is exclusive; no other independent thing can even observe the T. And certainly they cannot mutate (write to) the T. Despite the name, it's not just about mutation.

A &T can assume that nothing in T changes so long as the &T is valid, unless there's a shared mutability primitive in T.

In both cases, replacing the entirety of the T itself violates the assumptions, and thus must invalidate the reference.


Here's an example which does not involve anything on the heap which demonstrates why a reference must be invalidated.

5 Likes