From 05e35572259636fd202f8840ba3d9a1a8f832402 Mon Sep 17 00:00:00 2001 From: Yuren0312 <168316868+Yuren0312@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:46:23 +0800 Subject: [PATCH] fix(shells): decode background-shell output with errors=replace to avoid pipe deadlock A command that writes non-UTF-8 bytes to stdout makes the reader thread raise UnicodeDecodeError (text=True defaults to strict decoding). The reader stops draining the pipe, the child blocks in write() once the 64KB buffer fills, and _reader's final proc.wait() hangs forever, freezing background shells and security scanners. Decode with errors="replace" so invalid bytes become U+FFFD and the reader keeps consuming the pipe until EOF. Fixes #13 --- openhack/shells.py | 1 + tests/test_shells.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/openhack/shells.py b/openhack/shells.py index 77405b9..48aa232 100644 --- a/openhack/shells.py +++ b/openhack/shells.py @@ -94,6 +94,7 @@ def spawn(self, command: str, cwd: Optional[str] = None, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + errors="replace", # non-UTF-8 output must not crash the reader and deadlock the pipe bufsize=1, cwd=cwd or os.getcwd(), env=env, diff --git a/tests/test_shells.py b/tests/test_shells.py index 629fb1e..0364740 100644 --- a/tests/test_shells.py +++ b/tests/test_shells.py @@ -69,3 +69,21 @@ def test_ids_are_sequential_and_listed(): ids = [mgr.spawn("true") for _ in range(3)] assert ids == ["sh1", "sh2", "sh3"] assert {s.id for s in mgr.list()} == {"sh1", "sh2", "sh3"} + + +def test_non_utf8_output_does_not_deadlock(): + # Regression for #13: a command that emits invalid UTF-8 bytes must not + # crash the reader thread and deadlock on a full stdout pipe (the child + # blocks in write() while the reader stops draining, and wait() hangs). + mgr = ShellManager() + sid = mgr.spawn( + "python3 -c 'import sys; sys.stdout.buffer.write(b\"\\xff\" * 200000)'" + ) + sh = mgr.get(sid) + assert _wait(lambda: sh.status == "exited", timeout=5.0), ( + "shell deadlocked on non-UTF-8 output" + ) + assert sh.returncode == 0 + # the invalid bytes are decoded as replacement characters, not dropped + assert any("\ufffd" in line for line in sh.tail(20)) +