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.
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 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:
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 represent the total program execution state space. Safe Rust constrains state transitions to a verified safe subset . Undefined Behavior represents entering an invalid state :
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.
Fatal Actions Triggering Undefined Behavior in Rust:#
1. Dereferencing Invalid Pointers
Dereferencing null pointers, dangling pointers to deallocated stack/heap memory, or unaligned memory addresses.
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. Mutating Through `&T` Without `UnsafeCell`
Mutating data behind a shared reference without wrapping it in UnsafeCell<T>.
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. 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:
1234567891011121314151617181920212223242526272829pub 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:
Does placing code inside an unsafe block turn off the Rust borrow checker?
What happens if code invokes Undefined Behavior in Rust?