Rust: Unsafe Rust

Keywords

rust, unsafe, raw pointers, ffi, undefined behavior, safe abstraction, invariants, miri, transmute, soundness

Introduction

A team was porting a battle-tested C compression library into a Rust service. The library was decades old, fast, and correct; rewriting it was out of the question — they just needed to call it. But there was no way to express that call in safe Rust. The C function took a pointer to a buffer and a length, wrote into the buffer, and returned a status code. Safe Rust has no vocabulary for “here is a raw address, trust me, write n bytes into it.” The borrow checker has nothing to check; the type system has no idea what decompress(ptr, len) does to the memory behind ptr. The only door into that C function was marked unsafe, and an engineer who had spent two years learning that Rust never lets you shoot yourself in the foot read that keyword as “this is the part where the safety turns off.” So they wrapped the call in unsafe, passed a buffer that was sometimes a few bytes too short, and shipped it. It worked in every test, and for months in production. Then a payload hit the exact size that made the C function write one byte past the end of the buffer, corrupting an adjacent allocation — and the service began returning wrong answers from a completely unrelated code path, the classic signature of memory corruption, the exact bug Rust was supposed to make impossible.

The bug was not in the C library. It was in the belief that unsafe means “safety off.” It does not. unsafe is not a switch that turns Rust into C; it is a small, explicit, auditable escape hatch that unlocks a handful of operations the compiler cannot verify — and in exchange, you take on the obligation to uphold the invariants the compiler normally proves for you. The team’s mistake was not using unsafe; it was using it without writing down, and then upholding, the one invariant that mattered: the buffer must be at least as large as the C function will write. Misunderstanding this single point is how unsound libraries ship — code that looks safe from the outside but has a hole a well-behaved caller can fall through. This chapter is about how to use the escape hatch without falling through.

The Core Insight

The first thing to internalize, and the thing the introduction’s team got wrong, is that unsafe does not disable the borrow checker, switch off the type system, or make Rust behave like C. Inside an unsafe block, lifetimes are still tracked, moves still move, &mut is still exclusive, and the compiler still rejects a type error. unsafe unlocks exactly five extra powers and nothing else:

  1. Dereference a raw pointer — read or write the memory a *const T or *mut T points at, which safe Rust forbids because it cannot prove the pointer is valid.
  2. Call an unsafe function — including every function imported from C through FFI, because the compiler cannot see what the callee does.
  3. Access or modify a mutable static — global mutable state, which is a data race waiting to happen unless you synchronize it yourself.
  4. Implement an unsafe trait — most importantly Send and Sync, where you are asserting a thread-safety property the compiler cannot derive.
  5. Access a union’s fields — reading a field of an untagged union, where the compiler cannot know which variant is actually live.

That is the whole list. unsafe shifts the burden of proof for these specific operations from the compiler to you; it does not grant a license to ignore everything else. And the discipline that makes this workable — the discipline the standard library itself is built on — is to wrap a small unsafe core inside a safe abstraction that upholds the invariants, so that callers never touch unsafe at all. Vec, Box, Arc, Mutex, String — every one of them is a few hundred lines of carefully audited unsafe code behind a public API that is impossible to misuse into undefined behavior. The phrase to hold onto is a tiny island of unsafe in a sea of safe.

A mental model

Think of an unsafe block as a sealed room with sharp tools. Outside the room, the compiler is a vigilant safety inspector who refuses to let anything dangerous happen. Inside, the inspector stands at the door and stops checking — not because the tools became safe, but because the operations in there are ones it has no way to verify. The tools are still sharp: a misused raw pointer still corrupts memory, an aliasing &mut still triggers undefined behavior. Your job is to keep the room small (less code the inspector can’t see, less to get right by hand), to document the invariants that make the work inside correct (so the next person — including you in six months — can audit it), and to expose only a safe door: a public API anyone can use, in any order, with any inputs, without ever being able to cause harm.

That safe door is the formal goal, and it has a name: soundness. An API is sound when no safe caller can trigger undefined behavior, no matter what they do — not “no reasonable caller,” but no caller, including a hostile one passing the worst inputs they can construct. The introduction’s wrapper was unsound: it exposed a safe-looking function, yet a caller who passed a particular buffer size could drive the unsafe core into corrupting memory. The hole was inside the room, but a perfectly well-behaved caller, standing entirely in the safe world, could reach in and trigger it. Soundness is the property that this can never happen.

