Audit report AR-43
Reported privately in an independent security review, 2026-09-26.
- Report date
- 2026-09-26
- Kind
- Bug
- Status on master
- Present
- Proposed issue state
- Open
- Linked bugs
- Bug 44: Node aborts at shutdown while an IPC mining call is still running
Summary
When bitcoin-node shuts down, an IPC call that is already running on a worker thread can keep using the chain manager, mempool or block template state after Shutdown() has freed it. This is a known bug with a fix under review: bitcoin-node aborts at shutdown while IPC calls are still running. The report adds evidence that the RPC interface is affected too, and proposes a different fix (a gate around each method that touches node state) plus stopping IPC listeners at the start of shutdown.
The report
The first version of the report was a hardening proposal: during shutdown, Bitcoin Core stops its HTTP RPC server early but disconnects IPC clients only near the end, so a connected IPC client can still run RPC methods after HTTP RPC has stopped. A functional test showed a second client's gettxout call succeeding after the log said RPC had stopped.
The current version keeps that and adds the larger problem. Stopping listeners and disconnecting clients prevents new calls but does not wait for calls already executing, so shutdown can free node state while a mining or RPC call is still using it. The report says controlled runs showed a use-after-free in block submission, assertion failures in chain and template access, an invalid mempool access, and a use-after-free in a getblockcount call made through the IPC RPC interface. Its tests hold a call inside the node with test-only hooks and check that shutdown does not finish while the call is held.
The report rates it low: the caller needs access to a local IPC socket and a call that overlaps shutdown. The result is a crash of one node; a restart restores service.
Proposed change
Two parts. First, keep the handle returned when each IPC listener is created, and at the start of Shutdown() stop the listeners and disconnect clients. This relies on an unmerged listener-stop API (AR-21). Second, add a gate that every mining, block template and RPC method enters before touching node state; once shutdown starts, new entries are refused, and shutdown waits for the active ones to leave before freeing anything:
void Shutdown(NodeContext& node)
{
+ // Stop accepting new -ipcbind requests before tearing down node state
+ if (interfaces::Ipc* ipc = node.init->ipc()) {
+ ipc->disconnectIncoming();
+ }
+ node.ipc_node_state_call_gate.StopAndWait();
...
bool checkBlock(const CBlock& block, const node::BlockCheckOptions& options, std::string& reason, std::string& debug) override
{
+ auto call{m_node.ipc_node_state_call_gate.TryEnter()};
+ if (!call) {
+ reason = "shutdown";
+ debug.clear();
+ return false;
+ }
The report considered giving IPC objects shared ownership of node components (rejected: it spreads shutdown policy and keeps state alive past its intended teardown) and a general drain in libmultiprocess (useful, but Bitcoin Core would still need to decide which calls depend on node state).
Assessment
Accurate, and the same bug as bitcoin-node aborts at shutdown while IPC calls are still running, first reported in bitcoin#33387 (2025-09-14) and found again by Antithesis in bitcoin#35845. On Bitcoin Core master, Shutdown() calls disconnectIncoming() just before it resets the mempool and chain manager. disconnectIncoming() destroys the incoming connections, but a method already running on a worker thread runs to completion and then uses the freed state. The report's getblockcount case shows the RPC interface is exposed in the same way as the mining interface. Its crash runs are described but are not part of the proposed tests, which check only that shutdown waits, and they were not repeated here.
The pending fix, bitcoin#35932 with mp#335 (both open), works one level down: it disconnects each connection and then waits, off the event loop thread, until the connection has no live server objects. That covers every interface and every method, including ones added later, without editing each method. The gate only covers methods that remember to enter it, and not destructors that run after the method returns. Both approaches rely on waits inside node code being woken at shutdown. Interrupt() wakes waitTipChanged and waitNext, but not a makeMining call waiting for startup after initialization fails (AR-48). Reading the code, both the gate (which makeMining enters before it waits) and the pending drain would wait for that call, so in that case the node would hang during shutdown rather than after it.
The listener half needs a way to stop a listener, which libmultiprocess does not have yet (AR-21, and mp#269 proposes one). Moving disconnectIncoming() to the start of Shutdown(), so IPC clients lose access at the same time as HTTP RPC clients, is a small change worth considering alongside bitcoin#35932.
Status
Present on master. A fix is under review in bitcoin#35932 and mp#335. Stopping listeners at shutdown waits on a listener-stop API.
Reported privately in an independent security review on 2026-09-26.