Skip to content

A pure function decides a CPU's speculation mitigations, and each boot says its verdict - #1243

Merged
Japabu merged 6 commits into
mainfrom
cpu-mitigation-decision
Nov 11, 2026
Merged

Japabu merged 6 commits into
mainfrom
cpu-mitigation-decision

Conversation

@Japabu

@Japabu Japabu commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

Nothing decided which speculative-execution vulnerabilities a CPU has or what its vendor prescribes, and no boot said where the machine stands (issues/a-pure-function-decides-a-cpus-speculation-mitigations-from-its-vendors-guidance.md, issues/each-boot-prints-the-vulnerabilities-lines-linux-prints.md). Separately, the kernel never made AMD's LFENCE dispatch serializing, though its own ordered TSC read relies on it.

Behavior

  • The decision. kernel::mitigation::decide is pure and host-tested. It maps a CPU's vendor, signature, microcode revision, guest status, whether another online CPU shares its core, and its enumeration words to a verdict per vulnerability. Each verdict is either not affected, or the mitigations the vendor prescribes plus which of them the CPU does not enumerate.
    • Each rule cites its Intel article or AMD bulletin or white paper.
    • Enumeration decides where the vendor's guidance lets it. Otherwise Intel's affected-processor table (its machine-readable list as of 2026-09-04) decides, and a CPU the table omits is reported as assumed affected. AMD's documents name affected families, so a newer family is held to its _NO bits.
  • ToyOS's policy, stated on each line. Where a vendor prescribes a protection for every program, ToyOS applies it for a program that asks.
    • Between programs (SRSO's IBPB across user/user, SRSO paper §3), the line marks it: "IBPB between programs that ask (policy)".
    • The kernel's own protection. Branch Type Confusion §6.1.1 asks STIBP for Jmp2Ret. That requirement stays out of what can read "mitigated", and the line names the exposure: "; SMT vulnerable: STIBP only for programs that ask (policy)". On Family 15h, Zen and Zen+, which the guidance's appendix (footnotes 5 and 6) says have no STIBP at all, the line instead reads "; SMT vulnerable: this CPU has no STIBP".
    • Each rule quotes AMD's unconditional guidance. The manual's Trust page states the policy, its reason (the cost of the barriers at every switch, and that a program asking gets them), and the consequence for the kernel on AMD processors affected by Branch Type Confusion.
  • What each boot says. Each CPU reads its words at bring-up, after its microcode loads.
    • Once every CPU is up, the boot logs cpu0's words, plus the registers that show what is in force (mcu_opt_ctrl, de_cfg2, c001_1029, and C001_1029 as found before the kernel's write), and one line per vulnerability: not affected, mitigated: …, or vulnerable: needs …; has …; this CPU enumerates no …. A later CPU logs only where it differs.
    • smt comes from the topology of the CPUs actually brought up, by APIC ID, not from CPUID.0BH/1FH EBX[15:0]. SDM Vol. 1, Chapter 21 says that field must not be used to enumerate topology.
  • One control is applied: AMD LFENCE dispatch serializing.
    • AMD 70586 ("Software Techniques for Managing Speculation", mitigation G-2) sets MSR C001_1029[1]. Per APM Vol. 3's LFENCE entry, CPUID Fn8000_0021_EAX[2] means LFENCE is always serializing.
    • The clocksource's rdtsc_ordered (RDTSCP; LFENCE) relies on it, as AMD's V1-1 bounds-check barrier does.
    • The decision names it on spectre_v1 for every AMD family the paper gives the MSR (10h, 12h to 17h, and later), unless CPUID says LFENCE always serializes; 0Fh and 11h always serialize.
    • Each CPU sets the bit right after its microcode loads, and stops the machine if the bit does not then read set, as a CPU without SMEP does.
    • A guest cannot write its host's MSR, so a guest is reported lacking it (recorded under Left).
  • Intel's enhanced IBRS needs no STIBP (§2.4.1.3, "there is no need to use STIBP when IBRS is set"). AMD's Spectre v2 relies on SMEP, not a return-predictor fill. Branch Type Confusion §5 asks for "a RAP stuffing sequence (mitigation V2-3) and/or SMEP". The kernel holds SMEP on every CPU, and SMEP keeps Ring 0 from fetching the user addresses user code leaves in the predictor (70586 G-3).
  • MMIO stale data is decided by which propagators a part has. Per Intel's "Processor MMIO Stale Data Vulnerabilities":
    • VERW's fill-buffer overwrite blocks the fill buffer propagator. It is asked where the part has that propagator, and is enumerated by FB_CLEAR, or by MD_CLEAR plus L1D_FLUSH where MDS affects the part.
    • Against Shared Buffers Data Read, the microcode's RDRAND/RDSEED/EGETKEY overwrite of the uncore buffers "is also relevant". It is asked where the part has that vulnerability, and SRBDS_CTRL enumerates its control.
    • Against the primary propagator, a non-secret MMIO read after each secret one ("Mitigating PSDP Propagation") is asked, unless the part enumerates MDS_NO, FBSDP_NO, and TAA_NO or no TSX, the condition PSDP_NO's paragraph gives.
    • This departs from the article's selection pseudocode, which asks a microcode update of every affected part without FB_CLEAR. The rule follows the article's fill-buffer section, which gives FB_CLEAR only to parts the fill buffer propagator affects.
    • Pinning the T14 exposed the old rule: it asked VERW wherever the table listed the part. The T14 runs the microcode Intel lists (0xbe) and enumerates FBSDP_NO and no FB_CLEAR. Intel's list has one combined SBDS/SBDR cell, "MCU", for it, and on the T14 (no MDS, no TSX) only SBDR can apply. Its line is now mitigated: microcode's RDRAND and RDSEED mitigation.
  • Nothing else is applied, and syscall entry, exit and context switch are unchanged.

The manual's kernel page gains a "Speculative execution" section, and its Trust page a section titled "Speculative execution applied on request".

