From fce5c5ec6aa0c69e7d216b12f1ec0b06ffef4099 Mon Sep 17 00:00:00 2001 From: Robert Baldyga Date: Wed, 21 Oct 2026 13:34:23 +0200 Subject: [PATCH] ssh_executor: Execute commands without creating stdin file SSHClient.exec_command() returns a stdin file object, which _execute() and is_active() never use. When it is garbage collected, its close() calls Channel.shutdown_write(), which builds the EOF message under the channel lock but sends it only after releasing the lock. If the transport thread handles the server's channel CLOSE in that window, it sends EOF and CLOSE of its own, and our EOF reaches the server after the CLOSE. OpenSSH sshd (seen with 10.2) treats this as a protocol error: channel_by_id: 1: bad id: channel free Disconnecting user root ...: ieof packet referred to nonexistent channel 1 and drops the whole connection, killing every command running on it. With several threads sharing one connection, this shows up as random commands failing with exit code -1. Open the session channel directly instead, run the command on it and read only stdout and stderr, so no stdin file object is ever created. Close the channel explicitly once the command completes; Channel.close() sends EOF and CLOSE together, in order, and only if the channel is not closed already. Signed-off-by: Robert Baldyga --- connection/ssh_executor.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/connection/ssh_executor.py b/connection/ssh_executor.py index beaf415..89ca88a 100644 --- a/connection/ssh_executor.py +++ b/connection/ssh_executor.py @@ -105,17 +105,26 @@ def disconnect(self): except Exception: raise Exception(f"An exception occurred while trying to disconnect from {self.host}") + def _exec_command(self, command, timeout): + channel = self.ssh.get_transport().open_session(timeout=timeout) + channel.settimeout(timeout) + channel.exec_command(command) + return channel + def _execute(self, command, timeout): try: - (stdin, stdout, stderr) = self.ssh.exec_command( - command, timeout=timeout.total_seconds() - ) + channel = self._exec_command(command, timeout.total_seconds()) except paramiko.SSHException as e: raise ConnectionError( f"An exception occurred while executing command '{command}' on" f" {self.host}\n{e}" ) - return Output(stdout.read(), stderr.read(), stdout.channel.recv_exit_status()) + try: + stdout = channel.makefile("r").read() + stderr = channel.makefile_stderr("r").read() + return Output(stdout, stderr, channel.recv_exit_status()) + finally: + channel.close() def _rsync( self, @@ -200,7 +209,7 @@ def reboot(self): def is_active(self): try: - self.ssh.exec_command("", timeout=5) + self._exec_command("", timeout=5).close() return True except Exception: return False