| |

Rust 17 🦀 The Aliasing XOR Mutability Rule

The aliasing XOR mutability rule is the single most important invariant in Rust’s type system. It states that at any given time, a value may be accessed through either one mutable reference or any number of immutable references, but never both. Every other safety guarantee in the language — the absence of data races, the absence of iterator invalidation, the absence of use-after-free through references — follows from this rule. The borrow checker exists to enforce it, and the lifetime system exists to make it expressible. Understanding the rule as a single idea, rather than as a collection of error messages, is the key to understanding why the borrow checker accepts some code and rejects others.

The rule is usually written as “aliasing XOR mutation.” Aliasing means multiple references to the same value. Mutation means at least one reference can write. If you have aliasing, you cannot have mutation. If you have mutation, you cannot have aliasing. The two states are mutually exclusive, and the compiler tracks which state every value is in at every point in the program.

This chapter covers the formal statement of the rule, the two states it defines, the compiler’s reasoning about lifetimes, the consequences of the rule for aliasing and mutation, the interaction with interior mutability, and the patterns that satisfy the rule.

Key point: A value may have either one mutable reference or any number of immutable references, but not both at the same time. This is the aliasing XOR mutability rule. The borrow checker enforces it, and every other memory-safety guarantee follows from it. Interior mutability (Cell, RefCell, Mutex) is the controlled exception: it allows mutation through an immutable reference, with the mutation checked at runtime.


Why the aliasing XOR mutability rule matters

The data-race definition. A data race occurs when two threads access the same memory concurrently and at least one writes, with no synchronization. The rule forbids the combination at compile time, so safe Rust cannot have data races. This is the formal justification for the rule.

The aliasing bug. When two pointers refer to the same memory and one writes, the other sees a value it did not expect. Iterator invalidation, stale reads, and the classic memcpy aliasing problems are all consequences of aliasing with mutation. The rule eliminates the class.

The optimization opportunity. The compiler can assume that an immutable reference’s value does not change while the reference is alive. It can hoist reads out of loops, cache values in registers, and reorder operations. These optimizations are only sound because the rule guarantees no mutation through an alias.

The reasoning simplification. A reader of Rust code can assume that a &T does not change and that a &mut T is not observed by anyone else. This makes the code easier to reason about. The rule turns a global property (no aliasing with mutation anywhere) into a local property (this reference is the only one that can mutate).

The API contract. The rule is part of every function signature. A function that takes &T promises not to mutate. A function that takes &mut T requires exclusive access. The caller and the callee agree on the contract, and the compiler enforces it.


a. The formal statement

The rule has two parts.

Part 1: Exclusivity of mutation. If a value is accessed through a mutable reference, no other reference — mutable or immutable — may access the same value for the duration of that reference.

Part 2: Freedom of reading. If a value is accessed only through immutable references, any number of them may exist simultaneously, and no mutation may occur.

The two parts are the two allowed states. The rule forbids the mixed state.

StateReferencesMutation
ReadingMany &TNone
WritingOne &mut TAllowed
Mixed&T and &mut TForbidden

The rule is checked at compile time. It is not a runtime check, and it does not cost anything at runtime.


b. The two allowed states

The reading state allows any number of immutable references. This is the safe state for concurrent reads, for sharing data, and for passing values to functions that do not modify them.

let data = vec![1, 2, 3];

let r1 = &data;
let r2 = &data;
let r3 = &data;

println!("{:?} {:?} {:?}", r1, r2, r3);

The three references read the same value. The compiler permits this because none of them can mutate, so there is no risk of one seeing a change made by another.

The writing state allows exactly one mutable reference. This is the safe state for modification, for building a value, and for passing a value to a function that modifies it.

let mut data = vec![1, 2, 3];

let w = &mut data;
w.push(4);
w.push(5);

The single mutable reference modifies the value. The compiler permits this because no other reference can observe the value while the mutable reference is alive.

