Merge bitcoin/bitcoin#36260: torcontrol: Use reconnect backoff after dropped connections
What changed, and why it matters
This change fixes a bug in how Bitcoin Core reconnects to the Tor control port. A previous update accidentally removed the wait time between reconnect attempts when an already-established Tor control connection was dropped. Without the wait, Bitcoin Core would rapidly reconnect in a tight loop, creating a flood of connection attempts and log messages. The fix restores the intended pause between reconnect attempts. It is primarily a denial-of-service/operational-stability issue for the local node, not a remote code execution or theft vulnerability.
No immediate emergency action is required. Users and node operators running Tor-enabled Bitcoin Core should plan to update to a release containing this fix to avoid log/control-port noise and minor resource exhaustion if the Tor control connection is repeatedly dropped. Operators can also review Tor control port credentials to reduce AUTHENTICATE failures.
Security signals we found
Uncontrolled retry loop causing resource exhaustion and log flooding
Local-only Tor control port interaction; no remote attacker path by default
Regression introduced by prior refactor (#34158) and restored here
Functional test added to prevent future regression
Evidence from the diff
In src/torcontrol.cpp, the Tor control thread’s main loop was restructured so that every disconnect path now calls disconnected_cb(). That callback now performs the exponential-backoff sleep before the next reconnect attempt. Previously, after a dropped connection (e.g., Tor closing the socket after a failed AUTHENTICATE), the loop would immediately reconnect because the backoff logic was only inside the initial Connect() failure branch. A functional test was added to verify that reconnects wait at least the initial backoff timeout.
Changed components
src/torcontrol.cppTorController::ThreadControl()TorController::disconnected_cb()test/functional/feature_torcontrol.pyInspect captured patch +32 / −33
### src/torcontrol.cpp
@@ -391,42 +391,21 @@ void TorController::ThreadControl()
LogDebug(BCLog::TOR, "Entering Tor control thread");
while (!m_interrupt) {
- // Try to connect if not connected already
- if (!m_conn.IsConnected()) {
- LogDebug(BCLog::TOR, "Attempting to connect to Tor control port %s", m_tor_control_center);
-
- if (!m_conn.Connect(m_tor_control_center)) {
- LogWarning("tor: Initiating connection to Tor control port %s failed", m_tor_control_center);
- if (!m_reconnect) {
- break;
- }
- // Wait before retrying with exponential backoff
- LogDebug(BCLog::TOR, "Retrying in %.1f seconds", m_reconnect_timeout.count());
- if (!m_interrupt.sleep_for(std::chrono::duration_cast<std::chrono::milliseconds>(m_reconnect_timeout))) {
+ LogDebug(BCLog::TOR, "Attempting to connect to Tor control port %s", m_tor_control_center);
+ if (m_conn.Connect(m_tor_control_center)) {
+ connected_cb(m_conn);
+ while (!m_interrupt) {
+ if (!m_conn.WaitForData(std::chrono::seconds(1))) {
+ if (m_conn.IsConnected()) continue;
+ LogDebug(BCLog::TOR, "Lost connection to Tor control port");
break;
}
- m_reconnect_timeout = std::min(m_reconnect_timeout * RECONNECT_TIMEOUT_EXP, RECONNECT_TIMEOUT_MAX);
- continue;
- }
- // Successfully connected, reset timeout and trigger connected callback
- m_reconnect_timeout = RECONNECT_TIMEOUT_START;
- connected_cb(m_conn);
- }
- // Wait for data with a timeout
- if (!m_conn.WaitForData(std::chrono::seconds(1))) {
- // Check if still connected
- if (!m_conn.IsConnected()) {
- LogDebug(BCLog::TOR, "Lost connection to Tor control port");
- disconnected_cb(m_conn);
- continue;
+ if (!m_conn.ReceiveAndProcess()) break;
}
- // Just a timeout, continue waiting
- continue;
- }
- // Process incoming data
- if (!m_conn.ReceiveAndProcess()) {
- disconnected_cb(m_conn);
+ } else {
+ LogWarning("tor: Initiating connection to Tor control port %s failed", m_tor_control_center);
}
+ disconnected_cb(m_conn);
}
LogDebug(BCLog::TOR, "Exited Tor control thread");
}
@@ -737,8 +716,12 @@ void TorController::disconnected_cb(TorControlConnection& _conn)
if (!m_reconnect)
return;
- LogDebug(BCLog::TOR, "Not connected to Tor control port %s, will retry", m_tor_control_center);
+ LogDebug(BCLog::TOR, "Not connected to Tor control port %s, retrying in %.2f s",
+ m_tor_control_center, m_reconnect_timeout.count());
_conn.Disconnect();
+
+ m_interrupt.sleep_for(std::chrono::duration_cast<std::chrono::milliseconds>(m_reconnect_timeout));
+ m_reconnect_timeout = std::min(m_reconnect_timeout * RECONNECT_TIMEOUT_EXP, RECONNECT_TIMEOUT_MAX);
}
fs::path TorController::GetPrivateKeyFile()
### test/functional/feature_torcontrol.py
@@ -131,6 +131,8 @@ def expect_disconnect(self, expect, mock_tor):
yield
if expect:
+ # No reconnect before the initial reconnect timeout of 1s has passed
+ ensure_for(duration=0.5, f=lambda: len(mock_tor.received_commands) == initial_len)
# Expect to receive a PROTOCOLINFO 1 on reconnect, bumping the received
# commands length.
self.wait_until(lambda: len(mock_tor.received_commands) == initial_len + 1)
@@ -256,12 +258,26 @@ def test_overmany_lines(self):
mock_tor.stop()
+ def test_reconnect_backoff(self):
+ self.log.info("Test that a connection closed by Tor is re-established with backoff")
+
+ mock_tor = MockTorControlServer(self.next_port(), manual_mode=True)
+ self.restart_with_mock(mock_tor)
+
+ with self.expect_disconnect(True, mock_tor):
+ # Reply before closing, like Tor does after a failed AUTHENTICATE
+ mock_tor.send_raw("515 Authentication failed\r\n")
+ mock_tor.conn.shutdown(socket.SHUT_WR)
+
+ mock_tor.stop()
+
def run_test(self):
self.test_basic()
self.test_partial_data()
self.test_pow_fallback()
self.test_oversized_line()
self.test_overmany_lines()
+ self.test_reconnect_backoff()
if __name__ == '__main__':Why this scored 30/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.