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
22 changes: 19 additions & 3 deletions cron/lifecycle_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,12 +321,28 @@ def _contains_unsafe_gateway_action(
if resolved in visited:
continue
visited.add(resolved)
script_text, unsafe = _read_referenced_script(script_path)
try:
script_text, unsafe = _read_referenced_script(script_path)
except (OSError, ValueError):
# A NUL-laden path tokenized from binary content must never
# crash the guard (mirrors the resolve() guard above).
script_text, unsafe = None, False
if unsafe:
return True
if script_text is None and read_remote_script is not None:
# Local path missing; try the remote backend if one is available.
script_text = read_remote_script(str(script_path))
# Only fall back to the remote backend when the local path is
# genuinely absent. _read_referenced_script deliberately returns
# None for NUL-skipped binaries ("nothing to scan") — treating
# that as "missing" made the guard `cat` the entire binary
# through the remote fallback and recursively scan machine code
# for minutes before crashing with ValueError: embedded null
# byte (same family as #76762, via the env.execute path).
try:
_local_missing = not resolved.exists() or not resolved.is_file()
except (OSError, ValueError):
_local_missing = True
if _local_missing:
script_text = read_remote_script(str(script_path))
if not script_text:
continue
# Relative references inside a script resolve against that script's
Expand Down
53 changes: 53 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,59 @@ def test_absolute_path_binary_does_not_crash_guard(self):
)
assert result is False

def test_existing_binary_path_does_not_trigger_remote_fallback(self, tmp_path):
"""A command referencing an existing binary by absolute path must not
invoke the remote ``cat`` fallback.

Regression: ``_read_referenced_script`` returns None for NUL-skipped
binaries ("nothing to scan"), and that None used to be misread as
"path missing" — so with a ``read_remote_script`` supplied (as
terminal_tool always does), the guard `cat`-ed the whole binary via
the environment and recursively scanned machine code for minutes
before crashing with ``ValueError: embedded null byte``. The fallback
must only fire when the local path is genuinely absent.
"""
from cron.lifecycle_guard import (
contains_gateway_lifecycle_command_or_referenced_script,
)
binary = tmp_path / "tool.bin"
binary.write_bytes(b"\x7fELF\x00\x01\x02\x03") # NUL bytes -> binary
calls = []

def spy(path):
calls.append(path)
return None

result = contains_gateway_lifecycle_command_or_referenced_script(
f"{binary} --version",
cwd=str(tmp_path),
read_remote_script=spy,
)
assert result is False
assert calls == []

def test_missing_path_still_uses_remote_fallback(self, tmp_path):
"""A genuinely absent path still goes to the remote backend — the
fallback exists for SSH/Modal/Daytona backends where the same path is
remote, and must keep working."""
from cron.lifecycle_guard import (
contains_gateway_lifecycle_command_or_referenced_script,
)
missing = tmp_path / "no-such-script.sh"
calls = []

def spy(path):
calls.append(path)
return None

result = contains_gateway_lifecycle_command_or_referenced_script(
str(missing),
cwd=str(tmp_path),
read_remote_script=spy,
)
assert result is False
assert calls == [str(missing)]

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
13 changes: 12 additions & 1 deletion tools/terminal_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2549,10 +2549,21 @@ def _read_script_in_env(script_path: str) -> Optional[str]:
except Exception:
pass
# Remote / sandboxed backend: read via the environment's shell.
# Cap the size and skip binary output: a NUL byte in the first
# chunk means an ELF/Mach-O binary, not a shell script. Without
# this, a command referencing a large binary by path made the
# guard `cat` the whole file through this fallback and
# recursively scan machine code for minutes before crashing
# with ValueError: embedded null byte.
try:
result = env.execute(f"cat {shlex.quote(script_path)}")
if result.get("returncode", -1) == 0:
return result.get("output", "")
_out = result.get("output", "")
if len(_out) > 1024 * 1024:
return None
if "\x00" in _out[:4096]:
return None
return _out
except Exception:
pass
return None
Expand Down