Skip to content
← All Rust articles

Rust explainer

Aliasing, mutable references and unsafe code

About 5 min to read. Allow extra time to work through examples.

Understand reference guarantees, distinguish reborrowing from conflicting access, and see what UnsafeCell changes and what it does not.

This article uses Rust. You can make it your practice mode to browse related content across the site.

Safe Rust checks how references are borrowed. Unsafe code can create references through raw pointers, so its author must uphold the guarantees that safe callers and the compiler rely on.

An aliasing bug need not involve an out-of-bounds access or a null pointer. Two individually plausible accesses can conflict because of the references through which they were made.

Rust's guarantees and compiler optimizations

A mutable reference grants exclusive access for its borrow. A shared reference normally prevents mutation of the referenced data, except for portions inside UnsafeCell. These guarantees enable optimizations such as removing redundant loads when no permitted intervening access can change the value.

rustc can communicate aliasing information to LLVM using attributes such as noalias and readonly. Their presence depends on the type and code generation context. They are not a complete definition of Rust's rules, and they do not mean a value can be cached across every arbitrary call. A call may legitimately receive a reborrow and modify the value.

Unsafe code must satisfy Rust's requirements even in an unoptimized build. Correct-looking output does not establish that invalid code is sound.

Reborrowing is valid

It is too broad to say that two reference bindings in one scope are always a violation. A mutable reference can be reborrowed temporarily:

let mut value = 1;
let outer = &mut value;
{
    let inner = &mut *outer;
    *inner += 1;
}
*outer += 1;
assert_eq!(value, 3);

Access through outer resumes after the inner borrow ends. The bindings' textual scopes alone do not decide which accesses are permitted. Conversely, not writing through a reference is not sufficient to make it valid: creating a reference imposes requirements too.

The precise unsafe aliasing model is still being developed. Use the current Reference and API safety contracts rather than turning one compiler model into an all-purpose rule about how many pointer variables may exist.

Raw pointers do not erase an existing borrow

Raw pointers can be copied without creating additional references. That does not grant unrestricted access to their target. Allocation lifetime, alignment, initialization, provenance and outstanding borrows still matter when using them.

Here one derived pointer is used while the original reference is not:

let mut value = 10;
let reference = &mut value;
let pointer: *mut i32 = reference;
// SAFETY: pointer targets the live, initialized i32. This access does not
// overlap any access through reference or another pointer.
unsafe { *pointer += 2; }
assert_eq!(*reference, 12);

Converting a shared-reference-derived *const T into *mut T changes the pointer type. The cast alone does not authorize mutation. Writing through it or manufacturing an incompatible mutable reference can violate the original shared borrow. Use references only where their full requirements hold; keeping an address in raw-pointer form is not a safety proof.

What UnsafeCell changes

UnsafeCell<T> permits interior mutation of its contents through a shared reference to the cell. It does not permit conflicting mutable borrows, dangling references or data races. get() supplies a raw pointer whose use still requires a safety argument.

Prefer a safe abstraction suited to the task. For example, Cell handles replacement of small values without exposing a borrowed reference to them:

use std::cell::Cell;

let value = Cell::new(3);
let first = &value;
let second = &value;
first.set(7);
assert_eq!(second.get(), 7);

RefCell checks borrowing at runtime. Locks and atomics address particular cross-thread access patterns. UnsafeCell alone supplies neither runtime borrow checking nor synchronization. See The UnsafeCell exercise for a closer look at that primitive.

Check your understanding A diagnostic suggests UnsafeCell after an attempted shared-to-mutable reference conversion. Is wrapping the data enough? Show answer

No. The cell changes the shared-reference immutability guarantee for its contents. The surrounding API must still enforce valid access and lifetimes. Returning unrestricted mutable references on every shared access would not supply that enforcement. Often the better repair is to request exclusive access in the function signature or use an existing safe cell or lock.

Diagnostics help, but do not prove soundness

mutable_transmutes rejects transmuting an ordinary shared reference into a mutable reference. invalid_reference_casting catches some attempts to mutate shared-reference data through casts. Their absence does not establish that every unsafe access is legal.

Clippy's mut_from_ref flags APIs that produce a mutable borrow from shared input. Examine the lifetime relationship and access protocol rather than assuming every superficially similar signature has identical behavior. transmute_ptr_to_ref suggests a direct raw-pointer dereference when forming a reference; changing the syntax does not validate the pointer or its lifetime. A safety comment should explain those obligations explicitly.

Miri can detect many invalid accesses on executed paths under its supported models. A passing run is useful evidence, not proof over all inputs, schedules or foreign code. See Stacked and Tree Borrows for the model distinctions.

Valid examples can be tested without invoking undefined behavior. For a suspected invalid example, do not infer legality from whether debug and optimized binaries happen to agree. Identify the violated contract and use diagnostics or an appropriate interpreter to investigate it.

step 13 of 24

Unsafe and Soundness

Up next Stacked Borrows, Tree Borrows and Miri