Skip to content

The kernel loads pinned Intel and AMD microcode on every CPU before anything reads what an update changes, and a CPU that refuses one says so - #886

Draft
Japabu wants to merge 9 commits into
mainfrom
wt/toyos-microcode
Draft

Japabu wants to merge 9 commits into
mainfrom
wt/toyos-microcode

Conversation

@Japabu

@Japabu Japabu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Fix round 2. Head e2b398cb6: the ruling after both negative-control arms hung their benches (ruling, points 1-4), the owner's clean-room ruling (#902: nothing here cites Linux or Xen, and no test's oracle for hardware is a reading captured under Linux), and a merge of origin/main at 5096551ca (#888, #890, #876, #883, #891, #889, #898, #902, #897). It takes up issues/the-kernel-loads-no-cpu-microcode.md. A draft. Before it lands: the metal readings requested below; #873's landing and the hand-off to its PCIDE verdict; the issue's deletion once those readings meet its exit; a review.

This round's logs, patches and vendor excerpts are on ToyOSOrg/evidence pr-886 at 8b6b8027; the build round's, where still cited, at 4dab62b2. Authority is Intel's and AMD's documentation: Intel's SDM Vol. 3A (325384-093US) §12.11 and its Microcode Update Guidance; AMD's PPR for Family 17h Model 60h A1 (55922 rev 3.06, the AMD bench's Renoir), AMD's APM Vol. 3 (24594 rev 3.36) and AMD's own container parser amd_ucode_info.py.

What this round changed

A CPU that does not take its update is a refused line, not a panic (ruling points 1, 3)

Why. The BSP loads in after_console, before power::init_reset decodes the FADT's reset and, on the AMD bench, before any clock can time the panic's bound. The read-back's panic there held both benches until a power cycle.

By the vendors. Intel judges a load by MSR 8BH read after it: equal to the update's revision is Success, anything else Fail (SDM §12.11.7.2, Example 12-10), and the register reports "the currently functioning revision" (§12.11.7.1). AMD's PPR has MSR0000_008B return "an identification number for the microcode patch that has been loaded" (excerpts). So a CPU that did not take an update runs what it reads back.

What the kernel does (kernel/src/arch/x86_64/microcode.rs):

  • verified's assert is gone. taken reads the revision back and answers Outcome::Loaded or Outcome::Refused { update, running }.
  • The line says refused <update>, and its running is the read-back: microcode: cpu0 Intel signature 0x806c1 platform 7 found 0xbe refused 0xbf running 0xbe.
  • agree takes that read-back like any other revision.
  • The load sites do not move, so CPUID, RDRAND and RDSEED still read after the load.
  • The module header states this and drops the two statements this run showed false: "every failure this loader can see is a panic, which reports before the IDT", and the read-back's panic.

The judge reds a refused line on either bench, on a boot that completes (point 2)

  • microcode_lines refuses the first refused line on any machine: "cpu refused the update it was handed and runs ".
  • microcode_on_metal is no longer held to Linux's reading of either bench (Another implementation's source is read only clean-room, and the tree names Linux only as a host platform or a libc interface #902). Each bench is held to the update its maker's embedded file holds for its signature: 0xbe in Intel's 06-8c-01 for the T14's 0x806c1, and 0x0860010d in the embedded container for the AMD bench's 0x860f01. Every CPU's outcome is current or loaded, and every CPU runs that revision. The CPU's read-back is the oracle for what the hardware did, checked against the vendor's file.
  • tests/checks.rs's microcode_verdict accepts both benches and refuses nine machines by name, a refused T14 BSP and a refused AMD bench among them.

The reseal admission's reason (point 4)

FLASHABLE admits microcode-reseal because the payload is Intel's own, the revision the CPU already runs. So the CPU refuses it or runs what it ran, and the boot goes on and ends the way an unarmed one does. Nothing it writes outlives a hard reset (SDM §12.11.6.1). The panic's reset is no longer part of the reason.

The T14's reset is a hard reset. Its FADT names RESET_REG SystemIO 0xCF9, 8 bits, with RESET_VALUE 0x06 (read off its table dump). Intel's 500 Series On-Package PCH datasheet, the T14's PCH, defines both outcomes of that write:

  • Vol. 2, 631120-002 §26.1.4 (RST_CNT). 0x06 sets SYS_RST and RST_CPU. The datasheet calls SYS_RST = 1 "Hard Reset, not Soft Reset". RST_CPU going 0 to 1 with SYS_RST set "will force PCI reset active". SYS_RST = 0 would be INIT#, which an update survives.
  • Vol. 1, 631119-007, Table 73. "Write of 06h to CF9h" with CF9GR clear is a Host Reset Without Power Cycle: PLTRST# asserted, the processor reset. With CF9GR set it is a Global Reset With Power Cycle.

Either way the reset is a hard one in the SDM's terms, and Intel's guidance says "the microcode update is generally cleared by a warm reset" (excerpts).

Every Linux and Xen reference is gone (#902)

  • Deleted:
    • tests/linux/amd_shas.c, its NOTICE section and the digest test that read it. With it go h14 and the toyos-sha2 dev-dependency. NOTICE's "Nothing else in this repository is GPL" is true again and comes back.
    • tests/metal/amd-linux/, the AMD bench's Linux capture.
  • Re-cited to Intel's or AMD's documents:
    • MSR 8BH's lower dword as AMD's patch level: 55922, MSR0000_008B.
    • CPUID.01H:ECX[31], which a hypervisor sets for its guest: APM Vol. 3 §E.3.2.
    • EntrySign: CVE-2024-36347, AMD-SB-7033. The pinned container is the only check on an affected CPU.
  • Left uncited, because AMD documents nothing (open questions, below):
    • the patch loader;
    • the family-0x17 INVLPG;
    • the equal-revision reload;
    • the patch-ID decode;
    • the meaning of 0xffffffff.
  • The word at patch offset 12 is named as AMD's parser names it, mc_patch_data_checksum, and refused when not 0: Refusal::DataChecksum, test a_patch_with_a_data_checksum_is_refused. Its old name called it a minimum revision, which only Xen's reading said.
  • Revision compares the unsigned dword with no other OS cited. DISABLED is "the highest revision, which no update loads over".
  • NOTICE names the AMD container's upstream repository by its URL, as provenance.

The early-panic hold is filed

issues/a-panic-before-the-reset-register-is-read-holds-the-machine-for-good.md, owned by the orchestrator:

  • On x86-64 a panic before power::init_reset holds the machine for good.
  • On the AMD bench so does every panic before clock::init, because the CPU states no counter frequency.
  • Evidence: the two hung arms.
  • Exit: a test-early-panic boot on each bench resets itself and the next boot reports it.

issues/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md covers only the later window.

The issue's exit

issues/the-kernel-loads-no-cpu-microcode.md's exit no longer reads "at least as current as Linux's". It reads: the newest update the maker's embedded file holds, loaded early on every CPU, and a CPU that does not take one says so and runs what it reads back. Its metal arms are the ones below.

The design as it stands

Unchanged from the build round except as above. Where each CPU loads:

  • the BSP in arch::boot::after_console, right after control_regs::init_cr0(0);
  • each AP in smp::ap_entry, right after control_regs::init_cr0.

Both run before anything reads what an update changes (Intel's guidance), and boot_aps serialises per core (SDM §12.11.6.3). A fault inside a trigger, before the IDT, reports nothing: that cost is ToyOS's own. The t14-reseal rerun tells a fault at the trigger apart from the panic hold, which this head no longer takes.

Intel follows SDM §12.11:

  • the update file's header, checksum and extended signatures;
  • IA32_PLATFORM_ID bits 52:50;
  • write 0, CPUID, read MSR 8BH's upper dword;
  • only a newer update loads (Example 12-10);
  • WRMSR 79H at the update data's 16-byte-aligned address (§12.11.6).

AMD:

  • The container is read as AMD's parser reads it, except that a second container behind the first is refused rather than read on into.
  • The patch level is read as the PPR defines it.
  • The patch is matched by the CPUID its patch ID decodes to.
  • A patch at or above the running revision loads, an equal one again on every thread.
  • WRMSR C001_0020 is written the patch header's address, and on family 0x17 that is followed by INVLPG of the patch's first and last byte.

The licence gate's Terms::Firmware, NOTICE, the ROOT licence texts and the files are as in the build round: 06-8c-01 efe83e31…, microcode_amd_fam17h.bin 966e4b79…, LICENSE.amd-ucode 2103bd99….

Tests, cheapest tier first

Host. This round's mutations, each a checked patch, applied, run and reverted with the porcelain empty after each (run.sh, run.out, at e2b398cb6):

mutation red (exit 101 each)
j1 the judge takes a refused line microcode_verdict ("a T14 BSP that refused its update was refused for the wrong reason")
j2 the judge takes any outcome microcode_verdict ("a T14 CPU that loads nothing was accepted")
j3 the judge takes any revision microcode_verdict ("a T14 CPU below Intel's update was accepted")
h07 a data checksum accepted a_patch_with_a_data_checksum_is_refused

The build round's h01-h06 and h08-h13, h15 (run.out, at f126fb514) guard code this round changed only in comments. h14 (the digest test) is gone with amd_shas.c. h16 and h17 judged the Linux-held judge, and j1-j3 replace them. The refusal itself is kernel code a CPU decides, so the metal arms below are its test.

Guest. No guest test is added or changed. Every x86-64 guest takes the hypervisor arm, which this round does not change. The whole suite is green at this head (below).

Metal (staged; the request is below). Every arm should complete its boot and reset; none should hold a panel.

arm bench expected
t14-reseal (m1), first T14 red on a completed boot: cpu0 found 0xbe refused 0xbf running 0xbe, the APs current 0xbe. A boot stopped with no record is a fault at the trigger.
t14-head T14 green: eight CPUs run 0xbe
t14-reverted (m3) T14 red: no microcode line
amd-trigger-removed (m2) AMD red on a completed boot: every CPU refused 0x860010d running <firmware's> (0x8600106 last run)
amd-head AMD green: sixteen CPUs loaded 0x860010d; report each found
amd-reverted (m3) AMD red: no microcode line
amd-no-invlpg (m4) AMD green; phase times reported beside amd-head's, never gated

Checks (root CLAUDE.md)

  • The negative control is the whole change reverted onto 5096551ca, the base this head's gates ran on, booted on each bench (m3: every production path back to origin/main, with the row kept).
  • The refusal's own controls:
    • t14-reseal and amd-trigger-removed, each a CPU that does not take its update on real silicon, must red the row on a completed boot;
    • j1-j3 hold the judge.
  • Independent oracles:
    • Intel's SDM, Microcode Update Guidance and 500 Series PCH datasheet;
    • AMD's PPR 55922 and APM Vol. 3;
    • AMD's own parser, whose section offset 9764 the Renoir test holds;
    • the vendors' files themselves;
    • each bench's own read-back.
  • What the new tests see that reading cannot:
    • microcode_verdict: what the judge refuses;
    • the metal row: what two real CPUs take and report.

Gates at e2b398cb6

gate exit log
cargo run -- --ci host 0 ("Host: 76 step(s), all green") ci-host.log L8846
the same at bbac92034, before the judge's fix 1 (clippy: found never read; is_multiple_of) ci-host-red-1.log L9178
cargo run -- --build-only 0 build-x86_64.log L108
cargo run -- --build-only --arch aarch64 0 build-aarch64.log L106
cargo test, the whole guest suite, 62 of 62 (#897 cut netstack_socket_churn), at a one-minute load average of 19.66 to 31.63 0 guest.log L1097
seven --metal-readback stagings 2 each (Verdict::Staged) stage.out
every metal row not run the request below

Metal

The request orders the seven boots, t14-reseal first. It gives each image's sha256, the tree it was built from, its toyos-metal invocation, and what it should show. The AMD arms carry --host '<user>@<amd-bench>'. The script is stage.sh. Each mutation arm is a local commit on a detached head, never pushed.

What I am unsure of

  • AMD documents no part of the loader. 55922 documents MSR0000_008B and not MSRC001_0020. Undocumented:

    • the trigger's address form and alignment;
    • whether a rejected patch faults;
    • the family-0x17 INVLPG after a load;
    • whether an equal patch should load again (55922 makes the patch level one per core, so a core's second thread finds its first thread's patch);
    • EntrySign's per-CPU cutoff.

    The code keeps the build round's behaviour on each, with no citation. The AMD bench's readings are its only check.

  • The patch-ID decode departs from AMD's parser, which names a patch's CPU through the equivalence table. On the embedded container the two agree for all seven patches (AMD's tool output in the design round's log), but a re-pin could part them.

  • amd_ucode_info.py is AMD's code, cited as AMD's documentation of its format. Whether Another implementation's source is read only clean-room, and the tree names Linux only as a host platform or a libc interface #902's clean-room rule admits a vendor's own tool as documentation is the orchestrator's call.

  • Intel revisions with the top bit set. The SDM calls the header's revision "a signed 32-bit number", and Intel's guidance pseudocode loads any negative update_id. Revision compares unsigned, as acceptance 1 ruled. No pinned file carries one.

  • 0xffffffff has no meaning in either maker's documents. The refusal stands as accepted, by its own reason only.

  • Whether the reseal arm faults at the trigger rather than returning: the rerun says.

Size

git diff --shortstat origin/main...e2b398cb6: 24 files, +1568 −521.

  • Production: the loader module is +8 net: the assert gives way to a 6-line taken and one Outcome arm. toyos-microcode and the wiring are as in the build round, less the comments that cited Linux.
  • Tests:
    • the judge is smaller (−50 in tests/toyos.rs);
    • microcode_verdict is +5;
    • the digest test and its reader are −39.
  • Data: amd_shas.c (−556) and the Linux capture (−24) are deleted.

🤖 Generated with Claude Code

https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

The AMD bench's Linux microcode reading, owed by the orchestrator, is now on the evidence branch: amd-bench-linux-microcode.txt at 9e5f88e5.

  • Firmware 0x08600106; Linux early-loads 0x0860010d.
  • Family 23, model 96, stepping 1.
  • amd-ucode-firmware-20260810.

The command is recorded in the file's header.

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Design review of #886 at bc44b7601, round 1, against .claude/agents/reviewer.md and root CLAUDE.md on origin/main (867ec23ed).

Size: git diff --shortstat origin/main...bc44b7601 is empty, because the head is one empty commit. The body estimates production at about +409: toyos-microcode +230, arch/x86_64/microcode.rs +110, the witness +15, licence and build +19, and the boot-actuators reseal arm +35. Tests come to about +360, plus data. I accept the production growth: the issue the branch takes up mandates it, and the tree has no loader it could be merged into.

Judged against the sources, no finding. I fetched Linux v7.2 and Xen 2c40f10b; their digests equal the log's §5.

  • AMD. Every line the body cites says what the body says:
    • container and sections: amd.c:56-61,432-460
    • chipset refusal: :543-546
    • Zen match by patch ID: :300-315,580-587
    • load at or above the running revision, BSP and AP: :818,996
    • EAX: :336
    • trigger at the header: :700-705
    • family-0x17 INVLPG: :707-718. Xen's is family 0x17 too, at the start and at +3199 (amd.c:366-370).
    • read-back: :723-729
    • EntrySign families: :246-262
    • the 4800U's cutoff 0x860010e: :196
    • Xen reads offset 12 as a minimum revision from processor ID 0xa000 up (amd.c:291-313).
  • Re-measured independently. linux-firmware 20260916 microcode_amd_fam17h.bin is 966e4b79…. Bytes 9772 to 9772+3200 hash to 86b61583…, which is amd_shas.c's 0x860010d. The section at 9764 has type 1 and size 0xc80. The offset-12 word is 0.
  • Intel. The cited rules match Linux:
    • no MSR 17H under a hypervisor, bits 52:50: intel.c:138-158
    • only a strictly newer update: :379,697
    • write 0, CPUID, read: :696-709
    • bit 31 disables the loader: core.c:114-139
  • EntrySign at build time. A runtime digest of an embedded patch would compare the image with itself. Linux's amd_shas.c has no row above its CPU's cutoff: I checked all 79 against get_cutoff_revision, amd.c:190-231.
  • The witness. Running is the right shape for the verdict. It is a compile-time order where the alternative is a static and a runtime check.
  • The licence kind. It mirrors Terms::Font and FONTS_ONLY (build/licence.rs:82-110).
  • Metal controls. The T14's reseal arm and the AMD bench's trigger-removed mutation each turn the read-back's assert red on real silicon. The revert of the whole change is root CLAUDE.md's control.

BLOCKER

  • PR body, the plan ("Tests / Host") — a CPU that reports revision 0xffffffff is not modelled. Xen takes rev == ~0 to mean that a hypervisor, or a firmware toggle on bare metal, has disabled loading, and it ignores the interface (xen/.../microcode/core.c:944-957). Linux loads nothing over it, because it compares Intel revisions unsigned (intel.c:355,379,697; microcode.h:37). This design compares them signed (kernel/microcode/src/lib.rs:65-75). An Intel CPU at -1 would be handed the update, refuse it, and the read-back would panic a machine that both references boot. The plan should refuse ~0 by name, on both makers, before select, and a host test should hold that refusal.

  • PR body, "Guest" — the guest assertion that every CPU's line says hypervisor guards one CPUID bit, which a reader checks against core.c:114-139.

    • Every x86-64 guest boot already runs that arm, so a fault there turns the whole suite red.
    • A misread bit on CI's hosts changes only the line, because no embedded file names an EPYC or qemu64.
    • On a host that a file does name, every guest boot stops at the load: at the read-back panic the body describes, or at an MSR 17H fault.

    Drop the assertion. Rewrite the guest exit of issues/the-kernel-loads-no-cpu-microcode.md, the issue the branch takes up, to rest on the QMP reading (log L256-269).

  • PR body, "Host / The plan", "which half of MSR 8BH each maker reports in" — this tests a shift. Make the half each maker's own revision constructor, test nothing, and let a reader hold it to SDM §12.11.7.1 and Linux amd.c:336. Intel's constructor already exists: Revision::from_sign_id, kernel/microcode/src/lib.rs:70-75. On either bench, the wrong half already reads found 0 and panics at the read-back.

NOTE

  • PR body, Roast 1 — false of the record on which the placement is weighed. Linux's order is not "load, then verdict, then CR4, then IDT":

    • Linux's BSP loads after an early IDT handler (head64.c:276,286).
    • Its APs load after CR4 and the IDT (smpboot.c:236,248,266), with PCIDE already applied from the BSP's post-load revision (init.c:288-307).
    • Its "plain" native_wrmsrq carries an EX_TYPE_WRMSR fixup that reports and continues (msr.h:78-84,101-102). cda64169bade's own log shows that fixup catching MSR 17H in a guest.
    • Xen uses wrmsr_safe (amd.c:352).

    Both survive a fault at the trigger and report it. ToyOS alone hangs silently on the BSP and resets on an AP. The placement still stands on Intel's guidance (log L143), because declaration reads CPUID on every AP (control_regs.rs:363,408) and random::key comes after it (main.rs:359). The body should say three things:

    • the cost is ToyOS's own;
    • today the cost buys the PCIDE verdict nothing, because no Alder Lake or Raptor Lake file is embedded, so the verdict judges the firmware's revision either way;
    • only the first of the two exits of issues/an-ap-loads-the-idt-before-its-control-registers.md covers the load, because the second leaves the AP with no table at all.
  • Linux amd.c:554-572 — the EntrySign cutoff-side rule is missing from the design's AMD rules: a CPU at or below its cutoff is offered only patches at or below it. This loader would hand a new-scheme patch to a pre-fix CPU, inside the window with no IDT. What refuses such a patch here works at build time:

    • amd_shas.c holds no digest above a cutoff;
    • Zen 3 and later new-scheme patches carry the minimum-revision word (Xen amd.c:279-313).

    Name the digest test as the build-time form of that rule, so a re-pin cannot weaken it.

  • Xen amd.c:327-334 — Xen refuses an equal AMD revision unless forced, so the reload on every thread is Linux's alone (amd.c:813-819,996; e7ad18d1169c). The body lists Xen among the sources of the AMD rules without saying that this rule goes against it.

  • PR body, "The order is a type" — Running's revision should be the unsigned dword that the verdicts compare. Linux judges PCID on boot_cpu_data.microcode < driver_data as a u32 (init.c:288-289), and toyos_cpuvuln::Facts::microcode is a u32 (kernel/cpuvuln/src/lib.rs:77-78). An i32 would withhold PCIDE at -1, where Linux keeps it. x86-64 kernel leaves are global under a declared CR4.PGE, and only a kernel-half change drops them #873's accepted fix-round design (comment 6125061009) does not say that its verdict takes Running; record the hand-off on x86-64 kernel leaves are global under a declared CR4.PGE, and only a kernel-half change drops them #873.

  • PR body, "Metal, the AMD bench" — "sixteen lines, each found 0x08600106" contradicts "What I am unsure of". e7ad18d1169c says SMT threads share the engine, so a sibling may find 0x0860010d. Set the siblings' found column from the reading.

  • PR body, opening — the body says "Every number below comes from a command that was run, logged in design-measurements.txt". Neither 0x08600106 nor Fedora's transition from 0x08600106 to 0x0860010d is in any log at 499ad40d; the evidence branch pr-886 holds that one file. The AMD load arm and its trigger-removed control judge something only if found differs from running on that bench. The owed capture is therefore needed before that row is written.

  • PR body §5 — the body says LICENSE.amd-ucode, "read whole", allows free redistribution on terms it lists as three. The text at 2103bd99… also:

    • forbids referencing AMD in promotion in a way that implies endorsement;
    • makes the redistributor responsible for the rights to "third party technologies" that the microcode may implement, which AMD says it does not convey.

    Whoever accepts the exception should read all five terms.

  • build/licence.rs:82-110 — the new kind is scoped to CPU microcode. The owner's ruling (root CLAUDE.md, Dependencies) names one category: vendor firmware that a device or CPU verifies by its maker's signature. Scope the kind to that category, so the first device firmware is a new row and not a second kind beside this one.

  • PR body, the digest test — the committed copy of Linux's amd_shas.c rows is a third-party file. Root CLAUDE.md requires a NOTICE entry for it, with its hash, upstream and licence, and the design names none.

  • kernel/src/arch/x86_64/cpu.rs:132 — the plan's vendor should be the toyos_cpuvuln::Vendor that cpu::vendor() returns. A vendor decode of its own in toyos-microcode would be a sibling.

  • kernel/microcode/Cargo.toml:6 — the description says Intel only, and becomes false once amd lands.

  • PR body, the reseal arm — after_console runs before mm::init, so the resealed copy of 112,640 bytes has to be a 16-byte-aligned static compiled only with boot-actuators. The design should say where it lives.

  • issues/the-kernel-loads-no-cpu-microcode.md — the body does not say whether the branch deletes the issue at landing, and Roast 4 says the exit holds on the benches only.

DESIGN NEEDS CHANGES: a CPU that reports revision 0xffffffff loads nothing and is refused by name on both makers, as Xen does; drop the guest assertion of the hypervisor line, and rewrite that exit in the branch's own issue; replace the host test of which half of MSR 8BH each maker reports with a revision constructor per maker.

@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Design acceptance, by the orchestrator, with the review's named changes and NOTEs (#886 (comment)). This follows the owner's standing ruling: the manufacturers' documents and the published state of the art, Linux and Xen. The build is the body's design with these changes:

  1. Revision 0xffffffff loads nothing on either maker and is refused by name before select, as Xen does. A host test holds it. Revisions are the unsigned dword throughout, Running's included, as Linux and toyos_cpuvuln::Facts::microcode compare them.
  2. The guest assertion of the hypervisor line is dropped. The guest exit in issues/the-kernel-loads-no-cpu-microcode.md rests on the QMP reading.
  3. One revision constructor per maker for MSR 8BH's half, with no host test of the shift.
  4. The NOTEs, taken:
    • Roast 1 restated: the cost of an unreported fault before the IDT is ToyOS's own, and it buys the PCIDE verdict nothing until an Alder Lake or Raptor Lake file is embedded;
    • the EntrySign cutoff rule, named in its build-time form;
    • Xen's equal-revision divergence stated;
    • the siblings' "found" column taken from the reading;
    • the AMD bench capture: now on pr-886 at 9e5f88e5, The kernel loads pinned Intel and AMD microcode on every CPU before anything reads what an update changes, and a CPU that refuses one says so #886 (comment);
    • LICENSE.amd-ucode's five terms stated in the body;
    • the licence kind scoped to vendor firmware a device or CPU verifies by its maker's signature, as root CLAUDE.md names the category;
    • a NOTICE entry for the committed amd_shas.c rows;
    • the vendor taken from toyos_cpuvuln::Vendor;
    • the crate description;
    • the reseal arm's 16-byte-aligned static, built only with boot-actuators;
    • the issue deleted at landing, once its exit holds on the benches.

The hand-off with #873: #873 lands first and its PCIDE verdict reads the revision from the CPU. This branch then threads Running into that verdict, so the verdict judges the revision the load verified.

Japabu and others added 5 commits October 12, 2026 16:51
…nything reads what an update changes

`toyos-microcode` gains AMD's containers beside Intel's files (`amd`, as
AMD's `amd_ucode_info.py` and Linux v7.2's `amd.c` read them: patches matched
by the CPUID their Zen patch ID decodes to, chipset-specific patches and a
nonzero minimum-revision word refused as unmodelled, a patch at or above the
running revision loaded, an equal one again on every thread as Linux's early
loader does), whether a CPU loads at all (`Maker::of` over
`toyos_cpuvuln::Vendor`, `may_load` refusing a hypervisor and revision
0xffffffff by name, as Linux and Xen do), one revision constructor per maker's
half of MSR 8BH, revisions as the unsigned dword Linux compares, and the two
files the kernel embeds 16-byte aligned: Intel's 06-8c-01 and linux-firmware
20260916's microcode_amd_fam17h.bin.

`arch::x86_64::microcode::load` runs on the BSP in `after_console`, right
after `init_cr0` and before `random::key` and `control_regs::init`, and on each
AP right after its `init_cr0`, as Intel's guidance asks. Intel's trigger takes
the update data's address, AMD's the patch's, each an `asm!` of its own that
reads memory; family 0x17 then drops the translation of the patch's first and
last byte (Linux c809b0d0e52d). The load is read back, and every CPU that
reads a revision must run the first CPU's. One line per CPU says what it found,
what the file holds and what it runs.

There is no runtime SHA-256 of an AMD patch, where Linux and Xen have one:
their patch is a file the kernel did not ship, and these are inside the kernel
image. A host test holds every patch in the embedded container to Linux v7.2's
`amd_shas.c` (committed under tests/linux/, outside every shipped package),
which records none above EntrySign's cutoff: that is the cutoff rule's
build-time form.

The licence gate gains `Terms::Firmware`, scoped to vendor firmware a device
or CPU verifies by its maker's signature, the one place
LicenseRef-Intel-Microcode and LicenseRef-AMD-ucode pass; every x86-64 ROOT
carries both makers' licence texts under share/licenses/, as each asks.

The `boot-actuators` arm `microcode-reseal` hands the BSP the T14's update with
its header's revision raised to 0xbf and its checksum resealed, a const-built
aligned static, so the read-back is shown to stop a boot on real Intel. The
`microcode` metal row holds every CPU's line to Linux's reading of the same
machine: the T14's cpuinfo, and the AMD bench's capture, committed under
tests/metal/amd-linux/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Clippy's type_complexity refused the boxed closure it held per signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Ruling after the hung negative controls. The T14 t14-reseal arm and the AMD amd-trigger-removed arm both hung, and both needed a power cycle. The log partitions saved before each flash were read.

Cause. The BSP loads in after_console, before power::init_reset decodes the FADT's reset register, and on the AMD bench before any clock exists. A panic there takes the early-panic branch. That branch finds no reset to hand the machine back (and on AMD no clock to time a hold), so it holds the panel for good. On AMD the failed read-back is shown: the control read 0x8600106 where it wanted 0x860010d. On the T14 it is not yet separated from a fault at the trigger before the IDT, because the partitions cannot tell the two apart.

Ruling: a failed read-back is a refusal, not a panic. By the vendors' documentation, a microcode update the processor does not take leaves it running the revision it had. The CPU's own revision register (IA32_BIOS_SIGN_ID, 8BH) is the verdict, and that is what the kernel reports. So:

  1. verified stops asserting. A mismatch is a new Outcome (for example refused <want>: reads back <read>), the CPU runs the revision it reads back, and agree takes that value. The load sites do not move, so CPUID, RDRAND and RDSEED still read after the load.
  2. The judge's microcode_lines reds a refused line on both benches, on a boot that completes. A host case holds it.
  3. The module header and the body state this, citing Intel's SDM and AMD's documentation. The two statements this run showed false go: §8's "the panic's reset is one", and the header's "every failure … is a panic, which reports before the IDT".
  4. The microcode-reseal admission in FLASHABLE gets a reason that is true without the panic reset.

Filed separately, not fixed here: on x86-64 a panic before power::init_reset holds the machine for good, and on AMD so does every panic before clock::init. This run is its evidence.

Japabu and others added 4 commits October 12, 2026 21:43
…os-microcode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…tes Linux or Xen

Both negative-control arms of the microcode row hung their benches: the
read-back's panic came inside after_console, before power::init_reset
decodes the reset and, on the AMD bench, before any clock can time the
panic's bound, so the panel was held until a power cycle. The ruling on
#886 (comment 6131006022) makes a failed read-back a refusal: by the SDM
(Vol. 3A §12.11.7.1, §12.11.7.2 Example 12-10) and AMD's PPR 55922
(MSR0000_008B), MSR 8BH after a load is the revision the CPU runs, so a CPU
that does not take an update runs what it reads back. The loader now says
`refused <update>` with that read-back as `running`, holds it to the
machine's like any other, and the boot goes on; the metal judge reds any
refused line.

By the owner's clean-room ruling (#902), the judge is no longer held to a
Linux reading of either bench but to the update each maker's embedded file
holds for it, Linux's amd_shas.c and the AMD bench's Linux capture are
deleted with the digest test that read them, and every Linux and Xen
citation is replaced by Intel's or AMD's document or removed. The word at
patch offset 12 is named as AMD's parser names it, mc_patch_data_checksum.

A panic before the reset register is read holds the machine for good: filed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
…rocode

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy
@Japabu Japabu changed the title The kernel loads pinned Intel and AMD microcode on every CPU before anything reads what an update changes The kernel loads pinned Intel and AMD microcode on every CPU before anything reads what an update changes, and a CPU that refuses one says so Oct 12, 2026
@Japabu

Japabu commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 on metal, at e2b398cb6, run by the orchestrator from agents/886-r2/metal/request.txt. Each image's hash was checked by the runner before the flash, and each stick's log partition was saved before it. No arm hung: every boot completed and came back.

arm judge reading
T14 head exit 0, 1 passed
T14 reverted (m3) exit 1 "expected one microcode line from each of {0..7}, got []"
T14 reseal exit 1 (judged from its tree b7a7b062a) cpu0 found 0xbe refused 0xbf running 0xbe; cpus 1-7 current 0xbe; red on "cpu0 refused the update it was handed and runs 0xbe"
AMD head exit 0, 1 passed
AMD reverted (m3) exit 1 no microcode line
AMD no-INVLPG (m4, reported) exit 0, 1 passed
AMD trigger removed exit 1 cpu0 refused, running 0x8600106; red on "cpu0 refused the update it was handed"

The T14 reseal arm answers round 1's open question: the old hang was the panic hold, not a fault at the trigger, since with the refusal in place the boot completes. One thing to check in review: the judge rebuilt the reseal image from b7a7b062a with sha256 55bec413…, where the booted image was c6d44fe8…. The verdict reads the booted image's kernel log. The readbacks are under agents/886-r2/metal/ (the reseal judge under t14-reseal-judge/).

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #886 at e2b398cb6, round 2, against .claude/agents/reviewer.md and root CLAUDE.md on origin/main (5096551ca; b680a973b, #873, has landed since, and git merge-tree of it with this head is clean).

Size. git diff --shortstat origin/main...e2b398cb6: 24 files, +1568 −521.

  • Production: +835 −212, net +623.
  • Tests: +578 −241, net +337.
  • Records and data: NOTICE +34, licence text +64, issues +57 −68, and the container.

The deletion this review names is under BLOCKER 4.

Round 1's BLOCKERs (design review, comment 6126224559)

  • CLOSED — revision 0xffffffff loads nothing and is refused by name on both makers. Covered by may_load / Unloaded::Disabled (kernel/microcode/src/lib.rs) and revision_0xffffffff_loads_nothing; the round's run.out shows the host suite green at this head.
  • CLOSED — the guest assertion of the hypervisor line is dropped. No guest test reads the line, and the issue's guest exit rests on the QMP reading.
  • CLOSED — one constructor per maker reads MSR 8BH's half (Revision::from_sign_id, Revision::from_patch_level), and nothing tests the shift.

The ruling after the hung controls (comment 6131006022): built.

  1. taken answers Outcome::Refused { update, running }, agree takes the read-back, and the load sites have not moved (kernel/src/arch/x86_64/microcode.rs:81-88,195-200).
  2. microcode_lines reds a refused line on any machine, and microcode_verdict holds that for a T14 BSP and for the AMD bench. j1 turns it red.
  3. The module header cites SDM §12.11.7 and 55922 MSR0000_008B, and both false statements are gone.
  4. FLASHABLE's reason no longer rests on the panic reset.

On metal, both negative controls now finish their boots and red the row. The T14 reseal arm reads found 0xbe refused 0xbf running 0xbe and the AMD trigger-removed arm reads refused 0x860010d running 0x8600106, as the readbacks' kernel.logs show.

The image-hash question: the verdict stands.

  • The judge directory's kernel.log is byte-identical to the booted readback's (sha256 6f3a20ed… in both). That log names supervisor: build b7a7b062a… clean and actuators: …,microcode-reseal,….
  • The booted image is c6d44fe8…, the hash the request gives.
  • 55bec413… was rebuilt in a second checkout (toyos-microcode-reseal) of the same commit. It differs from the booted image in 2,388,453 bytes, spread over the ESP, loader and ROOT ranges. Three tracked defects account for that:
    • issues/two-checkouts-of-one-tree-build-different-guest-bytes.md (the kernel and ROOT, and the loader's per-checkout key);
    • issues/an-images-guids-are-drawn-from-the-hosts-random-source.md;
    • issues/an-images-signed-version-and-its-fat-dates-are-the-hosts-clock.md.
  • Both images contain b7a7b062a… once.
  • m1 changes only the row's registration, so the judge that read the log is this head's microcode_on_metal.
  • Every other arm's log names the tree its request gives (e2b398cb6, 1e92b3edc, 406b5863d, 5b029c0a8, 0cb620cae).

BLOCKER

  1. kernel/microcode/src/amd.rs:15-18,125,161-165 — this matches a patch by the CPUID its patch ID decodes to and never reads the equivalence table. The owner has ruled that a patch is matched the way AMD's own parser matches it, through the equivalence table. Required:

    • read the table and match the CPU's CPUID.01H:EAX to its processor ID, and that ID to the patch;
    • refuse a malformed table by name;
    • delete named;
    • add a host test whose container's table maps 0x00860f01 to a processor ID other than the one in a patch whose ID decodes to 0x00860f01, and which expects NoMatch.

    The implementer then runs the mutation that puts back named(patch.revision.0) == signature.0 in place of the table lookup, and that test must turn red. Every container the current tests build has a zeroed 16-byte table (amd/tests.rs container), so the table is never read and no test today can fail on how a patch is matched.

  2. kernel/microcode/src/amd.rs (whole module), amd/tests.rs — the AMD reader was not built clean-room. Root CLAUDE.md, as Another implementation's source is read only clean-room, and the tree names Linux only as a host platform or a libc interface #902 left it, and the owner's ruling require two steps: a reader writes the facts of AMD's amd_ucode_info with no implementation cited, and a builder works from those facts alone. Nothing on the record is such a facts specification:

    • r2/sources/excerpts.txt holds no fact from the tool;
    • the body cites amd_ucode_info.py directly as the builder's source.

    The behaviours this round kept are the ones round 1's design took from reading Linux's amd.c and Xen:

    • the patch-ID decode;
    • skipping the table;
    • the chipset refusal;
    • the family-0x17 INVLPG;
    • the equal-revision reload.

    Taking out the citations did not change where they came from. amd.rs:41 still names the field layout by a C struct name, struct microcode_header_amd, which the cited Python tool does not declare. Fix: rebuild the container reader from a facts specification written by a separate reader of amd_ucode_info (provenance kept in the reader's evidence, the specification never committed), and drop every behaviour that specification and AMD's documents do not state from the parser, unless it is kept under BLOCKER 3.

  3. kernel/microcode/src/amd.rs:24-26, :63-67 — the owner's third ruling is not met. Each undocumented behaviour has to be stated as undocumented in the module header, with the measurement behind it where one exists. The header names no measurement for either behaviour.

    • Its claim that "the read-back is the CPU's verdict on both" is false of the equal reload. On amd-head, cpus 1, 3, 5 and 7 read found 0x860010d loaded 0x860010d, so their read-back is the same with or without the reload. If the rule were strictly newer they would read current 0x860010d, and microcode_on_metal would still pass.
    • The header says nothing about what m4 measured for INVLPG.
  4. kernel/microcode/src/amd.rs:61-67,167-170, kernel/src/arch/x86_64/microcode.rs:170-173, amd/tests.rs:93 — the family-0x17 INVLPG has no document and no measurement behind it. The only measurement on record goes the other way: amd-no-invlpg (m4, tree 0cb620cae) reads the same eight lines as amd-head, passes the row, and completes in 1314 ms against 1318 ms. Delete Patch::leaves_its_mapping_uncached, family, the two cpu::invlpg calls in load_amd, and a_family_0x17_patch_drops_its_mapping_and_a_later_familys_does_not. The alternative is to put on the record a measurement that backs the step, and none does today. Root CLAUDE.md: not knowing whether code is needed is no reason to keep it.

  5. PR body, "Gates" and "Metal" — the hardware readings are not evidence at this head.

    • The body still reads "every metal row: not run".
    • The readings sit in an orchestrator comment that links local scratch paths.
    • ToyOSOrg/evidence pr-886 at 8b6b8027 holds only the staging (r2/metal/*.stage.log): no judge log, no kernel.log, no boot.txt.

    Publish each arm's judge command, exit and judge log, with its readback's kernel.log and boot.txt, on the evidence branch, and write them into the body:

    • t14-head 0;
    • t14-reverted 1;
    • t14-reseal 1, judged from b7a7b062a;
    • amd-head 0;
    • amd-reverted 1;
    • amd-trigger-removed 1;
    • amd-no-invlpg 0, with its phase times beside amd-head's.

    BLOCKERs 1 to 3 change the AMD reader, so its AMD arms have to be booted again at the new head.

NOTE

  • PR body, "Metal" — false of the record. It expects "sixteen CPUs loaded 0x860010d" on the AMD bench, but every AMD readback says SMP: roster full at 8 CPUs and 8 of 16 MADT cpus online.
  • PR body, "Before it lands" / design acceptance's hand-off — x86-64 kernel leaves are global under a declared CR4.PGE, and only a kernel-half change drops them #873 has landed as b680a973b. Its PCIDE is decided by PCID and INVPCID (control_regs.rs:539-546) and reads no microcode revision, so the accepted Running witness has nothing to thread into, and this head builds none. Merge origin/main and have the body say the hand-off is void, citing b680a973b.
  • issues/the-kernel-loads-no-cpu-microcode.md — the T14 half of its exit is now met at this head. Delete it in the head whose AMD readings meet the AMD half, with the evidence in the commit message.
  • tests/common/metal.rs:872-892 (off this branch; file it, don't fix it here) — read_readback does not hold the booted log's supervisor: build to the tree doing the judging. The t14-reseal-judge run also overwrote that directory's image.img at 01:41 with an image that was never booted, so a readback directory no longer proves which image produced its log.
  • ToyOSOrg/evidence pr-886 r2/metal/make.py, stage.sh, r2/mut/run.sh, make.py — this round's evidence was produced by Python and shell scripts of ToyOS's own making. Root CLAUDE.md says ToyOS writes none, and the harness's own --metal-readback staging and cargo test filters already do the job.

SEND BACK

@Japabu

Japabu commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Ruling after round 2's review (#886 (comment)): #886 lands Intel's loader alone, and the AMD loader leaves this branch.

Intel's half is built from the SDM, judged on the T14 (head green, both controls red, the reseal arm refused as ruled), and owes nothing. AMD's half owes a clean-room reader (BLOCKER 2), matching by the equivalence table (1), and undocumented behaviours stated with their measurements (3). Holding Intel's half for that work delays it for nothing. So:

  1. The AMD container reader, amd.rs, its tests and the family-0x17 INVLPG (BLOCKER 4) are deleted from this branch. On an AMD CPU the kernel loads nothing and logs by name that it has no AMD loader. The AMD row expects that line.
  2. issues/the-kernel-loads-no-cpu-microcode.md keeps the AMD half open. Its exit is a loader built from a facts specification written by a separate reader of AMD's amd_ucode_info (never committed). It matches through the equivalence table, with the host test BLOCKER 1 names, and states each undocumented behaviour with its measurement. The T14 half is recorded as met by this branch.
  3. BLOCKER 5: publish every arm's judge log, kernel.log and boot.txt on pr-886, and write them into the body. The arms at the new head are run again: T14 head and the reseal control, and AMD head, which must show the no-loader line.
  4. The NOTEs are taken: the roster count, the void x86-64 kernel leaves are global under a declared CR4.PGE, and only a kernel-half change drops them #873 hand-off citing b680a973b, and the readback-provenance issue filed. The evidence is produced with the harness and cargo, not scripts of ToyOS's own.

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