When unsafe is justified (and the bar)

Most Rust code should contain zero unsafe. The optimizer is good enough that “I’ll use a raw pointer for speed” is almost always a premature optimization that buys nothing, and “the borrow checker won’t let me” is almost always a sign your data structure needs rethinking, not an exemption from the rules. There are exactly three situations where unsafe is genuinely justified.

Foreign function interfaces. Calling a C or C++ library is inherently unsafe — the other language makes no memory guarantees Rust can rely on — and it is not optional: there is no safe way to express extern "C".

Performance-critical primitives, after you have measured. A hot inner loop where profiling proves the bounds check is the bottleneck, or a SIMD kernel that needs CPU intrinsics, can legitimately drop into unsafe. Order matters: benchmark the safe version first, because LLVM elides most bounds checks on its own and the unsafe version is frequently no faster.

Data structures safe Rust cannot express. A doubly-linked list, an intrusive collection, a custom allocator, a lock-free queue — structures whose aliasing pattern the borrow checker cannot prove correct even though it is. These are real but rare, and a safe crate (crossbeam, petgraph, an arena) usually exists already.

In every case the bar is the same, and it is high: the unsafe must live behind a safe API with documented invariants, validated by Miri. Figure 27.1 shows the shape — a small unsafe core, sealed behind a safe public door, with an FFI call crossing out to C. If you cannot state the invariant in a comment and defend why no safe caller can violate it, you are not ready to write the unsafe block.

What you’ll learn

  • The five specific powers unsafe unlocks — and, just as important, everything it does not turn off
  • How raw pointers differ from references, and the narrow set of jobs that genuinely require them
  • The safe-abstraction pattern: how to wrap a small unsafe core behind a public API that upholds its invariants, the way Vec and Box are built
  • How to call C through FFI, what extern and #[repr(C)] are for, and how to track memory ownership across the language boundary
  • What undefined behavior actually is, why a sound API can never be misused into it, and why transmute is the sharpest tool in the room
  • How to use Miri to catch undefined behavior that ordinary tests sail straight past

Prerequisites

  • Rust: Ownership & Borrowing — references, lifetimes, moves, and the exclusivity of &mut. Unsafe Rust is where you manually uphold exactly what the borrow checker usually proves, so you need to know what it proves.
  • Rust: Fundamentals — structs, traits, generics, Drop, and the standard collection types you will be reimplementing in miniature.
  • Comfort with the memory basics: stack versus heap, what a pointer is, alignment, and why a dangling pointer is dangerous. A little C is helpful for the FFI sections but not required.

What unsafe actually unlocks

It is worth dwelling on the negative space, because almost every unsafe-Rust disaster starts with a wrong belief about what the keyword does. Inside an unsafe block the borrow checker is still running: write two overlapping &mut references and the compiler still rejects them, and even when you launder them through raw pointers to slip past it, the aliasing model the borrow checker enforces remains in force — violating it is undefined behavior whether or not anything complained at compile time. The type system is still enforced too. What changes is narrow: the five powers become available, and for each, the compiler stops guaranteeing correctness and hands that responsibility to you.

The practical consequence is that an unsafe block is a claim — you are asserting to every future reader, “I have checked the invariants that make these operations valid, and here is why they hold.” That is why the single most important habit in unsafe Rust is the // SAFETY: comment above every block, stating precisely which invariant you rely on and why it is satisfied. The introduction’s team had no such comment, because they could not have written an honest one: they did not know the invariant they were violating. If no // SAFETY: comment comes to mind, that is not a documentation gap — it is a signal that you do not yet understand why the code is correct, and you should stop.

Raw pointers

The first power, and the foundation of most of the others, is dereferencing raw pointers. Rust has two: *const T (read-only) and *mut T (read-write). They look like references but they have given up every guarantee a reference carries. A &T is always valid, always aligned, never null, and borrow-checked; a *const T might be dangling, might be misaligned, might be null, and is tracked by nobody. That is exactly why the compiler refuses to let you read through one without unsafe: it has no way to know whether the read is sound.

