Audit report AR-48

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 96: Failed startup does not exit while a mining client is waiting

Summary

If bitcoind or bitcoin-node fails to start while an IPC mining client is waiting in makeMining for the node to load, the node logs "Shutdown done" and then never exits. In -daemon mode the stuck process keeps the data directory locked, so a corrected restart fails until it is killed. The report is accurate and its fix, a few lines in bitcoind.cpp, is right. Bug page: Failed startup does not exit while a mining client is waiting.

The report

Init.makeMining waits until either the chainstate is loaded or the shutdown signal is raised. bitcoind starts its IPC listener before the chainstate is loaded. If startup (AppInit) fails in that interval, main calls Interrupt() and Shutdown() without raising the shutdown signal. The client is disconnected, but the server request stays parked in the wait, which keeps the IPC event loop alive, and the process blocks on it during teardown.

The reproduction is a functional test that replaces peers.dat with a named pipe, so startup blocks after the IPC socket is listening and before the chainstate loads. It waits until the node is executing the client's makeMining request, then writes a corrupt peers.dat so startup fails, and expects the node to exit with status 1. The report also describes earlier runs, not repeated on its final branch: the same hang after a later -bind failure with a waitTipChanged waiter, and in -daemon mode, where the parent process had already reported a successful launch and the failed child stayed alive holding the data directory lock.

The report rates it low: it needs IPC enabled, a client calling makeMining during startup, and a startup failure. The node never comes up, but the process stays until killed.

Proposed change

Raise the normal shutdown signal when AppInit fails, before interrupting services. Interrupt() then wakes the waiting call, makeMining returns no interface, and the node exits with its failure status:

-    if (!AppInit(node) || !Assert(node.shutdown_signal)->wait()) {
+    if (!AppInit(node)) {
+        node.exit_status = EXIT_FAILURE;
+        // Wake threads waiting for initialization to finish, such as IPC makeMining calls.
+        if (!(Assert(node.shutdown_request))()) {
+            LogError("Failed to send shutdown signal after initialization failure\n");
+        }
+    } else if (!Assert(node.shutdown_signal)->wait()) {
         node.exit_status = EXIT_FAILURE;
     }
     Interrupt(node);

The report considered a separate "initialization failed" flag for makeMining (rejected: it duplicates the shutdown state the wait already checks) and disconnecting clients earlier (rejected: that does not wake a thread already waiting inside the node).

Assessment

Accurate. On Bitcoin Core master, main in src/bitcoind.cpp (also used by bitcoin-node) does not raise the shutdown signal when AppInit fails. Interrupt() wakes the threads waiting on the tip condition variable, but they wait for the chainstate to load or for the shutdown signal, so they find nothing changed and wait again. This is not limited to makeMining: waitTipChanged, waitNext and createNewBlock check the same signal, so a client waiting in one of them when a later startup step fails is stuck the same way. The process then stops at the same place as an earlier bug, Ctrl-C did not stop a node with a waiting mining client: teardown waits for the event loop thread, which is kept alive by the parked call. There the signal was raised but nobody woke the waiters; here the waiters are woken but the signal is not raised. The GUI is not affected, because its startup failure path raises the signal. The daemon-mode effect follows from the code (the parent reports success before startup runs, and the child takes the lock), but that run was not repeated.

The makeMining wait came from bitcoin#34661 (merged 2026-02-27, v31.0), which fixed a crash when mining clients connected before the chainstate existed. The fix proposed here is minimal and covers every wait, because they all check the same signal. Raising the signal inside Interrupt() would also protect any future caller that skips shutdown_request. This should land before any change that makes shutdown wait for running IPC calls, such as bitcoin#35932 or the report in AR-43. Otherwise the hang moves from after "Shutdown done" into Shutdown() itself.

Status

Present on master. Next step: a Bitcoin Core PR with the bitcoind.cpp change and the named pipe test. No public issue or PR yet.

Reported privately in an independent security review on 2026-09-27.