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 Hide 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.