# Safety in an unsafe { world }

Joshua Liebow-Feeser's RustConf 2024 talk, written up nearly verbatim. The argument is that "buggy programs don't compile" is not a property Rust gives you — it is a property a library gives you, and Rust only ships one instance of it (memory and thread safety) in the box.

The evidence is Netstack3, Fuchsia's pure-Rust networking stack, meant to replace the Golang-written Netstack2. Six years, roughly ten developers, 63 crates, 192,000 lines — more code than the top ten crates on crates.io combined. Networking code is famously hard to test, so the expectation for a ground-up netstack rewrite is months or years of field dogfooding turning up tens to hundreds of bugs before real users see it. Netstack3 ran an 11-month dogfooding program, at peak about 60 devices running nearly 24/7 in developers' homes, and found **three** bugs. A note added after the talk reports it now runs on millions of devices, with roughly 20x fewer crashes per million devices per day and 50% less memory than Netstack2.

Memory safety alone cannot explain that. Implementing what RFC 4614 calls "basic functionality" for TCP means six standards over 270 pages; with the recommended enhancements it's 18 standards over 476 pages. That's one protocol out of Ethernet, ARP, NDP, IPv4, IPv6, ICMP, IGMP, MLD, UDP and the rest, plus their interactions. Memory and threading bugs are a small slice of what can go wrong.

## The framework

The example is a binary tree whose ordering invariant Rust has no visibility into:

```rust
struct Node<T> {
    // INVARIANT: All values in `left` are less than `value`.
    left: Option<Box<Node<T>>>,
    // INVARIANT: All values in `right` are greater than `value`.
    right: Option<Box<Node<T>>>,
    value: T,
}
```

Three components. **Definition**: create an object the type system can reason about (`Node`) and attach to it a property the type system cannot (ordering), documented in prose because there is nowhere else to put it. **Enforcement**: make the fields private so outside code cannot break the property, and make every method that constructs or mutates a `Node` preserve it. **Consumption**: write code that is only correct *because* the property holds — here, finding a value's position in `O(log N)` instead of walking the whole tree.

The claim that makes this more than a documentation convention: from the perspective of safe code outside the module, there is no difference between this invariant and one the language enforces. Code that would violate the ordering does not compile, for the same reason code that would violate memory safety does not compile.

## Send is a library, not a language feature

The first worked example is that Rust's advertised thread safety is not a language feature at all. `std::thread::spawn` has to call out to something like `pthread_create`, which lives outside the language, so it goes in an `unsafe` block — the programmer takes on the proof obligation Rust can't discharge. That's unsound on its own, because nothing stops the caller passing a closure capturing an `Rc`. So the standard library defines an unsafe marker trait whose meaning exists only in its doc comment:

```rust
/// # Safety
///
/// `Self` is thread-safe.
pub unsafe trait Send {}
```

Definition is the trait plus its prose. Enforcement is `unsafe impl` (you can only lie by typing `unsafe`), hand-written impls for primitives and for `&Mutex<T>`, and a derive that requires every field to be `Send`. Consumption is the `F: Send` bound on `spawn`, which is what justifies the `pthread_create` call. The real `Send` uses auto-trait machinery for ergonomics, but nothing about the design needed to live in the compiler — a third party could have written it. See [[rust-send-sync]] for what `Send` and `Sync` actually mean and how `&T: Send` relates to `T: Sync`; [[rust-async-trait-sync-bound]] is a case of those bounds propagating somewhere nobody asked for them.

[[memory-safety-absolutists]] applies this reading to the claim that `unsafe` disqualifies Rust from being called memory-safe. If `unsafe` is the tool for encapsulating a proof the compiler cannot check, then its presence says nothing on its own, and the question worth asking is how often the proofs turn out to be wrong.

## Deadlock freedom as a type-system property

The second example is Alex Konradi's lock-ordering work in Netstack3. Fine-grained locking gives you a lock-order graph, and deadlock freedom means that graph is acyclic. Netstack3 has 77 mutexes across 192,000 lines, so tracking acquisition order by hand does not scale — and Netstack2, in Golang, had a history of deadlocks.

Each mutex gets a name as a phantom type parameter:

```rust
struct Mutex<Id, T> {
    mtx: std::sync::Mutex<T>,
    _marker: PhantomData<Id>,
}

enum IpLock {}
enum DeviceLock {}
```

The graph edges become unsafe traits `LockAfter<M>` and `LockBefore<M>`, generated by an `impl_lock_after!(A => B)` macro that also emits a blanket impl. That blanket impl is the trick: a cyclic invocation of the macro produces conflicting blanket implementations, so cyclic graphs fail to compile. Position in the graph is tracked by a zero-sized `LockCtx<Id>`, and `lock` threads it through:

```rust
pub fn lock<L>(&self, ctx: &mut LockCtx<L>) -> (MutexGuard<'_, T>, LockCtx<Id>)
where
    L: LockBefore<Id>,
```

The mutable borrow disables the old context for as long as the guard lives, so the returned `LockCtx<Id>` is the only usable one, and it only permits locking things downstream of `Id`. Locking device then IP, when the graph says IP comes first, is a trait-bound error at the second `lock` call rather than a hang in production.

The payoff was a one-line diff. After years of threading mutexes and guards through the whole stack, the team changed the default worker count from 1 to 4 — and it was bug-free on the first try.

Liebow-Feeser is candid that the presented version is simplified: he knows of two ways to circumvent deadlock safety in it, and the real library deliberately settles for "hard enough that you won't do it by accident" rather than literally impossible. That tradeoff is left open rather than resolved.

## The advice

Treat partial functions as a code smell. Public functions shouldn't panic, and shouldn't return `Option` or `Result` unless failure is inherent to what they model — I/O failing is fine, rejecting an illegal argument means the call should not have compiled.

Make the API's shape match the problem's shape. Netstack3's IP parse error carries a *parameter problem pointer* — the byte offset of the malformed field, echoed back in the error packet. IPv4 stores that in one byte, IPv6 in four, so the field is generic over the IP version (`I::ParameterProblemPointer`) rather than uniformly `u32`. A `u32` would push a fallible conversion somewhere else in the codebase, where a bug either emits a wrong error packet or panics the stack.

Apply the same rigor to internal APIs as to public ones — the obvious-in-the-moment calling convention won't be obvious to whoever arrives four years and six refactors later. And don't expect a recipe: the lock-ordering cycle detection is not something that falls out of the language naturally. Quoting Rob Pike on Golang's design, "simplicity is the art of hiding complexity" — the internals of the lock-ordering library are ugly, and the mental model its users need is one sentence: cyclic graphs don't compile.

The methodology has a blind spot worth naming, and [[tokio-progress-not-ordering]] is a good illustration of it: the invariant there (all tasks from one event finish near their submission time) is a property of the *application*, not of any type the runtime can see, so no amount of library discipline inside Tokio would have caught it.