The two states do not overlap. Once a mutable reference is created, the immutable references must be out of scope. Once the mutable reference is last used, the immutable references can be created again.


c. How the compiler reasons about lifetimes

The compiler tracks the lifetime of every reference. A reference’s lifetime begins when it is created and ends at its last use. The borrow checker verifies that the lifetimes of the references to a value do not create a forbidden combination.

let mut s = String::from("hello");

let r1 = &s;          // r1's lifetime begins
let r2 = &s;          // r2's lifetime begins
println!("{} {}", r1, r2);  // r1 and r2 last used here
// r1 and r2 lifetimes end

let r3 = &mut s;      // r3's lifetime begins
r3.push_str("!");     // r3 last used here
// r3 lifetime ends

println!("{}", s);    // s is valid again

The immutable references r1 and r2 end at the println!. The mutable reference r3 begins after that, and it ends at the push_str. The two states do not overlap, so the code compiles.

Before non-lexical lifetimes, the borrows would have lasted until the end of the block, and the code would have been rejected. The change made the rule ergonomic without changing its meaning.

The compiler’s reasoning is local: it does not need to understand what the program does. It only needs to verify that the lifetimes of the references do not overlap in a forbidden way.


d. Consequences for aliasing

The rule forbids several patterns that are common in other languages.

Iterator invalidation. A loop that holds a reference to a collection and modifies the collection is rejected.

let mut v = vec![1, 2, 3];

// Error: cannot borrow v as mutable because it is also borrowed as immutable
for item in &v {
    v.push(*item);
}

The for loop borrows v immutably, and the v.push tries to borrow it mutably. The two overlap, so the compiler rejects the code.

Stale references. A reference that outlives the value it points to is rejected.

// Error: returns a reference to a local value
fn dangling() -> &String {
    let s = String::from("hello");
    &s
}

The String is dropped at the end of the function, so the returned reference would dangle. The compiler rejects the code.

Concurrent mutation. Two threads that access the same value with at least one writing are rejected. The rule extends to threads through the Send and Sync marker traits.

use std::thread;

let mut data = vec![1, 2, 3];
// Error: cannot borrow data as mutable more than once
let t1 = thread::spawn(|| data.push(1));
let t2 = thread::spawn(|| data.push(2));

Each closure captures data mutably, which requires two mutable references. The compiler rejects the code. The correct pattern is to move the data into one thread, or to wrap it in a Mutex.


e. Interior mutability: the controlled exception

The rule is about references and mutability. It does not say that a value cannot change; it says that a value cannot be changed through a shared reference. Interior mutability is the mechanism that allows a value to be changed through a shared reference, with the mutation checked at runtime instead of compile time.

The Cell<T> and RefCell<T> types in the standard library provide interior mutability for single-threaded use. Cell allows replacing the value but not taking a reference to it. RefCell enforces the borrowing rules at runtime, panicking if they are violated.

use std::cell::RefCell;

let data = RefCell::new(vec![1, 2, 3]);
let r1 = data.borrow();
let r2 = data.borrow();
// let w = data.borrow_mut();  // panics at runtime if r1 or r2 are alive

The borrow and borrow_mut methods return Ref and RefMut guards. The guards track how many shared and exclusive borrows are active, and borrow_mut panics if a shared borrow is live. This is the compile-time rule enforced at runtime.

Mutex<T> and RwLock<T> provide the same idea for multi-threaded use, with the synchronization handled by the lock. The lock enforces the rule across threads.

Interior mutability is the escape hatch that makes the rule practical. It allows the rule to be relaxed in controlled ways, while keeping the safe default. The cost is that violations are detected at runtime rather than compile time, and the developer is responsible for ensuring the code does not panic.


f. Patterns that satisfy the rule

The rule is not an obstacle; it is a design constraint that shapes idiomatic Rust.

Short-lived borrows. The borrow ends at its last use, so the code does not need to be restructured as long as the borrows do not overlap.

