Skip to content
← All articles

Aliasing: why &mut means noalias

The aliasing guarantee survives raw pointers, `UnsafeCell` relaxes exactly one half of it, and transmuting `&T` to `&mut T` is always undefined behaviour — no exceptions, and rustc says so by name.

You met aliasing XOR mutability in track 3 as a rule the borrow checker enforces. Now that raw pointers are in play, the same rule reappears as a rule the borrow checker cannot enforce — and which you owe anyway.

Everything in track 3 still holds. This page adds the three things that only become visible once unsafe is available.

The guarantee, restated for unsafe code

rustc lowers &mut T parameters to LLVM’s noalias attribute — the automatic, everywhere-applied equivalent of C’s restrict. It also marks &T as readonly where it can. Those attributes are promises the compiler makes to the optimiser on your behalf, and the optimiser spends them:

  • a value loaded through a &mut can be cached in a register across an arbitrary function call, because nothing else can have written to it;
  • stores can be reordered, merged, or sunk out of loops;
  • a redundant load can be deleted entirely;
  • branches that depend on a value the compiler believes unchanged can be folded away.

The guarantee has already been spent by codegen before your unsafe block runs. That is the real answer to “why can’t I just have two &mut, I’ll be careful”: careful is not the currency. The machine code has already been generated on the assumption that you cannot.

Addition 1: uniqueness is not recoverable through raw pointers

This is the one people get wrong.

let r: &mut i32 = &mut x;
let p: *mut i32 = r;
let q: *mut i32 = r;

Deriving two raw pointers from one &mut is fine. Raw pointers are not tracked, carry no uniqueness claim, and creating them is safe.

let a: &mut i32 = unsafe { &mut *p };
let b: &mut i32 = unsafe { &mut *q };   // both live -> UB

Using those pointers so that two &mut are live at the same time is undefined behaviour, and the fact that you went through raw pointers changes nothing. The rule is about references, and it applies to references that are merely live, not merely used. Creating them is enough.

💡If two &mut are UB even when neither is written through, what exactly is the compiler allowed to do? click to reveal

Reorder and cache freely across both, on the assumption they cannot see each other.

Suppose a and b are both &mut i32 to the same place. The compiler may keep *a in a register across a write through b, then write the stale register value back — losing b‘s store. Or it may sink a‘s store past b‘s load, so b reads the old value. Neither transformation is “wrong”; both are licensed by noalias.

Note this happens without either reference being misused in any way you would recognise. There is no bad read, no out-of-bounds, no null. The bug is that two objects exist which the compiler was told could not both exist, and every optimisation that consumed that fact is now unsound.

Which is why the practical discipline is: keep it in raw pointers, and mint the reference as late and as briefly as possible. A *mut T you hold for the whole function costs nothing. A &mut T you hold for the whole function is a claim you must be able to defend for the whole function.

Addition 2: UnsafeCell relaxes exactly one half

UnsafeCell<T> is the single language-level construct that retracts the &T immutability guarantee. It is the foundation of Cell, RefCell, Mutex, RwLock and every atomic.

What it does not do is the part that matters here:

UnsafeCell relaxes ONLY the &T immutability guarantee. There is no legal way to obtain aliasing &mut — not even with UnsafeCell.

The std documentation says this explicitly. UnsafeCell::get() hands you a *mut T; what you do with that raw pointer is still governed by the &mut uniqueness rule the moment you turn it into a reference. Two live &mut derived from one UnsafeCell are exactly as undefined as two live &mut derived from anywhere else.

So the correct mental model is: UnsafeCell widens the set of places you may write through a shared reference. It does not widen the set of &mut you may hold. Item 17.15 is the exercise.

Addition 3: transmuting &T to &mut T is always undefined behaviour

Always. Not “usually”. Not “unless you’re careful”. The Nomicon’s phrasing is the memorable one:

“Transmuting an & to &mut is Undefined Behavior. No you can’t do it. No you’re not special.”

rustc’s mutable_transmutes lint is deny-by-default and produces:

error: transmuting &T to &mut T is undefined behavior, even if the reference is unused,
consider instead using an UnsafeCell

Read “even if the reference is unused”. The &mut violates the &T‘s immutability guarantee the instant it exists, because a &mut is a claim of exclusivity, and the &T you transmuted from is direct evidence that the claim is false.

The same applies to the cast route — &x as *const T as *mut T — which rustc catches with the deny-by-default invalid_reference_casting, again naming UnsafeCell in the diagnostic. Both lints suggest the same fix because there is only one fix.

💡Both diagnostics say "consider using an UnsafeCell". Why is that a real fix rather than a way of hiding the same operation? click to reveal

Because UnsafeCell changes what the original reference promised, at the point the data was declared — not at the point you want to mutate it.

When you write let x: UnsafeCell<i32>, every &UnsafeCell<i32> that is ever created carries a weaker guarantee: the compiler knows this place may change behind a shared reference, and it does not emit the readonly attribute or cache loads across calls. The permission is granted by the type, up front, to every holder.

Transmuting takes a reference that was already created under the strong guarantee — and about which the optimiser may already have reasoned — and lies about it retroactively. The information the compiler used is gone; you cannot un-emit the attribute.

This is a general principle worth carrying: you cannot retroactively weaken a guarantee that has already been made. Permission has to be in the type, from the start, visible to everyone who touches the place. Every legitimate interior-mutability design in Rust obeys it.

Why there is no problem attached to this page

Because observing the difference requires committing undefined behaviour.

You could write two functions, one with aliasing &mut and one without, and compare their output — and on today’s rustc, at -O, on your machine, they will usually agree. The divergence is not deterministic, not reproducible, and not something a test can assert. Demonstrating it means writing a program with no meaning and drawing conclusions from what the compiler happened to emit, which is the exact reasoning error this track exists to eliminate.

What you can do is run the examples under Miri, which does model the aliasing rules and will reject them. Item 17.14 is about that — what the models say, what Miri detects, and why “Miri is clean” is a floor rather than a ceiling.

Lints on this page

clippy::mut_from_ref is correctness/deny and rejects any function taking &self (or any shared reference) and returning &mut:

error: mutable borrow from immutable input(s)

That signature is unsound by construction — call it twice and you have two live &mut with no unsafe at either call site. It is the most useful single lint in this track.

clippy::transmute_ptr_to_ref catches transmute::<*const T, &T>, which produces a reference with an unbounded lifetime the caller chooses. Write &*p instead, inside a function whose signature bounds the lifetime.

clippy::undocumented_unsafe_blocks is where you write down which of these arguments you are relying on.

Three sentences to carry

  1. &mut means noalias, the compiler has already spent it, and going through raw pointers does not recover the ability to have two.
  2. UnsafeCell relaxes the &T immutability rule and nothing else.
  3. The aliasing rules apply to references that are live, not references that are used — creating one is enough.