Tests

  • Pure module: 19 host tests in kernel/pure/mitigation.rs. Among them are rules for Intel 806C1, 806F8 and 506E3, AMD Family 17h Model 60h, Zen+ and 19h, the policy marking and the SMT exposure, the primary propagator's condition, and what_is_in_force_is_what_the_registers_show.
  • What host tests cannot reach: that the boot CPU's path allocates nothing before the allocator is up. The values it handles are Copy, so they own no heap, and its slot is a static. The guest row below is the check that the head boots.
  • checks::the_benches_words_decide_as_their_vendors_rows: holds each bench's pinned words, as its own boot at 3b07ea1 logged them, to the lines its vendor's documents give that CPU. For the T14 that is CPUID 806C1's row of Intel's list on its listed microcode; for the AMD bench, AMD's documents for Family 17h Model 60h.
  • The metal judge: the mitigations row (T14 and AMD bench) judges every CPU that logged lines.
    • Its words must be its bench's pinned words, all but the microcode revision, which the microcode row holds.
    • Each line must be decide's verdict on those words, with what its logged registers show in force.
    • The LFENCE bit the kernel sets must be in force wherever the decision names it.
  • checks::mitigations_verdict: holds the judge to a boot's own lines and to a later CPU's own lines. It refuses a later CPU whose LFENCE bit reads clear, a misread whose lines agree with it (only the pin catches that), the T14 with ARCH_CAPABILITIES read as 0 (the record's mutation), a machine no bench is, a guest, and missing words, lines or registers.
  • Metal only: a guest's words are its hypervisor's. One x86-64 SMP guest row shows that the new code boots under KVM.

Evidence

The head is 003c0e3e2, rebased onto main 3b93f3770 (r6 index).

  • The rebase: its one new commit declares the mitigations row on: On::Every. A metal row declares its benches, a boot names its bench, and judges name the one they judged #1254 made every metal row name its benches, and this row ran on both.
  • Checks at the head: toyos-checks (52), toyos-build clippy, the pure tests (19) and the x86-64 kernel clippy all exit 0 (log).
  • The kernel is unchanged across the rebase. The range-diff differs only in context lines (range-diff), and the kernel diff adds and removes the same 1799 lines as at 8c203f6e8.
  • What the guest booted: 8c203f6e8 differs from 8d57dcf87 only in two comments, one assertion inside the pure module's test module, and the lines record (r5).

The checks below ran at 8d57dcf87. Index: r4/index.md.

Host checks, chosen for what the change touches. All exit 0 at 8d57dcf87 (log):

  • cargo test -p kernel --lib mitigation: 19 passed.
  • cargo test --test toyos-checks: 47 passed. This includes mitigations_verdict, the_benches_words_decide_as_their_vendors_rows and microcode_verdict.
  • clippy on the kernel: the host library and tests with sched-check, and x86_64-unknown-none by default and with boot-actuators,test-actuators. clippy on toyos-build --all-targets. All with the adopted lints and -D warnings. AArch64 is untouched.
  • Not run: the full --ci host, because nothing in the build system or harness mechanics changed. The merge queue runs it.

Negative controls, all at 8d57dcf87 (r4 index). Each is applied with git apply, tested, and reversed with git apply -R; the tree's digest is identical before and after. Each builds and fails with exit 101:

  • the AMD G-2 rule removed;
  • family 17h out of AMD's named families;
  • Intel's 806C1 row dropped (the record's mutation, failing the T14's bench test);
  • STIBP beside enhanced IBRS;
  • SRSO's IBPB unmarked;
  • the retbleed SMT exposure dropped;
  • the MMIO rule before ab39884e2;
  • the primary propagator's mitigation made unconditional;
  • the judge reading cpu0 alone;
  • the judge holding no pin;
  • the judge skipping the kernel's LFENCE check.

Guest:

  • At ab39884e2, run 43077064338 (log) failed with EARLY PANIC … memory allocation of 480 bytes failed. That is the defect this round fixes.
  • At 8d57dcf87, whose kernel the head carries unchanged, run 43082719564 (std_threads_count_the_cpus_and_give_back_ended_threads --nocapture) is green (log).
    • The x86-64 guest booted through both vCPUs with no EARLY PANIC and no mitigation: panic.
    • It logged mitigations: cpu0 words Intel … hypervisor=true smt=false … mcu_opt_ctrl=- de_cfg2=- c001_1029=- c001_1029_found=- and its 17 lines. The second vCPU, being alike, logged none.
    • The row passed with 2 CPUs.

Metal is not a landing condition: both benches are down. Both benches' metalcase boots at 3b07ea1dd passed, and their cpu0 words lines, minus microcode=, are the pinned words (T14, AMD). The boots of ab39884e2 did not come back (B1).

Owed on metal, recorded under the lines record's Left, for one batched boot of main once a bench returns: the mitigations and microcode rows, one metalcase boot each on the T14 and the AMD bench. Main must show:

  • Both benches: both rows green, with no hang and no early or mitigation: panic. A later CPU logs lines only where its registers differ.
  • The T14: cpu0's lines are exactly T14_BENCH_LINES, and mcu_opt_ctrl has bits 0 (RNGDS_MITG_DIS) and 4 (GDS_MITG_DIS) clear. A set bit is to be reported, because those lines assume both clear.
  • The AMD bench: cpu0's lines are exactly AMD_BENCH_LINES, and de_cfg2 and c001_1029 each have bit 1 set. The value of c001_1029_found is to be reported: it tells whether firmware had already set the bit.

Earlier rounds: r1 at 3b07ea1dd, r2 at a2350e613, r3 at ab39884e2.

Risks and open items

  • Round 1's head did not boot. At ab39884e2, record grew a Vec on the boot CPU before the allocator was up, so every x86-64 boot panicked early. Both benches' boots of it hung, and they have not come back since. This head fixes that. Run 43082719564 boots its kernel under KVM; no boot of it on metal exists yet.
  • The kernel's exposure under the ruled policy. On AMD processors affected by Branch Type Confusion, while a CPU runs a program that has not asked, the core's other thread can steer the kernel's speculative returns. Where the processor has no STIBP, that holds whenever SMT is on. The lines say "SMT vulnerable", and the Trust page states it.
  • Shared Buffers Data Read lets a program with MMIO access read stale data that other cores' MMIO left in the uncore buffers. The decision names only the microcode's overwrite, and the gap is recorded under the record's Left, with why the T14 keeps it.
  • The AMD readback cannot show that ToyOS's write set the bit: firmware may already set C001_1029[1]. The c001_1029_found field shows which.

🤖 Generated with Claude Code

Japabu added a commit that referenced this pull request Nov 11, 2026
…s the read section's reader orders the recheck

- AArch64's counter is `ISB; MRS; ISB` again. The Arm ARM (K.a D12.2.2) lets
  a counter read happen speculatively and out of order against other
  instructions; nothing architectural orders it before a later store that
  does not depend on its value, so x86's argument does not carry over, and
  no AArch64 bench could measure the change. Its doc no longer says it reads
  as the x86-64 one does.
- The latch no longer says nothing orders its recheck after `inside`'s read:
  the kernel reads the same page through `cpu::counter_ordered`, whose
  trailing fence does. Whether the recheck may be answered first is
  `inside`'s choice, and the clock page's header says which reader pairs a
  line with a counter read just after it was replaced.
- `issues/the-kernel-relies-on-an-amd-lfence-it-never-makes-dispatch-serializing.md`
  records that `cpu::rdtsc_ordered` and `cpu::wrmsr_fence` rely on an AMD
  LFENCE the kernel never makes dispatch serializing; #1243 is its exit.

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

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1243 at 3b07ea1 (round 1)

Architecture

The split is right:

  • the decision is a pure function in kernel/pure, tested on the host;
  • arch/x86_64 holds the reads and the one MSR write;
  • a metal row covers the rest.

Size: about 925 lines of production code in kernel/pure/mitigation.rs, of which the Intel table is about 70, plus 159 lines in arch, cpu and microcode. Tests add about 405 lines in the pure module and 140 in the harness (the judge and its test). For a table that cites a vendor document per rule, that size is justified.

What I checked against the vendors' documents

Intel.

  • IA32_ARCH_CAPABILITIES bits 0, 1, 2, 4–8, 13–15, 17, 19, 20, 25–28 and 62, and the CPUID bits 7.0 EDX 9/10/26–29/31 and 7.2 EDX 2/4, all match Intel's "CPUID Enumeration and Architectural MSRs" Tables 1 and 2.
  • The IA32_MCU_OPT_CTRL read is gated as that article's Table 6 allows.

The table and the row for 806C1.

  • I regenerated all 67 rows of INTEL_ROWS from the machine-readable list in the evidence. They match; the only differences are how CPUIDs the list combines into one row were split out.
  • The row for 806C1 is right: MMIO through SBDS ("MCU"), GDS ("MCU"), ITS (IBPB "MCU", cBPF "Software") and BHI ("Software"), and no other column.
  • INTEL_RSBA matches Retpoline's Table 5.
  • The ITS rule follows the algorithm in Intel's ITS article.
  • The rule that RRSBA_CTRL means not affected follows the RSBU article ("not affected … regardless of the setting of IBRS or RRSBA_DIS_S").

AMD family 17h.

  • The Zen 2 model ranges match the BTC guidance's appendix and §5 of the SRSO white paper.
  • The Fn8000_0008_EBX and Fn8000_0021_EAX/ECX bits match APM Vol. 3, rev 3.39, §E.4.
  • The TSA patch levels match the appendix of the TSA guidance, rev 3.0.
  • The families given the MSR match 70586 G-2. The code's "12h to 17h" includes 13h, which does not exist; that is harmless.

AMD LFENCE.

  • The decision drops the MSR when Fn8000_0021_EAX[2] is set. APM Vol. 3's LFENCE entry: "In systems where CPUID Fn8000_0021_EAX[LFenceAlwaysSerializing](bit 2) = 1, LFENCE is always dispatch serializing." It also drops it for families 0Fh and 11h, per G-2. It never writes the MSR in a guest.
  • So the kernel does read EAX[2] first and skips the write; nothing more is needed there.
  • The write happens on the BSP right after microcode::load(0), and on each AP in ap_entry before clocksource::start_ap. ToyOS has no S3 resume and does not bring an AP back once it is offline, so there is no other path to cover.
  • applied() reads the bit back on every CPU.

cpu::wrmsr_fence on Zen 2. It is correct whether the bit is set or not. APM Vol. 3 rev 3.39, MFENCE: "The MFENCE instruction is a serializing instruction" (clarified since rev 3.15). So no WRMSR after it starts before the earlier loads and stores complete.

The note that after #1240 only wrmsr_fence relies on LFENCE ordering has it backwards. #1240 changes only userland's counter() (in sdk/abi and std). The kernel's cpu::rdtsc_ordered, which is RDTSCP; LFENCE, still serves the clocksource at clocksource.rs:102, 104, 169, 171, 418, 429, 501. That read is what relies on the bit.

Required

  1. STIBP is prescribed alongside enhanced IBRS.
    • Location: kernel/pure/mitigation.rs:530-543, with the tests at :996 and :1103.
    • Intel, "Speculative Execution Side Channel Mitigations" §2.4.1.3: once IBRS is set on a part with enhanced IBRS, the predicted targets of indirect branches "cannot be controlled by software that was executed … on another logical processor"; and "Again, there is no need to use STIBP when IBRS is set."
    • Consequence: the T14 (806C1, with SMT) will print spectre_v2: … needs … STIBP for programs that ask, which contradicts the vendor.
    • Fix: drop Stibp when the branch mitigation is EnhancedIbrs, and correct the 806C1 and 806F8 expectations.
  2. AMD rules use an opt-in mitigation where AMD prescribes an unconditional one.
    • Location: kernel/pure/mitigation.rs:726-730, and :740.
    • Retbleed: BTC guidance rev 3.0, §6.1.1 "SMT Safety": "If the code on the sibling thread cannot be trusted, software should set STIBP to 1 or disable SMT to ensure SMT safety when using Jmp2Ret." That STIBP protects the kernel's return thunk from a sibling. It is not something a program opts into, yet the line prints "STIBP for programs that ask".
    • SRSO: the SRSO white paper rev 2.0, §3: "Mitigation of SRSO across user/user … boundaries requires the use of IBPB unless SRSO_NO is 1".
    • Consequence: the manual section this PR adds makes the vendor's guidance the only authority, and later mitigation work will apply what the decision names. These rules would make it under-apply.
    • Fix: give each its own mitigation (STIBP while SMT is on, or SMT off; IBPB at every switch between programs). If ToyOS means an opt-in policy, that is the owner's decision to make in the manual, not a relabel in the table.
  3. smt is read from a field Intel says not to use for topology.
    • Location: kernel/src/arch/x86_64/cpu.rs:622-633, which feeds Facts::smt at kernel/pure/mitigation.rs:36.
    • The field is EBX[15:0] of CPUID.1FH/0BH. Intel SDM Vol. 1 (September 2026), Chapter 21, leaves 0BH and 1FH: "This number reflects configuration as shipped by Intel … Software must not use the value of EBX[15:0] to enumerate processor topology of the system … The actual number of logical processors available … may be different."
    • Consequence: with HT disabled in firmware, the field's contract ("More than one logical processor shares this CPU's core") is false. On an AMD part with no populated 0BH thread level, the code falls back to 1 and under-reports.
    • Fix, either of:
      • derive smt from the CPUs the kernel actually brought up (siblings under the SMT shift, which smp already collects as topology keys), reporting cpu0's lines after bring-up; or
      • name the field for what it reads and record the over- and under-approximation under Left.
    • The citation also needs a fix: the current SDM keeps the CPUID leaf tables in Vol. 1, Chapter 21, not Vol. 2A.
  4. The claim that the x2APIC fence relies on dispatch-serializing LFENCE is false on AMD, because MFENCE is serializing there (see above).
    • Location: kernel/src/arch/x86_64/mitigation.rs:91-95, issues/a-pure-function-…-guidance.md:24-27, and the commit message.
    • Fix: drop the x2APIC claim from all three.
  5. Both issues' exits were weakened without saying so.
    • Location: issues/each-boot-prints-the-vulnerabilities-lines-linux-prints.md:19-27 and issues/a-pure-function-…-guidance.md:44-46.
    • Dropped or softened:
      • "on the T14 the words it logs equal the T14's words that issue commits with their provenance";
      • "Mutation: ARCH_CAPABILITIES read as 0 reds the T14", now "changes the T14's lines", which no check can fail on;
      • "spec_store_bypass reads 'Mitigation: Speculative Store Bypass disabled per program'";
      • "committed with its provenance".
    • These were set in 07c881c, on the orchestrator's ruling that oracles are the vendor's guidance and ToyOS's own reads.
    • Fix: building part of an issue does not change what it owes. Restore these clauses, and put what is still unmet under Left.
  6. The metal judge reads only cpu0.
    • Location: tests/toyos.rs:6909 onward.
    • Consequence: an AP whose C001_1029[1] write did not take logs mitigations: cpuN spectre_v1: vulnerable: needs LFENCE dispatch-serializing, and the row still passes. The PR claims every CPU sets the bit.
    • Fix: judge every CPU that logged lines, each against its own words. On these two benches, whose CPUs are all alike, it would also be enough to refuse any later CPU's lines.
  7. A commit-message paragraph describes code that main never had. "AMD's branchless user-pointer clamp is the paper's V1-2, not V1-1 … both citations are corrected." Neither citation exists on main; the paragraph describes an earlier revision of this branch. Delete it. The PR body's matching bullet is the orchestrator's to correct.
  8. An x86-64 guest boot is needed before landing.
    • Every x86-64 QEMU boot now runs facts, decide and report, with MSR reads under KVM's -cpu host, on the BSP before the IDT exists and on each AP.
    • This code has not booted anywhere, and the merge queue runs no guest rows, so a broken guest boot would first show up in the nightly run.
    • A guest cannot show the decision, but it can show the change does not break the boot. Dispatch one x86-64 SMP guest row at the final head. It must boot through and log a mitigations: cpu0 words … hypervisor=true line.

Should the benches' words be pinned now?

Yes: in this PR, from the readback already queued (without the microcode revision, per the orchestrator), as a follow-up commit before landing.

  • Why not for trust. Inspection already establishes that the reads are right today. The leaves, subleaves, MSR addresses and gates match the Intel and AMD documents cited above. Pinning is not what makes this boot's words trustworthy.
  • Why it matters. It is what makes a later misread fail. The metal judge recomputes decide from the words the same boot logged. A boot that read ARCH_CAPABILITIES as 0 on the T14 would print "not affected" for BHI and ITS and still pass. This is the issue's own Mutation.
  • What closing it takes. The bench rows in the host tests must carry the logged words, and the judge must hold a boot's logged words to the committed row for its signature.
  • What then becomes redundant. The judge's search over all eight readings of the three registers.
  • The minimum. If the implementer would rather not pin here, Required 5 (restore the exits) is the minimum, and pinning stays under Left.

What the readback must show

T14:

  • Words line. Intel, signature=0x806c1, hypervisor=false, smt=true.
  • arch_capabilities bits that must be set:
    • IBRS_ALL (bit 1): Intel's list gives this row's Spectre v2 as "Hardware+Software".
    • FB_CLEAR (bit 17) and GDS_CTRL (bit 25): its SBDS and GDS entries are "MCU". If either is clear, compare the loaded microcode revision with the list's.
  • Bits that must be clear: BHI_NO (20), GDS_NO (26) and ITS_NO (62): the row lists BHI, GDS and ITS.
  • Lines (after fix 1):
    • spectre_v2: vulnerable: needs enhanced IBRS, IBPB between programs that ask; has SMEP
    • branch_history_injection: needs a BHB clear;
    • indirect_target_selection: needs indirect branches in a line's upper half;
    • mmio_stale_data: needs VERW;
    • gather_data_sampling: mitigated: microcode's gather mitigation. GDS_MITG_DIS defaults to 0 per Intel's IA32_MCU_OPT_CTRL table, unless firmware opted out.
    • spectre_v1: mitigated by the clamp.
    • meltdown, l1tf, mds, tsx_async_abort, srbds, reg_file_data_sampling, itlb_multihit, retbleed, spec_rstack_overflow and tsa: not affected.
  • No later CPU's mitigations: lines.

AMD bench:

  • Words line.
    • Amd, signature=0x860f01, hypervisor=false.
    • smt=true, consistent with the online CPU count. The PR lists this as unverified, and the judge does not hold it.
    • cpuid.80000008.ebx has IBPB (12), STIBP (15) and SSBD (24) set.
    • cpuid.80000021.eax is 0.
  • Lines:
    • spectre_v1: mitigated: user pointers clamped without a branch, LFENCE dispatch-serializing: the read-back.
    • retbleed lists DE_CFG2[SuppressBPOnNonBr] under "has". The BTC guidance's appendix, footnote 7, says microcode 08600109h or newer sets it on Family 17h Model 60h Stepping 1, and ToyOS loads 0x0860010D (that boot's microcode row).
    • spec_rstack_overflow needs IBPB and Safe-RET.
  • No later CPU's lines.
  • What it cannot show. This readback cannot prove that ToyOS's write set the bit: AGESA may already set C001_1029[1].