let first = &v[0];
println!("{}", first);
v.push(4);  // OK: first is no longer used

Cloning instead of aliasing. When two parts of the code need to modify independent copies, cloning creates separate values.

let v = vec![1, 2, 3];
let doubled: Vec<i32> = v.iter().map(|n| n * 2).collect();

The iter() borrows v immutably, and the collect() produces a new vector. The original is untouched.

Split borrows. When two fields of a struct need to be modified independently, the borrow checker allows it because the fields are disjoint.

struct Pair {
    a: Vec<i32>,
    b: Vec<i32>,
}

let mut pair = Pair { a: vec![], b: vec![] };
let a = &mut pair.a;
let b = &mut pair.b;  // OK: different fields

The two mutable references are to different fields, so they do not alias.

Interior mutability for shared mutation. When a value must be mutated through a shared reference, RefCell or Mutex provides the mechanism.

use std::cell::RefCell;

let counter = RefCell::new(0);
*counter.borrow_mut() += 1;

The RefCell allows the mutation through an immutable binding, with the borrowing rule checked at runtime.


Complete Example Session

// ============================================
// PART 1: MANY IMMUTABLE REFERENCES
// ============================================
fn main() {
    let data = vec![1, 2, 3];
    let r1 = &data;
    let r2 = &data;
    let r3 = &data;
    println!("{:?} {:?} {:?}", r1, r2, r3);
}
// ============================================
// PART 2: ONE MUTABLE REFERENCE
// ============================================
fn main() {
    let mut data = vec![1, 2, 3];
    let w = &mut data;
    w.push(4);
    println!("{:?}", w);
}
// ============================================
// PART 3: THE MIXED STATE IS FORBIDDEN
// ============================================
// This does not compile:
//
// let mut data = vec![1, 2, 3];
// let r = &data;
// let w = &mut data;
// println!("{:?} {:?}", r, w);
// ============================================
// PART 4: NON-LEXICAL LIFETIMES
// ============================================
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{} {}", r1, r2);

    let r3 = &mut s;
    r3.push_str("!");
    println!("{}", r3);
}
// ============================================
// PART 5: ITERATOR INVALIDATION REJECTED
// ============================================
// let mut v = vec![1, 2, 3];
// for item in &v {
//     v.push(*item);
// }
// ============================================
// PART 6: CLONE TO AVOID ALIASING
// ============================================
fn main() {
    let v = vec![1, 2, 3];
    let doubled: Vec<i32> = v.iter().map(|n| n * 2).collect();
    println!("{:?} {:?}", v, doubled);
}
// ============================================
// PART 7: SPLIT BORROWS
// ============================================
struct Pair {
    a: Vec<i32>,
    b: Vec<i32>,
}

fn main() {
    let mut pair = Pair { a: vec![], b: vec![] };
    let a = &mut pair.a;
    let b = &mut pair.b;
    a.push(1);
    b.push(2);
    println!("{:?} {:?}", pair.a, pair.b);
}
// ============================================
// PART 8: INTERIOR MUTABILITY WITH REFCELL
// ============================================
use std::cell::RefCell;

fn main() {
    let counter = RefCell::new(0);
    *counter.borrow_mut() += 1;
    *counter.borrow_mut() += 1;
    println!("{}", counter.borrow());
}
// ============================================
// PART 9: RUNTIME BORROW CHECK
// ============================================
use std::cell::RefCell;

fn main() {
    let data = RefCell::new(vec![1, 2, 3]);
    let r1 = data.borrow();
    let r2 = data.borrow();
    // let w = data.borrow_mut();  // panics at runtime
    println!("{:?} {:?}", r1, r2);
}
// ============================================
// PART 10: MUTEX FOR MULTI-THREADED MUTATION
// ============================================
use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Mutex::new(0);
    let mut handles = vec![];

    for _ in 0..10 {
        let c = std::sync::Arc::clone(&std::sync::Arc::new(counter));
        handles.push(thread::spawn(move || {
            *c.lock().unwrap() += 1;
        }));
    }

    for h in handles {
        h.join().unwrap();
    }
}