Crucially, creating a raw pointer is safe — it is just an address, an inert number. Only dereferencing it crosses into unsafe, because that is the moment the address is actually trusted. The canonical example is splitting a mutable slice into two non-overlapping halves: the borrow checker cannot prove this safe on its own, because it cannot reason that the halves don’t overlap — but you can, with an assertion the unsafe core then relies on.

/// Split `slice` into two non-overlapping mutable halves at `mid`.
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = slice.len();
    let ptr = slice.as_mut_ptr();
    assert!(mid <= len); // the invariant the unsafe block below relies on

    // SAFETY: `mid <= len`, so the two ranges `[0, mid)` and `[mid, len)`
    // are disjoint; the resulting `&mut` slices never alias the same element.
    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

The assert! is not decoration. It is the precondition that makes the // SAFETY: claim true, and it converts a potential out-of-bounds disaster into a clean panic. Notice the shape already forming: a check in safe code, a minimal unsafe block, and a comment connecting the two. That shape is the whole game.

The safe-abstraction pattern

This is the centerpiece of the chapter, because it is how essentially all correct unsafe Rust is organized — the standard library included. The pattern is three nested layers, shown in Figure 27.1: on the outside a safe public API any caller can use freely; just inside it a layer of invariant checks in ordinary safe code (bounds, null, alignment, capacity); and at the core the smallest possible unsafe block, which runs only once those checks have guaranteed its preconditions, with a // SAFETY: comment naming the invariant it depends on. The checks make the unsafe sound; the unsafe is what the checks protect.

Vec is the textbook case, and reimplementing a sliver of it makes the pattern concrete. A vector is, internally, a raw pointer to a heap allocation, a length, and a capacity. Pushing requires writing into raw memory; growing requires calling the allocator directly; dropping requires freeing the allocation by hand. Every one of those is unsafe — and yet Vec’s public API is perfectly safe, because each unsafe operation is fenced behind a check that guarantees its precondition.

pub struct MyVec<T> {
    ptr: *mut T,       // raw pointer to the heap allocation (may be dangling when cap == 0)
    len: usize,        // number of initialized elements; INVARIANT: len <= capacity
    capacity: usize,   // number of T slots the allocation can hold
}

impl<T> MyVec<T> {
    /// Append a value. Safe public API: any caller, any value, always sound.
    pub fn push(&mut self, value: T) {
        if self.len == self.capacity {
            self.grow(); // re-establishes the invariant: now len < capacity
        }
        // SAFETY: `grow` guarantees `len < capacity`, so `ptr.add(len)` points to
        // an allocated, uninitialized slot we own exclusively (&mut self).
        unsafe {
            std::ptr::write(self.ptr.add(self.len), value);
        }
        self.len += 1;
    }
}

The safe caller writes v.push(x) and can never cause undefined behavior, however many times they call it — because push checks capacity before it dereferences the pointer, and grow re-establishes the invariant the // SAFETY: comment depends on. The unsafe is a single line, its precondition is enforced one line above it, and the invariant tying them together is written down. That is what “upholding the invariants the compiler normally checks” looks like: the compiler can’t prove len < capacity at the write, so you prove it yourself with a branch, and you document the proof.

The same care extends to cleanup. Because MyVec owns a raw allocation, it must implement Drop to free it — itself an unsafe operation gated by an invariant: only deallocate if something was actually allocated, using the exact layout that was allocated. Forget the Drop and you leak; get the layout wrong and you corrupt the allocator. The lesson generalizes past Vec: any type holding a raw resource must pair acquisition with release, and the safe abstraction owns both ends.

FFI: calling across the boundary

The second power — calling unsafe functions — is most often about FFI, the foreign function interface, the one place unsafe is non-negotiable. When you import a C function, the compiler sees its signature but not its behavior: it has no idea whether decompress(ptr, len) reads len bytes or len + 1, whether it frees the pointer, or whether it spawns a thread that touches the buffer later. Every C function is therefore declared and called as unsafe, and the burden of matching the C contract — buffer sizes, null-termination, ownership, struct layout — falls entirely on you.

Declaring the boundary is mechanical. An extern "C" block imports C functions, and #[repr(C)] forces a Rust struct to use C’s field layout instead of Rust’s (Rust is otherwise free to reorder fields, which would scramble anything C reads). The hard part is never the syntax; it is the contract.

use std::os::raw::c_int;