Both: the microcode row is green.

Suggestions

  • Log C001_1029 as found before the write, as one more field. That separates the bit firmware set from the bit the kernel set, which this readback otherwise cannot.
  • Fail loudly if the bit reads back clear outside a guest. The kernel's own rdtsc_ordered depends on it, so this would follow control_regs' rule for SMEP rather than only reporting it.
  • Return-predictor fill plus SMEP. The AMD Spectre v2 rule asks for both. BTC guidance §5 says "a RAP stuffing sequence (mitigation V2-3 …) and/or … SMEP". Either cite why both are needed, or take one.

SEND BACK. Of the required items, 1–3 and 6 change code. 4, 5 and 7 change only comments, records and the commit message. 8 and the metal readback are evidence that must arrive before landing.

Japabu added a commit that referenced this pull request Nov 11, 2026
…#MC and #DF install it whatever GS holds

The kernel ran no `swapgs`: GS held this CPU's `PerCpu` from boot on, and
every entry's first stores went through `gs:`. Ring 3 replaces the GS
base with a segment load (`mov gs, 0x1b`: the user data descriptor, base
0), so its next syscall stored near address 0 at CPL 0 on its own stack.
`SYSCALL` switches no stack and `#PF` has no IST, so the fault's frame was
pushed to the stack pointer the program chose, a kernel address if it
chose one, and every handler after it addressed the same base. Found by
reading in #1251; the previous commit's two rows red on that kernel.

Now `IA32_GS_BASE` is Ring 3's own while it runs and the kernel's while
the kernel runs, with `IA32_KERNEL_GS_BASE` the other:

- `SYSCALL` comes only from Ring 3 and swaps first and on its way out.
  The image gate on the shipping `syscall_entry` now requires it to open
  `swapgs`, `cld`, the save and the switch.