These ten parts cover many immutable references, one mutable reference, the forbidden mixed state, non-lexical lifetimes, iterator invalidation rejected, cloning to avoid aliasing, split borrows, interior mutability with RefCell, the runtime borrow check, and Mutex for multi-threaded mutation.


Quick Reference

The Rule

StateReferencesMutation
ReadingMany &TNone
WritingOne &mut TAllowed
MixedBothForbidden

Consequences

PatternResult
Many readersAllowed
One writerAllowed
Reader and writerRejected
Two writersRejected
Dangling referenceRejected
Data raceRejected

Interior Mutability Types

TypeThread-safeBorrow check
Cell<T>NoCopy in/out
RefCell<T>NoRuntime
Mutex<T>YesLock
RwLock<T>YesLock

Compiler Errors

ErrorCause
cannot borrow as mutableImmutable borrow still live
cannot borrow as immutableMutable borrow still live
cannot borrow as mutable more than onceTwo mutable borrows
does not live long enoughReference outlives referent

Satisfying the Rule

PatternPurpose
Short-lived borrowsLet borrows end at last use
CloneIndependent copies
Split borrowsDifferent fields
Interior mutabilityControlled shared mutation

Best Practices

✅ Do This:

// Let borrows end at their last use
let first = &v[0];
println!("{}", first);
v.push(4);

// Clone for independent copies
let doubled: Vec<_> = v.iter().map(|n| n * 2).collect();

// Split borrows for different fields
let a = &mut pair.a;
let b = &mut pair.b;

// Use RefCell for shared mutation
let counter = RefCell::new(0);
*counter.borrow_mut() += 1;

// Use Mutex for multi-threaded mutation
let counter = Mutex::new(0);

❌ Don’t Do This:

// Hold a mutable and immutable reference
let r = &v;
let w = &mut v;  // ❌

// Mutate during iteration
for item in &v { v.push(*item); }  // ❌

// Return a reference to a local
fn dangling() -> &String { &String::from("x") }  // ❌

// Assume RefCell is free
let data = RefCell::new(vec![]);
let r = data.borrow();
data.borrow_mut();  // ❌ panics at runtime

Common Pitfalls

PitfallWhy It HappensFix
Mixed borrow rejectedImmutable borrow still liveLet it end first
Iterator invalidationMutating during iterationCollect first
RefCell panicBorrow rules violated at runtimeTrack the guards
DeadlockMutex lock held too longDrop the guard
Clone overheadAliasing avoided by cloningRestructure or use references
Split borrow failsAccessing the struct as a wholeAccess fields separately

Real-World Examples

1. Many Readers

let r1 = &data;
let r2 = &data;

2. One Writer

let w = &mut data;
w.push(1);

3. Borrow Ends

let first = &v[0];
println!("{}", first);
v.push(4);

4. Clone to Avoid Conflict

let doubled: Vec<_> = v.iter().map(|n| n * 2).collect();

5. Split Borrows

let a = &mut pair.a;
let b = &mut pair.b;

6. RefCell Counter

let counter = RefCell::new(0);
*counter.borrow_mut() += 1;

7. Mutex Counter

let counter = Mutex::new(0);
*counter.lock().unwrap() += 1;

8. Arc for Sharing

let shared = Arc::clone(&data);

9. Collect Instead of Mutate

let evens: Vec<_> = v.iter().filter(|n| *n % 2 == 0).collect();

10. Atomic for Shared Counter

use std::sync::atomic::{AtomicUsize, Ordering};
let count = AtomicUsize::new(0);
count.fetch_add(1, Ordering::SeqCst);

Visual

The Rule

