Conversation
`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>
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>
`format_bytes` scaled while the raw value was >= 1024, then printed it to one decimal place. Between those two steps a value like 1023.95 KiB stops the loop and then rounds up, so 1_048_525 bytes rendered as "1024.0 KiB" instead of "1.0 MiB" -- a size shown in a unit it has outgrown, in the user-facing "not enough free disk space" message and its "Free up ..." figure. The same happened at every unit boundary. This defect was already known and already fixed in the OTHER byte formatter, `apps/rocm`'s, whose test is named `format_bytes_steps_up_instead_of_printing_1024_of_the_smaller_unit`. The copy here kept it, because an example test sits next to one implementation and cannot see the other. So the guard added here is a property, not another example: no rendered size may carry a mantissa of 1024 unless it is already in the largest unit. It is attached to the contract rather than to a call site, so it holds wherever it is pointed. The minimal input proptest shrank to, 1_048_525, is pinned as an example too, so the specific defect stays named if the generator is ever retuned. Four more contracts come with it, covering margin arithmetic and mount selection (soundness, maximality, and irrelevance of a non-matching mount). All pure and in-process: the module runs in ~0.03s on the ordinary unit-test lane, with no subprocess, filesystem or GPU. One caveat is written into the generator's doc comment, because it is the part that is easy to get wrong: the naive version, drawing a uniform u64, PASSED against a defect that was definitely present, because essentially every draw lands in the exabyte range and never visits a unit boundary. The sampling window has to scale with the boundary, since the band where rounding bites is itself proportional. The generator is part of the specification. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
The previous commit made the `cfg(not(unix))` stub a `const fn` while the unix implementation stays a plain `fn`, so the one function has a different signature on each platform. That is the pattern to avoid: `missing_const_for_fn` only fires on the platform with the trivial body, and a const on one side invites callers to rely on something the other side cannot provide. Neither caller is const, and the lint is not enabled on main, so nothing needed it. The commit message didn't mention the change either. Signed-off-by: Roman Inflianskas <Roman.Inflianskas@amd.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.Fixes #502
Summary
format_bytesscaled while the raw value was>= 1024.0, then printed it to one decimal place. A value like1023.95KiB stops the loop and is then rounded up by the formatting, so1_048_525bytes rendered as"1024.0 KiB"instead of"1.0 MiB"— a size shown in a unit it has outgrown. The same happened at every boundary.That string reaches users: this formatter renders the out-of-space refusal and its "Free up …" shortfall figure, so an install blocked on a nearly-full disk could quote
1024.0 KiB.Risk: low. One pure function, no API or schema change. The loop condition now compares the value as printed rather than the raw value.
The part worth a reviewer's attention
This defect was already known and already fixed — in the other byte formatter, in
apps/rocm/src/main.rs, whose test is namedformat_bytes_steps_up_instead_of_printing_1024_of_the_smaller_unitand whose comment reads "One byte short of the next unit used to round to1024.0 KiB". Therocm-corecopy kept the bug, because an example test sits next to one implementation and cannot see its twin.So the guard here is a property, not another example: no rendered size may carry a mantissa of 1024 unless it is already in the largest unit. A property is attached to the contract rather than to a call site, so it holds wherever it is pointed.
The two formatters still disagree in output shape (
"1023 bytes"vs"1023 B") and in their largest unit (GiB vs TiB). Unifying them touches user-facing strings on two surfaces and is deliberately not attempted here.Test plan
Red-before/green-after verified by reverting only the loop condition: the property fails and shrinks to
bytes = 1048525, the same minimal input each run. That value and the other boundaries are also pinned as explicit examples, so the specific defect stays named if the generator is ever retuned — including1_048_524 -> "1023.9 KiB", so the promotion cannot reach down and swallow a value that genuinely belongs to the smaller unit.Four further properties come with it, covering margin arithmetic (
with_marginnever shrinks a requirement; the extracted-size estimate never underestimates),classify_spacetotality, and mount selection (soundness, maximality, and irrelevance of a non-matching mount).cargo test -p rocm-core --lib disk_space— 28 passed, 0 failed, ~0.03s: no subprocess, no filesystem, no GPUcargo clippy --locked --workspace --all-targets -- -D warnings,cargo fmt --all --check,cargo xtask manifest --check,cargo xtask tpn --check— all exit 0A caveat recorded in the code, because it is easy to get wrong
The first version of the central property passed against a defect that was definitely present. A uniform
u64generator essentially never lands near a unit boundary — almost every draw is in the exabyte range — so it never sampled the only region where the scaling loop can go wrong. The sampling window has to scale with the boundary, because the band where rounding bites is itself proportional. That reasoning is in the generator's doc comment rather than only in this description, since the next person to touch it needs it.The reference the properties measure against is also kept derived from the contract rather than restated from the implementation, for the same reason.
On the Gherkin requirement
No scenario is added. Reaching this string end-to-end requires a genuinely nearly-full filesystem; a scenario would have to fake one, which tests the fake rather than the behaviour. The formatter is covered at the unit level, where it is a pure function.