- Every other gate swaps when its frame's `CS` is Ring 3's, and back
  before an `iretq` to Ring 3. That decision is sound because the
  kernel's CPL-0 spans with a user's base live (before `SYSCALL`'s
  `swapgs`, and from an exit's `swapgs` to its `SYSRET` or `IRETQ`) run
  with `IF` clear and no instruction that can fault: `SYSRET`'s `rcx` is
  canonical by the user half's placement, and every `iretq` frame is the
  CPU's or a validated entry's. No `#DB` lands there: nothing arms a
  debug register, `IA32_FMASK` clears `TF`, and a single-step trap after
  `MOV SS` is suppressed, not deferred (SDM Vol. 3A §7.8.3).
- NMI, `#MC` and `#DF` can land in those spans and their frame does not
  say so. They take the kernel's base from `GDTR`, whose base is the
  block's own `gdt`, and write it to `IA32_GS_BASE` whatever it held;
  the NMI puts back the base it found before its `iretq`. They decide
  nothing: a test of the found base's sign would hold only while no
  thread can write its own GS base, and `WRGSBASE` writes any canonical
  one, the kernel half's included (`issues/fsgsbase-is-taken.md`).
  `#DF` and `#MC` never return, so any dispatched vector on an IST stack
  goes through the new `fatal_entry`.

Speculation (CVE-2019-1125): the gate's `swapgs` is conditional, so an
`LFENCE` follows the join, before any load through GS, which is Intel's
placement for both a skipped and an extra `swapgs` ("Speculative Behavior
of SWAPGS and Segment Registers", ID 758371). The unconditional
`SYSCALL` swap needs none, the exits load nothing through GS after their
`swapgs` and end in a serializing `IRETQ` or in `SYSRET`, and the IST
entries have no branch before their serializing `WRMSR` (SDM Vol. 3A
§11.3). On AMD the fence orders dispatch only where `LFENCE` is
dispatch-serializing (AMD 70586 G-2, MSR C001_1029[1]), which #1243 sets;
AMD's own statement on SWAPGS was not reachable to read.

The switch moves no GS base: every descriptor Ring 3 may load into GS
has base 0 (asserted at compile time beside the GDT), AMD's null load
keeps the base it found, and `CR4.FSGSBASE` stays forbidden, so each CPU's
`IA32_KERNEL_GS_BASE` starts at 0 and every thread's user GS base is 0.
The switch's save and restore of it belongs with taking FSGSBASE.

AArch64 has no GS and is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
… from the CPUs up, and ToyOS's policy marked

STIBP is no longer asked of an Intel CPU with enhanced IBRS: "there is no
need to use STIBP when IBRS is set" (Speculative Execution Side Channel
Mitigations §2.4.1.3). The T14 and 806F8 drop it.

Where AMD prescribes a protection between programs for every program
(Branch Type Confusion §6.1.1's STIBP for Jmp2Ret, the SRSO paper §3's IBPB
across user/user), the decision keeps ToyOS's policy, which applies it for a
program that asks, and its verdict carries the deviation: the line reads
"STIBP for programs that ask (policy)". Each rule quotes AMD's unconditional
guidance. The policy and its reason are on the manual's Trust page.

AMD's Spectre v2 rule no longer asks for a return predictor fill beside SMEP:
Branch Type Confusion §5 keeps the return address predictor safe with "a RAP
stuffing sequence (mitigation V2-3) and/or SMEP", and SMEP, which every CPU
holds, keeps Ring 0 from fetching the user addresses user code's calls leave
there (70586 G-3). `ReturnPredictorFill` goes with it.

A CPU's `smt` is whether another CPU the kernel brought up shares its core,
by the cores the topology places their APIC IDs in, rather than CPUID.0BH or
1FH EBX[15:0], which the SDM (Vol. 1, Chapter 21) says is the count as
shipped and must not enumerate the topology. So each CPU records its words
and registers at bring-up, and once `boot_aps` has installed the topology the
BSP says cpu0's lines and those of each later CPU that differ.
`cpu::threads_per_core` goes.

The words line now carries the registers that show what is in force
(`mcu_opt_ctrl`, `de_cfg2`, `c001_1029`, `-` where not read) and C001_1029
as `apply` found it before setting LFENCE dispatch serializing, which tells
the bit firmware set from the bit the kernel set. What is in force is
`Decision::in_force` on those registers, with `ALWAYS` the one list of what
the kernel does on every CPU. `apply` stops the machine if the bit does not
hold after its write, as a CPU without SMEP stops it: the clocksource's
`RDTSCP; LFENCE` relies on it.

The `mitigations` metal row judges every CPU that logged lines, each against
its own words and registers, and holds a bench's words to its pinned words.
The AMD bench's are pinned from its boot of 3b07ea1, but the microcode
revision, which the `microcode` row holds; the search over register readings
goes. `checks::the_benches_words_decide_as_their_vendors_rows` holds the
pinned words to AMD's documents for Family 17h Model 60h, and
`checks::mitigations_verdict` holds the judge to a later CPU's own lines and
refuses a later CPU whose LFENCE bit reads clear, words other than the
bench's, and a machine no bench is.

Both issue records' exits are again as main has them; what is built is
under Built and what is unmet under Left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the cpu-mitigation-decision branch from 3b07ea1 to a2350e6 Compare November 11, 2026 17:40
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1243 at ab39884 (round 2)

Scope

This round checks only two things: whether round 1's required items R1–R8 are resolved, and whether the code that changed since 3b07ea1dd is correct. That code is now a2350e613 and ab39884e2 on top of the rebased a8923c9de; the rebase changed only the commit message. Since round 1 the branch has changed +649/−276 lines in 11 files.

Architecture

The round-2 shape is right:

  • Recording and reporting. Each CPU records its words at bring-up, and the BSP reports once smp has installed the topology. That is where "another CPU shares this core" is known.
  • One definition of "in force". Decision::in_force lives in the pure module. Both the kernel and the metal judge use it, so there is one definition.
  • Pinned words. The bench words in the harness turn round 1's Mutation into a failing check.
  • The return-predictor fill. Deleting the mitigation and its variant is a simplification backed by the vendor (see below).

Two problems remain:

  • The new per-CPU storage breaks every x86-64 boot (B1).
  • The policy representation hides the one exposure the ruling accepted (B2).

Round 1's items

  • R1, resolved. .and_if(f.smt && !enhanced, Stibp, …). The tests for 806C1 and 806F8 are corrected, and a case for 806C1 without IBRS_ALL keeps STIBP.
  • R2, implemented as ruled. The presentation is not yet plain; see B2.
  • R3, resolved in design. smt is topology.core(cpu).len() > 1 over the online CPUs. core is keyed by APIC ID above the SMT shift, which is the documented use of 0BH/1FH. cpu::threads_per_core is gone.
    • The 3b07ea1 readbacks agree with the new rule: the T14's cpu0 and cpu4 share core 0, and the AMD bench's cpu0 and cpu1 share core 0.
    • B1 is in this new code.
  • R4, resolved. Nothing in the diff, the commit messages or the PR body mentions x2APIC, wrmsr_fence or MFENCE. The comment at arch/x86_64/mitigation.rs:74-79 and the record's Left name only the clocksource's RDTSCP; LFENCE.
  • R5, resolved. Both Exit sections are byte-identical to main's. Unmet clauses are under Left.
  • R6, resolved.
    • mitigations_on_metal groups lines by CPU and judges each CPU against its own words and registers.
    • The judge-reads-cpu0-alone control fails with "a later CPU's LFENCE bit read clear was accepted".
  • R7, resolved. The V1-2 paragraph is gone from a8923c9de's message (range-diff against 3b07ea1dd).
  • R8, open. Run 43077064338 is pending. B1 says what it will show.

Checked against the vendors' documents

AMD Spectre v2: SMEP alone. Correct.

  • BTC guidance rev 3.0 §5 reads: "AMD recommends software use a RAP stuffing sequence (mitigation V2-3 in [2]) and/or Supervisor Mode Execution Protection (SMEP) to ensure that the addresses in the RAP are safe for speculation."
  • 70586 rev 5.09.23 G-3 reads: "The processor will never speculatively fetch instruction bytes in supervisor mode if the RIP address points to a user page."
  • ReturnPredictorFill, its name and its slot in ALL are deleted together.
  • The code's quotation abbreviates "Supervisor Mode Execution Protection (SMEP)" to "SMEP" inside the quotation marks. This is harmless.

C001_1029. Correct as written.

  • apply reads the MSR before the write and returns that value as c001_1029_found.
  • It asserts the bit after the write. It is gated on enumerates(LfenceDispatchSerializing), which is false in a guest (!f.hypervisor makes it lacking) and false where Fn8000_0021_EAX[2] is set or the family is 0Fh or 11h.
  • The G-2 text matches the comment: "AMD family 0Fh/11h processors support LFENCE as serializing always but do not support this MSR. AMD plans support for this MSR … for all future processors."

The judge. Correct.

  • It parses mcu_opt_ctrl, de_cfg2 and c001_1029 (- means not read) and computes decision.in_force(&registers).
  • It refuses a clear LFENCE bit wherever the decision names it.
  • It requires each line to equal decision.line(of, in_force).

The pins. Correct. Both MITIGATION_BENCHES strings are the cpu0 words lines of the 3b07ea1 readbacks, minus microcode=, character for character (evidence r3 for the T14, r2 for the AMD bench).

The MMIO rule, the substantive claim. It is right for the T14. I checked it against Intel's "Processor MMIO Stale Data Vulnerabilities" and the consolidated affected-processor table, both fetched today.

  • The table's row for 806C1 (Mobile, MCU 0xbe):
    • DRPW: "Not Affected".
    • The single combined column "SBDS … / SBDR …": "MCU".
    • SRBDS: "Not Affected".
    • SBDS and SBDR share that one column, so "Intel's list calls its Shared Buffers Data Read 'MCU'" in the commit message reads one cell as two. On the T14 only SBDR can apply, since it has neither MDS nor TSX.
  • The T14's words.
    • It enumerates SBDR_SSDP_NO=0, FBSDP_NO=1 and PSDP_NO=0, with no FB_CLEAR.
    • It enumerates MDS_NO. RTM and HLE are clear, so it has no TSX.
    • It enumerates SRBDS_CTRL (7.0 EDX[9]).
  • What the article supports. Three passages support the new rule for this part:
    • "RDRAND/RDSEED/EGETKEY Protection": "This type of mitigation is also relevant for processors affected by SBDR or SSDP … Microcode updates have been released to add this mitigation to processors that were not affected by SRBDS but are affected by SBDR or SSDP."
    • "Enumeration": updates "may set SRBDS_CTRL … on parts that did not previously enumerate it".
    • "Fill Buffer Clearing Operations": FB_CLEAR is issued "for processors affected by FBSDP".
  • So the old line was wrong. It asked FB_CLEAR of a part that is not affected by FBSDP.
  • The new line follows the vendor. mitigated: microcode's RDRAND and RDSEED mitigation follows Intel's "MCU" for this row, provided IA32_MCU_OPT_CTRL[0] reads clear on the bench.
  • A departure to record. The rule departs from the article's own "Example Mitigation Selection" pseudocode, which says "Microcode update needed" for any affected part without FB_CLEAR. The passage above about FBSDP is the reason, and the rule's comment could say so in one clause.

Required

B1. Every x86-64 boot now panics on the BSP before mm::init.

  • Location. kernel/src/arch/x86_64/mitigation.rs:110-114. record takes READ: Lock<Vec<Option<Read>>> and calls all.resize(at + 1, None).

  • Where it runs. On the BSP, record(0, …) runs from boot::after_console (boot.rs:69). kernel_main calls that at main.rs:244, while mm::init, which first moves the allocator out of PHASE_UNINIT, is at main.rs:369. main.rs:246 says so itself: "percpu, the allocator and our own paging aren't up yet".

  • What happens. In PHASE_UNINIT, GlobalAlloc::alloc returns null (mm/alloc.rs:536). The resize from capacity 0 therefore turns into an allocation-failure panic, before the IDT exists.

  • What was different. Round 1's code logged directly at this point and allocated nothing. This came in with a2350e613. No x86-64 boot has run it yet: the guest row dispatched at a2350e613 was cancelled.

  • Consequences.

    • Guest run 43077064338 cannot show the R8 line.
    • The metalcase queued at ab39884e2 cannot boot on either bench. Hold it.
  • Fix. Keep the BSP's path off the heap. Two ways:

    • a fixed [Option<Read>; MAX_CPUS] (kernel::sched::affinity::MAX_CPUS);
    • or record the BSP's words after mm::init, for instance in start_other_cpus, carrying only apply's c001_1029_found from after_console. The CPUID words do not change once the microcode has loaded.

    Either way, record can take the facts apply already read, rather than reading CPUID and deciding a second time.

  • What will show the fix. The R8 guest row at the fixed head.

B2. The retbleed verdict and the Trust page do not state the exposure the ruling accepts.

  • The problem. The retbleed rule puts the policy's reduced form into needs (mitigation.rs:833, .by_policy(f.smt, Stibp, stibp)). The line therefore reads vulnerable: needs STIBP for programs that ask (policy), Jmp2Ret; has DE_CFG2[SuppressBPOnNonBr] (checks.rs:521). Once Jmp2Ret and STIBP-on-request are applied, the same verdict will print mitigated: … STIBP for programs that ask (policy).
  • What the vendor says. BTC §6.1.1 calls that state not SMT-safe: "If the code on the sibling thread cannot be trusted, software should set STIBP to 1 or disable SMT to ensure SMT safety when using Jmp2Ret." Linux's default line for this state ends "SMT vulnerable". The record's own Exit counts a clause the ToyOS line lacks as a finding.
  • What "(policy)" leaves out. The marker says who chose. It does not say that, on a CPU running a program that has not asked, code on the other thread of the core can steer the speculation of the kernel's own returns.
  • The Trust page (trust.md:47-57):
    • It names the protection but not that consequence.
    • It limits the protection to "AMD's Zen 2". BTC's appendix lists Zen and Zen+ (Family 17h Models 00h–2Fh and 50h–5Fh) and Family 15h as affected too. Footnotes 5 and 6 say those have no STIBP at all, so there the policy cannot apply it even on request, and AMD's only remedy is SMT off.
    • Its first bullet, the module header (mitigation.rs:16-18), the record's Built (line 17) and the kernel page (kernel.md:60) call STIBP for Jmp2Ret a protection "between programs". It protects the kernel.
  • Fix.
    • (a) Keep the vendor's requirement out of the set that can read "mitigated", and print the declined part as a named exposure. For example: …; SMT vulnerable: STIBP only for programs that ask (policy). Hold the retbleed line in checks.rs and the pure tests to the new text.
    • (b) Give the Trust page one plain sentence. For example: "On an AMD processor affected by Branch Type Confusion, while a CPU runs a program that has not asked, untrusted code on its core's other thread can steer the kernel's speculative returns; where the processor has no STIBP (Zen, Zen+), always while SMT is on."
    • (c) Stop calling it "between programs" in the four places above.
  • What stays. SRSO's IBPB (§3, user/user) is between programs, and its (policy) mark matches the first Trust bullet.

B3. The MMIO comment misstates Intel's condition, and the case that follows from it is unconditional.

  • The comment. mitigation.rs:718-726 (and the commit message) says: "What the primary propagator alone leaves reaches only a fill buffer, which only MDS's sampling reads."
  • What Intel's text says. Intel's PSDP_NO paragraph names more: such processors need no PSDP mitigation only "if they are not affected by any other known vulnerability that can reveal the fill buffer contents (specifically, they enumerate MDS_NO, FBSDP_NO, and either enumerate TAA_NO or do not support Intel TSX)". So TAA reads those fill buffers too.
  • The code. asks.needs.is_empty() → NotAffected (:735) returns "not affected" for a part affected by PSDP alone whether or not MDS or TAA affects it. For such a part Intel prescribes "Mitigating PSDP Propagation", and the decision has no mitigation for that.
  • Who it reaches. It does not reach the T14, which is affected by SBDR.
  • Fix. Correct the comment to Intel's condition. Then either:
    • return NotAffected there only where MDS and TAA do not affect the part; or
    • record the remaining case under Left.

Suggestions

  • The SBDR item under Left. It says Intel "answers it by keeping MMIO from untrusted software" and "The T14 is affected". Intel's FBSDP_NO paragraph says parts that enumerate FBSDP_NO, MDS_NO and TAA_NO "do not need the mitigations described in the Preventing Untrusted Software from Using … to Extract Data section". Cite it, and state why ToyOS keeps the item anyway: the T14 enumerates no TAA_NO, and SBDR reads transaction buffers, not fill buffers. Otherwise the record contradicts the vendor text the mitigated line relies on.
  • Who decided the policy. The opt-in policy on the Trust page is the orchestrator's ruling on R2. Whether the owner has agreed to a security policy in the manual is the orchestrator's call. Nothing in the branch presents it as the owner's.

Evidence

  • Host. r3/host/a-r3.log is at ab39884e2, with a clean tree:

    • kernel --lib mitigation: 18 passed.
    • toyos-checks: 46 passed.
    • The clippy shapes: exit 0.
    • The 15 control runs each exit 101 for their own reason.

    B1 is outside what host tests reach.

  • Guest, R8. See B1. Dispatch it again at the fixed head.

  • Metal. The queued metalcase at ab39884e2 will not boot (B1). Restage it at the fixed head. B1 changes kernel code, so no ab39884e2 boot could stand in for it.

    T14:

    • One cpu0 words line: the pinned words plus microcode=0xbe mcu_opt_ctrl=<v> de_cfg2=- c001_1029=- c001_1029_found=-. Bits 0 (RNGDS_MITG_DIS) and 4 (GDS_MITG_DIS) of <v> are clear; Intel's default is 0x0.
    • If either bit is set, the row still passes, but T14_BENCH_LINES assumed them clear. Report it.
    • The lines are exactly T14_BENCH_LINES. In particular:
      • spectre_v2: vulnerable: needs enhanced IBRS, IBPB between programs that ask; has SMEP, with no STIBP;
      • mmio_stale_data: mitigated: microcode's RDRAND and RDSEED mitigation;
      • gather_data_sampling: mitigated: microcode's gather mitigation.
    • No other CPU's lines.

    AMD bench:

    • One cpu0 words line: the pinned words plus microcode=0x860010d mcu_opt_ctrl=-, with de_cfg2 bit 1 set and c001_1029 bit 1 set. BTC appendix footnote 7: microcode 08600109h or newer sets DE_CFG2[1] on Family 17h Model 60h Stepping 1.
    • c001_1029_found is the reading round 1 asked for. Report its value: 0x…2 means firmware had set the bit.
    • The lines are AMD_BENCH_LINES as B2 revises them. spectre_v2 names no return predictor fill.
    • A later CPU may log lines only if its registers differ, most likely c001_1029_found, and those lines pass the judge.

    Both:

    • The microcode row is green.
    • No mitigation: panic appears.

SEND BACK:

  • B1: one code fix in arch/x86_64/mitigation.rs and its call site.
  • B2: the retbleed line's text and the Trust page, plus wording in three places.
  • B3: a comment, plus either a condition or a Left item.

R8 and the metal readback must then arrive at the fixed head.

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Addendum to the round-2 review: run 43077064338 confirms B1

I read the guest row with gh run view --log. It ran at ab39884e2, the head, and ended failure.

The first CPU stopped two seconds into QEMU, before any other serial line:

[--.--- cpu0 kernel alert] EARLY PANIC: panicked at library/alloc/src/alloc.rs:659:9:
memory allocation of 480 bytes failed

The 480 bytes are the first growth of READ's Vec<Option<Read>> in record(0, …), called from after_console before mm::init. This matches B1.

The harness reports std_threads_count_the_cpus_and_give_back_ended_threads: The supervisor process crashed during boot. There is no mitigations: line.

R8 stays open. It needs a new dispatch once B1 is fixed, and the metalcase at ab39884e2 should not be run.

Japabu added a commit that referenced this pull request Nov 11, 2026
… from the CPUs up, and ToyOS's policy marked

STIBP is no longer asked of an Intel CPU with enhanced IBRS: "there is no
need to use STIBP when IBRS is set" (Speculative Execution Side Channel
Mitigations §2.4.1.3). The T14 and 806F8 drop it.

