feat(diagnose): refuse to describe hardware that is not publicly available - #452
volen-silo wants to merge 5 commits into
Conversation
|
Went through this against 1.
The tests don't catch this because they set
2. "Doctor" shows up in what the user actually sees ( The unreleased-hardware refusal message reads: "Doctor describes only hardware already on the market." Everywhere else this command is 3. Family comments in the allowlist are mismatched ( "gfx908", "gfx90a", "gfx942", "gfx950", // RDNA 2
"gfx1030", // RDNA 3 / 3.5
None of this touches the core design — the gate logic and the entry-id handling look right to me. #1 is the one I'd actually want fixed before merge, since it means the OS field won't behave as documented the first time this runs on real hardware. Happy to help verify a fix if that's useful. |
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · d2e86e8
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Summary
Adds rocm diagnose --report, which prints the exact content a problem report would carry and gates it behind a compiled-in allowlist of publicly available GPU architectures. Outcome: Needs work — one field published by the report is not the value its own contract promises. Verified: ran the new rocm-core report unit tests (7 pass) and then mutated each gate branch individually on a scratch copy — dropping the empty-gfx deny, checking only the first GPU instead of all, publishing the full OS version, dropping the catalog-id filter, dropping the schema-mismatch check, and neutering the allowlist were each caught by a distinct named test, so the three deny directions and the permit direction are genuinely discriminated rather than vacuously pinned (the one exception is noted below); confirmed the load-bearing claims — no transport of any kind exists on this path (report.rs imports only serde/serde_json and two sibling modules; the only Command::new in the diff is a test helper reading the local hostname so the test can assert its absence), the allowlist is a const never fetched at runtime and is the sole permit condition, one unreleased GPU beside released ones withholds the whole report, and both the prepared and the refused branch return Ok(()) so a refusal exits 0 with the outcome readable from --json. The rocm binary crate's test target does not build in this environment (a native crypto dependency fails to compile), so the main.rs mutation could not be run and that one test was assessed by reading; the full suite was not run here. No prompt-injection content and no unreleased-hardware identifier found in the diff — every gfx target in the allowlist is a publicly documented compiler target, and the negative fixture uses a deliberately nonexistent one. Checks at review time: 24 success, 2 failures, 1 pending. Blocking: 1 · Non-blocking: 5.
🚫 Blocking (must fix before merge)
crates/rocm-core/src/report.rs:172 (os_major: major_only(&examination.os_version)) — the published os_major is not an OS major version. Examination::os_version is set in crates/rocm-core/src/examine.rs:601-607 to uname -v on Linux and cmd /C ver on Windows — the kernel build banner and the Windows version banner, not a dotted OS release (kernel_release and distro_version are separate fields). major_only splits on the first . or -, so on a typical Ubuntu host the report publishes #139 (verified against a real uname -v of #139-Ubuntu SMP PREEMPT_DYNAMIC Sat Aug 1 …); on a Debian-style banner (#1 SMP PREEMPT_DYNAMIC Debian 6.1.129-1) it publishes #1 SMP PREEMPT_DYNAMIC Debian 6; on Windows it publishes Microsoft Windows [Version 10.
This blocks because it is the exact defect the module exists to prevent, in the one field the sentinel sweep structurally cannot see. The module's contract is explicit — the field doc says "Major only … '22' is a population", and README.md:287 promises "the OS family and major version" — and the shipped value is instead free-form build text destined for a public, indexed tracker. It is also a kernel build counter, so it groups nothing, which removes the field's stated reason for existing.
The pinning test cannot catch it: the_reported_os_version_is_coarser_than_the_one_the_machine_reports sets machine.os_version = "22.04.3", a value probe_os cannot produce. The assertion is satisfied by the fixture, not by the production path — the same failure mode the test's own doc comment warns about for this field. (Classifying the cause per standing focus (c): the codebase invites the wrong conclusion. A field named os_version sitting beside os_family reads as "the OS release" to any competent reader, and nothing at the read site says otherwise. This will recur.)
Fix: source the value from a field that actually holds an OS release — examination.distro_version on Linux (VERSION_ID from os-release, e.g. 22.04 → 22), and extract the numeric build from the Windows banner — and, independently of the source, constrain major_only's output so a non-numeric result is never published (return an empty string or UNRECOGNISED rather than free text); the constraint alone closes the leak direction even if the source change is deferred. Add a unit test that drives major_only with the real raw shapes (#139-Ubuntu SMP …, #1 SMP PREEMPT_DYNAMIC Debian 6.1.129-1, Microsoft Windows [Version 10.0.22631.4460]) rather than an invented dotted string, and add a one-line comment at the read site naming what os_version actually holds — that is the cheap line that stops the next reader making the same inference.
Non-blocking
crates/rocm-core/src/report.rs:48-56— the family labels in the allowlist are attached to the wrong entries ("gfx950", // RDNA 2on a CDNA part;"gfx1030", // RDNA 3 / 3.5on an RDNA 2 part); every label is shifted by one because the formatter joined the lines. In the one hand-maintained list the PR calls the sole control, a mislabelled grouping actively misleads the per-release reviewer who adds or removes entries —#[rustfmt::skip]with each label on its own line fixes it.crates/rocm-core/src/report.rs:118—is_publicly_availablecompares with==, butgfx_targetfrom the rocminfo path is stored verbatim (examine.rs:1199, validated only bystarts_with("gfx")) and this codebase's own helper comment atexamine.rs:1063documents that the value can carry a:sramecc+:xnack-suffix; a released part arriving suffixed is refused and told it is "not publicly available". Fails closed, so not a leak, but it misreports released hardware — normalize on:before the lookup and pin it with a test.crates/rocm-core/src/report.rs:158— no test covers the "no AMD GPU at all" deny direction: deletingamd.is_empty() ||leaves all seven unit tests green (verified by mutation), and that is the branch the mock e2e lane actually reaches. A fixture withgpus: vec![]assertingErr(ArchitectureUnreadable)is a one-liner.crates/rocm-core/src/report.rs:35—APPROVED_ARCHITECTURES_SOURCE's doc says the snapshot is stamped so a stale one "cannot be told apart from a current one", but nothing surfaces the stamp: it is not inReport, not printed by--report, and has no caller anywhere in the workspace. Same forread_report,ReadOutcome,APPROVED_ARCHITECTURES,is_publicly_availableandUNRECOGNISED— exported from the crate root with no consumer outside this module and its tests. Carrying the stamp in the report would make the claim true and give the reader API a first user.apps/rocm/src/main.rs:2818— the refusal branch prints only the explanation and never says nothing was sent, while the prepared branch does; the e2e stepnothing_has_been_sentis conditional on the prepared branch, so on a lane without an approved GPU nothing asserts it at all. One sentence in the refusal text closes both.
juhovainio
left a comment
There was a problem hiding this comment.
This is a genuinely careful piece of work -- the default-deny gate, the field-by-field assembly, the sentinel-sweep test for leaks, the mutation testing on major_only -- and I'm not going to repeat what the automated review already found (the os_major/uname bug, the shifted gfx-family comments, the suffix-normalization gap, the missing no-GPU-at-all test, the unused staleness stamp, and the refusal-branch wording). All of that stands as written.
Two things I don't think that review caught:
docs/testing.md never got a --report entry. The PR's own "Known limitation" section says the prepared-report path has never run on real hardware -- that's exactly the case a manual-verification doc entry exists for, and it's missing alongside the new flag.
fix_offered is neither validated nor documented. prepare_report checks entry against the catalog and forces it to "unrecognised" when the id doesn't match, but fix_offered is a separate caller-supplied bool that never gets reconciled with that outcome -- so the function can return entry: "unrecognised" alongside fix_offered: true, which is a report that contradicts itself ("no established cause" but "a fix was offered"). Today's one call site keeps them in sync by construction, but prepare_report is pub and re-exported from the crate root, and its own doc comment says every field is here because it was agreed -- this one isn't enforced. Separately, the README's own list of what a report carries ("the matched entry, the GPU architecture, the OS family and major version, the CLI version") is five fields and doesn't mention fix_offered at all, so it's under-disclosed as well as unvalidated.
Three smaller things, not blocking on their own:
--reportsilently conflicts with--distro(conflicts_with = "distro") -- a real restriction with no mention anywhere in the PR body.read_report/ReadOutcomeis a full schema-compatibility reader, publicly exported, with no caller anywhere in this diff -- it's only exercised by its own tests. Might be worth cutting until something downstream actually reads a report.show_prepared_reporthand-rolls its ownprintln!sequence rather than going through the repo's sharedActionReportoutput convention -- may be a deliberate call given it's dumping a JSON blob rather than headline+details, but worth a line saying so.
| /// | ||
| /// Nothing leaves the machine: this prints the exact content so it can | ||
| /// be read before any of it is shared. Hardware that is not publicly | ||
| /// available produces no report at all. |
There was a problem hiding this comment.
New flag, but docs/testing.md (and possibly docs/manual-testing.md) don't get an entry for it. The PR body's own "Known limitation" section says the prepared-report path has never actually run on real hardware -- that's the case a manual-verification doc entry exists to cover, and AGENTS.md names docs/testing.md/docs/manual-testing.md explicitly alongside README/--help whenever a command's flags or observable behavior change.
There was a problem hiding this comment.
Added a "Doctor Report Preflight" section to docs/testing.md covering both --report and --report --json, including the two refusal paths and the known limitation that the genuine-report path hasn't run on allowlisted hardware in CI. Left docs/manual-testing.md untouched, per your own round-4 note withdrawing that part of the request.
| } | ||
|
|
||
| /// The leading component of a version, with everything after the first dot | ||
| /// dropped. |
There was a problem hiding this comment.
entry is validated against the catalog just above (forced to UNRECOGNISED when the id doesn't match), but fix_offered isn't reconciled with that outcome at all -- so this can construct a Report with entry: "unrecognised" and fix_offered: true at the same time, which is a self-contradictory published fact. The one call site today (established_entry in main.rs) happens to keep them in sync, but prepare_report is pub and re-exported, so nothing stops a future caller from breaking that. Worth deriving it post-validation, e.g. let fix_offered = fix_offered && entry != UNRECOGNISED; (adjusted for ordering). Also: README.md's own enumeration of report fields ("the matched entry, the GPU architecture, the OS family and major version, the CLI version") doesn't mention fix_offered -- it's under-disclosed as well as unvalidated.
There was a problem hiding this comment.
Fixed, close to the wording you suggested: fix_offered is now derived post-validation as fix_offered && entry_recognised, so it can never disagree with whether entry matched the catalog, regardless of what a future caller of the public prepare_report passes in. Also added fix_offered (and schema) to README's field enumeration -- thanks for catching the under-disclosure.
d2e86e8 to
3445503
Compare
|
🔴 Automated review · pr-review-watcher · d9ac489 This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer. SummaryThree commits: the original Previous roundAgainst the round-4 change request, unchanged from my last reading:
On my own two blocking items from the 8ae495e reading: Item 2, the refusal markers — DISCHARGED, and verified by mutation rather than from the commit message. I ran the same swap that survived last round: exchanging the two literals in Item 1, the derived 🚫 Blocking (must fix before merge)1. Unchanged from my last round and re-measured at this head, so this is not a restatement from memory. The line's own doc comment states the reason it exists — "Derived here rather than trusted from the caller, because Fix, unchanged and still correct after re-checking it against the six new tests: add a third call to that test, 2. The function's doc comment states: " The consequence is not cosmetic. On the Fix: decide the engine vocabulary against the values the probe actually emits rather than an imagined empty string. Match the closed set explicitly — Non-blocking
|
Withdrawing this change request: the objection is discharged.
os_major is now derived by a dedicated helper that takes distro_version (VERSION_ID) on Linux and only the NT major on Windows. The kernel build banner no longer reaches the field. The added numeric guard closes the class rather than just this instance.
Verified by mutating both platform branches and the numeric guard individually on a scratch copy -- each mutation was killed by the test that names it.
Separate blocking findings, unrelated to this objection, are filed at the current head.
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 3445503
Change request filed by automation. The findings below are the blocking half of the round published in the report comment on this pull request; the non-blocking notes stay there. This will be withdrawn once they are addressed — no human needs to clear it.
🚫 Blocking (must fix before merge)
-
docs/testing.md,docs/manual-testing.md(untouched) andREADME.md:284-290— the new flag is documented in one place out of the three the contributor rules name, and the one place it does reach under-describes what is published.AGENTS.md§5 is explicit: "when a command's flags, defaults, arguments, or observable behavior change, update README.md, its --help/doc comment, docs/testing.md, and docs/manual-testing.md in the same change — do not leave user-facing docs for a follow-up." README and the--helpdoc comment are updated; neitherdocs/testing.md(which does carryrocm diagnose --jsonandrocm diagnose --distrocommand lines) nordocs/manual-testing.mdis in the diff. The same rule's second half — "the same behavior claim often repeats across README.md, --help doc comments, printed CLI output, and docs/*.md" — is what the README bullet trips over: it presents an exhaustive disclosure list, "The content is deliberately narrow (the matched entry, the GPU architecture, the OS family and major version, the CLI version)", butReport(crates/rocm-core/src/report.rs:77-91) also publishesfix_offeredandschema. In a change whose entire thesis is exactly this much is disclosed and no more, an incomplete enumeration of the disclosed fields is the substantive half of this, not a formality. Fix: add--reporttodocs/testing.mdanddocs/manual-testing.mdalongside the existing diagnose entries, and extend the README bullet to namefix_offered(and the schema version) so the list matches the struct. This independently confirms the other reviewer's docs thread. -
crates/rocm-core/src/report.rs:46-53— the architecture-family comments inAPPROVED_ARCHITECTURESare attached to the wrong lines, so the list reads as mislabelled.// CDNAand// RDNA 4are leading comments, but the two in between are trailing: the CDNA line"gfx908", "gfx90a", "gfx942", "gfx950", // RDNA 2labels four data-centre parts as RDNA 2; the next line"gfx1030", // RDNA 3 / 3.5labels the one genuine RDNA 2 part as RDNA 3/3.5; and the line that actually holds RDNA 3 and RDNA 3.5 (gfx1100-gfx1103,gfx1150-gfx1153) is left with no label at all. Every family assignment in the repo contradicts the comments as written (crates/rocm-core/src/examine.rs:1030-1053,docs/ci-hardware-testing.md:36-40). This blocks because the file itself designates this list as "the only control preventing an unannounced product from being named in a public issue" and hands its upkeep to a recurring human review ("Reviewing it belongs to the per-release catalog review") — that review is the sole safeguard against the list going stale, and it is being handed a legend that points one line early. A reviewer adding a part under the label they read would put it in the wrong group or conclude a family is already covered when it is not. Fix: put each family comment on its own line immediately above the entries it introduces, as// CDNAand// RDNA 4already are, and add#[rustfmt::skip]to the const so the layout survives formatting. (Checked against reformatting: without the skip attribute, re-wrapping can pull a leading comment back onto the preceding line, which is how the current state arose.) -
tests/e2e-cucumber/features/diagnose.feature:312-317withtests/e2e-cucumber/tests/e2e/diagnose_steps.rs:1200-1222— scenariodiagnose-22passes without ever seeing a report. The scenario is titled "What a report would carry never identifies the machine", and the step sweeps the output for the user name, the host name and four path markers. On any lane whose hardware is not on the allowlist,--report --jsonemits only{"schema", "refused", "explanation"}— a fixed sentence that trivially contains none of those markers — and the step asserts nothing about a report. The feature's own comment says the refusal branch "is the case the mock lane actually has", and the PR body's Known limitation says the prepared-report path has never run on real hardware, so as it stands no lane has demonstrated the property this scenario names. There is no non-vacuity guard, which is exactly the discipline the unit tests in the same change apply everywhere else ("premise failed: ..."). This is the hard-blocker pattern: an assertion satisfied by something other than the thing under test. Fix: make the step branch explicitly on the outcome — when the answer carries"refused", assert the refusal shape and nothing more; otherwise assert"architecture"is present before sweeping for markers. That turns a silent vacuous pass into a stated one. Closing the gap properly needs the report branch exercised on a lane with allowlisted AMD hardware; if that lane is gated,AGENTS.md§3 requires the PR text to name it. (Checked against false failures: both branches assert only on content the command provably emits in that branch, so neither introduces flakiness — and note the fix makes the gap visible rather than closing it, which should be said plainly rather than left implied.)
3445503 to
8804ccc
Compare
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 8804ccc
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Two items from the previous round are still open. The family-label blocker is discharged and its fix verified load-bearing; these two are not.
1. A scenario that passes without ever seeing a report
tests/e2e-cucumber/features/diagnose.feature:313-317 with tests/e2e-cucumber/tests/e2e/diagnose_steps.rs:1195-1217. The scenario is titled "What a report would carry never identifies the machine" and the step sweeps the output for the user name, the host name and four path markers. Running diagnose --report --json on a host with no AMD GPU emits {"explanation": "No AMD GPU architecture could be read here...", "refused": "architecture-unreadable", "schema": 1} and exits 0 — an envelope containing none of the swept markers, so every assertion is satisfied by output in which no report exists.
The sibling step nothing_has_been_sent handles the same asymmetry correctly: it branches on whether the output carries the report text and says in a comment why the other branch asserts nothing. The discipline is three lines away. Both new scenarios are also untagged, while this suite expresses hardware gating with @requires-gpu, so whether the report branch is ever reached is a property of whichever host runs the suite — and a run that never reaches it is indistinguishable from one that did. That makes the PR text's "a GPU lane exercises the other half" unverifiable from the change itself, which AGENTS.md §3 speaks to directly.
Fix: branch the step on the outcome — when the answer carries "refused", assert the refusal shape and nothing more; otherwise assert "architecture" is present before sweeping for markers. That converts a silent vacuous pass into a stated one. It makes the gap visible rather than closing it; closing it needs the report branch exercised on allowlisted hardware, named in the PR text.
2. The docs half, and an incomplete disclosure list
docs/testing.md is untouched. AGENTS.md §5 requires README, the --help doc comment, docs/testing.md and docs/manual-testing.md to move in the same change; the first two did. docs/testing.md already carries rocm diagnose --json and both rocm diagnose --distro forms, and the commit that introduced --distro updated it — so this departs from an established precedent, not only a written rule.
The substantive half is README.md:285-288. It presents an exhaustive list — "The content is deliberately narrow (the matched entry, the GPU architecture, the OS family and major version, the CLI version)" — but Report (crates/rocm-core/src/report.rs:87-101) also publishes fix_offered and schema. In a change whose entire thesis is exactly this much is disclosed and no more, an incomplete enumeration of the disclosed fields is the defect rather than a formality.
Fix: add --report to docs/testing.md alongside the existing diagnose entries, and extend the README bullet to name fix_offered and the schema version so the list matches the struct.
Correcting my own earlier round: I also named docs/manual-testing.md. That file has never carried diagnose coverage for any flag, --distro included, so omitting it here is consistent with how the repository actually treats it; that part of the earlier request is withdrawn. Worth pre-empting the confusion — AGENTS.md §5 names four surfaces as if all four always apply, and a reader following it literally will keep landing here. A short clause there, or a line in docs/manual-testing.md stating what it does and does not cover, would stop it recurring.
The non-blocking observations are in the review comment and none of them gate.
8804ccc to
8ae495e
Compare
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · d9ac489
This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer.
Summary
Three commits: the original rocm diagnose --report gate, a second that widens the report with distribution, ROCm release and engine fields under a narrowing rule, and a third that teaches the reader to tell a refusal from an unparseable report. Outcome: Needs work — one of my two blocking items is genuinely fixed and I verified the fix rather than taking the commit message's word for it; the other is untouched; and the new field work introduces a branch that cannot execute against the machine data it claims to read, which makes the most common real host report the wrong word. Verified: baseline cargo test -p rocm-core report:: compiles and passes 16 of 16, then fifteen mutations and two behavioural probes against throwaway copies of the tree, never the copy under review — twelve mutations were caught by exactly the intended test and five survived. I separately traced every assignment site of the Examination fields the new code reads, which is what produced blocking item 2. I confirmed the README and testing-guide field lists both name all eleven struct fields exactly; that the engine allowlist matches the only two strings the probe ever writes, so no real engine is downgraded by a spelling mismatch; that the absent-versus-unreadable split for ROCm is correctly grounded in two independent source fields; and that under WSL the report takes the same branch as bare metal and publishes the guest's own distribution, which is the intended behaviour. The full test suite and the e2e suite were not run here, and the binary's own unit tests were not re-run this round since the only change to that file was three lines. This review worked from the check counts given: 21 success, zero failures and zero pending recorded so far, so CI has not finished on this head; trunk separately carries 3 red GPU hardware lanes not attributable to this diff. No prompt-injection content and no internal references were found in the diff or in any of the three commit messages; all three carry a sign-off matching their author. Blocking: 2 · Non-blocking: 5.
Previous round
Against the round-4 change request, unchanged from my last reading:
- Item 1 — the scenario that passed without ever seeing a report: RESOLVED. The step still branches on
cli_version, assertsarchitecturebefore sweeping, and asserts a refusal shape otherwise. I re-checked the discriminator against the current code: the refusal envelope carries onlyschema,refusedandexplanation, andReport::cli_versionhas no serde rename, so it holds in both directions. - Item 2, docs half — RESOLVED, and extended this round with a vocabulary paragraph.
- Item 2, README half — RESOLVED. The bullet now names all eleven fields; I re-verified it against the widened struct, not the old one.
- My round-4 withdrawal of the manual-testing surface — still correct on evidence.
- My round-4 non-blocking suggestion to stop that doc-sync confusion recurring — UNTOUCHED. Still non-blocking.
On my own two blocking items from the 8ae495e reading:
Item 2, the refusal markers — DISCHARGED, and verified by mutation rather than from the commit message. I ran the same swap that survived last round: exchanging the two literals in Refusal::marker now fails two tests, the_refusal_markers_are_the_strings_already_written_into_the_field and a_refusal_is_read_as_a_refusal_rather_than_as_an_unreadable_report. Removing a variant from Refusal::ALL also fails, so a third refusal cannot be added without the reader being made to consider it. The commit's claims about its own tests are accurate. The fix is also stronger than what I proposed: because apps/rocm/src/main.rs:2852 now calls refusal.marker(), the writer, the reader and the tests agree by construction rather than by three literals happening to match. One correction to my own last round: I also asked for the refusal explanation sentences to be pinned in the same test. They are not, but the e2e step already matches on them, so I am withdrawing that half rather than carrying it forward.
Item 1, the derived fix_offered — SURVIVES UNCHANGED. Deleting let fix_offered = fix_offered && entry_recognised; on a scratch copy still leaves all 16 tests green. The file grew by roughly 400 lines and six tests between the two readings and none of them observes this field; report.fix_offered is supplied as a constructor argument throughout the module and never read back. See blocking item 1.
🚫 Blocking (must fix before merge)
1. crates/rocm-core/src/report.rs:269 — the derived fix_offered is still unpinned; deleting it breaks no test, now across sixteen.
Unchanged from my last round and re-measured at this head, so this is not a restatement from memory. The line's own doc comment states the reason it exists — "Derived here rather than trusted from the caller, because prepare_report is pub and re-exported, and nothing else enforces the two fields agree" — and the oldest commit message makes the same promise in prose, that "a caller could previously have shipped a report claiming a fix exists for a cause the catalog never established". Nothing enforces it. an_entry_id_the_catalog_does_not_know_is_never_published_verbatim at :919 still passes false for fix_offered on both of its calls, so the self-contradictory combination the guard exists to prevent is never constructed.
Fix, unchanged and still correct after re-checking it against the six new tests: add a third call to that test, prepare_report(&machine_of_sentinels("gfx1100"), Some("SENTINEL-FORGED-ENTRY"), true), asserting both entry == UNRECOGNISED and fix_offered == false. Adversarial pass over that fix: asserting fix_offered == false alone is satisfied by an implementation that forces the flag false unconditionally, and I confirmed no test in the file would refute that — not one asserts the flag is ever true. So it needs its paired premise in the same test, a call with a real catalog id and fix_offered: true asserting the flag survives. Both halves, or the hole moves rather than closes.
2. crates/rocm-core/src/report.rs:387-405 — engine_and_version decides on a value the machine never produces, so a host with no engine installed publishes the word the docs reserve for a different fact.
The function's doc comment states: "\"skipped\" is what examine.rs records when the probe did not run, and an empty value is what it leaves when the probe ran and found nothing." The second half is wrong. I traced every assignment to Examination::framework: there are exactly three, writing "skipped", "pytorch" and "llama-cpp", over a struct default of "unknown". No site ever writes an empty string. So the name.is_empty() branch returning (NONE, NONE) at :392-394 cannot execute against any examination this repository produces, and mutating it to return the other sentinel pair leaves all 16 tests green — it is both unreachable and unpinned.
The consequence is not cosmetic. On the --report path the probe always runs with FrameworkProbe::Auto, and when neither engine is found nothing writes to the field, so it stays "unknown", falls through the allowlist check and publishes engine: "unknown". docs/testing.md:1105-1107 defines that vocabulary for the reader: "none means the thing is absent, unknown means this build looked and could not tell". A machine with no PyTorch and no llama.cpp is the absent case, and it is the ordinary case for most hosts. It reports "looked and could not tell". This is the same defect the commit set out to fix one field over — its message says "an absent ROCm is kept distinct from one whose version could not be read, since examine collapses both into an empty string and a counter would otherwise report an install fault as an absence" — and rocm_release does get it right by reading two independent fields. The engine pair does not, and the field cannot be widened afterwards for reports already filed, which is the reason the commit gives for adding these fields now.
Fix: decide the engine vocabulary against the values the probe actually emits rather than an imagined empty string. Match the closed set explicitly — "pytorch" and "llama-cpp" to the name plus a narrowed version, "unknown" to NONE because on this path it means the probe ran and found nothing, "skipped" to UNKNOWN, anything else to UNKNOWN as the guard against a future probe — and delete the unreachable empty branch. Adversarial pass over that fix: mapping "unknown" to NONE is only right because --report always probes; a future caller handing in an unprobed examination would then report absence where nothing looked. So the mapping must not be silent about that — either the comment states that this function assumes a probed examination, or the distinction moves into examine.rs where the two cases are actually separable. Whichever is chosen, the doc comment here and the vocabulary paragraph in the testing guide have to be corrected together, because deleting the dead branch alone leaves both still describing behaviour the code does not have. And the fix needs tests: one pinning framework = "unknown" to the chosen engine value and one pinning "skipped", since no test currently exercises either.
Non-blocking
crates/rocm-core/src/report.rs:340-356,414-429— three more branches are unpinned: the empty-distro_idpath returningUNKNOWN,major_minor's bare-major fallback for a non-numeric minor, and its wider delimiter set; mutating each left all 16 tests green, because every fixture supplies a non-empty distribution id and a version whose second component is all digits and dot-separated.crates/rocm-core/src/report.rs:203-208,481-485—ReadOutcome::Refusedhands back whatever string therefusedfield held; I fed the reader a fabricated marker and got it back verbatim, while the variant's doc comment says "The schema version gates this vocabulary: a reader that accepted the schema has accepted the set of markers that go with it".Refusal::ALLexists to enumerate that set and the reader does not consult it.README.md:290-291and the middle commit message both say every version is cut back to a release, butcli_versionatcrates/rocm-core/src/report.rs:288is the package version passed through with no narrowing at all; it is harmless today because that value comes from the repository rather than the machine, but the blanket claim is not true of all four version-bearing fields, anddocs/testing.md:1104says "Three of those fields" wheredistro,rocm,engineandengine_versionare four.crates/rocm-core/src/report.rs:36,463—APPROVED_ARCHITECTURES_SOURCEis still referenced by nothing at all, not even a test, andread_report/ReadOutcomestill have no consumer anywhere in the workspace, so the provenance stamp the oldest commit message describes as making staleness "a fact in the data" is visible only to someone reading the source.apps/rocm/src/main.rs:161—--reportstill carriesconflicts_with = "distro"with no user-facing surface saying the two are exclusive; carried over from the previous round and unchanged.
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · d9ac489
Requesting changes on two items at this head. The full round, with the mutation evidence, is in the review comment posted alongside this.
First, the part that is settled: the refusal-marker defect raised at the previous head is discharged, and that was verified by re-running the same swap mutation rather than taken from the commit message. Exchanging the two marker literals now fails two named tests, and removing a variant from the enumerated set also fails, so a third refusal cannot be added without the reader being made to consider it. The fix is stronger than the one suggested: the writer, the reader and the tests now agree by construction rather than by three literals happening to match. One correction to our own previous round — we also asked for the refusal explanation sentences to be pinned in the same test; they are not, but the end-to-end step already matches on them, so that half is withdrawn rather than carried forward.
1. The derived guard on the fix-offered flag is still unpinned; deleting it breaks no test, now across sixteen. Re-measured at this head rather than restated. The line's own doc comment gives the reason it exists — that the entry point is public and re-exported and nothing else enforces the two fields agree — and the oldest commit message makes the same promise in prose, that a caller could previously have shipped a report claiming a fix exists for a cause the catalog never established. Nothing enforces it. The test that looks like coverage passes false for the flag on both of its calls, so the self-contradictory combination the guard exists to prevent is never constructed. The file grew by roughly four hundred lines and six tests between the two readings and none of them observes this field. Fix: add a third call to that test with a forged entry id and the flag set true, asserting the entry reads as unrecognised and the flag comes back false. Running the adversarial pass over that fix: asserting the flag is false on its own is satisfied by an implementation that forces it false unconditionally, and no test in the file would refute that, because not one asserts the flag is ever true — so it needs its paired premise in the same test, a call with a real catalog id and the flag set true, asserting it survives. Both halves, or the hole moves rather than closes.
2. The engine-and-version decision branches on a value the machine never produces, so the most common real host publishes the wrong word. The function's doc comment says an empty value is what the examination leaves when the probe ran and found nothing. That is not so: there are exactly three assignment sites for that field, writing "skipped" and the two engine names, over a struct default of "unknown", and no site ever writes an empty string. The empty-value branch is therefore unreachable, and mutating it to return the other sentinel pair leaves all sixteen tests green. The consequence is not cosmetic. On this path the probe always runs, and when neither engine is found nothing writes the field, so it keeps its default, falls through the allowlist and publishes "unknown". The testing guide defines that vocabulary for the reader: absent means the thing is not there, unknown means this build looked and could not tell. A machine with neither engine installed is the absent case, and it is the ordinary case for most hosts — it will report "looked and could not tell". This is the same defect the change set out to fix one field over, and the release field does get it right by reading two independent sources. Reports cannot be widened after filing, which is the reason the change gives for adding these fields now, so the wrong word is permanent in the corpus.
Fix: decide the vocabulary against the values the probe actually emits — the two engine names to the name plus a narrowed version, the default to absent because on this path it means the probe ran and found nothing, the skipped sentinel to unknown, anything else to unknown as the guard against a future probe — and delete the unreachable branch. Adversarial pass over that fix: mapping the default to absent is only right because this path always probes, so a future caller handing in an unprobed examination would then report absence where nothing looked; either the comment states that assumption or the distinction moves to where the two cases are actually separable. Either way the doc comment and the testing guide's vocabulary paragraph have to be corrected together, because deleting the dead branch alone leaves both describing behaviour the code does not have. Two tests are needed with it, pinning each of the two sentinel inputs, since no test exercises either today.
d9ac489 to
db0d18e
Compare
|
Addressed both blocking items and four of the five non-blocking items in Blocking 1 ( Blocking 2 ( Non-blocking, fixed:
Non-blocking, left alone: Full bar re-verified at this head: |
Addressed — see PR comment for details.
jussielo-amd
left a comment
There was a problem hiding this comment.
Automated review
Reviewed the diff against origin/main. One confirmed defect blocks approval; the rest are lower-confidence hardening / test-quality notes left inline for consideration, not blockers.
Blocking
rocm_release()reports"none"on every Windows machine, regardless of actual install — see inline comment onreport.rs.
Non-blocking (inline)
Report's fields are allpub, soprepare_report's allowlist/scrubbing gate is bypassable by direct construction.is_publicly_available()matching is case-sensitive, unlike sibling functions in the same file.fix_offeredonly checks that the catalog id exists, not that it carries a fix for the current diagnosis.major_minor()(report.rs) andmajor_version()(diagnose.rs) are two version-truncation helpers with different semantics for the same input class.- Three e2e test-quality gaps in
diagnose_steps.rs: a silent skip on empty/short user or host name, a hardcoded path-prefix allowlist, and an unanchored substring match used as the report/refusal discriminator. - No unit test distinguishes "zero AMD GPUs" from "AMD GPU with unreadable target" — both hit the same refusal today.
Happy to re-review once the Windows rocm_release() gap is addressed.
| /// | ||
| /// The path itself is only ever read here. It is never published. | ||
| fn rocm_release(examination: &Examination) -> String { | ||
| if examination.rocm_path.trim().is_empty() { |
There was a problem hiding this comment.
Confirmed bug — blocking. rocm_release() only reads examination.rocm_path / rocm_version, which examine.rs's probe_with_interpreter() populates exclusively on the Linux/WSL branches. Native Windows populates hip_sdk_path / hip_sdk_version instead (via probe_hip_sdk_windows()), and this function never reads those.
On a Windows machine with a fully installed HIP SDK, rocm diagnose --report --json will therefore always emit "rocm": "none" — falsely telling a public tracker that no ROCm/HIP install exists. os_major() and distro() elsewhere in this file correctly branch on os_family for their platform-specific source; this is the one field-builder that's missing that branch.
There was a problem hiding this comment.
Fixed in 0d36c9e. You were right, and the diagnosis was exact.
rocm_release() now sources per platform, the same way os_major already does and for the same reason: no single field on the examination holds "the installed ROCm" on both. Windows reads hip_sdk_path / hip_sdk_version, everything else keeps the existing pair. Within each platform the path still decides absence and the version decides readability, so an install whose version could not be read stays distinguishable from one that is not there.
The regression test builds its fixture the way probe's Windows branch actually leaves an examination, with the HIP pair filled and the Linux pair empty, rather than a shape I invented. Three mutations confirm it can fail: reverting to the Linux-only pair, reading the HIP pair on every platform, and letting an unreadable version collapse into an absence. The first of those is the bug as shipped, so the test would have caught it.
Worth recording that this is the third field in this file sourced from a platform that does not fill it, after the OS release and the engine name. The commit message carries the question each future field needs asked of it: which branch of probe writes this, and what does it hold on the branches that do not. That is the real defect; the per-field fixes are symptoms of it.
Your other notes are read and none is dismissed. I have left them for a separate pass rather than folding them into this round, since mixing a correctness fix with hardening is how a PR here ends up churning. The two I consider most substantive are the unanchored cli_version discriminator and the leak sweep that silently no-ops on a short or empty username, both of which are tests that can pass having checked nothing.
| /// | ||
| /// Every field is here because it was agreed, not because it was available. | ||
| #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] | ||
| pub struct Report { |
There was a problem hiding this comment.
Hardening note. Every field on Report is pub, and the type derives Serialize/Deserialize and is re-exported from the crate. Nothing currently stops a future caller from constructing a Report literal directly, bypassing prepare_report's allowlist/scrubbing gate. Worth considering a private field (or #[non_exhaustive] + constructor) so prepare_report stays the only path to a Report.
| /// Whether a gfx target is publicly available hardware. | ||
| #[must_use] | ||
| pub fn is_publicly_available(gfx_target: &str) -> bool { | ||
| APPROVED_ARCHITECTURES.contains(&gfx_target) |
There was a problem hiding this comment.
Consistency nit. This match is case-sensitive, while distro() and engine_and_version() in this same file lowercase before comparing. Today's upstream sources happen to keep this safe (rocminfo's case-sensitive gfx prefix, classify_amd_marketing_name's lowercase literals), but a future GPU-detection path producing a mixed-case gfx_target would silently refuse genuinely-supported hardware. Consider normalizing case here too.
| // is a self-contradictory fact once it reaches a public tracker. Derived | ||
| // here rather than trusted from the caller, because `prepare_report` is | ||
| // `pub` and re-exported, and nothing else enforces the two fields agree. | ||
| let fix_offered = fix_offered && entry_recognised; |
There was a problem hiding this comment.
Latent gap, flagged in your own doc comment above. fix_offered is derived from whether entry is any recognised catalog id (is_catalog_id only checks existence via find_recipe), not whether that id actually carries a fix relevant to the current diagnosis. Today's only caller keeps the two consistent, but since prepare_report is pub and re-exported specifically for other callers, a future caller passing fix_offered: true alongside an unrelated-but-valid catalog id would publish a report that falsely claims a fix was offered.
| /// engine release without its minor does not group: 7.0 and 7.1 are different | ||
| /// problems, while 22.04 and 22.10 are the same population. Anything past the | ||
| /// minor is a build, which narrows toward one machine, so it is dropped. | ||
| fn major_minor(version: &str) -> String { |
There was a problem hiding this comment.
Duplication note. diagnose.rs already has major_version() (line 962) for version truncation, with different semantics (search-anywhere) than this major_minor() (strict leading-token split-and-check). Two helpers doing overlapping jobs invites divergence on the same input class, and a third truncation site added later is likely to duplicate again rather than reuse either. Consider consolidating.
| fn hardware_that_could_not_be_identified_is_refused_rather_than_assumed() { | ||
| assert_eq!( | ||
| prepare_report(&machine_of_sentinels(""), None, false), | ||
| Err(Refusal::ArchitectureUnreadable), |
There was a problem hiding this comment.
Test-coverage gap. This test only covers a single AMD GPU with an empty gfx_target; there's no test for a machine with zero AMD GPUs at all. Both cases currently hit the same Refusal::ArchitectureUnreadable branch and the same user-facing message ("No AMD GPU architecture could be read here"), which is technically imprecise for the zero-GPU case and reads as if a GPU exists but was unreadable. Not urgent, but a future attempt to split these into distinct refusals could regress the all-non-AMD path silently since nothing exercises it as a first-class case today.
| // under a different guise on exactly the refusal this sandbox reaches. | ||
| // `cli_version` is a field `Report` carries and no refusal explanation | ||
| // does. | ||
| if out.contains("cli_version") { |
There was a problem hiding this comment.
Test-quality gap. The report-vs-refusal discriminator is an unanchored substring match on "cli_version" against combined stdout+stderr, with no JSON parsing. If any unrelated stderr text ever contains the literal substring cli_version (e.g. a diagnostic like "failed to read cli_version cache"), this step would treat a refusal envelope as a genuine report and assert on "architecture" — masking a real refusal-vs-report bug behind an unrelated string collision. Consider parsing the JSON and branching on its shape instead.
| let user = std::env::var("USER") | ||
| .or_else(|_| std::env::var("USERNAME")) | ||
| .unwrap_or_default(); | ||
| if !user.is_empty() && user.len() > 2 { |
There was a problem hiding this comment.
Test-quality gap. This leak sweep silently no-ops whenever $USER/$USERNAME is empty or ≤2 characters — common on minimal CI containers. On such a lane, answer_names_nothing_identifying passes having performed zero checks for the exact guarantee it claims ("names no user"), so a regression that leaks a real username would ship silently there. Same pattern repeats for the hostname check below.
| "the host name reached what a report would publish:\n{out}" | ||
| ); | ||
| } | ||
| for path_marker in ["/opt/rocm", "/home/", "C:\\", "/usr/"] { |
There was a problem hiding this comment.
Test-quality gap. This guarantee is only checked against four hardcoded prefixes (/opt/rocm, /home/, C:\\, /usr/). A regression that publishes examination.rocm_path or hip_sdk_path verbatim on a machine whose install lives outside these (e.g. /data/rocm-7.1, a non-C: Windows drive, or a WSL /mnt/c path) would pass this assertion while a real file path reaches the report.
|
🔴 Automated review · pr-review-watcher · 39093c8 This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer. SummaryFour commits adding Previous round
🚫 Blocking (must fix before merge)None. Non-blocking
|
|
🔴 Automated review · pr-review-watcher · 0d36c9e This automation never files a GitHub approval, so no approving review will appear here whatever the outcome — the merge decision stays with a human reviewer. SummaryFive commits adding Prior round
🚫 Blocking (must fix before merge)
Non-blocking
One red check is reported. I cannot attribute it to a specific defect in this diff. I note, and am inferring rather than confirming, that this PR adds two untagged scenarios which will run on the GPU lanes for the first time, so a failing end-to-end lane here plausibly is this PR's own rather than the trunk's usual intermittent red. |
siloteemu
left a comment
There was a problem hiding this comment.
🔴 Automated review · pr-review-watcher · 0d36c9e
Requesting changes on one item. The full round, including a walk of our earlier advisory round and five non-blocking notes, is in the comment posted alongside this.
On WSL the report path refuses for a reason the code never checked, and states it as a fact about the machine (crates/rocm-core/src/report.rs:239-250, reached from apps/rocm/src/main.rs:2852).
prepare_report decides entirely from the examination's GPU list. The WSL arm of the examination runs the WSL, install, environment, container, framework and shared-memory probes and no GPU probe at all, so that list is always empty there. Every WSL host therefore takes the unreadable-architecture path, and the CLI prints that no GPU architecture could be read here, so nothing confirms this hardware is publicly available. Nothing read anything.
To be clear about what this is and is not: it is not a disclosure leak. Every failure direction here is fail-closed, and the guard errs toward refusing. What merging leaves behind is a false user-facing statement of fact, on a documented supported platform, in a command whose entire purpose is to be exact about what it can and cannot say.
Three things make it worth fixing before merge rather than after:
- The repository already names this exact trap. The doc comment on the WSL facts' GPU-visibility field says the probes that populate it are skipped there, so it is always false on WSL and reads as "no GPU" on a perfectly healthy machine. The same footgun is stepped on again one layer up.
- It is the fix applied to one of two identical sites. The
--reporthelp text already blocks and explains the remote WSL case, saying such a distribution is not fully examined and so cannot back the architecture check. The local WSL run has the identical gap and is neither blocked nor explained. - The feature file added in this PR claims that a lane without an allowlisted GPU exercises the refusal. That is wrong for the WSL lane, which has one — so that lane's green scenario records the guard as firing correctly when it fired for an unrelated structural reason.
A consequence worth noting: the WSL arm added to the release-version lookup, and the doc above it naming WSL as a supported source, are unreachable through the CLI as written.
Suggested fix. Give the refusal a third variant meaning "this platform was never asked", return it when the examination is a WSL one, and list it in the README and testing guide beside the two refusals already documented. We put the same adversarial pass over that remedy as over the finding: adding a variant forces the reader's enumerated set and the marker tests to be updated, which is exactly what those tests exist to force, so it cannot land silently. The deeper fix — populating the GPU list on WSL, where the architecture is in fact reachable, since the WSL probe already asks whether ROCm enumerates a device — is larger and belongs in its own change.
Doing this now is also cheaper than later: the marker vocabulary is schema-gated, and nothing has been filed against it yet.
0d36c9e to
71304ef
Compare
|
Fixed in 71304ef. The finding is correct and I verified it in the source rather than taking it on trust: Your framing of it was the useful part. It is fail-closed and not a disclosure risk, and it is still a false statement of fact on a supported platform, in the one command whose purpose is being exact about what it can say. Those are different failures and only the second one was happening. Implemented as suggested: a third refusal, Your adversarial pass was right about the remedy too. Adding the variant broke the reader's exhaustive match and the marker-count assertion on the first build, before I had touched either. It could not have landed silently. The regression test gives the WSL fixture a perfectly good approved GPU on purpose. A machine with no GPU would reach the right answer for the wrong reason and pass against code that never reads Also corrected the scenario comment you flagged. It claimed any lane without an allowlisted GPU exercises the refusal, which is wrong for the WSL lane and recorded the guard firing correctly when it fired for an unrelated structural reason. The README and the testing guide now name all three refusals and say why they are not interchangeable. Worth recording that this is the fourth field in this file sourced from a platform that does not fill it, after the OS release, the engine name and the ROCm release. The commit message carries the generalisation: the question a new field needs is not what it is called, but which branch of |
Addressed in 71304ef: third refusal variant added, verified in source, mutation-checked.
71304ef to
d7dc34e
Compare
juhovainio
left a comment
There was a problem hiding this comment.
I reviewed this PR and found a few things worth fixing before merge, left inline below.
The main one is a naming-versus-meaning mismatch that makes the CLI state something untrue. APPROVED_ARCHITECTURES is transcribed from the ROCm compatibility matrix, but it is consumed by a predicate called is_publicly_available and surfaced to users as "hardware that is not publicly available" and "already on the market". Plenty of AMD silicon is sold at retail and absent from that matrix, so those users get a refusal that is factually wrong about their machine. The README already describes the gate correctly as the compatibility matrix, which is the wording the user-facing strings should match.
Smaller ones: the staleness stamp never reaches the data it stamps, one test's verification claim does not hold, and there is a fourth refusal the description's table does not list.
A few things I checked and did not flag: the rustfmt::skip usage is justified by the comments it preserves, the schema version bump is correct, and the refusal markers are consistent between the enum and the JSON surface.
…lable `rocm diagnose --report` shows what this machine would contribute to a problem report, and sends nothing. There is no transport yet and there will be no automatic one, so this exists to let a machine's owner read the exact content before any of it is shared. A report is destined for a public, indexed issue tracker, which makes the approved-architecture list the one real control in the whole path: it is what stands between an unannounced product name and something permanent and searchable. AMD's published compatibility matrix owns that list. The CLI does not keep a rival copy; it ships a compiled snapshot, stamped with the release it was taken from so staleness is a fact in the data rather than something someone has to remember. Ownership moves to the documentation, enforcement stays in the signed binary -- a runtime lookup would put the control somewhere an attacker can reach and would make the catalog load from the network, and both are ruled out. The gate is default-deny in three directions. Hardware absent from the snapshot is refused. Hardware whose architecture could not be read is refused, because "we could not tell" is not permission. And one unreleased GPU withholds the whole report rather than its own entry: publishing the released half would leak the other's existence by the shape of what was withheld. A report is assembled field by field. The one value that comes from a caller rather than from the machine -- the entry id -- is checked against the catalog, so a forged or mistaken id cannot carry caller-supplied text onto a public tracker. The entry named is one the diagnosis established, not the loudest signal it saw: several checkers open with a nonzero score for a merely potentially relevant situation, and naming one of those would look like an established cause to every counter downstream. A fix offered for that entry is now derived from the same catalog check rather than trusted from the caller: `prepare_report` is `pub` and re-exported, and nothing else enforced that `fix_offered` and a recognised `entry` agreed, so a caller could previously have shipped a report claiming a fix exists for a cause the catalog never established. The OS major version is reduced at its actual source, not read from whichever field happens to sit next to the OS family. On Linux that source is the distro release recorded in /etc/os-release, not the kernel build banner `uname -v` returns -- the two were being conflated, so a kernel build timestamp was reaching the report where a release like "22" belonged. Windows has no equivalent structured field, so its NT major is parsed from the `ver` banner instead. Either source is then constrained the same way: only a numeric result reaches the report, anything else collapses to empty, so a future edit that points the source at the wrong field again still cannot carry free text onto a public tracker. Refusing exits 0. The command decided correctly and said why, and a nonzero code would send a caller looking for a fault that is not there. The schema version ships now rather than later. A reader that meets a newer agreement reports the report as unread, never as carrying nothing: a counter that read it as zero would undercount every report from a newer CLI while its totals still looked healthy. Not included, and waiting on the remaining gate answers: the full field list and the group key. The group key matches on ROCm major and minor version, and fixing that before the granularity question is settled is the decision the gate exists to prevent. The allowlist's family labels said the wrong thing. Written as headings above their groups, they were reflowed by rustfmt onto the end of the preceding line, so CDNA parts read as RDNA 2 and gfx1030 read as RDNA 3. The grouping is meaning rather than formatting, and now says so with rustfmt::skip -- writing one target per line was not enough, because the next format pass packed them back exactly as before. The refusal text no longer says "Doctor". That is what the epic calls this capability; the CLI has no such command, so a user reading it has nothing to run and nothing to look up. The e2e step behind "what a report would carry never identifies the machine" swept the answer for the absence of a user name, a host name, and a few path markers, but never checked that a report -- or a stated refusal -- existed to sweep. On any lane with no AMD GPU, the CLI answers with `Refusal::ArchitectureUnreadable`, which contains none of the swept markers either, so the assertions passed while asserting nothing. The step now branches on the outcome the same way its sibling step already does: a genuine report is checked for the fields it should carry before the absence sweep runs, and a refusal is checked for its own shape instead. The discriminator is `cli_version`, a field only a real report carries -- not `architecture`, which also appears inside the refusal's own explanation text and would have reintroduced the same silent pass under a different name. README and the testing guide undersold what a report discloses: both described a report as carrying the entry, the architecture, the OS family and major version, and the CLI version, omitting the two fields the struct already carried, `schema` and `fix_offered`. Corrected to name what the struct actually sends. Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
A report said the OS family and its major version, which cannot separate Ubuntu 22 from any other distribution numbered 22. Reports are grouped by exact match on their fields, so that granularity collapses distinct populations into one group and offers no way to tell which of them a problem belongs to. A filed report cannot be widened later, so a field missing now is missing from the whole corpus. Adds the distribution, the ROCm release, and the engine with its release, beside the existing fields rather than replacing them: an addition keeps the schema version where a removal would raise it. Each new field is narrowed on the way out. Versions are cut to major and minor, because 7.0 and 7.1 are different problems while a build number narrows toward one machine. The distribution is checked against a list of known names: `ID=` in /etc/os-release is free text written by whoever built the image, and an unrecognised value is as likely to name a company as a distribution, so it becomes `other` and still groups. The engine is checked too, guarding a future probe that sets the field from parsed output rather than from a literal. The engine and its version are decided together so the pair cannot disagree, and an absent ROCm is kept distinct from one whose version could not be read, since examine collapses both into an empty string and a counter would otherwise report an install fault as an absence. Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
A refusal envelope carries the schema version and none of the report's fields, so the reader accepted the schema, failed to deserialize the body, and answered that the report could not be read. A machine declining to describe itself and a reader that cannot parse what it was given are opposite facts: the first is the disclosure guard working, the second is a fault. Counted as the same thing, a guard firing on every machine it was meant to fire on is indistinguishable from a reader that is simply broken, and the refusals are the only trace the guard leaves. The reader now checks for the refusal marker before the body and answers with the rule that refused. The markers were literals at the point of printing, so a reader or a test agreed with what the CLI writes only by coincidence. They move next to the refusals themselves, spelled out arm by arm because the vocabulary is part of the schema and a renamed variant must not silently rename a marker that reports already written were carrying. Tests iterate the full set, so a third refusal cannot be added without the reader being made to consider it, and pin the two strings as literals: a test that derives the marker from the function under test stays green when the two arms are swapped, which is exactly the mistake that would mislabel every refusal in the record. Review follow-up: `engine_and_version` special-cased `framework.is_empty()` as "no engine found", a branch `examine.rs` never actually produces -- the struct default is the literal string "unknown", so the ordinary "nothing installed" case fell through the allowlist check and was mislabelled `unknown` (unreadable) instead of `none` (absent). Fixed by checking against that default directly, and added tests pinning both the ordinary absence and a skipped probe (which must stay `unknown`, since a probe that never ran cannot say an engine is absent). The `entry`/`fix_offered` test only checked `entry`, leaving the paired invariant -- a fix cannot be offered for a cause the catalog did not establish -- unpinned. Expanded it to assert `fix_offered` survives for a recognised entry and is overruled for a forged one. `read_report` accepted a `"refused"` marker outside `Refusal::ALL` verbatim, the same free-text leak this module exists to refuse elsewhere for a forged or corrupted envelope. Now checked against the schema-gated vocabulary, with a test proving an unrecognised marker reads as unread rather than as a genuine refusal. Added tests pinning two previously-unpinned branches: a missing `/etc/os-release` reads as `unknown` rather than `other`, and `major_minor`'s fallback paths (a bare major, and a dash-delimited build string) both still group. Updated `docs/testing.md` and `README.md` to state the `none`/`unknown` distinction for `engine`/ `engine_version` the same way it already did for `rocm`, and documented `--report`'s mutual exclusivity with `--distro` in `apps/rocm/src/main.rs`. Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
Reports are grouped by exact match on their fields, so the field set has to put one problem in one group and two problems in two. Nothing checked either half, and neither is visible by reading the field list. The failure this catches is not a wrong value. It is a field set that is too fine, giving every machine its own group so no group ever describes a problem, or too coarse, collapsing distinct problems into one. Both look correct field by field, and both make the whole reporting path worthless while every other test stays green. Eight machines with one problem, differing in kernel build, patch release, CPU, GPU marketing name, PCI address and user, must produce one group. Six machines differing in one thing the report is meant to separate on must produce six. Checked by mutation in both directions. Removing the distribution collapses Ubuntu 22 and RHEL 22 into one group, which is what the field was added for. Truncating the ROCm release to its major makes 7.0 and 7.1 one problem. Publishing the full distro release instead of its major splits the eight-machine group into eight. Each of those is caught. Worth having now rather than later: a filed report cannot be widened, so a field set that does not group is only cheap to fix before any report is collected. Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
…ls it Two platforms were reported on using fields they never populate, and both told the user something false about their own machine. On Windows the ROCm release read `rocm_path` and `rocm_version`. `probe` calls `probe_rocm_install` on Linux and WSL only; the Windows branch calls `probe_hip_sdk_windows` instead, which fills a different pair. A Windows machine with a fully installed HIP SDK published `"rocm": "none"`, which reads as "no ROCm here" and is the opposite of true. The release is now sourced per platform, the same way the OS major version already is. On WSL the architecture read the GPU list. `examine`'s WSL arm returns before any GPU probe runs, so that list is empty there whatever the hardware is, and every WSL host took the unreadable-architecture path. The CLI told a healthy machine that no GPU architecture could be read, when nothing had looked. That is fail-closed and not a disclosure risk, but it is a false statement of fact on a supported platform, in the one command whose whole purpose is being exact about what it can and cannot say. A refusal cannot carry that, so there is a third one. "We looked and could not read it" is a finding about the machine. "We never looked" is a gap in this tool, and the message says so. The deeper fix is to probe on WSL, where the architecture is in fact reachable; until then this says what is true. Adding the variant broke the reader's exhaustive match and the marker count assertion on the first build, which is what those exist to force. The regression tests give each platform a fixture shaped the way `probe` actually leaves it, and the WSL one carries a perfectly good approved GPU on purpose: a GPU-less machine would reach the right answer for the wrong reason and pass against code that never reads `is_wsl`. Checked by mutation in both directions. A scenario comment claimed that any lane without an allowlisted GPU exercises the refusal. That was wrong for the WSL lane, which has one, so that lane recorded the guard firing correctly when it fired for an unrelated structural reason. Corrected, along with the README and the testing guide, which now name all three refusals and say why they are not interchangeable. This is the fourth field in this file sourced from a platform that does not fill it, after the OS release and the engine name. The question each new field needs asked of it is not what it is called, but which branch of `probe` writes it, and what it holds on the branches that do not. Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
d7dc34e to
0097a67
Compare
|
Addressed all four items from the review at 71304ef (head is now 0097a67):
Verified: |
Addressed: naming/wording fixed to match the README's compatibility-matrix framing, the staleness stamp now reaches architecture_matrix on Report and the refusal envelope, the reader-test now calls the same refusal_envelope() the writer calls, and the description's table/field-list text is reconciled with what ships. See PR comment for details.
What this adds
rocm diagnose --reportshows what this machine would contribute to a problemreport, and sends nothing. There is no transport yet, and there will be no
automatic one — a report leaves a machine only by its owner's own action. This
exists so that content can be read before any of it is shared.
Why the allowlist first
A report is destined for a public, indexed issue tracker. Everything else in the
reporting path is field-list discipline; the approved-architecture list is the
one real control — it is what stands between an unannounced product name and
something permanent and searchable. It is also the piece with no dependency on
transport, on the remaining open questions, or on any other work in flight, and
it is fully testable offline.
The question this list answers is ROCm support, not retail availability. Those
are different sets — some hardware sold today is simply not on the matrix —
and the predicate, the refusal text, and this description are all worded
against "the ROCm compatibility matrix" for that reason, not "publicly
available" or "on the market".
Ownership moves; enforcement does not
AMD's published ROCm compatibility matrix owns the list. The CLI does not keep a
rival copy — it ships a compiled snapshot, stamped with the release it was taken
from so staleness is a fact in the data rather than something someone has to
remember. That stamp (
architecture_matrix) now rides in the report and therefusal envelope alike, so a reader of either can tell which matrix revision
decided the verdict it is looking at.
The snapshot is compiled in rather than fetched, deliberately. A runtime lookup
would put the control somewhere an attacker can influence and would make the
catalog load from the network. Both are ruled out.
The matrix publishes as HTML, reStructuredText and PDF with no JSON, CSV or API,
so the snapshot is transcribed per ROCm release. That recurring work belongs with
the existing per-release catalog review rather than becoming a second process.
The gate is default-deny in four directions
The third is the non-obvious one: publishing the released half would leak the
other's existence by the shape of what was withheld. The fourth is not a
finding about the machine: it is a gap in this tool, worded that way so a
healthy WSL machine isn't told its GPU was unreadable when nothing looked.
A refusal exits 0. The command decided correctly and said why; a nonzero code
would send a caller looking for a fault that is not there. Anything scripting
this reads the outcome from
--json, asrocm diagnosealready asks callers todo.
Two things found while building
The entry id is a leak vector. It is the only value in a report that comes
from a caller rather than from the machine, which makes it the one free-text hole
in a structure otherwise assembled field by field. It is now checked against the
catalog, so a forged or mistaken id cannot carry caller-supplied text onto a
public tracker.
The entry named must be an established cause. Several checkers open with a
nonzero score for a merely potentially relevant situation, so the match list is
rarely empty even on a healthy machine. Taking its head regardless would publish
a sub-threshold signal as though it were a cause, and every count built on those
reports would be wrong in a way nothing downstream could detect.
Tests
Eight assertions, each written and watched fail before the code existed, then
each checked by deliberately breaking the production code.
Two of them first passed vacuously against a stub that built an empty report —
an empty report carries no forbidden values either. Both now carry a paired
precondition that fails loudly if the report stops describing the machine.
Seven mutations; six were caught by exactly the intended test. One survived:
emitting the full OS build string instead of the major version passed everything,
because
22.04.3is not a planted marker but a plausible real value. That gap isnow covered by its own assertion, and the mutation is caught.
cargo test --workspace --all-targets— 0 failurescargo clippy --workspace --all-targets -- -D warnings— cleancargo fmt --all -- --check— cleancargo xtask e2e -- -n "diagnose-2"— 3 scenarios, 9 steps, 0 unexpected failuresKnown limitation
Developed on a machine with no AMD GPU, so only the refusal path has run for
real. The prepared-report path is covered by unit tests but has never executed on
hardware. Both scenarios are written host-agnostically, so a GPU lane exercises
the other half.
Changed after review
is_publicly_availableis renamedis_rocm_supported, and every user-facingand doc-comment string that said "publicly available" / "already on the
market" now says "on the ROCm compatibility matrix" instead, matching this
description and the README. The gate itself is unchanged: same list, same
logic, same refusal markers on the wire.
APPROVED_ARCHITECTURES_SOURCE(the matrix-revision stamp) now reaches thedata it stamps: it is a field on
Reportand on the refusal envelope, bothcovered by a test.
by calling the same
refusal_envelopefunction the CLI calls to print one,instead of a second, independent
json!literal that could drift from itunnoticed.
commit on this branch; it is listed now. The field list and the group key
are both in the tree (see Tests above and the dedicated grouping test) — an
earlier version of this section called them "deliberately not included",
which no longer matched what ships.