Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion cron/lifecycle_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,32 @@ def _resolve_terminal_script_path(candidate: str, cwd: Optional[str]) -> Path:
return path


def _looks_like_shell_script(path: Path) -> bool:
"""True if *path* is plausibly a shell script (extension or shebang).

Absolute-path binaries (e.g. ``/usr/bin/python3``) are not scripts;
scanning their bytes is wasted work and risks false positives on binary
content (#76762). Extension-matched files always count; otherwise only
regular files whose first two bytes are a ``#!`` shebang do. Non-regular
or unreadable files fall through to the legacy scan so
``_read_referenced_script`` keeps its fails-closed behaviour.
"""
if path.suffix.lower() in {".sh", ".bash", ".zsh"}:
return True
try:
flags = os.O_RDONLY | getattr(os, "O_NONBLOCK", 0)
descriptor = os.open(path, flags)
except OSError:
return True
try:
metadata = os.fstat(descriptor)
if not stat.S_ISREG(metadata.st_mode):
return True
return os.read(descriptor, 2) == b"#!"
finally:
os.close(descriptor)


def _iter_referenced_shell_scripts(
command: str,
*,
Expand Down Expand Up @@ -226,7 +252,9 @@ def _iter_referenced_shell_scripts(
# (#77131). Skip pure-separator tokens.
if executable.strip("/"):
if "/" in executable or executable.endswith((".sh", ".bash", ".zsh")):
yield _resolve_terminal_script_path(executable, cwd)
candidate = _resolve_terminal_script_path(executable, cwd)
if _looks_like_shell_script(candidate):
yield candidate


def _iter_shell_command_payloads(command: str) -> Iterator[str]:
Expand Down
22 changes: 22 additions & 0 deletions tests/hermes_cli/test_gateway_restart_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,28 @@ def test_absolute_path_binary_does_not_crash_guard(self):
)
assert result is False

def test_absolute_path_extensionless_non_script_is_not_walked(self, tmp_path):
"""#76762 follow-up: an absolute path to a non-script file (no .sh
extension, no shebang) is not treated as a referenced shell script,
so its bytes are never read and scanned.

Before this fix the walker yielded every path containing ``/``,
reading and re-tokenizing arbitrary files (logs, data, binaries) —
wasted I/O and a false-positive source.
"""
from cron.lifecycle_guard import _iter_referenced_shell_scripts

non_script = tmp_path / "notes.txt"
non_script.write_text("hermes gateway stop\n")
walked = list(_iter_referenced_shell_scripts(str(non_script)))
assert walked == []

shebang = tmp_path / "tool" # extensionless, but a real script
shebang.write_text("#!/bin/bash\necho hi\n")
shebang.chmod(0o700)
walked = list(_iter_referenced_shell_scripts(str(shebang)))
assert walked == [shebang]

def test_shell_script_reference_walk_still_works(self, tmp_path):
"""The referenced-script walk still applies to real shell scripts:
a .sh script that itself invokes a lifecycle command is caught."""
Expand Down