Engineering

Memory Safety

How programming languages and runtimes prevent entire classes of memory errors at compile time and runtime

Lead Summary

Memory safety is the property of a program that it can never read from, write to, or deallocate memory in ways that corrupt its own state or create exploitable vulnerabilities. Violations — buffer overruns, dangling pointers, use-after-free bugs, double-frees, and null pointer dereferences — account for a substantial share of critical software vulnerabilities in production systems. The challenge of memory safety sits at the intersection of language design, type theory, runtime architecture, and formal verification: different parts of the ecosystem have developed strikingly different answers to the same underlying problem, each with distinct tradeoffs between safety, performance, and expressiveness.

The field has matured considerably. Where C and C++ leave memory management entirely to programmers — with all the associated risk — languages like Rust eliminate entire classes of errors at compile time through ownership and borrowing rules. Garbage-collected runtimes like the JVM and BEAM isolate processes and enforce bounds checking at runtime. Sandboxed execution environments like WebAssembly enforce isolation through architectural constraints. And formal verification projects like RustBelt have begun to provide machine-checked proofs that these guarantees actually hold.

Core Concepts

The Fundamental Tradeoff

Manual memory management provides superior performance characteristics compared to automatic approaches like garbage collection, but introduces significant safety risks including heap corruption, premature object destruction, and undefined behavior. The performance advantage comes from the programmer's ability to optimize allocation and deallocation patterns and avoid GC overhead. However, this control requires correctly managing lifetimes — a task that is error-prone and can lead to critical runtime failures.

This tradeoff has shaped language design decisions for decades: C and C++ prioritize performance and control, while Java and Go prioritize safety and ease of use. Rust represents a third path — a systems language that offers performance comparable to C while eliminating memory safety bugs at the language level rather than at runtime.

What Memory Safety Means in Practice

The three canonical violations are:

  • Use-after-free: accessing memory that has already been deallocated. The memory region may have been reused for another purpose, so reads return garbage and writes corrupt unrelated data.
  • Double-free: calling the memory deallocation function twice on the same allocation. This corrupts allocator metadata and can lead to arbitrary code execution.
  • Buffer overrun: reading or writing past the end of an allocated region. Out-of-bounds writes can overwrite adjacent data structures, including return addresses.

Pony's type system guarantees the absence of null pointer dereferences, buffer overruns, and dangling pointers — achieved through the type system's structure rather than garbage collection or runtime checks. If a Pony program compiles, it cannot crash due to these memory errors.

Mechanism & Process

Ownership and the Borrow Checker

Rust's ownership system is a memory management paradigm that guarantees memory safety and thread safety without requiring garbage collection. The core principle is that each object has exactly one owner at any time. Memory is managed through ownership rules enforced at compile time by the type system, eliminating use-after-free and double-free at the language level rather than at runtime.

The borrow checker implements this by enforcing memory safety through compile-time constraint solving. It builds a formal proof that data is either uniquely owned (allowing unguarded mutation) or collectively shared (read-only), but never both simultaneously. The key constraints are:

  1. Any borrow must not outlive the scope of the owner
  2. Only one mutable borrow, or any number of immutable borrows, may exist at any time
  3. References are only used where their associated lifetime is valid

These constraints make memory-unsafe states unrepresentable in compiled code. The only runtime cost for memory safety under this model is array bounds checking.

Linear and Affine Type Systems

Linear and affine type systems enable memory-safe programming without garbage collection by allowing compile-time tracking of when resources are no longer needed. Linear types allow the system to safely deallocate an object after its single use is complete. Affine types relax this to allow discarding resources, enabling practical memory management without forcing explicit deallocation. This provides a "best of both worlds" between garbage-collected languages (which sacrifice control over deallocation) and manual memory management languages (which risk memory safety bugs). Linearity guarantees that resources cannot leak or be accessed after deallocation.

Rust's ownership model is best understood as an affine type system baked into the language's semantics.

Garbage Collection as a Safety Strategy

Garbage collection eliminates dangling pointers and use-after-free bugs by construction: memory is only reclaimed when no references to it remain. Different runtimes implement this at different granularities.

BEAM implements per-process garbage collection, where each process has its own heap and garbage collector. This per-process design prevents global GC pauses from affecting all processes simultaneously. When a process's heap reaches its limit, only that process's garbage collector is triggered, minimizing latency impact.

BEAM process isolation further prevents a crashing process from corrupting the state of its siblings. Each BEAM process is a separate memory space that can only affect other processes through explicit message passing, enabling fault containment at the process level.

Pony implements the ORCA (Ownership and Reference Counting-based Garbage Collection in the Actor World) protocol, a fully concurrent garbage collection algorithm that collects both objects and actors without stop-the-world pauses or synchronization barriers. ORCA leverages the data-race-freedom guarantees of Pony's reference capability type system to eliminate the need for read/write barriers.

