test: Add test for partial message handling in torcontrol
What changed, and why it matters
This commit only adds a new automated test for Bitcoin Core's Tor control feature. It checks that the software correctly waits for a complete message before acting, rather than reacting to a partial message. There is no change to the actual Bitcoin Core program code that users run.
No security action needed; this is a test-only addition. Reviewers may optionally confirm the underlying Tor control buffering logic already exists in production code if they want to map the test to a prior fix.
Security signals we found
Tests partial-message buffering behavior in Tor control protocol parsing
No modification to production parsing or networking code
Evidence from the diff
The diff modifies test/functional/feature_torcontrol.py, adding a test case (test_partial_data) that uses a mock Tor control server to send a fragmented PROTOCOLINFO response. The test verifies that bitcoind buffers the partial data and only sends AUTHENTICATE after the response is completed with a final CRLF. No production code in src/ is changed.
Changed components
test/functional/feature_torcontrol.pyInspect captured patch +64 / −9
diff --git a/test/functional/feature_torcontrol.py b/test/functional/feature_torcontrol.py
index 6efdafe3..dfc649a6 100755
--- a/test/functional/feature_torcontrol.py
+++ b/test/functional/feature_torcontrol.py
@@ -3,20 +3,26 @@
# Distributed under the MIT software license, see the accompanying
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
"""Test torcontrol functionality with a mock Tor control server."""
-
import socket
import threading
from test_framework.test_framework import BitcoinTestFramework
-from test_framework.util import assert_equal, p2p_port
+from test_framework.util import (
+ assert_equal,
+ ensure_for,
+ p2p_port,
+)
class MockTorControlServer:
- def __init__(self, port):
+ def __init__(self, port, manual_mode=False):
self.port = port
self.sock = None
+ self.conn = None
self.running = False
self.thread = None
self.received_commands = []
+ self.manual_mode = manual_mode
+ self.conn_ready = threading.Event()
def start(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -31,6 +37,8 @@ class MockTorControlServer:
def stop(self):
self.running = False
+ if self.conn:
+ self.conn.close()
if self.sock:
self.sock.close()
if self.thread:
@@ -39,9 +47,10 @@ class MockTorControlServer:
def _serve(self):
while self.running:
try:
- conn, _ = self.sock.accept()
- conn.settimeout(1.0)
- self._handle_connection(conn)
+ self.conn, _ = self.sock.accept()
+ self.conn.settimeout(1.0)
+ self.conn_ready.set()
+ self._handle_connection(self.conn)
except socket.timeout:
continue
except OSError:
@@ -61,13 +70,18 @@ class MockTorControlServer:
command = line.decode('utf-8').strip()
if command:
self.received_commands.append(command)
- response = self._get_response(command)
- conn.sendall(response.encode('utf-8'))
+ if not self.manual_mode:
+ response = self._get_response(command)
+ conn.sendall(response.encode('utf-8'))
except socket.timeout:
continue
finally:
conn.close()
+ def send_raw(self, data):
+ if self.conn:
+ self.conn.sendall(data.encode('utf-8'))
+
def _get_response(self, command):
if command == "PROTOCOLINFO 1":
return (
@@ -93,7 +107,7 @@ class TorControlTest(BitcoinTestFramework):
def set_test_params(self):
self.num_nodes = 1
- def run_test(self):
+ def test_basic(self):
self.log.info("Test Tor control basic functionality")
tor_control_port = p2p_port(self.num_nodes + 1)
mock_tor = MockTorControlServer(tor_control_port)
@@ -117,6 +131,47 @@ class TorControlTest(BitcoinTestFramework):
# Clean up
mock_tor.stop()
+ def test_partial_data(self):
+ self.log.info("Test that partial Tor control responses are buffered until complete")
+
+ tor_port = p2p_port(self.num_nodes + 2)
+ mock_tor = MockTorControlServer(tor_port, manual_mode=True)
+ mock_tor.start()
+
+ self.restart_node(0, extra_args=[
+ f"-torcontrol=127.0.0.1:{tor_port}",
+ "-listenonion=1",
+ "-debug=tor"
+ ])
+
+ # Wait for connection and PROTOCOLINFO command
+ mock_tor.conn_ready.wait(timeout=10)
+ self.wait_until(lambda: len(mock_tor.received_commands) >= 1, timeout=10)
+ assert_equal(mock_tor.received_commands[0], "PROTOCOLINFO 1")
+
+ # Send partial response (no \r\n on last line)
+ mock_tor.send_raw(
+ "250-PROTOCOLINFO 1\r\n"
+ "250-AUTH METHODS=NULL\r\n"
+ "250 OK"
+ )
+
+ # Verify AUTHENTICATE is not sent
+ ensure_for(duration=2, f=lambda: len(mock_tor.received_commands) == 1)
+
+ # Complete the response
+ mock_tor.send_raw("\r\n")
+
+ # Should now process the complete response and send AUTHENTICATE
+ self.wait_until(lambda: len(mock_tor.received_commands) >= 2, timeout=5)
+ assert_equal(mock_tor.received_commands[1], "AUTHENTICATE")
+
+ # Clean up
+ mock_tor.stop()
+
+ def run_test(self):
+ self.test_basic()
+ self.test_partial_data()
if __name__ == '__main__':
TorControlTest(__file__).main()
Why this scored 12/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.