fix(guard): handle binary executables in lifecycle script scan - #77233
fix(guard): handle binary executables in lifecycle script scan#77233mohitagrawal-marvis wants to merge 1 commit into
Conversation
Commands referencing a binary executable by path (e.g. a venv
interpreter like .venv/bin/python) crashed the gateway lifecycle
guard with ValueError('embedded null byte'): the referenced file was
read and decoded, then its content tokenized as shell text, and
Path.resolve() blew up on NUL bytes in the resulting tokens. Every
terminal call touching such a binary was blocked with a hard error.
Fix in two parts:
- _read_referenced_script strips NUL bytes after decoding, so binary
content can still be scanned as text without poisoning path
resolution. The scan still runs, preserving the fail-closed
guarantee that a crafted binary cannot hide a lifecycle command
behind non-UTF-8 bytes.
- _contains_unsafe_gateway_action also catches ValueError from
Path.resolve() as defense-in-depth, treating it like OSError.
Adds regression tests: a NUL-byte binary executable passes through
cleanly, while a binary embedding the gateway restart command is
still blocked.
|
Acknowledging the overlap flagged in triage — #76797 is the better fix, closing mine in favor of it. Comparison:
Why #76797's scan-selection policy is more correct than mine: a binary executable is never interpreted by a shell, so scanning its bytes for a lifecycle command string produces only false positives (my Closing this PR in favor of #76797 to reduce maintainer selection burden. My regression tests are redundant with theirs ( |
|
Closing in favor of #76797 (superset fix). Details in my analysis comment above. |
Summary
The gateway lifecycle guard (
cron/lifecycle_guard.py) crashes withValueError: embedded null bytewhenever a terminal command references a binary executable by path — e.g..venv/bin/python -m src.mainor/usr/local/opt/python@3.14/bin/python3.14 --version. The guard reads the referenced file, decodes it, tokenizes the content as shell text, andPath.resolve()blows up on NUL bytes found in the binary. The result: legitimate commands get hard-blocked with a confusing error, and any workflow that shells out to a venv interpreter breaks.I hit this immediately after upgrading to the version that introduced the referenced-script scanning — the very first command referencing the events-scraper's venv python crashed the guard.
Root Cause
_iter_referenced_shell_scriptstreats any executable containing/(or ending in.sh/.bash/.zsh) as a script to scan./), so the guard opens and reads the binary.data.decode("utf-8", errors="replace")keeps NUL bytes intact (they're valid UTF-8 code points)._iter_referenced_shell_scriptsyields tokenized "paths" containing\x00, andPath.resolve()raisesValueError("embedded null byte").OSErrorwas caught, so theValueErrorpropagated up and killed the terminal call.Fix
Two changes, both in
cron/lifecycle_guard.py:_read_referenced_script— strip NUL bytes from the decoded text before returning it. Binary content can then still be scanned as text (preserving the fail-closed guarantee that a crafted binary can't hide a lifecycle command behind non-UTF-8 bytes) without poisoning downstream path resolution._contains_unsafe_gateway_action— also catchValueErrorfromPath.resolve()alongsideOSError, as defense-in-depth for any other path that could smuggle a NUL byte in.Test Plan
Added regression tests to
tests/hermes_cli/test_gateway_restart_loop.py:test_binary_executable_referenced_in_command_does_not_crash— a NUL-byte-laden binary executable (ELF-ish signature) passes through the terminal tool cleanly (exit 0, command executed).test_binary_executable_hiding_lifecycle_command_still_blocked— a binary embeddinghermes gateway restartis still blocked (fail-closed preserved).Manually verified all four behavior classes against the patched module:
Repro (before fix)