Skip to content
Home
Rust Unsafe Code Guide: Safety & Soundness

Rust Unsafe Code Guide: Safety & Soundness

Rust Rust 9 min read 1715 words Intermediate ExcellentWiki Editorial Team

Understanding Unsafe Rust

Unsafe Rust is not a waiver of safety — it is an explicit acknowledgment that the compiler cannot verify certain operations, and the programmer must uphold the safety invariants manually. The unsafe keyword enables five specific capabilities: dereferencing raw pointers, calling foreign functions (FFI), accessing or modifying mutable static variables, implementing unsafe traits, and accessing fields of union types. The entire Rust safety model relies on the principle that unsafety is contained: unsafe code should be wrapped in safe abstractions that enforce invariants at the API boundary. The Rust compiler trusts that the unsafe block upholds memory safety, aliasing rules, and thread safety guarantees. Violating these guarantees is undefined behavior — the program can crash, produce wrong results, or introduce security vulnerabilities (The Rustonomicon, doc.rust-lang.org/nomicon/). Understanding undefined behavior is critical: the compiler is allowed to assume that UB never occurs, meaning UB-tainted code can produce arbitrarily incorrect behavior including security exploits. The Rust standard library itself is built on carefully audited unsafe abstractions — Vec, HashMap, Box, Rc, Arc, and String all use unsafe internally while exposing safe APIs.

The Unsafe Superpower

Safe Rust enforces strict rules: all references must be valid, data races are prevented by the ownership system, and types must not be reinterpreted. Unsafe Rust relaxes these rules but does not disable the borrow checker. References inside an unsafe block are still checked for borrowing violations. Only the five specific operations listed above are unchecked. This design means that most Rust code — including the implementations of Vec, HashMap, Box, and Rc — uses unsafe internally but exposes a safe API. The standard library itself proves that unsafe code can be encapsulated safely at industrial scale. The unsafe keyword acts as a contract between the programmer and the compiler: the programmer promises to uphold invariants that the compiler cannot verify. Writing safe abstractions over unsafe code is a skill that distinguishes expert Rust programmers. The goal is always to minimize the surface area of unsafe code and to make each unsafe block independently verifiable.

Raw Pointers: *const T and *mut T

Raw pointers are the most common unsafe feature. Unlike references, they are allowed to be null, dangling, or aliased with mutation. They opt out of Rust’s reference guarantees:

fn raw_pointer_safety() {
    let mut data = [1, 2, 3, 4, 5];
    let ptr: *mut i32 = data.as_mut_ptr();
    unsafe { *ptr = 42; }
    assert_eq!(data[0], 42);
---

Pointer Arithmetic and Layout

Raw pointer arithmetic follows byte-addressed semantics (like C). The add, sub, offset, and wrapping_offset methods advance the pointer by count * size_of::<T>() bytes. The pointer::offset method has the same safety requirements as C’s pointer arithmetic: the result must stay within the allocation (or one past the end), and both the starting and ending pointers must be properly aligned. Violating these requirements is undefined behavior, even if the pointer is not dereferenced. The wrapping_offset variant does not require staying within allocation but still requires alignment. The std::ptr module provides helper functions like copy, copy_nonoverlapping, write, read, drop_in_place, and swap for safe raw pointer manipulation. The NonNull<T> type represents a non-null raw pointer and is the recommended type for pointer fields in safe abstractions, as it preserves the null-pointer optimization for Option<NonNull<T>>.

Safe Abstraction Patterns

The fundamental pattern for unsafe code is the unsafe function or block wrapped in a safe API that maintains invariants. A well-designed safe abstraction encapsulates all unsafe operations behind an API where the type system enforces correctness. For example, a custom ring buffer allocates memory using unsafe but exposes safe push and pop methods that check bounds before accessing memory. The Pin type from the standard library demonstrates how Rust’s type system can encode safety invariants for self-referential types. The standard library’s Vec is the canonical example: it uses unsafe for low-level memory operations but exposes a completely safe API that guarantees element initialization, bounds checking, and memory cleanup on drop.

Unsafe Traits: Send and Sync

The Send and Sync traits are unsafe because incorrectly implementing them can introduce data races. Most types are automatically Send and Sync, but types containing raw pointers opt out:

pub struct SharedPtr<T> { ptr: *mut T }

unsafe impl<T: Send> Send for SharedPtr<T> {}
unsafe impl<T: Sync> Sync for SharedPtr<T> {}

Mutable Statics

Accessing mutable statics is inherently unsafe. The recommended alternative is thread-local storage (std::thread_local!) or atomic types. Mutable statics should be avoided in favor of these safe alternatives in all but the most performance-critical low-level code.

Testing Unsafe Code with Miri

Miri detects undefined behavior by interpreting Rust’s MIR. Install with rustup +nightly component add miri and run cargo +nightly miri test. Combine Miri with loom for concurrency model checking and sanitizers (ASan, TSan) for comprehensive validation. Miri catches violations of pointer provenance rules, use-after-free, and incorrect Uninit access. For rigorous verification, the proptest crate combined with Miri can systematically explore edge cases in unsafe code.

Unsafe Code Guidelines in Practice

The Rust project maintains a set of unsafe code guidelines that define what constitutes undefined behavior. These include: references must always be aligned, non-null, and point to valid memory for the duration of their lifetime; mutable references must have exclusive access (no aliasing); and the provenance of a pointer must remain valid for all accesses through it. The Stacked Borrows model formalizes these rules by tracking the access history of each memory location. Violating Stacked Borrows — for example, creating a mutable reference to a location that also has an active shared reference — is undefined behavior even if the two pointers are never used simultaneously. The miri tool detects Stacked Borrows violations, making it the most important tool for validating unsafe code. The planned Tree Borrows model extends Stacked Borrows to handle more real-world patterns. Production unsafe code should be specifically tested under Miri and with address sanitizers enabled. The safety documentation convention requires that every unsafe function has a # Safety section documenting the caller’s obligations.

Interior Mutability Patterns

Interior mutability allows mutation through shared references, enabled by UnsafeCell under the hood. The standard library provides safe wrappers: Cell<T> for Copy types with no thread safety overhead, RefCell<T> for runtime borrow checking in single-threaded contexts, Mutex<T> for thread-safe mutation, and RwLock<T> for read-optimized concurrent access. Custom interior mutability types can be built on UnsafeCell when the standard wrappers don’t fit. The key safety invariant for interior mutability is that no mutable reference can exist while any shared reference is being used, enforced at different times depending on the wrapper. UnsafeCell disables the compiler’s aliasing analysis for the wrapped type, placing the burden of correctness on the implementer. Common patterns include: concurrent data structures that use atomic operations for lock-free access; lazy initialization with OnceCell or OnceLock; and memory-mapped I/O where hardware registers are accessed through volatile operations on UnsafeCell. The unsafe implementation must ensure that the aliasing guarantees not enforced by the compiler are maintained at runtime through appropriate synchronization.

Unsafe Trait Implementation Patterns

Implementing unsafe traits requires proving that implementors satisfy invariants the trait assumes. The Send and Sync traits are the most commonly implemented unsafe traits: unsafe impl Send for MyType {} asserts the type can be transferred across threads safely. The unsafe trait pattern is justified when the trait has safety invariants that cannot be expressed in the type system. The Pin type’s invariants prevent moves after pinning, and unsafe code must ensure pinned values are never moved or dropped incorrectly. The Box::into_raw and Box::from_raw pair enables manual memory management of heap-allocated values, but requires that the allocator’s layout matches exactly.

Raw pointer arithmetic follows the same rules as C pointer arithmetic but with stricter provenance requirements in Rust. The std::ptr::addr_of! and addr_of_mut! macros create pointers to packed struct fields or UnsafeCell contents without creating intermediate references. The NonNull<T> type represents a non-null raw pointer with niche optimization benefits. The MaybeUninit<T> type handles uninitialized memory without triggering undefined behavior, replacing the deprecated mem::uninitialized(). Unions provide C-style type punning but accessing the wrong variant is undefined behavior; use MaybeUninit and safe abstractions instead. The #[repr(C)] attribute for layout compatibility with C should also specify alignment with #[repr(C, align(N))] when matching external struct layouts.

Frequently Asked Questions

Does unsafe disable the borrow checker? No — references inside an unsafe block are still subject to borrowing rules.

Is unsafe code always dangerous? Unsafe code requires care but is not inherently dangerous when invariants are well-understood and properly encapsulated.

What is the most common unsafe mistake? Creating dangling pointers when the underlying memory is freed, and violating pointer aliasing rules.

How do I know if my unsafe code has UB? Run Miri (cargo +nightly miri test) and address sanitizers.

Can I write a performant RingBuffer in safe Rust? Yes, using std::collections::VecDeque. But custom unsafe implementations can be faster for specific use cases.

Related: Rust FFI Guide | Rust Serialization with Serde | Rust Macros Guide

Rust in Production: Case Studies

Rust has been adopted in production by major technology companies for performance-critical and safety-critical systems. Mozilla uses Rust in the Firefox browser engine (Servo/Quantum) for improved memory safety and performance in CSS layout and rendering. Dropbox rewrote their sync engine core in Rust, achieving significant performance improvements and eliminating entire classes of bugs. Cloudflare uses Rust for edge computing infrastructure, including their Pingora HTTP proxy that handles 10% of the world’s internet traffic. Figma rewrote their multiplayer editing engine in Rust, achieving predictable performance and eliminating crashes. Discord used Rust for their read states service, handling millions of concurrent connections with minimal resources. These case studies demonstrate Rust’s value in systems where performance, reliability, and memory safety are critical. The common pattern: rewrite performance-critical components in Rust while maintaining the rest of the system in the original language, proving that incremental adoption works in practice.

Embedded Systems Programming

Rust is gaining traction in embedded systems for its memory safety guarantees without a garbage collector or runtime. The cortex-m-quickstart template provides a starting point for ARM Cortex-M microcontrollers. The RTIC framework provides real-time interrupt-driven concurrency. Embassy offers an async embedded runtime with USB, networking, and BLE support. Rust’s type system prevents common embedded errors like misconfiguring peripherals, incorrect register access, and data races between interrupt handlers and main code. The embedded ecosystem continues to mature with HAL implementations for major microcontroller families from STM, Nordic, and Espressif.

Section: Rust 1715 words 9 min read Intermediate 756 articles in section Report inaccuracy Back to top