test: Test SIGTERM handling during waitforblockheight call
What changed, and why it matters
This commit only adds a new automated test. The test checks that when a user presses Ctrl-C (or sends SIGTERM) while the node is handling a long-running 'waitforblockheight' RPC call, the node currently does not shut down immediately and instead waits for the RPC to time out. The commit itself does not fix the problem; it only creates test coverage so a later commit can prove the fix works.
No immediate action required for this commit. Treat it as a regression test. Review the subsequent commit that the message references to confirm the actual fix for SIGTERM handling of wait RPCs and IPC waitTipChanged calls is implemented and also covered by this test.
Security signals we found
Denial-of-service-like shutdown delay: SIGTERM/CTRL-C is ignored until a long RPC timeout expires
Test-only commit: no production code change, only regression test added
Behavioral inconsistency between stop RPC and signal-based shutdown
Evidence from the diff
The diff adds a break_wait_test() function to test/functional/feature_init.py. It starts a bitcoind node, issues a waitforblockheight RPC with a timeout twice the RPC timeout from a background thread, then sends SIGTERM (or CTRL_BREAK_EVENT on Windows) to the node. The test currently expects the RPC to time out with error code -344, documenting the existing undesirable behavior where SIGTERM does not interrupt wait calls. The commit message explicitly states this is preparatory test coverage for a behavior change in the next commit.
Changed components
test/functional/feature_init.pyInspect captured patch +56 / −0
diff --git a/test/functional/feature_init.py b/test/functional/feature_init.py
index b9d41a97..d8a773bb 100755
--- a/test/functional/feature_init.py
+++ b/test/functional/feature_init.py
@@ -3,13 +3,16 @@
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Tests related to node initialization."""
+from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import os
import platform
import shutil
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,
@@ -240,9 +243,62 @@ class InitTest(BitcoinTestFramework):
self.stop_node(0)
assert not custom_pidfile_absolute.exists()
+ def break_wait_test(self):
+ """Test what happens when a break signal is sent during a
+ waitforblockheight RPC call with a long timeout. Ctrl-Break is sent on
+ Windows and SIGTERM is sent on other platforms, to trigger the same node
+ shutdown sequence that would happen if Ctrl-C were pressed in a
+ 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."""
+
+ self.log.info("Testing waitforblockheight RPC call followed by break signal")
+ node = self.nodes[0]
+
+ if platform.system() == 'Windows':
+ # CREATE_NEW_PROCESS_GROUP prevents python test from exiting
+ # with STATUS_CONTROL_C_EXIT (-1073741510) when break is sent.
+ self.start_node(node.index, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
+ else:
+ self.start_node(node.index)
+
+ current_height = node.getblock(node.getbestblockhash())['height']
+
+ with ThreadPoolExecutor(max_workers=1) as ex:
+ # Call waitforblockheight with wait timeout longer than RPC timeout,
+ # so it is possible to distinguish whether it times out or returns
+ # early. If it times out it will throw an exception, and if it
+ # returns early it will return the current block height.
+ self.log.debug(f"Calling waitforblockheight with {self.rpc_timeout} sec RPC timeout")
+ fut = ex.submit(node.waitforblockheight, height=current_height+1, timeout=self.rpc_timeout*1000*2)
+ time.sleep(1)
+
+ self.log.debug(f"Sending break signal to pid {node.process.pid}")
+ if platform.system() == 'Windows':
+ # Note: CTRL_C_EVENT should not be sent here because unlike
+ # CTRL_BREAK_EVENT it can not be targeted at a specific process
+ # group and may behave unpredictably.
+ node.process.send_signal(signal.CTRL_BREAK_EVENT)
+ else:
+ # Note: signal.SIGINT would work here as well
+ 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
+ node.wait_until_stopped()
+
def run_test(self):
self.init_pid_test()
self.init_stress_test()
+ self.break_wait_test()
if __name__ == '__main__':
Why this scored 19/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.