Sandboxing and Architectural Isolation

WebAssembly approaches memory safety through architectural constraints rather than language-level rules. Each WebAssembly module executes with its own isolated linear memory space, and all memory accesses are dynamically bounds-checked at runtime. Any attempt to read or write beyond the allocated memory region causes an execution trap. This isolation is enforced through software fault isolation rather than hardware virtualization.

WebAssembly's linear memory is initialized to zero by default, preventing information leakage through uninitialized memory — a common class of vulnerability where sensitive data from previous executions could be exposed. Memory regions are isolated from the WASM runtime's internal memory, ensuring no accidental cross-contamination.

WebAssembly also enforces structured control flow where all jumps and branches are restricted to properly nested block, loop, and if/then/else constructs. This prevents arbitrary jumps and return-oriented programming (ROP) attacks that are common in native code execution.

Notably, WebAssembly's MVP achieves memory safety without garbage collection. Safety comes from sandboxing and bounds-checked linear memory, not from automatic memory management. The stack is not stored in linear memory, maintaining separation between the operand stack and linear memory, providing protection against stack-smashing attacks.

Variants & Subtypes

Spatial Safety

Spatial safety ensures that memory accesses stay within the bounds of their intended region. Java's Foreign Function & Memory (FFM) API enforces spatial safety by ensuring every memory segment maintains precise size information, with all dereference operations bounds-checked. Attempts to access memory beyond a segment's boundaries throw IndexOutOfBoundsException rather than causing silent data corruption or JVM crashes.

Temporal Safety

Temporal safety ensures that memory is only accessed while it is still valid — never after it has been freed or gone out of scope. The FFM API provides temporal safety through Arena scopes that prevent use-after-free vulnerabilities. When an Arena is closed, all memory segments allocated within it become "dead" and accessing them throws IllegalStateException, preventing the memory unsafety patterns common in traditional native code.

The FFM API offers two Arena implementations: confined arenas (Arena.ofConfined()) restrict segment access to a single owner thread and provide bounded, deterministic deallocation, while automatic arenas (Arena.ofAuto()) permit multi-threaded access and delegate deallocation to garbage collection.

Type Safety

Type safety prevents memory corruption from type confusion — accessing memory through an incompatible type interpretation. The FFM API's memory access model provides type safety by guaranteeing that memory dereference operations either succeed correctly or throw runtime exceptions, but can never result in VM crashes, silent data corruption, or memory safety violations outside the region of memory associated with a memory segment.

Null Safety

Null pointer dereferences are a special case of temporal safety — accessing a pointer that was never valid. Java addresses null safety retroactively through two mechanisms: Optional<T> (since Java 8), which wraps potentially absent values and forces explicit handling; and JSpecify annotations (@Nullable, @NonNull, @NullMarked), which use static analysis tools like NullAway to detect null-related errors at compile time. Neither provides runtime null-checking, but both make nullability intent explicit in code and enable tooling to catch errors before deployment.

Controversies & Debates

The Unsafe Escape Hatch

Rust's unsafe keyword allows programmers to bypass the borrow checker when they can guarantee memory safety through informal reasoning. But unsafe code imposes constraints: the Rust type system must maintain the invariant that only unsafe code can violate safety, while safe code assuming correctness of unsafe implementations remains sound. This creates a semantic boundary where formal verification must reason about unsafe code's correctness to validate safe APIs.

This is non-trivial in practice. 57 soundness issues have been filed against the Rust standard library, and 20 CVEs demonstrate that verifying this boundary is an active challenge. RustHornBelt and similar projects aim to formalize this boundary, enabling sound reasoning about libraries that implement safe abstractions through unsafe primitives.

Rust requires FFI calls to be wrapped in unsafe blocks that explicitly mark the boundary where the compiler cannot enforce memory safety guarantees. This linguistic convention creates auditable boundaries that identify where developer assumptions about memory safety must be manually verified and documented, making FFI safety violations explicitly visible in code review and audit processes.

Safety at FFI Boundaries

Memory safety guarantees weaken or disappear when code crosses language boundaries. Rust's lifetime constraints — which track how long references remain valid — are fundamentally lost when converting Rust references to raw C pointers across FFI boundaries. Raw pointers lack the compile-time lifetime information that Rust uses to enforce safety. Foreign code can hold raw pointers beyond the original object's lifetime, creating use-after-free vulnerabilities that Rust's type system cannot prevent.

Double-free vulnerabilities are particularly prevalent at FFI boundaries where ownership semantics differ between languages or are ambiguous. When multiple parts of code across an FFI boundary believe they own the same memory allocation and both call the deallocation function, the corruption of allocator metadata can lead to arbitrary code execution or program crashes.

