Conversation
45f8838 to
fdc38fa
Compare
jussielo-amd
left a comment
There was a problem hiding this comment.
Reviewed the path-comparison fix in crates/rocm-core/src/runtime.rs plus its two other consumers. Hand-traced the lexical resolver against the respelling cases named in the PR body (/etc/, $HOME/../../etc, C:\Windows\..\ProgramData, etc.) and found no mismatches. Ran cargo test -p rocm-core, clippy, fmt, and xtask manifest --check/check-crate-edges locally — all clean, consistent with the green CI run.
A few non-blocking nits for a follow-up, not required for this PR:
- The PR body's list of "fixed consumers" misses a fourth caller,
apps/rocm/src/storage.rs:628(the prune planner) — it's automatically correct post-fix since it delegates toruntime_install_root_is_protected, just undocumented. rocmd'ssystem_prefix_requires_ackis named as a fixed consumer, but its only test (install_sdk_rejects_system_prefix_without_ack) uses an already-canonical path, so it doesn't actually exercise the respelling bug this PR fixes. Worth a regression test with a respelled path.- The new respelling tests are
#[cfg(unix)]-only; the public gate's Windows branch (as opposed to the platform-parameterized helpers) isn't exercised end-to-end on the Windows CI lane. An adjacent existing test already shows thecfg!(windows)-at-runtime pattern to do this cheaply. delete_guard_properties's reference oracle unconditionally assumes absolute input — harmless today since all generator seeds are absolute, but worth a comment/assertion pinning that assumption so a future generator change can't silently desync oracle from production.- The two explicitly-disclosed deferred gaps (symlink-through-home bypass, incomplete Windows protected-root list) are only noted in prose/comments — might be worth a tracked follow-up issue for each.
Approving — solid fix, well-tested, clearly reasoned PR description.
`runtime_install_root_is_protected` decides whether a folder may be handed to `fs::remove_dir_all`. It compared normalized path TEXT, and off Windows that normalization is `value.to_owned()` -- a no-op. So the check `path == root || root in path.ancestors()` saw `/etc/` and `/etc` as different folders, and nothing resolved `.` or `..` at all. Measured against the real gate, `ensure_runtime_install_root_is_safe_to_remove` accepted every one of `/etc/`, `//etc`, `/./etc`, `/etc/.`, `/usr/` and `$HOME/../../etc`, refusing only the exact spelling `/etc`. The trailing-slash case needs no adversary: a `dirname`, a shell variable or a hand-edit produces it. The `..` case defeats the guard differently -- `Path::ancestors()` treats `..` as an ordinary component, so a path that escapes `$HOME` still looks like it is inside it and takes the home exemption's early return. The guard's own comment says it exists for "a hand-edited or corrupted registry entry [pointing] `install_root` at a protected system location". It failed at exactly that. The existing test passed throughout because it used `/etc/rocm-cli-test-runtime` -- a subpath, the one shape that works. Both comparisons now resolve `.`, `..`, repeated separators and a trailing separator before comparing. Folding `\` to `/` stays gated on Windows: off Windows a backslash is an ordinary filename byte, so a directory named `..\tmp` inside `/usr` must not be rewritten into `/usr/../tmp`. Raw-text comparison tolerated an unconditional fold because equality never *removed* components; resolving `..` does, which would have turned this fix into a new bypass. The resolution is lexical because these paths often do not exist yet, so `canonicalize` has nothing to resolve. It is NOT symlink-safe, and the comments say so rather than claiming otherwise: because the home exemption is consulted first, a symlink the user owns is enough -- with `$HOME/link -> /etc`, `$HOME/link/child` is lexically inside home, so the guard reports it removable while the kernel lands in `/etc`. Closing that needs a decision about canonicalizing the part of the path that does exist, and belongs in its own change. Two further consumers of the same comparison are fixed by this: `chat_install_prefix_is_system`, which gates an install prefix that arrives as model-generated JSON, and `rocmd`'s `system_prefix_requires_ack`, which stopped demanding acknowledgement for a `..` path that escaped home. The guard is pinned by properties rather than more examples, because the contract is about every spelling of a path and an example only ever covers the spellings someone thought of. The reference the properties measure against is derived from POSIX semantics, not restated from the code, so it does not inherit the code's mistakes -- it is what makes a one-component change to the generator alphabet catch the backslash case above. The comparison helpers are now parameterised by platform instead of reading `runtime_is_windows()`, so the Windows path-resolution rules are exercised from a Linux host. Note this covers the helpers only: the branch selecting the Windows protected-root list still reads `runtime_is_windows()` and remains reachable only on the Windows lane. Parameterising also required the containment test to compare bytes rather than `&path[..n]`, which splits a non-ASCII path mid-character and panics. On Windows this trades one inaccuracy for another, deliberately: direct respellings (`C:/Windows/`, `c:/windows`, `C:/Windows//System32`) are now caught, while `C:\Windows\..\ProgramData` -- previously refused only because the text walk matched `C:\Windows` as a literal ancestor -- now correctly resolves to `C:/ProgramData` and is accepted, because that path is not on the list. The three-entry Windows list omits the drive root, `C:/Users` and `C:/ProgramData`, and hardcodes drive `C:` and English folder names. That is an incomplete policy rather than a broken comparison, it was previously masked by the ancestor-walk bug, and it deserves its own change. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
fdc38fa to
347aa39
Compare
|
Thanks for the careful review — and apologies for the timing: I force-pushed a rebase after your approval, which I should have checked for first. Nothing in the reviewed change was modified. What happened:
Diff between the commit you approved ( Re-verified after the rebase, since a conflict-free rebase is not proof of a working build:
On your nits, all fair:
Let me know whether you would prefer 2–4 in this PR or as an immediate follow-up. My instinct is that 2 and 4 belong here, since both are about this PR's claims being backed by tests rather than by prose, while 3 is a genuine addition that could go either way. I will file issues for 5 regardless. |
Three reviewer nits on the respelling fix for runtime_install_root_is_protected, addressed with tests only: - rocmd's system_prefix_requires_ack was only claimed as a fixed consumer; its existing test used an already-canonical prefix, so it never drove the `..`-escaping bug. Add a regression built from the real home directory plus `/../../usr`, confirmed red against the pre-fix comparison and green against the fix. - The new respelling tests are unix-only, so the public gate's Windows arm (home exemption plus the three protected roots) was never exercised on the Windows lane end to end. Add a test, following the existing cfg!(windows)-at-runtime fixture-selection pattern, that checks respelled Windows paths when actually running on Windows and the Unix answer otherwise. - The property suite's POSIX reference oracle silently assumed absolute input, true only because every generator seed happens to be absolute today. Pin that assumption with an assertion so a future generator change can't desync the oracle from the code it measures. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
|
Pushed Nit 2 — Nit 3 — Windows lane now reaches the public gate. Since Nit 4 — oracle assumption pinned with an assertion, not a comment, at the top of the property suite's Nit 1 (fourth consumer) is in the PR description now. Nit 5 is filed: #494 for the symlink-through-home bypass and #495 for the narrow Windows protected-root list. One thing worth flagging since you may see it locally: Verification after the new commit: Re-approval is yours to give or withhold — happy to split any of this into a follow-up if you would rather keep the approved diff untouched. |
tests/e2e-cucumber/expectations.tomlfor the fixed ticket ID and removed/narrowed any now-stale xfail rows. — no rows reference this area.docs/architecture.md. — N/A, no new subcommand.Fixes #490
Summary
runtime_install_root_is_protecteddecides whether a folder may be handed tofs::remove_dir_all. It compared path text, and off Windows the normalization applied is a no-op, so/etc/and/etcwere different folders. Nothing resolved.or...Measured against the real gate on
main,ensure_runtime_install_root_is_safe_to_removeaccepted/etc/,//etc,/./etc,/etc/.,/usr/and$HOME/../../etc, refusing only the exact spelling/etc. The guard's own comment says it exists for "a hand-edited or corrupted registry entry [pointing]install_rootat a protected system location" — it failed at precisely that. A trailing slash needs no adversary; adirname, a shell variable or a hand-edit produces one.Both comparisons now resolve
.,.., repeated separators and a trailing separator before comparing.Three further consumers are fixed by the same change:
chat_install_prefix_is_system, which gates an install prefix arriving as model-generated JSON;rocmd'ssystem_prefix_requires_ack, which had stopped demanding acknowledgement for a..path that escaped home; and the prune planner atapps/rocm/src/storage.rs:666, which is automatically correct post-fix because it delegates toruntime_install_root_is_protected.Risk: medium. It changes a comparison used by several call sites. Every caller was enumerated and reviewed; the only behaviour changes are the intended ones. The failure mode of getting this wrong is severe, which is why it is pinned by properties rather than examples and why the review notes below are spelled out.
Non-obvious decisions
Resolution is lexical, not
canonicalize. These paths frequently do not exist yet — they come from a registry entry — socanonicalizehas nothing to resolve.It is therefore not symlink-safe, and the code now says so. Because the home exemption is consulted first, a user-owned symlink is enough: with
$HOME/link -> /etc,$HOME/link/childis lexically inside home, so the guard reports it removable while the kernel lands in/etc. This hole is identical before and after this PR. An earlier draft of this change claimed a symlink "can only land somewhere the lexical answer did not already call safe" — that is false, it was the argument for the whole design, and it is corrected in the comments rather than left for the next reader to inherit.Folding
\to/stays gated on Windows. Off Windows a backslash is an ordinary filename byte. An unconditional fold plus the new..resolution would rewrite a real directory named..\tmpinside/usrinto/usr/../tmpand walk it out of the protected root — turning this fix into a new bypass. Raw-text comparison tolerated the unconditional fold because equality never removed components; resolving..does. The generator alphabet includes..\tmpso the property suite holds that line.Platform parameterization. The comparison helpers now take a platform instead of reading
runtime_is_windows(), so Windows path rules are exercised from a Linux host. This covers the helpers only — the branch selecting the Windows protected-root list still readsruntime_is_windows()and remains Windows-lane-only. Parameterizing also required the containment test to compare bytes rather than&path[..n], which splits a non-ASCII path mid-character and panics.Windows trade, stated deliberately. Direct respellings (
C:/Windows/,c:/windows,C:/Windows//System32) are now caught. ConverselyC:\Windows\..\ProgramData— previously refused only because the text walk matchedC:\Windowsas a literal ancestor — now correctly resolves toC:/ProgramDataand is accepted, because that path is not on the list. The three-entry Windows list omits the drive root,C:/UsersandC:/ProgramDataand hardcodes driveC:and English names. That is an incomplete policy which was previously masked by the ancestor-walk bug; widening it is a separate change and is not attempted here.Test plan
Properties, not more examples — the defect's nature was that the examples pinned the spellings somebody thought of. The reference oracle is derived from POSIX semantics rather than restated from the implementation, so it does not inherit the implementation's mistakes.
Verified against the real gate, before and after:
/etc/,//etc,/./etc,/etc/.,/usr///,/../../..,/etc/..$HOME/../../etc/usr/..\tmp(one real directory)$HOME/rocm,$HOME/rocm/../rocm,$HOME/a\bThat last row matters as much as the rest: a guard that protects everything is useless.
cargo test --workspace --all-targets— 3195 passed, 0 failed (32 suites), exit 0cargo clippy --locked --workspace --all-targets -- -D warnings— exit 0cargo fmt --all --check— exit 0/bin/..\tmp..., ~31% a.component, ~15%//, ~30% a trailing separator, reaching several hundred distinct resolved locations across all protected roots.On the Gherkin requirement
AGENTS.md asks for a scenario when observable CLI behaviour changes, and it does:
rocm runtimes uninstallon an entry spelled/etc/now refuses where it previously deleted. No scenario is added, deliberately. To exercise that end-to-end a scenario would have to plant a registry entry pointing at a real protected system path on a CI runner — and if the guard ever regresses, the test deletes it. The failure mode of the test is the catastrophe it tests for. The behaviour is covered at the unit level instead, including a regression test on the gate itself inapps/rocm.Follow-ups deliberately not in this PR
~is not protected (pre-existing and unchanged); nothing absolutizes before the guard runs, and two consumers take model-generated JSON.