From 25851957d099f1f91056063feea09d551c158db8 Mon Sep 17 00:00:00 2001 From: Ivan Shcheklein Date: Tue, 4 Aug 2026 17:22:04 -0700 Subject: [PATCH] tests: fix command-queue race in mock-ssh-server exec handling Handler.run() creates the per-channel command queue with a check-then-assign while paramiko's transport thread creates it with setdefault() and puts the command into it from check_channel_exec_request(). When the exec request lands inside the window, run() replaces the queue that already holds the command, handle_client() blocks on Queue.get() forever and the client never receives an exit status. This is the intermittent CI hang: every _execute()-based operation (cp_file, checksum, the move fallback) rolls these dice, and test_concurrency_for_raw_commands rolls them 16 at a time. Confirmed by the faulthandler dump from the first run with #71 merged (handle_client threads parked on Queue.get with the client waiting), and reproduced deterministically by widening the window with a 5ms sleep: upstream logic deadlocks on the first round, the same logic with an atomic setdefault() survives, with or without the delay. mock-ssh-server is unmaintained, so the method is patched in conftest instead of upstream. Co-Authored-By: Claude Fable 5 --- tests/conftest.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..cd5eba3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,28 @@ +import threading +from queue import Queue + +import mockssh.server + + +def _handler_run(self): + # Identical to mockssh.server.Handler.run except that the command + # queue is created atomically. Upstream checks `chanid not in + # command_queues` and then assigns a fresh Queue, while paramiko's + # transport thread may concurrently create the queue and put the + # command into it via check_channel_exec_request(); the assignment + # then replaces that queue, the command is lost, and handle_client + # blocks on Queue.get() forever -- the client never receives an + # exit status. mock-ssh-server is unmaintained, so it is patched + # here instead of upstream. + self.transport.start_server(server=self) + while True: + channel = self.transport.accept() + if channel is None: + break + self.command_queues.setdefault(channel.chanid, Queue()) + thread = threading.Thread(target=self.handle_client, args=(channel,)) + thread.daemon = True + thread.start() + + +mockssh.server.Handler.run = _handler_run