Skip to content

fix(cron): lifecycle guard crashes with 'embedded null byte' instead of failing open — kills terminal tool (#78256) - #78448

Closed
ayushnangia wants to merge 1 commit into
NousResearch:mainfrom
ayushnangia:fix/lifecycle-guard-nullbyte
Closed

fix(cron): lifecycle guard crashes with 'embedded null byte' instead of failing open — kills terminal tool (#78256)#78448
ayushnangia wants to merge 1 commit into
NousResearch:mainfrom
ayushnangia:fix/lifecycle-guard-nullbyte

Conversation

@ayushnangia

Copy link
Copy Markdown
Contributor

Summary

Any terminal command whose recursive script-scan produces a path token with an embedded NUL byte crashes the entire lifecycle guard — and the terminal tool call around it — with an unhandled ValueError: embedded null byte (#78256; reporter hits it on every python -m pip … from inside a v0.20.0 gateway). One-line-class fix restoring the fail-open contract #76762 established.

Root cause

_read_referenced_script() (cron/lifecycle_guard.py:260) catches only OSError around os.open() — but CPython raises ValueError (not OSError) for a NUL byte in the path itself:

>>> os.open("a\x00b", os.O_RDONLY)
ValueError: embedded null byte

#76762 handled NUL bytes in scanned contents (binary detection) and in Path.resolve — this is the third member of the same class, one call earlier. Reproducible on current main without any environment specifics:

>>> contains_gateway_lifecycle_command_or_referenced_script("source /tmp/e\x00vil.sh")
ValueError: embedded null byte     # before — crashes guard + terminal tool
False                              # after — fail-open skip, per #76762 semantics

Changes

  • cron/lifecycle_guard.py: os.open gets an explicit except ValueError (documented — NUL-in-path is "a path the OS cannot even represent", same semantics as unreadable); the fstat/read block broadened to (OSError, ValueError).
  • tests/hermes_cli/test_gateway_restart_loop.py: TestNulByteReferencedPathFailsOpen — unit (NUL path → (None, False)), end-to-end (guard survives the NUL-carrying command), and a positive control (real lifecycle script still blocked).

Validation

before after
_read_referenced_script(Path("x\x00y")) ValueError escapes (None, False)
guard on source /tmp/e\x00vil.sh crash — terminal tool dies False (nothing to scan)
guard on script containing hermes gateway restart blocked blocked (unchanged)
scripts/run_tests.sh tests/hermes_cli/test_gateway_restart_loop.py 2 new tests fail 85 passed, 0 failed

Scope notes

Fail-open here is the guard's documented design (#76762): a token the OS can't even represent as a path is by definition not a readable shell script — there is nothing to scan, and a crashed guard blocks every command, which is strictly worse than skipping one unscannable token. The reporter's exact NUL source (their pip launcher shape) isn't needed to fix the class: any NUL-carrying token now takes the same skip path as unreadable files.

Fixes #78256.

…ousResearch#78256)

os.open() raises ValueError — not OSError — when a path contains an
embedded NUL byte, and _read_referenced_script caught only OSError. A
recursive scan that produced such a token (reporter: any 'python -m pip'
command from inside the gateway on v0.20.0) crashed the entire guard,
taking the terminal tool call down with an unhandled traceback instead
of the fail-open skip NousResearch#76762 established for exactly this byte in
scanned contents.

Treat NUL-in-path identically to an unreadable file: nothing to scan.
Regressions: unit (NUL path → (None, False)), end-to-end (guard survives
'source /tmp/e\x00vil.sh'), and the real-lifecycle-script block still
fires.
@alt-glitch alt-glitch added type/bug Something isn't working comp/cron Cron scheduler and job management tool/terminal Terminal execution and process management P2 Medium — degraded but workaround exists duplicate This issue or pull request already exists labels Aug 4, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #77898: both apply the same os.open ValueError catch for embedded-NUL referenced paths with the same fail-open behavior and regression coverage.

@kshitijk4poor

Copy link
Copy Markdown
Collaborator

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 read_remote_script callback is sanitized at the recursion boundary (NUL = binary = nothing to scan; >1 MiB = fail closed), the remote fallback read is bounded at the source (head -c, so oversized binaries never cross the wire), and the public guard is total by construction — an unexpected walk failure logs and falls back to the direct-scan verdict instead of breaking every terminal command.

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.

@ayushnangia

Copy link
Copy Markdown
Contributor Author

Boundary fix over per-callsite is clearly right — the four-incidents-one-frame-apart history makes the case by itself. The adversarial never-raises sweep is the shape these guards should all converge on. Thanks for the close-out note.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management duplicate This issue or pull request already exists P2 Medium — degraded but workaround exists tool/terminal Terminal execution and process management type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lifecycle_guard crashes with ValueError: embedded null byte on 'python -m pip' commands (incomplete #76762 fix)

3 participants