Would Rust have prevented these bugs?

Readers of a survey of C++ memory and threading bugs ask this, so each lesson records an answer: would writing this code in safe Rust have prevented the bug? Each answer is a one-line judgment made by reading the fix, not by porting the code, and the framing assumes the native pure-Rust Cap'n Proto implementation with RPC (capnproto-rust), so there is no FFI layer to hide behind. The counts:

Records Yes Partly No Not applicable
All 102 records 29 28 38 7
The 82 defects in production code 24 27 29 2

Over production defects the three verdicts are close to a third each. That is the honest summary: Rust would have rejected about a third of these bugs at compile time, changed the shape of another third without removing the mistake, and had nothing to say about the rest.

What the verdicts mean

Three things the counts hide

Send and Sync catch data races, not ordering. Every field in the disconnect path that was written under a lock on one thread and read bare on another is a compile error in Rust, because the field would have to live inside a Mutex for the type to be Sync. That is the bulk of the "yes" verdicts in the locks group. But Condvar::notify_all takes no guard, so the unlocked notify that lost a wakeup compiles; holding one mutex while blocking on a hand-off to the loop thread is a lock-order deadlock in either language; and a "done" signal sent for a partial condition is a protocol error the type system cannot express. Rust removes the races ThreadSanitizer found and leaves the hangs gdb found.

capnproto-rust's RPC objects are single-threaded. Its clients, connections and call contexts are Rc-based and !Send, so a worker thread cannot hold a proxy, destroy one, or call getResults() on a call context. The whole wrong-thread group is ruled out by construction, and so is the two-destroyer layout behind several lifetime bugs: nothing but the loop thread can touch those objects. The cost is a different model for blocking calls. libmultiprocess runs method bodies on worker threads that block on locks and on other IPC calls; in capnproto-rust, blocking work has to be handed to another thread explicitly and its result sent back. Several of the fixes ended up in that shape anyway (results built on the loop thread, parameters copied before the hand-off), and the hand-off itself has the same cancel-versus-join gap that the open shutdown work is closing.

The idiomatic Arc fix extends a lifetime instead of freeing it. Where C++ had a raw pointer to a connection or to node state, Rust would have an Arc or a Weak. The Arc compiles and removes the use-after-free, and in most programs that is the end of it. In this code the object kept alive is often a connection or an EventLoopRef, and the event loop exits when its last reference drops, so a lifetime that extends past shutdown is a node that never exits. The ledger already contains that hang, twice, from C++ code that kept references too long. The "yes" verdicts in the outlives-referent group therefore come with a caveat their notes state: the crash goes away, and the ownership design still has to be right or the failure moves to shutdown.

By pattern group

The split follows the mechanism. Lifetime and thread-ownership groups are mostly "yes"; the groups about what a peer sends, what an error path does and when a process is ready are mostly "partly" or "no", because those are decisions rather than memory.

Examples

Each note is the one sentence recorded with the lesson.

Yes

Partly

No

The seven "not applicable" records are the toolchain and build problems: a CI memory limit, a two-compiler ABI disagreement, a Cap'n Proto header under C++20, a misdetected feature macro, a mingw-w64 runtime bug. A capnproto-rust build would not contain kj at all, but that is a different program, not the same program in a different language, and the question this page asks is about the code as written.