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.

Bug page: Server worker thread's first getResults() reads Cap'n Proto connection state while the event loop tears it down on an abrupt client disconnect.

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.