diff --git a/Cargo.lock b/Cargo.lock index db215931436..126652cbcee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1831,6 +1831,7 @@ dependencies = [ "toyos-gpt", "toyos-hda", "toyos-its", + "toyos-microcode", "toyos-pci", "toyos-phys", "toyos-ps2", @@ -4279,6 +4280,9 @@ dependencies = [ [[package]] name = "toyos-microcode" version = "0.1.0" +dependencies = [ + "toyos-cpuvuln", +] [[package]] name = "toyos-mixer" diff --git a/NOTICE b/NOTICE index d4b445726cd..e6dd7161714 100644 --- a/NOTICE +++ b/NOTICE @@ -428,6 +428,14 @@ kernel/microcode/intel-ucode/* — Intel microcode, redistributable unmodified sha256 03efb1491c7e899feb2665fa299363e64035e5444c1b8bc1f6ebed30de964e12) SPDX-License-Identifier: LicenseRef-Intel-Microcode +The kernel embeds 06-8c-01 and loads it on the CPU it names; 06-cc-02 is +`toyos-microcode`'s test input. The licence permits redistribution in binary +form, unmodified, on three conditions: the copyright notice and the +disclaimer are reproduced with the distribution, which every x86-64 image does +as /system/share/licenses/Intel-microcode-license.txt; Intel's name endorses +nothing derived from it; and no reverse engineering, decompilation or +disassembly. + tests/cavp/ — NIST's SHA-2 test vectors, CAVP SHAVS ---------------------------------------------------- diff --git a/build/build.rs b/build/build.rs index 7dcec06119b..5b009f2233e 100644 --- a/build/build.rs +++ b/build/build.rs @@ -803,6 +803,13 @@ pub const TRUST_ROOTS_LICENCE: (&str, &[u8]) = ( include_bytes!("../licenses/CDLA-Permissive-2.0-webpki-root-certs.txt"), ); +/// Where on ROOT the licence of the Intel microcode an x86-64 kernel embeds +/// is, and its text: the licence asks that a redistribution carry it. +pub const MICROCODE_LICENCE: (&str, &[u8]) = ( + "share/licenses/Intel-microcode-license.txt", + include_bytes!("../licenses/Intel-microcode-license.txt"), +); + /// The Mozilla root program's authorities as `webpki-root-certs` publishes /// them, each certificate a PEM block in the published order. pub fn trust_roots() -> Vec { @@ -837,6 +844,10 @@ fn build_and_assemble( root_files.push((TRUST_ROOTS.to_string(), trust_roots())); root_files.push((TRUST_ROOTS_LICENCE.0.to_string(), TRUST_ROOTS_LICENCE.1.to_vec())); + match arch { + Arch::X86_64 => root_files.push((MICROCODE_LICENCE.0.to_string(), MICROCODE_LICENCE.1.to_vec())), + Arch::Aarch64 => {} + } // Extra files (test binaries, shared libs) for (name, data) in extra_files { diff --git a/build/licence.rs b/build/licence.rs index cfafd044feb..be62f8b1429 100644 --- a/build/licence.rs +++ b/build/licence.rs @@ -87,13 +87,20 @@ const FONTS_ONLY: &[&str] = &["OFL-1.1"]; /// CDLA-Permissive-2.0 for the Mozilla roots. const DATA_ONLY: &[&str] = &["CDLA-Permissive-2.0"]; -/// What a licence is judged as: [`FONTS_ONLY`] passes only for a font and -/// [`DATA_ONLY`] only for the trust roots. +/// Allowed for a [`Terms::Firmware`] row, and nowhere else: the owner's +/// ruling that vendor firmware a device or CPU verifies by its maker's +/// signature may ship, pinned and unmodified (root `CLAUDE.md`). +const FIRMWARE_ONLY: &[&str] = &["LicenseRef-Intel-Microcode"]; + +/// What a licence is judged as: [`FONTS_ONLY`] passes only for a font, +/// [`DATA_ONLY`] only for the trust roots and [`FIRMWARE_ONLY`] only for +/// vendor firmware. #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum Scope { Code, Font, Data, + Firmware, } impl Scope { @@ -103,6 +110,7 @@ impl Scope { Scope::Code => false, Scope::Font => FONTS_ONLY.contains(&id), Scope::Data => DATA_ONLY.contains(&id), + Scope::Firmware => FIRMWARE_ONLY.contains(&id), } } } @@ -119,12 +127,15 @@ pub enum Terms { /// A font's glyphs, or a raster of them, under an SPDX expression: the one /// place [`FONTS_ONLY`] passes. Font(&'static str), + /// Vendor firmware a device or CPU verifies by its maker's signature, + /// under an SPDX expression: the one place [`FIRMWARE_ONLY`] passes. + Firmware(&'static str), } impl Terms { fn expr(self) -> &'static str { match self { - Terms::Spdx(e) | Terms::Font(e) => e, + Terms::Spdx(e) | Terms::Font(e) | Terms::Firmware(e) => e, } } } @@ -577,13 +588,13 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ "kernel/microcode/intel-ucode/06-8c-01", "efe83e312b90f7fe4b8f75260087edf03e048d2f4f80caef2ef631c842714bb3", "NOTICE", - Terms::Spdx("LicenseRef-Intel-Microcode"), + Terms::Firmware("LicenseRef-Intel-Microcode"), ), ( "kernel/microcode/intel-ucode/06-cc-02", "4e43bb4d23c3638f8c16967d61e8f6456ae0da2ddd1da82ab579a50715147bb1", "NOTICE", - Terms::Spdx("LicenseRef-Intel-Microcode"), + Terms::Firmware("LicenseRef-Intel-Microcode"), ), ]; @@ -1136,6 +1147,7 @@ fn judge_files(ledger: &[Row], tracked: &[String], shipping: &Shipping, report: let scope = match terms { Terms::Spdx(_) => Scope::Code, Terms::Font(_) => Scope::Font, + Terms::Firmware(_) => Scope::Firmware, }; report.judge(Finding::at(Kind::File, path, terms.expr(), String::new()), scope); } @@ -1493,6 +1505,12 @@ mod tests { assert!(verdict_of("CDLA-Permissive-2.0").is_some(), "CDLA passed outside data"); assert!(judge_licence("CDLA-Permissive-2.0", Scope::Font).is_some(), "CDLA passed for a font"); assert_eq!(judge_licence("CDLA-Permissive-2.0", Scope::Data), None); + for firmware in FIRMWARE_ONLY { + assert!(verdict_of(firmware).is_some(), "{firmware} passed outside firmware"); + assert!(judge_licence(firmware, Scope::Font).is_some(), "{firmware} passed for a font"); + assert_eq!(judge_licence(firmware, Scope::Firmware), None); + } + assert!(judge_licence("OFL-1.1", Scope::Firmware).is_some(), "OFL passed as firmware"); assert_eq!(verdict_of("MPL-2.0"), None); } diff --git a/issues/a-metal-readback-does-not-prove-which-image-wrote-its-log.md b/issues/a-metal-readback-does-not-prove-which-image-wrote-its-log.md new file mode 100644 index 00000000000..6ae0d0c95bc --- /dev/null +++ b/issues/a-metal-readback-does-not-prove-which-image-wrote-its-log.md @@ -0,0 +1,32 @@ +--- +status: open +kind: tooling +opened: 2026-10-13 +--- + +# A metal readback does not prove which image wrote its log + +`read_readback` (`tests/common/metal.rs`) judges a readback directory's +`kernel.log` without holding the log's `supervisor: build clean` line +to the tree doing the judging, so a judge from one tree passes or reds a boot +of another's image and says nothing. And `--metal --metal-readback ` +from a tree whose images the directory does not hold stages that tree's +images into it, overwriting the `image.img` that was booted, so the directory +no longer holds the image that wrote its log. + +**Evidence.** #886's round-2 `t14-reseal` arm, booted from tree `b7a7b062a` +(image sha256 `c6d44fe8…`): a first judging from the head tree `e2b398cb6` +staged that tree's image beside the readback and judged nothing (exit 2, +https://github.com/ToyOSOrg/evidence/blob/f17cea3bdae1bda4586220fc7b9f551d9cecff49/r2/readings/t14-reseal/judge-from-e2b398cb6-tree.log); +the judging from `b7a7b062a` in a second checkout, over a copy of the +readback, rebuilt and wrote its `image.img` (`55bec413…`), never booted and +2,388,453 bytes from the booted one +(https://github.com/ToyOSOrg/ToyOS/pull/886#issuecomment-6135201083). That +the log came from `b7a7b062a` was told by reading its `supervisor: build` +line; no check did. + +Owner: the orchestrator. + +**Exit**: a judge from a tree other than the one a readback's log names +refuses the readback by name, and judging a readback directory never +writes its `image.img`. diff --git a/issues/a-panic-before-the-reset-register-is-read-holds-the-machine-for-good.md b/issues/a-panic-before-the-reset-register-is-read-holds-the-machine-for-good.md new file mode 100644 index 00000000000..a7b30c01bea --- /dev/null +++ b/issues/a-panic-before-the-reset-register-is-read-holds-the-machine-for-good.md @@ -0,0 +1,35 @@ +--- +status: open +kind: defect +opened: 2026-10-12 +--- + +# A panic before the reset register is read holds the machine for good + +On x86-64 the kernel learns its reset from the FADT in `power::init_reset`, +which `arch::boot::interrupts` calls, well after `arch::boot::after_console` +(`kernel/src/main.rs`). A panic before it finds +`power::can_reboot` false, and `panic_reboot::arm` returns `Bound::Held`: +the panel is held until somebody cuts the power. On the AMD bench every panic +before `clock::init` (`arch::boot::clock`) is held as well, whatever the +reset: its CPU states no counter frequency (CPUID leaves 0x15 and 0x16), so +`panic_reboot::deadline` has nothing to time the bound with and `arm` takes +its "no clock" arm. + +Everything `after_console` runs is in that window, the BSP's microcode load +among them. `issues/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md` +covers a later window, where a reset is known and both bounds stand down. + +**Evidence.** #886's two negative-control arms panicked at the microcode +read-back inside `after_console`, and both benches had to be power-cycled by +hand: the T14 armed with `microcode-reseal` and the AMD bench with its +trigger write removed. The AMD bench's log partition, read after the power +cycle, carries the failed read-back; the T14's cannot tell the panic from a +fault at the trigger +(https://github.com/ToyOSOrg/ToyOS/pull/886#issuecomment-6131006022). + +Owner: the orchestrator. + +**Exit**: a T14 boot and an AMD bench boot armed with `test-early-panic`, +which panics right after `after_console`, each reset themselves with nobody +at the machine, and the boot after it reports the panic. diff --git a/issues/the-kernel-is-at-least-as-secure-as-linux-on-every-machine-toyos-supports.md b/issues/the-kernel-is-at-least-as-secure-as-linux-on-every-machine-toyos-supports.md index 75ceb272e47..74564696d80 100644 --- a/issues/the-kernel-is-at-least-as-secure-as-linux-on-every-machine-toyos-supports.md +++ b/issues/the-kernel-is-at-least-as-secure-as-linux-on-every-machine-toyos-supports.md @@ -27,7 +27,7 @@ firmware's memory-overwrite request, by the owner's "Keep crash records" - `issues/user-pointer-checks-have-no-spectre-v1-fence-and-smap-is-optional.md` - `issues/indirect-branches-and-returns-run-without-thunks.md` - `issues/tsx-stays-as-firmware-left-it.md` -- `issues/the-kernel-loads-no-cpu-microcode.md` +- `issues/the-kernel-loads-no-amd-microcode.md` - `issues/no-user-address-is-drawn-per-spawn.md` - `issues/the-kernel-has-no-stack-protector.md` - `issues/no-kernel-address-is-drawn-per-boot.md` diff --git a/issues/the-kernel-loads-no-amd-microcode.md b/issues/the-kernel-loads-no-amd-microcode.md new file mode 100644 index 00000000000..a60b6d6e8b1 --- /dev/null +++ b/issues/the-kernel-loads-no-amd-microcode.md @@ -0,0 +1,48 @@ +--- +status: open +kind: defect +opened: 2026-09-29 +--- + +# The kernel loads no AMD microcode + +The kernel is to load CPU microcode signed by the CPU's maker, pinned by +version and hash the way vendor device firmware is (owner, 2026-09-30). +It loads Intel's (`toyos-microcode`, `kernel/src/arch/x86_64/microcode.rs`) +and no AMD CPU's: on one, every CPU's line says ToyOS has no AMD microcode +loader, and the `microcode` metal row holds the AMD bench to that line. + +**Met** by #886: on the T14 every CPU runs Intel's 0xbe for 0x806c1 +platform 7, `current` or `loaded`; the T14 armed with `microcode-reseal` +completes its boot and reds the row on cpu0's `refused` line; every guest +loads nothing, for its CPU reports a hypervisor or a vendor ToyOS has no +loader for. + +**AMD.** AMD documents MSR0000_008B (55922) and not the patch loader, so +the loader rests on AMD's own parser, `amd_ucode_info`. Root `CLAUDE.md` +("Zero legacy") lets that source be read only clean-room: one agent writes +from it a specification of facts citing no implementation, and another +builds from that specification alone. #886's loader was not built that way +and was taken out. + +Owner: the orchestrator. + +**Exit**: the kernel loads, early on every AMD CPU, the newest patch the +embedded container holds for it, and a CPU that does not take one says so +and runs what it reads back: + +- the loader is built clean-room: one agent reads `amd_ucode_info` and + writes from it a specification of facts of the container format citing no + implementation, keeping the provenance in its own evidence, and another + builds the loader from that specification alone, handed to it in its + brief and never committed; +- a patch is matched through the container's equivalence table, CPUID.01H:EAX + to processor ID and processor ID to patch, and a malformed table is refused + by name; a host test whose table maps 0x00860f01 to a processor ID other + than the one in a patch whose ID decodes to 0x00860f01 expects no match; +- every behaviour neither AMD's documents nor that specification states is + named undocumented in the module header with the measurement behind it, + or is not in the loader; +- the `microcode` row is green on the AMD bench with every CPU running the + container's patch, and the bench with its trigger write removed completes + its boot and reds the row on a `refused` line. diff --git a/issues/the-kernel-loads-no-cpu-microcode.md b/issues/the-kernel-loads-no-cpu-microcode.md deleted file mode 100644 index da29d49f314..00000000000 --- a/issues/the-kernel-loads-no-cpu-microcode.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-29 ---- - -# The kernel loads no CPU microcode - -The kernel is to load CPU microcode signed by the CPU's maker, pinned by -version and hash the way vendor device firmware is (owner, 2026-09-30). It -loads none, so a CPU whose BIOS ships stale microcode stays below Linux at -`Ubuntu-6.8.0-142.142` on every line that rests on microcode. - -**Exit**: the kernel loads current microcode early on every CPU, at least as -current as Linux's. - -`toyos-microcode` validates an Intel update file and picks the update for one -CPU; nothing calls it. The T14's eight CPUs run 0xbe from its firmware, which -is Intel's newest for them at `microcode-20260925`, so a load there is a no-op. - -**Where.** The kernel, on every CPU, from a file it embeds per CPU ToyOS -supports on metal. Not the loader: it runs on the BSP alone, reaching the APs -takes EFI MP Services, and the kernel reads every CPU's revision anyway. In -`percpu::init_bsp` after `idt::init` and in `percpu::init_ap` after -`control_regs::init`, before `fpu::init` on each: after the IDT, so a load that -faults reports on this kernel's channels; before anything acts on an -enumeration an update changes (CPUID.7.0:EDX, `IA32_ARCH_CAPABILITIES`, RTM -and HLE); and before `ROSTER.echo`, so `boot_aps` starting one AP at a time is -the serialisation per core that SDM Vol. 3A §12.11.6.3 asks. A CPU with -CPUID.1:ECX[31] set loads nothing, as Linux does, and `IA32_PLATFORM_ID` is -read on a GenuineIntel CPU only. The update data starts 16-byte aligned (§12.11.6), -and the trigger is an `asm!` of its own: `cpu::wrmsr` is `nomem`, and the CPU -reads the update through this write. INIT keeps an update; a hard reset clears -it (§12.11.6.1). - -**Verified.** After the trigger the CPU writes 0 to `IA32_BIOS_SIGN_ID`, runs -CPUID.01H, and panics unless it reads back the update's revision (Example -12-10). After `boot_aps`, CPUs that report different revisions panic the boot. -Each CPU logs its platform, the revision it found and the one it runs. It -stops rather than run the firmware's revision behind a log line: the file is -pinned and chosen by the CPU's own signature, platform and revision, so a CPU -that does not take it means one of those is wrong, a ToyOS defect, and a boot -that went on would run below the revision its image claims with nothing red. - -**AMD.** linux-firmware's `amd-ucode/microcode_amd_fam{17,19,1a}h.bin` is a -container (magic 0x00414d44): an equivalence table from CPUID.01H:EAX to a -processor ID, then patches. MSR C001_0020 takes a patch's address, and MSR 8B -must then read back its patch ID; family 0x17 also invalidates the patch's -pages (Linux `amd.c`, `__apply_microcode_amd`). On families 0x17, 0x19 and part -of 0x1a below a per-CPU cutoff revision the CPU's own signature check is broken -(EntrySign; `cpu_has_entrysign`, `need_sha_check`), so the hash pin is the only -check, as `amd_shas.c` is Linux's. linux-firmware's `LICENSE.amd-ucode` is -unread. ToyOS has no AMD metal: CI's EPYCs are KVM guests, which load -nothing. - -**Licence.** `LicenseRef-Intel-Microcode` is in no `ALLOWED` row of -`build/licence.rs`, and a committed file ships once a shipped package's directory -holds it. So the commit that makes the kernel depend on `toyos-microcode` reds -the licence gate on both files under `kernel/microcode/intel-ucode/`, the -test-only `06-cc-02` included, whatever the kernel embeds, until an exception -scoped to CPU microcode admits them. - -Each step's exit, in the testing ladder's order: - -- host: the step's decisions are pure in `toyos-microcode`, each refused by - name and each red under its mutation: a hypervisor or a vendor no file - covers, `select`, a read-back that is not the update's revision, CPUs that - disagree; -- metal: a T14 boot logs platform 7 and 0xbe current on all eight CPUs; a - `boot-actuators` arm that raises only the header's revision to 0xbf and - reseals its checksum stops that boot at the read-back or at a fault, since - Intel's payload still says 0xbe; -- metal, on no machine ToyOS has: the load arm, on a CPU whose firmware runs - older microcode than the embedded file; AMD's loader, on AMD metal; -- guest: every CPU logs why it loads nothing, a hypervisor under KVM and a - vendor no file covers under TCG's `AuthenticAMD` `qemu64`, and a guest test - asserts the line. diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 03c0089de92..ef5a3dcd907 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -490,6 +490,7 @@ toyos-gicv3 = { path = "gicv3" } toyos-gpt = { path = "../lib/gpt" } toyos-hda = { path = "../lib/hda" } toyos-its = { path = "its" } +toyos-microcode = { path = "microcode" } toyos-pci = { path = "pci" } toyos-phys = { path = "phys" } toyos-smmu = { path = "smmu" } diff --git a/kernel/microcode/Cargo.toml b/kernel/microcode/Cargo.toml index 5a259ff2508..3b44fb5d7c3 100644 --- a/kernel/microcode/Cargo.toml +++ b/kernel/microcode/Cargo.toml @@ -3,8 +3,11 @@ [package] name = "toyos-microcode" -description = "Intel microcode update files validated and matched to a CPU as the SDM's Microcode Update Facilities define them, pure." +description = "Intel microcode update files validated and matched to a CPU as the SDM's Microcode Update Facilities define them, whether a CPU loads at all, and the file the kernel embeds, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" publish = false + +[dependencies] +toyos-cpuvuln = { path = "../cpuvuln" } diff --git a/kernel/microcode/src/lib.rs b/kernel/microcode/src/lib.rs index 0f5d0907164..c9afcd03120 100644 --- a/kernel/microcode/src/lib.rs +++ b/kernel/microcode/src/lib.rs @@ -1,18 +1,28 @@ -//! Intel microcode update files, read as the SDM defines them, pure. +//! CPU microcode as Intel publishes it, matched to one CPU, and the file the +//! kernel loads it from, pure. //! //! A file Intel publishes for one processor signature is updates laid end to -//! end. [`select`] validates every one against SDM Vol. 3A §12.11 (order -//! number 325384-093US) — header, checksum and the optional extended signature -//! table — and answers with the newest that names a CPU's signature and -//! platform, or why it loads none. One malformed update refuses the whole file. +//! end, and so is any number of such files laid end to end. [`select`] +//! validates every update against SDM Vol. 3A §12.11 (order number +//! 325384-093US) — header, checksum and the optional extended signature table +//! — and answers with the newest that names a CPU's signature and platform, +//! or why it loads none. One malformed update refuses the whole file. Only +//! the documented headers are read: an update's data is Intel's signed +//! payload, handed on whole and never interpreted. //! -//! Only the documented header, checksums and signature table are read. The -//! update data is the maker's signed payload: handed on whole, never -//! interpreted. +//! Only an update newer than the revision the CPU runs loads (Example 12-10): +//! the CPU "may or may not reload" an equal one, and loading an update on one +//! logical processor loads it on every other in its core. +//! +//! Before any file is read, [`loader`] says whether ToyOS has a loader for a +//! CPU's vendor and [`may_load`] whether the CPU loads at all. +//! +//! [`INTEL`] is the file the kernel embeds, as Intel publishes it and pinned +//! by its digest in `NOTICE`. //! //! Pure: no I/O, no allocation, no `unsafe`. The caller reads the CPU. //! -//! The file is untrusted, so nothing here may panic on it. +//! A file is untrusted, so nothing here may panic on one. #![no_std] #![forbid(unsafe_code)] @@ -28,9 +38,108 @@ clippy::unimplemented )] +use core::fmt; + +use toyos_cpuvuln::Vendor; + #[cfg(test)] mod tests; +/// A file the kernel loads from, starting 16-byte aligned: Intel's trigger +/// takes an update's data only at such an address (SDM §12.11.6), and every +/// update behind the first starts a multiple of 1024 bytes in, its data 48 +/// bytes after that. +#[repr(C, align(16))] +pub struct Aligned(pub Bytes); + +/// Intel's updates for the CPUs ToyOS has a bench for, end to end: +/// `intel-ucode/06-8c-01`, the T14's i5-1135G7. +pub static INTEL: &Aligned<[u8]> = &Aligned(*include_bytes!("../intel-ucode/06-8c-01")); + +/// CPUID.01H:EAX, which Intel's updates name their CPU by. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Signature(pub u32); + +/// An Intel update revision, the dword MSR 8BH reports it in, compared +/// unsigned. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Revision(pub u32); + +impl Revision { + /// The highest revision, which no update loads over. + pub const DISABLED: Self = Self(u32::MAX); + + /// MSR 8BH read after writing it 0 and executing CPUID.01H, whose upper + /// dword the revision is (SDM §12.11.7.1). + pub const fn from_sign_id(msr: u64) -> Self { + Self((msr >> 32) as u32) + } +} + +impl fmt::LowerHex for Revision { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::LowerHex::fmt(&self.0, f) + } +} + +/// What a valid file holds for one CPU. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Choice<'a> { + /// The newest update naming the CPU, newer than the revision it runs. + Load(Update<'a>), + /// The newest update naming the CPU is this revision, and the CPU already + /// runs it or a newer one. + Current(Revision), + /// No update in the file names the CPU. + NoMatch, +} + +/// Whether ToyOS has a loader for a CPU of `vendor`: Intel's alone. +pub fn loader(vendor: Vendor) -> Result<(), Unloaded> { + match vendor { + Vendor::Intel => Ok(()), + Vendor::Amd | Vendor::Hygon | Vendor::Centaur | Vendor::Zhaoxin | Vendor::Unknown => { + Err(Unloaded::Vendor(vendor)) + } + } +} + +/// Why a CPU loads nothing, said before any file is read. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Unloaded { + /// ToyOS has no loader for this vendor, so nothing of its CPU is read. + Vendor(Vendor), + /// CPUID.01H:ECX[31], which a hypervisor sets for its guest (AMD's APM + /// Vol. 3, 24594 rev 3.36, §E.3.2): the guest's microcode is the + /// hypervisor's. + Hypervisor, + /// The CPU reports [`Revision::DISABLED`]. + Disabled, +} + +impl fmt::Display for Unloaded { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Vendor(vendor) => write!(f, "ToyOS has no {vendor:?} microcode loader"), + Self::Hypervisor => f.write_str("a hypervisor owns this CPU's microcode"), + Self::Disabled => f.write_str("revision 0xffffffff, which no update loads over"), + } + } +} + +/// Whether a CPU [`loader`] admits may load, from its +/// CPUID.01H:ECX and the revision it reports. A guest loads nothing whatever +/// it reports. +pub fn may_load(cpuid_1_ecx: u32, found: Revision) -> Result<(), Unloaded> { + if cpuid_1_ecx & 1 << 31 != 0 { + return Err(Unloaded::Hypervisor); + } + if found == Revision::DISABLED { + return Err(Unloaded::Disabled); + } + Ok(()) +} + /// The header's length, and so the offset of the update data (Table 12-7). const HEADER: usize = 48; /// The extended signature table's header: count, checksum, 12 reserved bytes @@ -41,10 +150,6 @@ const EXT_SIGNATURE: usize = 12; /// Total Size is "always a multiple of 1024" (Table 12-7). const GRANULE: usize = 1024; -/// CPUID.01H:EAX, compared whole with an update's (§12.11.3). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Signature(pub u32); - /// Which bit of an update's processor flags names this CPU's platform /// (§12.11.4). #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -62,16 +167,9 @@ impl PlatformId { } } -/// An update revision. Signed (Table 12-7): one update is newer than another -/// only when numerically larger (Example 12-10). -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Revision(pub i32); - -impl Revision { - /// `IA32_BIOS_SIGN_ID` (MSR 8BH) read after writing it 0 and executing - /// CPUID.01H: the revision is its upper dword (§12.11.7.1). - pub const fn from_sign_id(msr: u64) -> Self { - Self((msr >> 32) as u32 as i32) +impl fmt::Display for PlatformId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) } } @@ -117,18 +215,6 @@ impl<'a> Update<'a> { } } -/// What a valid file holds for one CPU. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Choice<'a> { - /// The newest update naming the CPU, newer than the revision it runs. - Load(Update<'a>), - /// The newest update naming the CPU is this revision, and the CPU already - /// runs it or a newer one. - Current(Revision), - /// No update in the file names the CPU. - NoMatch, -} - /// Why a file is refused: the offset of the update that is malformed, and how. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Refused { @@ -229,7 +315,7 @@ fn parse(bytes: &[u8]) -> Result<(Update<'_>, &[u8]), Refusal> { [] => &[], table => extended(table, signature.wrapping_add(checksum).wrapping_add(flags))?, }; - let update = Update { revision: Revision(revision as i32), signature, flags, data, extended }; + let update = Update { revision: Revision(revision), signature, flags, data, extended }; Ok((update, rest)) } diff --git a/kernel/microcode/src/tests.rs b/kernel/microcode/src/tests.rs index 506f1a5018b..2339ba4f922 100644 --- a/kernel/microcode/src/tests.rs +++ b/kernel/microcode/src/tests.rs @@ -1,5 +1,6 @@ -//! Intel's updates as Intel publishes them, and updates built here to the -//! SDM's layout for every refusal and every arm the real files do not reach. +//! Intel's updates as Intel publishes them, updates built here to the SDM's +//! layout for every refusal and every arm the real files do not reach, and +//! whether a CPU loads at all. #![allow( clippy::indexing_slicing, @@ -120,7 +121,6 @@ fn the_msrs_are_read_where_the_sdm_puts_them() { // `IA32_PLATFORM_ID` as all eight of the T14's CPUs read it. assert_eq!(PlatformId::from_msr(0x001c_0000_0000_0000), PlatformId(7)); assert_eq!(PlatformId::from_msr(!(7 << 50)), PlatformId(0)); - assert_eq!(Revision::from_sign_id(0xbe << 32 | 0xffff_ffff), Revision(0xbe)); } #[test] @@ -251,8 +251,31 @@ fn the_newest_update_naming_the_cpu_is_chosen_wherever_it_sits() { } #[test] -fn a_revision_is_signed() { - let negative = build(T14.signature.0, 0x80, 0x8000_0000, &[]); - let cpu = Cpu { revision: Revision(0x7fff_ffff), ..T14 }; - assert_eq!(select(&negative, &cpu), Ok(Choice::Current(Revision(i32::MIN)))); +fn a_vendor_toyos_has_no_loader_for_loads_nothing() { + assert_eq!(loader(Vendor::Intel), Ok(())); + for vendor in [Vendor::Amd, Vendor::Hygon, Vendor::Centaur, Vendor::Zhaoxin, Vendor::Unknown] { + assert_eq!(loader(vendor), Err(Unloaded::Vendor(vendor))); + } +} + +#[test] +fn a_guest_loads_nothing_whatever_revision_it_reports() { + for found in [Revision(0xbe), Revision::DISABLED] { + assert_eq!(may_load(1 << 31, found), Err(Unloaded::Hypervisor)); + } +} + +#[test] +fn revision_0xffffffff_loads_nothing() { + assert_eq!(may_load(0, Revision::DISABLED), Err(Unloaded::Disabled)); + assert_eq!(may_load(0, Revision(0xffff_fffe)), Ok(())); +} + +/// The T14's i5-1135G7 below the update its firmware already runs. +#[test] +fn the_embedded_intel_file_hands_the_t14_its_update_16_byte_aligned() { + let below = Cpu { revision: Revision(0xbd), ..T14 }; + let Ok(Choice::Load(update)) = select(&INTEL.0, &below) else { panic!("0xbe is newer than 0xbd") }; + assert_eq!(update.revision(), Revision(0xbe)); + assert_eq!(update.data().as_ptr().align_offset(16), 0, "SDM §12.11.6 takes the data 16-byte aligned"); } diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index da65626226b..2420be035be 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -140,6 +140,13 @@ actuators! { /// Leave every AP holding the CR0/CR4 that INIT left it. no_ap_control_regs = "no-ap-control-regs"; + /// Hand the BSP the T14's Intel update with its header's revision raised + /// by one and its checksum sealed again: the read-back's negative control + /// on real Intel, whose payload still says the revision it did. + // An Intel file's alone, so AArch64 builds an accessor it never reads. + #[allow(dead_code)] + microcode_reseal = "microcode-reseal"; + /// Skip the startup for the AP that would be cpu2, so a non-last AP never starts. smp_skip_ap = "smp-skip-ap"; diff --git a/kernel/src/arch/x86_64/boot.rs b/kernel/src/arch/x86_64/boot.rs index 6396ec8cee1..64c46d76fb2 100644 --- a/kernel/src/arch/x86_64/boot.rs +++ b/kernel/src/arch/x86_64/boot.rs @@ -4,7 +4,7 @@ use toyos_abi::boot::{KernelArgs, MemoryMapEntry}; -use super::{apic, control_regs, idt, ioapic, pat, percpu}; +use super::{apic, control_regs, idt, ioapic, microcode, pat, percpu}; use crate::drivers::acpi::{self, MadtInfo}; use crate::log; use crate::mm::Region; @@ -58,6 +58,10 @@ pub fn after_console(_args: &KernelArgs, _maps: &[MemoryMapEntry]) { // `CD` — which would make every mapping uncacheable whatever the PAT // says — ends here. control_regs::init_cr0(0); + // The first point with every channel a refusal reaches, and before + // anything reads what an update changes: `random::key`'s RDRAND and + // RDSEED, and `control_regs::init`'s CPUID. + microcode::load(0); // The read-back `pat::init` owes, on a boot that now has three channels to // carry a refusal. diff --git a/kernel/src/arch/x86_64/microcode.rs b/kernel/src/arch/x86_64/microcode.rs new file mode 100644 index 00000000000..a650b0413e9 --- /dev/null +++ b/kernel/src/arch/x86_64/microcode.rs @@ -0,0 +1,232 @@ +//! This CPU's microcode, loaded from the Intel file `toyos_microcode` embeds +//! before anything reads what an update changes: Intel asks it "on each core +//! as early as possible in the OS boot sequence, before any CPUID feature +//! flags or other enumeration values are cached". The BSP loads in +//! `boot::after_console`, before `random::key` draws on RDRAND and RDSEED and +//! before `control_regs::init` reads CPUID; each AP in `smp::ap_entry`, before +//! its own. `boot_aps` starts one AP at a time, the serialisation per core SDM +//! Vol. 3A §12.11.6.3 asks. +//! +//! **Both run before this kernel's IDT**, so a fault inside a trigger reports +//! nothing: the BSP's vectors into firmware's table, an AP's into the kernel's +//! with `CR4.OSFXSR` still clear, a triple fault. Intel documents no fault for +//! a valid update at a present, canonical address. +//! +//! **A CPU's verdict on an update is the revision it reads back, and a +//! refusal is reported, not a panic.** Intel judges a load by MSR 8BH read +//! after it, equal to the update's revision or a failure (SDM Vol. 3A +//! §12.11.7.2, Example 12-10), and the register reports "the currently +//! functioning revision" (§12.11.7.1). So a CPU that does not take an update +//! runs the revision it reads back: its line says `refused` with the +//! update it was handed, its `running` is that read-back, and it is held to +//! the machine's like any other. +//! +//! One line per CPU says what it found, what the file holds for it and what +//! it runs; a CPU that loads nothing says why, and a CPU of a vendor ToyOS +//! has no loader for says so by the vendor's name and reads nothing more. +//! Every CPU that reads a revision runs the one the first did, or the boot +//! stops naming it: whatever reads the revision assumes one. +//! +//! INIT keeps an update and a hard reset clears it (§12.11.6.1): a resume +//! path, when one exists, loads again. + +use core::arch::asm; +use core::fmt; +use core::sync::atomic::{AtomicU64, Ordering}; + +use toyos_microcode::{Choice, Cpu, PlatformId, Revision, Signature, Unloaded}; + +use super::cpu; +use crate::log; + +/// `IA32_PLATFORM_ID`. +const PLATFORM_ID: u32 = 0x17; +/// `IA32_BIOS_UPDT_TRIG`. +const TRIGGER: u32 = 0x79; +/// `IA32_BIOS_SIGN_ID`. +const SIGN_ID: u32 = 0x8b; + +/// The revision every CPU runs, as the first to read one published it. +static MACHINE: AtomicU64 = AtomicU64::new(UNPUBLISHED); +/// Outside a dword, so that [`Revision::DISABLED`] is a revision like another. +const UNPUBLISHED: u64 = u64::MAX; + +/// Loads this CPU's microcode, says so, and holds it to the machine's. +pub fn load(cpu_id: u32) { + let vendor = cpu::vendor(); + let (eax, _, ecx, _) = cpu::cpuid(1, 0); + if let Err(why) = toyos_microcode::loader(vendor) { + log!("microcode: cpu{cpu_id} {vendor:?} signature {eax:#x} loads nothing: {why}"); + return; + } + let found = revision(); + let (platform, outcome) = match toyos_microcode::may_load(ecx, found) { + Err(why) => (None, Outcome::Unloaded(why)), + Ok(()) => { + let platform = PlatformId::from_msr(cpu::rdmsr(PLATFORM_ID)); + let cpu = Cpu { signature: Signature(eax), platform, revision: found }; + (Some(platform), load_intel(cpu_id, &cpu)) + } + }; + let running = match outcome { + Outcome::Loaded(loaded) => loaded, + Outcome::Refused { running, .. } => running, + Outcome::Unloaded(_) | Outcome::NoMatch | Outcome::Current(_) => found, + }; + let platform = Platform(platform); + log!("microcode: cpu{cpu_id} {vendor:?} signature {eax:#x}{platform} found {found:#x} {outcome} running {running:#x}"); + agree(cpu_id, running); +} + +/// What one CPU did with the embedded file. +enum Outcome { + Unloaded(Unloaded), + NoMatch, + /// The newest update naming it, which it runs, or runs one above. + Current(Revision), + /// The update it loaded and read back. + Loaded(Revision), + /// The update it was handed and did not take, and what it reads back. + Refused { update: Revision, running: Revision }, +} + +impl fmt::Display for Outcome { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Unloaded(why) => write!(f, "loads nothing: {why}"), + Self::NoMatch => f.write_str("loads nothing: no embedded update names it"), + Self::Current(newest) => write!(f, "current {newest:#x}"), + Self::Loaded(loaded) => write!(f, "loaded {loaded:#x}"), + Self::Refused { update, .. } => write!(f, "refused {update:#x}"), + } + } +} + +/// A CPU's platform, where it was read. +struct Platform(Option); + +impl fmt::Display for Platform { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Some(platform) => write!(f, " platform {platform}"), + None => Ok(()), + } + } +} + +/// The revision this CPU runs (§12.11.7.1). +fn revision() -> Revision { + // SAFETY: writing `IA32_BIOS_SIGN_ID` 0 is §12.11.7.1's own first step, + // and loads nothing. + unsafe { cpu::wrmsr(SIGN_ID, 0) }; + cpu::cpuid(1, 0); + Revision::from_sign_id(cpu::rdmsr(SIGN_ID)) +} + +fn load_intel(cpu_id: u32, cpu: &Cpu) -> Outcome { + let choice = toyos_microcode::select(reseal::intel_file(cpu_id), cpu).unwrap_or_else(|refused| { + panic!("microcode: the embedded Intel file is refused at byte {}: {:?}", refused.at, refused.why) + }); + match choice { + Choice::NoMatch => Outcome::NoMatch, + Choice::Current(newest) => Outcome::Current(newest), + Choice::Load(update) => { + // SAFETY: a validated update's data, which `INTEL`'s alignment and + // every update's 1024-byte granule start 16-byte aligned, inside + // the kernel image and so canonical and present (§12.11.6). + unsafe { trigger(update.data().as_ptr()) }; + taken(update.revision()) + } + } +} + +/// `IA32_BIOS_UPDT_TRIG` written the address of the data the CPU loads. Not +/// `cpu::wrmsr`, which is `nomem`: the CPU reads memory through this write. +/// +/// # Safety +/// The CPU is Intel's, and `at` the start of a validated update's data, +/// placed as §12.11.6 asks. +unsafe fn trigger(at: *const u8) { + let at = at as u64; + // SAFETY: the caller's. + unsafe { + asm!("wrmsr", in("ecx") TRIGGER, in("eax") at as u32, in("edx") (at >> 32) as u32, options(nostack)); + } +} + +/// What the CPU made of the update of revision `update` it was handed: the +/// revision it reads back, which it runs. +fn taken(update: Revision) -> Outcome { + match revision() { + running if running == update => Outcome::Loaded(update), + running => Outcome::Refused { update, running }, + } +} + +/// Every CPU that reads a revision runs the first one's. +fn agree(cpu_id: u32, running: Revision) { + let mine = u64::from(running.0); + if let Err(machine) = MACHINE.compare_exchange(UNPUBLISHED, mine, Ordering::Relaxed, Ordering::Relaxed) { + assert!( + machine == mine, + "microcode: cpu{cpu_id} runs {running:#x} and the first CPU {machine:#x}: this machine's \ + CPUs do not run one revision", + ); + } +} + +/// `microcode-reseal`: the BSP is handed the T14's update with its header's +/// revision raised by one and its checksum sealed again, while Intel's payload +/// still says 0xbe, so its line must say `refused`. +#[cfg(feature = "boot-actuators")] +mod reseal { + use toyos_microcode::Aligned; + + const LEN: usize = 112_640; + + static RAISED: Aligned<[u8; LEN]> = + Aligned(raised(*include_bytes!("../../../microcode/intel-ucode/06-8c-01"))); + + /// Table 12-7's header: the revision at 4, the checksum at 16, the data + /// size at 28; the header and data sum to 0 (§12.11.5). + const fn raised(mut file: [u8; LEN]) -> [u8; LEN] { + file = put(file, 4, word(&file, 4) + 1); + file = put(file, 16, 0); + let end = 48 + word(&file, 28) as usize; + let (mut sum, mut at) = (0u32, 0); + while at < end { + sum = sum.wrapping_add(word(&file, at)); + at += 4; + } + put(file, 16, sum.wrapping_neg()) + } + + const fn word(file: &[u8; LEN], at: usize) -> u32 { + u32::from_le_bytes([file[at], file[at + 1], file[at + 2], file[at + 3]]) + } + + const fn put(mut file: [u8; LEN], at: usize, value: u32) -> [u8; LEN] { + let bytes = value.to_le_bytes(); + let mut i = 0; + while i < 4 { + file[at + i] = bytes[i]; + i += 1; + } + file + } + + pub fn intel_file(cpu_id: u32) -> &'static [u8] { + if crate::actuator::microcode_reseal() && cpu_id == 0 { + &RAISED.0 + } else { + &toyos_microcode::INTEL.0 + } + } +} + +#[cfg(not(feature = "boot-actuators"))] +mod reseal { + pub fn intel_file(_cpu_id: u32) -> &'static [u8] { + &toyos_microcode::INTEL.0 + } +} diff --git a/kernel/src/arch/x86_64/mod.rs b/kernel/src/arch/x86_64/mod.rs index f7e45b8dbb6..f733e9e9f98 100644 --- a/kernel/src/arch/x86_64/mod.rs +++ b/kernel/src/arch/x86_64/mod.rs @@ -28,6 +28,7 @@ pub mod i8042; pub mod idt; pub mod iommu_unit; pub mod ioapic; +pub mod microcode; pub mod mtrr; pub mod paging; pub mod pat; diff --git a/kernel/src/arch/x86_64/smp.rs b/kernel/src/arch/x86_64/smp.rs index d10dc7ce720..f13c92c08b3 100644 --- a/kernel/src/arch/x86_64/smp.rs +++ b/kernel/src/arch/x86_64/smp.rs @@ -280,6 +280,8 @@ extern "C" fn ap_entry() -> ! { // Must run before `pat::init`, which restores the CR0 this call sets. crate::arch::control_regs::init_cr0(percpu::cpu_id()); + // Before `control_regs::init` reads what an update changes. + crate::arch::microcode::load(percpu::cpu_id()); // Must run before this CPU touches the framebuffer, which needs write-combining mapped first. crate::arch::pat::init(); diff --git a/tests/checks.rs b/tests/checks.rs index 598bd35a766..e08aab8c67e 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -421,6 +421,59 @@ mod checks { Ok(()) } + /// [`microcode_on_metal`] against both benches' machines and the ways a + /// boot of either could fall short of what its bench is held to, with no + /// guest. + #[test] + fn microcode_verdict() -> Result<(), String> { + fn log(lines: &[(u32, &str)]) -> String { + lines.iter().map(|(cpu, said)| format!("[ 0.050 cpu{cpu} kernel] microcode: cpu{cpu} {said}\n")).collect() + } + const T14: &str = "Intel signature 0x806c1 platform 7 found 0xbe current 0xbe running 0xbe"; + const AMD: &str = "Amd signature 0x860f01 loads nothing: ToyOS has no Amd microcode loader"; + let t14 = |cpu: u32| (cpu, T14); + let amd = |cpu: u32| (cpu, AMD); + + let refused = |what: &str, lines: &[(u32, &str)], cpus: u32, says: &str| match microcode_on_metal(&log(lines), cpus) { + Ok(()) => Err(format!("{what} was accepted")), + Err(e) if e.contains(says) => Ok(()), + Err(e) => Err(format!("{what} was refused for the wrong reason: {e}")), + }; + + microcode_on_metal(&log(&(0..8).map(t14).collect::>()), 8) + .map_err(|e| format!("the T14 at Intel's 0xbe was refused: {e}"))?; + microcode_on_metal(&log(&(0..8).map(amd).collect::>()), 8) + .map_err(|e| format!("the AMD bench with no loader was refused: {e}"))?; + + refused("seven lines for eight CPUs", &(0..7).map(t14).collect::>(), 8, "one microcode line")?; + let mut twice: Vec<_> = (0..8).map(t14).collect(); + twice[7] = t14(6); + refused("cpu6 twice and cpu7 never", &twice, 8, "one microcode line")?; + let mut resealed: Vec<_> = (0..8).map(t14).collect(); + resealed[0] = (0, "Intel signature 0x806c1 platform 7 found 0xbe refused 0xbf running 0xbe"); + refused("a T14 BSP that refused its update", &resealed, 8, "cpu0 refused")?; + let mut below: Vec<_> = (0..8).map(t14).collect(); + below[3] = (3, "Intel signature 0x806c1 platform 7 found 0xbd current 0xbd running 0xbd"); + refused("a T14 CPU below Intel's update", &below, 8, "runs 0xbe")?; + let mut guest: Vec<_> = (0..8).map(t14).collect(); + guest[1] = (1, "Intel signature 0x806c1 found 0xbe loads nothing: a hypervisor owns this CPU's microcode running 0xbe"); + refused("a T14 CPU that loads nothing", &guest, 8, "runs 0xbe")?; + let mut loaded: Vec<_> = (0..8).map(amd).collect(); + loaded[5] = (5, "Amd signature 0x860f01 found 0x8600106 loaded 0x860010d running 0x860010d"); + refused("an AMD CPU that read and loaded", &loaded, 8, "has no Amd microcode loader")?; + let mut other: Vec<_> = (0..8).map(t14).collect(); + other[2] = (2, "Intel signature 0x806c2 platform 7 found 0xbe current 0xbe running 0xbe"); + refused("two signatures in one machine", &other, 8, "is not")?; + refused( + "a machine that is no bench", + &[(0, "Intel signature 0x906a3 platform 7 found 0x4 current 0x4 running 0x4"), (1, "Intel signature 0x906a3 platform 7 found 0x4 current 0x4 running 0x4")], + 2, + "no bench", + )?; + eprintln!(" [microcode] the verdict refuses 8 machines and accepts both benches"); + Ok(()) + } + /// [`mask_windows`] and `irqcensus::windows_under` against captures no /// boot has to produce: two CPUs and the T14 row's three exits, a report /// each, with cpu1 holding at the first. diff --git a/tests/metal/src/lib.rs b/tests/metal/src/lib.rs index 393d96fccd2..01ff007df19 100644 --- a/tests/metal/src/lib.rs +++ b/tests/metal/src/lib.rs @@ -770,6 +770,12 @@ pub const FLASHABLE: &[&str] = &[ // without interrupt remapping. It writes no register a reset does not // return to firmware. "iommu-no-remap", + // The BSP is handed the T14's Intel update with its header's revision + // raised by one and its checksum sealed again. Its 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 to userland and ends the way an unarmed one + // does. Nothing it writes outlives a hard reset (SDM Vol. 3A §12.11.6.1). + "microcode-reseal", // It seals this boot's own record under an identity one bit from this // stick's, so the pass that finds it clears it and boots a kernel. The page // is memory the loader allocated and the machine is what it was after. diff --git a/tests/toyos.rs b/tests/toyos.rs index a217035a99d..bb7953417c5 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -814,6 +814,12 @@ const METAL: &[(&str, metal::Metal)] = &[ "metal_sim_scanout_wc", metal::Metal { arms: METALCASE, judge: |b| scanout_wc(b[0].kernel().text()) }, ), + ( + // Every CPU's microcode line, held to what its bench is held to: + // `microcode_on_metal` says which machine is held to what. + "microcode", + metal::Metal { arms: METALCASE, judge: |b| microcode_on_metal(b[0].kernel().text(), b[0].cpus()?) }, + ), ( // Two boots the suite already flashes: the device boot writes and // fsyncs megabytes before its reset, and `metalcase` is the same reset @@ -4349,6 +4355,93 @@ fn ioapic_topology(log: &str) -> Result<(), String> { Ok(()) } +/// What each CPU's `microcode:` line says: one line from each CPU this +/// machine brought up, all of one signature, none refused, and each saying +/// what a bench of that signature is held to. +/// +/// - **The T14**, 0x806c1: every CPU runs 0xbe, the update Intel's +/// `06-8c-01` holds for it, whether it found it `current` or `loaded` it. +/// - **The AMD bench**, 0x860f01: ToyOS has no AMD loader, and every CPU says +/// so and reads nothing. +fn microcode_on_metal(log: &str, cpus: u32) -> Result<(), String> { + const T14: u32 = 0x0008_06c1; + const AMD_BENCH: u32 = 0x0086_0f01; + let lines = microcode_lines(log)?; + let ids: BTreeSet = lines.iter().map(|line| line.cpu).collect(); + let want: BTreeSet = (0..cpus).collect(); + if ids != want || lines.len() != want.len() { + return Err(format!("expected one microcode line from each of {want:?}, got {lines:?}")); + } + let signature = lines[0].signature; + if let Some(other) = lines.iter().find(|line| line.signature != signature) { + return Err(format!("cpu0 is signature {signature:#x} and {other:?} is not")); + } + let (holds, held_to): (fn(&MicrocodeSaid) -> bool, &str) = match signature { + T14 => ( + |said| matches!(said, MicrocodeSaid::Read { outcome, running: 0xbe } if ["current", "loaded"].contains(&outcome.as_str())), + "every CPU runs 0xbe, which Intel's 06-8c-01 holds for it", + ), + AMD_BENCH => ( + |said| matches!(said, MicrocodeSaid::Unread(why) if why == "ToyOS has no Amd microcode loader"), + "every CPU says ToyOS has no Amd microcode loader", + ), + _ => return Err(format!("no bench of signature {signature:#x} says what its CPUs are held to")), + }; + if let Some(line) = lines.iter().find(|line| !holds(&line.said)) { + return Err(format!("{line:?}: on signature {signature:#x} {held_to}")); + } + eprintln!(" [microcode] {} CPUs of signature {signature:#x}: {held_to}", lines.len()); + Ok(()) +} + +/// One `microcode:` line, the fields [`microcode_on_metal`] holds. +#[derive(Debug)] +struct MicrocodeLine { + cpu: u32, + signature: u32, + said: MicrocodeSaid, +} + +#[derive(Debug)] +enum MicrocodeSaid { + /// It read its revision: `loaded`, `current`, `refused`, or `loads` for + /// one that loads nothing, and the revision it runs. + Read { outcome: String, running: u32 }, + /// It read nothing, for this reason. + Unread(String), +} + +/// Every `microcode:` line in `log`, or the first CPU that refused the update +/// it was handed, on any machine. +fn microcode_lines(log: &str) -> Result, String> { + let mut out = Vec::new(); + for line in log.lines() { + let Some(rest) = line.split("microcode: cpu").nth(1) else { continue }; + let words: Vec<&str> = rest.split_whitespace().collect(); + let field = |name: &str| words.iter().position(|w| *w == name).and_then(|at| words.get(at + 1)); + let hex = |name: &str| -> Option { u32::from_str_radix(field(name)?.trim_start_matches("0x"), 16).ok() }; + let unreadable = || format!("unreadable microcode line: {line:?}"); + let said = match words.iter().position(|w| *w == "found") { + Some(found) => MicrocodeSaid::Read { + outcome: words.get(found + 2).ok_or_else(unreadable)?.to_string(), + running: hex("running").ok_or_else(unreadable)?, + }, + None => MicrocodeSaid::Unread(rest.split_once("loads nothing: ").ok_or_else(unreadable)?.1.to_string()), + }; + out.push(MicrocodeLine { + cpu: words.first().and_then(|id| id.parse().ok()).ok_or_else(unreadable)?, + signature: hex("signature").ok_or_else(unreadable)?, + said, + }); + if let Some(line @ MicrocodeLine { said: MicrocodeSaid::Read { outcome, running }, .. }) = out.last() { + if outcome == "refused" { + return Err(format!("cpu{} refused the update it was handed and runs {running:#x}: {line:?}", line.cpu)); + } + } + } + Ok(out) +} + /// Every CPU's `CR0`, `CR4` and `XCR0`, against what a CPU running this /// kernel must hold. ///