Audit report AR-47
Reported privately in an independent security review, 2026-09-27.
- Report date
- 2026-09-27
- Kind
- Bug
- Status on master
- Present
- Proposed issue state
- Open
Summary
When libmultiprocess creates a worker thread for a client (makeThread or makePool), there is a short step between starting the thread and handing it to the object that will later stop it. If an exception were thrown in that step, C++ would destroy a running thread handle and terminate the process. The mechanism is real, but the only thing that can throw there is a memory allocation, and the Bitcoin Core node and GUI already terminate when memory runs out. Robustness hardening for other users of the library, not a bug that can be hit in Bitcoin Core.
The report
Both makePool and makeThread start a std::thread, wait for it to publish its thread context, and only then construct the ProxyServer<Thread> object that owns the thread handle. An exception after the thread starts and before that object takes ownership destroys a std::thread that is still running, which calls std::terminate. A test that injects an exception at that point aborted the process through both functions.
The report rates it low. The trigger is an allocation or construction failure while creating a worker. No natural failure rate, remote trigger or lasting damage was established, and the API is local and trusted.
Proposed change
Move the stop-and-join code from the ProxyServer<Thread> destructor into a helper, and arm it with a scope guard as soon as the thread has started. Once the handle has been moved into ProxyServer<Thread>, the guard finds nothing to join and does nothing.
- auto thread_server = kj::heap<ProxyServer<Thread>>(m_connection, *thread_context.get_future().get(), std::move(thread));
+ ThreadContext& worker_context{*thread_context.get_future().get()};
+ KJ_DEFER(StopWorkerThread(worker_context, thread));
+ auto thread_server = kj::heap<ProxyServer<Thread>>(m_connection, worker_context, std::move(thread));
The test injects an exception in each path and checks that the client receives the error and the connection keeps working. The report considered a new owning wrapper for the half-started worker (another type for a short step) and allocating ProxyServer<Thread> before starting the thread (a restructuring of its setup).
Assessment
The mechanism is accurate. On libmultiprocess master and in Bitcoin Core's copy, the thread handle is a plain local variable until it is moved into ProxyServer<Thread>, and only that object's destructor stops and joins the thread.
In practice almost nothing can throw in that step. Waiting for the thread context fails only if the new thread itself throws, and that already terminates the process on the new thread, which the fix does not change. The ProxyServer<Thread> constructor does not throw. What remains is the memory allocation for ProxyServer<Thread>. Bitcoin Core installs an out-of-memory handler at startup that terminates the process instead of throwing, so in bitcoin-node and the GUI, which serve the node's IPC connections, the result is the same with or without the fix. Only the injected test exception reaches this path. Other applications built on libmultiprocess would get an error instead of an abort when memory runs out.
The fix reads as correct, though it was not run here. It keeps the destructor's existing shutdown steps under the same lock, and it does nothing once ownership has passed. It is a reasonable small change if this code is touched again, for example together with the pool rollback in AR-46, which edits the same loop.
Status
Present on master. Hardening; no public issue or PR yet.
Reported privately in an independent security review on 2026-09-27.