In garbage-collected languages, pinning prevents use-after-free bugs at FFI boundaries by preventing the garbage collector from moving or deallocating objects while native code holds pointers to them. If pinned objects were not locked in place, the GC could relocate them in memory or reclaim them entirely, causing native code to read from or write to invalid memory.

Retrofitting Safety into Existing Codebases

Hardened standard library implementations provide measurable memory safety benefits in production at negligible performance cost. Google deployed hardened libc++ across its entire production server fleet with an average performance cost of 0.30%, uncovering more than 1,000 memory safety bugs and preventing an estimated 1,000–2,000 bugs annually. This approach contributed to a 30% reduction in baseline segmentation faults across production systems. This result suggests that hardening existing C++ standard libraries is a viable near-term strategy even without migrating to memory-safe languages.

Concurrency and Memory Safety

Memory safety interacts with concurrency in subtle ways. Binary semaphores are unsafe as a drop-in replacement for mutexes because a buggy or hostile thread can release a critical section it does not hold. Mutexes enforce ownership — preventing non-owning threads from performing unlock operations — a safety property that binary semaphores lack. POSIX formalizes this distinction: pthread_mutex_unlock from a non-owner is an error condition that can be detected and reported, whereas sem_post from any thread is valid.

Memory reclamation is a central and difficult challenge in lock-free data structures. Removed nodes cannot be immediately freed; they must not be reused or deallocated while still potentially accessible by other threads. Lock-free systems require explicit memory reclamation strategies — hazard pointers, epoch-based reclamation, or retire-at-quiescence — to safely determine when removed nodes are no longer accessible and can be reclaimed.

Mechanism & Process: Advanced — Pin and Self-Referential Types

Rust's Pin<P> wrapper addresses a subtle memory safety problem in async programming. The Pin<P> wrapper guarantees that the pointee will not be moved in memory after pinning, preventing pointer invalidation in self-referential types. When a self-referential struct is pinned, Rust's type system prevents unsafe moves that would invalidate internal pointers, enabling safe self-referential data structures.

Async state machines can be self-referential structures that contain pointers to their own data across .await points. To maintain memory safety, the generated Future types are typically !Unpin (not Unpin), meaning they cannot be moved after being pinned. The Unpin trait allows types to opt out of pinning requirements when safe, enabling ergonomic usage while maintaining memory safety invariants for types that require it. The vast majority of Rust types implement Unpin and are not affected by pinning — only futures with self-referential state require Pin semantics.

Pin is a type-system invariant, not a runtime lock

Pin does not prevent mutation or freeze memory — it only prevents moving the value to a different memory address. Types implementing Unpin can be moved freely even behind a Pin wrapper. The safety guarantee comes from the type system refusing to call mem::swap or similar operations on !Unpin types.

Notable Examples

Rust's repr Attributes and Layout Control

Memory safety interacts with memory layout when doing low-level systems programming. Rust provides explicit representation attributes (repr annotations) that allow developers to control struct memory layout at a granular level. repr(C) enforces C-compatible layout enabling FFI and hardware register access; repr(packed) eliminates padding entirely; repr(align(N)) forces specific alignment boundaries for cache-line alignment. Crucially, this design allows safe systems programming while maintaining Rust's memory safety guarantees.

Python's Biased Reference Counting

Biased reference counting is the primary technical mechanism in PEP 703 to replace the Global Interpreter Lock's function in protecting reference count modifications. The technique leverages the observation that most objects in Python programs are accessed by only a single thread, allowing efficient non-atomic reference count updates in the common case while maintaining thread-safety through atomic operations when objects are accessed from multiple threads. This represents a careful engineering tradeoff between memory safety, thread safety, and performance.

Key Figures: Formal Verification

RustBelt

RustBelt is a formal (machine-checked) safety proof for a realistic subset of Rust that verifies the memory safety guarantees of the Rust language. RustBelt provides the first formal safety proof for Rust and is extensible: for each new Rust library using unsafe code, verification conditions can be specified to ensure it is a safe extension to the language. The formal semantics uses λRust, a continuation-passing style language that formalizes both static and dynamic semantics of Rust's central features including borrowing, lifetimes, and lifetime inclusion. The proof uses the Iris separation logic framework and verifies that standard library APIs written with unsafe code do not violate safety guarantees.

RustHornBelt

RustHornBelt extends RustBelt's type-safety framework to support reasoning about functional correctness properties beyond memory safety. It is the first approach to formally verify Rust programs that soundly accounts for safely encapsulated unsafe code — code that appears safe from the API perspective but uses internal unsafe operations like raw pointer accesses and unchecked type casts. RustHornBelt's semantic foundations are validated through adequacy theorems ensuring that well-typed programs never encounter undefined behavior.

RustHornBelt establishes that Rust's extensible ownership discipline means safety properties conferred by a type depend on the concrete type — allowing library code to safely export mutable-like behavior through immutable references, a non-obvious correctness requirement.