Stability bugs in libmultiprocess
and Bitcoin Core IPC

102 bugs, 2019 to 2026: what they were, how they were found, and what is left
Ryan Ofsky
ipcbugs.ofsky.org

What the survey is

Models did the triage, verification and drafting; I reviewed and made the judgment calls. Method: ipcbugs.ofsky.org/method

The numbers

102
stability bugs, 2019 to 2026
94 to 104 depending on how they are split
82
in production code
13 in test code, 7 CI, build or example
87
fail-stop: crash, assert, hang, deadlock, race, UB
one silent wrong result (53)
69
fixed or worked around
33 open, 13 of them with a fix in review
8
reported from real deployments
all Stratum v2 software on the mining interface
12
days, median from first report to merged fix
mean 123; 17 took over 90 days

What the bugs are

42 of 82 production defects need a disconnect, a shutdown or a signal to happen

The mechanisms fall into 11 pattern groups. These five cover 58 of the 82:

PatternThe rule the fixes implydefects
An object outliving what it points toWhatever an object touches in its destructor or callbacks, it must own or keep alive11
An in-process interface trusting what now comes from another processOnce an interface is on the wire, every assert reachable from its inputs becomes validation or an error14
Error paths that skip cleanup or completionAny call across a process boundary can fail; cleanup and “done” signals run on every exit path16
Work or cleanup on the wrong threadState owned by one thread is read, written and destroyed only on that thread7
Locks, waits and wakeupsEvery reader and writer of a guarded field takes the lock; notify under the waiter's mutex10
Six more groups, the largest serving before ready and freeing before done (8): ipcbugs.ofsky.org/patterns

Would Rust have helped?

24
yes: would not compile in safe Rust
13 of them lifetime or wrong-thread bugs
27
partly: harder to write, or fails differently
an abort becomes a panic, a crash a hang
29
no: ordering, deadlock, protocol, trust
plus 2 toolchain problems, not applicable
Rust would have ruled out about a third of production defects: mostly the lifetime and thread-affinity groups; not the hangs, lifecycle ordering or wire-boundary trust.
Over the 82 production defects, judged per bug from its fix: ipcbugs.ofsky.org/patterns/rust

Old code, new exposure

all bugs production defects production defects from 2019 code

9 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

How they were found

Production defects by first report, per half year (2026 H2 is to mid October)

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 bymedian days to fix
TSan5
review18
intermittent CI25
user report53.5
manual testing188

The black box, in one slide

Event-loop thread

  • all socket I/O, all promises
  • owns Connection and Cap'n Proto connection state
  • on disconnect: destroys the Connection, runs its cleanup callbacks
post the request
loop->sync(fn)
worker blocks until the loop has run fn

Worker threads

  • one per thread the client asks for
  • run the method body of every call with an mp.Context (all mining calls)
  • may still be running when the client goes away
Where the fixes converge: touch connection state only on the event-loop thread, and never hold a lock while waiting for it.

Bug 45: two threads erase one entry mp#201, 2025

Worker thread, after the method returns: PassField, include/mp/type-context.h
Event-loop thread, run by ~Connection after a disconnect: callback from SetThread, src/mp/proxy.cpp
Erasing destroys a ProxyClient<Thread>, whose cleanup waits for the event loop: ProxyClientBase, include/mp/proxy-io.h
lock taken (same mutex: threads.mutex is Waiter::m_mutex)   destroys the entry or waits

Bug 45: the lock cycle

Found: TSan on Sjors's CI (2025-07); mptest hanging on ARM CI; a segfault in CI
In production: a v30.0 node stopped answering IPC after about 25 minutes under a Rust Stratum v2 client

Bug 45: the fix 4a269b21b8c8, 76 lines

include/mp/type-context.h
  • Every add, erase and callback removal for these entries runs on the loop thread, including SetThread and ~ProxyClient<Thread>
  • The node is destroyed after the mutex is released
  • Merged 2025-09-17; reached 30.x after v30.0 shipped
  • The thread-exit path of the same map (37) is still open: mp#361
A lock order would not fix it. Moving the destruction onto the loop thread does.

The cancellation chain: 3, then 5, then 79

BUG 3 · mp#240

A killed client's waitNext returns into a deleted Connection

Field reports from Rust Stratum v2 clients, v30.0rc3 to v30.2, from 2025-10
Fix: cancel requests on disconnect. 267 lines, 2026-02-20
caused
BUG 5 · mp#249

The cancel frees the params; the worker reads them anyway

Antithesis, 2026-03-09; sanitizer CI; a Rust client on master
Fix: read params on the loop thread, after the cancel check. 9 lines, 2026-03-27
slipped past
BUG 79 · mp#349

The worker's first getResults() reads capnp state the loop is tearing down

Antithesis with TSan, 2026-08-19; in code since 2019
Fix: call it once on the loop thread. 19 lines, 2026-08-26

mp#240 also caused 4 and 6, found within three weeks of merging. 4, 5 and 6 never reached a release.

Bug 5: read first, check second mp#249, 2026

Worker thread: PassField, include/mp/type-context.h, before the fix. The sync() lambda runs on the event-loop thread.
Fix (9 lines): drop the early read; read the params inside sync(), after the check
Loop: posts the request  →  Loop: client disconnects, request cancelled, params freed  →  Worker: getParams() on freed memory

Bug 79: inside Cap'n Proto mp#349, 2026

Worker thread, after the method returns: ServerRet::invoke, include/mp/proxy-types.h
Cap'n Proto, first call only (later calls return a cached response): RpcCallContext::getResults, capnp/rpc.c++ 1.5.0
Event-loop thread, abrupt disconnect: RpcConnectionState::disconnect, two steps
Fix: fill the cache on the loop thread, at the end of PassField's existing loop.sync block

Optional: bug 30, a server outliving its connection

~Connection, src/mp/proxy.cpp: the assumption
Worker, after a long call returns: PassField (before mp#118) and ~ProxyServerBase (before mp#160)
2024-10-16Ctrl-C a mining client during waitFeesChanged: segfault. Same-day workaround leaks the server object
2025-06-10darosior's Rust client in a reconnect loop: ASan use-after-free on the other path
2025-06-19Fix, mp#160: every proxy holds its own event-loop pointer, m_context.loop

Fog of war: what the fix commits said

Commit saidLater 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
18 commit messages and 9 PR or review statements are corrected on the bug pages, each with the evidence. Several are mine.

Where the defects lived

of 82 production defectsn
ever on master78
trigger reachable in merged code44 +13
shipped in a release with IPC binaries (v30+)43
shipped only in source releases23
never released16

8 hit real deployments. All are the mining interface, all driven by Stratum v2 software.

  • crash when a killed client's waitNext completes (3)
  • node stops answering IPC after 25 minutes (45)
  • assert on two calls to one thread (47); Ctrl-C hang (52); connect during startup (58)
  • still open: shutdown assert (44), memory growth (55)

The waves idea: the hypothesis

My starting guess: each class of bug surfaced once the previous class was handled. Tested only after every record was classified.

WaveClass of bugWhy it appeared then
1Disconnect handling missingThe library assumed subprocesses live as long as the process and never disconnect before shutdown
2Unclean disconnectsOnce clean disconnects worked, only clean ones were expected. Aborting C++ peers, and Rust or Python clients that skip destructors, broke that
3Racy disconnectsWith the common unclean cases handled, the races left needed new code paths and refactoring: the cancellation work, mp#240 and mp#249
4Residual small bugsWith every disconnect path in place, the rest are one-line fixes: refcounts, lock order
4.5Server-side disconnectsEarlier waves were clients leaving servers. Servers leaving clients was less tested until xyzconstant added tests and fixes
5Theoretical and rare racesFound by LLM-assisted review, other reviewers and Antithesis

The waves idea: what held, what did not

Held

  • allAs the order the classes were fixed: the fix dates follow the waves
  • 2Foreign clients: the Rust client (2024-12) for 40, 41, 28, 8; Python and fuzzing (2025-09) for 42, 47, 57
  • 3New code paths were needed: mp#160, mp#240
  • 42026 disconnect fixes are small: lock moves (4, 64), a reorder (7), one prefetch (79)
  • 4.5xyzconstant's July 2026 tests found 72, 73, 80 in five weeks
  • 5Clearest in the data: of 32 records since July 2026, 20 from review, 25 reported with LLM help

Did not hold

  • allAs successive classes: by first report they overlap from the start
  • 1Clean disconnects did not come first: 24, 16, 17 were earlier; the oldest (38, 39, 2020) are unclean
  • 2Not done before wave 3: foreign-client finds kept coming (3, 42, 47, 57; 81, 82, 84)
  • 3Not one step: fourteen months of PRs, mp#186 to mp#361
  • 4It holds the two largest fixes: 3 (267 lines), 44 (288, pending)
  • 5Antithesis finds were real crashes and CI failures, not theoretical
What replaces it: the exposure timeline. New workloads reached code that had been there since 2019.

Still open, and what would help

33 open

  • Fix in review: 44 (bitcoin#35932 + mp#335), 37, 77, 78 (mp#361), 75 (bitcoin#36011), 56 (bitcoin#36047), 90, 92, 103 (bitcoin#36520), 83 (bitcoin#36525), 89 (mp#368), 93 (mp#369), 16 (branch)
  • Undiagnosed: 55, memory growth under Stratum v2; 48, 32-bit segfault in kj::str
  • Private review findings: 81, 82, 84, 86, 87, reproduced by tests in September 2026; 88, 91, 94 to 102, added in October
  • 79 is merged in mp, not yet in a subtree bump

What would help

  • Tests with foreign peers: abandon calls, skip destroy, two requests on one thread. 11 production defects need such a peer
  • Server-side disconnect tests: the July 2026 tests found three defects in five weeks
  • Sanitizer coverage: 15 records could only be seen with a sanitizer or Antithesis

Discussion

ipcbugs.ofsky.org  ·  all bugs  ·  findings  ·  corrections  ·  explorer
Bugs shown: 45, 3, 5, 79, 30  ·  tracking issue bitcoin#28722
Notes