// The C side, from a header: `int decompress(unsigned char *dst, size_t cap,
//                                            const unsigned char *src, size_t len);`
extern "C" {
    fn decompress(dst: *mut u8, cap: usize, src: *const u8, len: usize) -> c_int;
}

Calling this raw is exactly where the introduction’s team came undone. The safe abstraction is to wrap it in a function whose Rust types enforce the C contract — pass slices, not raw pointer/length pairs, so the length always matches the buffer, and the caller cannot get them out of sync.

/// Decompress `src` into a caller-provided buffer. Returns bytes written, or an error.
pub fn decompress_into(dst: &mut [u8], src: &[u8]) -> Result<usize, i32> {
    // SAFETY: `dst`/`src` are valid slices, so their pointers are valid for
    // `len`/`cap` bytes respectively and correctly aligned for u8; the C function
    // writes at most `cap` bytes (its documented contract), so it cannot overrun
    // `dst`. We pass each slice's true length, so the size can never be wrong.
    let written = unsafe {
        decompress(dst.as_mut_ptr(), dst.len(), src.as_ptr(), src.len())
    };
    if written < 0 { Err(written) } else { Ok(written as usize) }
}

By taking &mut [u8] instead of a pointer and a length, the safe wrapper makes the buffer-size invariant impossible to violate from outside — the length always describes the buffer it came from, so the introduction’s unsoundness is now unrepresentable. The other half of FFI discipline is ownership: whoever allocates must free, across the boundary. Memory C mallocs must return to C’s free; memory Rust hands to C via Box::into_raw must come back through Box::from_raw. Mixing the allocators corrupts the heap. The robust pattern is a Rust wrapper whose Drop calls the matching C free function, so the ownership contract is enforced by the type system, not by everyone remembering.

Undefined behavior and soundness

To wield unsafe you have to know precisely what you are avoiding, and the answer is undefined behavior — UB. UB is not “a crash” or “a wrong answer.” It is the compiler being permitted to assume the situation never happens, and optimizing on that assumption. When you dereference a dangling pointer, read uninitialized memory, or create two aliasing &mut references, you may not have done anything visible yet — you have told the optimizer a lie, and it will rearrange your program around that lie in ways that can corrupt unrelated code, work perfectly until a compiler upgrade, then fail bizarrely. This is why memory-corruption bugs surface far from their cause: the symptom and the sin are connected only through the optimizer’s assumptions.

This reframes soundness with full force. A sound API is one where no safe caller can provoke UB by any sequence of safe operations whatsoever — and the corollary is load-bearing: if your abstraction is sound, UB is simply not reachable from safe code, and your callers (the 99% of the codebase that never writes unsafe) are genuinely safe, just as if unsafe did not exist. Unsoundness is the opposite: a hole in the safe surface through which a well-behaved caller can fall into UB. The introduction’s wrapper was unsound; a split_at_mut that dropped its assert! would be unsound; a Send implementation on a type that isn’t actually thread-safe is unsound. Auditing unsafe code is hunting for unsoundness: for every public function you ask, “can any caller, with any input, reach the UB inside?” — and if the answer is anything but a confident no, there is a bug.

The sharpest tool in the room is std::mem::transmute, which reinterprets the bits of one type as another with zero checks — it will happily turn a u32 into an f32, a &T into a &mut T, or a valid value into an invalid one, and the compiler will not blink. Transmuting types of different sizes is instant UB; transmuting & to &mut violates aliasing; transmuting an integer to a bool that isn’t 0 or 1 produces an invalid value the optimizer may assume cannot exist. Almost every transmute has a safe alternative (f32::from_bits, bytemuck, zerocopy) that does the same reinterpretation with the checks left on, and reaching for those instead is one of the highest-value habits in the language.

War story: the transmute that worked until the optimizer woke up

