Audit report AR-51
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
- Linked bugs
- Bug 95: Concurrent first IPC use can abort the process
Summary
If two threads in one Bitcoin Core process make their first IPC call at the same time, both can try to start the IPC event loop thread, and the second attempt terminates the process. The report is accurate and its fix, one mutex around the startup, is correct. On master the only way to make two first calls at once is the hidden, test-only echoipc RPC on a bitcoin-node started without -ipcbind. Bug page: Concurrent first IPC use can abort the process.
The report
CapnpProtocol::startLoop() checks whether the event loop exists without any synchronization, and the background thread only creates the loop after it has started running. Two callers can both see no loop, both start a thread, and the second assignment to the std::thread member replaces a thread that is still running. In C++ that calls std::terminate.
The report reproduced this with a unit test that holds the first loop thread before it creates the loop, lets a second caller pass the same check, and saw the process abort. It later added a process-level reproducer: eight concurrent first-use echoipc RPC calls against a bitcoin-node built with ThreadSanitizer, which reported two races on master, one on the thread assignment and one where another RPC thread read an event loop that was still being constructed. With the fix, four ThreadSanitizer runs of the same burst returned all eight results without a report.
The report rates it low. The trigger is the hidden, authenticated echoipc RPC on an IPC-enabled bitcoin-node; plain bitcoind answers echoipc locally and never starts the loop. No unauthenticated or peer-to-peer trigger, persistent corruption or effect on funds was found.
Proposed change
Hold one mutex across the check, the thread start and the wait for the loop to be ready, so a second caller waits for the first and then reuses its loop:
void startLoop()
{
+ const std::lock_guard lock{m_loop_mutex};
if (m_loop) return;
...
}
+ std::mutex m_loop_mutex;
A unit test runs two makeStream() calls in a child process with barriers that force the bad interleaving, and the functional test rpc_misc.py sends eight first-use echoipc calls at once over separate RPC connections. The change builds on the fix for AR-50, which lets loop startup fail with an exception the caller can retry; the report rejected std::call_once because a failed first attempt would then block every retry, and a plain "started" flag because other callers could see a loop that is not ready.
Assessment
Accurate. In src/ipc/capnp/protocol.cpp, startLoop() begins with if (m_loop) return; and has no lock, and it has had this shape since the IPC code was added in bitcoin#19160 in 2021. Both races in the report follow from that one missing lock.
The reach on master is small. Of the three ways a process starts its loop, bitcoin-node -ipcbind does it once during startup, before RPC calls are accepted, and bitcoin-cli does it once from a single thread. The third, spawning a child process, is only used by echoipc, so the race needs a bitcoin-node without -ipcbind, an authenticated RPC client and two echoipc calls on different RPC threads. The planned multiprocess setup, where bitcoin-gui starts bitcoin-node and the node starts bitcoin-wallet, would use the same path for real.
The fix reads as correct, though it was not run here. The loop thread never takes the new mutex, so waiting for it while holding the mutex cannot deadlock. It needs the AR-50 change first, and the two are best taken together because both change startLoop(). The report also mentions a separate experiment with std::call_once that made every call fail; that was a different patch and says nothing about this one. The unit test is enough to show the bug; the functional burst spawns eight extra processes.
Status
Present on master. Next step: a Bitcoin Core PR with this fix and the AR-50 fix. No public issue or PR yet.
Reported privately in an independent security review on 2026-09-27.