┌──────────────────────────────────────────────────────────────┐
│  MANY READERS OR ONE WRITER                                  │
│                                                              │
│  READING STATE:                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  &T ──┐                                                │  │
│  │  &T ──┼──▶ value                                       │  │
│  │  &T ──┘                                                │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  WRITING STATE:                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  &mut T ──▶ value                                      │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  MIXED STATE (forbidden):                                    │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  &T ──┐                                                │  │
│  │       ├──▶ value  ✗                                    │  │
│  │  &mut T ─┘                                             │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

Lifetimes

┌──────────────────────────────────────────────────────────────┐
│  let mut s = String::from("hello");                          │
│                                                              │
│  r1 ──┐                                                      │
│  r2 ──┴── read ──┐                                           │
│                  │                                           │
│  r3 ── write ────┴──▶ OK: the states do not overlap          │
│                                                              │
│  The compiler verifies the lifetimes, not the values.        │
└──────────────────────────────────────────────────────────────┘

Interior Mutability

┌──────────────────────────────────────────────────────────────┐
│  RefCell<T>                                                  │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  borrow()     → Ref (shared)                           │  │
│  │  borrow_mut() → RefMut (exclusive)                     │  │
│  │                                                        │  │
│  │  The rules are the same as the compile-time rules,     │  │
│  │  but checked at runtime.                               │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  Mutex<T>                                                    │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  lock() → MutexGuard (exclusive)                       │  │
│  │                                                        │  │
│  │  The lock provides mutual exclusion across threads.    │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

Split Borrows

┌──────────────────────────────────────────────────────────────┐
│  struct Pair { a: Vec<i32>, b: Vec<i32> }                    │
│                                                              │
│  let a = &mut pair.a;   ← borrow of field a                  │
│  let b = &mut pair.b;   ← borrow of field b                  │
│                                                              │
│  The two references are to different fields,                 │
│  so they do not alias. The compiler permits this.            │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
RuleAliasing XOR mutation
Reading stateMany &T
Writing stateOne &mut T
Mixed stateForbidden
EnforcementCompile time (borrow checker)
Runtime costNone
Interior mutabilityCell, RefCell, Mutex, RwLock
RefCell checkRuntime
Mutex checkLock
Non-lexical lifetimesBorrow ends at last use
Split borrowsDifferent fields

Key takeaways:

  • The rule is “aliasing XOR mutation.” A value may have either one mutable reference or any number of immutable references, but not both. Every other safety guarantee follows from this.
  • The compiler enforces the rule at compile time. There is no runtime cost for the safety. The borrow checker verifies the lifetimes of the references, and the code is rejected if the lifetimes create a forbidden combination.
  • Non-lexical lifetimes end a borrow at its last use. This makes the rule ergonomic without changing its meaning. A borrow is no longer considered active after the last point where it is used.
  • The rule prevents data races, iterator invalidation, and dangling references. These are all consequences of aliasing with mutation, and the rule forbids the combination.
  • Interior mutability is the controlled exception. Cell, RefCell, Mutex, and RwLock allow mutation through a shared reference, with the borrowing rule checked at runtime instead of compile time.
  • Split borrows allow independent access to different fields. Two mutable references to different fields of the same struct do not alias, so the compiler permits them.
  • The rule shapes idiomatic Rust. Short-lived borrows, cloning for independent copies, split borrows, and interior mutability are the patterns that satisfy the rule without fighting it.

Remember: The aliasing XOR mutability rule is the heart of Rust’s type system. It is a single idea — a value is either shared for reading or exclusive for writing, never both — and every other safety guarantee follows from it. The borrow checker enforces it, the lifetime system makes it expressible, and interior mutability provides the controlled exception. Understanding the rule as one idea, rather than as a collection of error messages, makes the compiler’s behavior predictable and the language’s design coherent. When the borrow checker rejects code, the reason is always the same: the code has created a state where aliasing and mutation coexist.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!