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
10 changes: 9 additions & 1 deletion cron/lifecycle_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,14 @@ def _read_referenced_script(path: Path) -> tuple[Optional[str], bool]:
descriptor = os.open(path, flags)
except OSError:
return None, False
except ValueError:
# A path the OS cannot even represent — an embedded NUL byte from a
# token parsed out of scanned content (#78256). #76762 covered the
# NUL-in-*contents* case below and Path.resolve, but ``os.open``
# itself raises ValueError (not OSError) for NUL-in-*path*, which
# crashed the whole guard instead of failing open. Same semantics as
# an unreadable file: nothing to scan.
return None, False
try:
metadata = os.fstat(descriptor)
if not stat.S_ISREG(metadata.st_mode):
Expand All @@ -268,7 +276,7 @@ def _read_referenced_script(path: Path) -> tuple[Optional[str], bool]:
# chunk tells us if this is a binary (NUL bytes) that should be
# skipped as "nothing to scan" rather than failing closed (#76762).
data = os.read(descriptor, _MAX_REFERENCED_SCRIPT_BYTES + 1)
except OSError:
except (OSError, ValueError):
return None, False
finally:
os.close(descriptor)
Expand Down
40 changes: 40 additions & 0 deletions tests/hermes_cli/test_gateway_restart_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -857,3 +857,43 @@ def test_cron_nested_wrapper_script_is_scanned(self, tmp_path, capsys, monkeypat
assert rc == 1
out = capsys.readouterr().out
assert "Blocked" in out


class TestNulByteReferencedPathFailsOpen:
"""#78256: a referenced-script path carrying an embedded NUL byte must be
skipped ("nothing to scan"), not crash the guard. #76762 established the
fail-open contract for NUL bytes in scanned *contents* and Path.resolve,
but ``os.open()`` raises ValueError (not OSError) for NUL in the *path*
itself — which escaped the OSError-only handler and took down the whole
terminal tool for commands like ``python -m pip --version`` whose
recursive scan produced such a token."""

def test_read_referenced_script_nul_path_fails_open(self):
from pathlib import Path
from cron.lifecycle_guard import _read_referenced_script

text, unsafe = _read_referenced_script(Path("x\x00y"))
assert text is None
assert unsafe is False

def test_guard_survives_nul_byte_in_referenced_script_path(self):
from cron.lifecycle_guard import (
contains_gateway_lifecycle_command_or_referenced_script,
)

# A sourced path with an embedded NUL: previously ValueError from
# os.open crashed the guard (and the terminal tool call around it).
assert contains_gateway_lifecycle_command_or_referenced_script(
"source /tmp/e\x00vil.sh"
) is False

def test_guard_still_blocks_real_lifecycle_script(self, tmp_path):
from cron.lifecycle_guard import (
contains_gateway_lifecycle_command_or_referenced_script,
)

script = tmp_path / "restart.sh"
script.write_text("#!/bin/sh\nhermes gateway restart\n")
assert contains_gateway_lifecycle_command_or_referenced_script(
f"bash {script}"
) is True