init: Signal m_tip_block_cv on Ctrl-C
What changed, and why it matters
This commit fixes a shutdown bug in Bitcoin Core. When a user pressed Ctrl-C or the process received SIGTERM, threads waiting for the blockchain tip to change (such as those handling 'waitforblockheight' RPC calls or Qt wallet wait methods) would not be woken up. This delayed shutdown until those calls hit their timeouts. The fix moves the wake-up signal so it fires during any shutdown path, not just the 'stop' RPC path. It is a reliability/usability fix, not a security vulnerability that can be exploited by an attacker.
No urgent security action required. This is a shutdown-resilience improvement. Operators and downstream packagers should include it in routine updates to avoid delayed shutdowns, especially for GUI users and automated RPC workflows that rely on clean interruption.
Security signals we found
Shutdown hang / denial-of-service against local operator
Condition variable not signaled on SIGTERM/Ctrl-C shutdown path
Qt shutdown hang regression fix referenced (#18452)
Functional test updated to assert interruption instead of timeout
Evidence from the diff
The change moves the notification of node.notifications->m_tip_block_cv from the shutdown_request lambda in InitContext() to Interrupt(NodeContext&). This ensures the condition variable is signaled on all shutdown paths: Ctrl-C/SIGTERM, GUI shutdown, and the ‘stop’ RPC. The node/interfaces.cpp changes remove a duplicate Interrupt() call from appShutdown() and replace the ad-hoc InterruptRPC()/StopRPC() logic in startShutdown() with a single Interrupt(*m_context) call, centralizing shutdown interruption. The functional test is updated to assert that waitforblockheight returns promptly with the current height after SIGTERM/BREAK, instead of expecting an RPC timeout.
Changed components
src/init.cppsrc/node/interfaces.cpptest/functional/feature_init.pyNode shutdown / Interrupt() pathRPC waitforblockheightIPC waitTipChangedQt shutdownInspect captured patch +8 / −19
diff --git a/src/init.cpp b/src/init.cpp
index 2f518eba..db3d4ab2 100644
--- a/src/init.cpp
+++ b/src/init.cpp
@@ -215,8 +215,6 @@ void InitContext(NodeContext& node)
node.shutdown_request = [&node] {
assert(node.shutdown_signal);
if (!(*node.shutdown_signal)()) return false;
- // Wake any threads that may be waiting for the tip to change.
- if (node.notifications) WITH_LOCK(node.notifications->m_tip_block_mutex, node.notifications->m_tip_block_cv.notify_all());
return true;
};
}
@@ -267,6 +265,8 @@ void Interrupt(NodeContext& node)
#if HAVE_SYSTEM
ShutdownNotify(*node.args);
#endif
+ // Wake any threads that may be waiting for the tip to change.
+ if (node.notifications) WITH_LOCK(node.notifications->m_tip_block_mutex, node.notifications->m_tip_block_cv.notify_all());
InterruptHTTPServer();
InterruptHTTPRPC();
InterruptRPC();
diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp
index fd3fa226..27db0467 100644
--- a/src/node/interfaces.cpp
+++ b/src/node/interfaces.cpp
@@ -132,7 +132,6 @@ public:
}
void appShutdown() override
{
- Interrupt(*m_context);
Shutdown(*m_context);
}
void startShutdown() override
@@ -141,12 +140,7 @@ public:
if (!(Assert(ctx.shutdown_request))()) {
LogError("Failed to send shutdown signal\n");
}
-
- // Stop RPC for clean shutdown if any of waitfor* commands is executed.
- if (args().GetBoolArg("-server", false)) {
- InterruptRPC();
- StopRPC();
- }
+ Interrupt(*m_context);
}
bool shutdownRequested() override { return ShutdownRequested(*Assert(m_context)); };
bool isSettingIgnored(const std::string& name) override
diff --git a/test/functional/feature_init.py b/test/functional/feature_init.py
index d8a773bb..0fa99470 100755
--- a/test/functional/feature_init.py
+++ b/test/functional/feature_init.py
@@ -12,7 +12,6 @@ import signal
import subprocess
import time
-from test_framework.authproxy import JSONRPCException
from test_framework.test_framework import BitcoinTestFramework
from test_framework.test_node import (
BITCOIN_PID_FILENAME_DEFAULT,
@@ -251,9 +250,8 @@ class InitTest(BitcoinTestFramework):
terminal. (This can be different than the node shutdown sequence that
happens when the stop RPC is sent.)
- Currently when the break signal is sent, it does not interrupt the
- waitforblockheight RPC call, and the node does not exit until it times
- out."""
+ The waitforblockheight call should be interrupted and return right away,
+ and not time out."""
self.log.info("Testing waitforblockheight RPC call followed by break signal")
node = self.nodes[0]
@@ -287,12 +285,9 @@ class InitTest(BitcoinTestFramework):
node.process.send_signal(signal.SIGTERM)
node.process.wait()
- try:
- result = fut.result()
- raise Exception(f"waitforblockheight returned {result!r}")
- except JSONRPCException as e:
- self.log.debug(f"waitforblockheight raised {e!r}")
- assert_equal(e.error['code'], -344) # -344 is RPC timeout
+ result = fut.result()
+ self.log.debug(f"waitforblockheight returned {result!r}")
+ assert_equal(result["height"], current_height)
node.wait_until_stopped()
def run_test(self):
Why this scored 28/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.