Where AMD prescribes a protection between programs for every program
(Branch Type Confusion §6.1.1's STIBP for Jmp2Ret, the SRSO paper §3's IBPB
across user/user), the decision keeps ToyOS's policy, which applies it for a
program that asks, and its verdict carries the deviation: the line reads
"STIBP for programs that ask (policy)". Each rule quotes AMD's unconditional
guidance. The policy and its reason are on the manual's Trust page.

AMD's Spectre v2 rule no longer asks for a return predictor fill beside SMEP:
Branch Type Confusion §5 keeps the return address predictor safe with "a RAP
stuffing sequence (mitigation V2-3) and/or SMEP", and SMEP, which every CPU
holds, keeps Ring 0 from fetching the user addresses user code's calls leave
there (70586 G-3). `ReturnPredictorFill` goes with it.

A CPU's `smt` is whether another CPU the kernel brought up shares its core,
by the cores the topology places their APIC IDs in, rather than CPUID.0BH or
1FH EBX[15:0], which the SDM (Vol. 1, Chapter 21) says is the count as
shipped and must not enumerate the topology. So each CPU records its words
and registers at bring-up, and once `boot_aps` has installed the topology the
BSP says cpu0's lines and those of each later CPU that differ.
`cpu::threads_per_core` goes.

The words line now carries the registers that show what is in force
(`mcu_opt_ctrl`, `de_cfg2`, `c001_1029`, `-` where not read) and C001_1029
as `apply` found it before setting LFENCE dispatch serializing, which tells
the bit firmware set from the bit the kernel set. What is in force is
`Decision::in_force` on those registers, with `ALWAYS` the one list of what
the kernel does on every CPU. `apply` stops the machine if the bit does not
hold after its write, as a CPU without SMEP stops it: the clocksource's
`RDTSCP; LFENCE` relies on it.

