We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Unsafe and Soundness step 15 of 24
UnsafeCell: the only legal path to interior mutability
pub fn memo_fib(queries: Vec<u64>) -> Vec<u64>
Answer a list of Fibonacci queries using a memo table (a cache of answers
already computed). The Fibonacci numbers are fib(0) == 0, fib(1) == 1,
and fib(n) == fib(n - 1) + fib(n - 2) for n >= 2.
The contract:
- Every query is between 0 and 90 inclusive.
fib(90)is 2,880,067,194,370,816,120, which fits in au64, so no answer overflows. - Queries may repeat and may arrive in any order. The list may be empty, in which case the answer is an empty list.
- The output has one answer per query, in the same order as the queries.
For example,
[10, 3, 10]gives[55, 2, 55].
The point of the exercise is how the cache is stored. It lives in
struct Memo { cache: UnsafeCell<Vec<u64>> }, and the lookup method
fib(&self, n: usize) -> u64 takes &self (a shared reference), not
&mut self, yet it must add entries to the cache. The starter contains a
helper method that Clippy (Rust's standard linter, run as cargo clippy)
rejects; working out why is the lesson.
What UnsafeCell changes
A shared reference &T normally promises that the value it points to will
not change while the reference is in use. The compiler is allowed to
optimise on that promise, for example by reusing a value it already loaded
instead of reading memory again. Changing data behind a plain &T is
therefore undefined behaviour (UB): the program has broken a rule the
compiler relies on, and Rust gives no guarantee about what it then does.
std::cell::UnsafeCell<T> is the type the language treats specially to lift
that promise. Data inside an UnsafeCell may be changed while shared
references to the cell exist. This is called interior mutability. The
standard library's other interior-mutability types, such as Cell,
RefCell, OnceCell, Mutex, RwLock and the atomic integer types, all
store their data in an UnsafeCell and add their own rules (moving values
in and out instead of lending a shared caller a reference to the contents,
run-time borrow counting, locking, or atomic instructions) to make the
mutation safe to use.
Three rules matter here:
- It relaxes only the "shared means unchanged" promise. The promise
that a
&mut Tis the only live reference to its target still holds. Two live&mutreferences to the same place are UB, inside anUnsafeCellor not. - To mutate the contents, get a raw pointer from the cell.
get(&self)returns*mut T;raw_getdoes the same starting from a*const UnsafeCell<T>; and if you hold&mut UnsafeCell<T>,get_mutgives a&mut Tsafely. Do not cast a reference to a raw pointer and write through it (see the question below). - It does nothing about threads.
UnsafeCell<T>does not implementSync, so a type that contains one is notSyncunless its author writes anunsafe impl Syncand enforces a synchronisation rule (a lock or atomics) themselves. That is whyCellandRefCellcannot be shared between threads.
Check your understanding What does rustc do if you cast a shared reference to a mutable Show answer Hide answer
raw pointer, as in *(r as *const i32 as *mut i32) = 6?
It refuses to compile the write:
error: assigning to `&T` is undefined behavior, consider using an `UnsafeCell`
That is the invalid_reference_casting lint, which is deny-by-default, so
it is an error unless you explicitly allow it. With Rust 1.98.1 it fires
for a plain &i32 and also when the reference is to an UnsafeCell<i32>
and the cast goes through *const i32. Writing through a pointer made from
a reference to data outside an UnsafeCell is UB, because the promise
attached to the &T already applies. The diagnostic names the intended
tool: put the data in an UnsafeCell and mutate it through get().
Why the starter's cache_mut helper is rejected
The starter contains this helper:
fn cache_mut(&self) -> &mut Vec<u64> {
unsafe { &mut *self.cache.get() }
}
Clippy's mut_from_ref lint is in its correctness group, which is
deny-by-default, and it rejects the signature:
error: mutable borrow from immutable input(s)
The problem is what the signature promises. Any number of &Memo references
can exist at once, and this safe function turns each of them into a
&mut Vec<u64>. Calling it twice and keeping both results gives two live
&mut references to the same vector, which is UB, and neither call site
contains the word unsafe. A safe function that lets safe callers cause UB
is called unsound: the bug is in the API itself, however carefully it is
called.
The fix is to keep the raw pointer and create the &mut only inside the
method that uses it:
fn fib(&self, n: usize) -> u64 {
// SAFETY: explain why no other reference to the cache is live here.
let cache = unsafe { &mut *self.cache.get() };
// ... extend and read `cache` ...
}
Here the &mut is a local variable. It cannot outlive the method body and is
never returned, so the safety argument only has to cover this one function:
nothing inside it calls fib again or creates another reference to the
cache while cache is in use.
Check your understanding
self.cache.get() returns *mut Vec<u64> without an unsafe
Show answer
Hide answer
block. Why? Creating a raw pointer is safe; dereferencing it is the unsafe operation. See raw pointers for that distinction.
This is what makes "keep it raw, create the reference late" a workable
pattern. Raw pointers do not carry the guarantees that & and &mut
carry, so the rules you have to argue about apply mainly at the moments you
turn a pointer into a reference and use it. Stacked Borrows and Tree
Borrows, two proposed models of Rust's aliasing rules that the Miri
interpreter can check, describe this precisely; Rust has not yet adopted
either as its official rules. See
Stacked and Tree Borrows.
A practical rule for shared-mutable data structures: store raw pointers or
UnsafeCells in the fields, create a reference for as short a span as
possible, and do not return a &mut from a method that only took &self.
Writing the solution
- Start the cache as
UnsafeCell::new(vec![0, 1]), which holdsfib(0)andfib(1). - For a query
n, push new values (the sum of the last two entries) whilecache.len() <= n, then returncache[n]. - Use one
Memofor the whole query list.
A repeated query, or one smaller than an earlier query, is answered from the
table without new work, because filling up to n also fills every index
below n.
Within the contract no addition overflows. The official solution uses
wrapping_add only so that debug and release builds behave the same: in a
debug build + panics on overflow, while a release build (with default
settings) wraps silently. Either choice passes the tests.
The starter and solution also include impl Default for Memo. It is not
required here. Clippy's new_without_default lint asks for it when a
publicly reachable type has a public new() that takes no arguments, and
this Memo is private, so the lint does not fire without it.
A note on threads
Because UnsafeCell<Vec<u64>> is not Sync, Memo is not Sync, so a
&Memo cannot be sent to another thread. The compiler enforces this
automatically. This design is sound with one thread; if two threads could
call fib on the same Memo at once, they could write to the cache at the
same time, which is a data race.
The standard library has SyncUnsafeCell, an UnsafeCell that is Sync
when T: Sync, for code that provides its own synchronisation. It is still
unstable in Rust 1.98.1 (using it on stable is error E0658). On stable you
write a newtype around UnsafeCell<T> with an unsafe impl Sync, choose
the bound on T that your synchronisation actually justifies (a lock that
gives one thread at a time exclusive access, like Mutex, needs
T: Send), and document that reasoning in a # Safety section.
What the tests cannot check
The grade is: the code compiles, the tests pass, and
cargo clippy -- -D warnings reports nothing. If your safety comment is
wrong and there is a path on which two &mut references into the cache are
live at once, the tests can still pass, because UB does not reliably produce
a wrong answer. Clippy's mut_from_ref catches the one helper shape above;
other aliasing mistakes need a careful argument, or a run under Miri, which
this grader does not do.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.