Skip to content

Each CPU's bring-up writes SPEC_CTRL and clears the MCU_OPT_CTRL opt-outs its decision names - #1277

Draft
Japabu wants to merge 2 commits into
mainfrom
mitigate-msr
Draft

Japabu wants to merge 2 commits into
mainfrom
mitigate-msr

Conversation

@Japabu

@Japabu Japabu commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

kernel::mitigation::decide (#1243) names, per CPU, enhanced IBRS, AMD's always-on IBRS, BHI_DIS_S and the microcode's RDRAND/RDSEED and gather mitigations, and nothing set them: every CPU ran under the IA32_SPEC_CTRL (0x48) and IA32_MCU_OPT_CTRL (0x123) firmware left (issues/spec-ctrl-and-gds-stay-as-firmware-left-them.md).

What changes

  • IA32_SPEC_CTRL is written whole at each CPU's bring-up with Decision::spec_ctrl: IBRS where enhanced IBRS or AMD's always-on IBRS is named, BHI_DIS_S where it is, every other bit clear. That is the value while no program that asked runs; the per-program bits are the next PR's. It is written only where Facts::has_spec_ctrl says the MSR exists (Intel "CPUID Enumeration and Architectural MSRs" Table 3; APM Vol. 3 Fn8000_0008_EBX).
  • IA32_MCU_OPT_CTRL's opt-outs are cleared where their microcode mitigation is named (Decision::mcu_opt_ctrl_clears): RNGDS_MITG_DIS and GDS_MITG_DIS. The other bits are kept.
  • Enhanced IBRS needs CPUID.(07H,0):EDX[26], which enumerates the bit it sets. A CPU that claims IBRS_ALL without that bit is reported as lacking the control rather than taking #GP.
  • The words line gives each register as found and as left, and the lines take what is in force from the read-back. Decision::unapplied names anything the kernel sets that a register shows off; the mitigations metal row refuses such a CPU. Before, it refused only LFENCE's bit.
  • gds-found-disabled, a boot arm, sets GDS_MITG_DIS before the kernel's write. gds_found_disabled, a T14 row on the shared-debug boot, holds that every CPU read it set and left it clear.
  • mitigations_in_a_guest holds a KVM guest's lines and read-backs as the metal row holds a bench's. It prints the guest's lines, its words without signature= and microcode=, which would name the host's model in a public log. It runs on x86-64 KVM only: the words are the hypervisor's, and TCG's CPU enumerates none of the controls.

IBRS on entry, which the decision names for RSBA parts without enhanced IBRS, and AMD's automatic IBRS (EFER) are recorded rather than built: issues/no-entry-sets-ibrs-on-a-cpu-with-rsba-and-no-enhanced-ibrs.md and issues/automatic-ibrs-stays-off-on-an-amd-cpu-that-has-it.md. No proving machine needs either.

Checks

Head 98c605c46, rebased onto main 302fb3698. Round 2 answers review r1: the masked guest lines (B1), and suggestions S1 to S4. The heads moved once more after the host round: mitigations_in_a_guest now prints every line (masked as before), so a green run shows the guest's verdicts too; only tests/toyos.rs differs from the heads the host checks ran at.

  • Host, round 2: the kernel's mitigation tests and the harness's mitigation checks pass, including the new refusal of a CPU that read no IA32_MCU_OPT_CTRL. The full cargo run -- --ci host ran at the top of the stack, Every entry from Ring 3 clears branch history where the CPU's decision names it #1279's c07ac2ef3, which carries this branch: exit 1, 122 of 123 steps green; the red is the build system, the recorded macOS llvm::tests failure (issues/the-llvm-environment-test-breaks-git-on-macos.md). Clippy at this head: 26 invocations clean.
  • Host, round 1 (1a747db24): --ci host exited 1 with 122 of 123 green; the red is the recorded macOS llvm::tests failure (issues/the-llvm-environment-test-breaks-git-on-macos.md). The round 2 edits are a harness message, a doc sentence and one redundant operand.
  • Image: the shipping image and the testcases image on the test kernel both build at the top of the stack.
  • Negative controls (host):
    • mitigations_verdict refuses the T14's words with IBRS read clear, and with GDS_MITG_DIS read set, even when the lines agree with the misread.
    • Patching the decision to ignore IA32_SPEC_CTRL reds the_benches_words_decide_as_their_vendors_rows and mitigations_verdict (patch and log in the evidence).
    • The GDS clear's own mutation needs the T14; it is owed below.
  • Guest: run 43168154333 (mitigations_in_a_guest, shared_debug_members_exit_0), queued. Runs 43151862065 and 43165344560 at earlier heads were cancelled.
  • What the guest runner's CPU shows a guest: the words A pure function decides a CPU's speculation mitigations, and each boot says its verdict #1243's guest run logged show IBRS, IBPB and SSBD enumerated, and no IBRS_ALL or BHI_CTRL. So its decision sets no bit at bring-up, and spec_ctrl should read 0x0. That is an inference until this run's masked words are in the evidence.

Evidence: round 1, https://github.com/ToyOSOrg/evidence/tree/b2652ff9150accb5fd2bdb369edfd78e88fcdb97/r1. Round 2 (host), https://github.com/ToyOSOrg/evidence/tree/53e1770d17f34deea19cb865d3d569cd8c9e55f9/r2. The guest round follows.

Owed on metal (benches down)

Recorded in issues/spec-ctrl-and-gds-stay-as-firmware-left-them.md:

  • the mitigations row, metalcase boot of main, on both benches. On the T14, spec_ctrl=0x1 and mcu_opt_ctrl with bits 0 and 4 clear on every CPU; on the AMD bench, spec_ctrl=0x0;
  • gds_found_disabled, shared-debug boot of main, on the T14, and the same boot with the clear deleted, which must red it;
  • Linux's reads of 0x48 and 0x123 on each bench, compared in evidence.

🤖 Generated with Claude Code

@Japabu

Japabu commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Review r1 of #1277 at 1a747db

Architecture. The work sits where it belongs. The pure decision now owns two things: what each register must hold (Decision::spec_ctrl, mcu_opt_ctrl_clears) and how a read-back is judged (unapplied, in_force). The x86-64 arch module owns the MSR access, at each CPU's bring-up, after its microcode has loaded and before anything else runs. Nothing is added to any hot path. Writing IA32_SPEC_CTRL whole is the approved design. has_spec_ctrl matches Intel's Table 3 enumeration (7.0.EDX[26,27,31], 7.2.EDX[0..4]) and the APM's (Fn8000_0008_EBX IBRS/STIBP/SSBD/PSFD). The IA32_MCU_OPT_CTRL change is a read-modify-write that clears only enumerated opt-outs and writes only when one is set. If firmware has locked an opt-out set, the read-back shows it, the line says vulnerable and unapplied refuses the CPU, which is the right outcome. Enhanced IBRS now requires CPUID.(07H,0):EDX[26], which closes a #GP route. Recording IBRS-on-entry and AutoIBRS without building them matches the approved plan. The record's facts about the guest runner's CPU (IBRS without IBRS_ALL, with RSBA) agree with the guest words #1243's guest run logged.

On the guest runner, the decision sets no bit, so mitigations_in_a_guest cannot tell a write from no write. The PR says this. The write itself is first observable in the metal rows owed under benches-down.

Size: about +120 lines of production code and about +205 of tests and harness. The records grow by about +80 lines: two new records, and spec-ctrl rewritten to Built/Left/Owed.

Evidence: the host CI result is exit 1 with 122 of 123 steps green. The only red is llvm::tests (keyed_and_built, the_caller_s_environment…), which is recorded in issues/the-llvm-environment-test-breaks-git-on-macos.md, and this branch does not touch build/. The control that ignores SPEC_CTRL reds both checks, with the right messages. The r1 logs are masked. The branch merges cleanly onto main 302fb36. Guest run 43151862065 is still queued, and judging it is the orchestrator's.

Blocking

B1 tests/toyos.rs:7212: mitigations_in_a_guest writes every CPU's full words line, signature= and microcode= included, to the run's output with eprintln!. That output is the GitHub Actions log of a public repository. The row runs in this PR's guest dispatch and in every nightly run. The orchestrator's rule masks exactly these fields before any guest words are published and names no CPU model, and this print publishes them on every green run. The row's purpose, showing which controls KVM exposes, needs the CPUID words and the registers, not the signature or microcode. Print the words with those two fields removed. Run 43151862065, already queued at this head, will print them when it runs.

Suggestions

S1 kernel/src/arch/x86_64/mitigation.rs:101: in mcu_opt_ctrl && decision.enumerates(GdsMicrocode), the first operand is implied by the second. Drop it.

S2 kernel/src/arch/x86_64/mitigation.rs:93: the sentence "A register that does not hold another write is its line's to say." is hard to parse. Something like "Any other write that does not take is reported by the lines, not stopped" says the same thing.

S3 tests/toyos.rs:7356: when mcu_opt_ctrl_found is - or missing, the judge reports "GDS_MITG_DIS stands", which is not what happened. Give that case its own message.

S4 issues/spec-ctrl-and-gds-stay-as-firmware-left-them.md:33: if the T14's firmware sets GDS_MITG_LOCK (IA32_MCU_OPT_CTRL bit 5), the arm's write is ignored and gds_found_disabled reds with "did not take". The row's premise is then false, not the kernel. Naming bit 5 in the owed item, or in the judge's message, would let a red on the bench be read correctly.

SEND BACK

…outs its decision names

kernel::mitigation::decide named enhanced IBRS, AMD's always-on IBRS,
BHI_DIS_S and the microcode's RDRAND and gather mitigations, and nothing
set them: every CPU ran under the IA32_SPEC_CTRL and IA32_MCU_OPT_CTRL
firmware left.

- Decision::spec_ctrl is the CPU's IA32_SPEC_CTRL while no program that
  asked runs: IBRS where enhanced IBRS or AMD's IBRS-always-on is named,
  BHI_DIS_S where it is, every other bit clear. arch::mitigation::apply
  writes it whole wherever Facts::has_spec_ctrl says the MSR exists
  (Intel "CPUID Enumeration and Architectural MSRs" Table 3; APM Vol. 3
  Fn8000_0008_EBX).
- Decision::mcu_opt_ctrl_clears names RNGDS_MITG_DIS and GDS_MITG_DIS
  where their mitigation is named; apply clears them and keeps the rest.
- Enhanced IBRS is enumerated only with CPUID.(07H,0):EDX[26], whose bit it
  sets: a CPU claiming IBRS_ALL without it says so rather than taking #GP.
- The words line gives each register as found and as left, the lines take
  what is in force from what was read back, and Decision::unapplied names
  whatever the kernel sets that a register shows off. The metal judge
  refuses a CPU with any, as it refused only LFENCE's before.
- The gds-found-disabled arm sets GDS_MITG_DIS before the kernel's write;
  gds_found_disabled, a T14 row on the shared-debug boot, holds that every
  CPU read it set and left it clear.
- mitigations_in_a_guest holds a KVM guest's lines and read-backs as the
  metal row holds a bench's, and prints its words: what the guest runner's
  CPU shows a guest.

IBRS on entry, which RSBA parts without enhanced IBRS are named, and AMD's
automatic IBRS (EFER) are recorded, not built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 12, 2026
…code

- mitigations_in_a_guest prints each guest CPU's words without signature=
  and microcode=, which name the host's model, into the public run log (B1).
- gds_found_disabled says when a CPU read no IA32_MCU_OPT_CTRL, and names
  GDS_MITG_LOCK where the arm's write did not take; the record says how
  to read that red (S3, S4).
- apply's arm drops an operand the other implies, and its doc says plainly
  that any other write that does not take is reported (S1, S2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit to ToyOSOrg/evidence that referenced this pull request Nov 12, 2026
…14dd245dd5e8c: the masked guest words, the GDS judge's messages; masked
…code

- mitigations_in_a_guest prints each guest CPU's lines, its words without
  signature= and microcode=, which name the host's model, into the public
  run log (B1). The verdict lines show what the guest's decision makes of
  the words once the signature is gone.
- gds_found_disabled says when a CPU read no IA32_MCU_OPT_CTRL, and names
  GDS_MITG_LOCK where the arm's write did not take; the record says how
  to read that red (S3, S4).
- apply's arm drops an operand the other implies, and its doc says plainly
  that any other write that does not take is reported (S1, S2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu

Japabu commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

Review r2 of #1277 at 98c605c

Architecture. The architecture is unchanged since r1. Round 2 touches four files (+23/−9): the guest row's print, two judge messages, one doc sentence and one redundant operand. The head merges cleanly onto current main 29c5f2dfa (checked with git merge-tree).

Round 1

  • B1 resolved for what the row prints. tests/toyos.rs:7213-7219 prints each of a guest CPU's lines without any field that starts with signature= or microcode=.
    • Only the words line carries those two fields. The vulnerability lines are fixed vocabulary from Line's Display (kernel/pure/mitigation.rs:548-597), with no revision or model text.
    • What remains is the vendor, hypervisor, smt, the CPUID words, ARCH_CAPABILITIES and the register values, which are the measured values the owner ruled not identifying.
    • Checked on a real run. Run 43168172877, at Every entry from Ring 3 clears branch history where the CPU's decision names it #1279's head, carries this harness code unchanged. It printed cpu0's lines to its public job log, and that log contains no signature and no microcode= anywhere. Its lines also show spec_ctrl=0x0 spec_ctrl_found=0x0 for the guest, which matches this PR's inference. That run is not at this head, though, and this head's own run 43168154333 is the orchestrator's to judge.
  • S1–S4 taken. The new (None, _) arm is reached only for - or a missing field, and gds_found_disabled_verdict covers it. The GDS_MITG_LOCK (bit 5) reading in the message and the record is correct.

Can identifying fields still reach public output?

Yes, on a red run, by three routes.

  1. This row's own errors quote the raw words line, signature and microcode included:
    • tests/toyos.rs:7164, 7259, 7266, 7269 and 7308.
    • Only a malformed or duplicated kernel line reaches them, which would be a kernel defect.
  2. Serial::must_be_clean puts the whole guest log into its error (tests/common/serial.rs:204-207). That log carries the microcode: line's signature and revision, and the words line. Every guest row shares this path.
  3. The red-run artifact. .github/workflows/guest-rows.yml:60-67 uploads every red run's UART logs as a public artifact, kept 7 days. This is measured:

Routes 2 and 3 are older than this PR: #1173 added the artifact, the microcode loader logs the signature, and #1243 added the words line. Neither belongs to this row, so they do not block it. They do mean the owner's masking rule is not met on any red guest run, and the orchestrator has to act:

  • delete those artifacts if the rule requires it;
  • take to the owner, or record, whether a guest boot should log the signature at all.

Findings

No blocking findings.

S1 tests/toyos.rs:7212 — mitigations_of_a_cpu(...)? passes errors that quote the raw words straight into the public job log. Applying the same field filter to the error text would close route 1. That only matters once routes 2 and 3 are closed.

Evidence

  • Host round 2: exit 1, 122 of 123 steps green, at c07ac2ef3. The only red is the recorded macOS llvm::tests failure.
  • Reuse of that evidence holds: c07ac2ef3 differs from this stack's heads only in tests/toyos.rs's print. git diff c07ac2ef3 a9892c2a8 and git diff 389bda042 98c605c46 each change only that file's 13 lines.
  • Clippy is clean at 389bda042 (a1r2-clippy.log), which differs from this head only in that same print.
  • Guest: run 43168154333 is queued and is the orchestrator's to judge.

LAND

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant