fix: never crash lifecycle guard on NUL-byte paths from binaries - #79038
fix: never crash lifecycle guard on NUL-byte paths from binaries#79038etc wants to merge 1 commit into
Conversation
A terminal command referencing a binary executable (any executable whose path contains a slash, e.g. `uv pip install ...` or `python -c ...`) crashed the gateway lifecycle guard with `ValueError: embedded null byte` from os.open. The local scan deliberately skips binaries by their NUL bytes, but the remote-read fallback then re-fetched the raw bytes as text; tokenizing machine code produced NUL-byte paths that the guard's os.open call did not survive. The NousResearch#76762 fix guarded Path.resolve but missed os.open. - Catch ValueError in _read_referenced_script's os.open: a guarded path must never crash the guard - Return "" instead of None when a referenced file is a binary, so the remote-read fallback is not triggered for content that was deliberately skipped as non-script - Regression tests: NUL-byte path read, binary skip contract, binary referenced script passes the guard, binary remote content does not crash it Related: NousResearch#76762, NousResearch#30719
|
Note: This PR was created by my Hermes agent using DeepSeek V4 Flash 0731. It fixes a bug that kept slowing it down on various coding tasks. |
|
Closing as superseded by #80258, which fixes this whole bug class architecturally rather than per-callsite: path candidates are sanitized once at the ingestion boundary (NUL/empty/unexpandable tokens rejected before any OS call), text from any Your report and fix targeted a real member of this class — thank you. The per-callsite patches kept leaving sibling frames exposed (#76762 → #77703 → #77780 → #78256 each crashed one frame away from the previous fix), which is why we went with the boundary fix instead of merging the fragments individually. #80258 carries regression tests for the NUL-path, binary-callback, oversized-read, unset-HOME, and walk-crash cases plus an adversarial never-raises sweep. |
Summary
cron/lifecycle_guard.py— the gateway lifecycle guard — crashes withValueError: embedded null bytewhen a terminal command references a binary executable (any executable whose path contains a slash). Inside a gateway process this takes down every guarded terminal call, e.g.uv pip install ...orpython -c ..., with the raw traceback surfaced as the tool error.Root cause
_iter_referenced_shell_scriptstreats any executable containing a/as a script to scan._read_referenced_scriptreads the first chunk and deliberately skips binaries (NUL bytes) by returning(None, False)._contains_unsafe_gateway_actioninterpretsscript_text is Noneas "file missing" and callsread_remote_script— which re-fetches the file's raw bytes as text (via thecatfallback in_read_script_in_env).Path, and_read_referenced_script'sos.open(path)raisesValueError: embedded null byte— uncaught, because the existing terminal tool: lifecycle_guard crashes on absolute-path executables (ValueError: embedded null byte), blocks all such commands #76762 guard only wrappedPath.resolve().Fix
os.opennow catches(OSError, ValueError)and returns(None, False)— a guarded path must never crash the guard.("", False)instead of(None, False), so the remote-read fallback (keyed onscript_text is None) is never triggered for content that was deliberately skipped as non-script.Verification
tests/hermes_cli/test_gateway_restart_loop.py, including 4 new regression tests:""(notNone)python -c "print(1)"anduv pip listpreviously crashed with the ValueError; after the fix they execute normally, while blocking ofhermes gateway restart/launchctl submitstill holds.Related: #76762 (a guarded path must never crash the guard), #30719 (original restart-loop defense).