A networking crate needed to hand the same buffer to two helpers — one reading, one writing — and the borrow checker, correctly, refused to allow a &mut and a & to the same memory at once. Rather than restructure, an engineer reached for transmute to forge a second mutable reference from a shared one, reasoning that “the two helpers never actually touch the same bytes, so it’s fine.” It compiled, passed the test suite, and ran in production for the better part of a year. Then a routine toolchain upgrade brought a smarter optimizer, which saw a &mut and — trusting Rust’s guarantee that a &mut is exclusive — cached a value it had every right to assume nothing else could change. The forged shared reference changed it anyway. The function began returning stale data intermittently, only under load, only on the new compiler, with no crash to point at. The root cause was a transmute that violated the aliasing model: the code never observed the violation until an optimizer that trusted the model finally exploited it. The fix was not a better transmute but deleting it and restructuring the helpers to take disjoint slices, the way split_at_mut does. The lesson: unsafe does not suspend the aliasing rules, it makes you responsible for them, and UB that “works” is a debt the optimizer collects on its own schedule. Run under Miri, this would have been flagged the first day.

Tooling: catching what tests miss

The war story’s punchline is the chapter’s most practical advice: Miri. Ordinary tests cannot reliably catch UB, because UB-laden code frequently produces the right answer — until it doesn’t. You need a tool that checks the rules, not just the outputs. Miri is an interpreter for Rust’s mid-level IR that runs your code while watching for undefined behavior: out-of-bounds accesses, use-after-free, reads of uninitialized memory, misaligned accesses, and — via its Stacked Borrows model — aliasing violations exactly like the forged &mut above. Run your existing test suite under it with cargo +nightly miri test and it reports the precise unsafe operation that broke a rule, at the moment it broke it, instead of leaving you to debug a heisenbug three modules away months later.

Miri is not a proof — it only checks the paths your tests actually exercise — which makes test coverage of unsafe code matter more than anywhere else. The working discipline for any nontrivial unsafe code: keep the core small, give every block a // SAFETY: comment stating its exact invariant, write tests that drive the unsafe paths with adversarial inputs (empty buffers, capacity-boundary sizes, the worst case a hostile caller could pick), and run those tests under Miri in CI. That combination — minimal core, documented invariants, adversarial tests, Miri — is what turns “I wrote some unsafe code” into “I wrote a sound abstraction,” and it is the same combination the standard library uses to let millions of programmers build on Vec and Arc without ever thinking about the unsafe underneath.

Build it → Unsafe Rust at production scale, each kind for a different reason: Project 20: SIMD Analytics Engine wraps AVX2/AVX-512 CPU intrinsics — unsafe by nature — behind safe vectorized operators, the safe-abstraction pattern applied to performance; Project 15: Minimal OS Kernel runs bare-metal with no standard library, where raw pointers, mutable statics, and memory-mapped I/O are unavoidable; and Project 19: GPU Kernel Optimization drives the GPU across an FFI boundary, the same unsafe-call discipline as the C example above, scaled up to a real device.


Practical exercise

Difficulty: Level I · Level II · Level III

  1. Level I — Name the power and the obligation. Write a few lines that create a raw pointer from a local variable, then dereference it inside a small unsafe block to read and modify the value. Then write two sentences: which of the five powers did the unsafe block invoke, and what invariant are you now responsible for that the compiler would otherwise have checked? (Hint: the pointer must still be pointing at live, valid memory at the moment you dereference it — what in your code guarantees that, and what change would break it?)
  2. Level II — Wrap an unsafe core in a safe abstraction. Build a small sound type around an unsafe core — either a fixed-capacity stack backed by a raw allocation and ptr::write/ptr::read, or a safe wrapper around a tiny C function called through FFI. Expose only safe public methods. Then write the safety contract: state, in prose, the invariants your public API guarantees (e.g. “the pointer is always valid for capacity elements,” “len is always <= capacity”) and explain how each public method preserves them, so that no caller can break the unsafe core.
  3. Level III — Audit for soundness. Take a piece of unsafe code — your Level II type, a snippet from a crate, or a deliberately flawed example — and audit it as an adversary: for every public function, can any safe caller, with any input or sequence of calls, reach the undefined behavior inside? Find the hole if there is one (a missing bounds check, an aliasing &mut, an unchecked length, a too-eager Send). Then describe how you would establish its soundness: which adversarial test inputs you’d write to drive the unsafe paths, how you’d run them under Miri, and what the # Safety doc comment on each unsafe function would have to say for a reviewer to trust it.

Summary

