How Safe and Unsafe Interact
What’s the relationship between Safe Rust and Unsafe Rust? How do theyinteract?
The separation between Safe Rust and Unsafe Rust is controlled with theunsafe
keyword, which acts as an interface from one to the other. This iswhy we can say Safe Rust is a safe language: all the unsafe parts are keptexclusively behind the unsafe
boundary. If you wish, you can even toss#![forbid(unsafe_code)]
into your code base to statically guarantee thatyou’re only writing Safe Rust.
The unsafe
keyword has two uses: to declare the existence of contracts thecompiler can’t check, and to declare that a programmer has checked that thesecontracts have been upheld.
You can use unsafe
to indicate the existence of unchecked contracts onfunctions and trait declarations. On functions, unsafe
means thatusers of the function must check that function’s documentation to ensurethey are using it in a way that maintains the contracts the functionrequires. On trait declarations, unsafe
means that implementors of thetrait must check the trait documentation to ensure their implementationmaintains the contracts the trait requires.
You can use unsafe
on a block to declare that all unsafe actions performedwithin are verified to uphold the contracts of those operations. For instance,the index passed to slice::get_unchecked
is in-bounds.
You can use unsafe
on a trait implementation to declare that the implementationupholds the trait’s contract. For instance, that a type implementing Send
isreally safe to move to another thread.
The standard library has a number of unsafe functions, including:
slice::get_unchecked
, which performs unchecked indexing, allowingmemory safety to be freely violated.mem::transmute
reinterprets some value as having a given type, bypassingtype safety in arbitrary ways (see conversions for details).- Every raw pointer to a sized type has an
offset
method thatinvokes Undefined Behavior if the passed offset is not “in bounds”. - All FFI (Foreign Function Interface) functions are
unsafe
to call because theother language can do arbitrary operations that the Rust compiler can’t check.
As of Rust 1.29.2 the standard library defines the following unsafe traits(there are others, but they are not stabilized yet and some of them may neverbe):
Send
is a marker trait (a trait with no API) that promises implementorsare safe to send (move) to another thread.Sync
is a marker trait that promises threads can safely share implementorsthrough a shared reference.GlobalAlloc
allows customizing the memory allocator of the whole program.
Much of the Rust standard library also uses Unsafe Rust internally. Theseimplementations have generally been rigorously manually checked, so the Safe Rustinterfaces built on top of these implementations can be assumed to be safe.
The need for all of this separation boils down a single fundamental propertyof Safe Rust:
No matter what, Safe Rust can’t cause Undefined Behavior.
The design of the safe/unsafe split means that there is an asymmetric trustrelationship between Safe and Unsafe Rust. Safe Rust inherently has totrust that any Unsafe Rust it touches has been written correctly.On the other hand, Unsafe Rust has to be very careful about trusting Safe Rust.
As an example, Rust has the PartialOrd
and Ord
traits to differentiatebetween types which can “just” be compared, and those that provide a “total”ordering (which basically means that comparison behaves reasonably).
BTreeMap
doesn’t really make sense for partially-ordered types, and so itrequires that its keys implement Ord
. However, BTreeMap
has Unsafe Rust codeinside of its implementation. Because it would be unacceptable for a sloppy Ord
implementation (which is Safe to write) to cause Undefined Behavior, the Unsafecode in BTreeMap must be written to be robust against Ord
implementations whicharen’t actually total — even though that’s the whole point of requiring Ord
.
The Unsafe Rust code just can’t trust the Safe Rust code to be written correctly.That said, BTreeMap
will still behave completely erratically if you feed invalues that don’t have a total ordering. It just won’t ever cause UndefinedBehavior.
One may wonder, if BTreeMap
cannot trust Ord
because it’s Safe, why can ittrust any Safe code? For instance BTreeMap
relies on integers and slices tobe implemented correctly. Those are safe too, right?
The difference is one of scope. When BTreeMap
relies on integers and slices,it’s relying on one very specific implementation. This is a measured risk thatcan be weighed against the benefit. In this case there’s basically zero risk;if integers and slices are broken, everyone is broken. Also, they’re maintainedby the same people who maintain BTreeMap
, so it’s easy to keep tabs on them.
On the other hand, BTreeMap
‘s key type is generic. Trusting its Ord
implementationmeans trusting every Ord
implementation in the past, present, and future.Here the risk is high: someone somewhere is going to make a mistake and mess uptheir Ord
implementation, or even just straight up lie about providing a totalordering because “it seems to work”. When that happens, BTreeMap
needs to beprepared.
The same logic applies to trusting a closure that’s passed to you to behavecorrectly.
This problem of unbounded generic trust is the problem that unsafe
traitsexist to resolve. The BTreeMap
type could theoretically require that keysimplement a new trait called UnsafeOrd
, rather than Ord
, that might looklike this:
use std::cmp::Ordering;
unsafe trait UnsafeOrd {
fn cmp(&self, other: &Self) -> Ordering;
}
Then, a type would use unsafe
to implement UnsafeOrd
, indicating thatthey’ve ensured their implementation maintains whatever contracts thetrait expects. In this situation, the Unsafe Rust in the internals ofBTreeMap
would be justified in trusting that the key type’s UnsafeOrd
implementation is correct. If it isn’t, it’s the fault of the unsafe traitimplementation, which is consistent with Rust’s safety guarantees.
The decision of whether to mark a trait unsafe
is an API design choice.Rust has traditionally avoided doing this because it makes UnsafeRust pervasive, which isn’t desirable. Send
and Sync
are marked unsafebecause thread safety is a fundamental property that unsafe code can’tpossibly hope to defend against in the way it could defend against a buggyOrd
implementation. Similarly, GlobalAllocator
is keeping accounts of allthe memory in the program and other things like Box
or Vec
build on top ofit. If it does something weird (giving the same chunk of memory to anotherrequest when it is still in use), there’s no chance to detect that and doanything about it.
The decision of whether to mark your own traits unsafe
depends on the samesort of consideration. If unsafe
code can’t reasonably expect to defendagainst a broken implementation of the trait, then marking the trait unsafe
isa reasonable choice.
As an aside, while Send
and Sync
are unsafe
traits, they are alsoautomatically implemented for types when such derivations are provably safeto do. Send
is automatically derived for all types composed only of valueswhose types also implement Send
. Sync
is automatically derived for alltypes composed only of values whose types also implement Sync
. This minimizesthe pervasive unsafety of making these two traits unsafe
. And not many peopleare going to implement memory allocators (or use them directly, for thatmatter).
This is the balance between Safe and Unsafe Rust. The separation is designed tomake using Safe Rust as ergonomic as possible, but requires extra effort andcare when writing Unsafe Rust. The rest of this book is largely a discussionof the sort of care that must be taken, and what contracts Unsafe Rust must uphold.