fix(cron): skip binary content returned by remote script readers - #78067
fix(cron): skip binary content returned by remote script readers#78067dvbaecker wants to merge 1 commit into
Conversation
The gateway lifecycle guard's terminal-tool reader falls back to a backend
shell read ("cat ...") when a referenced file exceeds the 1MB local read
limit. The guard then scanned the decoded binary content, feeding junk path
tokens into its recursion — crashing with ValueError (embedded null byte)
from os.open and fail-closed blocking innocent commands such as
`/.../venv/bin/python --version` inside the gateway (NousResearch#76510).
Treat NUL-byte content from the remote reader exactly like the local binary
skip from NousResearch#76762 (a binary is not a referenced shell script), and catch
ValueError on os.open for junk paths tokenized out of such content.
Security invariants are preserved: direct lifecycle commands and genuine
shell scripts are still blocked (regression tests included, red on main).
Duplicate of #77729: both add the remote referenced-script fallback binary/NUL skip for the lifecycle guard. This PR also catches the resulting invalid-path ValueError, but the primary repair mechanism is already open there. |
|
Thanks for the triage. After reading #77729 carefully, I confirm the core repair (skipping binary content from the remote-read fallback) and the 1. Fix placement. #77729 applies the NUL skip inside 2. Test coverage. This PR adds 7 regression tests including the security invariants: a genuine lifecycle command is still blocked directly and via a referenced script, both through the local reader and through the remote reader. Those guard-still-blocks assertions are not covered by #77729's two crash-focused tests. Happy to close this in favor of #77729 and contribute the chokepoint placement + invariant tests there if that is cleaner for review. Deferring to maintainer preference. |
|
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. |
What does this PR do?
Fixes a false-positive block (and an uncaught crash) in the gateway lifecycle guard when a terminal command executes a large binary by absolute path from inside the gateway (
_HERMES_GATEWAY=1).When a referenced file exceeds the 1MB local read limit (
_MAX_REFERENCED_SCRIPT_BYTES), the terminal tool's reader falls back to a backend shell read (cat ...) and hands the guard the entire binary as text. The guard then scanned that decoded machine code: junk tokens tokenized out of it (which contain/and NUL bytes) were fed into the referenced-script recursion, causing:ValueError: embedded null bytefromos.openon junk path tokens./.../venv/bin/python --versionreturn "Blocked: command or referenced script cannot restart or stop the gateway..." even though they are harmless.This is the residual half of the fix from #76762: that patch added the NUL-byte binary skip to the local read path (
_read_referenced_script), but the remote-reader content path was left unguarded, and theos.openthere only caughtOSError, notValueError.The fix treats NUL-byte content returned by the remote reader exactly like the existing local binary skip — a binary is never a referenced shell script — and catches
ValueErroronos.openfor junk paths. The security boundary must be immune to whatever the reader callback returns (local, SSH, Modal, or any other backend).Related Issue
Fixes #76510
(Follow-up hardening to #76762; that issue fixed the local read path but left the remote-reader content path and the
os.opencrash.)Type of Change
Changes Made
cron/lifecycle_guard.py:_contains_unsafe_gateway_action: skip NUL-byte content returned byread_remote_script(mirrors the local binary skip from terminal tool: lifecycle_guard crashes on absolute-path executables (ValueError: embedded null byte), blocks all such commands #76762)._read_referenced_script: catchValueErroralongsideOSErroronos.open(embedded NUL byte in junk paths tokenized from binary content).tests/cron/test_lifecycle_guard_binary_false_positive.py: new regression tests (red onmain, green with this patch).How to Test
mainwithValueError: embedded null byte, pass with this patch):_HERMES_GATEWAY=1):Blocked: command or referenced script cannot restart or stop the gateway...(or aValueErrortraceback). After: runs normally.hermes gateway restartand genuine shell scripts containing it are still blocked, both from the local reader and via the remote reader.Verified with the full cron + gateway suite:
pytest tests/cron/ tests/hermes_cli/test_gateway_restart_loop.py tests/hermes_cli/test_gateway_service.py tests/tools/test_terminal_none_command_guard.py→ 551 passed.Full suite (
pytest tests/ -q --ignore=tests/docker): 1897 passed. One pre-existing, order-dependent failure intests/agent/test_file_safety.py::TestCacheFileReadBlocking::test_hub_index_cache_blocked— verified to fail identically on unmodifiedmainin the same run order (passes in isolation); unrelated to this change.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — docstring comments explain the NUL-skip and reference both issuescli-config.yaml.exampleif I added/changed config keys — N/A (no config changes)CONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/AScreenshots / Logs
Red on
main(unpatched guard):Green with this patch:
Production scenario (21MB venv interpreter returned by the backend reader):