unsafe is not an off switch for Rust’s safety; it is a small, explicit escape hatch that unlocks exactly five operations the compiler cannot verify — dereferencing raw pointers, calling unsafe functions and FFI, touching mutable statics, implementing unsafe traits, and reading union fields — and, for those operations only, transfers the burden of proof from the compiler to you. The borrow checker and type system keep running everywhere else. The discipline that makes this work is the safe-abstraction pattern: a small, auditable unsafe core, fenced behind invariant checks in safe code, exposed only through a safe public API — the way Vec, Box, and Arc are built. The property you are aiming for is soundness: no safe caller can provoke undefined behavior, no matter what they do. You get there by keeping the core minimal, documenting every block’s invariant with a // SAFETY: comment, testing the unsafe paths with adversarial inputs, and running those tests under Miri — because ordinary tests pass straight over UB that the optimizer will eventually collect on.

Key takeaways

  • unsafe unlocks five specific powers and turns nothing off — the borrow checker, the type system, and the aliasing rules all still apply; you become responsible for the ones the compiler can no longer prove.
  • The unit of correctness is the safe abstraction: a tiny unsafe core behind invariant checks and a safe public door, so callers never write unsafe themselves.
  • Soundness means no safe caller can reach undefined behavior by any sequence of safe operations — an unsound “safe” API is the real failure mode, and auditing is hunting for that hole.
  • FFI is inherently unsafe; wrap C calls in Rust signatures (slices, owned wrappers) that make the C contract impossible to violate, and pair every allocation with its matching free across the boundary.
  • UB “working” is a debt, not a reprieve — transmute and aliasing violations can run for years until an optimizer exploits them; Miri checks the rules, not just the outputs, so run it on every unsafe path.

Connections to other chapters

  • Rust: Ownership & Borrowing (prerequisite): unsafe Rust is the mirror image of that chapter. There the borrow checker proves that references are valid, exclusive, and outlive their data; here you do that proving by hand. Every // SAFETY: comment discharges an obligation the borrow checker would otherwise have discharged for you — which is why you cannot write sound unsafe code without first understanding what “safe” guarantees.
  • Memory and Resource Management (contrast): unsafe Rust is roughly what C++ does by default — manual pointers, manual lifetimes, no aliasing checks — except quarantined to explicit blocks and documented with invariants. The instructive difference is containment: in C++ the whole program is the sealed room with sharp tools; in Rust the room is a few lines you can point at and audit. The danger is the same; the scope of where it can hide is not. That chapter compares the languages’ resource models, including C++’s manual-pointer defaults this chapter quarantines.
  • Concurrency and Parallelism Models (extension): the Send and Sync marker traits, and every thread-safe primitive built on them — Mutex, Arc, the lock-free queues in crossbeam — sit on unsafe cores. Implementing Send/Sync is power number four, and getting it wrong is unsound in exactly the way this chapter describes; it is how the safe concurrency surface is built from unsafe foundations. That chapter sets Rust’s concurrency model beside the other languages’.
  • The Polyglot Landscape (extension): FFI is how Rust slots into systems written in other languages — a hot path inside a Python service, a native extension, a drop-in replacement for a C library. The unsafe boundary here is the seam where Rust meets the rest of the software world, and the safe-abstraction pattern is how you present a Rust-safe face to code on either side.

Further reading

Essential

  • The Rustonomicon — the official guide to unsafe Rust, covering raw pointers, ownership across FFI, aliasing, transmute, and the safe-abstraction pattern in far more depth than one chapter can; the canonical next step.
  • Programming Rust (Blandy, Orendorff, Tindall), the unsafe and FFI chapters — a careful, example-driven treatment of building safe abstractions over unsafe cores and interoperating with C.

Deep dives

  • The Miri project (rust-lang/miri) — documentation and source for the interpreter that catches undefined behavior, including the Stacked Borrows aliasing model it enforces; essential reading for anyone shipping unsafe code.
  • The Rust reference’s Behavior considered undefined and the std::mem::transmute API docs — the precise, authoritative list of what counts as UB and what obligations transmute places on you.

Historical context

  • RustBelt (Jung et al., POPL 2018) — the first formal proof that Rust’s type system, including its unsafe core, is sound; the academic foundation for the claim that a correct safe abstraction over unsafe is genuinely safe.
  • Stacked Borrows (Jung et al., POPL 2020) — the formal aliasing model that defines what “valid” raw-pointer use means and that Miri implements; the theory behind why the war story’s forged reference is undefined behavior.