fix(cron): handle embedded null byte in lifecycle guard command tokens (#77988) - #78013
fix(cron): handle embedded null byte in lifecycle guard command tokens (#77988)#78013webtecnica wants to merge 1 commit into
Conversation
|
生产事故实证:这个 bug 不是理论问题,我们在生产环境被它打崩过。 事故经过(2026-08-04,Hermes v0.20.0 / v2026.8.3,macOS):
验证过的本地修复(与 #78013 方向一致): # cron/lifecycle_guard.py _read_referenced_script
try:
descriptor = os.open(path, flags)
except (OSError, ValueError): # ValueError: embedded NUL byte in path
return None, False修复后模拟 null-byte 路径不再崩溃,gateway 恢复正常。 建议: 这个修复值得尽快合并——影响面是"任何引用脚本的 terminal 命令都可能触发",对 gateway 生产环境是单点故障。合并后我们的本地补丁会移除,跟随官方升级。 |
ce9370b to
1224884
Compare
|
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. |
|
Thanks @kshitijk4poor for the architecturally cleaner fix in #80258 — sanitizing path candidates once at the source beats per-callsite handling. Agreed on closing both; glad the whole bug class is covered. |
Summary
Fixes #77988 — a
ValueError: embedded null byteraised bycron/lifecycle_guard.pywhen a NUL byte appears in a terminal command token (executable or script-path string). The unhandled exception propagated out of the guard on every terminal-tool invocation once a NUL byte showed up in a command token, taking down completely benign commands ("Operacja nieudana: embedded null byte w lifecycle_guard").Distinct from #77927 (NUL-byte security bypass in file content): this is the opposite failure direction — the guard crashes instead of silently letting something through.
Root Cause
Several sites in
cron/lifecycle_guard.pyconstruct apathlib.Path(or callos.open) directly from a token parsed out of the raw command string, without guarding againstValueError(whichpathlib/osraise for an embedded NUL byte):_resolve_terminal_script_path()—Path(candidate).expanduser()unguarded; runs lazily inside the_iter_referenced_shell_scriptsgenerator, so thetry/except (OSError, ValueError)in_contains_unsafe_gateway_actionwraps only the laterresolve(strict=False)step and never catches the generator's ownPath()construction.contains_launchctl_submit_command()—Path(segment[index]).nameunguarded._iter_referenced_shell_scripts()—Path(executable).nameunguarded._iter_shell_command_payloads()—Path(segment[index]).nameunguarded._read_referenced_script()—os.open(path, flags)catches onlyOSError, but a NUL byte in the path string makesos.openraiseValueError.Sites 1–4 run on every terminal-tool invocation via
contains_gateway_lifecycle_command_or_referenced_script()/contains_launchctl_submit_command(), whichtools/terminal_tool.pycalls unconditionally before executing any shell command.Change
Guard must never crash on unresolvable/invalid path tokens — treat them as "nothing to scan" (mirrors the existing #76762 philosophy for binary content), not as a hard failure:
_safe_path_name(token)helper: likePath(token).namebut returnsNoneinstead of raising onValueError; use it at all threePath(...).namesites._resolve_terminal_script_path()now returnsOptional[Path]and returnsNoneonValueError; the three yield sites in_iter_referenced_shell_scripts()skipNoneresults._read_referenced_script()catchesValueErrorfromos.open(NUL in the path string itself, distinct from NUL in file content handled below) and returns(None, False).Verification
tests/hermes_cli/test_gateway_restart_loop.py:test_nul_byte_in_command_token_does_not_crash_guard— the issue's exact repro (bash \x00engine/scripts/portfolio_report.py --date 2026-08-03) plus NUL in a-cpayload token and a.shpath token all returnFalseinstead of raising.test_nul_byte_in_launchctl_token_does_not_crash_guard— NUL in the launchctl executable token tolerated.test_nul_byte_tolerance_does_not_weaken_guard—hermes gateway restartstill blocked,launchctl submit -l ai.hermes.svc-reload-tmp -- ...still blocked, benignls -la /tmpstill passes.tests/hermes_cli/test_gateway_restart_loop.py(85 tests) andtests/tools/test_terminal_tool_requirements.py+test_local_env_blocklist.py+test_approved_command_clean_slate.py(63 tests).Closes #77988