fix(lifecycle-guard): return a verdict when the home directory is unresolvable - #79830
fix(lifecycle-guard): return a verdict when the home directory is unresolvable#79830ruochu88s wants to merge 1 commit into
Conversation
`_resolve_terminal_script_path()` called `Path(candidate).expanduser()`, which
raises `RuntimeError` — not `OSError` — when the home directory cannot be
determined. This guard runs on *every* terminal command, so one command
mentioning a `~/`-relative script in an environment without HOME/USERPROFILE
aborted the entire tool call:
RuntimeError: Could not determine home directory.
File "cron/lifecycle_guard.py", line 174, in _resolve_terminal_script_path
path = Path(candidate).expanduser()
The failure is not a guard verdict and not a command error — the guard itself
raises before deciding anything, and the caller sees a crash for a command that
has nothing to do with gateway lifecycle.
Reproduced with HOME/USERPROFILE/HOMEDRIVE/HOMEPATH unset:
'echo hello' -> verdict=False
'bash ~/script.sh' -> RuntimeError: Could not determine home directory.
'sh ~/nested/run.sh' -> RuntimeError: Could not determine home directory.
'bash ./local.sh' -> verdict=False
Two conditions must coincide: the command references a `~/`-relative script,
and the environment lacks the home variables. Affected environments include
processes started with `env -i`, service accounts, stripped containers, and CI
runners.
- Catch `RuntimeError` around `expanduser()` and fall back to the literal path.
An unexpanded `~/x.sh` simply will not be readable, which the caller already
handles.
- Also guard `Path.cwd()`, so a deleted or inaccessible working directory
cannot break the scan either.
Detection is unchanged: a gateway lifecycle command is still caught with HOME
absent, and a relative script's body is still read and scanned, so the
degradation only affects `~` expansion.
Tests: tests/cron/test_lifecycle_guard_unresolvable_home.py, 9 cases. The
no-HOME fixture asserts `Path('~').expanduser()` really does raise, so the
tests cannot silently pass on a platform where it resolves.
Fault injection: restoring the bare `expanduser()` call turns 4 assertions red;
the fix returns 9/9.
|
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?
The lifecycle guard raises instead of returning a verdict when
~cannot beexpanded, which aborts the whole terminal tool call:
Path.expanduser()raisesRuntimeError, notOSError, so the surroundingexcept OSErrorhandlers do not catch it. Because this guard runs on everyterminal command, the result is not "command blocked" and not "command failed" —
it is the guard itself crashing for a command that has nothing to do with
gateway lifecycle.
Encountered in real use while running an unrelated
git add+pythonpipeline.
Reproduction
With
HOME/USERPROFILE/HOMEDRIVE/HOMEPATHunset:Two conditions must coincide:
~/-relative script, andCommands without
~never reachexpanduser()and are unaffected.Environments that land in this state: processes started with
env -i, serviceaccounts, stripped containers, and CI runners. This repo's own
scripts/run_tests.shhas needed fixes for the same class of problem, sinceenv -idropsUSERPROFILEon Windows.The fix
RuntimeErroraroundexpanduser()and fall back to the literal path.An unexpanded
~/x.shsimply will not be readable, and the caller alreadyhandles unreadable scripts.
Path.cwd(), so a deleted or inaccessible working directory cannotbreak the scan either.
Detection strength is unchanged. With
HOMEabsent, a gateway lifecycle commandis still caught, and a relative script's body is still read and scanned — the
degradation is confined to
~expansion.How was it tested?
New:
tests/cron/test_lifecycle_guard_unresolvable_home.py— 9 passing.The no-HOME fixture asserts that
Path('~').expanduser()really does raisebefore running the case, so the tests cannot silently pass on a platform where
it resolves.
Cases: three
~-script shapes return a verdict instead of raising; unrelatedcommands unaffected; a dangerous command is still detected; a relative script
body is still scanned; baseline behaviour with
HOMEpresent is unchanged(including scanning a tilde script's body); a deleted cwd does not raise.
Fault injection. Restoring the bare
expanduser()call turns 4 assertionsred; the fix returns 9/9:
Relationship to existing PRs (not a duplicate)
Two open PRs touch the same class of defect. Both were checked against the
current
main:lifecycle guard. Same file, different function and different call path:
it guards
_read_script_for_scanning/_resolve_script_path, reached when acron job's
scriptfield is scanned. On currentmainthose symbols nolonger exist under those names, so that patch does not cover the terminal
path. This PR guards
_resolve_terminal_script_path, reached for everyterminal command that references a script.
changed files do not include
cron/lifecycle_guard.py, so the sweep leavesthis call site untouched.
If
safe_expanduser()from #41870/#41881 lands first, this change should berewritten to call it instead of the local
try/except— the fallback semanticsare identical, and I am happy to rebase onto that helper. The test file stays
valid either way, since it asserts observable behaviour (returns a verdict,
detection unchanged) rather than the mechanism.
Type of change
Checklist
"Relationship to existing PRs" above — fix(cron): catch RuntimeError from Path.expanduser() in lifecycle guard #56517 and refactor: sweep remaining bare expanduser() to safe_expanduser() (depends on #41870) #41881 both checked)
Platform
Reproduced and verified on Windows 10, but the defect is not
Windows-specific:
Path.expanduser()raises the sameRuntimeErroron POSIXwhen
HOMEis unset and the uid has no passwd entry. On Windows the trigger issimply easier to hit, because both
USERPROFILEand theHOMEDRIVE+HOMEPATHpair must be present.