The mechanisms fall into 11 pattern groups. These five cover 58 of the 82:
| Pattern | The rule the fixes imply | defects |
|---|---|---|
| An object outliving what it points to | Whatever an object touches in its destructor or callbacks, it must own or keep alive | 11 |
| An in-process interface trusting what now comes from another process | Once an interface is on the wire, every assert reachable from its inputs becomes validation or an error | 14 |
| Error paths that skip cleanup or completion | Any call across a process boundary can fail; cleanup and “done” signals run on every exit path | 16 |
| Work or cleanup on the wrong thread | State owned by one thread is read, written and destroyed only on that thread | 7 |
| Locks, waits and wakeups | Every reader and writer of a guarded field takes the lock; notify under the waiter's mutex | 10 |
Send and Sync stop data races (all 7), not ordering races or deadlocks (none of the 15 hangs and deadlocks)Arc turns a use-after-free into a longer lifetime; here that means a node that never exits9 first reports before 2024, 13 in 2024, 32 in 2025, 48 so far in 2026
37 of 73 dated production defects were in the library's first commits, June to August 2019
19 of the 41 production defects reported in 2026 had been in the code since 2019
Median bug lived 996 days from introducing merge to fix
2026: 2 of 41 production defects first found by someone running the software
20 of 30 production defects since July 2026 came from review
| first found by | median days to fix |
|---|---|
| TSan | 5 |
| review | 18 |
| intermittent CI | 25 |
| user report | 53.5 |
| manual testing | 188 |
Connection and Cap'n Proto connection stateConnection, runs its cleanup callbacksloop->sync(fn)fnmp.Context (all mining calls)PassField, include/mp/type-context.h~Connection after a disconnect: callback from SetThread, src/mp/proxy.cppProxyClient<Thread>, whose cleanup waits for the event loop: ProxyClientBase, include/mp/proxy-io.hthreads.mutex is Waiter::m_mutex) destroys the entry or waitsSetThread and ~ProxyClient<Thread>waitNext returns into a deleted ConnectiongetResults() reads capnp state the loop is tearing downmp#240 also caused 4 and 6, found within three weeks of merging. 4, 5 and 6 never reached a release.
PassField, include/mp/type-context.h, before the fix. The sync() lambda runs on the event-loop thread.sync(), after the checkgetParams() on freed memory
ServerRet::invoke, include/mp/proxy-types.hRpcCallContext::getResults, capnp/rpc.c++ 1.5.0RpcConnectionState::disconnect, two stepsPassField's existing loop.sync block~Connection, src/mp/proxy.cpp: the assumptionPassField (before mp#118) and ~ProxyServerBase (before mp#160)| 2024-10-16 | Ctrl-C a mining client during waitFeesChanged: segfault. Same-day workaround leaks the server object |
| 2025-06-10 | darosior's Rust client in a reconnect loop: ASan use-after-free on the other path |
| 2025-06-19 | Fix, mp#160: every proxy holds its own event-loop pointer, m_context.loop |
| Commit said | Later analysis found |
|---|---|
| 196e6fcdbc1f (2024, bug 30) "prevent null pointer dereference" | A use-after-free of the freed Connection; ASan reported it that way in 2025 |
| c685fa9bedaf (2019, mp#7) "onDisconnect events are now cancelled as Connection objects are destroyed" | A handler already re-posted to the event loop is not cancelled: bug 77, found 2026-08, fix in mp#361 |
| c4762c7b513a (2026, mp#240) "does not change behavior other than changing log messages" | Running the call inside kj::runCatchingExceptions is the fix for 57: an exception on a worker thread terminated the node |
| of 82 production defects | n |
|---|---|
| ever on master | 78 |
| trigger reachable in merged code | 44 +13 |
| shipped in a release with IPC binaries (v30+) | 43 |
| shipped only in source releases | 23 |
| never released | 16 |
8 hit real deployments. All are the mining interface, all driven by Stratum v2 software.
waitNext completes (3)My starting guess: each class of bug surfaced once the previous class was handled. Tested only after every record was classified.
| Wave | Class of bug | Why it appeared then |
|---|---|---|
| 1 | Disconnect handling missing | The library assumed subprocesses live as long as the process and never disconnect before shutdown |
| 2 | Unclean disconnects | Once clean disconnects worked, only clean ones were expected. Aborting C++ peers, and Rust or Python clients that skip destructors, broke that |
| 3 | Racy disconnects | With the common unclean cases handled, the races left needed new code paths and refactoring: the cancellation work, mp#240 and mp#249 |
| 4 | Residual small bugs | With every disconnect path in place, the rest are one-line fixes: refcounts, lock order |
| 4.5 | Server-side disconnects | Earlier waves were clients leaving servers. Servers leaving clients was less tested until xyzconstant added tests and fixes |
| 5 | Theoretical and rare races | Found by LLM-assisted review, other reviewers and Antithesis |
kj::strdestroy, two requests on one thread. 11 production defects need such a peerCallContext, patch capnp) instead of relying on "only on the loop thread"?