diff --git a/cron/lifecycle_guard.py b/cron/lifecycle_guard.py index ee73aa6639..526ca88578 100644 --- a/cron/lifecycle_guard.py +++ b/cron/lifecycle_guard.py @@ -440,6 +440,13 @@ def _read_referenced_script(path: Path) -> tuple[Optional[str], bool]: try: metadata = os.fstat(descriptor) if not stat.S_ISREG(metadata.st_mode): + # Directories are not scripts. Docker Desktop writes + # ``fpath=(~/.docker/completions …)`` into ``~/.zshrc``; the + # walk then treats that dir as a referenced script and used + # to fail-closed, blocking ``source ~/.zshrc`` (#86753). + # Devices/sockets stay fail-closed. + if stat.S_ISDIR(metadata.st_mode): + return None, False return None, True # Sniff a small prefix first: files that are clearly compiled # binaries (executable magic, or NUL bytes in the head) are never diff --git a/tests/hermes_cli/test_gateway_restart_loop.py b/tests/hermes_cli/test_gateway_restart_loop.py index b47139edba..a859ce5bbe 100644 --- a/tests/hermes_cli/test_gateway_restart_loop.py +++ b/tests/hermes_cli/test_gateway_restart_loop.py @@ -1204,11 +1204,55 @@ class TestLifecycleGuardNeverRaises: weird.write_bytes(b"\xff\xfe\x00\x01 not really a script") assert self._scan(f"bash {weird}") is False + def test_sourced_zshrc_docker_completions_dir_is_not_blocked(self, tmp_path): + """#86753: Docker Desktop writes ``fpath=(~/.docker/completions …)`` + into ``.zshrc``. Completions is a directory. The walk must treat + that as nothing-to-scan, not fail-closed, or ``source ~/.zshrc`` + is blocked on every terminal command.""" + completions = tmp_path / ".docker" / "completions" + completions.mkdir(parents=True) + zshrc = tmp_path / ".zshrc" + zshrc.write_text( + f"fpath=({completions} /usr/local/share/zsh/site-functions $fpath)\n", + encoding="utf-8", + ) + assert self._scan(f"source {zshrc}") is False + + def test_fstat_directory_mode_is_not_unsafe(self, tmp_path, monkeypatch): + """#86753 Unix contract: os.open(dir) succeeds, fstat is not S_ISREG. + + Windows raises OSError on os.open(dir) and already returns + nothing-to-scan. Linux/macOS open the directory and used to + return unsafe=True, blocking sourced zshrcs that mention + ``~/.docker/completions``. + """ + import os + import stat as statmod + + from cron.lifecycle_guard import _read_referenced_script + + probe = tmp_path / "probe" + probe.write_text("echo hi\n", encoding="utf-8") + orig = os.fstat + + def _dir_fstat(fd): + orig(fd) + class _DirStat: + st_mode = statmod.S_IFDIR | 0o755 + return _DirStat() + + monkeypatch.setattr(os, "fstat", _dir_fstat) + text, unsafe = _read_referenced_script(probe) + assert text is None + assert unsafe is False + def test_directory_and_dev_null_fail_closed_not_crash(self, tmp_path): - # Non-regular files are suspicious (fail closed = blocked), but the - # important contract is: verdict, not exception. - assert self._scan(f"bash {tmp_path}") is True - assert self._scan("bash /dev/null") is True + # Directories are not scripts (#86753). Devices stay fail-closed + # where the OS actually exposes them (POSIX /dev/null). + # The important contract is: verdict, not exception. + assert self._scan(f"bash {tmp_path}") is False + if os.name != "nt": + assert self._scan("bash /dev/null") is True def test_magic_prefix_binaries_skipped_without_full_read(self, tmp_path): """Executable magic (ELF/PE/Mach-O) short-circuits the read: the @@ -1236,8 +1280,8 @@ class TestLifecycleGuardNeverRaises: ) binary = tmp_path / "prog" binary.write_bytes(b"\x7fELF" + bytes(128)) - for value in ("nul\x00byte.sh", str(binary), "/nonexistent/x.sh"): + for value in ("nul\x00byte.sh", str(binary), "/nonexistent/x.sh", str(tmp_path)): check_gateway_lifecycle("clean prompt", value) # must not raise - for value in ("/dev/null", str(tmp_path)): + if os.name != "nt": with pytest.raises(GatewayLifecycleBlocked): - check_gateway_lifecycle("clean prompt", value) + check_gateway_lifecycle("clean prompt", "/dev/null")