fix(cron): do not treat directories as unsafe lifecycle scripts (#86753)
Docker Desktop writes fpath=(~/.docker/completions ...) into .zshrc. The referenced-script walk then opened that directory, saw a non-regular file, and fail-closed — blocking source ~/.zshrc on every terminal command. Directories are not scripts; devices stay fail-closed.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user