The `mitigations` metal row judges every CPU that logged lines, each against
its own words and registers, and holds a bench's words to its pinned words.
The AMD bench's are pinned from its boot of 3b07ea1, but the microcode
revision, which the `microcode` row holds; the search over register readings
goes. `checks::the_benches_words_decide_as_their_vendors_rows` holds the
pinned words to AMD's documents for Family 17h Model 60h, and
`checks::mitigations_verdict` holds the judge to a later CPU's own lines and
refuses a later CPU whose LFENCE bit reads clear, words other than the
bench's, and a machine no bench is.

Both issue records' exits are again as main has them; what is built is
under Built and what is unmet under Left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…el's SMT exposure named, and PSDP as Intel conditions it

`record` grew a `Vec` on the boot CPU from `after_console`, before `mm::init`
turns the allocator on, so every x86-64 boot panicked with "memory
allocation of 480 bytes failed" before the IDT existed. Guest run
43077064338 shows it, and both benches' boots of ab39884 did not come back.
Now `apply` reads a CPU's words and registers once and returns them as a
`Copy` value, which owns no heap. The boot CPU keeps its value in a one-slot
static, and each later CPU its own in a `CpuSlice` that `arch::size` sizes
with the other per-CPU tables after the allocator is up. The module names no
`alloc`.

