Audit report AR-32
Reported privately in an independent security review, 2026-09-07.
- Report date
- 2026-09-07
- Kind
- Bug
- Status on master
- Fixed in mp#349
- Proposed issue state
- Closed
- Linked bugs
- Bug 79: Node can crash when a client dies while a call's reply is being built
Summary
On an abrupt client disconnect, a server worker thread's first getResults() call could read Cap'n Proto connection state while the event loop was tearing that state down, a data race with a possible null dereference. The report is a backport to Bitcoin Core's copy of libmultiprocess of the public fix mp#349, and cites it; it is not an independent discovery. Real bug, fixed in libmultiprocess, reaching Bitcoin Core with the next subtree update.
The report
In libmultiprocess's request path, a call dispatched to a worker thread first touches its results struct on that worker. At the same time, disconnect cleanup on the event-loop thread can change the same connection state. The report cites the upstream report of the race and a possible crash interleaving, mp#348, and notes that mp#349, which repairs it, is merged.
It states that the IPC listener is optional and that a client able to reach it already has broad RPC access including shutdown, so it frames this as a library reliability problem, not a privilege boundary or peer route. It reproduced no null dereference itself, and notes that a local ThreadSanitizer run of its own used an uninstrumented Cap'n Proto and so cannot rule out the upstream race.
The reproduction is the KJ test from mp#349, "Calling async IPC method with a remote disconnect while results are built", which uses a new testing hook and a new asynchronous test method to line up a disconnect with the worker's first results access.
Proposed change
Initialize the results struct on the event-loop thread, inside the synchronization that already runs before the call is dispatched to the worker, so the worker later only reads the response Cap'n Proto has cached:
std::tie(request_thread, inserted) = SetThread(
GuardedRef{thread_context.waiter->m_mutex, request_threads}, server.m_context.connection,
[&] { return Accessor::get(call_context.getParams()).getCallbackThread(); });
+ // Initialize the request's results struct here on the event loop
+ // thread, so later getResults() calls on the execution thread
+ // return the response capnp caches on first use instead of
+ // reading Cap'n Proto connection state, ...
+ call_context.getResults();
});
A method that throws may leave the preallocated response unused. The report itself recommends a normal libmultiprocess subtree import instead of maintaining this as a separate downstream backport.
Assessment
The diff is mp#349 unchanged: the same change and comment in include/mp/type-context.h, the same test hook and the same KJ test. The race was found by Antithesis and reported as mp#348 on 2026-08-19. It needs no hostile peer: any client that disconnects abruptly while a call is being dispatched can trigger it, although the window is narrow.
Status
Fixed in libmultiprocess master by mp#349. Bitcoin Core master still carries the subtree imported on 2026-07-14, so the fix arrives with the next subtree update; no separate backport is needed.
Reported privately in an independent security review on 2026-09-07.