It is a very deep rule of Rust that mut means "exclusive". More than just thread safety relies on this!
Very roughly speaking, the Rust compiler has two parts:
- The Rust "frontend", which includes the borrow checker and translates your Rust code to an assembly-like language called LLVM IR
- LLVM, which translates LLVM IR to machine code
The frontend tells LLVM that mut references are exclusive. That is, the frontend promises that while a mut reference to a value exists, no other pointers will be used to access that value. The frontend can make this promise because it borrow-checked your code.
LLVM has features like noalias and alias.scope metadata specifically to allow compilers to provide this kind of promise. It unlocks powerful optimizations in LLVM. But if the promise is broken, those optimizations could go powerfully wrong. For example, LLVM might legitimately reason as follows:
First, inline the call to <String as Display>::fmt inside the println on line 13.
The inlined code only needs two parts of the String: the length and the pointer to the characters.
Between the point where we set rs on line 8, and the point where it's used on line 13, rs is not used to modify the String.
And rs is exclusive; therefore nobody else is modifying it either. The String is not changed. (Note that this conclusion is wrong; the program does modify that string. So after this point LLVM's reasoning is going to be increasingly off-the-rails.)
Therefore we don't have to wait until line 13 to read the pointer and length from *rs. We can read them at any point in that range. It'll work because those fields aren't going to change.
Memory accesses will finish faster if you get started earlier. So let's read as early as possible. Move those reads to line 8.
Actually, if we're doing that, the length is guaranteed to be 5. So we don't need to read that at all.
Of course, LLVM doesn't actually "reason" like a human would, but it consists of multiple optimizer passes, each of which incrementally tweaks the code in ways that can have the same cumulative effect.
So you can see how LLVM could generate code that fetches rs.ptr before calling borrow_mut, which then grows the string, invalidating the pointer. The println! would then access freed memory, or at least print the wrong number of bytes.
I'm not sure LLVM would actually do something like this, if you somehow commented out the Rust borrow checker and tried it. But I wouldn't be surprised. Moving memory accesses around ("hoisting reads") is a real optimization that LLVM and other compilers really do. It's not even considered particularly fancy! And the Rust frontend might do similar optimizations before handing the code off to LLVM—I don't know.
That's a long answer to your question. The short answer is, there is no "when it's safe". It's never safe to break this rule, because it's not just for humans reasoning about their code. mut means "exclusive" to the compiler too.
sas mutable more than once at a time".borrow_mutwon't, say, pass the mutable borrow to a thread it launches, it just performs a mutation and is done. But Rust doesn't allow an interface to be defined as correct on anything but the prototype IIRC, so it can't know it's being used safely here.