Retbleed on AMD no longer puts STIBP among what can read "mitigated".
Branch Type Confusion §6.1.1 asks it of every program for the kernel's own
Jmp2Ret ("If the code on the sibling thread cannot be trusted, software
should set STIBP to 1 or disable SMT"), so what the policy leaves is a
verdict's `SmtExposure`. The line names it, after whatever is in force:
"; SMT vulnerable: STIBP only for programs that ask (policy)", or "this CPU
has no STIBP" on Family 15h, Zen and Zen+, which the guidance's appendix
footnotes 5 and 6 say have none. The Trust page states the consequence in
one sentence. The module header, the record's Built and the kernel page stop
calling it protection between programs; SRSO's IBPB keeps its "(policy)"
mark, since it is.

The MMIO rule asks the primary propagator's mitigation, a non-secret MMIO
read after each secret one ("Mitigating PSDP Propagation"), unless the part
enumerates `MDS_NO`, `FBSDP_NO`, and `TAA_NO` or no TSX, which is the
condition `PSDP_NO`'s paragraph gives; only then is a part whose only
propagator is the primary one not affected. The rule says why it departs
from the article's selection pseudocode: `FB_CLEAR` is for parts the fill
buffer propagator affects. The record's SBDR item cites `FBSDP_NO`'s
paragraph and says why the T14 keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the cpu-mitigation-decision branch from ab39884 to 8d57dcf Compare November 11, 2026 18:57
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #1243 at 8d57dcf (round 3)

Scope

This round checks only round 2's blockers B1–B3 and the code changed to fix them, which is commit 8d57dcf87. The three earlier commits are round 2's, rebased onto main e558cc153. git range-diff shows that only 21ae6c243's commit message changed.

The fix adds +228/−102 lines in 9 files:

  • Production:
    • The pure module: +85/−24, mostly the rules' vendor comments.
    • The kernel's arch code: +55/−46.
  • Tests:
    • The pure tests: +55/−13.
    • checks.rs: +2/−2.
  • Manual and records: +31/−17.

Architecture

The fix keeps the round-2 design and puts each piece in the right place:

  • apply reads each CPU's words and registers once. It returns them as a value of a Copy type. That removes round 2's second read in record, and the allocation that came with it.
  • The boot CPU's value has a fixed home. It goes in a one-slot static.
  • The APs use the existing per-CPU table. LATER is a CpuSlice, sized with the other per-CPU tables after the allocator is up.
  • The SMT exposure is a field of the verdict. It is not a mitigation, so the vendor's requirement can never count towards "mitigated".
  • The MMIO rule follows Intel's text. Each propagator asks for its own mitigation, and the primary propagator's mitigation now carries Intel's exemption.

I found nothing that needs a simpler representation.

B1: resolved by inspection. The boot proof is pending.

No allocation between after_console and mm::init. The boot CPU's path at boot.rs:68 is mitigation::record_boot_cpu(mitigation::apply()). Each step:

  • words() uses only cpuid, rdmsr, cpu::vendor and microcode::running. The last is Loader::of(vendor).map(revision), an MSR read.
  • decide lives in the pure module. Its non-test code names no alloc, Vec, Box, String or format!; only the tests use std::format!.
  • apply itself does rdmsr/wrmsr, and its assert! formats into the panic handler without touching the heap.
  • record_boot_cpu stores a Copy value into a static Lock<Option<Read>>. Lock::acquire calls preempt::disable, which returns early before percpu, and then spins on its ticket.
  • Round 2's guest log already shows Lock working at this point: READ.lock() succeeded there, and the panic came from the resize after it.
  • arch/x86_64/mitigation.rs no longer names alloc.

The AP table is sized and filled safely.

  • Sizing. smp::size runs at main.rs:378. That is after mm::init (:369) and before start_other_cpus (:421). It calls arch::size, which now calls mitigation::size.
  • Publication. CpuSlice::size claims the table once, then publishes the length and then the pointer with Release. Readers deref with Acquire.
  • Order on each AP:
    • apply runs at the top of ap_entry, before load_kernel_flush, and touches no heap.
    • record runs after load_kernel_flush and init_ap, and before the echo. It writes only that AP's own slot, under that slot's own lock.
  • Order on the boot CPU:
    • It commits an AP only on that attempt's echo.
    • After a failed attempt it reuses neither the id nor the trampoline. It stops (break).
    • report reads 0..topology.cpus() after set_topology.
    • So every slot it reads was written before the echo the boot CPU waited for, and the slot's lock orders the access anyway.
  • The index bound. It is the existing one: dense ids below the MADT capacity, the same as clocksource and tlb.

Does Copy prevent a recurrence? Only in part.

  • What it guarantees. A Copy type has no Drop. So Read cannot own a Vec, Box or String, and storing it into the static cannot allocate. That rules out round 2's exact shape: storage that grows when written.
  • What it does not guarantee. It does not prove that producing the value allocated nothing.
    • A future &'static field, such as a &'static str, is Copy, and could come from Box::leak or String::leak.
    • Any callee of apply could also allocate.
  • What actually guards against a recurrence. The allocator does. In PHASE_UNINIT, GlobalAlloc::alloc returns null (mm/alloc.rs:536), and the boot stops with EARLY PANIC … memory allocation of N bytes failed. That happens deterministically on the first x86-64 boot of any guest row, which is how run 43077064338 showed B1. The orchestrator's new rule then keeps such a head off metal until a guest has booted it.
  • The contract has a home. The module header states it for whoever edits apply.

A build gate is not warranted.

  • A gate on this module naming alloc would miss every callee: decide, microcode, log!.
  • It would duplicate a refusal the runtime already makes on every boot.
  • Root says "A rule visible in review belongs in instructions rather than another custom gate."

B2: resolved. Checked against AMD's BTC guidance (PID 221404394-A, fetched today).

§6.1.1 "SMT Safety". The comment's quotation matches: "If the code on the sibling thread cannot be trusted, software should set STIBP to 1 or disable SMT to ensure SMT safety when using Jmp2Ret."

The appendix's footnotes. Its "Available Mitigations" row marks Jmp2Ret "X⁵" under Fam 15h (Models 00–7Fh) and "X⁶" under Fam 17h Models 00h–2Fh and 50h–5Fh. The footnote text is drawn in a CID font, and I decoded it through its ToUnicode map:

  • ⁵: "AMD 'Bulldozer' processors do not support STIBP".
  • ⁶: "AMD 'Zen' and 'Zen+' processors do not support STIBP".

So "Family 15h, Zen and Zen+" in the code comment, the Trust page and the record is correct.

The code.

  • .exposed_to_smt(f.smt, stibp) reads stibp from Fn8000_0008_EBX[15]:
    • NoStibp where SMT is on and the bit is clear;
    • StibpOnRequest where SMT is on and the bit is set;
    • None without SMT.
  • Stibp is no longer in needs, so it can never read "mitigated".
  • The new assertion line(Retbleed, &[Jmp2Ret, SuppressBpOnNonBr]) reads "mitigated: Jmp2Ret, DE_CFG2[SuppressBPOnNonBr]; SMT vulnerable: STIBP only for programs that ask (policy)". So the exposure stays named once everything else is in force.
  • The Zen+ case (0x800f82, no STIBP) gives NoStibp.
  • The AMD bench enumerates STIBP (0x90cf757 has bit 15 set) and has smt=true, which yields its pinned retbleed line.
  • The control smt-exposure-dropped fails both the pure test and the bench test.

The prose.

  • The Trust page's new bullet states the consequence plainly.
  • All four places are corrected: the pure module header, the record's Built, kernel.md:60 and the Trust page's first bullet.
  • SRSO's IBPB keeps its (policy) mark.
  • On AMD, STIBP for programs that ask is still visible as missing on the spectre_v2 line.

B3: resolved. Checked against Intel's "Processor MMIO Stale Data Vulnerabilities" (fetched today).

The PSDP_NO paragraph. The comment's quotation matches: processors that do not enumerate PSDP_NO "may still not need the mitigations described in the Mitigating PSDP Propagation section if … (specifically, they enumerate MDS_NO, FBSDP_NO, and either enumerate TAA_NO or do not support Intel TSX)."

"Mitigating PSDP Propagation". It prescribes "an additional read of some non-secret data from a valid MMIO register on the primary bus with the same cache line offset (address mod 64)". Mitigation::ReadAfterSecret's doc says exactly that.

The code.

  • fill_buffers_unread is MDS_NO && FBSDP_NO && (TAA_NO || !(HLE|RTM)).
  • ReadAfterSecret is asked where !PSDP_NO && !fill_buffers_unread.
  • So needs.is_empty() → NotAffected is now reachable only with SBDR_SSDP_NO, FBSDP_NO, no PSDP_NO, and the fill buffers unread. That is Intel's exemption, no wider.
  • The control psdp-unconditional fails five pure tests.

The T14 is unchanged.

  • arch_capabilities=0xa005c6b has MDS_NO and FBSDP_NO, no TAA_NO and no PSDP_NO.
  • CPUID 7.0 EBX 0xf3bfa7eb has HLE and RTM clear.
  • So no ReadAfterSecret, and mmio_stale_data: mitigated: microcode's RDRAND and RDSEED mitigation stands.

The citations.

  • HLE and RTM bits. I checked the cited SDM, Vol. 1 rev 093:
    • §17.3.1.1 gives "CPUID.07H.00H:EBX.HLE[4]".
    • Chapter 21's Table 21-19 is leaf 07H.00H EBX.
    • So "SDM Vol. 1, Chapter 21" is a valid citation.
  • The FB_CLEAR departure clause. It quotes "processors affected by FBSDP" exactly.

The SBDR item under Left. It now cites the FBSDP_NO paragraph correctly. That paragraph spares parts that enumerate FBSDP_NO, MDS_NO and TAA_NO the "Preventing Untrusted Software from Using … to Extract Data" section. The T14 lacks TAA_NO, so the reason the record gives for keeping the item holds.

Findings

No blockers.

Suggestions (optional):

  • kernel/src/arch/x86_64/mod.rs:145-146 — the doc comment on size lists "the TLB's and the SMI command port's rounds", two of the four tables it sizes. It was already missing clocksource on main, and it now also misses mitigation. Deleting the list after "outside that record" fixes it for good.
  • kernel/src/arch/x86_64/mitigation.rs:8-9 and :33-34 — the "no heap before the allocator" constraint is stated twice. The header's sentence is the one an editor of apply needs, so Read's second clause can go.
  • kernel/src/cpu_slice.rs header: it says "an element is a few words: a record of its own per CPU is a CpuSlice<&'static T>". LATER's element is a record of roughly 120 bytes. That is harmless: at MAX_CPUS = 1024 it comes to about 120 KiB in one allocation, far below MAX_HEAP_ALLOC. I mention it only because the code departs from the stated contract.
  • kernel/pure/mitigation.rs, the_primary_propagator_alone_matters_where_fill_buffers_can_be_read:
    • The TSX and TAA_NO terms each have an assertion, and the FBSDP_NO term is held by the 806C1 propagating case.
    • The MDS_NO term has no assertion.
    • One line would hold it: primary with MDS_NO cleared → needs(&[ReadAfterSecret]).
  • issues/each-boot-prints-the-vulnerabilities-lines-linux-prints.md Built — its list of line shapes omits the new ; SMT vulnerable: … clause. One clause would add it.
  • The PR description (for the orchestrator). It says the Trust page gains "a section on speculative execution between programs". That section is now titled "Speculative execution applied on request".

Evidence

Host. r4/host/a-r4.log is at 8d57dcf87 with dirty=[]:

  • kernel --lib mitigation: 19 passed.
  • toyos-checks: 47 passed.
  • clippy: the kernel's host lib and tests, x86_64-unknown-none by default and with the actuator features, and toyos-build.

There are 18 control runs, each with exit 101, and the tree is restored after each. Those runs are 8 pure, 7 bench and 3 judge. The two controls new this round expose the two defects B2 and B3 named.

Guest: run 43082719564 at 8d57dcf87. It was still queued at review time. It is the only check for B1. It must show:

  • conclusion success, and the row passing;
  • no EARLY PANIC, and no mitigation: or "recorded no words at bring-up" panic;
  • mitigations: cpu0 words … hypervisor=true … followed by its 17 vulnerability lines;
  • a second vCPU's lines only where its words or registers differ.

Metal: metalcase at 8d57dcf87 on both benches, staged only after the guest run is green. Both benches must show:

  • the mitigations and microcode rows green;
  • no hang, and no early or mitigation: panic.

The T14:

  • The lines are exactly T14_BENCH_LINES.
  • mcu_opt_ctrl has bits 0 and 4 clear. If either is set, report it, as round 2 asked.

The AMD bench:

  • The lines are exactly AMD_BENCH_LINES, with the new retbleed: vulnerable: needs Jmp2Ret; has DE_CFG2[SuppressBPOnNonBr]; SMT vulnerable: STIBP only for programs that ask (policy).
  • de_cfg2 has bit 1 set, and c001_1029 has bit 1 set.
  • Report the value of c001_1029_found.
  • A later CPU logs lines only where its registers differ.

LAND, pending guest run 43082719564 and then the metal readback, each showing what is listed above.

Japabu added a commit that referenced this pull request Nov 11, 2026
… and two stale comments

The benches are down, so the `mitigations` and `microcode` rows' readback
on both benches is recorded under the lines record's Left as owed on metal,
with what main must show there: each bench's exact lines, the T14's
`IA32_MCU_OPT_CTRL` opt-out bits clear, the AMD bench's `DE_CFG2[1]` and
C001_1029[1] set and its `c001_1029_found` reported. The record's Built names
the `; SMT vulnerable:` clause.

`the_primary_propagator_alone_matters_where_fill_buffers_can_be_read` holds
the `MDS_NO` term of Intel's exemption. `arch::size`'s comment stops listing
two of the four tables it sizes, and `Read`'s second statement of the
module header's no-heap constraint goes.

Nothing an image builds changes: the kernel's code, the harness and the
guest programs are as at 8d57dcf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Nov 11, 2026
…#MC and #DF install it whatever GS holds

The kernel ran no `swapgs`: GS held this CPU's `PerCpu` from boot on, and
every entry's first stores went through `gs:`. Ring 3 replaces the GS
base with a segment load (`mov gs, 0x1b`: the user data descriptor, base
0), so its next syscall stored near address 0 at CPL 0 on its own stack.
`SYSCALL` switches no stack and `#PF` has no IST, so the fault's frame was
pushed to the stack pointer the program chose, a kernel address if it
chose one, and every handler after it addressed the same base. Found by
reading in #1251; the previous commit's two rows red on that kernel.

Now `IA32_GS_BASE` is Ring 3's own while it runs and the kernel's while
the kernel runs, with `IA32_KERNEL_GS_BASE` the other:

- `SYSCALL` comes only from Ring 3 and swaps first and on its way out.
  The image gate on the shipping `syscall_entry` now requires it to open
  `swapgs`, `cld`, the save and the switch.
- Every other gate swaps when its frame's `CS` is Ring 3's, and back
  before an `iretq` to Ring 3. That decision is sound because the
  kernel's CPL-0 spans with a user's base live (before `SYSCALL`'s
  `swapgs`, and from an exit's `swapgs` to its `SYSRET` or `IRETQ`) run
  with `IF` clear and no instruction that can fault: `SYSRET`'s `rcx` is
  canonical by the user half's placement, and every `iretq` frame is the
  CPU's or a validated entry's. No `#DB` lands there: nothing arms a
  debug register, `IA32_FMASK` clears `TF`, and a single-step trap after
  `MOV SS` is suppressed, not deferred (SDM Vol. 3A §7.8.3).
- NMI, `#MC` and `#DF` can land in those spans and their frame does not
  say so. They take the kernel's base from `GDTR`, whose base is the
  block's own `gdt`, and write it to `IA32_GS_BASE` whatever it held;
  the NMI puts back the base it found before its `iretq`. They decide
  nothing: a test of the found base's sign would hold only while no
  thread can write its own GS base, and `WRGSBASE` writes any canonical
  one, the kernel half's included (`issues/fsgsbase-is-taken.md`).
  `#DF` and `#MC` never return, so any dispatched vector on an IST stack
  goes through the new `fatal_entry`.

Speculation (CVE-2019-1125): the gate's `swapgs` is conditional, so an
`LFENCE` follows the join, before any load through GS, which is Intel's
placement for both a skipped and an extra `swapgs` ("Speculative Behavior
of SWAPGS and Segment Registers", ID 758371). The unconditional
`SYSCALL` swap needs none, the exits load nothing through GS after their
`swapgs` and end in a serializing `IRETQ` or in `SYSRET`, and the IST
entries have no branch before their serializing `WRMSR` (SDM Vol. 3A
§11.3). On AMD the fence orders dispatch only where `LFENCE` is
dispatch-serializing (AMD 70586 G-2, MSR C001_1029[1]), which #1243 sets;
AMD's own statement on SWAPGS was not reachable to read.

The switch moves no GS base: every descriptor Ring 3 may load into GS
has base 0 (asserted at compile time beside the GDT), AMD's null load
keeps the base it found, and `CR4.FSGSBASE` stays forbidden, so each CPU's
`IA32_KERNEL_GS_BASE` starts at 0 and every thread's user GS base is 0.
The switch's save and restore of it belongs with taking FSGSBASE.

AArch64 has no GS and is unchanged.

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

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator: landing under the benches-down rule. Review r3 LAND. Guest run 43082719564 at 8d57dcf: success, and its log carries the boot proof review r3 asked for: no EARLY PANIC, mitigations: cpu0 words … hypervisor=true … followed by the vulnerability lines (17). Head 8c203f6 differs from 8d57dcf by comments, one pure-test assertion (MDS_NO) and the record (verified; no executable kernel line). Owed on metal: the T14 and AMD mitigations/microcode rows and register readbacks, as recorded.

@Japabu
Japabu marked this pull request as ready for review November 11, 2026 22:51
@Japabu
Japabu added this pull request to the merge queue Nov 11, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 11, 2026
Japabu and others added 6 commits November 12, 2026 01:13
…t says its verdict

Nothing decided which speculative-execution vulnerabilities a CPU has or
what its vendor prescribes against each. `kernel::mitigation::decide` maps a
CPU's vendor, signature, microcode revision, whether it is a guest, whether
its core has a second thread, and its enumeration words (CPUID 7.0 EBX/EDX,
7.2 EDX, 0x80000008 EBX, 0x80000021 EAX/ECX and IA32_ARCH_CAPABILITIES where
enumerated) to a verdict per vulnerability: not affected, or the mitigations
the vendor's guidance prescribes and which of them the CPU does not
enumerate. Each rule cites the Intel article or AMD bulletin or white paper
it reads. Enumeration decides where the guidance lets it; otherwise Intel's
affected-processor table, carried as its machine-readable list gave it on
2026-09-04, decides for a CPU it lists, and a CPU it omits is reported as
assumed affected. AMD's documents name affected families, so a newer family
is held to its `_NO` bits and reported as assumed affected without them.

Every CPU reads its words after its microcode loads, as Intel's enumeration
article asks. The first CPU logs them and one line per vulnerability:
`not affected`, `mitigated: ` and what by, or `vulnerable: needs ` and what
is missing, with what is in force and what the CPU does not enumerate. A
later CPU logs only where its words or standing differ. What is in force is
what the kernel already does everywhere (branchless user-pointer clamping,
SMEP, no guest) and what the CPU's own MCU_OPT_CTRL, DE_CFG2 and C001_1029
show. Syscall entry, exit and context switch are unchanged.

One control is applied. On AMD, LFENCE holds back later instructions only
where MSR C001_1029[1] is set (AMD 70586, "Software Techniques for Managing
Speculation", mitigation G-2) or CPUID Fn8000_0021_EAX[2] says it always
does (APM Vol. 3, LFENCE). The kernel never set it, though the paper's
V1-1 bounds-check barrier and the clocksource's ordered TSC read,
`rdtsc_ordered`'s `RDTSCP; LFENCE`, rely on that. The decision now names it on spectre_v1 for every
AMD family the paper gives the MSR (10h, 12h-17h and later) unless CPUID
says LFENCE always serializes; 0Fh and 11h always serialize. A guest cannot
write its host's MSR, so a guest is reported lacking it. Each CPU sets the
bit right after its microcode loads, before the clocksource reads the TSC,
and its line reads the bit back. The metal judge holds it set.

The `mitigations` metal row holds a boot's lines to the decision on the
words that boot logged; `checks::mitigations_verdict` holds that judge to a
boot's lines and to the ways one could fall short of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… from the CPUs up, and ToyOS's policy marked

STIBP is no longer asked of an Intel CPU with enhanced IBRS: "there is no
need to use STIBP when IBRS is set" (Speculative Execution Side Channel
Mitigations §2.4.1.3). The T14 and 806F8 drop it.

Where AMD prescribes a protection between programs for every program
(Branch Type Confusion §6.1.1's STIBP for Jmp2Ret, the SRSO paper §3's IBPB
across user/user), the decision keeps ToyOS's policy, which applies it for a
program that asks, and its verdict carries the deviation: the line reads
"STIBP for programs that ask (policy)". Each rule quotes AMD's unconditional
guidance. The policy and its reason are on the manual's Trust page.

AMD's Spectre v2 rule no longer asks for a return predictor fill beside SMEP:
Branch Type Confusion §5 keeps the return address predictor safe with "a RAP
stuffing sequence (mitigation V2-3) and/or SMEP", and SMEP, which every CPU
holds, keeps Ring 0 from fetching the user addresses user code's calls leave
there (70586 G-3). `ReturnPredictorFill` goes with it.

A CPU's `smt` is whether another CPU the kernel brought up shares its core,
by the cores the topology places their APIC IDs in, rather than CPUID.0BH or
1FH EBX[15:0], which the SDM (Vol. 1, Chapter 21) says is the count as
shipped and must not enumerate the topology. So each CPU records its words
and registers at bring-up, and once `boot_aps` has installed the topology the
BSP says cpu0's lines and those of each later CPU that differ.
`cpu::threads_per_core` goes.

The words line now carries the registers that show what is in force
(`mcu_opt_ctrl`, `de_cfg2`, `c001_1029`, `-` where not read) and C001_1029
as `apply` found it before setting LFENCE dispatch serializing, which tells
the bit firmware set from the bit the kernel set. What is in force is
`Decision::in_force` on those registers, with `ALWAYS` the one list of what
the kernel does on every CPU. `apply` stops the machine if the bit does not
hold after its write, as a CPU without SMEP stops it: the clocksource's
`RDTSCP; LFENCE` relies on it.

The `mitigations` metal row judges every CPU that logged lines, each against
its own words and registers, and holds a bench's words to its pinned words.
The AMD bench's are pinned from its boot of 3b07ea1, but the microcode
revision, which the `microcode` row holds; the search over register readings
goes. `checks::the_benches_words_decide_as_their_vendors_rows` holds the
pinned words to AMD's documents for Family 17h Model 60h, and
`checks::mitigations_verdict` holds the judge to a later CPU's own lines and
refuses a later CPU whose LFENCE bit reads clear, words other than the
bench's, and a machine no bench is.

Both issue records' exits are again as main has them; what is built is
under Built and what is unmet under Left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ors a part has

The T14's boot of 3b07ea1 logged its words, and they are pinned beside
the AMD bench's, but the microcode revision, which the `microcode` row
holds. `checks::the_benches_words_decide_as_their_vendors_rows` holds what
they decide to CPUID 806C1's row of Intel's affected-processor list on its
listed microcode 0xbe, which the T14 runs, and `checks::mitigations_verdict`
refuses the T14's words with `ARCH_CAPABILITIES` read as 0 under lines that
agree with them: the record's mutation, which only the pin catches.

The pin showed the MMIO rule wrong for this CPU. It asked VERW wherever the
table lists a part and found it lacking without `FB_CLEAR`. The T14 runs the
microcode Intel lists as its mitigation and enumerates `FBSDP_NO` and no
`FB_CLEAR`. Intel's list gives Shared Buffers Data Sampling and Shared
Buffers Data Read one combined cell, "MCU", and on the T14, which has
neither MDS nor TSX, only the read can apply. In
"Processor MMIO Stale Data Vulnerabilities", VERW's fill-buffer overwrite
blocks the fill buffer propagator; against Shared Buffers Data Read and the
sideband propagator, the microcode that has RDRAND, RDSEED and EGETKEY
overwrite the uncore buffers "is also relevant", under the control
`SRBDS_CTRL` enumerates. So the rule asks VERW where the part has the fill
buffer propagator and the microcode's overwrite where it has Shared Buffers
Data Read, and the T14's line reads
`mitigated: microcode's RDRAND and RDSEED mitigation`. The synthetic 806C1
row takes the bits the T14 enumerates.

What the decision does not name, that a program with MMIO access can read
other cores' stale MMIO data, is recorded under the record's Left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…el's SMT exposure named, and PSDP as Intel conditions it

`record` grew a `Vec` on the boot CPU from `after_console`, before `mm::init`
turns the allocator on, so every x86-64 boot panicked with "memory
allocation of 480 bytes failed" before the IDT existed. Guest run
43077064338 shows it, and both benches' boots of ab39884 did not come back.
Now `apply` reads a CPU's words and registers once and returns them as a
`Copy` value, which owns no heap. The boot CPU keeps its value in a one-slot
static, and each later CPU its own in a `CpuSlice` that `arch::size` sizes
with the other per-CPU tables after the allocator is up. The module names no
`alloc`.

Retbleed on AMD no longer puts STIBP among what can read "mitigated".
Branch Type Confusion §6.1.1 asks it of every program for the kernel's own
Jmp2Ret ("If the code on the sibling thread cannot be trusted, software
should set STIBP to 1 or disable SMT"), so what the policy leaves is a
verdict's `SmtExposure`. The line names it, after whatever is in force:
"; SMT vulnerable: STIBP only for programs that ask (policy)", or "this CPU
has no STIBP" on Family 15h, Zen and Zen+, which the guidance's appendix
footnotes 5 and 6 say have none. The Trust page states the consequence in
one sentence. The module header, the record's Built and the kernel page stop
calling it protection between programs; SRSO's IBPB keeps its "(policy)"
mark, since it is.

The MMIO rule asks the primary propagator's mitigation, a non-secret MMIO
read after each secret one ("Mitigating PSDP Propagation"), unless the part
enumerates `MDS_NO`, `FBSDP_NO`, and `TAA_NO` or no TSX, which is the
condition `PSDP_NO`'s paragraph gives; only then is a part whose only
propagator is the primary one not affected. The rule says why it departs
from the article's selection pseudocode: `FB_CLEAR` is for parts the fill
buffer propagator affects. The record's SBDR item cites `FBSDP_NO`'s
paragraph and says why the T14 keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and two stale comments

The benches are down, so the `mitigations` and `microcode` rows' readback
on both benches is recorded under the lines record's Left as owed on metal,
with what main must show there: each bench's exact lines, the T14's
`IA32_MCU_OPT_CTRL` opt-out bits clear, the AMD bench's `DE_CFG2[1]` and
C001_1029[1] set and its `c001_1029_found` reported. The record's Built names
the `; SMT vulnerable:` clause.

`the_primary_propagator_alone_matters_where_fill_buffers_can_be_read` holds
the `MDS_NO` term of Intel's exemption. `arch::size`'s comment stops listing
two of the four tables it sizes, and `Read`'s second statement of the
module header's no-heap constraint goes.

Nothing an image builds changes: the kernel's code, the harness and the
guest programs are as at 8d57dcf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1254 made every metal row say which benches it runs on. The `mitigations`
row's judge holds a boot to its own bench's pinned words, and it ran on both
benches, so it is `On::Every`, as the `microcode` row it rides with is. The
comment stops listing the benches the field now names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the cpu-mitigation-decision branch from 8c203f6 to 003c0e3 Compare November 11, 2026 23:16
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Orchestrator: re-queued after the merge-queue compile failure (missing on on the metal row after #1254). 003c0e3 adds on: On::Every (tests/toyos.rs only, verified); the rebase leaves the kernel's diff identical. Guest run 43082719564 stands.

@Japabu
Japabu added this pull request to the merge queue Nov 11, 2026
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