Safe vs Unsafe Boundaries & Undefined Behavior Invariants

Master the 5 superpowers of Unsafe Rust, formal invariants of Undefined Behavior (UB), and encapsulation principles for safe systems abstractions.

04. Unsafe Rust, Layout Control & Systems Internals7 min read

Safe Rust provides a mathematical guarantee: no matter how you write your safe code, it will never trigger memory corruption, use-after-free bugs, or data races.

However, real computer hardware has no concept of borrow checkers or lifetimes. Direct interaction with operating systems, hardware memory-mapped registers, CPU intrinsics, and high-performance allocators requires stepping beyond compile-time verification into Unsafe Rust.

Unsafe SuperpowersSystems Boundary
5 Operations

Unsafe Rust does not disable the borrow checker; it unlocks exactly five low-level compiler operations.

1. The 5 Superpowers of Unsafe Rust#

Writing unsafe does not turn off type checking, disable the borrow checker, or grant arbitrary lawlessness. It enables exactly five specific capabilities:

Architecture Diagram
Rendering diagram...

All other standard compiler rules (lifetime constraints, type checking, and expression validity) remain fully active inside unsafe blocks.

2. Formal Definition of Undefined Behavior (UB)#

In systems programming, Undefined Behavior (UB) is not simply a runtime exception or crash; it is the total breakdown of compiler assumptions.

Let SprogramS_{\text{program}} represent the total program execution state space. Safe Rust constrains state transitions to a verified safe subset Ssafe⊂SprogramS_{\text{safe}} \subset S_{\text{program}}. Undefined Behavior represents entering an invalid state SUBS_{\text{UB}}:

∀t∈Execution Traces,Trace(t)∩SUB=∅\forall t \in \text{Execution Traces}, \quad \text{Trace}(t) \cap S_{\text{UB}} = \emptyset

Because the LLVM optimizer assumes that Undefined Behavior is impossible, introducing UB allows the optimizer to silently delete branches, discard condition checks, reorder instructions arbitrarily, and emit corrupted machine code.

Architecture Diagram
Rendering diagram...

Fatal Actions Triggering Undefined Behavior in Rust:#

1

1. Dereferencing Invalid Pointers

Dereferencing null pointers, dangling pointers to deallocated stack/heap memory, or unaligned memory addresses.

2

2. Violating Reference Aliasing

Producing two overlapping mutable references (&mut T) to the same data, or having an immutable reference (&T) coexist with a mutable reference.

3

3. Mutating Through `&T` Without `UnsafeCell`

Mutating data behind a shared reference without wrapping it in UnsafeCell<T>.

4

4. Fabricating Invalid Primitive Representations

Constructing an invalid value representation, such as a bool containing byte value 0x03 (valid booleans must be strictly 0x00 or 0x01), or an uninitialized integer treated as initialized.

5

5. Concurrent Data Races

Reading and writing to un-synchronized shared memory across threads without atomic instructions or locks.

3. The Encapsulation Principle#

The primary tenet of systems architecture in Rust is:

> Unsafe code must be encapsulated inside safe abstractions.

The consumer of a library should never need to write unsafe to use a data structure. The internal implementation may rely on unsafe to allocate memory or manipulate raw pointers (as Vec<T>, String, and HashMap do), but its public API must guarantee complete memory safety through type system invariants:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
pub struct ByteStorage { ptr: *mut u8, len: usize, } impl ByteStorage { pub fn new(size: usize) -> Self { let layout = std::alloc::Layout::array::<u8>(size).unwrap(); // Unsafe allocation encapsulated internally: let ptr = unsafe { std::alloc::alloc(layout) }; if ptr.is_null() { std::alloc::handle_alloc_error(layout); } ByteStorage { ptr, len: size } } // Public method is completely SAFE to call! pub fn len(&self) -> usize { self.len } } impl Drop for ByteStorage { fn drop(&mut self) { let layout = std::alloc::Layout::array::<u8>(self.len).unwrap(); // Unsafe deallocation encapsulated in Drop: unsafe { std::alloc::dealloc(self.ptr, layout) }; } }

4. Concept Verification#

Test your understanding of safe vs unsafe boundaries:

Concept check

Does placing code inside an unsafe block turn off the Rust borrow checker?

Concept check

What happens if code invokes Undefined Behavior in Rust?