From 2355b4af849521ccad8f1cf9c5b2f1215f37862b Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 16:51:25 +0200 Subject: [PATCH 1/6] The kernel loads pinned Intel and AMD microcode on every CPU before anything 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 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- Cargo.lock | 5 + NOTICE | 53 +- build/build.rs | 13 + build/licence.rs | 34 +- issues/the-kernel-loads-no-cpu-microcode.md | 81 +-- kernel/Cargo.toml | 1 + kernel/microcode/Cargo.toml | 10 +- .../amd-ucode/microcode_amd_fam17h.bin | Bin 0 -> 22596 bytes kernel/microcode/src/amd.rs | 181 ++++++ kernel/microcode/src/amd/tests.rs | 208 +++++++ kernel/microcode/src/intel.rs | 226 +++++++ kernel/microcode/src/intel/tests.rs | 250 ++++++++ kernel/microcode/src/lib.rs | 314 ++++------ kernel/microcode/src/tests.rs | 267 +-------- kernel/src/actuator.rs | 7 + kernel/src/arch/x86_64/boot.rs | 6 +- kernel/src/arch/x86_64/microcode.rs | 260 ++++++++ kernel/src/arch/x86_64/mod.rs | 1 + kernel/src/arch/x86_64/smp.rs | 2 + licenses/AMD-ucode-license.txt | 64 ++ tests/checks.rs | 50 ++ tests/linux/amd_shas.c | 556 ++++++++++++++++++ tests/metal/amd-linux/SOURCE | 16 + tests/metal/amd-linux/microcode.txt | 8 + tests/metal/src/lib.rs | 6 + tests/toyos.rs | 113 ++++ 26 files changed, 2213 insertions(+), 519 deletions(-) create mode 100644 kernel/microcode/amd-ucode/microcode_amd_fam17h.bin create mode 100644 kernel/microcode/src/amd.rs create mode 100644 kernel/microcode/src/amd/tests.rs create mode 100644 kernel/microcode/src/intel.rs create mode 100644 kernel/microcode/src/intel/tests.rs create mode 100644 kernel/src/arch/x86_64/microcode.rs create mode 100644 licenses/AMD-ucode-license.txt create mode 100644 tests/linux/amd_shas.c create mode 100644 tests/metal/amd-linux/SOURCE create mode 100644 tests/metal/amd-linux/microcode.txt diff --git a/Cargo.lock b/Cargo.lock index 8e7c4dc1663..c5fc36211a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1900,6 +1900,7 @@ dependencies = [ "toyos-gpt", "toyos-hda", "toyos-its", + "toyos-microcode", "toyos-pci", "toyos-phys", "toyos-ps2", @@ -4452,6 +4453,10 @@ dependencies = [ [[package]] name = "toyos-microcode" version = "0.1.0" +dependencies = [ + "toyos-cpuvuln", + "toyos-sha2", +] [[package]] name = "toyos-mixer" diff --git a/NOTICE b/NOTICE index d4b445726cd..9df72678190 100644 --- a/NOTICE +++ b/NOTICE @@ -124,8 +124,7 @@ Inc." and "Copyright(C) 2005-2014 Simon Howard" over GPL "version 2 ... or any later version" — id's Doom source release by way of Chocolate Doom. The crate declares `GPL-2.0-only`, which is a narrowing a distributor may make. -Nothing else in this repository is GPL, and no GPL code is linked into the -kernel, the SDK, or any other program. +No GPL code is linked into the kernel, the SDK, or any other program. assets/JetBrainsMono-Regular.ttf — SIL Open Font License 1.1 @@ -428,6 +427,56 @@ 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. + + +kernel/microcode/amd-ucode/* — AMD microcode, redistributable unmodified +------------------------------------------------------------------------ + + microcode_amd_fam17h.bin 22,596 bytes, a container of seven patches for + family 0x17 CPUs, 0x0860010d for CPUID 0x00860f01 (Renoir, the + AMD bench's Ryzen 7 4800U) among them + sha256 966e4b796ec689c618868d08f8a37f347b0e7bfce4ae9df793e08471d363b7d0 + Upstream: https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git, + `amd-ucode/` at tag 20260916 + (commit ab23307cfe7f9366c819025ca3e4778299bc2db2), byte for byte + Licence text: licenses/AMD-ucode-license.txt (upstream's + `LICENSES/LICENSE.amd-ucode`, sha256 + 2103bd999f77522c5ab5fd35df0579ed00e2a6b885f6f8ebd444e97ffa482991) + SPDX-License-Identifier: LicenseRef-AMD-ucode + +The kernel embeds it whole and loads, on a CPU one of its patches names, that +patch. The licence grants redistribution free of fees on five terms: the +copyright notice and the licence travel with the software, which every x86-64 +image does as /system/share/licenses/AMD-ucode-license.txt; AMD is not +referenced in promoting a product in a way that implies AMD endorses or +certified it; no reverse engineering, decompilation or disassembly; the rights +to third-party technologies the software may implement are the +redistributor's to obtain, AMD conveying none; and U.S. and European export +law binds whoever uses it. + + +tests/linux/amd_shas.c — Linux's digests of AMD's microcode patches, GPL-2.0-only +--------------------------------------------------------------------------------- + + amd_shas.c sha256 98af9ffb8a6e39e9dc37c86028a88853a75b74f229bcb1dabf84b72f6700a156 + Upstream: https://github.com/torvalds/linux, + `arch/x86/kernel/cpu/microcode/amd_shas.c` at tag v7.2 + (commit 8d3ae59288f1e7d58d76558a6ee96d533bc5019f), byte for byte + SPDX-License-Identifier: GPL-2.0-only + +The SHA-256 of every AMD patch Linux loads on a CPU whose own signature check +EntrySign broke: the independent record `toyos-microcode`'s tests hold every +patch the kernel embeds to. The file carries no licence line of its own, so +the kernel's `COPYING` decides it. Test input only: nothing built from this +repository carries it, and no shipped package's directory holds it. + tests/cavp/ — NIST's SHA-2 test vectors, CAVP SHAVS ---------------------------------------------------- diff --git a/build/build.rs b/build/build.rs index 64acf05f309..110d2819612 100644 --- a/build/build.rs +++ b/build/build.rs @@ -785,6 +785,14 @@ pub const TRUST_ROOTS_LICENCE: (&str, &[u8]) = ( include_bytes!("../licenses/CDLA-Permissive-2.0-webpki-root-certs.txt"), ); +/// Where on ROOT the licence texts of the CPU microcode an x86-64 kernel +/// embeds are, and the texts: each maker's licence asks that a redistribution +/// carry it. +pub const MICROCODE_LICENCES: &[(&str, &[u8])] = &[ + ("share/licenses/Intel-microcode-license.txt", include_bytes!("../licenses/Intel-microcode-license.txt")), + ("share/licenses/AMD-ucode-license.txt", include_bytes!("../licenses/AMD-ucode-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 { @@ -819,6 +827,11 @@ 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())); + let microcode = match arch { + Arch::X86_64 => MICROCODE_LICENCES, + Arch::Aarch64 => &[], + }; + root_files.extend(microcode.iter().map(|(path, text)| (path.to_string(), text.to_vec()))); // Extra files (test binaries, shared libs) for (name, data) in extra_files { diff --git a/build/licence.rs b/build/licence.rs index cfafd044feb..6f93a2b4c9c 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", "LicenseRef-AMD-ucode"]; + +/// 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,19 @@ 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"), + ), + ( + "kernel/microcode/amd-ucode/microcode_amd_fam17h.bin", + "966e4b796ec689c618868d08f8a37f347b0e7bfce4ae9df793e08471d363b7d0", + "NOTICE", + Terms::Firmware("LicenseRef-AMD-ucode"), ), ]; @@ -1136,6 +1153,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 +1511,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/the-kernel-loads-no-cpu-microcode.md b/issues/the-kernel-loads-no-cpu-microcode.md index da29d49f314..6baea082462 100644 --- a/issues/the-kernel-loads-no-cpu-microcode.md +++ b/issues/the-kernel-loads-no-cpu-microcode.md @@ -1,5 +1,5 @@ --- -status: open +status: assigned kind: defect opened: 2026-09-29 --- @@ -7,71 +7,24 @@ 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. +version and hash the way vendor device firmware is (owner, 2026-09-30). +Pull request #886 builds the loader (`toyos-microcode`, +`kernel/src/arch/x86_64/microcode.rs`) and deletes this file once its exit +holds on both benches. **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 +- host: every decision `toyos-microcode` takes is refused by name and red + under its mutation; +- metal: the `microcode` row is green on the T14, whose eight CPUs find 0xbe + current on platform 7 as Linux runs them, and on the AMD bench, whose CPUs + each load 0x0860010d over the firmware's 0x08600106 as Linux does; the T14 + armed with `microcode-reseal`, and the AMD bench with its trigger write + removed, each stop at the read-back; +- guest: every guest loads nothing, for its CPU reports a hypervisor. QEMU + 11.1.1 sets CPUID.01H:ECX[31] under TCG as under KVM, read over QMP from a + paused `qemu64` with the suite's own `-cpu` string + (https://github.com/ToyOSOrg/evidence/blob/499ad40d53eca38cfe6eb7c3689088ee57d9402e/design-measurements.txt#L256-L269), + so the guest suite booting green is the whole guest exit, and no guest test asserts the line. diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 7b0d5e2787b..e8c5c18e23b 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..b899a4bc412 100644 --- a/kernel/microcode/Cargo.toml +++ b/kernel/microcode/Cargo.toml @@ -3,8 +3,16 @@ [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's microcode update files and AMD's containers validated and matched to a CPU as each maker reads them, whether a CPU loads at all, and the files the kernel embeds, pure." version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" publish = false + +[dependencies] +toyos-cpuvuln = { path = "../cpuvuln" } + +[dev-dependencies] +# Hashes the embedded container's patches for the test that holds them to +# Linux's digests. +toyos-sha2 = { path = "../../lib/sha2" } diff --git a/kernel/microcode/amd-ucode/microcode_amd_fam17h.bin b/kernel/microcode/amd-ucode/microcode_amd_fam17h.bin new file mode 100644 index 0000000000000000000000000000000000000000..4011bdbb51a9f495d5cef8739411362bb6c65cc5 GIT binary patch literal 22596 zcmeG^2{=@3+h@jLhLLSX$TId(L?TluTZ^<%NtQ&5k``;4N*O}dM2e&ur6fxzLYqRX zO8Xuy5>g_qmj8av%v7)5_s#$PUH|v~|JS)Lp7WgZtoL*8_kCvER7(t99Qc!x+=y=R zOA2mfBsu&V+{3MuQ-lZ^TL9)sJ^@7jlc>-x0~0q!_d z4AW$25RIjL$t2F#SNIKD6S}Os43;)K+@76M(z*Q*cly|e1HvCAuUpA_bzu78bf>h@ zTxvKW(dLPB^0K4J#Nx+0BHgN+PIT-J4%nM(yFhEVRh<{(E`~)6Jwou^+F}}bb-Y7l0R2o=@>WbyaCIrov%&q&j?|2YCxH^8d742@X z>G>YH8kR}esI^_``X46b>-$}Nu=rz)iP@5i<5ONvoDqFPa%#6j^O7`^i|h7;^qltd z)!pkpgYkGOy*6Ug#;7giOD`s;V+U{7KEwtupWvW)kvw##w2qu^-seOY>zY@2A2vFe z8!xF|F_7Vm9qbW57;R_w+%j*~-gT~nGVPpKyQmCK$($TF_GXpcoszdvo41(S2e_OG zP^8CLuZ}NYr*LAX<*a*i+GWi5%)KsUGtXd(`p%S;hC6E4St^mMTbovuUgvV!x(eM@ zi!L5Nb|%8bP($o=?OC_1nU5|@UQjCY2>e){$EkE#xqZp#wL=Eqq1v3YR$5<8}JQX&1GKir_)BFPi($;*5IaB}K{WYCW`{{%j6?uNG!J z{+ip6_M!(i$~#=Nit}lt2NMpx6d9&`Bwzf*4D-$jw|fG_ug>s2<>FPoLwmuNL3bwF zsk!V-xWA#O&_4Lw({be9#SglyQ%vtstfmfHgE?=sQ+QfSM|6Vdqp@JJn66(r^*z&G zY`mwg*vF+?sM?;Y)Qb0_5>tocq=NtBVA!f}H4Ll3u-@crL%lEE-4*@8xOXJZ?6!Z}=n(pYIS#!w=1v1A65G{;^( zQWDx=n2{XWorPfx5*BU1QpL!a9Mzs}j*$%=sgu~j17)y4jI7C_GTEn9dHpwFr@1I*);o7~wQcAEs@jPB>|ted=dbb*xR5-v-{nGkIYl zK_3_i#vx!9aKr0Qf;H3hd=9`qGtnOIMZh?)zh)U&b25WOGxK4R0R|k{YoHXYD;@S_ z2))xyiD8%&3`Gt%R+@s=vqV`5-(M@QAuhrNObWx|(q}71iE5Lk)QH4jvT(-f zYF2`|9}XlEVXZ=O>9z{+j1!kWQ9*>H9X_Q>BxV!p17#hZNPM}K5l%PrIWQ0->tV{; zU(Y}=4$MV*9;rhTUglnE3~MJ%u}=jUa?qJRgY&acB5-G6GT3Io7daRQ<}3hwVTmR4 z#w9=rusv{kAS?%Cj6-L6CWT>)<|N^G;KWu&`^{jMiVG7l8k_~-QmPR~Cef5M*osin zp~Nr?H5$NuHrf`3?W3hTv0yxgv63y6P=|L}+W-%sFAuXBfHU(L1kC~Aa_NKF2!F6C z_VTDR!i``85eFl>7d-1*u2*v=y#f5C<9JABU=rHwXBe3|3-B0l<^Tpb|4x;NA=*Lj z>ifJS@}J`qkwqA=W;V7#ghNxp@j4Um3GD&j*BLn?W)r2Oht`i^fMDjtuM5_N#*rnE zHEBRQ0vd!z!k7e;MI`dp*%%>%Rl>P=C<$>p2)t+SxAMjl;;-kCD&Rb8fW2a>0UPEg zapDcoI!IWY!B_YooTmcp9pYfG7?wu|c;68sV?3IKCjhs-aPIi^H!y@Xlm2OK*g#iw zpuwJA-kxivh?Zt1adBox{xi^L&12s!CQx5oK{sC(%fylGX$E!`GynXQNVkFzIeD&pahU6{ox&E zH~>E{Cn+Wai<9A=Ggrp`p4XZ4cpaMf7o4d)td*w3V0Q^YNdlawlAw-Y!5bf^3wZ`Z zPS1x4G-N1;-~@FMM5hRD=>}}nW){ZO#Hcb{fMF&rj>$sZzhB>LRU;CT3v!x12W+6E zF?5;6rCUnbylB{MV;pO=GmIVfqfQCsYZW+|_(&VCED4DiuJvJ9V#I+P_M zM8;)RFJD&S3JVsAfUYl{&W6%a2|5D509z*lQse#Q_*MK9$2%0&s6n! zCzun>wUCcLs4Y4JR7boLL&P7^bF{9((m0?0+x589iKOa2dxp7590@o# zX^>}16t0N03`PXGfaDG6FS&HUF;o}+ZtqT@Zy*>UJ4Az_AmbwgjE@5kzPpeugs!!f zACHfy7n<&`dFHs`b!ESEIm8bAseVfz<@o11CiqS)%$H2Q*JP+NaAAzz^!#i&SNUQ$ z@ydTd0~4mD-2VMdu;thd;PVI*Jd=0BgEBpKjF0R^_%eK zy&`>Dyw8fq540TjNl#*rcl1(wb9{8 zc}ETO*ZsCb{I2&8Jp4aaf!iS#{8GY$L`JkHDB%`#)x`>_8$stpG)e^f1LOeei+KJM zRZsvq8b=ThB6BowJyHrvq%SEUIhQWOMdKm;NaU4MDaI0E@N^hXD9F~Ya(n{`_=^G9=IyaD5%|UpnaE!bCC`$?7`LSrtIe5JiX>$Yii77_dghWd$m- z4J@G4gpxPjVHqNpn8M>Zo1x_KT?P}2XR;irphKqoDZ6pz1@>+ z1A9kv{Q-uF!ZIX|ip4%QBFHfmU$BDV2+GUm$|QJa!EeKF%kaom zl1KZ3KJto}k)HjNaeDeOP7fNVkA5y#hoF2*A(AXG@;Ht(ll=wG1mP62eds*e8HY*D zW6b_Q$Nmb=s^tG3oMA3Z9$*h82c19Y8XQ!YhUcgb=Z+oW>l_Te&VlOR>l}_$kj;GC z1ko3g|EP|);oB#njVDHb!GNd}bekKZbEC+lE32?>;27)%_ zVltRCgQv@N@!JaC3CdHVps!1jILO|kfZh7Njv#20fq4kSy*A(<3+Nu#w*l8kXkF%| zLM9l)XERk+1{(}`7{*|hG6X&nG{AQ{^-5tP3AG^tt%-x~i|8HfEh)rDP#?5*2IxS9 z3I=YMjg=yfqv1Xh3*lSTNvs;a-{3Xqe?ugJH^mvS``USv5I%sdm=KBZkX(b)r#+cr zOa&Nvkh$!*$?T25$K)CArRcoy>$^4ftpnfq3!De~;L}MjvN3)_7rlVb8K8rK{hZ3> zmy&$^u02J>=wKh8h>^iQh5&uTy%7H$XFb?iQ1=ie9YA>9j_4wpQNY(nGx_>xCSM;d z06da6B_lnOVJHW71LAFerjNdIS0H^6-~`{d1^CEZNuA7oqsn9=+v6BT27~j-1v{9X z1-t~=!23H~3uY2YfEUzJQ*XG$l<$UrbDxiI;SxZ$7eiIQKn#dU~fC zrG`-zxwWUjH$lSfbsX0ap9h-JMB@OT)kHC*|N|Yy0L-{;QT9_y2hk!p$823{#*+cj=2W@cNMKTxp1`wRT$+)kw5|?d& z54g-kvhtfe{Xq_XZG(@6+adBY!BU1l*dZtegRV6XgbqzsKe0vplIwvt%j1K|d#-hn z&C7k~yuOjEo~V#nC^F3;TXXu#`=t}z&)jz6au&E2=QgN&&y4yYWfosj?f0;L7`=Lx za?1Q^fjd;X^}5w&f2lp59kgS|z!U8KDRbN(Ms2hGbi3s0^XEA!@}tk*w;ylbd7?&X zQ0+94NM`c32uCBS&F(IJys_MX; zz0*XuE*Y>#IC4*PY%C*Mg44<7lsKgfh<8gX9`)$eor0yIQ=>*C>fE2b`uwoT?K7U9 zC_l5YD1FNY?>%)!pA?^rS*%0qakeDxqnm_H)9NvJ=BL}cp8MMl`OmgP5S<|UK=F|7 z_EXf4{s$?FemAJ9fwt7CMPtM)bXQA!%$YCm9A^DPI8;$576#zk;_)ah+b!N6_XOuGjMHGVn`>?EskH-t<8cdSGK6R_%x|9@?$U!;=zy)9mb{kDC7i=DRPa*$)`Ncu! zKt9*MjTik2Km8kk56Ev0&*%Q1D6Zn+A#UFb3_6dSv zf8uh#B3^X0pYb9jXOMp#_$iYPat76r?TpvKUU~rV0sfBTO%)#}p7TqPDLBr2Lb8C& zFr*zfG9-gcfwDjfN;;G{+JHUK3Fj`)Yg54Bw_)Wd_RJB;TeK|Vxm7|3cC^f8A1@&Z_%1G&5z;wg9>gM{qRZ$420d|Wrlkoptr zz~|t}cZjEfe8(69{+LULdI!J5_Z{UB(S5%Xd4_lBAqd?fE8ZPuQig=c&a`uRaqEA7ou@4I91b(&OlJE2iJ{ z;l?XgPRJQb$Y{}MFAwdYbdnfmiw4w8S@WTKQ|8QPBjath%6YqIaF2%=CG9;+v`k;% zxVtc+EhPKqhZVk8S)V&DXDlKg zcUg*5y|mEnPz-t@X60(T!u9gN#)5R^OIz+7%X}(~P2QTBCR?vR^0yuG+YUkW@uzuN z`zBD=6;P-ZBPLU^3SNE|r=Ru*PF(P>KXyoemn(?>zi`giALb@2UL|DW@+1oqyY|DnD2Grbn&SM*1(t)MV$kiPzs zz{>>^=y7Ah${0 z@UL@|1Z^;W8^mi+yx`M!x+czt{>B&U7;p5`{4RZIf6}k8hfgCNu^)MBz%MNDxGvw9 z|7Urs{n73Eqf7k?J-el!dUk*O4v1%>_}RBOGo0Xe=T{H8WdF{*tj2%AF8aH?tj6zf z_5WgCUw`ZnunkBk7Zy$u&vWg2w&xIC{KN9HerE5zfHeaT2fl}F9*=!MuV9D#5m!-; zrN9nRV91dq!T=B))act4#^K$&I*A6Uq%_vk=|g>L8txQtPAb#5o&UHsgm_a?g<@nQ|LUENNrZ;t{GBbAdhv(N6618QS_a@rsT*f|58*8+Nel%bFKoC~gx#!)j1$7*iTh2zR$|KyK zal3QHim#2*yMA=%BlPF(5J4Nd9~ zmW^TC`}m8cdF3Vf%uQUAFrB-Z8GiV2L`$#oMC#zBYD-5(Q%)upP)O@a56{0|`PxLw zbj7mT`!*s@UG43fm+d;vd_G~cyL=t-w;l4IZHFK#K=cu_u!`Ef9P+Y;ZlRQ}+CzEl zi}JFlVr`9)l%2y+URJQ!3CPRhQO$qr|KH|iQDys?m-Um}2^kcB;6Usf&n04@+%491 z$df?dd|cz_Cdon@$eCcGoQc;Gyj%%E?iA#a;=aokF(UR_!b=Oq#e2GiIZEK~2Y&Nh z3Hkfc1u_xwmVa7a7V_6}!CyO_zgG{0c=Xr2ELaDLmnR0kvv^GucM34=?u1Tbp41zGZ8j!cT3vSE|s4qU&0wPQT~}= zycTx_jEv&d$VW|Puf)cg) zCcl1=J74pl5Z^%GJmPULh;ja_d0CeHd@lI@G#uGbLnur&Oi7U|SP#nYk`f_vlOa!9 zklzJ;e#q}aXU~g=kc7lA?hked`qmv?_9hyG4}LmdHh*I7S`INVS;5g}dcleC*-ZO| z#rNt4bOcmJt}L7DFgw%RJFa@#Fi6)jov}FgzFQtq;Zpfh+40Qf9VOuxp1lf`X~^=l z(|zK5BjeMNlb2(+O2wDWdA~$gPWPtqz7H+A@5To&@Sa)eGD+X|Qvg@~^991w)yj>p zwib%KkqxkjKf7zPc*)Kgx$pfPu0|f0&5JUfW>NM0;>D24(Vk9pKe-DM^=TxYnAY>H^gmNlDD&BO01dQ$){s(-s;tOKk&C5@}K^lIuHt?kJS99IW`tmQz|2?NNPrfVZp?+?)ub0_Cxww(Fisw7tUFdw$q z6{oAxttHJXl4e+SE%#Qp$*vc^Vzo&nCHaH>!->v4iXTUhrAHl3c=4LWQG1g$ve`g6 z{z}B%l)^1{Z+^bx=usoK)1bt`I72^5W^iop?&mJIX@}D~tv`4^jLc{+t0x&c5k#gp zI9hGqStLEKzJA{65A&O1nWgsTq>f8Mt_^{@&7muf#Sgl#bk5@!bZoQ}k=E*K4 zR|nIH!-_hcu5*P%XoWjAhOa91&q@@_3h8wF7%!WfrP=#=<1*D9X786@S334Y`p)da zroxmN*(Hu9mriTHTmHJfb(|FS$>+3SQ%cBZ+wq63d|y8`epnPYQF-*nBl}iro^F+V z+_ok#=(w@aD;3G5H;L!Jym}IHBEzMq`|g^K;NkX*Y#VbLms$&5o2Q(2p_!d>RC3MH zlu!qs1uV{#D({e$bKa_qZ+tfI-0T%Yn&Q^i#JVh@hV`BrADA`DMc%u%uG@UOq|YkP zn!^`odnbwMPPUr=Xq1Q1P}jxhrd2xYPF1u`AS5gjiCHIpNNoH~wW93=+h)%}*JEWG zla3ISyEimXE*pPogUF=OIZ+dAH5Xa03e{9Sc*WJL<;~)~BM0_uz8KQB0T0v^I2=F!l(%m(X;cLL#^80Yrc5l?crUDy!0{@&>b;iF@3UAVVDzoB+oYoz;b zf7jTl1*Llys=Q9SQQjdcl(v!^*eISOoPD|Dk?iA2U0QIk_OvGk8pY3@wdG5uUM(%U zP`aagzbA+6G-u>TPkr%_fcFxuF*8Mayi0mCK79C4!gA@9Ryjh?)Lm3~lh8w4Jk+#$ z;Ouvc0@(+H%n!dLj|99iW zisRKXP8gB5igR+`jBhMdoEg9A-jhN*uEC+MA=>fzZS?r;p|ocsnHyd)#+aRq79W*p zrZe8W+U988nVVSj39sV2HPx+0L{~2QOyaye=n&CLuWB36axt>PE0&U-_Tu=QqPyOE zOCGxTiJdjhlUnAK7{AVMqd~71Gkwh%2KCF^qm#bSFRD)}%Z?YF9}$_?99TE>!1?U+ zdxsNb7Mc5toA3b!k+2|Fe>$7=R*L)~45nz=EIZvT=fR?2`a6GxNU2PP~HcXD^? zW}T^I3LRC~xi@1$k0pEUr~2OMZ6|>e^|A7(%k_gfOG7+IH84uv3? zQ19Tm1(Cu%cM`NKPqELL$FH>3ao(ay4clZh`AT{h>4Mj=Yi{X&%$)Tq%PLLXc5Q3h ze0SXz{rRuhvYE$nRaF}2J_}l`x@p>_O9!_GbQK3hS-$SP7~dgQIPJtCGp?dS)4B7v z_T8Q>m|?p@e$yS-BNO|#6V9DeRC=f)`v)v~L8dD40_v0GUpLV|ny@X6EL zQ|ul_+g`tMOx-AUmFMTef}qczV`95cd))Y}c4q9@5KUu?w6du4xshZEp@K1HOZ{)e z=B72gpdT^1T056mNJ;Dt6xs8^nsO#->iXJl*L{YpqU)z37fe20QkL6#c=Dlw*7nG8 z%_AFgth-K)&3ZW_P%|j?2|H|BR_yFEv)V zeqY&(d#~rrx|Sp1Y=j+maeO$k!zZZ5Hr2q7seiI<;N;f(X;Qm)h~^COyyNTaqGG%z zse@3y$HUCbb}3HP84oEnHMzGa`JOeyQ*VW-d-;0%DXLTLR;5vP#HF8NLhi+iC{+$_sq-Q zvJUh2zPPutt$|yaEj3){`O~Q*2!3G-a$#?#l&4EngbXe&70Vtym}R4L<&yYt$01Wa zg4eVM44iU&UoCCo+>uQc9Zs{##??l)i%%oSiC`f$bz8FNGavK@kmQqOTTYb;HS9gI zebX?zBdwV3k>_^2zt?u;fck(zj!S4kXQ5vMObDsJYWEKV3!zXY3U_8|&1eEz*mYrr263 zneD%v&(5D8N%*Y4t=Rj1LHYfoiiw<;lj^P=bBk){cuZq+Rb&-H(n*pvzalm?PEai&9R?HssZZ+8$*znMGpNjFH7(nTT3s# zZ$FeQv@K%}B9=6fO&FeQzR;CoD_~#rTd?bZG3b;D{DfygI9+Bqwa!i zg(wdNlZCwrt(2Q4uJgO&Mw-?t4;Sq;%x+d{I-{sNqr2TRx$4}6z%%WcX}ZOms)ogI zui9p(EVtUO(YDhv{p^t3i|;<=NxI(AHh`Gf~sgQs;$9(dI5 { + revision: Revision, + bytes: &'a [u8], +} + +impl<'a> Patch<'a> { + /// Its patch ID, which is the patch level a CPU reports once it runs it. + pub fn revision(&self) -> Revision { + self.revision + } + + /// The patch as its section holds it, header first. MSR C001_0020 is + /// written the linear address of its first byte (Linux `amd.c:700-705`). + pub fn bytes(&self) -> &'a [u8] { + self.bytes + } + + /// Whether the CPU that loads it must then drop the translations of its + /// first and last byte: family 0x17's loader leaves the mapping of a + /// patch's bytes in the TLB as uncacheable (Linux `c809b0d0e52d` and + /// `amd.c:707-718`, Xen at AMD's instruction). + pub fn leaves_its_mapping_uncached(&self) -> bool { + family(self.revision.0) == 0x17 + } +} + +/// Why a container is refused: the offset of the section that is malformed, +/// and how. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Refused { + pub at: usize, + pub why: Refusal, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Refusal { + /// The bytes end before the container's header, the table, a section's + /// header or its patch does. + Truncated { need: usize, have: usize }, + /// The container does not open with [`MAGIC`]. + Magic(u32), + /// The first section is not the equivalence table. + TableType(u32), + /// A section after the table is not a patch. + SectionType(u32), + /// The section is shorter than a patch's header. + Short(u32), + /// The patch is for one chipset (`nb_dev_id`, `sb_dev_id`). + Chipset { north: u32, south: u32 }, + /// The patch names the lowest revision it may be loaded over. + MinimumRevision(u32), +} + +/// The patch in `container` a CPU with `signature` running `found` loads, or +/// why there is none. +pub fn select<'a>( + container: &'a [u8], + signature: Signature, + found: Revision, +) -> Result>, Refused> { + let have = container.len(); + let at_header = |why| Refused { at: 0, why }; + let (header, rest) = container + .split_first_chunk::
() + .ok_or(at_header(Refusal::Truncated { need: HEADER, have }))?; + let [magic, table, len] = dwords(header); + if magic != MAGIC { + return Err(at_header(Refusal::Magic(magic))); + } + if table != EQUIVALENCE_TABLE { + return Err(at_header(Refusal::TableType(table))); + } + let need = HEADER.saturating_add(len as usize); + let (_, mut rest) = + rest.split_at_checked(len as usize).ok_or(at_header(Refusal::Truncated { need, have }))?; + + let mut newest: Option> = None; + while !rest.is_empty() { + // `rest` is a suffix of `container`. + let at = have.wrapping_sub(rest.len()); + let (patch, after) = section(rest).map_err(|why| Refused { at, why })?; + if named(patch.revision.0) == signature.0 && newest.is_none_or(|n| patch.revision > n.revision) { + newest = Some(patch); + } + rest = after; + } + Ok(match newest { + None => Choice::NoMatch, + Some(patch) if patch.revision >= found => Choice::Load(patch), + Some(patch) => Choice::Current(patch.revision), + }) +} + +/// The patch the section `bytes` starts with, and the bytes after it. +fn section(bytes: &[u8]) -> Result<(Patch<'_>, &[u8]), Refusal> { + let have = bytes.len(); + let (header, rest) = + bytes.split_first_chunk::
().ok_or(Refusal::Truncated { need: SECTION, have })?; + let [kind, size] = dwords(header); + if kind != PATCH { + return Err(Refusal::SectionType(kind)); + } + let need = SECTION.saturating_add(size as usize); + let (bytes, rest) = rest.split_at_checked(size as usize).ok_or(Refusal::Truncated { need, have })?; + let (patch, _) = bytes.split_first_chunk::().ok_or(Refusal::Short(size))?; + let [_, id, _, minimum, north, south, ..]: [u32; 16] = dwords(patch); + if north != 0 || south != 0 { + return Err(Refusal::Chipset { north, south }); + } + if minimum != 0 { + return Err(Refusal::MinimumRevision(minimum)); + } + Ok((Patch { revision: Revision(id), bytes }, rest)) +} + +/// The CPUID.01H:EAX a Zen patch ID names (Linux's `ucode_rev_to_cpuid`): its +/// stepping, model, extended model and extended family, over base family 0xf. +const fn named(id: u32) -> u32 { + let (stepping, model) = (id >> 8 & 0xf, id >> 12 & 0xf); + let (ext_model, ext_family) = (id >> 20 & 0xf, id >> 24); + stepping | model << 4 | 0xf << 8 | ext_model << 16 | ext_family << 20 +} + +/// The family a Zen patch ID names: base family 0xf plus its extended family. +const fn family(id: u32) -> u32 { + (id >> 24).wrapping_add(0xf) +} diff --git a/kernel/microcode/src/amd/tests.rs b/kernel/microcode/src/amd/tests.rs new file mode 100644 index 00000000000..f16b4a4d3a8 --- /dev/null +++ b/kernel/microcode/src/amd/tests.rs @@ -0,0 +1,208 @@ +//! linux-firmware's container as AMD publishes it, and containers built here +//! to its layout for every refusal and every arm the real one does not reach. + +#![allow( + clippy::indexing_slicing, + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + reason = "a test fails by panicking" +)] + +extern crate std; + +use std::vec; +use std::vec::Vec; + +use super::*; + +/// The AMD bench's Ryzen 7 4800U: family 0x17, model 0x60, stepping 1. +const RENOIR: Signature = Signature(0x0086_0f01); + +/// Its firmware's revision, and the one linux-firmware's container holds for +/// it, which Linux loads early on the same machine +/// (`tests/metal/amd-linux/microcode.txt`). +const FIRMWARE: Revision = Revision(0x0860_0106); +const CONTAINER: Revision = Revision(0x0860_010d); + +/// Where the Renoir patch's section starts in the container, as AMD's +/// `amd_ucode_info.py` names it (its patch 8 bytes later, 3200 bytes long). +const RENOIR_SECTION: usize = 9764; + +fn renoir_patch() -> Patch<'static> { + let Ok(Choice::Load(patch)) = select(&crate::AMD.0, RENOIR, FIRMWARE) else { + panic!("the container holds 0x0860010d for the 4800U") + }; + patch +} + +fn put(bytes: &mut [u8], at: usize, value: u32) { + bytes[at..at + 4].copy_from_slice(&value.to_le_bytes()); +} + +/// A 3200-byte patch: a header naming `id` and nothing else, and a body. +fn patch(id: u32) -> Vec { + let mut patch = vec![0; 3200]; + put(&mut patch, 4, id); + for (i, byte) in patch[PATCH_HEADER..].iter_mut().enumerate() { + *byte = i as u8; + } + patch +} + +/// A container: header, a 16-byte equivalence table, then each `(type, +/// bytes)` as a section. +fn container(sections: &[(u32, &[u8])]) -> Vec { + let mut out = Vec::new(); + for word in [MAGIC, EQUIVALENCE_TABLE, 16] { + out.extend(word.to_le_bytes()); + } + out.extend([0; 16]); + for (kind, bytes) in sections { + out.extend(kind.to_le_bytes()); + out.extend((bytes.len() as u32).to_le_bytes()); + out.extend(*bytes); + } + out +} + +fn refusal(container: &[u8]) -> Refused { + select(container, RENOIR, FIRMWARE).expect_err("a malformed container selects nothing") +} + +#[test] +fn the_4800u_at_its_firmwares_revision_loads_the_containers_patch_in_place() { + let patch = renoir_patch(); + assert_eq!(patch.revision(), CONTAINER); + let at = RENOIR_SECTION + SECTION; + assert!(patch.bytes() == &crate::AMD.0[at..at + 3200], "the bytes are not the section's patch"); +} + +#[test] +fn an_equal_revision_loads_again_and_a_newer_one_is_current() { + assert!(matches!(select(&crate::AMD.0, RENOIR, CONTAINER), Ok(Choice::Load(p)) if p.revision() == CONTAINER)); + assert_eq!(select(&crate::AMD.0, RENOIR, Revision(0x0860_010e)), Ok(Choice::Current(CONTAINER))); +} + +#[test] +fn another_stepping_is_named_by_no_patch() { + assert_eq!(select(&crate::AMD.0, Signature(0x0086_0f00), FIRMWARE), Ok(Choice::NoMatch)); +} + +#[test] +fn a_family_0x17_patch_drops_its_mapping_and_a_later_familys_does_not() { + assert!(renoir_patch().leaves_its_mapping_uncached()); + let milan = patch(0x0a00_1173); + let built = container(&[(PATCH, &milan)]); + let Ok(Choice::Load(patch)) = select(&built, Signature(0x00a0_0f11), Revision(0x0a00_1100)) else { + panic!("0x0a001173 names CPUID 0x00a00f11") + }; + assert!(!patch.leaves_its_mapping_uncached()); +} + +#[test] +fn the_newest_patch_naming_the_cpu_is_chosen_wherever_it_sits() { + let (older, newer) = (patch(0x0860_0106), patch(0x0860_0110)); + for order in [[&older[..], &newer[..]], [&newer[..], &older[..]]] { + let built = container(&[(PATCH, order[0]), (PATCH, order[1])]); + let Ok(Choice::Load(patch)) = select(&built, RENOIR, FIRMWARE) else { panic!("0x08600110 names it") }; + assert_eq!(patch.revision(), Revision(0x0860_0110)); + } +} + +#[test] +fn a_container_that_is_not_one_is_refused() { + let good = container(&[(PATCH, &patch(0x0860_010d))]); + let mut magic = good.clone(); + put(&mut magic, 0, 0x0041_4d45); + assert_eq!(refusal(&magic), Refused { at: 0, why: Refusal::Magic(0x0041_4d45) }); + let mut table = good.clone(); + put(&mut table, 4, PATCH); + assert_eq!(refusal(&table), Refused { at: 0, why: Refusal::TableType(PATCH) }); + assert_eq!(refusal(&good[..HEADER - 1]).why, Refusal::Truncated { need: HEADER, have: HEADER - 1 }); + assert_eq!(refusal(&good[..HEADER + 15]).why, Refusal::Truncated { need: HEADER + 16, have: HEADER + 15 }); +} + +#[test] +fn a_section_that_is_not_a_patch_is_refused_a_second_containers_magic_among_them() { + let first = HEADER + 16; + for kind in [EQUIVALENCE_TABLE, 2, MAGIC] { + let built = container(&[(PATCH, &patch(0x0860_010d)), (kind, &patch(0x0860_010d))]); + assert_eq!(refusal(&built), Refused { at: first + SECTION + 3200, why: Refusal::SectionType(kind) }); + } +} + +#[test] +fn a_patch_shorter_than_its_header_or_than_its_section_says_is_refused() { + let short = container(&[(PATCH, &patch(0x0860_010d)[..PATCH_HEADER - 1])]); + assert_eq!(refusal(&short).why, Refusal::Short(PATCH_HEADER as u32 - 1)); + let whole = container(&[(PATCH, &patch(0x0860_010d))]); + let cut = &whole[..whole.len() - 1]; + assert_eq!(refusal(cut).why, Refusal::Truncated { need: SECTION + 3200, have: SECTION + 3199 }); + assert_eq!(refusal(&whole[..whole.len() - 3200 - 1]).why, Refusal::Truncated { need: SECTION, have: SECTION - 1 }); +} + +#[test] +fn a_chipset_specific_patch_is_refused() { + for (at, north, south) in [(16, 1, 0), (20, 0, 1)] { + let mut chipset = patch(0x0860_010d); + put(&mut chipset, at, 1); + let built = container(&[(PATCH, &chipset)]); + assert_eq!(refusal(&built).why, Refusal::Chipset { north, south }); + } +} + +#[test] +fn a_patch_naming_a_minimum_revision_is_refused() { + let mut minimum = patch(0x0860_010d); + put(&mut minimum, 12, 0x0860_0100); + assert_eq!(refusal(&container(&[(PATCH, &minimum)])).why, Refusal::MinimumRevision(0x0860_0100)); +} + +#[test] +fn a_malformed_section_after_the_cpus_own_refuses_the_container() { + let mut chipset = patch(0x0830_107c); + put(&mut chipset, 16, 1); + let built = container(&[(PATCH, &patch(0x0860_010d)), (PATCH, &chipset)]); + let at = HEADER + 16 + SECTION + 3200; + assert_eq!(refusal(&built), Refused { at, why: Refusal::Chipset { north: 1, south: 0 } }); +} + +/// Linux v7.2's `amd_shas.c` (`tests/linux/`, outside every shipped package's +/// directory; `NOTICE`): each patch ID's SHA-256, by its rows. +fn linux_digests() -> std::collections::BTreeMap { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/linux/amd_shas.c"); + let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())); + let hex = |word: &str| u32::from_str_radix(word.trim_start_matches("0x"), 16).expect("a hex word"); + text.split(" { 0x") + .skip(1) + .map(|row| { + let mut words = row.split(|c: char| !c.is_ascii_alphanumeric()).filter(|w| !w.is_empty()); + let id = hex(words.next().expect("the row's patch ID")); + let digest: Vec = words.take(32).map(|w| hex(w) as u8).collect(); + (id, digest.try_into().unwrap_or_else(|d| panic!("patch {id:#x}'s digest is {d:?}"))) + }) + .collect() +} + +/// Every patch the embedded container holds, and so every patch `select` can +/// return out of it, is the patch Linux records: the build-time form of the +/// hash Linux checks at run time below EntrySign's cutoff. +#[test] +fn every_patch_in_the_embedded_container_has_linuxs_digest() { + let linux = linux_digests(); + let container = &crate::AMD.0; + let table = u32::from_le_bytes(container[8..HEADER].try_into().unwrap()) as usize; + let mut rest = &container[HEADER + table..]; + let mut patches = 0; + while !rest.is_empty() { + let (patch, after) = section(rest).expect("the embedded container is valid"); + let id = patch.revision().0; + let want = linux.get(&id).unwrap_or_else(|| panic!("amd_shas.c records no patch {id:#x}")); + assert_eq!(&toyos_sha2::Sha256::digest(patch.bytes()), want, "patch {id:#x}"); + patches += 1; + rest = after; + } + assert_eq!(patches, 7, "the container's README names seven patches"); +} diff --git a/kernel/microcode/src/intel.rs b/kernel/microcode/src/intel.rs new file mode 100644 index 00000000000..3c3c0fd9880 --- /dev/null +++ b/kernel/microcode/src/intel.rs @@ -0,0 +1,226 @@ +//! Intel's microcode update files, read as the SDM defines them. +//! +//! A file Intel publishes for one processor signature is updates laid end to +//! 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 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. + +use crate::{dwords, Choice, Revision, Signature}; + +#[cfg(test)] +mod tests; + +/// 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 +/// (Table 12-9). +const EXT_HEADER: usize = 20; +/// One extended signature: signature, processor flags, checksum (Table 12-10). +const EXT_SIGNATURE: usize = 12; +/// Total Size is "always a multiple of 1024" (Table 12-7). +const GRANULE: usize = 1024; + +/// Which bit of an update's processor flags names this CPU's platform +/// (§12.11.4). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PlatformId(u8); + +impl PlatformId { + /// `IA32_PLATFORM_ID` (MSR 17H): bits 52:50 (Table 12-11). + pub const fn from_msr(msr: u64) -> Self { + Self((msr >> 50) as u8 & 7) + } + + const fn flag(self) -> u32 { + // `self.0` < 8: the field is private and `from_msr` masks it. + 1 << self.0 + } +} + +impl core::fmt::Display for PlatformId { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Display::fmt(&self.0, f) + } +} + +/// What one CPU reports, read on that CPU. +#[derive(Clone, Copy, Debug)] +pub struct Cpu { + pub signature: Signature, + pub platform: PlatformId, + pub revision: Revision, +} + +/// One update out of a file, validated. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Update<'a> { + revision: Revision, + signature: u32, + flags: u32, + data: &'a [u8], + extended: &'a [[u8; EXT_SIGNATURE]], +} + +impl<'a> Update<'a> { + pub fn revision(&self) -> Revision { + self.revision + } + + /// The update data. `IA32_BIOS_UPDT_TRIG` is written its linear address, + /// which must be 16-byte aligned and mapped present (§12.11.6). + pub fn data(&self) -> &'a [u8] { + self.data + } + + /// The header's signature and flags, then each extended signature's + /// (Examples 12-5 and 12-6). + fn names(&self, cpu: &Cpu) -> bool { + let extended = self.extended.iter().map(|entry| { + let [signature, flags, _] = dwords(entry); + (signature, flags) + }); + core::iter::once((self.signature, self.flags)) + .chain(extended) + .any(|(signature, flags)| signature == cpu.signature.0 && flags & cpu.platform.flag() != 0) + } +} + +/// Why a file is refused: the offset of the update that is malformed, and how. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Refused { + pub at: usize, + pub why: Refusal, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Refusal { + /// The bytes end before the header does, or before Total Size says the + /// update does. + Truncated { need: usize, have: usize }, + /// Header Version is not 1, the only version §12.11 defines. + HeaderVersion(u32), + /// Loader Revision is not 1, the only loader §12.11.6 describes. + LoaderRevision(u32), + /// Data Size is 0, which Table 12-7 reads as 2000 bytes: the form of + /// updates for CPUs older than any ToyOS runs on. + ZeroDataSize, + /// Data Size is not a multiple of a dword. + DataSize(usize), + /// Total Size is not a multiple of 1024. + TotalSize(usize), + /// Total Size is smaller than the header and data it holds. + TotalBelowData { total: usize, data: usize }, + /// The header's and data's dwords sum to this, not to 0 (§12.11.5). + Checksum(u32), + /// The extended signature table is not 20 bytes and 12 for each signature + /// its count names. + ExtendedTableSize { len: usize }, + /// The extended signature table's dwords sum to this, not to 0 (§12.11.2). + ExtendedTableChecksum(u32), + /// This extended signature's checksum does not stand in for the header's + /// (Table 12-10). + ExtendedSignatureChecksum { index: usize }, +} + +/// The update in `file` to load on `cpu`, or why there is none. A file holds +/// at least one update. +pub fn select<'a>(file: &'a [u8], cpu: &Cpu) -> Result>, Refused> { + let mut newest: Option> = None; + let mut rest = file; + loop { + // `rest` is a suffix of `file`. + let at = file.len().wrapping_sub(rest.len()); + let (update, after) = parse(rest).map_err(|why| Refused { at, why })?; + if update.names(cpu) && newest.is_none_or(|n| update.revision > n.revision) { + newest = Some(update); + } + if after.is_empty() { + break; + } + rest = after; + } + Ok(match newest { + None => Choice::NoMatch, + Some(update) if update.revision > cpu.revision => Choice::Load(update), + Some(update) => Choice::Current(update.revision), + }) +} + +/// The update `bytes` starts with, and the bytes after it. +fn parse(bytes: &[u8]) -> Result<(Update<'_>, &[u8]), Refusal> { + let have = bytes.len(); + let (header, rest) = + bytes.split_first_chunk::
().ok_or(Refusal::Truncated { need: HEADER, have })?; + let [version, revision, _, signature, checksum, loader, flags, data, total, ..]: [u32; 12] = + dwords(header); + if version != 1 { + return Err(Refusal::HeaderVersion(version)); + } + if loader != 1 { + return Err(Refusal::LoaderRevision(loader)); + } + let (data, total) = (data as usize, total as usize); + if data == 0 { + return Err(Refusal::ZeroDataSize); + } + if !data.is_multiple_of(4) { + return Err(Refusal::DataSize(data)); + } + if !total.is_multiple_of(GRANULE) { + return Err(Refusal::TotalSize(total)); + } + let table = HEADER + .checked_add(data) + .and_then(|signed| total.checked_sub(signed)) + .ok_or(Refusal::TotalBelowData { total, data })?; + let truncated = Refusal::Truncated { need: total, have }; + let (data, rest) = rest.split_at_checked(data).ok_or(truncated)?; + let (table, rest) = rest.split_at_checked(table).ok_or(truncated)?; + + match sum(header).wrapping_add(sum(data)) { + 0 => {} + sum => return Err(Refusal::Checksum(sum)), + } + let extended = match table { + [] => &[], + table => extended(table, signature.wrapping_add(checksum).wrapping_add(flags))?, + }; + let update = Update { revision: Revision(revision), signature, flags, data, extended }; + Ok((update, rest)) +} + +/// The entries of an extended signature table whose update's header +/// signature, checksum and flags sum to `primary`. +fn extended(table: &[u8], primary: u32) -> Result<&[[u8; EXT_SIGNATURE]], Refusal> { + let size = Refusal::ExtendedTableSize { len: table.len() }; + let (header, entries) = table.split_first_chunk::().ok_or(size)?; + let [count, ..]: [u32; 5] = dwords(header); + if (count as usize).checked_mul(EXT_SIGNATURE) != Some(entries.len()) { + return Err(size); + } + match sum(table) { + 0 => {} + sum => return Err(Refusal::ExtendedTableChecksum(sum)), + } + let (entries, _) = entries.as_chunks::(); + // An extended signature, flags and checksum replace the header's three in + // the update it stands for, so the two triples sum alike. + for (index, entry) in entries.iter().enumerate() { + let [signature, flags, checksum] = dwords(entry); + if signature.wrapping_add(flags).wrapping_add(checksum) != primary { + return Err(Refusal::ExtendedSignatureChecksum { index }); + } + } + Ok(entries) +} + +/// Every dword of `bytes` summed, unsigned, with wrap (§12.11.5). +fn sum(bytes: &[u8]) -> u32 { + bytes.as_chunks::<4>().0.iter().fold(0, |sum, &w| sum.wrapping_add(u32::from_le_bytes(w))) +} diff --git a/kernel/microcode/src/intel/tests.rs b/kernel/microcode/src/intel/tests.rs new file mode 100644 index 00000000000..28dce66dafd --- /dev/null +++ b/kernel/microcode/src/intel/tests.rs @@ -0,0 +1,250 @@ +//! 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. + +#![allow( + clippy::indexing_slicing, + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + reason = "a test fails by panicking" +)] + +extern crate std; + +use std::vec; +use std::vec::Vec; + +use super::*; + +/// `intel-ucode/06-8c-01`, pinned by its digest in `NOTICE`. +const T14_FILE: &[u8] = include_bytes!("../../intel-ucode/06-8c-01"); + +/// `intel-ucode/06-cc-02`, pinned by its digest in `NOTICE`: one update, +/// revision 0x11c, Data Size 0x2878c, flags 0x94, and an extended signature +/// table whose last entry is 0xe0652 with flags 0x94. +const CC02_FILE: &[u8] = include_bytes!("../../intel-ucode/06-cc-02"); + +/// The T14's i5-1135G7 at the revision its firmware loads. +const T14: Cpu = Cpu { + signature: Signature(0x0008_06c1), + platform: PlatformId(7), + revision: Revision(0xbe), +}; + +fn get(bytes: &[u8], at: usize) -> u32 { + u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) +} + +fn put(bytes: &mut [u8], at: usize, value: u32) { + bytes[at..at + 4].copy_from_slice(&value.to_le_bytes()); +} + +/// Rewrite the header's checksum so the header and data sum to 0 again. +fn reseal(update: &mut [u8], data: usize) { + put(update, 16, 0); + let sum = sum(&update[..HEADER + data]); + put(update, 16, sum.wrapping_neg()); +} + +/// A 2048-byte update to Tables 12-7 to 12-10: `signature` and `flags` in the +/// header, `extended` as its extended signature table, every checksum true. +fn build(signature: u32, flags: u32, revision: u32, extended: &[(u32, u32)]) -> Vec { + let table = if extended.is_empty() { 0 } else { EXT_HEADER + EXT_SIGNATURE * extended.len() }; + let data = 2048 - HEADER - table; + let mut update = vec![0; 2048]; + for (i, word) in update[HEADER..HEADER + data].as_chunks_mut::<4>().0.iter_mut().enumerate() { + *word = (i as u32).wrapping_mul(0x9e37_79b9).to_le_bytes(); + } + for (at, value) in [(0, 1), (4, revision), (12, signature), (20, 1), (24, flags)] { + put(&mut update, at, value); + } + put(&mut update, 28, data as u32); + put(&mut update, 32, 2048); + reseal(&mut update, data); + if table != 0 { + let header = get(&update, 12).wrapping_add(get(&update, 16)).wrapping_add(get(&update, 24)); + let at = HEADER + data; + put(&mut update, at, extended.len() as u32); + for (i, &(sig, flags)) in extended.iter().enumerate() { + let entry = at + EXT_HEADER + EXT_SIGNATURE * i; + put(&mut update, entry, sig); + put(&mut update, entry + 4, flags); + put(&mut update, entry + 8, header.wrapping_sub(sig).wrapping_sub(flags)); + } + let sum = sum(&update[at..]); + put(&mut update, at + 4, sum.wrapping_neg()); + } + update +} + +fn refusal(file: &[u8]) -> Refused { + select(file, &T14).expect_err("a malformed file selects nothing") +} + +#[test] +fn the_t14_runs_intels_newest_update_for_its_cpu() { + assert_eq!(select(T14_FILE, &T14), Ok(Choice::Current(Revision(0xbe)))); +} + +#[test] +fn a_t14_below_it_loads_its_data_which_follows_the_header() { + let older = Cpu { revision: Revision(0xbd), ..T14 }; + let Ok(Choice::Load(update)) = select(T14_FILE, &older) else { panic!("0xbe is newer than 0xbd") }; + assert_eq!(update.revision(), Revision(0xbe)); + assert_eq!(update.data(), &T14_FILE[HEADER..]); +} + +#[test] +fn another_platform_or_stepping_is_not_named() { + for platform in 0..7 { + assert_eq!(select(T14_FILE, &Cpu { platform: PlatformId(platform), ..T14 }), Ok(Choice::NoMatch)); + } + let stepping_2 = Cpu { signature: Signature(0x0008_06c2), ..T14 }; + assert_eq!(select(T14_FILE, &stepping_2), Ok(Choice::NoMatch)); +} + +#[test] +fn an_extended_signature_in_intels_file_names_its_cpu() { + let below = Cpu { signature: Signature(0x000e_0652), platform: PlatformId(7), revision: Revision(0x11b) }; + let Ok(Choice::Load(update)) = select(CC02_FILE, &below) else { panic!("0x11c is newer than 0x11b") }; + assert_eq!(update.revision(), Revision(0x11c)); + assert!(update.data() == &CC02_FILE[HEADER..HEADER + 0x2878c], "the data is not the file's"); + let current = Cpu { revision: Revision(0x11c), ..below }; + assert_eq!(select(CC02_FILE, ¤t), Ok(Choice::Current(Revision(0x11c)))); + assert_eq!(select(CC02_FILE, &Cpu { platform: PlatformId(0), ..below }), Ok(Choice::NoMatch)); +} + +#[test] +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)); +} + +#[test] +fn a_flipped_bit_anywhere_in_header_or_data_is_refused() { + let mut file = T14_FILE.to_vec(); + for at in (0..HEADER).chain((HEADER..file.len()).step_by(4093)) { + file[at] ^= 1; + assert!(select(&file, &T14).is_err(), "a flip at byte {at} was accepted"); + file[at] ^= 1; + } + file[4096] ^= 1; + assert_eq!(refusal(&file), Refused { at: 0, why: Refusal::Checksum(u32::MAX) }); +} + +#[test] +fn a_file_cut_short_is_refused() { + for file in [T14_FILE, CC02_FILE] { + let len = file.len(); + assert_eq!(refusal(&file[..len - 4]).why, Refusal::Truncated { need: len, have: len - 4 }); + } + assert_eq!(refusal(&T14_FILE[..HEADER - 1]).why, Refusal::Truncated { need: HEADER, have: HEADER - 1 }); +} + +#[test] +fn an_empty_file_is_refused() { + assert_eq!(refusal(&[]), Refused { at: 0, why: Refusal::Truncated { need: HEADER, have: 0 } }); +} + +#[test] +fn a_malformed_update_after_a_good_one_refuses_the_file() { + let mut file = T14_FILE.to_vec(); + file.extend_from_slice(&[0; HEADER]); + assert_eq!(refusal(&file), Refused { at: T14_FILE.len(), why: Refusal::HeaderVersion(0) }); + for have in 1..HEADER { + let why = Refusal::Truncated { need: HEADER, have }; + assert_eq!(refusal(&file[..T14_FILE.len() + have]), Refused { at: T14_FILE.len(), why }); + } +} + +#[test] +fn a_header_or_loader_version_other_than_1_is_refused() { + for (at, why) in [(0, Refusal::HeaderVersion(2)), (20, Refusal::LoaderRevision(2))] { + let mut update = build(T14.signature.0, 0x80, 0xc0, &[]); + put(&mut update, at, 2); + reseal(&mut update, 2000); + assert_eq!(refusal(&update).why, why); + } +} + +#[test] +fn a_size_zero_off_its_granule_or_inside_out_is_refused() { + let cases: [(u32, u32, Refusal); 5] = [ + (0, 0, Refusal::ZeroDataSize), + (1998, 2048, Refusal::DataSize(1998)), + (2000, 2047, Refusal::TotalSize(2047)), + (2000, 2560, Refusal::TotalSize(2560)), + (3024, 2048, Refusal::TotalBelowData { total: 2048, data: 3024 }), + ]; + for (data, total, why) in cases { + let mut update = build(T14.signature.0, 0x80, 0xc0, &[]); + put(&mut update, 28, data); + put(&mut update, 32, total); + assert_eq!(refusal(&update).why, why); + } +} + +#[test] +fn any_multiple_of_1024_is_a_total_size() { + let mut update = build(T14.signature.0, 0x80, 0xc0, &[]); + update.resize(3072, 0); + put(&mut update, 28, 3072 - HEADER as u32); + put(&mut update, 32, 3072); + reseal(&mut update, 3072 - HEADER); + assert!(matches!(select(&update, &T14), Ok(Choice::Load(_)))); +} + +#[test] +fn an_extended_signature_names_a_cpu_the_header_does_not() { + let other = (0x0008_06c2, 0x01); + let t14 = (T14.signature.0, 0x80); + for extended in [[other, t14], [t14, other]] { + let update = build(0x0009_06a3, 0x80, 0xc0, &extended); + let Ok(Choice::Load(loaded)) = select(&update, &T14) else { panic!("an entry names the T14") }; + assert_eq!(loaded.data().len(), 2048 - HEADER - EXT_HEADER - 2 * EXT_SIGNATURE); + } + let wrong_platform = build(0x0009_06a3, 0x80, 0xc0, &[(T14.signature.0, 0x01)]); + assert_eq!(select(&wrong_platform, &T14), Ok(Choice::NoMatch)); +} + +#[test] +fn a_damaged_extended_table_is_refused() { + let at = 2048 - EXT_HEADER - 2 * EXT_SIGNATURE; + let good = build(0x0009_06a3, 0x80, 0xc0, &[(0x0008_06c2, 0x01), (T14.signature.0, 0x80)]); + + let mut entry = good.clone(); + let signature = at + EXT_HEADER + EXT_SIGNATURE; + let (sig, sum) = (get(&entry, signature), get(&entry, at + 4)); + put(&mut entry, signature, sig + 1); + put(&mut entry, at + 4, sum.wrapping_sub(1)); + assert_eq!(refusal(&entry).why, Refusal::ExtendedSignatureChecksum { index: 1 }); + + let mut table = good.clone(); + table[at + 8] ^= 1; + assert_eq!(refusal(&table).why, Refusal::ExtendedTableChecksum(1)); + + for count in [1, 3] { + let mut wrong = good.clone(); + put(&mut wrong, at, count); + let len = EXT_HEADER + 2 * EXT_SIGNATURE; + assert_eq!(refusal(&wrong).why, Refusal::ExtendedTableSize { len }, "count {count}"); + } + + let mut short = build(T14.signature.0, 0x80, 0xc0, &[]); + put(&mut short, 28, 1984); + short[2032..].fill(0); + reseal(&mut short, 1984); + assert_eq!(refusal(&short).why, Refusal::ExtendedTableSize { len: 16 }); +} + +#[test] +fn the_newest_update_naming_the_cpu_is_chosen_wherever_it_sits() { + let newer = build(T14.signature.0, 0x80, 0xc0, &[]); + let other = build(T14.signature.0, 0x01, 0xd0, &[]); + for file in [[T14_FILE, &newer[..], &other[..]].concat(), [&other[..], &newer[..], T14_FILE].concat()] { + let Ok(Choice::Load(update)) = select(&file, &T14) else { panic!("0xc0 is newer than 0xbe") }; + assert_eq!(update.revision(), Revision(0xc0)); + } +} diff --git a/kernel/microcode/src/lib.rs b/kernel/microcode/src/lib.rs index 0f5d0907164..4d42c1b9fb5 100644 --- a/kernel/microcode/src/lib.rs +++ b/kernel/microcode/src/lib.rs @@ -1,18 +1,30 @@ -//! Intel microcode update files, read as the SDM defines them, pure. +//! CPU microcode as Intel and AMD publish it, matched to one CPU, and the +//! files 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. +//! [`intel`] reads Intel's update files as SDM Vol. 3A §12.11 defines them, +//! [`amd`] AMD's containers as AMD's own parser and Linux's `amd.c` read them. +//! Each validates the whole file, refuses it by name and offset where any +//! part is malformed, and answers with the update a CPU loads, or why it loads +//! none. Only the documented headers are read: an update's body is its +//! maker'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. +//! Before any file is read, [`Maker::of`] says whose interface a CPU's +//! revision is read through and [`may_load`] whether it loads at all. +//! +//! [`INTEL`] and [`AMD`] are the files the kernel embeds, one per CPU ToyOS +//! has a bench for, each as its maker publishes it and pinned by its digest +//! in `NOTICE`. **On AMD's families 0x17 and 0x19 and part of 0x1a that pin +//! is the only check**: at or below a per-CPU cutoff revision the CPU's own +//! signature check can be forged (EntrySign; Linux's `cpu_has_entrysign`). +//! Linux hashes a patch at run time against `amd_shas.c` because it loads a +//! file the kernel did not ship; these are inside the kernel image, so the +//! hash moves to build time, where a host test holds every patch +//! [`amd::select`] can return out of [`AMD`] to Linux's digest. A microcode +//! source outside the kernel image brings the runtime check with it. //! //! 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,234 +40,131 @@ clippy::unimplemented )] +use core::fmt; + +use toyos_cpuvuln::Vendor; + +pub mod amd; +pub mod intel; + #[cfg(test)] mod tests; -/// 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 -/// (Table 12-9). -const EXT_HEADER: usize = 20; -/// One extended signature: signature, processor flags, checksum (Table 12-10). -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); +/// 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); -/// Which bit of an update's processor flags names this CPU's platform -/// (§12.11.4). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct PlatformId(u8); +/// 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")); -impl PlatformId { - /// `IA32_PLATFORM_ID` (MSR 17H): bits 52:50 (Table 12-11). - pub const fn from_msr(msr: u64) -> Self { - Self((msr >> 50) as u8 & 7) - } +/// AMD's container for the CPUs ToyOS has a bench for: linux-firmware's +/// `amd-ucode/microcode_amd_fam17h.bin`, the AMD bench's Ryzen 7 4800U among +/// them. +pub static AMD: &Aligned<[u8]> = &Aligned(*include_bytes!("../amd-ucode/microcode_amd_fam17h.bin")); - const fn flag(self) -> u32 { - // `self.0` < 8: the field is private and `from_msr` masks it. - 1 << self.0 - } -} +/// CPUID.01H:EAX, which both makers' updates name their CPU by. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Signature(pub u32); -/// An update revision. Signed (Table 12-7): one update is newer than another -/// only when numerically larger (Example 12-10). +/// A microcode revision: Intel's update revision, AMD's patch level, each the +/// dword its maker reports it in `IA32_BIOS_SIGN_ID` (MSR 8BH), compared +/// unsigned as Linux compares both. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Revision(pub i32); +pub struct Revision(pub u32); 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) - } -} - -/// What one CPU reports, read on that CPU. -#[derive(Clone, Copy, Debug)] -pub struct Cpu { - pub signature: Signature, - pub platform: PlatformId, - pub revision: Revision, -} - -/// One update out of a file, validated. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Update<'a> { - revision: Revision, - signature: u32, - flags: u32, - data: &'a [u8], - extended: &'a [[u8; EXT_SIGNATURE]], -} + /// What a CPU whose loading is disabled, by a hypervisor or by a firmware + /// toggle, reports (Xen's `rev == ~0`). + pub const DISABLED: Self = Self(u32::MAX); -impl<'a> Update<'a> { - pub fn revision(&self) -> Revision { - self.revision + /// Intel's: 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) } - /// The update data. `IA32_BIOS_UPDT_TRIG` is written its linear address, - /// which must be 16-byte aligned and mapped present (§12.11.6). - pub fn data(&self) -> &'a [u8] { - self.data + /// AMD's: MSR 8BH's lower dword, the patch level (Linux `amd.c:336`). + pub const fn from_patch_level(msr: u64) -> Self { + Self(msr as u32) } +} - /// The header's signature and flags, then each extended signature's - /// (Examples 12-5 and 12-6). - fn names(&self, cpu: &Cpu) -> bool { - let extended = self.extended.iter().map(|entry| { - let [signature, flags, _] = dwords(entry); - (signature, flags) - }); - core::iter::once((self.signature, self.flags)) - .chain(extended) - .any(|(signature, flags)| signature == cpu.signature.0 && flags & cpu.platform.flag() != 0) +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. +pub enum Choice { + /// The newest update naming the CPU, which it loads. + Load(Update), + /// The newest update naming the CPU is this revision, and the CPU runs + /// one its maker's rule does not load it over. 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 { - pub at: usize, - pub why: Refusal, -} - +/// A maker whose files the kernel embeds: whose file a CPU chooses from, and +/// whose half of MSR 8BH its revision is. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Refusal { - /// The bytes end before the header does, or before Total Size says the - /// update does. - Truncated { need: usize, have: usize }, - /// Header Version is not 1, the only version §12.11 defines. - HeaderVersion(u32), - /// Loader Revision is not 1, the only loader §12.11.6 describes. - LoaderRevision(u32), - /// Data Size is 0, which Table 12-7 reads as 2000 bytes: the form of - /// updates for CPUs older than any ToyOS runs on. - ZeroDataSize, - /// Data Size is not a multiple of a dword. - DataSize(usize), - /// Total Size is not a multiple of 1024. - TotalSize(usize), - /// Total Size is smaller than the header and data it holds. - TotalBelowData { total: usize, data: usize }, - /// The header's and data's dwords sum to this, not to 0 (§12.11.5). - Checksum(u32), - /// The extended signature table is not 20 bytes and 12 for each signature - /// its count names. - ExtendedTableSize { len: usize }, - /// The extended signature table's dwords sum to this, not to 0 (§12.11.2). - ExtendedTableChecksum(u32), - /// This extended signature's checksum does not stand in for the header's - /// (Table 12-10). - ExtendedSignatureChecksum { index: usize }, +pub enum Maker { + Intel, + Amd, } -/// The update in `file` to load on `cpu`, or why there is none. A file holds -/// at least one update. -pub fn select<'a>(file: &'a [u8], cpu: &Cpu) -> Result, Refused> { - let mut newest: Option> = None; - let mut rest = file; - loop { - // `rest` is a suffix of `file`. - let at = file.len().wrapping_sub(rest.len()); - let (update, after) = parse(rest).map_err(|why| Refused { at, why })?; - if update.names(cpu) && newest.is_none_or(|n| update.revision > n.revision) { - newest = Some(update); - } - if after.is_empty() { - break; +impl Maker { + /// The maker a CPU of `vendor` loads from, or why it loads nothing. + pub fn of(vendor: Vendor) -> Result { + match vendor { + Vendor::Intel => Ok(Self::Intel), + Vendor::Amd => Ok(Self::Amd), + Vendor::Hygon | Vendor::Centaur | Vendor::Zhaoxin | Vendor::Unknown => { + Err(Unloaded::Vendor(vendor)) + } } - rest = after; } - Ok(match newest { - None => Choice::NoMatch, - Some(update) if update.revision > cpu.revision => Choice::Load(update), - Some(update) => Choice::Current(update.revision), - }) } -/// The update `bytes` starts with, and the bytes after it. -fn parse(bytes: &[u8]) -> Result<(Update<'_>, &[u8]), Refusal> { - let have = bytes.len(); - let (header, rest) = - bytes.split_first_chunk::
().ok_or(Refusal::Truncated { need: HEADER, have })?; - let [version, revision, _, signature, checksum, loader, flags, data, total, ..]: [u32; 12] = - dwords(header); - if version != 1 { - return Err(Refusal::HeaderVersion(version)); - } - if loader != 1 { - return Err(Refusal::LoaderRevision(loader)); - } - let (data, total) = (data as usize, total as usize); - if data == 0 { - return Err(Refusal::ZeroDataSize); - } - if !data.is_multiple_of(4) { - return Err(Refusal::DataSize(data)); - } - if !total.is_multiple_of(GRANULE) { - return Err(Refusal::TotalSize(total)); - } - let table = HEADER - .checked_add(data) - .and_then(|signed| total.checked_sub(signed)) - .ok_or(Refusal::TotalBelowData { total, data })?; - let truncated = Refusal::Truncated { need: total, have }; - let (data, rest) = rest.split_at_checked(data).ok_or(truncated)?; - let (table, rest) = rest.split_at_checked(table).ok_or(truncated)?; +/// Why a CPU loads nothing, said before any file is read. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Unloaded { + /// No embedded file is from this vendor, so nothing of its CPU is read. + Vendor(Vendor), + /// CPUID.01H:ECX[31]: a hypervisor owns this CPU's microcode (Linux + /// `core.c:114-139`). + Hypervisor, + /// The CPU reports [`Revision::DISABLED`]. + Disabled, +} - match sum(header).wrapping_add(sum(data)) { - 0 => {} - sum => return Err(Refusal::Checksum(sum)), +impl fmt::Display for Unloaded { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Vendor(_) => "no embedded file is from this vendor", + Self::Hypervisor => "a hypervisor owns this CPU's microcode", + Self::Disabled => "revision 0xffffffff says loading is disabled", + }) } - let extended = match table { - [] => &[], - table => extended(table, signature.wrapping_add(checksum).wrapping_add(flags))?, - }; - let update = Update { revision: Revision(revision as i32), signature, flags, data, extended }; - Ok((update, rest)) } -/// The entries of an extended signature table whose update's header -/// signature, checksum and flags sum to `primary`. -fn extended(table: &[u8], primary: u32) -> Result<&[[u8; EXT_SIGNATURE]], Refusal> { - let size = Refusal::ExtendedTableSize { len: table.len() }; - let (header, entries) = table.split_first_chunk::().ok_or(size)?; - let [count, ..]: [u32; 5] = dwords(header); - if (count as usize).checked_mul(EXT_SIGNATURE) != Some(entries.len()) { - return Err(size); +/// Whether a CPU whose maker [`Maker::of`] names 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); } - match sum(table) { - 0 => {} - sum => return Err(Refusal::ExtendedTableChecksum(sum)), + if found == Revision::DISABLED { + return Err(Unloaded::Disabled); } - let (entries, _) = entries.as_chunks::(); - // An extended signature, flags and checksum replace the header's three in - // the update it stands for, so the two triples sum alike. - for (index, entry) in entries.iter().enumerate() { - let [signature, flags, checksum] = dwords(entry); - if signature.wrapping_add(flags).wrapping_add(checksum) != primary { - return Err(Refusal::ExtendedSignatureChecksum { index }); - } - } - Ok(entries) + Ok(()) } /// `bytes` as little-endian dwords; a `LEN` that is not `4 * N` does not build. @@ -267,8 +176,3 @@ fn dwords(bytes: &[u8; LEN]) -> [u32; N] { } dwords } - -/// Every dword of `bytes` summed, unsigned, with wrap (§12.11.5). -fn sum(bytes: &[u8]) -> u32 { - bytes.as_chunks::<4>().0.iter().fold(0, |sum, &w| sum.wrapping_add(u32::from_le_bytes(w))) -} diff --git a/kernel/microcode/src/tests.rs b/kernel/microcode/src/tests.rs index 506f1a5018b..2385edf2cea 100644 --- a/kernel/microcode/src/tests.rs +++ b/kernel/microcode/src/tests.rs @@ -1,258 +1,43 @@ -//! 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. +//! Whether a CPU loads at all, and the Intel file the kernel embeds. -#![allow( - clippy::indexing_slicing, - clippy::arithmetic_side_effects, - clippy::unwrap_used, - clippy::expect_used, - clippy::panic, - reason = "a test fails by panicking" -)] - -extern crate std; - -use std::vec; -use std::vec::Vec; +#![allow(clippy::unwrap_used, clippy::panic, reason = "a test fails by panicking")] use super::*; -/// `intel-ucode/06-8c-01`, pinned by its digest in `NOTICE`. -const T14_FILE: &[u8] = include_bytes!("../intel-ucode/06-8c-01"); - -/// `intel-ucode/06-cc-02`, pinned by its digest in `NOTICE`: one update, -/// revision 0x11c, Data Size 0x2878c, flags 0x94, and an extended signature -/// table whose last entry is 0xe0652 with flags 0x94. -const CC02_FILE: &[u8] = include_bytes!("../intel-ucode/06-cc-02"); - -/// The T14's i5-1135G7 at the revision its firmware loads. -const T14: Cpu = Cpu { - signature: Signature(0x0008_06c1), - platform: PlatformId(7), - revision: Revision(0xbe), -}; - -fn get(bytes: &[u8], at: usize) -> u32 { - u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) -} - -fn put(bytes: &mut [u8], at: usize, value: u32) { - bytes[at..at + 4].copy_from_slice(&value.to_le_bytes()); -} - -/// Rewrite the header's checksum so the header and data sum to 0 again. -fn reseal(update: &mut [u8], data: usize) { - put(update, 16, 0); - let sum = sum(&update[..HEADER + data]); - put(update, 16, sum.wrapping_neg()); -} - -/// A 2048-byte update to Tables 12-7 to 12-10: `signature` and `flags` in the -/// header, `extended` as its extended signature table, every checksum true. -fn build(signature: u32, flags: u32, revision: u32, extended: &[(u32, u32)]) -> Vec { - let table = if extended.is_empty() { 0 } else { EXT_HEADER + EXT_SIGNATURE * extended.len() }; - let data = 2048 - HEADER - table; - let mut update = vec![0; 2048]; - for (i, word) in update[HEADER..HEADER + data].as_chunks_mut::<4>().0.iter_mut().enumerate() { - *word = (i as u32).wrapping_mul(0x9e37_79b9).to_le_bytes(); - } - for (at, value) in [(0, 1), (4, revision), (12, signature), (20, 1), (24, flags)] { - put(&mut update, at, value); - } - put(&mut update, 28, data as u32); - put(&mut update, 32, 2048); - reseal(&mut update, data); - if table != 0 { - let header = get(&update, 12).wrapping_add(get(&update, 16)).wrapping_add(get(&update, 24)); - let at = HEADER + data; - put(&mut update, at, extended.len() as u32); - for (i, &(sig, flags)) in extended.iter().enumerate() { - let entry = at + EXT_HEADER + EXT_SIGNATURE * i; - put(&mut update, entry, sig); - put(&mut update, entry + 4, flags); - put(&mut update, entry + 8, header.wrapping_sub(sig).wrapping_sub(flags)); - } - let sum = sum(&update[at..]); - put(&mut update, at + 4, sum.wrapping_neg()); - } - update -} - -fn refusal(file: &[u8]) -> Refused { - select(file, &T14).expect_err("a malformed file selects nothing") -} - #[test] -fn the_t14_runs_intels_newest_update_for_its_cpu() { - assert_eq!(select(T14_FILE, &T14), Ok(Choice::Current(Revision(0xbe)))); -} - -#[test] -fn a_t14_below_it_loads_its_data_which_follows_the_header() { - let older = Cpu { revision: Revision(0xbd), ..T14 }; - let Ok(Choice::Load(update)) = select(T14_FILE, &older) else { panic!("0xbe is newer than 0xbd") }; - assert_eq!(update.revision(), Revision(0xbe)); - assert_eq!(update.data(), &T14_FILE[HEADER..]); -} - -#[test] -fn another_platform_or_stepping_is_not_named() { - for platform in 0..7 { - assert_eq!(select(T14_FILE, &Cpu { platform: PlatformId(platform), ..T14 }), Ok(Choice::NoMatch)); +fn a_vendor_no_embedded_file_is_from_loads_nothing() { + assert_eq!(Maker::of(Vendor::Intel), Ok(Maker::Intel)); + assert_eq!(Maker::of(Vendor::Amd), Ok(Maker::Amd)); + for vendor in [Vendor::Hygon, Vendor::Centaur, Vendor::Zhaoxin, Vendor::Unknown] { + assert_eq!(Maker::of(vendor), Err(Unloaded::Vendor(vendor))); } - let stepping_2 = Cpu { signature: Signature(0x0008_06c2), ..T14 }; - assert_eq!(select(T14_FILE, &stepping_2), Ok(Choice::NoMatch)); } #[test] -fn an_extended_signature_in_intels_file_names_its_cpu() { - let below = Cpu { signature: Signature(0x000e_0652), platform: PlatformId(7), revision: Revision(0x11b) }; - let Ok(Choice::Load(update)) = select(CC02_FILE, &below) else { panic!("0x11c is newer than 0x11b") }; - assert_eq!(update.revision(), Revision(0x11c)); - assert!(update.data() == &CC02_FILE[HEADER..HEADER + 0x2878c], "the data is not the file's"); - let current = Cpu { revision: Revision(0x11c), ..below }; - assert_eq!(select(CC02_FILE, ¤t), Ok(Choice::Current(Revision(0x11c)))); - assert_eq!(select(CC02_FILE, &Cpu { platform: PlatformId(0), ..below }), Ok(Choice::NoMatch)); -} - -#[test] -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] -fn a_flipped_bit_anywhere_in_header_or_data_is_refused() { - let mut file = T14_FILE.to_vec(); - for at in (0..HEADER).chain((HEADER..file.len()).step_by(4093)) { - file[at] ^= 1; - assert!(select(&file, &T14).is_err(), "a flip at byte {at} was accepted"); - file[at] ^= 1; +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)); } - file[4096] ^= 1; - assert_eq!(refusal(&file), Refused { at: 0, why: Refusal::Checksum(u32::MAX) }); } +/// Xen's reading of a revision of `~0`: loading is disabled, by a hypervisor +/// or a firmware toggle; an Intel CPU handed an update over it would refuse +/// it at the read-back. #[test] -fn a_file_cut_short_is_refused() { - for file in [T14_FILE, CC02_FILE] { - let len = file.len(); - assert_eq!(refusal(&file[..len - 4]).why, Refusal::Truncated { need: len, have: len - 4 }); - } - assert_eq!(refusal(&T14_FILE[..HEADER - 1]).why, Refusal::Truncated { need: HEADER, have: HEADER - 1 }); +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 an_empty_file_is_refused() { - assert_eq!(refusal(&[]), Refused { at: 0, why: Refusal::Truncated { need: HEADER, have: 0 } }); -} - -#[test] -fn a_malformed_update_after_a_good_one_refuses_the_file() { - let mut file = T14_FILE.to_vec(); - file.extend_from_slice(&[0; HEADER]); - assert_eq!(refusal(&file), Refused { at: T14_FILE.len(), why: Refusal::HeaderVersion(0) }); - for have in 1..HEADER { - let why = Refusal::Truncated { need: HEADER, have }; - assert_eq!(refusal(&file[..T14_FILE.len() + have]), Refused { at: T14_FILE.len(), why }); - } -} - -#[test] -fn a_header_or_loader_version_other_than_1_is_refused() { - for (at, why) in [(0, Refusal::HeaderVersion(2)), (20, Refusal::LoaderRevision(2))] { - let mut update = build(T14.signature.0, 0x80, 0xc0, &[]); - put(&mut update, at, 2); - reseal(&mut update, 2000); - assert_eq!(refusal(&update).why, why); - } -} - -#[test] -fn a_size_zero_off_its_granule_or_inside_out_is_refused() { - let cases: [(u32, u32, Refusal); 5] = [ - (0, 0, Refusal::ZeroDataSize), - (1998, 2048, Refusal::DataSize(1998)), - (2000, 2047, Refusal::TotalSize(2047)), - (2000, 2560, Refusal::TotalSize(2560)), - (3024, 2048, Refusal::TotalBelowData { total: 2048, data: 3024 }), - ]; - for (data, total, why) in cases { - let mut update = build(T14.signature.0, 0x80, 0xc0, &[]); - put(&mut update, 28, data); - put(&mut update, 32, total); - assert_eq!(refusal(&update).why, why); - } -} - -#[test] -fn any_multiple_of_1024_is_a_total_size() { - let mut update = build(T14.signature.0, 0x80, 0xc0, &[]); - update.resize(3072, 0); - put(&mut update, 28, 3072 - HEADER as u32); - put(&mut update, 32, 3072); - reseal(&mut update, 3072 - HEADER); - assert!(matches!(select(&update, &T14), Ok(Choice::Load(_)))); -} - -#[test] -fn an_extended_signature_names_a_cpu_the_header_does_not() { - let other = (0x0008_06c2, 0x01); - let t14 = (T14.signature.0, 0x80); - for extended in [[other, t14], [t14, other]] { - let update = build(0x0009_06a3, 0x80, 0xc0, &extended); - let Ok(Choice::Load(loaded)) = select(&update, &T14) else { panic!("an entry names the T14") }; - assert_eq!(loaded.data().len(), 2048 - HEADER - EXT_HEADER - 2 * EXT_SIGNATURE); - } - let wrong_platform = build(0x0009_06a3, 0x80, 0xc0, &[(T14.signature.0, 0x01)]); - assert_eq!(select(&wrong_platform, &T14), Ok(Choice::NoMatch)); -} - -#[test] -fn a_damaged_extended_table_is_refused() { - let at = 2048 - EXT_HEADER - 2 * EXT_SIGNATURE; - let good = build(0x0009_06a3, 0x80, 0xc0, &[(0x0008_06c2, 0x01), (T14.signature.0, 0x80)]); - - let mut entry = good.clone(); - let signature = at + EXT_HEADER + EXT_SIGNATURE; - let (sig, sum) = (get(&entry, signature), get(&entry, at + 4)); - put(&mut entry, signature, sig + 1); - put(&mut entry, at + 4, sum.wrapping_sub(1)); - assert_eq!(refusal(&entry).why, Refusal::ExtendedSignatureChecksum { index: 1 }); - - let mut table = good.clone(); - table[at + 8] ^= 1; - assert_eq!(refusal(&table).why, Refusal::ExtendedTableChecksum(1)); - - for count in [1, 3] { - let mut wrong = good.clone(); - put(&mut wrong, at, count); - let len = EXT_HEADER + 2 * EXT_SIGNATURE; - assert_eq!(refusal(&wrong).why, Refusal::ExtendedTableSize { len }, "count {count}"); - } - - let mut short = build(T14.signature.0, 0x80, 0xc0, &[]); - put(&mut short, 28, 1984); - short[2032..].fill(0); - reseal(&mut short, 1984); - assert_eq!(refusal(&short).why, Refusal::ExtendedTableSize { len: 16 }); -} - -#[test] -fn the_newest_update_naming_the_cpu_is_chosen_wherever_it_sits() { - let newer = build(T14.signature.0, 0x80, 0xc0, &[]); - let other = build(T14.signature.0, 0x01, 0xd0, &[]); - for file in [[T14_FILE, &newer[..], &other[..]].concat(), [&other[..], &newer[..], T14_FILE].concat()] { - let Ok(Choice::Load(update)) = select(&file, &T14) else { panic!("0xc0 is newer than 0xbe") }; - assert_eq!(update.revision(), Revision(0xc0)); - } -} - -#[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 the_embedded_intel_file_hands_the_t14_its_update_16_byte_aligned() { + let below = intel::Cpu { + signature: Signature(0x0008_06c1), + platform: intel::PlatformId::from_msr(7 << 50), + revision: Revision(0xbd), + }; + let Ok(Choice::Load(update)) = intel::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..786761a8078 --- /dev/null +++ b/kernel/src/arch/x86_64/microcode.rs @@ -0,0 +1,260 @@ +//! This CPU's microcode, loaded from the files `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. Neither maker documents a +//! fault for a valid update at a present, canonical address, and every +//! failure this loader can see is a panic, which reports before the IDT. +//! +//! 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. A load is read back (SDM +//! Example 12-10; Linux `amd.c:723-729`) and a revision other than the +//! update's panics naming both, rather than run behind a log line below the +//! revision this image pins. 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::{amd, intel, Choice, Maker, Revision, Signature, Unloaded}; + +use super::cpu; +use crate::log; + +/// `IA32_PLATFORM_ID`. +const PLATFORM_ID: u32 = 0x17; +/// `IA32_BIOS_UPDT_TRIG`. +const INTEL_TRIGGER: u32 = 0x79; +/// `IA32_BIOS_SIGN_ID`, AMD's `MSR_AMD64_PATCH_LEVEL`. +const SIGN_ID: u32 = 0x8b; +/// `MSR_AMD64_PATCH_LOADER`. +const AMD_TRIGGER: u32 = 0xc001_0020; + +/// 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); + let signature = Signature(eax); + let maker = match Maker::of(vendor) { + Ok(maker) => maker, + Err(why) => { + log!("microcode: cpu{cpu_id} {vendor:?} signature {eax:#x} loads nothing: {why}"); + return; + } + }; + let found = revision(maker); + let (platform, outcome) = match toyos_microcode::may_load(ecx, found) { + Err(why) => (None, Outcome::Unloaded(why)), + Ok(()) => match maker { + Maker::Intel => { + let platform = intel::PlatformId::from_msr(cpu::rdmsr(PLATFORM_ID)); + let cpu = intel::Cpu { signature, platform, revision: found }; + (Some(platform), load_intel(cpu_id, &cpu)) + } + Maker::Amd => (None, load_amd(cpu_id, signature, found)), + }, + }; + let running = match outcome { + Outcome::Loaded(loaded) => loaded, + 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 file its maker's is. +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), +} + +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}"), + } + } +} + +/// An Intel 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, read as its maker reports it. +fn revision(maker: Maker) -> Revision { + match maker { + Maker::Intel => { + // 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)) + } + Maker::Amd => Revision::from_patch_level(cpu::rdmsr(SIGN_ID)), + } +} + +fn load_intel(cpu_id: u32, cpu: &intel::Cpu) -> Outcome { + let choice = intel::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(INTEL_TRIGGER, update.data().as_ptr()) }; + verified(cpu_id, Maker::Intel, cpu.revision, update.revision()); + Outcome::Loaded(update.revision()) + } + } +} + +fn load_amd(cpu_id: u32, signature: Signature, found: Revision) -> Outcome { + let choice = amd::select(&toyos_microcode::AMD.0, signature, found).unwrap_or_else(|refused| { + panic!("microcode: the embedded AMD container is refused at byte {}: {:?}", refused.at, refused.why) + }); + match choice { + Choice::NoMatch => Outcome::NoMatch, + Choice::Current(newest) => Outcome::Current(newest), + Choice::Load(patch) => { + let bytes = patch.bytes().as_ptr_range(); + // SAFETY: a validated patch inside the kernel image, so its + // address is canonical and present (Linux `amd.c:700-705`). + unsafe { trigger(AMD_TRIGGER, bytes.start) }; + if patch.leaves_its_mapping_uncached() { + cpu::invlpg(bytes.start as u64); + cpu::invlpg(bytes.end as u64 - 1); + } + verified(cpu_id, Maker::Amd, found, patch.revision()); + Outcome::Loaded(patch.revision()) + } + } +} + +/// A trigger MSR written the address of the bytes the CPU loads from. Not +/// `cpu::wrmsr`, which is `nomem`: the CPU reads memory through this write. +/// +/// # Safety +/// `msr` is a maker's trigger on a CPU of that maker, and `at` the start of +/// one of that maker's validated updates, placed as its trigger asks. +unsafe fn trigger(msr: u32, at: *const u8) { + let at = at as u64; + // SAFETY: the caller's. + unsafe { + asm!("wrmsr", in("ecx") msr, in("eax") at as u32, in("edx") (at >> 32) as u32, options(nostack)); + } +} + +/// The revision read back after a load is the update's, or the boot stops. +fn verified(cpu_id: u32, maker: Maker, found: Revision, loaded: Revision) { + let read = revision(maker); + assert!( + read == loaded, + "microcode: cpu{cpu_id} found {found:#x}, was handed {maker:?}'s update {loaded:#x} and \ + reads back {read:#x}", + ); +} + +/// 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 the read-back must stop the boot. +#[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 9e2712141df..29efacb3688 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/licenses/AMD-ucode-license.txt b/licenses/AMD-ucode-license.txt new file mode 100644 index 00000000000..d882d266bb1 --- /dev/null +++ b/licenses/AMD-ucode-license.txt @@ -0,0 +1,64 @@ +Copyright (C) 2010-2025 Advanced Micro Devices, Inc., All rights reserved. + +Permission is hereby granted by Advanced Micro Devices, Inc. ("AMD"), +free of any license fees, to any person obtaining a copy of this +microcode in binary form (the "Software") ("You"), to install, +reproduce, copy and distribute copies of the Software and to permit +persons to whom the Software is provided to do the same, subject to +the following terms and conditions. Your use of any portion of the +Software shall constitute Your acceptance of the following terms and +conditions. If You do not agree to the following terms and conditions, +do not use, retain or redistribute any portion of the Software. + +If You redistribute this Software, You must reproduce the above +copyright notice and this license with the Software. +Without specific, prior, written permission from AMD, You may not +reference AMD or AMD products in the promotion of any product derived +from or incorporating this Software in any manner that implies that +AMD endorses or has certified such product derived from or +incorporating this Software. + +You may not reverse engineer, decompile, or disassemble this Software +or any portion thereof. + +THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY EXPRESS OR IMPLIED +WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO WARRANTIES OF +MERCHANTABILITY, NONINFRINGEMENT, TITLE, FITNESS FOR ANY PARTICULAR +PURPOSE, OR WARRANTIES ARISING FROM CONDUCT, COURSE OF DEALING, OR +USAGE OF TRADE. IN NO EVENT SHALL AMD OR ITS LICENSORS BE LIABLE FOR +ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR +LOSS OF PROFITS, BUSINESS INTERRUPTION, OR LOSS OF DATA OR +INFORMATION) ARISING OUT OF AMD'S NEGLIGENCE, GROSS NEGLIGENCE, THE +USE OF OR INABILITY TO USE THE SOFTWARE, EVEN IF AMD HAS BEEN ADVISED +OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME JURISDICTIONS +PROHIBIT THE EXCLUSION OR LIMITATION OF LIABILITY FOR CONSEQUENTIAL OR +INCIDENTAL DAMAGES OR THE EXCLUSION OF IMPLIED WARRANTIES, THE ABOVE +LIMITATION MAY NOT APPLY TO YOU. + +Without limiting the foregoing, the Software may implement third party +technologies for which You must obtain licenses from parties other +than AMD. You agree that AMD has not obtained or conveyed to You, and +that You shall be responsible for obtaining the rights to use and/or +distribute the applicable underlying intellectual property rights +related to the third party technologies. These third party +technologies are not licensed hereunder. + +If You use the Software (in whole or in part), You shall adhere to all +applicable U.S., European, and other export laws, including but not +limited to the U.S. Export Administration Regulations ("EAR"), (15 +C.F.R. Sections 730 through 774), and E.U. Council Regulation (EC) No +1334/2000 of 22 June 2000. Further, pursuant to Section 740.6 of the +EAR, You hereby certify that, except pursuant to a license granted by +the United States Department of Commerce Bureau of Industry and +Security or as otherwise permitted pursuant to a License Exception +under the U.S. Export Administration Regulations ("EAR"), You will not +(1) export, re-export or release to a national of a country in Country +Groups D:1, E:1 or E:2 any restricted technology, software, or source +code You receive hereunder, or (2) export to Country Groups D:1, E:1 +or E:2 the direct product of such technology or software, if such +foreign produced direct product is subject to national security +controls as identified on the Commerce Control List (currently found +in Supplement 1 to Part 774 of EAR). For the most current Country +Group listings, or for additional information about the EAR or Your +obligations under those regulations, please refer to the U.S. Bureau +of Industry and Security?s website at ttp://www.bis.doc.gov/. diff --git a/tests/checks.rs b/tests/checks.rs index 546e548bae3..8eaba848d08 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -402,6 +402,56 @@ mod checks { Ok(()) } + /// [`microcode_on_metal`] against both benches' machines and the ways a + /// boot of either could fall short of Linux's reading, 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 BSP: &str = "Amd signature 0x860f01 found 0x8600106 loaded 0x860010d running 0x860010d"; + const SIBLING: &str = "Amd signature 0x860f01 found 0x860010d loaded 0x860010d running 0x860010d"; + let t14 = |cpu: u32| (cpu, T14); + let amd = |cpu: u32| (cpu, if cpu == 0 || cpu % 2 == 1 { BSP } else { SIBLING }); + + 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 as Linux reads it was refused: {e}"))?; + microcode_on_metal(&log(&(0..16).map(amd).collect::>()), 16) + .map_err(|e| format!("the AMD bench as Linux reads it 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 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 Linux", &below, 8, "Linux runs 0xbe")?; + let mut unloaded: Vec<_> = (0..16).map(amd).collect(); + unloaded[5] = (5, "Amd signature 0x860f01 found 0x8600106 current 0x860010d running 0x8600106"); + refused("an AMD CPU left on its firmware's", &unloaded, 16, "Linux loads 0x860010d")?; + let mut no_load: Vec<_> = (0..16).map(amd).collect(); + no_load[0] = (0, SIBLING); + refused("a BSP that found the patch already", &no_load, 16, "which the BSP finds")?; + 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 nobody read under Linux", + &[(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 Linux reading", + )?; + eprintln!(" [microcode] the verdict refuses 7 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/linux/amd_shas.c b/tests/linux/amd_shas.c new file mode 100644 index 00000000000..1fd349cfc80 --- /dev/null +++ b/tests/linux/amd_shas.c @@ -0,0 +1,556 @@ +/* Keep 'em sorted. */ +static const struct patch_digest phashes[] = { + { 0x8001227, { + 0x99,0xc0,0x9b,0x2b,0xcc,0x9f,0x52,0x1b, + 0x1a,0x5f,0x1d,0x83,0xa1,0x6c,0xc4,0x46, + 0xe2,0x6c,0xda,0x73,0xfb,0x2d,0x23,0xa8, + 0x77,0xdc,0x15,0x31,0x33,0x4a,0x46,0x18, + } + }, + { 0x8001250, { + 0xc0,0x0b,0x6b,0x19,0xfd,0x5c,0x39,0x60, + 0xd5,0xc3,0x57,0x46,0x54,0xe4,0xd1,0xaa, + 0xa8,0xf7,0x1f,0xa8,0x6a,0x60,0x3e,0xe3, + 0x27,0x39,0x8e,0x53,0x30,0xf8,0x49,0x19, + } + }, + { 0x800126e, { + 0xf3,0x8b,0x2b,0xb6,0x34,0xe3,0xc8,0x2c, + 0xef,0xec,0x63,0x6d,0xc8,0x76,0x77,0xb3, + 0x25,0x5a,0xb7,0x52,0x8c,0x83,0x26,0xe6, + 0x4c,0xbe,0xbf,0xe9,0x7d,0x22,0x6a,0x43, + } + }, + { 0x800126f, { + 0x2b,0x5a,0xf2,0x9c,0xdd,0xd2,0x7f,0xec, + 0xec,0x96,0x09,0x57,0xb0,0x96,0x29,0x8b, + 0x2e,0x26,0x91,0xf0,0x49,0x33,0x42,0x18, + 0xdd,0x4b,0x65,0x5a,0xd4,0x15,0x3d,0x33, + } + }, + { 0x800820d, { + 0x68,0x98,0x83,0xcd,0x22,0x0d,0xdd,0x59, + 0x73,0x2c,0x5b,0x37,0x1f,0x84,0x0e,0x67, + 0x96,0x43,0x83,0x0c,0x46,0x44,0xab,0x7c, + 0x7b,0x65,0x9e,0x57,0xb5,0x90,0x4b,0x0e, + } + }, + { 0x8301025, { + 0xe4,0x7d,0xdb,0x1e,0x14,0xb4,0x5e,0x36, + 0x8f,0x3e,0x48,0x88,0x3c,0x6d,0x76,0xa1, + 0x59,0xc6,0xc0,0x72,0x42,0xdf,0x6c,0x30, + 0x6f,0x0b,0x28,0x16,0x61,0xfc,0x79,0x77, + } + }, + { 0x8301055, { + 0x81,0x7b,0x99,0x1b,0xae,0x2d,0x4f,0x9a, + 0xef,0x13,0xce,0xb5,0x10,0xaf,0x6a,0xea, + 0xe5,0xb0,0x64,0x98,0x10,0x68,0x34,0x3b, + 0x9d,0x7a,0xd6,0x22,0x77,0x5f,0xb3,0x5b, + } + }, + { 0x8301072, { + 0xcf,0x76,0xa7,0x1a,0x49,0xdf,0x2a,0x5e, + 0x9e,0x40,0x70,0xe5,0xdd,0x8a,0xa8,0x28, + 0x20,0xdc,0x91,0xd8,0x2c,0xa6,0xa0,0xb1, + 0x2d,0x22,0x26,0x94,0x4b,0x40,0x85,0x30, + } + }, + { 0x830107a, { + 0x2a,0x65,0x8c,0x1a,0x5e,0x07,0x21,0x72, + 0xdf,0x90,0xa6,0x51,0x37,0xd3,0x4b,0x34, + 0xc4,0xda,0x03,0xe1,0x8a,0x6c,0xfb,0x20, + 0x04,0xb2,0x81,0x05,0xd4,0x87,0xf4,0x0a, + } + }, + { 0x830107b, { + 0xb3,0x43,0x13,0x63,0x56,0xc1,0x39,0xad, + 0x10,0xa6,0x2b,0xcc,0x02,0xe6,0x76,0x2a, + 0x1e,0x39,0x58,0x3e,0x23,0x6e,0xa4,0x04, + 0x95,0xea,0xf9,0x6d,0xc2,0x8a,0x13,0x19, + } + }, + { 0x830107c, { + 0x21,0x64,0xde,0xfb,0x9f,0x68,0x96,0x47, + 0x70,0x5c,0xe2,0x8f,0x18,0x52,0x6a,0xac, + 0xa4,0xd2,0x2e,0xe0,0xde,0x68,0x66,0xc3, + 0xeb,0x1e,0xd3,0x3f,0xbc,0x51,0x1d,0x38, + } + }, + { 0x860010d, { + 0x86,0xb6,0x15,0x83,0xbc,0x3b,0x9c,0xe0, + 0xb3,0xef,0x1d,0x99,0x84,0x35,0x15,0xf7, + 0x7c,0x2a,0xc6,0x42,0xdb,0x73,0x07,0x5c, + 0x7d,0xc3,0x02,0xb5,0x43,0x06,0x5e,0xf8, + } + }, + { 0x8608108, { + 0x14,0xfe,0x57,0x86,0x49,0xc8,0x68,0xe2, + 0x11,0xa3,0xcb,0x6e,0xff,0x6e,0xd5,0x38, + 0xfe,0x89,0x1a,0xe0,0x67,0xbf,0xc4,0xcc, + 0x1b,0x9f,0x84,0x77,0x2b,0x9f,0xaa,0xbd, + } + }, + { 0x8701034, { + 0xc3,0x14,0x09,0xa8,0x9c,0x3f,0x8d,0x83, + 0x9b,0x4c,0xa5,0xb7,0x64,0x8b,0x91,0x5d, + 0x85,0x6a,0x39,0x26,0x1e,0x14,0x41,0xa8, + 0x75,0xea,0xa6,0xf9,0xc9,0xd1,0xea,0x2b, + } + }, + { 0x8a00008, { + 0xd7,0x2a,0x93,0xdc,0x05,0x2f,0xa5,0x6e, + 0x0c,0x61,0x2c,0x07,0x9f,0x38,0xe9,0x8e, + 0xef,0x7d,0x2a,0x05,0x4d,0x56,0xaf,0x72, + 0xe7,0x56,0x47,0x6e,0x60,0x27,0xd5,0x8c, + } + }, + { 0x8a0000a, { + 0x73,0x31,0x26,0x22,0xd4,0xf9,0xee,0x3c, + 0x07,0x06,0xe7,0xb9,0xad,0xd8,0x72,0x44, + 0x33,0x31,0xaa,0x7d,0xc3,0x67,0x0e,0xdb, + 0x47,0xb5,0xaa,0xbc,0xf5,0xbb,0xd9,0x20, + } + }, + { 0xa00104c, { + 0x3c,0x8a,0xfe,0x04,0x62,0xd8,0x6d,0xbe, + 0xa7,0x14,0x28,0x64,0x75,0xc0,0xa3,0x76, + 0xb7,0x92,0x0b,0x97,0x0a,0x8e,0x9c,0x5b, + 0x1b,0xc8,0x9d,0x3a,0x1e,0x81,0x3d,0x3b, + } + }, + { 0xa00104e, { + 0xc4,0x35,0x82,0x67,0xd2,0x86,0xe5,0xb2, + 0xfd,0x69,0x12,0x38,0xc8,0x77,0xba,0xe0, + 0x70,0xf9,0x77,0x89,0x10,0xa6,0x74,0x4e, + 0x56,0x58,0x13,0xf5,0x84,0x70,0x28,0x0b, + } + }, + { 0xa001053, { + 0x92,0x0e,0xf4,0x69,0x10,0x3b,0xf9,0x9d, + 0x31,0x1b,0xa6,0x99,0x08,0x7d,0xd7,0x25, + 0x7e,0x1e,0x89,0xba,0x35,0x8d,0xac,0xcb, + 0x3a,0xb4,0xdf,0x58,0x12,0xcf,0xc0,0xc3, + } + }, + { 0xa001058, { + 0x33,0x7d,0xa9,0xb5,0x4e,0x62,0x13,0x36, + 0xef,0x66,0xc9,0xbd,0x0a,0xa6,0x3b,0x19, + 0xcb,0xf5,0xc2,0xc3,0x55,0x47,0x20,0xec, + 0x1f,0x7b,0xa1,0x44,0x0e,0x8e,0xa4,0xb2, + } + }, + { 0xa001075, { + 0x39,0x02,0x82,0xd0,0x7c,0x26,0x43,0xe9, + 0x26,0xa3,0xd9,0x96,0xf7,0x30,0x13,0x0a, + 0x8a,0x0e,0xac,0xe7,0x1d,0xdc,0xe2,0x0f, + 0xcb,0x9e,0x8d,0xbc,0xd2,0xa2,0x44,0xe0, + } + }, + { 0xa001078, { + 0x2d,0x67,0xc7,0x35,0xca,0xef,0x2f,0x25, + 0x4c,0x45,0x93,0x3f,0x36,0x01,0x8c,0xce, + 0xa8,0x5b,0x07,0xd3,0xc1,0x35,0x3c,0x04, + 0x20,0xa2,0xfc,0xdc,0xe6,0xce,0x26,0x3e, + } + }, + { 0xa001079, { + 0x43,0xe2,0x05,0x9c,0xfd,0xb7,0x5b,0xeb, + 0x5b,0xe9,0xeb,0x3b,0x96,0xf4,0xe4,0x93, + 0x73,0x45,0x3e,0xac,0x8d,0x3b,0xe4,0xdb, + 0x10,0x31,0xc1,0xe4,0xa2,0xd0,0x5a,0x8a, + } + }, + { 0xa00107a, { + 0x5f,0x92,0xca,0xff,0xc3,0x59,0x22,0x5f, + 0x02,0xa0,0x91,0x3b,0x4a,0x45,0x10,0xfd, + 0x19,0xe1,0x8a,0x6d,0x9a,0x92,0xc1,0x3f, + 0x75,0x78,0xac,0x78,0x03,0x1d,0xdb,0x18, + } + }, + { 0xa001143, { + 0x56,0xca,0xf7,0x43,0x8a,0x4c,0x46,0x80, + 0xec,0xde,0xe5,0x9c,0x50,0x84,0x9a,0x42, + 0x27,0xe5,0x51,0x84,0x8f,0x19,0xc0,0x8d, + 0x0c,0x25,0xb4,0xb0,0x8f,0x10,0xf3,0xf8, + } + }, + { 0xa001144, { + 0x42,0xd5,0x9b,0xa7,0xd6,0x15,0x29,0x41, + 0x61,0xc4,0x72,0x3f,0xf3,0x06,0x78,0x4b, + 0x65,0xf3,0x0e,0xfa,0x9c,0x87,0xde,0x25, + 0xbd,0xb3,0x9a,0xf4,0x75,0x13,0x53,0xdc, + } + }, + { 0xa00115d, { + 0xd4,0xc4,0x49,0x36,0x89,0x0b,0x47,0xdd, + 0xfb,0x2f,0x88,0x3b,0x5f,0xf2,0x8e,0x75, + 0xc6,0x6c,0x37,0x5a,0x90,0x25,0x94,0x3e, + 0x36,0x9c,0xae,0x02,0x38,0x6c,0xf5,0x05, + } + }, + { 0xa001173, { + 0x28,0xbb,0x9b,0xd1,0xa0,0xa0,0x7e,0x3a, + 0x59,0x20,0xc0,0xa9,0xb2,0x5c,0xc3,0x35, + 0x53,0x89,0xe1,0x4c,0x93,0x2f,0x1d,0xc3, + 0xe5,0xf7,0xf3,0xc8,0x9b,0x61,0xaa,0x9e, + } + }, + { 0xa0011a8, { + 0x97,0xc6,0x16,0x65,0x99,0xa4,0x85,0x3b, + 0xf6,0xce,0xaa,0x49,0x4a,0x3a,0xc5,0xb6, + 0x78,0x25,0xbc,0x53,0xaf,0x5d,0xcf,0xf4, + 0x23,0x12,0xbb,0xb1,0xbc,0x8a,0x02,0x2e, + } + }, + { 0xa0011ce, { + 0xcf,0x1c,0x90,0xa3,0x85,0x0a,0xbf,0x71, + 0x94,0x0e,0x80,0x86,0x85,0x4f,0xd7,0x86, + 0xae,0x38,0x23,0x28,0x2b,0x35,0x9b,0x4e, + 0xfe,0xb8,0xcd,0x3d,0x3d,0x39,0xc9,0x6a, + } + }, + { 0xa0011d1, { + 0xdf,0x0e,0xca,0xde,0xf6,0xce,0x5c,0x1e, + 0x4c,0xec,0xd7,0x71,0x83,0xcc,0xa8,0x09, + 0xc7,0xc5,0xfe,0xb2,0xf7,0x05,0xd2,0xc5, + 0x12,0xdd,0xe4,0xf3,0x92,0x1c,0x3d,0xb8, + } + }, + { 0xa0011d3, { + 0x91,0xe6,0x10,0xd7,0x57,0xb0,0x95,0x0b, + 0x9a,0x24,0xee,0xf7,0xcf,0x56,0xc1,0xa6, + 0x4a,0x52,0x7d,0x5f,0x9f,0xdf,0xf6,0x00, + 0x65,0xf7,0xea,0xe8,0x2a,0x88,0xe2,0x26, + } + }, + { 0xa0011d5, { + 0xed,0x69,0x89,0xf4,0xeb,0x64,0xc2,0x13, + 0xe0,0x51,0x1f,0x03,0x26,0x52,0x7d,0xb7, + 0x93,0x5d,0x65,0xca,0xb8,0x12,0x1d,0x62, + 0x0d,0x5b,0x65,0x34,0x69,0xb2,0x62,0x21, + } + }, + { 0xa0011d7, { + 0x35,0x07,0xcd,0x40,0x94,0xbc,0x81,0x6b, + 0xfc,0x61,0x56,0x1a,0xe2,0xdb,0x96,0x12, + 0x1c,0x1c,0x31,0xb1,0x02,0x6f,0xe5,0xd2, + 0xfe,0x1b,0x04,0x03,0x2c,0x8f,0x4c,0x36, + } + }, + { 0xa001223, { + 0xfb,0x32,0x5f,0xc6,0x83,0x4f,0x8c,0xb8, + 0xa4,0x05,0xf9,0x71,0x53,0x01,0x16,0xc4, + 0x83,0x75,0x94,0xdd,0xeb,0x7e,0xb7,0x15, + 0x8e,0x3b,0x50,0x29,0x8a,0x9c,0xcc,0x45, + } + }, + { 0xa001224, { + 0x0e,0x0c,0xdf,0xb4,0x89,0xee,0x35,0x25, + 0xdd,0x9e,0xdb,0xc0,0x69,0x83,0x0a,0xad, + 0x26,0xa9,0xaa,0x9d,0xfc,0x3c,0xea,0xf9, + 0x6c,0xdc,0xd5,0x6d,0x8b,0x6e,0x85,0x4a, + } + }, + { 0xa001227, { + 0xab,0xc6,0x00,0x69,0x4b,0x50,0x87,0xad, + 0x5f,0x0e,0x8b,0xea,0x57,0x38,0xce,0x1d, + 0x0f,0x75,0x26,0x02,0xf6,0xd6,0x96,0xe9, + 0x87,0xb9,0xd6,0x20,0x27,0x7c,0xd2,0xe0, + } + }, + { 0xa001229, { + 0x7f,0x49,0x49,0x48,0x46,0xa5,0x50,0xa6, + 0x28,0x89,0x98,0xe2,0x9e,0xb4,0x7f,0x75, + 0x33,0xa7,0x04,0x02,0xe4,0x82,0xbf,0xb4, + 0xa5,0x3a,0xba,0x24,0x8d,0x31,0x10,0x1d, + } + }, + { 0xa00122e, { + 0x56,0x94,0xa9,0x5d,0x06,0x68,0xfe,0xaf, + 0xdf,0x7a,0xff,0x2d,0xdf,0x74,0x0f,0x15, + 0x66,0xfb,0x00,0xb5,0x51,0x97,0x9b,0xfa, + 0xcb,0x79,0x85,0x46,0x25,0xb4,0xd2,0x10, + } + }, + { 0xa001231, { + 0x0b,0x46,0xa5,0xfc,0x18,0x15,0xa0,0x9e, + 0xa6,0xdc,0xb7,0xff,0x17,0xf7,0x30,0x64, + 0xd4,0xda,0x9e,0x1b,0xc3,0xfc,0x02,0x3b, + 0xe2,0xc6,0x0e,0x41,0x54,0xb5,0x18,0xdd, + } + }, + { 0xa001234, { + 0x88,0x8d,0xed,0xab,0xb5,0xbd,0x4e,0xf7, + 0x7f,0xd4,0x0e,0x95,0x34,0x91,0xff,0xcc, + 0xfb,0x2a,0xcd,0xf7,0xd5,0xdb,0x4c,0x9b, + 0xd6,0x2e,0x73,0x50,0x8f,0x83,0x79,0x1a, + } + }, + { 0xa001236, { + 0x3d,0x30,0x00,0xb9,0x71,0xba,0x87,0x78, + 0xa8,0x43,0x55,0xc4,0x26,0x59,0xcf,0x9d, + 0x93,0xce,0x64,0x0e,0x8b,0x72,0x11,0x8b, + 0xa3,0x8f,0x51,0xe9,0xca,0x98,0xaa,0x25, + } + }, + { 0xa001238, { + 0x72,0xf7,0x4b,0x0c,0x7d,0x58,0x65,0xcc, + 0x00,0xcc,0x57,0x16,0x68,0x16,0xf8,0x2a, + 0x1b,0xb3,0x8b,0xe1,0xb6,0x83,0x8c,0x7e, + 0xc0,0xcd,0x33,0xf2,0x8d,0xf9,0xef,0x59, + } + }, + { 0xa00123b, { + 0xef,0xa1,0x1e,0x71,0xf1,0xc3,0x2c,0xe2, + 0xc3,0xef,0x69,0x41,0x7a,0x54,0xca,0xc3, + 0x8f,0x62,0x84,0xee,0xc2,0x39,0xd9,0x28, + 0x95,0xa7,0x12,0x49,0x1e,0x30,0x71,0x72, + } + }, + { 0xa00820c, { + 0xa8,0x0c,0x81,0xc0,0xa6,0x00,0xe7,0xf3, + 0x5f,0x65,0xd3,0xb9,0x6f,0xea,0x93,0x63, + 0xf1,0x8c,0x88,0x45,0xd7,0x82,0x80,0xd1, + 0xe1,0x3b,0x8d,0xb2,0xf8,0x22,0x03,0xe2, + } + }, + { 0xa00820d, { + 0xf9,0x2a,0xc0,0xf4,0x9e,0xa4,0x87,0xa4, + 0x7d,0x87,0x00,0xfd,0xab,0xda,0x19,0xca, + 0x26,0x51,0x32,0xc1,0x57,0x91,0xdf,0xc1, + 0x05,0xeb,0x01,0x7c,0x5a,0x95,0x21,0xb7, + } + }, + { 0xa10113e, { + 0x05,0x3c,0x66,0xd7,0xa9,0x5a,0x33,0x10, + 0x1b,0xf8,0x9c,0x8f,0xed,0xfc,0xa7,0xa0, + 0x15,0xe3,0x3f,0x4b,0x1d,0x0d,0x0a,0xd5, + 0xfa,0x90,0xc4,0xed,0x9d,0x90,0xaf,0x53, + } + }, + { 0xa101144, { + 0xb3,0x0b,0x26,0x9a,0xf8,0x7c,0x02,0x26, + 0x35,0x84,0x53,0xa4,0xd3,0x2c,0x7c,0x09, + 0x68,0x7b,0x96,0xb6,0x93,0xef,0xde,0xbc, + 0xfd,0x4b,0x15,0xd2,0x81,0xd3,0x51,0x47, + } + }, + { 0xa101148, { + 0x20,0xd5,0x6f,0x40,0x4a,0xf6,0x48,0x90, + 0xc2,0x93,0x9a,0xc2,0xfd,0xac,0xef,0x4f, + 0xfa,0xc0,0x3d,0x92,0x3c,0x6d,0x01,0x08, + 0xf1,0x5e,0xb0,0xde,0xb4,0x98,0xae,0xc4, + } + }, + { 0xa10114c, { + 0x9e,0xb6,0xa2,0xd9,0x87,0x38,0xc5,0x64, + 0xd8,0x88,0xfa,0x78,0x98,0xf9,0x6f,0x74, + 0x39,0x90,0x1b,0xa5,0xcf,0x5e,0xb4,0x2a, + 0x02,0xff,0xd4,0x8c,0x71,0x8b,0xe2,0xc0, + } + }, + { 0xa10123e, { + 0x03,0xb9,0x2c,0x76,0x48,0x93,0xc9,0x18, + 0xfb,0x56,0xfd,0xf7,0xe2,0x1d,0xca,0x4d, + 0x1d,0x13,0x53,0x63,0xfe,0x42,0x6f,0xfc, + 0x19,0x0f,0xf1,0xfc,0xa7,0xdd,0x89,0x1b, + } + }, + { 0xa101244, { + 0x71,0x56,0xb5,0x9f,0x21,0xbf,0xb3,0x3c, + 0x8c,0xd7,0x36,0xd0,0x34,0x52,0x1b,0xb1, + 0x46,0x2f,0x04,0xf0,0x37,0xd8,0x1e,0x72, + 0x24,0xa2,0x80,0x84,0x83,0x65,0x84,0xc0, + } + }, + { 0xa101248, { + 0xed,0x3b,0x95,0xa6,0x68,0xa7,0x77,0x3e, + 0xfc,0x17,0x26,0xe2,0x7b,0xd5,0x56,0x22, + 0x2c,0x1d,0xef,0xeb,0x56,0xdd,0xba,0x6e, + 0x1b,0x7d,0x64,0x9d,0x4b,0x53,0x13,0x75, + } + }, + { 0xa10124c, { + 0x29,0xea,0xf1,0x2c,0xb2,0xe4,0xef,0x90, + 0xa4,0xcd,0x1d,0x86,0x97,0x17,0x61,0x46, + 0xfc,0x22,0xcb,0x57,0x75,0x19,0xc8,0xcc, + 0x0c,0xf5,0xbc,0xac,0x81,0x9d,0x9a,0xd2, + } + }, + { 0xa108108, { + 0xed,0xc2,0xec,0xa1,0x15,0xc6,0x65,0xe9, + 0xd0,0xef,0x39,0xaa,0x7f,0x55,0x06,0xc6, + 0xf5,0xd4,0x3f,0x7b,0x14,0xd5,0x60,0x2c, + 0x28,0x1e,0x9c,0x59,0x69,0x99,0x4d,0x16, + } + }, + { 0xa108109, { + 0x85,0xb4,0xbd,0x7c,0x49,0xa7,0xbd,0xfa, + 0x49,0x36,0x80,0x81,0xc5,0xb7,0x39,0x1b, + 0x9a,0xaa,0x50,0xde,0x9b,0xe9,0x32,0x35, + 0x42,0x7e,0x51,0x4f,0x52,0x2c,0x28,0x59, + } + }, + { 0xa20102d, { + 0xf9,0x6e,0xf2,0x32,0xd3,0x0f,0x5f,0x11, + 0x59,0xa1,0xfe,0xcc,0xcd,0x9b,0x42,0x89, + 0x8b,0x89,0x2f,0xb5,0xbb,0x82,0xef,0x23, + 0x8c,0xe9,0x19,0x3e,0xcc,0x3f,0x7b,0xb4, + } + }, + { 0xa20102e, { + 0xbe,0x1f,0x32,0x04,0x0d,0x3c,0x9c,0xdd, + 0xe1,0xa4,0xbf,0x76,0x3a,0xec,0xc2,0xf6, + 0x11,0x00,0xa7,0xaf,0x0f,0xe5,0x02,0xc5, + 0x54,0x3a,0x1f,0x8c,0x16,0xb5,0xff,0xbe, + } + }, + { 0xa201210, { + 0xe8,0x6d,0x51,0x6a,0x8e,0x72,0xf3,0xfe, + 0x6e,0x16,0xbc,0x62,0x59,0x40,0x17,0xe9, + 0x6d,0x3d,0x0e,0x6b,0xa7,0xac,0xe3,0x68, + 0xf7,0x55,0xf0,0x13,0xbb,0x22,0xf6,0x41, + } + }, + { 0xa201211, { + 0x69,0xa1,0x17,0xec,0xd0,0xf6,0x6c,0x95, + 0xe2,0x1e,0xc5,0x59,0x1a,0x52,0x0a,0x27, + 0xc4,0xed,0xd5,0x59,0x1f,0xbf,0x00,0xff, + 0x08,0x88,0xb5,0xe1,0x12,0xb6,0xcc,0x27, + } + }, + { 0xa404107, { + 0xbb,0x04,0x4e,0x47,0xdd,0x5e,0x26,0x45, + 0x1a,0xc9,0x56,0x24,0xa4,0x4c,0x82,0xb0, + 0x8b,0x0d,0x9f,0xf9,0x3a,0xdf,0xc6,0x81, + 0x13,0xbc,0xc5,0x25,0xe4,0xc5,0xc3,0x99, + } + }, + { 0xa404108, { + 0x69,0x67,0x43,0x06,0xf8,0x0c,0x62,0xdc, + 0xa4,0x21,0x30,0x4f,0x0f,0x21,0x2c,0xcb, + 0xcc,0x37,0xf1,0x1c,0xc3,0xf8,0x2f,0x19, + 0xdf,0x53,0x53,0x46,0xb1,0x15,0xea,0x00, + } + }, + { 0xa500011, { + 0x23,0x3d,0x70,0x7d,0x03,0xc3,0xc4,0xf4, + 0x2b,0x82,0xc6,0x05,0xda,0x80,0x0a,0xf1, + 0xd7,0x5b,0x65,0x3a,0x7d,0xab,0xdf,0xa2, + 0x11,0x5e,0x96,0x7e,0x71,0xe9,0xfc,0x74, + } + }, + { 0xa500012, { + 0xeb,0x74,0x0d,0x47,0xa1,0x8e,0x09,0xe4, + 0x93,0x4c,0xad,0x03,0x32,0x4c,0x38,0x16, + 0x10,0x39,0xdd,0x06,0xaa,0xce,0xd6,0x0f, + 0x62,0x83,0x9d,0x8e,0x64,0x55,0xbe,0x63, + } + }, + { 0xa601209, { + 0x66,0x48,0xd4,0x09,0x05,0xcb,0x29,0x32, + 0x66,0xb7,0x9a,0x76,0xcd,0x11,0xf3,0x30, + 0x15,0x86,0xcc,0x5d,0x97,0x0f,0xc0,0x46, + 0xe8,0x73,0xe2,0xd6,0xdb,0xd2,0x77,0x1d, + } + }, + { 0xa60120a, { + 0x0c,0x8b,0x3d,0xfd,0x52,0x52,0x85,0x7d, + 0x20,0x3a,0xe1,0x7e,0xa4,0x21,0x3b,0x7b, + 0x17,0x86,0xae,0xac,0x13,0xb8,0x63,0x9d, + 0x06,0x01,0xd0,0xa0,0x51,0x9a,0x91,0x2c, + } + }, + { 0xa704107, { + 0xf3,0xc6,0x58,0x26,0xee,0xac,0x3f,0xd6, + 0xce,0xa1,0x72,0x47,0x3b,0xba,0x2b,0x93, + 0x2a,0xad,0x8e,0x6b,0xea,0x9b,0xb7,0xc2, + 0x64,0x39,0x71,0x8c,0xce,0xe7,0x41,0x39, + } + }, + { 0xa704108, { + 0xd7,0x55,0x15,0x2b,0xfe,0xc4,0xbc,0x93, + 0xec,0x91,0xa0,0xae,0x45,0xb7,0xc3,0x98, + 0x4e,0xff,0x61,0x77,0x88,0xc2,0x70,0x49, + 0xe0,0x3a,0x1d,0x84,0x38,0x52,0xbf,0x5a, + } + }, + { 0xa705206, { + 0x8d,0xc0,0x76,0xbd,0x58,0x9f,0x8f,0xa4, + 0x12,0x9d,0x21,0xfb,0x48,0x21,0xbc,0xe7, + 0x67,0x6f,0x04,0x18,0xae,0x20,0x87,0x4b, + 0x03,0x35,0xe9,0xbe,0xfb,0x06,0xdf,0xfc, + } + }, + { 0xa705208, { + 0x30,0x1d,0x55,0x24,0xbc,0x6b,0x5a,0x19, + 0x0c,0x7d,0x1d,0x74,0xaa,0xd1,0xeb,0xd2, + 0x16,0x62,0xf7,0x5b,0xe1,0x1f,0x18,0x11, + 0x5c,0xf0,0x94,0x90,0x26,0xec,0x69,0xff, + } + }, + { 0xa708007, { + 0x6b,0x76,0xcc,0x78,0xc5,0x8a,0xa3,0xe3, + 0x32,0x2d,0x79,0xe4,0xc3,0x80,0xdb,0xb2, + 0x07,0xaa,0x3a,0xe0,0x57,0x13,0x72,0x80, + 0xdf,0x92,0x73,0x84,0x87,0x3c,0x73,0x93, + } + }, + { 0xa708008, { + 0x08,0x6e,0xf0,0x22,0x4b,0x8e,0xc4,0x46, + 0x58,0x34,0xe6,0x47,0xa2,0x28,0xfd,0xab, + 0x22,0x3d,0xdd,0xd8,0x52,0x9e,0x1d,0x16, + 0xfa,0x01,0x68,0x14,0x79,0x3e,0xe8,0x6b, + } + }, + { 0xa70c005, { + 0x88,0x5d,0xfb,0x79,0x64,0xd8,0x46,0x3b, + 0x4a,0x83,0x8e,0x77,0x7e,0xcf,0xb3,0x0f, + 0x1f,0x1f,0xf1,0x97,0xeb,0xfe,0x56,0x55, + 0xee,0x49,0xac,0xe1,0x8b,0x13,0xc5,0x13, + } + }, + { 0xa70c008, { + 0x0f,0xdb,0x37,0xa1,0x10,0xaf,0xd4,0x21, + 0x94,0x0d,0xa4,0xa2,0xe9,0x86,0x6c,0x0e, + 0x85,0x7c,0x36,0x30,0xa3,0x3a,0x78,0x66, + 0x18,0x10,0x60,0x0d,0x78,0x3d,0x44,0xd0, + } + }, + { 0xaa00116, { + 0xe8,0x4c,0x2c,0x88,0xa1,0xac,0x24,0x63, + 0x65,0xe5,0xaa,0x2d,0x16,0xa9,0xc3,0xf5, + 0xfe,0x1d,0x5e,0x65,0xc7,0xaa,0x92,0x4d, + 0x91,0xee,0x76,0xbb,0x4c,0x66,0x78,0xc9, + } + }, + { 0xaa00212, { + 0xbd,0x57,0x5d,0x0a,0x0a,0x30,0xc1,0x75, + 0x95,0x58,0x5e,0x93,0x02,0x28,0x43,0x71, + 0xed,0x42,0x29,0xc8,0xec,0x34,0x2b,0xb2, + 0x1a,0x65,0x4b,0xfe,0x07,0x0f,0x34,0xa1, + } + }, + { 0xaa00213, { + 0xed,0x58,0xb7,0x76,0x81,0x7f,0xd9,0x3a, + 0x1a,0xff,0x8b,0x34,0xb8,0x4a,0x99,0x0f, + 0x28,0x49,0x6c,0x56,0x2b,0xdc,0xb7,0xed, + 0x96,0xd5,0x9d,0xc1,0x7a,0xd4,0x51,0x9b, + } + }, + { 0xaa00215, { + 0x55,0xd3,0x28,0xcb,0x87,0xa9,0x32,0xe9, + 0x4e,0x85,0x4b,0x7c,0x6b,0xd5,0x7c,0xd4, + 0x1b,0x51,0x71,0x3a,0x0e,0x0b,0xdc,0x9b, + 0x68,0x2f,0x46,0xee,0xfe,0xc6,0x6d,0xef, + } + }, + { 0xaa00216, { + 0x79,0xfb,0x5b,0x9f,0xb6,0xe6,0xa8,0xf5, + 0x4e,0x7c,0x4f,0x8e,0x1d,0xad,0xd0,0x08, + 0xc2,0x43,0x7c,0x8b,0xe6,0xdb,0xd0,0xd2, + 0xe8,0x39,0x26,0xc1,0xe5,0x5a,0x48,0xf1, + } + }, +}; diff --git a/tests/metal/amd-linux/SOURCE b/tests/metal/amd-linux/SOURCE new file mode 100644 index 00000000000..0a23fe3a8cf --- /dev/null +++ b/tests/metal/amd-linux/SOURCE @@ -0,0 +1,16 @@ +Linux's microcode reading of the AMD bench (a Ryzen 7 4800U, family 23 model +96 stepping 1) under that machine's Fedora, `7.2.4-ogc3.1.fc44.x86_64` with +`amd-ucode-firmware-20260810`, whose `microcode_amd_fam17h.bin` is the +container `kernel/microcode/amd-ucode/` pins, byte for byte. Taken over ssh as +the bench's unprivileged user by the orchestrator, as the tools printed it, +and recorded at https://github.com/ToyOSOrg/evidence/blob/9e5f88e5e5de5dadba86d6939a158c728752dec8/amd-bench-linux-microcode.txt +with its command: + + journalctl -k -b 0 --no-pager | grep -i microcode | sed -E "s/^.*kernel: //"; + grep -m1 "^microcode" /proc/cpuinfo; grep -m1 "cpu family" /proc/cpuinfo; + grep -m1 "^model\s" /proc/cpuinfo; grep -m1 stepping /proc/cpuinfo; + rpm -q amd-ucode-firmware; uname -r + + microcode.txt its output: the revision the firmware ran and the one Linux + loaded early over it. `microcode_on_metal` in + `tests/toyos.rs` holds every CPU's `microcode:` line to it. diff --git a/tests/metal/amd-linux/microcode.txt b/tests/metal/amd-linux/microcode.txt new file mode 100644 index 00000000000..ae4a377cb90 --- /dev/null +++ b/tests/metal/amd-linux/microcode.txt @@ -0,0 +1,8 @@ +microcode: Current revision: 0x0860010d +microcode: Updated early from: 0x08600106 +microcode : 0x860010d +cpu family : 23 +model : 96 +stepping : 1 +amd-ucode-firmware-20260810-1.fc44.noarch +7.2.4-ogc3.1.fc44.x86_64 diff --git a/tests/metal/src/lib.rs b/tests/metal/src/lib.rs index 393d96fccd2..16e4e05c1a7 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, whose payload is the + // revision the CPU already runs, so the boot stops at the read-back. An + // update lasts until a hard reset (SDM Vol. 3A §12.11.6.1), and the panic's + // reset is one, so the machine is what it was. + "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 4bb3bc94a32..3b9ef4da9ac 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -907,6 +907,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, held to Linux's reading of the same machine: + // `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 @@ -5466,6 +5472,113 @@ 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, and each held to Linux's reading +/// of the same machine, which is the exit's "at least as current as Linux's". +/// +/// - **The T14** (`kernel/cpuvuln/fixtures/t14/cpuinfo.txt`): Linux runs 0xbe +/// on all eight CPUs, the update its firmware loads and Intel's newest for +/// it, so every CPU reads platform 7 and finds that revision current. +/// - **The AMD bench** (`tests/metal/amd-linux/microcode.txt`): Linux updated +/// early from the firmware's revision to linux-firmware's patch, so the BSP +/// finds the firmware's, and every CPU loads the patch and runs it. A thread +/// whose core-mate loaded before it finds either, whether one engine or two +/// serve the core. +fn microcode_on_metal(log: &str, cpus: u32) -> Result<(), String> { + const T14: u32 = 0x0008_06c1; + const RENOIR: 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 hex = |word: &str| u32::from_str_radix(word.trim_start_matches("0x"), 16).map_err(|e| format!("{word:?}: {e}")); + let held: Box Option> = match signature { + T14 => { + let linux: Vec<&str> = include_str!("../kernel/cpuvuln/fixtures/t14/cpuinfo.txt") + .lines() + .filter_map(|line| line.strip_prefix("microcode\t: ")) + .collect::>() + .into_iter() + .collect(); + let [linux] = linux[..] else { + return Err(format!("Linux's reading of the T14 runs more than one revision: {linux:?}")); + }; + let linux = hex(linux)?; + Box::new(move |line| { + ((line.platform, line.found, line.outcome.as_str(), line.running) != (Some(7), linux, "current", linux)) + .then(|| format!("Linux runs {linux:#x} on platform 7, the newest Intel names it")) + }) + } + RENOIR => { + let linux = include_str!("metal/amd-linux/microcode.txt"); + let said = |label: &str| { + linux.lines().find_map(|line| line.strip_prefix(label)).ok_or_else(|| format!("Linux's reading says no {label:?}")) + }; + let (firmware, current) = + (hex(said("microcode: Updated early from: ")?)?, hex(said("microcode: Current revision: ")?)?); + Box::new(move |line| { + if line.cpu == 0 && line.found != firmware { + Some(format!("its firmware runs {firmware:#x}, which the BSP finds")) + } else if ![firmware, current].contains(&line.found) { + Some(format!("a CPU finds its firmware's {firmware:#x} or its core's {current:#x}")) + } else { + ((line.outcome.as_str(), line.running) != ("loaded", current)) + .then(|| format!("Linux loads {current:#x} over the firmware's {firmware:#x}")) + } + }) + } + other => return Err(format!("no Linux reading of signature {other:#x} is committed")), + }; + if let Some((line, why)) = lines.iter().find_map(|line| held(line).map(|why| (line, why))) { + return Err(format!("{line:?}: {why}")); + } + eprintln!(" [microcode] {} CPUs of signature {signature:#x} hold Linux's reading", lines.len()); + Ok(()) +} + +/// One `microcode:` line, the fields [`microcode_on_metal`] holds. +#[derive(Debug)] +struct MicrocodeLine { + cpu: u32, + signature: u32, + platform: Option, + found: u32, + /// `loaded`, `current`, or `loads` for one that loads nothing. + outcome: String, + running: u32, +} + +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:?}"); + out.push(MicrocodeLine { + cpu: words.first().and_then(|id| id.parse().ok()).ok_or_else(unreadable)?, + signature: hex("signature").ok_or_else(unreadable)?, + platform: field("platform").map(|p| p.parse().map_err(|_| unreadable())).transpose()?, + found: hex("found").ok_or_else(unreadable)?, + outcome: words + .iter() + .position(|w| *w == "found") + .and_then(|at| words.get(at + 2)) + .ok_or_else(unreadable)? + .to_string(), + running: hex("running").ok_or_else(unreadable)?, + }); + } + Ok(out) +} + /// Every CPU's `CR0` and `CR4`, against what a CPU running this kernel must /// hold. /// From 729b168cd64b14e587216cf4a719912d99d51261 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 12 Oct 2026 17:32:12 +0200 Subject: [PATCH 2/6] The microcode row's judge names Linux's reading of a machine by an enum Clippy's type_complexity refused the boxed closure it held per signature. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- tests/toyos.rs | 58 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/tests/toyos.rs b/tests/toyos.rs index 3b9ef4da9ac..c775c4edd47 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -5498,50 +5498,62 @@ fn microcode_on_metal(log: &str, cpus: u32) -> Result<(), String> { return Err(format!("cpu0 is signature {signature:#x} and {other:?} is not")); } let hex = |word: &str| u32::from_str_radix(word.trim_start_matches("0x"), 16).map_err(|e| format!("{word:?}: {e}")); - let held: Box Option> = match signature { + let linux = match signature { T14 => { - let linux: Vec<&str> = include_str!("../kernel/cpuvuln/fixtures/t14/cpuinfo.txt") + let revisions: Vec<&str> = include_str!("../kernel/cpuvuln/fixtures/t14/cpuinfo.txt") .lines() .filter_map(|line| line.strip_prefix("microcode\t: ")) .collect::>() .into_iter() .collect(); - let [linux] = linux[..] else { - return Err(format!("Linux's reading of the T14 runs more than one revision: {linux:?}")); + let [revision] = revisions[..] else { + return Err(format!("Linux's reading of the T14 runs more than one revision: {revisions:?}")); }; - let linux = hex(linux)?; - Box::new(move |line| { - ((line.platform, line.found, line.outcome.as_str(), line.running) != (Some(7), linux, "current", linux)) - .then(|| format!("Linux runs {linux:#x} on platform 7, the newest Intel names it")) - }) + Linux::Current(hex(revision)?) } RENOIR => { let linux = include_str!("metal/amd-linux/microcode.txt"); let said = |label: &str| { linux.lines().find_map(|line| line.strip_prefix(label)).ok_or_else(|| format!("Linux's reading says no {label:?}")) }; - let (firmware, current) = - (hex(said("microcode: Updated early from: ")?)?, hex(said("microcode: Current revision: ")?)?); - Box::new(move |line| { - if line.cpu == 0 && line.found != firmware { - Some(format!("its firmware runs {firmware:#x}, which the BSP finds")) - } else if ![firmware, current].contains(&line.found) { - Some(format!("a CPU finds its firmware's {firmware:#x} or its core's {current:#x}")) - } else { - ((line.outcome.as_str(), line.running) != ("loaded", current)) - .then(|| format!("Linux loads {current:#x} over the firmware's {firmware:#x}")) - } - }) + Linux::Loaded { + firmware: hex(said("microcode: Updated early from: ")?)?, + current: hex(said("microcode: Current revision: ")?)?, + } } other => return Err(format!("no Linux reading of signature {other:#x} is committed")), }; - if let Some((line, why)) = lines.iter().find_map(|line| held(line).map(|why| (line, why))) { - return Err(format!("{line:?}: {why}")); + for line in &lines { + let why = match linux { + Linux::Current(linux) => ((line.platform, line.found, line.outcome.as_str(), line.running) + != (Some(7), linux, "current", linux)) + .then(|| format!("Linux runs {linux:#x} on platform 7, the newest Intel names it")), + Linux::Loaded { firmware, .. } if line.cpu == 0 && line.found != firmware => { + Some(format!("its firmware runs {firmware:#x}, which the BSP finds")) + } + Linux::Loaded { firmware, current } if ![firmware, current].contains(&line.found) => { + Some(format!("a CPU finds its firmware's {firmware:#x} or its core's {current:#x}")) + } + Linux::Loaded { firmware, current } => ((line.outcome.as_str(), line.running) != ("loaded", current)) + .then(|| format!("Linux loads {current:#x} over the firmware's {firmware:#x}")), + }; + if let Some(why) = why { + return Err(format!("{line:?}: {why}")); + } } eprintln!(" [microcode] {} CPUs of signature {signature:#x} hold Linux's reading", lines.len()); Ok(()) } +/// Linux's reading of a machine [`microcode_on_metal`] holds a boot to. +#[derive(Clone, Copy)] +enum Linux { + /// It runs this revision, which its firmware loaded. + Current(u32), + /// It loaded `current` early over the firmware's. + Loaded { firmware: u32, current: u32 }, +} + /// One `microcode:` line, the fields [`microcode_on_metal`] holds. #[derive(Debug)] struct MicrocodeLine { From bbac92034a10319c5830d16136d858bcca467d9e Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 01:14:24 +0200 Subject: [PATCH 3/6] A CPU that does not take its update is a refused line, and nothing cites Linux or Xen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ` 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 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- Cargo.lock | 1 - NOTICE | 19 +- ...ster-is-read-holds-the-machine-for-good.md | 35 ++ issues/the-kernel-loads-no-cpu-microcode.md | 15 +- kernel/microcode/Cargo.toml | 5 - kernel/microcode/src/amd.rs | 59 +- kernel/microcode/src/amd/tests.rs | 53 +- kernel/microcode/src/lib.rs | 32 +- kernel/microcode/src/tests.rs | 3 - kernel/src/arch/x86_64/microcode.rs | 60 +- tests/checks.rs | 29 +- tests/linux/amd_shas.c | 556 ------------------ tests/metal/amd-linux/SOURCE | 16 - tests/metal/amd-linux/microcode.txt | 8 - tests/metal/src/lib.rs | 8 +- tests/toyos.rs | 90 +-- 16 files changed, 166 insertions(+), 823 deletions(-) create mode 100644 issues/a-panic-before-the-reset-register-is-read-holds-the-machine-for-good.md delete mode 100644 tests/linux/amd_shas.c delete mode 100644 tests/metal/amd-linux/SOURCE delete mode 100644 tests/metal/amd-linux/microcode.txt diff --git a/Cargo.lock b/Cargo.lock index 6a33dfbd6cc..1914a0147f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4465,7 +4465,6 @@ name = "toyos-microcode" version = "0.1.0" dependencies = [ "toyos-cpuvuln", - "toyos-sha2", ] [[package]] diff --git a/NOTICE b/NOTICE index 9df72678190..859ee70f1a5 100644 --- a/NOTICE +++ b/NOTICE @@ -124,7 +124,8 @@ Inc." and "Copyright(C) 2005-2014 Simon Howard" over GPL "version 2 ... or any later version" — id's Doom source release by way of Chocolate Doom. The crate declares `GPL-2.0-only`, which is a narrowing a distributor may make. -No GPL code is linked into the kernel, the SDK, or any other program. +Nothing else in this repository is GPL, and no GPL code is linked into the +kernel, the SDK, or any other program. assets/JetBrainsMono-Regular.ttf — SIL Open Font License 1.1 @@ -462,22 +463,6 @@ redistributor's to obtain, AMD conveying none; and U.S. and European export law binds whoever uses it. -tests/linux/amd_shas.c — Linux's digests of AMD's microcode patches, GPL-2.0-only ---------------------------------------------------------------------------------- - - amd_shas.c sha256 98af9ffb8a6e39e9dc37c86028a88853a75b74f229bcb1dabf84b72f6700a156 - Upstream: https://github.com/torvalds/linux, - `arch/x86/kernel/cpu/microcode/amd_shas.c` at tag v7.2 - (commit 8d3ae59288f1e7d58d76558a6ee96d533bc5019f), byte for byte - SPDX-License-Identifier: GPL-2.0-only - -The SHA-256 of every AMD patch Linux loads on a CPU whose own signature check -EntrySign broke: the independent record `toyos-microcode`'s tests hold every -patch the kernel embeds to. The file carries no licence line of its own, so -the kernel's `COPYING` decides it. Test input only: nothing built from this -repository carries it, and no shipped package's directory holds it. - - tests/cavp/ — NIST's SHA-2 test vectors, CAVP SHAVS ---------------------------------------------------- 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-loads-no-cpu-microcode.md b/issues/the-kernel-loads-no-cpu-microcode.md index 6baea082462..2b8aa518577 100644 --- a/issues/the-kernel-loads-no-cpu-microcode.md +++ b/issues/the-kernel-loads-no-cpu-microcode.md @@ -12,16 +12,17 @@ Pull request #886 builds the loader (`toyos-microcode`, `kernel/src/arch/x86_64/microcode.rs`) and deletes this file once its exit holds on both benches. -**Exit**: the kernel loads current microcode early on every CPU, at least as -current as Linux's. +**Exit**: the kernel loads, early on every CPU, the newest update its +maker's embedded file holds for it, and a CPU that does not take one says so +and runs what it reads back. - host: every decision `toyos-microcode` takes is refused by name and red under its mutation; -- metal: the `microcode` row is green on the T14, whose eight CPUs find 0xbe - current on platform 7 as Linux runs them, and on the AMD bench, whose CPUs - each load 0x0860010d over the firmware's 0x08600106 as Linux does; the T14 - armed with `microcode-reseal`, and the AMD bench with its trigger write - removed, each stop at the read-back; +- metal: the `microcode` row is green on the T14, whose eight CPUs run + Intel's 0xbe for 0x806c1 platform 7, and on the AMD bench, whose CPUs each + run the embedded container's 0x0860010d; the T14 armed with + `microcode-reseal`, and the AMD bench with its trigger write removed, each + complete the boot and red the row on a `refused` line; - guest: every guest loads nothing, for its CPU reports a hypervisor. QEMU 11.1.1 sets CPUID.01H:ECX[31] under TCG as under KVM, read over QMP from a paused `qemu64` with the suite's own `-cpu` string diff --git a/kernel/microcode/Cargo.toml b/kernel/microcode/Cargo.toml index b899a4bc412..6effcc8c296 100644 --- a/kernel/microcode/Cargo.toml +++ b/kernel/microcode/Cargo.toml @@ -11,8 +11,3 @@ publish = false [dependencies] toyos-cpuvuln = { path = "../cpuvuln" } - -[dev-dependencies] -# Hashes the embedded container's patches for the test that holds them to -# Linux's digests. -toyos-sha2 = { path = "../../lib/sha2" } diff --git a/kernel/microcode/src/amd.rs b/kernel/microcode/src/amd.rs index 75050583f13..8a12be00a24 100644 --- a/kernel/microcode/src/amd.rs +++ b/kernel/microcode/src/amd.rs @@ -1,5 +1,7 @@ -//! AMD's microcode containers, as AMD's own parser (`amd_ucode_info.py`) and -//! Linux v7.2's `amd.c` read them; AMD's APM documents no patch loader. +//! AMD's microcode containers, as AMD's own parser (`amd_ucode_info.py`) +//! reads them. AMD's PPR for family 17h model 60h (55922 rev 3.06) documents +//! the patch level a CPU reports, MSR0000_008B, one per core; it documents +//! neither the container nor the loader. //! //! A container is a 12-byte header — [`MAGIC`], the type of its first //! section (0, the equivalence table) and that section's length — then the @@ -7,32 +9,21 @@ //! patch of that many bytes: a 64-byte header and its signed body. [`select`] //! validates every section and answers with the patch a CPU loads, or why it //! loads none. One malformed section refuses the whole container. A second -//! container behind the first is refused, by its magic standing where a -//! section's type does. +//! container behind the first, which AMD's parser reads on into, is refused +//! here, by its magic standing where a section's type does. //! -//! **A Zen patch names its CPU in its patch ID** (Linux `94838d230a6c`): a -//! patch names the CPU whose CPUID.01H:EAX its family, model and stepping -//! decode to, and the equivalence table is skipped, as Linux skips it from -//! family 0x17 up. +//! **A patch names its CPU in its patch ID**: the CPU whose CPUID.01H:EAX its +//! family, model and stepping decode to. The equivalence table, through which +//! AMD's parser names a patch's CPU, is not read. //! //! **Refused as unmodelled**, so that a container embedded later cannot load -//! what this loader does not understand: a chipset-specific patch, as Linux -//! refuses one; and a nonzero word at offset 12, which from Zen 3 on is the -//! lowest revision a patch may be loaded over (Xen's `check_min_rev`). +//! what this loader does not understand: a patch whose `nb_dev_id` or +//! `sb_dev_id` names a chipset, and one whose `mc_patch_data_checksum` is not +//! 0, AMD's parser's names for header words AMD documents no use of. //! //! **A patch at or above the revision the CPU runs loads**, an equal one -//! again on every thread, as Linux's early loader does (`amd.c:813-819,996`; -//! `e7ad18d1169c`: "there are microcode patches which do per-thread -//! modification … This is what the BIOS does too"). Xen refuses an equal -//! revision unless forced. -//! -//! **EntrySign's cutoff rule holds at build time.** Linux offers a CPU at or -//! below its cutoff revision only patches at or below that cutoff, and one -//! above it only patches above (`amd.c:554-572`). A patch below the running -//! revision never loads here, and every patch [`select`] can return out of -//! [`crate::AMD`] is held by a host test to its digest in Linux's -//! `amd_shas.c`, which records none above its CPU's cutoff: that test is the -//! rule's form here, and a container that drops it drops the rule. +//! again on every thread. No AMD document states either rule; the read-back +//! is the CPU's verdict on both. use crate::{dwords, Choice, Revision, Signature}; @@ -64,15 +55,13 @@ impl<'a> Patch<'a> { } /// The patch as its section holds it, header first. MSR C001_0020 is - /// written the linear address of its first byte (Linux `amd.c:700-705`). + /// written the linear address of its first byte. pub fn bytes(&self) -> &'a [u8] { self.bytes } - /// Whether the CPU that loads it must then drop the translations of its - /// first and last byte: family 0x17's loader leaves the mapping of a - /// patch's bytes in the TLB as uncacheable (Linux `c809b0d0e52d` and - /// `amd.c:707-718`, Xen at AMD's instruction). + /// Whether the CPU that loads it then drops the translations of its first + /// and last byte: on family 0x17 alone. No AMD document names the step. pub fn leaves_its_mapping_uncached(&self) -> bool { family(self.revision.0) == 0x17 } @@ -101,8 +90,8 @@ pub enum Refusal { Short(u32), /// The patch is for one chipset (`nb_dev_id`, `sb_dev_id`). Chipset { north: u32, south: u32 }, - /// The patch names the lowest revision it may be loaded over. - MinimumRevision(u32), + /// The patch's `mc_patch_data_checksum` is not 0. + DataChecksum(u32), } /// The patch in `container` a CPU with `signature` running `found` loads, or @@ -157,18 +146,18 @@ fn section(bytes: &[u8]) -> Result<(Patch<'_>, &[u8]), Refusal> { let need = SECTION.saturating_add(size as usize); let (bytes, rest) = rest.split_at_checked(size as usize).ok_or(Refusal::Truncated { need, have })?; let (patch, _) = bytes.split_first_chunk::().ok_or(Refusal::Short(size))?; - let [_, id, _, minimum, north, south, ..]: [u32; 16] = dwords(patch); + let [_, id, _, checksum, north, south, ..]: [u32; 16] = dwords(patch); if north != 0 || south != 0 { return Err(Refusal::Chipset { north, south }); } - if minimum != 0 { - return Err(Refusal::MinimumRevision(minimum)); + if checksum != 0 { + return Err(Refusal::DataChecksum(checksum)); } Ok((Patch { revision: Revision(id), bytes }, rest)) } -/// The CPUID.01H:EAX a Zen patch ID names (Linux's `ucode_rev_to_cpuid`): its -/// stepping, model, extended model and extended family, over base family 0xf. +/// The CPUID.01H:EAX a Zen patch ID names: its stepping, model, extended model +/// and extended family, over base family 0xf. const fn named(id: u32) -> u32 { let (stepping, model) = (id >> 8 & 0xf, id >> 12 & 0xf); let (ext_model, ext_family) = (id >> 20 & 0xf, id >> 24); diff --git a/kernel/microcode/src/amd/tests.rs b/kernel/microcode/src/amd/tests.rs index f16b4a4d3a8..ca666caf6e4 100644 --- a/kernel/microcode/src/amd/tests.rs +++ b/kernel/microcode/src/amd/tests.rs @@ -1,4 +1,4 @@ -//! linux-firmware's container as AMD publishes it, and containers built here +//! The embedded container as AMD publishes it, and containers built here //! to its layout for every refusal and every arm the real one does not reach. #![allow( @@ -20,9 +20,8 @@ use super::*; /// The AMD bench's Ryzen 7 4800U: family 0x17, model 0x60, stepping 1. const RENOIR: Signature = Signature(0x0086_0f01); -/// Its firmware's revision, and the one linux-firmware's container holds for -/// it, which Linux loads early on the same machine -/// (`tests/metal/amd-linux/microcode.txt`). +/// The revision its firmware runs, and the one the embedded container holds +/// for it. const FIRMWARE: Revision = Revision(0x0860_0106); const CONTAINER: Revision = Revision(0x0860_010d); @@ -154,10 +153,10 @@ fn a_chipset_specific_patch_is_refused() { } #[test] -fn a_patch_naming_a_minimum_revision_is_refused() { - let mut minimum = patch(0x0860_010d); - put(&mut minimum, 12, 0x0860_0100); - assert_eq!(refusal(&container(&[(PATCH, &minimum)])).why, Refusal::MinimumRevision(0x0860_0100)); +fn a_patch_with_a_data_checksum_is_refused() { + let mut checksum = patch(0x0860_010d); + put(&mut checksum, 12, 0x0860_0100); + assert_eq!(refusal(&container(&[(PATCH, &checksum)])).why, Refusal::DataChecksum(0x0860_0100)); } #[test] @@ -168,41 +167,3 @@ fn a_malformed_section_after_the_cpus_own_refuses_the_container() { let at = HEADER + 16 + SECTION + 3200; assert_eq!(refusal(&built), Refused { at, why: Refusal::Chipset { north: 1, south: 0 } }); } - -/// Linux v7.2's `amd_shas.c` (`tests/linux/`, outside every shipped package's -/// directory; `NOTICE`): each patch ID's SHA-256, by its rows. -fn linux_digests() -> std::collections::BTreeMap { - let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/linux/amd_shas.c"); - let text = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display())); - let hex = |word: &str| u32::from_str_radix(word.trim_start_matches("0x"), 16).expect("a hex word"); - text.split(" { 0x") - .skip(1) - .map(|row| { - let mut words = row.split(|c: char| !c.is_ascii_alphanumeric()).filter(|w| !w.is_empty()); - let id = hex(words.next().expect("the row's patch ID")); - let digest: Vec = words.take(32).map(|w| hex(w) as u8).collect(); - (id, digest.try_into().unwrap_or_else(|d| panic!("patch {id:#x}'s digest is {d:?}"))) - }) - .collect() -} - -/// Every patch the embedded container holds, and so every patch `select` can -/// return out of it, is the patch Linux records: the build-time form of the -/// hash Linux checks at run time below EntrySign's cutoff. -#[test] -fn every_patch_in_the_embedded_container_has_linuxs_digest() { - let linux = linux_digests(); - let container = &crate::AMD.0; - let table = u32::from_le_bytes(container[8..HEADER].try_into().unwrap()) as usize; - let mut rest = &container[HEADER + table..]; - let mut patches = 0; - while !rest.is_empty() { - let (patch, after) = section(rest).expect("the embedded container is valid"); - let id = patch.revision().0; - let want = linux.get(&id).unwrap_or_else(|| panic!("amd_shas.c records no patch {id:#x}")); - assert_eq!(&toyos_sha2::Sha256::digest(patch.bytes()), want, "patch {id:#x}"); - patches += 1; - rest = after; - } - assert_eq!(patches, 7, "the container's README names seven patches"); -} diff --git a/kernel/microcode/src/lib.rs b/kernel/microcode/src/lib.rs index 4d42c1b9fb5..8471c753558 100644 --- a/kernel/microcode/src/lib.rs +++ b/kernel/microcode/src/lib.rs @@ -2,7 +2,7 @@ //! files the kernel loads it from, pure. //! //! [`intel`] reads Intel's update files as SDM Vol. 3A §12.11 defines them, -//! [`amd`] AMD's containers as AMD's own parser and Linux's `amd.c` read them. +//! [`amd`] AMD's containers as AMD's own parser reads them. //! Each validates the whole file, refuses it by name and offset where any //! part is malformed, and answers with the update a CPU loads, or why it loads //! none. Only the documented headers are read: an update's body is its @@ -13,14 +13,11 @@ //! //! [`INTEL`] and [`AMD`] are the files the kernel embeds, one per CPU ToyOS //! has a bench for, each as its maker publishes it and pinned by its digest -//! in `NOTICE`. **On AMD's families 0x17 and 0x19 and part of 0x1a that pin -//! is the only check**: at or below a per-CPU cutoff revision the CPU's own -//! signature check can be forged (EntrySign; Linux's `cpu_has_entrysign`). -//! Linux hashes a patch at run time against `amd_shas.c` because it loads a -//! file the kernel did not ship; these are inside the kernel image, so the -//! hash moves to build time, where a host test holds every patch -//! [`amd::select`] can return out of [`AMD`] to Linux's digest. A microcode -//! source outside the kernel image brings the runtime check with it. +//! in `NOTICE`. **On an AMD CPU whose own signature check can be made to take +//! a patch AMD did not sign (CVE-2024-36347, AMD-SB-7033), that pin is the +//! only check**: the files are inside the kernel image, so no other patch +//! reaches the loader, and a microcode source outside the image brings a +//! check of its own. //! //! Pure: no I/O, no allocation, no `unsafe`. The caller reads the CPU. //! @@ -61,7 +58,7 @@ pub struct Aligned(pub Bytes); /// `intel-ucode/06-8c-01`, the T14's i5-1135G7. pub static INTEL: &Aligned<[u8]> = &Aligned(*include_bytes!("../intel-ucode/06-8c-01")); -/// AMD's container for the CPUs ToyOS has a bench for: linux-firmware's +/// AMD's container for the CPUs ToyOS has a bench for: /// `amd-ucode/microcode_amd_fam17h.bin`, the AMD bench's Ryzen 7 4800U among /// them. pub static AMD: &Aligned<[u8]> = &Aligned(*include_bytes!("../amd-ucode/microcode_amd_fam17h.bin")); @@ -71,14 +68,12 @@ pub static AMD: &Aligned<[u8]> = &Aligned(*include_bytes!("../amd-ucode/microcod pub struct Signature(pub u32); /// A microcode revision: Intel's update revision, AMD's patch level, each the -/// dword its maker reports it in `IA32_BIOS_SIGN_ID` (MSR 8BH), compared -/// unsigned as Linux compares both. +/// dword its maker reports it in MSR 8BH, compared unsigned. #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub struct Revision(pub u32); impl Revision { - /// What a CPU whose loading is disabled, by a hypervisor or by a firmware - /// toggle, reports (Xen's `rev == ~0`). + /// The highest revision, which no update loads over. pub const DISABLED: Self = Self(u32::MAX); /// Intel's: MSR 8BH read after writing it 0 and executing CPUID.01H, whose @@ -87,7 +82,7 @@ impl Revision { Self((msr >> 32) as u32) } - /// AMD's: MSR 8BH's lower dword, the patch level (Linux `amd.c:336`). + /// AMD's: MSR 8BH's lower dword, the patch level (55922, MSR0000_008B). pub const fn from_patch_level(msr: u64) -> Self { Self(msr as u32) } @@ -137,8 +132,9 @@ impl Maker { pub enum Unloaded { /// No embedded file is from this vendor, so nothing of its CPU is read. Vendor(Vendor), - /// CPUID.01H:ECX[31]: a hypervisor owns this CPU's microcode (Linux - /// `core.c:114-139`). + /// 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, @@ -149,7 +145,7 @@ impl fmt::Display for Unloaded { f.write_str(match self { Self::Vendor(_) => "no embedded file is from this vendor", Self::Hypervisor => "a hypervisor owns this CPU's microcode", - Self::Disabled => "revision 0xffffffff says loading is disabled", + Self::Disabled => "revision 0xffffffff, which no update loads over", }) } } diff --git a/kernel/microcode/src/tests.rs b/kernel/microcode/src/tests.rs index 2385edf2cea..013169d4b08 100644 --- a/kernel/microcode/src/tests.rs +++ b/kernel/microcode/src/tests.rs @@ -20,9 +20,6 @@ fn a_guest_loads_nothing_whatever_revision_it_reports() { } } -/// Xen's reading of a revision of `~0`: loading is disabled, by a hypervisor -/// or a firmware toggle; an Intel CPU handed an update over it would refuse -/// it at the read-back. #[test] fn revision_0xffffffff_loads_nothing() { assert_eq!(may_load(0, Revision::DISABLED), Err(Unloaded::Disabled)); diff --git a/kernel/src/arch/x86_64/microcode.rs b/kernel/src/arch/x86_64/microcode.rs index 786761a8078..31057d39c3e 100644 --- a/kernel/src/arch/x86_64/microcode.rs +++ b/kernel/src/arch/x86_64/microcode.rs @@ -10,16 +10,23 @@ //! **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. Neither maker documents a -//! fault for a valid update at a present, canonical address, and every -//! failure this loader can see is a panic, which reports before the IDT. +//! 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). AMD's PPR for family 17h model 60h +//! (55922 rev 3.06, MSR0000_008B) has it return "an identification number for +//! the microcode patch that has been loaded". 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. A load is read back (SDM -//! Example 12-10; Linux `amd.c:723-729`) and a revision other than the -//! update's panics naming both, rather than run behind a log line below the -//! revision this image pins. Every CPU that reads a revision runs the one the -//! first did, or the boot stops naming it: whatever reads the revision -//! assumes one. +//! it runs; a CPU that loads nothing says why. 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. @@ -37,9 +44,9 @@ use crate::log; const PLATFORM_ID: u32 = 0x17; /// `IA32_BIOS_UPDT_TRIG`. const INTEL_TRIGGER: u32 = 0x79; -/// `IA32_BIOS_SIGN_ID`, AMD's `MSR_AMD64_PATCH_LEVEL`. +/// `IA32_BIOS_SIGN_ID`; AMD's `PATCH_LEVEL` (55922, MSR0000_008B). const SIGN_ID: u32 = 0x8b; -/// `MSR_AMD64_PATCH_LOADER`. +/// AMD's patch loader, which no AMD document in hand describes. const AMD_TRIGGER: u32 = 0xc001_0020; /// The revision every CPU runs, as the first to read one published it. @@ -68,11 +75,12 @@ pub fn load(cpu_id: u32) { let cpu = intel::Cpu { signature, platform, revision: found }; (Some(platform), load_intel(cpu_id, &cpu)) } - Maker::Amd => (None, load_amd(cpu_id, signature, found)), + Maker::Amd => (None, load_amd(signature, found)), }, }; let running = match outcome { Outcome::Loaded(loaded) => loaded, + Outcome::Refused { running, .. } => running, Outcome::Unloaded(_) | Outcome::NoMatch | Outcome::Current(_) => found, }; let platform = Platform(platform); @@ -88,6 +96,8 @@ enum Outcome { 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 { @@ -97,6 +107,7 @@ impl fmt::Display for Outcome { 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}"), } } } @@ -139,13 +150,12 @@ fn load_intel(cpu_id: u32, cpu: &intel::Cpu) -> Outcome { // every update's 1024-byte granule start 16-byte aligned, inside // the kernel image and so canonical and present (§12.11.6). unsafe { trigger(INTEL_TRIGGER, update.data().as_ptr()) }; - verified(cpu_id, Maker::Intel, cpu.revision, update.revision()); - Outcome::Loaded(update.revision()) + taken(Maker::Intel, update.revision()) } } } -fn load_amd(cpu_id: u32, signature: Signature, found: Revision) -> Outcome { +fn load_amd(signature: Signature, found: Revision) -> Outcome { let choice = amd::select(&toyos_microcode::AMD.0, signature, found).unwrap_or_else(|refused| { panic!("microcode: the embedded AMD container is refused at byte {}: {:?}", refused.at, refused.why) }); @@ -155,14 +165,13 @@ fn load_amd(cpu_id: u32, signature: Signature, found: Revision) -> Outcome { Choice::Load(patch) => { let bytes = patch.bytes().as_ptr_range(); // SAFETY: a validated patch inside the kernel image, so its - // address is canonical and present (Linux `amd.c:700-705`). + // address is canonical and present. unsafe { trigger(AMD_TRIGGER, bytes.start) }; if patch.leaves_its_mapping_uncached() { cpu::invlpg(bytes.start as u64); cpu::invlpg(bytes.end as u64 - 1); } - verified(cpu_id, Maker::Amd, found, patch.revision()); - Outcome::Loaded(patch.revision()) + taken(Maker::Amd, patch.revision()) } } } @@ -181,14 +190,13 @@ unsafe fn trigger(msr: u32, at: *const u8) { } } -/// The revision read back after a load is the update's, or the boot stops. -fn verified(cpu_id: u32, maker: Maker, found: Revision, loaded: Revision) { - let read = revision(maker); - assert!( - read == loaded, - "microcode: cpu{cpu_id} found {found:#x}, was handed {maker:?}'s update {loaded:#x} and \ - reads back {read:#x}", - ); +/// What the CPU made of the update of revision `update` it was handed: the +/// revision it reads back, which it runs. +fn taken(maker: Maker, update: Revision) -> Outcome { + match revision(maker) { + running if running == update => Outcome::Loaded(update), + running => Outcome::Refused { update, running }, + } } /// Every CPU that reads a revision runs the first one's. @@ -205,7 +213,7 @@ fn agree(cpu_id: u32, running: 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 the read-back must stop the boot. +/// still says 0xbe, so its line must say `refused`. #[cfg(feature = "boot-actuators")] mod reseal { use toyos_microcode::Aligned; diff --git a/tests/checks.rs b/tests/checks.rs index 164d0d9a41b..0746768e11f 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -421,7 +421,7 @@ mod checks { } /// [`microcode_on_metal`] against both benches' machines and the ways a - /// boot of either could fall short of Linux's reading, with no guest. + /// boot of either could fall short of its embedded update, with no guest. #[test] fn microcode_verdict() -> Result<(), String> { fn log(lines: &[(u32, &str)]) -> String { @@ -430,8 +430,9 @@ mod checks { const T14: &str = "Intel signature 0x806c1 platform 7 found 0xbe current 0xbe running 0xbe"; const BSP: &str = "Amd signature 0x860f01 found 0x8600106 loaded 0x860010d running 0x860010d"; const SIBLING: &str = "Amd signature 0x860f01 found 0x860010d loaded 0x860010d running 0x860010d"; + const AMD_REFUSED: &str = "Amd signature 0x860f01 found 0x8600106 refused 0x860010d running 0x8600106"; let t14 = |cpu: u32| (cpu, T14); - let amd = |cpu: u32| (cpu, if cpu == 0 || cpu % 2 == 1 { BSP } else { SIBLING }); + let amd = |cpu: u32| (cpu, if cpu % 2 == 0 { BSP } else { SIBLING }); let refused = |what: &str, lines: &[(u32, &str)], cpus: u32, says: &str| match microcode_on_metal(&log(lines), cpus) { Ok(()) => Err(format!("{what} was accepted")), @@ -440,33 +441,37 @@ mod checks { }; microcode_on_metal(&log(&(0..8).map(t14).collect::>()), 8) - .map_err(|e| format!("the T14 as Linux reads it was refused: {e}"))?; + .map_err(|e| format!("the T14 at Intel's 0xbe was refused: {e}"))?; microcode_on_metal(&log(&(0..16).map(amd).collect::>()), 16) - .map_err(|e| format!("the AMD bench as Linux reads it was refused: {e}"))?; + .map_err(|e| format!("the AMD bench at the container's 0x860010d 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")?; + refused("an AMD bench that refused its patch", &(0..16).map(|cpu| (cpu, AMD_REFUSED)).collect::>(), 16, "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 Linux", &below, 8, "Linux runs 0xbe")?; + refused("a T14 CPU below Intel's update", &below, 8, "holds 0xbe")?; let mut unloaded: Vec<_> = (0..16).map(amd).collect(); unloaded[5] = (5, "Amd signature 0x860f01 found 0x8600106 current 0x860010d running 0x8600106"); - refused("an AMD CPU left on its firmware's", &unloaded, 16, "Linux loads 0x860010d")?; - let mut no_load: Vec<_> = (0..16).map(amd).collect(); - no_load[0] = (0, SIBLING); - refused("a BSP that found the patch already", &no_load, 16, "which the BSP finds")?; + refused("an AMD CPU left on its firmware's", &unloaded, 16, "holds 0x860010d")?; + 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, "holds 0xbe")?; 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 nobody read under Linux", + "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 Linux reading", + "no bench", )?; - eprintln!(" [microcode] the verdict refuses 7 machines and accepts both benches"); + eprintln!(" [microcode] the verdict refuses 9 machines and accepts both benches"); Ok(()) } diff --git a/tests/linux/amd_shas.c b/tests/linux/amd_shas.c deleted file mode 100644 index 1fd349cfc80..00000000000 --- a/tests/linux/amd_shas.c +++ /dev/null @@ -1,556 +0,0 @@ -/* Keep 'em sorted. */ -static const struct patch_digest phashes[] = { - { 0x8001227, { - 0x99,0xc0,0x9b,0x2b,0xcc,0x9f,0x52,0x1b, - 0x1a,0x5f,0x1d,0x83,0xa1,0x6c,0xc4,0x46, - 0xe2,0x6c,0xda,0x73,0xfb,0x2d,0x23,0xa8, - 0x77,0xdc,0x15,0x31,0x33,0x4a,0x46,0x18, - } - }, - { 0x8001250, { - 0xc0,0x0b,0x6b,0x19,0xfd,0x5c,0x39,0x60, - 0xd5,0xc3,0x57,0x46,0x54,0xe4,0xd1,0xaa, - 0xa8,0xf7,0x1f,0xa8,0x6a,0x60,0x3e,0xe3, - 0x27,0x39,0x8e,0x53,0x30,0xf8,0x49,0x19, - } - }, - { 0x800126e, { - 0xf3,0x8b,0x2b,0xb6,0x34,0xe3,0xc8,0x2c, - 0xef,0xec,0x63,0x6d,0xc8,0x76,0x77,0xb3, - 0x25,0x5a,0xb7,0x52,0x8c,0x83,0x26,0xe6, - 0x4c,0xbe,0xbf,0xe9,0x7d,0x22,0x6a,0x43, - } - }, - { 0x800126f, { - 0x2b,0x5a,0xf2,0x9c,0xdd,0xd2,0x7f,0xec, - 0xec,0x96,0x09,0x57,0xb0,0x96,0x29,0x8b, - 0x2e,0x26,0x91,0xf0,0x49,0x33,0x42,0x18, - 0xdd,0x4b,0x65,0x5a,0xd4,0x15,0x3d,0x33, - } - }, - { 0x800820d, { - 0x68,0x98,0x83,0xcd,0x22,0x0d,0xdd,0x59, - 0x73,0x2c,0x5b,0x37,0x1f,0x84,0x0e,0x67, - 0x96,0x43,0x83,0x0c,0x46,0x44,0xab,0x7c, - 0x7b,0x65,0x9e,0x57,0xb5,0x90,0x4b,0x0e, - } - }, - { 0x8301025, { - 0xe4,0x7d,0xdb,0x1e,0x14,0xb4,0x5e,0x36, - 0x8f,0x3e,0x48,0x88,0x3c,0x6d,0x76,0xa1, - 0x59,0xc6,0xc0,0x72,0x42,0xdf,0x6c,0x30, - 0x6f,0x0b,0x28,0x16,0x61,0xfc,0x79,0x77, - } - }, - { 0x8301055, { - 0x81,0x7b,0x99,0x1b,0xae,0x2d,0x4f,0x9a, - 0xef,0x13,0xce,0xb5,0x10,0xaf,0x6a,0xea, - 0xe5,0xb0,0x64,0x98,0x10,0x68,0x34,0x3b, - 0x9d,0x7a,0xd6,0x22,0x77,0x5f,0xb3,0x5b, - } - }, - { 0x8301072, { - 0xcf,0x76,0xa7,0x1a,0x49,0xdf,0x2a,0x5e, - 0x9e,0x40,0x70,0xe5,0xdd,0x8a,0xa8,0x28, - 0x20,0xdc,0x91,0xd8,0x2c,0xa6,0xa0,0xb1, - 0x2d,0x22,0x26,0x94,0x4b,0x40,0x85,0x30, - } - }, - { 0x830107a, { - 0x2a,0x65,0x8c,0x1a,0x5e,0x07,0x21,0x72, - 0xdf,0x90,0xa6,0x51,0x37,0xd3,0x4b,0x34, - 0xc4,0xda,0x03,0xe1,0x8a,0x6c,0xfb,0x20, - 0x04,0xb2,0x81,0x05,0xd4,0x87,0xf4,0x0a, - } - }, - { 0x830107b, { - 0xb3,0x43,0x13,0x63,0x56,0xc1,0x39,0xad, - 0x10,0xa6,0x2b,0xcc,0x02,0xe6,0x76,0x2a, - 0x1e,0x39,0x58,0x3e,0x23,0x6e,0xa4,0x04, - 0x95,0xea,0xf9,0x6d,0xc2,0x8a,0x13,0x19, - } - }, - { 0x830107c, { - 0x21,0x64,0xde,0xfb,0x9f,0x68,0x96,0x47, - 0x70,0x5c,0xe2,0x8f,0x18,0x52,0x6a,0xac, - 0xa4,0xd2,0x2e,0xe0,0xde,0x68,0x66,0xc3, - 0xeb,0x1e,0xd3,0x3f,0xbc,0x51,0x1d,0x38, - } - }, - { 0x860010d, { - 0x86,0xb6,0x15,0x83,0xbc,0x3b,0x9c,0xe0, - 0xb3,0xef,0x1d,0x99,0x84,0x35,0x15,0xf7, - 0x7c,0x2a,0xc6,0x42,0xdb,0x73,0x07,0x5c, - 0x7d,0xc3,0x02,0xb5,0x43,0x06,0x5e,0xf8, - } - }, - { 0x8608108, { - 0x14,0xfe,0x57,0x86,0x49,0xc8,0x68,0xe2, - 0x11,0xa3,0xcb,0x6e,0xff,0x6e,0xd5,0x38, - 0xfe,0x89,0x1a,0xe0,0x67,0xbf,0xc4,0xcc, - 0x1b,0x9f,0x84,0x77,0x2b,0x9f,0xaa,0xbd, - } - }, - { 0x8701034, { - 0xc3,0x14,0x09,0xa8,0x9c,0x3f,0x8d,0x83, - 0x9b,0x4c,0xa5,0xb7,0x64,0x8b,0x91,0x5d, - 0x85,0x6a,0x39,0x26,0x1e,0x14,0x41,0xa8, - 0x75,0xea,0xa6,0xf9,0xc9,0xd1,0xea,0x2b, - } - }, - { 0x8a00008, { - 0xd7,0x2a,0x93,0xdc,0x05,0x2f,0xa5,0x6e, - 0x0c,0x61,0x2c,0x07,0x9f,0x38,0xe9,0x8e, - 0xef,0x7d,0x2a,0x05,0x4d,0x56,0xaf,0x72, - 0xe7,0x56,0x47,0x6e,0x60,0x27,0xd5,0x8c, - } - }, - { 0x8a0000a, { - 0x73,0x31,0x26,0x22,0xd4,0xf9,0xee,0x3c, - 0x07,0x06,0xe7,0xb9,0xad,0xd8,0x72,0x44, - 0x33,0x31,0xaa,0x7d,0xc3,0x67,0x0e,0xdb, - 0x47,0xb5,0xaa,0xbc,0xf5,0xbb,0xd9,0x20, - } - }, - { 0xa00104c, { - 0x3c,0x8a,0xfe,0x04,0x62,0xd8,0x6d,0xbe, - 0xa7,0x14,0x28,0x64,0x75,0xc0,0xa3,0x76, - 0xb7,0x92,0x0b,0x97,0x0a,0x8e,0x9c,0x5b, - 0x1b,0xc8,0x9d,0x3a,0x1e,0x81,0x3d,0x3b, - } - }, - { 0xa00104e, { - 0xc4,0x35,0x82,0x67,0xd2,0x86,0xe5,0xb2, - 0xfd,0x69,0x12,0x38,0xc8,0x77,0xba,0xe0, - 0x70,0xf9,0x77,0x89,0x10,0xa6,0x74,0x4e, - 0x56,0x58,0x13,0xf5,0x84,0x70,0x28,0x0b, - } - }, - { 0xa001053, { - 0x92,0x0e,0xf4,0x69,0x10,0x3b,0xf9,0x9d, - 0x31,0x1b,0xa6,0x99,0x08,0x7d,0xd7,0x25, - 0x7e,0x1e,0x89,0xba,0x35,0x8d,0xac,0xcb, - 0x3a,0xb4,0xdf,0x58,0x12,0xcf,0xc0,0xc3, - } - }, - { 0xa001058, { - 0x33,0x7d,0xa9,0xb5,0x4e,0x62,0x13,0x36, - 0xef,0x66,0xc9,0xbd,0x0a,0xa6,0x3b,0x19, - 0xcb,0xf5,0xc2,0xc3,0x55,0x47,0x20,0xec, - 0x1f,0x7b,0xa1,0x44,0x0e,0x8e,0xa4,0xb2, - } - }, - { 0xa001075, { - 0x39,0x02,0x82,0xd0,0x7c,0x26,0x43,0xe9, - 0x26,0xa3,0xd9,0x96,0xf7,0x30,0x13,0x0a, - 0x8a,0x0e,0xac,0xe7,0x1d,0xdc,0xe2,0x0f, - 0xcb,0x9e,0x8d,0xbc,0xd2,0xa2,0x44,0xe0, - } - }, - { 0xa001078, { - 0x2d,0x67,0xc7,0x35,0xca,0xef,0x2f,0x25, - 0x4c,0x45,0x93,0x3f,0x36,0x01,0x8c,0xce, - 0xa8,0x5b,0x07,0xd3,0xc1,0x35,0x3c,0x04, - 0x20,0xa2,0xfc,0xdc,0xe6,0xce,0x26,0x3e, - } - }, - { 0xa001079, { - 0x43,0xe2,0x05,0x9c,0xfd,0xb7,0x5b,0xeb, - 0x5b,0xe9,0xeb,0x3b,0x96,0xf4,0xe4,0x93, - 0x73,0x45,0x3e,0xac,0x8d,0x3b,0xe4,0xdb, - 0x10,0x31,0xc1,0xe4,0xa2,0xd0,0x5a,0x8a, - } - }, - { 0xa00107a, { - 0x5f,0x92,0xca,0xff,0xc3,0x59,0x22,0x5f, - 0x02,0xa0,0x91,0x3b,0x4a,0x45,0x10,0xfd, - 0x19,0xe1,0x8a,0x6d,0x9a,0x92,0xc1,0x3f, - 0x75,0x78,0xac,0x78,0x03,0x1d,0xdb,0x18, - } - }, - { 0xa001143, { - 0x56,0xca,0xf7,0x43,0x8a,0x4c,0x46,0x80, - 0xec,0xde,0xe5,0x9c,0x50,0x84,0x9a,0x42, - 0x27,0xe5,0x51,0x84,0x8f,0x19,0xc0,0x8d, - 0x0c,0x25,0xb4,0xb0,0x8f,0x10,0xf3,0xf8, - } - }, - { 0xa001144, { - 0x42,0xd5,0x9b,0xa7,0xd6,0x15,0x29,0x41, - 0x61,0xc4,0x72,0x3f,0xf3,0x06,0x78,0x4b, - 0x65,0xf3,0x0e,0xfa,0x9c,0x87,0xde,0x25, - 0xbd,0xb3,0x9a,0xf4,0x75,0x13,0x53,0xdc, - } - }, - { 0xa00115d, { - 0xd4,0xc4,0x49,0x36,0x89,0x0b,0x47,0xdd, - 0xfb,0x2f,0x88,0x3b,0x5f,0xf2,0x8e,0x75, - 0xc6,0x6c,0x37,0x5a,0x90,0x25,0x94,0x3e, - 0x36,0x9c,0xae,0x02,0x38,0x6c,0xf5,0x05, - } - }, - { 0xa001173, { - 0x28,0xbb,0x9b,0xd1,0xa0,0xa0,0x7e,0x3a, - 0x59,0x20,0xc0,0xa9,0xb2,0x5c,0xc3,0x35, - 0x53,0x89,0xe1,0x4c,0x93,0x2f,0x1d,0xc3, - 0xe5,0xf7,0xf3,0xc8,0x9b,0x61,0xaa,0x9e, - } - }, - { 0xa0011a8, { - 0x97,0xc6,0x16,0x65,0x99,0xa4,0x85,0x3b, - 0xf6,0xce,0xaa,0x49,0x4a,0x3a,0xc5,0xb6, - 0x78,0x25,0xbc,0x53,0xaf,0x5d,0xcf,0xf4, - 0x23,0x12,0xbb,0xb1,0xbc,0x8a,0x02,0x2e, - } - }, - { 0xa0011ce, { - 0xcf,0x1c,0x90,0xa3,0x85,0x0a,0xbf,0x71, - 0x94,0x0e,0x80,0x86,0x85,0x4f,0xd7,0x86, - 0xae,0x38,0x23,0x28,0x2b,0x35,0x9b,0x4e, - 0xfe,0xb8,0xcd,0x3d,0x3d,0x39,0xc9,0x6a, - } - }, - { 0xa0011d1, { - 0xdf,0x0e,0xca,0xde,0xf6,0xce,0x5c,0x1e, - 0x4c,0xec,0xd7,0x71,0x83,0xcc,0xa8,0x09, - 0xc7,0xc5,0xfe,0xb2,0xf7,0x05,0xd2,0xc5, - 0x12,0xdd,0xe4,0xf3,0x92,0x1c,0x3d,0xb8, - } - }, - { 0xa0011d3, { - 0x91,0xe6,0x10,0xd7,0x57,0xb0,0x95,0x0b, - 0x9a,0x24,0xee,0xf7,0xcf,0x56,0xc1,0xa6, - 0x4a,0x52,0x7d,0x5f,0x9f,0xdf,0xf6,0x00, - 0x65,0xf7,0xea,0xe8,0x2a,0x88,0xe2,0x26, - } - }, - { 0xa0011d5, { - 0xed,0x69,0x89,0xf4,0xeb,0x64,0xc2,0x13, - 0xe0,0x51,0x1f,0x03,0x26,0x52,0x7d,0xb7, - 0x93,0x5d,0x65,0xca,0xb8,0x12,0x1d,0x62, - 0x0d,0x5b,0x65,0x34,0x69,0xb2,0x62,0x21, - } - }, - { 0xa0011d7, { - 0x35,0x07,0xcd,0x40,0x94,0xbc,0x81,0x6b, - 0xfc,0x61,0x56,0x1a,0xe2,0xdb,0x96,0x12, - 0x1c,0x1c,0x31,0xb1,0x02,0x6f,0xe5,0xd2, - 0xfe,0x1b,0x04,0x03,0x2c,0x8f,0x4c,0x36, - } - }, - { 0xa001223, { - 0xfb,0x32,0x5f,0xc6,0x83,0x4f,0x8c,0xb8, - 0xa4,0x05,0xf9,0x71,0x53,0x01,0x16,0xc4, - 0x83,0x75,0x94,0xdd,0xeb,0x7e,0xb7,0x15, - 0x8e,0x3b,0x50,0x29,0x8a,0x9c,0xcc,0x45, - } - }, - { 0xa001224, { - 0x0e,0x0c,0xdf,0xb4,0x89,0xee,0x35,0x25, - 0xdd,0x9e,0xdb,0xc0,0x69,0x83,0x0a,0xad, - 0x26,0xa9,0xaa,0x9d,0xfc,0x3c,0xea,0xf9, - 0x6c,0xdc,0xd5,0x6d,0x8b,0x6e,0x85,0x4a, - } - }, - { 0xa001227, { - 0xab,0xc6,0x00,0x69,0x4b,0x50,0x87,0xad, - 0x5f,0x0e,0x8b,0xea,0x57,0x38,0xce,0x1d, - 0x0f,0x75,0x26,0x02,0xf6,0xd6,0x96,0xe9, - 0x87,0xb9,0xd6,0x20,0x27,0x7c,0xd2,0xe0, - } - }, - { 0xa001229, { - 0x7f,0x49,0x49,0x48,0x46,0xa5,0x50,0xa6, - 0x28,0x89,0x98,0xe2,0x9e,0xb4,0x7f,0x75, - 0x33,0xa7,0x04,0x02,0xe4,0x82,0xbf,0xb4, - 0xa5,0x3a,0xba,0x24,0x8d,0x31,0x10,0x1d, - } - }, - { 0xa00122e, { - 0x56,0x94,0xa9,0x5d,0x06,0x68,0xfe,0xaf, - 0xdf,0x7a,0xff,0x2d,0xdf,0x74,0x0f,0x15, - 0x66,0xfb,0x00,0xb5,0x51,0x97,0x9b,0xfa, - 0xcb,0x79,0x85,0x46,0x25,0xb4,0xd2,0x10, - } - }, - { 0xa001231, { - 0x0b,0x46,0xa5,0xfc,0x18,0x15,0xa0,0x9e, - 0xa6,0xdc,0xb7,0xff,0x17,0xf7,0x30,0x64, - 0xd4,0xda,0x9e,0x1b,0xc3,0xfc,0x02,0x3b, - 0xe2,0xc6,0x0e,0x41,0x54,0xb5,0x18,0xdd, - } - }, - { 0xa001234, { - 0x88,0x8d,0xed,0xab,0xb5,0xbd,0x4e,0xf7, - 0x7f,0xd4,0x0e,0x95,0x34,0x91,0xff,0xcc, - 0xfb,0x2a,0xcd,0xf7,0xd5,0xdb,0x4c,0x9b, - 0xd6,0x2e,0x73,0x50,0x8f,0x83,0x79,0x1a, - } - }, - { 0xa001236, { - 0x3d,0x30,0x00,0xb9,0x71,0xba,0x87,0x78, - 0xa8,0x43,0x55,0xc4,0x26,0x59,0xcf,0x9d, - 0x93,0xce,0x64,0x0e,0x8b,0x72,0x11,0x8b, - 0xa3,0x8f,0x51,0xe9,0xca,0x98,0xaa,0x25, - } - }, - { 0xa001238, { - 0x72,0xf7,0x4b,0x0c,0x7d,0x58,0x65,0xcc, - 0x00,0xcc,0x57,0x16,0x68,0x16,0xf8,0x2a, - 0x1b,0xb3,0x8b,0xe1,0xb6,0x83,0x8c,0x7e, - 0xc0,0xcd,0x33,0xf2,0x8d,0xf9,0xef,0x59, - } - }, - { 0xa00123b, { - 0xef,0xa1,0x1e,0x71,0xf1,0xc3,0x2c,0xe2, - 0xc3,0xef,0x69,0x41,0x7a,0x54,0xca,0xc3, - 0x8f,0x62,0x84,0xee,0xc2,0x39,0xd9,0x28, - 0x95,0xa7,0x12,0x49,0x1e,0x30,0x71,0x72, - } - }, - { 0xa00820c, { - 0xa8,0x0c,0x81,0xc0,0xa6,0x00,0xe7,0xf3, - 0x5f,0x65,0xd3,0xb9,0x6f,0xea,0x93,0x63, - 0xf1,0x8c,0x88,0x45,0xd7,0x82,0x80,0xd1, - 0xe1,0x3b,0x8d,0xb2,0xf8,0x22,0x03,0xe2, - } - }, - { 0xa00820d, { - 0xf9,0x2a,0xc0,0xf4,0x9e,0xa4,0x87,0xa4, - 0x7d,0x87,0x00,0xfd,0xab,0xda,0x19,0xca, - 0x26,0x51,0x32,0xc1,0x57,0x91,0xdf,0xc1, - 0x05,0xeb,0x01,0x7c,0x5a,0x95,0x21,0xb7, - } - }, - { 0xa10113e, { - 0x05,0x3c,0x66,0xd7,0xa9,0x5a,0x33,0x10, - 0x1b,0xf8,0x9c,0x8f,0xed,0xfc,0xa7,0xa0, - 0x15,0xe3,0x3f,0x4b,0x1d,0x0d,0x0a,0xd5, - 0xfa,0x90,0xc4,0xed,0x9d,0x90,0xaf,0x53, - } - }, - { 0xa101144, { - 0xb3,0x0b,0x26,0x9a,0xf8,0x7c,0x02,0x26, - 0x35,0x84,0x53,0xa4,0xd3,0x2c,0x7c,0x09, - 0x68,0x7b,0x96,0xb6,0x93,0xef,0xde,0xbc, - 0xfd,0x4b,0x15,0xd2,0x81,0xd3,0x51,0x47, - } - }, - { 0xa101148, { - 0x20,0xd5,0x6f,0x40,0x4a,0xf6,0x48,0x90, - 0xc2,0x93,0x9a,0xc2,0xfd,0xac,0xef,0x4f, - 0xfa,0xc0,0x3d,0x92,0x3c,0x6d,0x01,0x08, - 0xf1,0x5e,0xb0,0xde,0xb4,0x98,0xae,0xc4, - } - }, - { 0xa10114c, { - 0x9e,0xb6,0xa2,0xd9,0x87,0x38,0xc5,0x64, - 0xd8,0x88,0xfa,0x78,0x98,0xf9,0x6f,0x74, - 0x39,0x90,0x1b,0xa5,0xcf,0x5e,0xb4,0x2a, - 0x02,0xff,0xd4,0x8c,0x71,0x8b,0xe2,0xc0, - } - }, - { 0xa10123e, { - 0x03,0xb9,0x2c,0x76,0x48,0x93,0xc9,0x18, - 0xfb,0x56,0xfd,0xf7,0xe2,0x1d,0xca,0x4d, - 0x1d,0x13,0x53,0x63,0xfe,0x42,0x6f,0xfc, - 0x19,0x0f,0xf1,0xfc,0xa7,0xdd,0x89,0x1b, - } - }, - { 0xa101244, { - 0x71,0x56,0xb5,0x9f,0x21,0xbf,0xb3,0x3c, - 0x8c,0xd7,0x36,0xd0,0x34,0x52,0x1b,0xb1, - 0x46,0x2f,0x04,0xf0,0x37,0xd8,0x1e,0x72, - 0x24,0xa2,0x80,0x84,0x83,0x65,0x84,0xc0, - } - }, - { 0xa101248, { - 0xed,0x3b,0x95,0xa6,0x68,0xa7,0x77,0x3e, - 0xfc,0x17,0x26,0xe2,0x7b,0xd5,0x56,0x22, - 0x2c,0x1d,0xef,0xeb,0x56,0xdd,0xba,0x6e, - 0x1b,0x7d,0x64,0x9d,0x4b,0x53,0x13,0x75, - } - }, - { 0xa10124c, { - 0x29,0xea,0xf1,0x2c,0xb2,0xe4,0xef,0x90, - 0xa4,0xcd,0x1d,0x86,0x97,0x17,0x61,0x46, - 0xfc,0x22,0xcb,0x57,0x75,0x19,0xc8,0xcc, - 0x0c,0xf5,0xbc,0xac,0x81,0x9d,0x9a,0xd2, - } - }, - { 0xa108108, { - 0xed,0xc2,0xec,0xa1,0x15,0xc6,0x65,0xe9, - 0xd0,0xef,0x39,0xaa,0x7f,0x55,0x06,0xc6, - 0xf5,0xd4,0x3f,0x7b,0x14,0xd5,0x60,0x2c, - 0x28,0x1e,0x9c,0x59,0x69,0x99,0x4d,0x16, - } - }, - { 0xa108109, { - 0x85,0xb4,0xbd,0x7c,0x49,0xa7,0xbd,0xfa, - 0x49,0x36,0x80,0x81,0xc5,0xb7,0x39,0x1b, - 0x9a,0xaa,0x50,0xde,0x9b,0xe9,0x32,0x35, - 0x42,0x7e,0x51,0x4f,0x52,0x2c,0x28,0x59, - } - }, - { 0xa20102d, { - 0xf9,0x6e,0xf2,0x32,0xd3,0x0f,0x5f,0x11, - 0x59,0xa1,0xfe,0xcc,0xcd,0x9b,0x42,0x89, - 0x8b,0x89,0x2f,0xb5,0xbb,0x82,0xef,0x23, - 0x8c,0xe9,0x19,0x3e,0xcc,0x3f,0x7b,0xb4, - } - }, - { 0xa20102e, { - 0xbe,0x1f,0x32,0x04,0x0d,0x3c,0x9c,0xdd, - 0xe1,0xa4,0xbf,0x76,0x3a,0xec,0xc2,0xf6, - 0x11,0x00,0xa7,0xaf,0x0f,0xe5,0x02,0xc5, - 0x54,0x3a,0x1f,0x8c,0x16,0xb5,0xff,0xbe, - } - }, - { 0xa201210, { - 0xe8,0x6d,0x51,0x6a,0x8e,0x72,0xf3,0xfe, - 0x6e,0x16,0xbc,0x62,0x59,0x40,0x17,0xe9, - 0x6d,0x3d,0x0e,0x6b,0xa7,0xac,0xe3,0x68, - 0xf7,0x55,0xf0,0x13,0xbb,0x22,0xf6,0x41, - } - }, - { 0xa201211, { - 0x69,0xa1,0x17,0xec,0xd0,0xf6,0x6c,0x95, - 0xe2,0x1e,0xc5,0x59,0x1a,0x52,0x0a,0x27, - 0xc4,0xed,0xd5,0x59,0x1f,0xbf,0x00,0xff, - 0x08,0x88,0xb5,0xe1,0x12,0xb6,0xcc,0x27, - } - }, - { 0xa404107, { - 0xbb,0x04,0x4e,0x47,0xdd,0x5e,0x26,0x45, - 0x1a,0xc9,0x56,0x24,0xa4,0x4c,0x82,0xb0, - 0x8b,0x0d,0x9f,0xf9,0x3a,0xdf,0xc6,0x81, - 0x13,0xbc,0xc5,0x25,0xe4,0xc5,0xc3,0x99, - } - }, - { 0xa404108, { - 0x69,0x67,0x43,0x06,0xf8,0x0c,0x62,0xdc, - 0xa4,0x21,0x30,0x4f,0x0f,0x21,0x2c,0xcb, - 0xcc,0x37,0xf1,0x1c,0xc3,0xf8,0x2f,0x19, - 0xdf,0x53,0x53,0x46,0xb1,0x15,0xea,0x00, - } - }, - { 0xa500011, { - 0x23,0x3d,0x70,0x7d,0x03,0xc3,0xc4,0xf4, - 0x2b,0x82,0xc6,0x05,0xda,0x80,0x0a,0xf1, - 0xd7,0x5b,0x65,0x3a,0x7d,0xab,0xdf,0xa2, - 0x11,0x5e,0x96,0x7e,0x71,0xe9,0xfc,0x74, - } - }, - { 0xa500012, { - 0xeb,0x74,0x0d,0x47,0xa1,0x8e,0x09,0xe4, - 0x93,0x4c,0xad,0x03,0x32,0x4c,0x38,0x16, - 0x10,0x39,0xdd,0x06,0xaa,0xce,0xd6,0x0f, - 0x62,0x83,0x9d,0x8e,0x64,0x55,0xbe,0x63, - } - }, - { 0xa601209, { - 0x66,0x48,0xd4,0x09,0x05,0xcb,0x29,0x32, - 0x66,0xb7,0x9a,0x76,0xcd,0x11,0xf3,0x30, - 0x15,0x86,0xcc,0x5d,0x97,0x0f,0xc0,0x46, - 0xe8,0x73,0xe2,0xd6,0xdb,0xd2,0x77,0x1d, - } - }, - { 0xa60120a, { - 0x0c,0x8b,0x3d,0xfd,0x52,0x52,0x85,0x7d, - 0x20,0x3a,0xe1,0x7e,0xa4,0x21,0x3b,0x7b, - 0x17,0x86,0xae,0xac,0x13,0xb8,0x63,0x9d, - 0x06,0x01,0xd0,0xa0,0x51,0x9a,0x91,0x2c, - } - }, - { 0xa704107, { - 0xf3,0xc6,0x58,0x26,0xee,0xac,0x3f,0xd6, - 0xce,0xa1,0x72,0x47,0x3b,0xba,0x2b,0x93, - 0x2a,0xad,0x8e,0x6b,0xea,0x9b,0xb7,0xc2, - 0x64,0x39,0x71,0x8c,0xce,0xe7,0x41,0x39, - } - }, - { 0xa704108, { - 0xd7,0x55,0x15,0x2b,0xfe,0xc4,0xbc,0x93, - 0xec,0x91,0xa0,0xae,0x45,0xb7,0xc3,0x98, - 0x4e,0xff,0x61,0x77,0x88,0xc2,0x70,0x49, - 0xe0,0x3a,0x1d,0x84,0x38,0x52,0xbf,0x5a, - } - }, - { 0xa705206, { - 0x8d,0xc0,0x76,0xbd,0x58,0x9f,0x8f,0xa4, - 0x12,0x9d,0x21,0xfb,0x48,0x21,0xbc,0xe7, - 0x67,0x6f,0x04,0x18,0xae,0x20,0x87,0x4b, - 0x03,0x35,0xe9,0xbe,0xfb,0x06,0xdf,0xfc, - } - }, - { 0xa705208, { - 0x30,0x1d,0x55,0x24,0xbc,0x6b,0x5a,0x19, - 0x0c,0x7d,0x1d,0x74,0xaa,0xd1,0xeb,0xd2, - 0x16,0x62,0xf7,0x5b,0xe1,0x1f,0x18,0x11, - 0x5c,0xf0,0x94,0x90,0x26,0xec,0x69,0xff, - } - }, - { 0xa708007, { - 0x6b,0x76,0xcc,0x78,0xc5,0x8a,0xa3,0xe3, - 0x32,0x2d,0x79,0xe4,0xc3,0x80,0xdb,0xb2, - 0x07,0xaa,0x3a,0xe0,0x57,0x13,0x72,0x80, - 0xdf,0x92,0x73,0x84,0x87,0x3c,0x73,0x93, - } - }, - { 0xa708008, { - 0x08,0x6e,0xf0,0x22,0x4b,0x8e,0xc4,0x46, - 0x58,0x34,0xe6,0x47,0xa2,0x28,0xfd,0xab, - 0x22,0x3d,0xdd,0xd8,0x52,0x9e,0x1d,0x16, - 0xfa,0x01,0x68,0x14,0x79,0x3e,0xe8,0x6b, - } - }, - { 0xa70c005, { - 0x88,0x5d,0xfb,0x79,0x64,0xd8,0x46,0x3b, - 0x4a,0x83,0x8e,0x77,0x7e,0xcf,0xb3,0x0f, - 0x1f,0x1f,0xf1,0x97,0xeb,0xfe,0x56,0x55, - 0xee,0x49,0xac,0xe1,0x8b,0x13,0xc5,0x13, - } - }, - { 0xa70c008, { - 0x0f,0xdb,0x37,0xa1,0x10,0xaf,0xd4,0x21, - 0x94,0x0d,0xa4,0xa2,0xe9,0x86,0x6c,0x0e, - 0x85,0x7c,0x36,0x30,0xa3,0x3a,0x78,0x66, - 0x18,0x10,0x60,0x0d,0x78,0x3d,0x44,0xd0, - } - }, - { 0xaa00116, { - 0xe8,0x4c,0x2c,0x88,0xa1,0xac,0x24,0x63, - 0x65,0xe5,0xaa,0x2d,0x16,0xa9,0xc3,0xf5, - 0xfe,0x1d,0x5e,0x65,0xc7,0xaa,0x92,0x4d, - 0x91,0xee,0x76,0xbb,0x4c,0x66,0x78,0xc9, - } - }, - { 0xaa00212, { - 0xbd,0x57,0x5d,0x0a,0x0a,0x30,0xc1,0x75, - 0x95,0x58,0x5e,0x93,0x02,0x28,0x43,0x71, - 0xed,0x42,0x29,0xc8,0xec,0x34,0x2b,0xb2, - 0x1a,0x65,0x4b,0xfe,0x07,0x0f,0x34,0xa1, - } - }, - { 0xaa00213, { - 0xed,0x58,0xb7,0x76,0x81,0x7f,0xd9,0x3a, - 0x1a,0xff,0x8b,0x34,0xb8,0x4a,0x99,0x0f, - 0x28,0x49,0x6c,0x56,0x2b,0xdc,0xb7,0xed, - 0x96,0xd5,0x9d,0xc1,0x7a,0xd4,0x51,0x9b, - } - }, - { 0xaa00215, { - 0x55,0xd3,0x28,0xcb,0x87,0xa9,0x32,0xe9, - 0x4e,0x85,0x4b,0x7c,0x6b,0xd5,0x7c,0xd4, - 0x1b,0x51,0x71,0x3a,0x0e,0x0b,0xdc,0x9b, - 0x68,0x2f,0x46,0xee,0xfe,0xc6,0x6d,0xef, - } - }, - { 0xaa00216, { - 0x79,0xfb,0x5b,0x9f,0xb6,0xe6,0xa8,0xf5, - 0x4e,0x7c,0x4f,0x8e,0x1d,0xad,0xd0,0x08, - 0xc2,0x43,0x7c,0x8b,0xe6,0xdb,0xd0,0xd2, - 0xe8,0x39,0x26,0xc1,0xe5,0x5a,0x48,0xf1, - } - }, -}; diff --git a/tests/metal/amd-linux/SOURCE b/tests/metal/amd-linux/SOURCE deleted file mode 100644 index 0a23fe3a8cf..00000000000 --- a/tests/metal/amd-linux/SOURCE +++ /dev/null @@ -1,16 +0,0 @@ -Linux's microcode reading of the AMD bench (a Ryzen 7 4800U, family 23 model -96 stepping 1) under that machine's Fedora, `7.2.4-ogc3.1.fc44.x86_64` with -`amd-ucode-firmware-20260810`, whose `microcode_amd_fam17h.bin` is the -container `kernel/microcode/amd-ucode/` pins, byte for byte. Taken over ssh as -the bench's unprivileged user by the orchestrator, as the tools printed it, -and recorded at https://github.com/ToyOSOrg/evidence/blob/9e5f88e5e5de5dadba86d6939a158c728752dec8/amd-bench-linux-microcode.txt -with its command: - - journalctl -k -b 0 --no-pager | grep -i microcode | sed -E "s/^.*kernel: //"; - grep -m1 "^microcode" /proc/cpuinfo; grep -m1 "cpu family" /proc/cpuinfo; - grep -m1 "^model\s" /proc/cpuinfo; grep -m1 stepping /proc/cpuinfo; - rpm -q amd-ucode-firmware; uname -r - - microcode.txt its output: the revision the firmware ran and the one Linux - loaded early over it. `microcode_on_metal` in - `tests/toyos.rs` holds every CPU's `microcode:` line to it. diff --git a/tests/metal/amd-linux/microcode.txt b/tests/metal/amd-linux/microcode.txt deleted file mode 100644 index ae4a377cb90..00000000000 --- a/tests/metal/amd-linux/microcode.txt +++ /dev/null @@ -1,8 +0,0 @@ -microcode: Current revision: 0x0860010d -microcode: Updated early from: 0x08600106 -microcode : 0x860010d -cpu family : 23 -model : 96 -stepping : 1 -amd-ucode-firmware-20260810-1.fc44.noarch -7.2.4-ogc3.1.fc44.x86_64 diff --git a/tests/metal/src/lib.rs b/tests/metal/src/lib.rs index 16e4e05c1a7..01ff007df19 100644 --- a/tests/metal/src/lib.rs +++ b/tests/metal/src/lib.rs @@ -771,10 +771,10 @@ pub const FLASHABLE: &[&str] = &[ // 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, whose payload is the - // revision the CPU already runs, so the boot stops at the read-back. An - // update lasts until a hard reset (SDM Vol. 3A §12.11.6.1), and the panic's - // reset is one, so the machine is what it was. + // 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 diff --git a/tests/toyos.rs b/tests/toyos.rs index 5350f274588..fd9d42abd37 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -916,8 +916,8 @@ const METAL: &[(&str, metal::Metal)] = &[ metal::Metal { arms: METALCASE, judge: |b| scanout_wc(b[0].kernel().text()) }, ), ( - // Every CPU's microcode, held to Linux's reading of the same machine: - // `microcode_on_metal` says which machine is held to what. + // Every CPU's microcode, held to the update the embedded file holds + // for it: `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()?) }, ), @@ -5527,20 +5527,14 @@ fn ioapic_topology(log: &str) -> Result<(), String> { } /// What each CPU's `microcode:` line says: one line from each CPU this -/// machine brought up, all of one signature, and each held to Linux's reading -/// of the same machine, which is the exit's "at least as current as Linux's". +/// machine brought up, all of one signature, none refused, and each running +/// the update its maker's embedded file holds for a bench of that signature, +/// whether it found it `current` or `loaded` it. /// -/// - **The T14** (`kernel/cpuvuln/fixtures/t14/cpuinfo.txt`): Linux runs 0xbe -/// on all eight CPUs, the update its firmware loads and Intel's newest for -/// it, so every CPU reads platform 7 and finds that revision current. -/// - **The AMD bench** (`tests/metal/amd-linux/microcode.txt`): Linux updated -/// early from the firmware's revision to linux-firmware's patch, so the BSP -/// finds the firmware's, and every CPU loads the patch and runs it. A thread -/// whose core-mate loaded before it finds either, whether one engine or two -/// serve the core. +/// - **The T14**, 0x806c1: Intel's `06-8c-01` holds 0xbe. +/// - **The AMD bench**, 0x860f01: the embedded container holds 0x0860010d. fn microcode_on_metal(log: &str, cpus: u32) -> Result<(), String> { - const T14: u32 = 0x0008_06c1; - const RENOIR: u32 = 0x0086_0f01; + const BENCHES: [(u32, u32); 2] = [(0x0008_06c1, 0xbe), (0x0086_0f01, 0x0860_010d)]; let lines = microcode_lines(log)?; let ids: BTreeSet = lines.iter().map(|line| line.cpu).collect(); let want: BTreeSet = (0..cpus).collect(); @@ -5551,75 +5545,31 @@ fn microcode_on_metal(log: &str, cpus: u32) -> Result<(), String> { if let Some(other) = lines.iter().find(|line| line.signature != signature) { return Err(format!("cpu0 is signature {signature:#x} and {other:?} is not")); } - let hex = |word: &str| u32::from_str_radix(word.trim_start_matches("0x"), 16).map_err(|e| format!("{word:?}: {e}")); - let linux = match signature { - T14 => { - let revisions: Vec<&str> = include_str!("../kernel/cpuvuln/fixtures/t14/cpuinfo.txt") - .lines() - .filter_map(|line| line.strip_prefix("microcode\t: ")) - .collect::>() - .into_iter() - .collect(); - let [revision] = revisions[..] else { - return Err(format!("Linux's reading of the T14 runs more than one revision: {revisions:?}")); - }; - Linux::Current(hex(revision)?) - } - RENOIR => { - let linux = include_str!("metal/amd-linux/microcode.txt"); - let said = |label: &str| { - linux.lines().find_map(|line| line.strip_prefix(label)).ok_or_else(|| format!("Linux's reading says no {label:?}")) - }; - Linux::Loaded { - firmware: hex(said("microcode: Updated early from: ")?)?, - current: hex(said("microcode: Current revision: ")?)?, - } - } - other => return Err(format!("no Linux reading of signature {other:#x} is committed")), + let Some(&(_, file)) = BENCHES.iter().find(|(bench, _)| *bench == signature) else { + return Err(format!("no bench of signature {signature:#x} names the update it runs")); }; - for line in &lines { - let why = match linux { - Linux::Current(linux) => ((line.platform, line.found, line.outcome.as_str(), line.running) - != (Some(7), linux, "current", linux)) - .then(|| format!("Linux runs {linux:#x} on platform 7, the newest Intel names it")), - Linux::Loaded { firmware, .. } if line.cpu == 0 && line.found != firmware => { - Some(format!("its firmware runs {firmware:#x}, which the BSP finds")) - } - Linux::Loaded { firmware, current } if ![firmware, current].contains(&line.found) => { - Some(format!("a CPU finds its firmware's {firmware:#x} or its core's {current:#x}")) - } - Linux::Loaded { firmware, current } => ((line.outcome.as_str(), line.running) != ("loaded", current)) - .then(|| format!("Linux loads {current:#x} over the firmware's {firmware:#x}")), - }; - if let Some(why) = why { - return Err(format!("{line:?}: {why}")); - } + if let Some(line) = + lines.iter().find(|line| !["current", "loaded"].contains(&line.outcome.as_str()) || line.running != file) + { + return Err(format!("{line:?}: the embedded file holds {file:#x} for signature {signature:#x}")); } - eprintln!(" [microcode] {} CPUs of signature {signature:#x} hold Linux's reading", lines.len()); + eprintln!(" [microcode] {} CPUs of signature {signature:#x} run {file:#x}", lines.len()); Ok(()) } -/// Linux's reading of a machine [`microcode_on_metal`] holds a boot to. -#[derive(Clone, Copy)] -enum Linux { - /// It runs this revision, which its firmware loaded. - Current(u32), - /// It loaded `current` early over the firmware's. - Loaded { firmware: u32, current: u32 }, -} - /// One `microcode:` line, the fields [`microcode_on_metal`] holds. #[derive(Debug)] struct MicrocodeLine { cpu: u32, signature: u32, - platform: Option, found: u32, - /// `loaded`, `current`, or `loads` for one that loads nothing. + /// `loaded`, `current`, `refused`, or `loads` for one that loads nothing. outcome: String, running: u32, } +/// 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() { @@ -5631,7 +5581,6 @@ fn microcode_lines(log: &str) -> Result, String> { out.push(MicrocodeLine { cpu: words.first().and_then(|id| id.parse().ok()).ok_or_else(unreadable)?, signature: hex("signature").ok_or_else(unreadable)?, - platform: field("platform").map(|p| p.parse().map_err(|_| unreadable())).transpose()?, found: hex("found").ok_or_else(unreadable)?, outcome: words .iter() @@ -5641,6 +5590,9 @@ fn microcode_lines(log: &str) -> Result, String> { .to_string(), running: hex("running").ok_or_else(unreadable)?, }); + if let Some(line) = out.last().filter(|line| line.outcome == "refused") { + return Err(format!("cpu{} refused the update it was handed and runs {:#x}: {line:?}", line.cpu, line.running)); + } } Ok(out) } From e2b398cb650fbeda4ca3105f8d41b4c778d19bd2 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 01:19:43 +0200 Subject: [PATCH 4/6] The microcode judge reads no field it does not hold Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- tests/checks.rs | 2 +- tests/toyos.rs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/checks.rs b/tests/checks.rs index 0746768e11f..c57dad5f88a 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -432,7 +432,7 @@ mod checks { const SIBLING: &str = "Amd signature 0x860f01 found 0x860010d loaded 0x860010d running 0x860010d"; const AMD_REFUSED: &str = "Amd signature 0x860f01 found 0x8600106 refused 0x860010d running 0x8600106"; let t14 = |cpu: u32| (cpu, T14); - let amd = |cpu: u32| (cpu, if cpu % 2 == 0 { BSP } else { SIBLING }); + let amd = |cpu: u32| (cpu, if cpu.is_multiple_of(2) { BSP } else { SIBLING }); let refused = |what: &str, lines: &[(u32, &str)], cpus: u32, says: &str| match microcode_on_metal(&log(lines), cpus) { Ok(()) => Err(format!("{what} was accepted")), diff --git a/tests/toyos.rs b/tests/toyos.rs index 731cab264cf..d4987255c40 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -5488,7 +5488,6 @@ fn microcode_on_metal(log: &str, cpus: u32) -> Result<(), String> { struct MicrocodeLine { cpu: u32, signature: u32, - found: u32, /// `loaded`, `current`, `refused`, or `loads` for one that loads nothing. outcome: String, running: u32, @@ -5507,7 +5506,6 @@ fn microcode_lines(log: &str) -> Result, String> { out.push(MicrocodeLine { cpu: words.first().and_then(|id| id.parse().ok()).ok_or_else(unreadable)?, signature: hex("signature").ok_or_else(unreadable)?, - found: hex("found").ok_or_else(unreadable)?, outcome: words .iter() .position(|w| *w == "found") From dfee7fa3d64c995a547cca2d5da31b7781957c8f Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 07:24:08 +0200 Subject: [PATCH 5/6] The kernel loads Intel's microcode alone, and an AMD CPU says ToyOS has no loader for it The owner's ruling after round 2's review (https://github.com/ToyOSOrg/ToyOS/pull/886#issuecomment-6135209087): the AMD container reader was not built clean-room, matched a patch by its decoded ID rather than the equivalence table, and carried behaviours no document or measurement backs, among them the family-0x17 INVLPG that the no-invlpg arm showed changes nothing. It leaves this branch. - amd.rs, its tests, the embedded container, its NOTICE section, its licence text, its ROOT copy and its licence-gate row are deleted, and with them Maker, Revision::from_patch_level and the kernel's AMD trigger and INVLPG. - toyos_microcode::loader admits Intel alone; any other vendor's CPU logs "loads nothing: ToyOS has no microcode loader" and reads nothing more. - With one maker left, the intel module folds back into lib.rs, where main has the reader, and Choice is no longer generic. - The metal judge holds the AMD bench to that line on every CPU; the host test accepts it on eight CPUs, the roster the bench brings up, and refuses an AMD line that read and loaded. - issues/the-kernel-loads-no-cpu-microcode.md keeps the AMD half open with the ruling's exit; the T14 half is recorded as met. - issues/a-metal-readback-does-not-prove-which-image-wrote-its-log.md files the review's readback-provenance note. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- NOTICE | 26 -- build/build.rs | 22 +- build/licence.rs | 8 +- ...oes-not-prove-which-image-wrote-its-log.md | 32 ++ issues/the-kernel-loads-no-cpu-microcode.md | 55 +-- kernel/microcode/Cargo.toml | 2 +- .../amd-ucode/microcode_amd_fam17h.bin | Bin 22596 -> 0 bytes kernel/microcode/src/amd.rs | 170 ---------- kernel/microcode/src/amd/tests.rs | 169 ---------- kernel/microcode/src/intel.rs | 226 ------------- kernel/microcode/src/intel/tests.rs | 250 -------------- kernel/microcode/src/lib.rs | 314 ++++++++++++++---- kernel/microcode/src/tests.rs | 267 ++++++++++++++- kernel/src/arch/x86_64/microcode.rs | 122 +++---- licenses/AMD-ucode-license.txt | 64 ---- tests/checks.rs | 26 +- tests/toyos.rs | 72 ++-- 17 files changed, 682 insertions(+), 1143 deletions(-) create mode 100644 issues/a-metal-readback-does-not-prove-which-image-wrote-its-log.md delete mode 100644 kernel/microcode/amd-ucode/microcode_amd_fam17h.bin delete mode 100644 kernel/microcode/src/amd.rs delete mode 100644 kernel/microcode/src/amd/tests.rs delete mode 100644 kernel/microcode/src/intel.rs delete mode 100644 kernel/microcode/src/intel/tests.rs delete mode 100644 licenses/AMD-ucode-license.txt diff --git a/NOTICE b/NOTICE index 859ee70f1a5..e6dd7161714 100644 --- a/NOTICE +++ b/NOTICE @@ -437,32 +437,6 @@ nothing derived from it; and no reverse engineering, decompilation or disassembly. -kernel/microcode/amd-ucode/* — AMD microcode, redistributable unmodified ------------------------------------------------------------------------- - - microcode_amd_fam17h.bin 22,596 bytes, a container of seven patches for - family 0x17 CPUs, 0x0860010d for CPUID 0x00860f01 (Renoir, the - AMD bench's Ryzen 7 4800U) among them - sha256 966e4b796ec689c618868d08f8a37f347b0e7bfce4ae9df793e08471d363b7d0 - Upstream: https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git, - `amd-ucode/` at tag 20260916 - (commit ab23307cfe7f9366c819025ca3e4778299bc2db2), byte for byte - Licence text: licenses/AMD-ucode-license.txt (upstream's - `LICENSES/LICENSE.amd-ucode`, sha256 - 2103bd999f77522c5ab5fd35df0579ed00e2a6b885f6f8ebd444e97ffa482991) - SPDX-License-Identifier: LicenseRef-AMD-ucode - -The kernel embeds it whole and loads, on a CPU one of its patches names, that -patch. The licence grants redistribution free of fees on five terms: the -copyright notice and the licence travel with the software, which every x86-64 -image does as /system/share/licenses/AMD-ucode-license.txt; AMD is not -referenced in promoting a product in a way that implies AMD endorses or -certified it; no reverse engineering, decompilation or disassembly; the rights -to third-party technologies the software may implement are the -redistributor's to obtain, AMD conveying none; and U.S. and European export -law binds whoever uses it. - - tests/cavp/ — NIST's SHA-2 test vectors, CAVP SHAVS ---------------------------------------------------- diff --git a/build/build.rs b/build/build.rs index d8e2d88fac9..f0f56b6b0f4 100644 --- a/build/build.rs +++ b/build/build.rs @@ -801,13 +801,12 @@ pub const TRUST_ROOTS_LICENCE: (&str, &[u8]) = ( include_bytes!("../licenses/CDLA-Permissive-2.0-webpki-root-certs.txt"), ); -/// Where on ROOT the licence texts of the CPU microcode an x86-64 kernel -/// embeds are, and the texts: each maker's licence asks that a redistribution -/// carry it. -pub const MICROCODE_LICENCES: &[(&str, &[u8])] = &[ - ("share/licenses/Intel-microcode-license.txt", include_bytes!("../licenses/Intel-microcode-license.txt")), - ("share/licenses/AMD-ucode-license.txt", include_bytes!("../licenses/AMD-ucode-license.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. @@ -843,11 +842,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())); - let microcode = match arch { - Arch::X86_64 => MICROCODE_LICENCES, - Arch::Aarch64 => &[], - }; - root_files.extend(microcode.iter().map(|(path, text)| (path.to_string(), text.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 6f93a2b4c9c..be62f8b1429 100644 --- a/build/licence.rs +++ b/build/licence.rs @@ -90,7 +90,7 @@ const DATA_ONLY: &[&str] = &["CDLA-Permissive-2.0"]; /// 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", "LicenseRef-AMD-ucode"]; +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 @@ -596,12 +596,6 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ "NOTICE", Terms::Firmware("LicenseRef-Intel-Microcode"), ), - ( - "kernel/microcode/amd-ucode/microcode_amd_fam17h.bin", - "966e4b796ec689c618868d08f8a37f347b0e7bfce4ae9df793e08471d363b7d0", - "NOTICE", - Terms::Firmware("LicenseRef-AMD-ucode"), - ), ]; /// `NOTICE` sections that name no files, and why. 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/the-kernel-loads-no-cpu-microcode.md b/issues/the-kernel-loads-no-cpu-microcode.md index 2b8aa518577..f8ae64f6ae7 100644 --- a/issues/the-kernel-loads-no-cpu-microcode.md +++ b/issues/the-kernel-loads-no-cpu-microcode.md @@ -1,31 +1,44 @@ --- -status: assigned +status: open kind: defect opened: 2026-09-29 --- -# The kernel loads no CPU microcode +# 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). -Pull request #886 builds the loader (`toyos-microcode`, -`kernel/src/arch/x86_64/microcode.rs`) and deletes this file once its exit -holds on both benches. +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. -**Exit**: the kernel loads, early on every CPU, the newest update its -maker's embedded file holds for it, and a CPU that does not take one says so -and runs what it reads back. +**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. -- host: every decision `toyos-microcode` takes is refused by name and red - under its mutation; -- metal: the `microcode` row is green on the T14, whose eight CPUs run - Intel's 0xbe for 0x806c1 platform 7, and on the AMD bench, whose CPUs each - run the embedded container's 0x0860010d; the T14 armed with - `microcode-reseal`, and the AMD bench with its trigger write removed, each - complete the boot and red the row on a `refused` line; -- guest: every guest loads nothing, for its CPU reports a hypervisor. QEMU - 11.1.1 sets CPUID.01H:ECX[31] under TCG as under KVM, read over QMP from a - paused `qemu64` with the suite's own `-cpu` string - (https://github.com/ToyOSOrg/evidence/blob/499ad40d53eca38cfe6eb7c3689088ee57d9402e/design-measurements.txt#L256-L269), - so the guest suite booting green is the whole guest exit, and no guest test - asserts the line. +**AMD.** AMD documents MSR0000_008B (55922) and not the patch loader. A +loader built from AMD's own parser by somebody who also read another +implementation is not clean-room, and #886's was taken out for that. + +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 from a facts specification of the container format, + written by a separate reader of AMD's `amd_ucode_info` with no other + implementation read, its provenance kept in that reader's evidence and the + specification 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/kernel/microcode/Cargo.toml b/kernel/microcode/Cargo.toml index 6effcc8c296..3b44fb5d7c3 100644 --- a/kernel/microcode/Cargo.toml +++ b/kernel/microcode/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "toyos-microcode" -description = "Intel's microcode update files and AMD's containers validated and matched to a CPU as each maker reads them, whether a CPU loads at all, and the files the kernel embeds, 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" diff --git a/kernel/microcode/amd-ucode/microcode_amd_fam17h.bin b/kernel/microcode/amd-ucode/microcode_amd_fam17h.bin deleted file mode 100644 index 4011bdbb51a9f495d5cef8739411362bb6c65cc5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22596 zcmeG^2{=@3+h@jLhLLSX$TId(L?TluTZ^<%NtQ&5k``;4N*O}dM2e&ur6fxzLYqRX zO8Xuy5>g_qmj8av%v7)5_s#$PUH|v~|JS)Lp7WgZtoL*8_kCvER7(t99Qc!x+=y=R zOA2mfBsu&V+{3MuQ-lZ^TL9)sJ^@7jlc>-x0~0q!_d z4AW$25RIjL$t2F#SNIKD6S}Os43;)K+@76M(z*Q*cly|e1HvCAuUpA_bzu78bf>h@ zTxvKW(dLPB^0K4J#Nx+0BHgN+PIT-J4%nM(yFhEVRh<{(E`~)6Jwou^+F}}bb-Y7l0R2o=@>WbyaCIrov%&q&j?|2YCxH^8d742@X z>G>YH8kR}esI^_``X46b>-$}Nu=rz)iP@5i<5ONvoDqFPa%#6j^O7`^i|h7;^qltd z)!pkpgYkGOy*6Ug#;7giOD`s;V+U{7KEwtupWvW)kvw##w2qu^-seOY>zY@2A2vFe z8!xF|F_7Vm9qbW57;R_w+%j*~-gT~nGVPpKyQmCK$($TF_GXpcoszdvo41(S2e_OG zP^8CLuZ}NYr*LAX<*a*i+GWi5%)KsUGtXd(`p%S;hC6E4St^mMTbovuUgvV!x(eM@ zi!L5Nb|%8bP($o=?OC_1nU5|@UQjCY2>e){$EkE#xqZp#wL=Eqq1v3YR$5<8}JQX&1GKir_)BFPi($;*5IaB}K{WYCW`{{%j6?uNG!J z{+ip6_M!(i$~#=Nit}lt2NMpx6d9&`Bwzf*4D-$jw|fG_ug>s2<>FPoLwmuNL3bwF zsk!V-xWA#O&_4Lw({be9#SglyQ%vtstfmfHgE?=sQ+QfSM|6Vdqp@JJn66(r^*z&G zY`mwg*vF+?sM?;Y)Qb0_5>tocq=NtBVA!f}H4Ll3u-@crL%lEE-4*@8xOXJZ?6!Z}=n(pYIS#!w=1v1A65G{;^( zQWDx=n2{XWorPfx5*BU1QpL!a9Mzs}j*$%=sgu~j17)y4jI7C_GTEn9dHpwFr@1I*);o7~wQcAEs@jPB>|ted=dbb*xR5-v-{nGkIYl zK_3_i#vx!9aKr0Qf;H3hd=9`qGtnOIMZh?)zh)U&b25WOGxK4R0R|k{YoHXYD;@S_ z2))xyiD8%&3`Gt%R+@s=vqV`5-(M@QAuhrNObWx|(q}71iE5Lk)QH4jvT(-f zYF2`|9}XlEVXZ=O>9z{+j1!kWQ9*>H9X_Q>BxV!p17#hZNPM}K5l%PrIWQ0->tV{; zU(Y}=4$MV*9;rhTUglnE3~MJ%u}=jUa?qJRgY&acB5-G6GT3Io7daRQ<}3hwVTmR4 z#w9=rusv{kAS?%Cj6-L6CWT>)<|N^G;KWu&`^{jMiVG7l8k_~-QmPR~Cef5M*osin zp~Nr?H5$NuHrf`3?W3hTv0yxgv63y6P=|L}+W-%sFAuXBfHU(L1kC~Aa_NKF2!F6C z_VTDR!i``85eFl>7d-1*u2*v=y#f5C<9JABU=rHwXBe3|3-B0l<^Tpb|4x;NA=*Lj z>ifJS@}J`qkwqA=W;V7#ghNxp@j4Um3GD&j*BLn?W)r2Oht`i^fMDjtuM5_N#*rnE zHEBRQ0vd!z!k7e;MI`dp*%%>%Rl>P=C<$>p2)t+SxAMjl;;-kCD&Rb8fW2a>0UPEg zapDcoI!IWY!B_YooTmcp9pYfG7?wu|c;68sV?3IKCjhs-aPIi^H!y@Xlm2OK*g#iw zpuwJA-kxivh?Zt1adBox{xi^L&12s!CQx5oK{sC(%fylGX$E!`GynXQNVkFzIeD&pahU6{ox&E zH~>E{Cn+Wai<9A=Ggrp`p4XZ4cpaMf7o4d)td*w3V0Q^YNdlawlAw-Y!5bf^3wZ`Z zPS1x4G-N1;-~@FMM5hRD=>}}nW){ZO#Hcb{fMF&rj>$sZzhB>LRU;CT3v!x12W+6E zF?5;6rCUnbylB{MV;pO=GmIVfqfQCsYZW+|_(&VCED4DiuJvJ9V#I+P_M zM8;)RFJD&S3JVsAfUYl{&W6%a2|5D509z*lQse#Q_*MK9$2%0&s6n! zCzun>wUCcLs4Y4JR7boLL&P7^bF{9((m0?0+x589iKOa2dxp7590@o# zX^>}16t0N03`PXGfaDG6FS&HUF;o}+ZtqT@Zy*>UJ4Az_AmbwgjE@5kzPpeugs!!f zACHfy7n<&`dFHs`b!ESEIm8bAseVfz<@o11CiqS)%$H2Q*JP+NaAAzz^!#i&SNUQ$ z@ydTd0~4mD-2VMdu;thd;PVI*Jd=0BgEBpKjF0R^_%eK zy&`>Dyw8fq540TjNl#*rcl1(wb9{8 zc}ETO*ZsCb{I2&8Jp4aaf!iS#{8GY$L`JkHDB%`#)x`>_8$stpG)e^f1LOeei+KJM zRZsvq8b=ThB6BowJyHrvq%SEUIhQWOMdKm;NaU4MDaI0E@N^hXD9F~Ya(n{`_=^G9=IyaD5%|UpnaE!bCC`$?7`LSrtIe5JiX>$Yii77_dghWd$m- z4J@G4gpxPjVHqNpn8M>Zo1x_KT?P}2XR;irphKqoDZ6pz1@>+ z1A9kv{Q-uF!ZIX|ip4%QBFHfmU$BDV2+GUm$|QJa!EeKF%kaom zl1KZ3KJto}k)HjNaeDeOP7fNVkA5y#hoF2*A(AXG@;Ht(ll=wG1mP62eds*e8HY*D zW6b_Q$Nmb=s^tG3oMA3Z9$*h82c19Y8XQ!YhUcgb=Z+oW>l_Te&VlOR>l}_$kj;GC z1ko3g|EP|);oB#njVDHb!GNd}bekKZbEC+lE32?>;27)%_ zVltRCgQv@N@!JaC3CdHVps!1jILO|kfZh7Njv#20fq4kSy*A(<3+Nu#w*l8kXkF%| zLM9l)XERk+1{(}`7{*|hG6X&nG{AQ{^-5tP3AG^tt%-x~i|8HfEh)rDP#?5*2IxS9 z3I=YMjg=yfqv1Xh3*lSTNvs;a-{3Xqe?ugJH^mvS``USv5I%sdm=KBZkX(b)r#+cr zOa&Nvkh$!*$?T25$K)CArRcoy>$^4ftpnfq3!De~;L}MjvN3)_7rlVb8K8rK{hZ3> zmy&$^u02J>=wKh8h>^iQh5&uTy%7H$XFb?iQ1=ie9YA>9j_4wpQNY(nGx_>xCSM;d z06da6B_lnOVJHW71LAFerjNdIS0H^6-~`{d1^CEZNuA7oqsn9=+v6BT27~j-1v{9X z1-t~=!23H~3uY2YfEUzJQ*XG$l<$UrbDxiI;SxZ$7eiIQKn#dU~fC zrG`-zxwWUjH$lSfbsX0ap9h-JMB@OT)kHC*|N|Yy0L-{;QT9_y2hk!p$823{#*+cj=2W@cNMKTxp1`wRT$+)kw5|?d& z54g-kvhtfe{Xq_XZG(@6+adBY!BU1l*dZtegRV6XgbqzsKe0vplIwvt%j1K|d#-hn z&C7k~yuOjEo~V#nC^F3;TXXu#`=t}z&)jz6au&E2=QgN&&y4yYWfosj?f0;L7`=Lx za?1Q^fjd;X^}5w&f2lp59kgS|z!U8KDRbN(Ms2hGbi3s0^XEA!@}tk*w;ylbd7?&X zQ0+94NM`c32uCBS&F(IJys_MX; zz0*XuE*Y>#IC4*PY%C*Mg44<7lsKgfh<8gX9`)$eor0yIQ=>*C>fE2b`uwoT?K7U9 zC_l5YD1FNY?>%)!pA?^rS*%0qakeDxqnm_H)9NvJ=BL}cp8MMl`OmgP5S<|UK=F|7 z_EXf4{s$?FemAJ9fwt7CMPtM)bXQA!%$YCm9A^DPI8;$576#zk;_)ah+b!N6_XOuGjMHGVn`>?EskH-t<8cdSGK6R_%x|9@?$U!;=zy)9mb{kDC7i=DRPa*$)`Ncu! zKt9*MjTik2Km8kk56Ev0&*%Q1D6Zn+A#UFb3_6dSv zf8uh#B3^X0pYb9jXOMp#_$iYPat76r?TpvKUU~rV0sfBTO%)#}p7TqPDLBr2Lb8C& zFr*zfG9-gcfwDjfN;;G{+JHUK3Fj`)Yg54Bw_)Wd_RJB;TeK|Vxm7|3cC^f8A1@&Z_%1G&5z;wg9>gM{qRZ$420d|Wrlkoptr zz~|t}cZjEfe8(69{+LULdI!J5_Z{UB(S5%Xd4_lBAqd?fE8ZPuQig=c&a`uRaqEA7ou@4I91b(&OlJE2iJ{ z;l?XgPRJQb$Y{}MFAwdYbdnfmiw4w8S@WTKQ|8QPBjath%6YqIaF2%=CG9;+v`k;% zxVtc+EhPKqhZVk8S)V&DXDlKg zcUg*5y|mEnPz-t@X60(T!u9gN#)5R^OIz+7%X}(~P2QTBCR?vR^0yuG+YUkW@uzuN z`zBD=6;P-ZBPLU^3SNE|r=Ru*PF(P>KXyoemn(?>zi`giALb@2UL|DW@+1oqyY|DnD2Grbn&SM*1(t)MV$kiPzs zz{>>^=y7Ah${0 z@UL@|1Z^;W8^mi+yx`M!x+czt{>B&U7;p5`{4RZIf6}k8hfgCNu^)MBz%MNDxGvw9 z|7Urs{n73Eqf7k?J-el!dUk*O4v1%>_}RBOGo0Xe=T{H8WdF{*tj2%AF8aH?tj6zf z_5WgCUw`ZnunkBk7Zy$u&vWg2w&xIC{KN9HerE5zfHeaT2fl}F9*=!MuV9D#5m!-; zrN9nRV91dq!T=B))act4#^K$&I*A6Uq%_vk=|g>L8txQtPAb#5o&UHsgm_a?g<@nQ|LUENNrZ;t{GBbAdhv(N6618QS_a@rsT*f|58*8+Nel%bFKoC~gx#!)j1$7*iTh2zR$|KyK zal3QHim#2*yMA=%BlPF(5J4Nd9~ zmW^TC`}m8cdF3Vf%uQUAFrB-Z8GiV2L`$#oMC#zBYD-5(Q%)upP)O@a56{0|`PxLw zbj7mT`!*s@UG43fm+d;vd_G~cyL=t-w;l4IZHFK#K=cu_u!`Ef9P+Y;ZlRQ}+CzEl zi}JFlVr`9)l%2y+URJQ!3CPRhQO$qr|KH|iQDys?m-Um}2^kcB;6Usf&n04@+%491 z$df?dd|cz_Cdon@$eCcGoQc;Gyj%%E?iA#a;=aokF(UR_!b=Oq#e2GiIZEK~2Y&Nh z3Hkfc1u_xwmVa7a7V_6}!CyO_zgG{0c=Xr2ELaDLmnR0kvv^GucM34=?u1Tbp41zGZ8j!cT3vSE|s4qU&0wPQT~}= zycTx_jEv&d$VW|Puf)cg) zCcl1=J74pl5Z^%GJmPULh;ja_d0CeHd@lI@G#uGbLnur&Oi7U|SP#nYk`f_vlOa!9 zklzJ;e#q}aXU~g=kc7lA?hked`qmv?_9hyG4}LmdHh*I7S`INVS;5g}dcleC*-ZO| z#rNt4bOcmJt}L7DFgw%RJFa@#Fi6)jov}FgzFQtq;Zpfh+40Qf9VOuxp1lf`X~^=l z(|zK5BjeMNlb2(+O2wDWdA~$gPWPtqz7H+A@5To&@Sa)eGD+X|Qvg@~^991w)yj>p zwib%KkqxkjKf7zPc*)Kgx$pfPu0|f0&5JUfW>NM0;>D24(Vk9pKe-DM^=TxYnAY>H^gmNlDD&BO01dQ$){s(-s;tOKk&C5@}K^lIuHt?kJS99IW`tmQz|2?NNPrfVZp?+?)ub0_Cxww(Fisw7tUFdw$q z6{oAxttHJXl4e+SE%#Qp$*vc^Vzo&nCHaH>!->v4iXTUhrAHl3c=4LWQG1g$ve`g6 z{z}B%l)^1{Z+^bx=usoK)1bt`I72^5W^iop?&mJIX@}D~tv`4^jLc{+t0x&c5k#gp zI9hGqStLEKzJA{65A&O1nWgsTq>f8Mt_^{@&7muf#Sgl#bk5@!bZoQ}k=E*K4 zR|nIH!-_hcu5*P%XoWjAhOa91&q@@_3h8wF7%!WfrP=#=<1*D9X786@S334Y`p)da zroxmN*(Hu9mriTHTmHJfb(|FS$>+3SQ%cBZ+wq63d|y8`epnPYQF-*nBl}iro^F+V z+_ok#=(w@aD;3G5H;L!Jym}IHBEzMq`|g^K;NkX*Y#VbLms$&5o2Q(2p_!d>RC3MH zlu!qs1uV{#D({e$bKa_qZ+tfI-0T%Yn&Q^i#JVh@hV`BrADA`DMc%u%uG@UOq|YkP zn!^`odnbwMPPUr=Xq1Q1P}jxhrd2xYPF1u`AS5gjiCHIpNNoH~wW93=+h)%}*JEWG zla3ISyEimXE*pPogUF=OIZ+dAH5Xa03e{9Sc*WJL<;~)~BM0_uz8KQB0T0v^I2=F!l(%m(X;cLL#^80Yrc5l?crUDy!0{@&>b;iF@3UAVVDzoB+oYoz;b zf7jTl1*Llys=Q9SQQjdcl(v!^*eISOoPD|Dk?iA2U0QIk_OvGk8pY3@wdG5uUM(%U zP`aagzbA+6G-u>TPkr%_fcFxuF*8Mayi0mCK79C4!gA@9Ryjh?)Lm3~lh8w4Jk+#$ z;Ouvc0@(+H%n!dLj|99iW zisRKXP8gB5igR+`jBhMdoEg9A-jhN*uEC+MA=>fzZS?r;p|ocsnHyd)#+aRq79W*p zrZe8W+U988nVVSj39sV2HPx+0L{~2QOyaye=n&CLuWB36axt>PE0&U-_Tu=QqPyOE zOCGxTiJdjhlUnAK7{AVMqd~71Gkwh%2KCF^qm#bSFRD)}%Z?YF9}$_?99TE>!1?U+ zdxsNb7Mc5toA3b!k+2|Fe>$7=R*L)~45nz=EIZvT=fR?2`a6GxNU2PP~HcXD^? zW}T^I3LRC~xi@1$k0pEUr~2OMZ6|>e^|A7(%k_gfOG7+IH84uv3? zQ19Tm1(Cu%cM`NKPqELL$FH>3ao(ay4clZh`AT{h>4Mj=Yi{X&%$)Tq%PLLXc5Q3h ze0SXz{rRuhvYE$nRaF}2J_}l`x@p>_O9!_GbQK3hS-$SP7~dgQIPJtCGp?dS)4B7v z_T8Q>m|?p@e$yS-BNO|#6V9DeRC=f)`v)v~L8dD40_v0GUpLV|ny@X6EL zQ|ul_+g`tMOx-AUmFMTef}qczV`95cd))Y}c4q9@5KUu?w6du4xshZEp@K1HOZ{)e z=B72gpdT^1T056mNJ;Dt6xs8^nsO#->iXJl*L{YpqU)z37fe20QkL6#c=Dlw*7nG8 z%_AFgth-K)&3ZW_P%|j?2|H|BR_yFEv)V zeqY&(d#~rrx|Sp1Y=j+maeO$k!zZZ5Hr2q7seiI<;N;f(X;Qm)h~^COyyNTaqGG%z zse@3y$HUCbb}3HP84oEnHMzGa`JOeyQ*VW-d-;0%DXLTLR;5vP#HF8NLhi+iC{+$_sq-Q zvJUh2zPPutt$|yaEj3){`O~Q*2!3G-a$#?#l&4EngbXe&70Vtym}R4L<&yYt$01Wa zg4eVM44iU&UoCCo+>uQc9Zs{##??l)i%%oSiC`f$bz8FNGavK@kmQqOTTYb;HS9gI zebX?zBdwV3k>_^2zt?u;fck(zj!S4kXQ5vMObDsJYWEKV3!zXY3U_8|&1eEz*mYrr263 zneD%v&(5D8N%*Y4t=Rj1LHYfoiiw<;lj^P=bBk){cuZq+Rb&-H(n*pvzalm?PEai&9R?HssZZ+8$*znMGpNjFH7(nTT3s# zZ$FeQv@K%}B9=6fO&FeQzR;CoD_~#rTd?bZG3b;D{DfygI9+Bqwa!i zg(wdNlZCwrt(2Q4uJgO&Mw-?t4;Sq;%x+d{I-{sNqr2TRx$4}6z%%WcX}ZOms)ogI zui9p(EVtUO(YDhv{p^t3i|;<=NxI(AHh`Gf~sgQs;$9(dI5 { - revision: Revision, - bytes: &'a [u8], -} - -impl<'a> Patch<'a> { - /// Its patch ID, which is the patch level a CPU reports once it runs it. - pub fn revision(&self) -> Revision { - self.revision - } - - /// The patch as its section holds it, header first. MSR C001_0020 is - /// written the linear address of its first byte. - pub fn bytes(&self) -> &'a [u8] { - self.bytes - } - - /// Whether the CPU that loads it then drops the translations of its first - /// and last byte: on family 0x17 alone. No AMD document names the step. - pub fn leaves_its_mapping_uncached(&self) -> bool { - family(self.revision.0) == 0x17 - } -} - -/// Why a container is refused: the offset of the section that is malformed, -/// and how. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Refused { - pub at: usize, - pub why: Refusal, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Refusal { - /// The bytes end before the container's header, the table, a section's - /// header or its patch does. - Truncated { need: usize, have: usize }, - /// The container does not open with [`MAGIC`]. - Magic(u32), - /// The first section is not the equivalence table. - TableType(u32), - /// A section after the table is not a patch. - SectionType(u32), - /// The section is shorter than a patch's header. - Short(u32), - /// The patch is for one chipset (`nb_dev_id`, `sb_dev_id`). - Chipset { north: u32, south: u32 }, - /// The patch's `mc_patch_data_checksum` is not 0. - DataChecksum(u32), -} - -/// The patch in `container` a CPU with `signature` running `found` loads, or -/// why there is none. -pub fn select<'a>( - container: &'a [u8], - signature: Signature, - found: Revision, -) -> Result>, Refused> { - let have = container.len(); - let at_header = |why| Refused { at: 0, why }; - let (header, rest) = container - .split_first_chunk::
() - .ok_or(at_header(Refusal::Truncated { need: HEADER, have }))?; - let [magic, table, len] = dwords(header); - if magic != MAGIC { - return Err(at_header(Refusal::Magic(magic))); - } - if table != EQUIVALENCE_TABLE { - return Err(at_header(Refusal::TableType(table))); - } - let need = HEADER.saturating_add(len as usize); - let (_, mut rest) = - rest.split_at_checked(len as usize).ok_or(at_header(Refusal::Truncated { need, have }))?; - - let mut newest: Option> = None; - while !rest.is_empty() { - // `rest` is a suffix of `container`. - let at = have.wrapping_sub(rest.len()); - let (patch, after) = section(rest).map_err(|why| Refused { at, why })?; - if named(patch.revision.0) == signature.0 && newest.is_none_or(|n| patch.revision > n.revision) { - newest = Some(patch); - } - rest = after; - } - Ok(match newest { - None => Choice::NoMatch, - Some(patch) if patch.revision >= found => Choice::Load(patch), - Some(patch) => Choice::Current(patch.revision), - }) -} - -/// The patch the section `bytes` starts with, and the bytes after it. -fn section(bytes: &[u8]) -> Result<(Patch<'_>, &[u8]), Refusal> { - let have = bytes.len(); - let (header, rest) = - bytes.split_first_chunk::
().ok_or(Refusal::Truncated { need: SECTION, have })?; - let [kind, size] = dwords(header); - if kind != PATCH { - return Err(Refusal::SectionType(kind)); - } - let need = SECTION.saturating_add(size as usize); - let (bytes, rest) = rest.split_at_checked(size as usize).ok_or(Refusal::Truncated { need, have })?; - let (patch, _) = bytes.split_first_chunk::().ok_or(Refusal::Short(size))?; - let [_, id, _, checksum, north, south, ..]: [u32; 16] = dwords(patch); - if north != 0 || south != 0 { - return Err(Refusal::Chipset { north, south }); - } - if checksum != 0 { - return Err(Refusal::DataChecksum(checksum)); - } - Ok((Patch { revision: Revision(id), bytes }, rest)) -} - -/// The CPUID.01H:EAX a Zen patch ID names: its stepping, model, extended model -/// and extended family, over base family 0xf. -const fn named(id: u32) -> u32 { - let (stepping, model) = (id >> 8 & 0xf, id >> 12 & 0xf); - let (ext_model, ext_family) = (id >> 20 & 0xf, id >> 24); - stepping | model << 4 | 0xf << 8 | ext_model << 16 | ext_family << 20 -} - -/// The family a Zen patch ID names: base family 0xf plus its extended family. -const fn family(id: u32) -> u32 { - (id >> 24).wrapping_add(0xf) -} diff --git a/kernel/microcode/src/amd/tests.rs b/kernel/microcode/src/amd/tests.rs deleted file mode 100644 index ca666caf6e4..00000000000 --- a/kernel/microcode/src/amd/tests.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! The embedded container as AMD publishes it, and containers built here -//! to its layout for every refusal and every arm the real one does not reach. - -#![allow( - clippy::indexing_slicing, - clippy::arithmetic_side_effects, - clippy::unwrap_used, - clippy::expect_used, - clippy::panic, - reason = "a test fails by panicking" -)] - -extern crate std; - -use std::vec; -use std::vec::Vec; - -use super::*; - -/// The AMD bench's Ryzen 7 4800U: family 0x17, model 0x60, stepping 1. -const RENOIR: Signature = Signature(0x0086_0f01); - -/// The revision its firmware runs, and the one the embedded container holds -/// for it. -const FIRMWARE: Revision = Revision(0x0860_0106); -const CONTAINER: Revision = Revision(0x0860_010d); - -/// Where the Renoir patch's section starts in the container, as AMD's -/// `amd_ucode_info.py` names it (its patch 8 bytes later, 3200 bytes long). -const RENOIR_SECTION: usize = 9764; - -fn renoir_patch() -> Patch<'static> { - let Ok(Choice::Load(patch)) = select(&crate::AMD.0, RENOIR, FIRMWARE) else { - panic!("the container holds 0x0860010d for the 4800U") - }; - patch -} - -fn put(bytes: &mut [u8], at: usize, value: u32) { - bytes[at..at + 4].copy_from_slice(&value.to_le_bytes()); -} - -/// A 3200-byte patch: a header naming `id` and nothing else, and a body. -fn patch(id: u32) -> Vec { - let mut patch = vec![0; 3200]; - put(&mut patch, 4, id); - for (i, byte) in patch[PATCH_HEADER..].iter_mut().enumerate() { - *byte = i as u8; - } - patch -} - -/// A container: header, a 16-byte equivalence table, then each `(type, -/// bytes)` as a section. -fn container(sections: &[(u32, &[u8])]) -> Vec { - let mut out = Vec::new(); - for word in [MAGIC, EQUIVALENCE_TABLE, 16] { - out.extend(word.to_le_bytes()); - } - out.extend([0; 16]); - for (kind, bytes) in sections { - out.extend(kind.to_le_bytes()); - out.extend((bytes.len() as u32).to_le_bytes()); - out.extend(*bytes); - } - out -} - -fn refusal(container: &[u8]) -> Refused { - select(container, RENOIR, FIRMWARE).expect_err("a malformed container selects nothing") -} - -#[test] -fn the_4800u_at_its_firmwares_revision_loads_the_containers_patch_in_place() { - let patch = renoir_patch(); - assert_eq!(patch.revision(), CONTAINER); - let at = RENOIR_SECTION + SECTION; - assert!(patch.bytes() == &crate::AMD.0[at..at + 3200], "the bytes are not the section's patch"); -} - -#[test] -fn an_equal_revision_loads_again_and_a_newer_one_is_current() { - assert!(matches!(select(&crate::AMD.0, RENOIR, CONTAINER), Ok(Choice::Load(p)) if p.revision() == CONTAINER)); - assert_eq!(select(&crate::AMD.0, RENOIR, Revision(0x0860_010e)), Ok(Choice::Current(CONTAINER))); -} - -#[test] -fn another_stepping_is_named_by_no_patch() { - assert_eq!(select(&crate::AMD.0, Signature(0x0086_0f00), FIRMWARE), Ok(Choice::NoMatch)); -} - -#[test] -fn a_family_0x17_patch_drops_its_mapping_and_a_later_familys_does_not() { - assert!(renoir_patch().leaves_its_mapping_uncached()); - let milan = patch(0x0a00_1173); - let built = container(&[(PATCH, &milan)]); - let Ok(Choice::Load(patch)) = select(&built, Signature(0x00a0_0f11), Revision(0x0a00_1100)) else { - panic!("0x0a001173 names CPUID 0x00a00f11") - }; - assert!(!patch.leaves_its_mapping_uncached()); -} - -#[test] -fn the_newest_patch_naming_the_cpu_is_chosen_wherever_it_sits() { - let (older, newer) = (patch(0x0860_0106), patch(0x0860_0110)); - for order in [[&older[..], &newer[..]], [&newer[..], &older[..]]] { - let built = container(&[(PATCH, order[0]), (PATCH, order[1])]); - let Ok(Choice::Load(patch)) = select(&built, RENOIR, FIRMWARE) else { panic!("0x08600110 names it") }; - assert_eq!(patch.revision(), Revision(0x0860_0110)); - } -} - -#[test] -fn a_container_that_is_not_one_is_refused() { - let good = container(&[(PATCH, &patch(0x0860_010d))]); - let mut magic = good.clone(); - put(&mut magic, 0, 0x0041_4d45); - assert_eq!(refusal(&magic), Refused { at: 0, why: Refusal::Magic(0x0041_4d45) }); - let mut table = good.clone(); - put(&mut table, 4, PATCH); - assert_eq!(refusal(&table), Refused { at: 0, why: Refusal::TableType(PATCH) }); - assert_eq!(refusal(&good[..HEADER - 1]).why, Refusal::Truncated { need: HEADER, have: HEADER - 1 }); - assert_eq!(refusal(&good[..HEADER + 15]).why, Refusal::Truncated { need: HEADER + 16, have: HEADER + 15 }); -} - -#[test] -fn a_section_that_is_not_a_patch_is_refused_a_second_containers_magic_among_them() { - let first = HEADER + 16; - for kind in [EQUIVALENCE_TABLE, 2, MAGIC] { - let built = container(&[(PATCH, &patch(0x0860_010d)), (kind, &patch(0x0860_010d))]); - assert_eq!(refusal(&built), Refused { at: first + SECTION + 3200, why: Refusal::SectionType(kind) }); - } -} - -#[test] -fn a_patch_shorter_than_its_header_or_than_its_section_says_is_refused() { - let short = container(&[(PATCH, &patch(0x0860_010d)[..PATCH_HEADER - 1])]); - assert_eq!(refusal(&short).why, Refusal::Short(PATCH_HEADER as u32 - 1)); - let whole = container(&[(PATCH, &patch(0x0860_010d))]); - let cut = &whole[..whole.len() - 1]; - assert_eq!(refusal(cut).why, Refusal::Truncated { need: SECTION + 3200, have: SECTION + 3199 }); - assert_eq!(refusal(&whole[..whole.len() - 3200 - 1]).why, Refusal::Truncated { need: SECTION, have: SECTION - 1 }); -} - -#[test] -fn a_chipset_specific_patch_is_refused() { - for (at, north, south) in [(16, 1, 0), (20, 0, 1)] { - let mut chipset = patch(0x0860_010d); - put(&mut chipset, at, 1); - let built = container(&[(PATCH, &chipset)]); - assert_eq!(refusal(&built).why, Refusal::Chipset { north, south }); - } -} - -#[test] -fn a_patch_with_a_data_checksum_is_refused() { - let mut checksum = patch(0x0860_010d); - put(&mut checksum, 12, 0x0860_0100); - assert_eq!(refusal(&container(&[(PATCH, &checksum)])).why, Refusal::DataChecksum(0x0860_0100)); -} - -#[test] -fn a_malformed_section_after_the_cpus_own_refuses_the_container() { - let mut chipset = patch(0x0830_107c); - put(&mut chipset, 16, 1); - let built = container(&[(PATCH, &patch(0x0860_010d)), (PATCH, &chipset)]); - let at = HEADER + 16 + SECTION + 3200; - assert_eq!(refusal(&built), Refused { at, why: Refusal::Chipset { north: 1, south: 0 } }); -} diff --git a/kernel/microcode/src/intel.rs b/kernel/microcode/src/intel.rs deleted file mode 100644 index 3c3c0fd9880..00000000000 --- a/kernel/microcode/src/intel.rs +++ /dev/null @@ -1,226 +0,0 @@ -//! Intel's microcode update files, read as the SDM defines them. -//! -//! A file Intel publishes for one processor signature is updates laid end to -//! 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 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. - -use crate::{dwords, Choice, Revision, Signature}; - -#[cfg(test)] -mod tests; - -/// 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 -/// (Table 12-9). -const EXT_HEADER: usize = 20; -/// One extended signature: signature, processor flags, checksum (Table 12-10). -const EXT_SIGNATURE: usize = 12; -/// Total Size is "always a multiple of 1024" (Table 12-7). -const GRANULE: usize = 1024; - -/// Which bit of an update's processor flags names this CPU's platform -/// (§12.11.4). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct PlatformId(u8); - -impl PlatformId { - /// `IA32_PLATFORM_ID` (MSR 17H): bits 52:50 (Table 12-11). - pub const fn from_msr(msr: u64) -> Self { - Self((msr >> 50) as u8 & 7) - } - - const fn flag(self) -> u32 { - // `self.0` < 8: the field is private and `from_msr` masks it. - 1 << self.0 - } -} - -impl core::fmt::Display for PlatformId { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - core::fmt::Display::fmt(&self.0, f) - } -} - -/// What one CPU reports, read on that CPU. -#[derive(Clone, Copy, Debug)] -pub struct Cpu { - pub signature: Signature, - pub platform: PlatformId, - pub revision: Revision, -} - -/// One update out of a file, validated. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Update<'a> { - revision: Revision, - signature: u32, - flags: u32, - data: &'a [u8], - extended: &'a [[u8; EXT_SIGNATURE]], -} - -impl<'a> Update<'a> { - pub fn revision(&self) -> Revision { - self.revision - } - - /// The update data. `IA32_BIOS_UPDT_TRIG` is written its linear address, - /// which must be 16-byte aligned and mapped present (§12.11.6). - pub fn data(&self) -> &'a [u8] { - self.data - } - - /// The header's signature and flags, then each extended signature's - /// (Examples 12-5 and 12-6). - fn names(&self, cpu: &Cpu) -> bool { - let extended = self.extended.iter().map(|entry| { - let [signature, flags, _] = dwords(entry); - (signature, flags) - }); - core::iter::once((self.signature, self.flags)) - .chain(extended) - .any(|(signature, flags)| signature == cpu.signature.0 && flags & cpu.platform.flag() != 0) - } -} - -/// Why a file is refused: the offset of the update that is malformed, and how. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Refused { - pub at: usize, - pub why: Refusal, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Refusal { - /// The bytes end before the header does, or before Total Size says the - /// update does. - Truncated { need: usize, have: usize }, - /// Header Version is not 1, the only version §12.11 defines. - HeaderVersion(u32), - /// Loader Revision is not 1, the only loader §12.11.6 describes. - LoaderRevision(u32), - /// Data Size is 0, which Table 12-7 reads as 2000 bytes: the form of - /// updates for CPUs older than any ToyOS runs on. - ZeroDataSize, - /// Data Size is not a multiple of a dword. - DataSize(usize), - /// Total Size is not a multiple of 1024. - TotalSize(usize), - /// Total Size is smaller than the header and data it holds. - TotalBelowData { total: usize, data: usize }, - /// The header's and data's dwords sum to this, not to 0 (§12.11.5). - Checksum(u32), - /// The extended signature table is not 20 bytes and 12 for each signature - /// its count names. - ExtendedTableSize { len: usize }, - /// The extended signature table's dwords sum to this, not to 0 (§12.11.2). - ExtendedTableChecksum(u32), - /// This extended signature's checksum does not stand in for the header's - /// (Table 12-10). - ExtendedSignatureChecksum { index: usize }, -} - -/// The update in `file` to load on `cpu`, or why there is none. A file holds -/// at least one update. -pub fn select<'a>(file: &'a [u8], cpu: &Cpu) -> Result>, Refused> { - let mut newest: Option> = None; - let mut rest = file; - loop { - // `rest` is a suffix of `file`. - let at = file.len().wrapping_sub(rest.len()); - let (update, after) = parse(rest).map_err(|why| Refused { at, why })?; - if update.names(cpu) && newest.is_none_or(|n| update.revision > n.revision) { - newest = Some(update); - } - if after.is_empty() { - break; - } - rest = after; - } - Ok(match newest { - None => Choice::NoMatch, - Some(update) if update.revision > cpu.revision => Choice::Load(update), - Some(update) => Choice::Current(update.revision), - }) -} - -/// The update `bytes` starts with, and the bytes after it. -fn parse(bytes: &[u8]) -> Result<(Update<'_>, &[u8]), Refusal> { - let have = bytes.len(); - let (header, rest) = - bytes.split_first_chunk::
().ok_or(Refusal::Truncated { need: HEADER, have })?; - let [version, revision, _, signature, checksum, loader, flags, data, total, ..]: [u32; 12] = - dwords(header); - if version != 1 { - return Err(Refusal::HeaderVersion(version)); - } - if loader != 1 { - return Err(Refusal::LoaderRevision(loader)); - } - let (data, total) = (data as usize, total as usize); - if data == 0 { - return Err(Refusal::ZeroDataSize); - } - if !data.is_multiple_of(4) { - return Err(Refusal::DataSize(data)); - } - if !total.is_multiple_of(GRANULE) { - return Err(Refusal::TotalSize(total)); - } - let table = HEADER - .checked_add(data) - .and_then(|signed| total.checked_sub(signed)) - .ok_or(Refusal::TotalBelowData { total, data })?; - let truncated = Refusal::Truncated { need: total, have }; - let (data, rest) = rest.split_at_checked(data).ok_or(truncated)?; - let (table, rest) = rest.split_at_checked(table).ok_or(truncated)?; - - match sum(header).wrapping_add(sum(data)) { - 0 => {} - sum => return Err(Refusal::Checksum(sum)), - } - let extended = match table { - [] => &[], - table => extended(table, signature.wrapping_add(checksum).wrapping_add(flags))?, - }; - let update = Update { revision: Revision(revision), signature, flags, data, extended }; - Ok((update, rest)) -} - -/// The entries of an extended signature table whose update's header -/// signature, checksum and flags sum to `primary`. -fn extended(table: &[u8], primary: u32) -> Result<&[[u8; EXT_SIGNATURE]], Refusal> { - let size = Refusal::ExtendedTableSize { len: table.len() }; - let (header, entries) = table.split_first_chunk::().ok_or(size)?; - let [count, ..]: [u32; 5] = dwords(header); - if (count as usize).checked_mul(EXT_SIGNATURE) != Some(entries.len()) { - return Err(size); - } - match sum(table) { - 0 => {} - sum => return Err(Refusal::ExtendedTableChecksum(sum)), - } - let (entries, _) = entries.as_chunks::(); - // An extended signature, flags and checksum replace the header's three in - // the update it stands for, so the two triples sum alike. - for (index, entry) in entries.iter().enumerate() { - let [signature, flags, checksum] = dwords(entry); - if signature.wrapping_add(flags).wrapping_add(checksum) != primary { - return Err(Refusal::ExtendedSignatureChecksum { index }); - } - } - Ok(entries) -} - -/// Every dword of `bytes` summed, unsigned, with wrap (§12.11.5). -fn sum(bytes: &[u8]) -> u32 { - bytes.as_chunks::<4>().0.iter().fold(0, |sum, &w| sum.wrapping_add(u32::from_le_bytes(w))) -} diff --git a/kernel/microcode/src/intel/tests.rs b/kernel/microcode/src/intel/tests.rs deleted file mode 100644 index 28dce66dafd..00000000000 --- a/kernel/microcode/src/intel/tests.rs +++ /dev/null @@ -1,250 +0,0 @@ -//! 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. - -#![allow( - clippy::indexing_slicing, - clippy::arithmetic_side_effects, - clippy::unwrap_used, - clippy::expect_used, - clippy::panic, - reason = "a test fails by panicking" -)] - -extern crate std; - -use std::vec; -use std::vec::Vec; - -use super::*; - -/// `intel-ucode/06-8c-01`, pinned by its digest in `NOTICE`. -const T14_FILE: &[u8] = include_bytes!("../../intel-ucode/06-8c-01"); - -/// `intel-ucode/06-cc-02`, pinned by its digest in `NOTICE`: one update, -/// revision 0x11c, Data Size 0x2878c, flags 0x94, and an extended signature -/// table whose last entry is 0xe0652 with flags 0x94. -const CC02_FILE: &[u8] = include_bytes!("../../intel-ucode/06-cc-02"); - -/// The T14's i5-1135G7 at the revision its firmware loads. -const T14: Cpu = Cpu { - signature: Signature(0x0008_06c1), - platform: PlatformId(7), - revision: Revision(0xbe), -}; - -fn get(bytes: &[u8], at: usize) -> u32 { - u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) -} - -fn put(bytes: &mut [u8], at: usize, value: u32) { - bytes[at..at + 4].copy_from_slice(&value.to_le_bytes()); -} - -/// Rewrite the header's checksum so the header and data sum to 0 again. -fn reseal(update: &mut [u8], data: usize) { - put(update, 16, 0); - let sum = sum(&update[..HEADER + data]); - put(update, 16, sum.wrapping_neg()); -} - -/// A 2048-byte update to Tables 12-7 to 12-10: `signature` and `flags` in the -/// header, `extended` as its extended signature table, every checksum true. -fn build(signature: u32, flags: u32, revision: u32, extended: &[(u32, u32)]) -> Vec { - let table = if extended.is_empty() { 0 } else { EXT_HEADER + EXT_SIGNATURE * extended.len() }; - let data = 2048 - HEADER - table; - let mut update = vec![0; 2048]; - for (i, word) in update[HEADER..HEADER + data].as_chunks_mut::<4>().0.iter_mut().enumerate() { - *word = (i as u32).wrapping_mul(0x9e37_79b9).to_le_bytes(); - } - for (at, value) in [(0, 1), (4, revision), (12, signature), (20, 1), (24, flags)] { - put(&mut update, at, value); - } - put(&mut update, 28, data as u32); - put(&mut update, 32, 2048); - reseal(&mut update, data); - if table != 0 { - let header = get(&update, 12).wrapping_add(get(&update, 16)).wrapping_add(get(&update, 24)); - let at = HEADER + data; - put(&mut update, at, extended.len() as u32); - for (i, &(sig, flags)) in extended.iter().enumerate() { - let entry = at + EXT_HEADER + EXT_SIGNATURE * i; - put(&mut update, entry, sig); - put(&mut update, entry + 4, flags); - put(&mut update, entry + 8, header.wrapping_sub(sig).wrapping_sub(flags)); - } - let sum = sum(&update[at..]); - put(&mut update, at + 4, sum.wrapping_neg()); - } - update -} - -fn refusal(file: &[u8]) -> Refused { - select(file, &T14).expect_err("a malformed file selects nothing") -} - -#[test] -fn the_t14_runs_intels_newest_update_for_its_cpu() { - assert_eq!(select(T14_FILE, &T14), Ok(Choice::Current(Revision(0xbe)))); -} - -#[test] -fn a_t14_below_it_loads_its_data_which_follows_the_header() { - let older = Cpu { revision: Revision(0xbd), ..T14 }; - let Ok(Choice::Load(update)) = select(T14_FILE, &older) else { panic!("0xbe is newer than 0xbd") }; - assert_eq!(update.revision(), Revision(0xbe)); - assert_eq!(update.data(), &T14_FILE[HEADER..]); -} - -#[test] -fn another_platform_or_stepping_is_not_named() { - for platform in 0..7 { - assert_eq!(select(T14_FILE, &Cpu { platform: PlatformId(platform), ..T14 }), Ok(Choice::NoMatch)); - } - let stepping_2 = Cpu { signature: Signature(0x0008_06c2), ..T14 }; - assert_eq!(select(T14_FILE, &stepping_2), Ok(Choice::NoMatch)); -} - -#[test] -fn an_extended_signature_in_intels_file_names_its_cpu() { - let below = Cpu { signature: Signature(0x000e_0652), platform: PlatformId(7), revision: Revision(0x11b) }; - let Ok(Choice::Load(update)) = select(CC02_FILE, &below) else { panic!("0x11c is newer than 0x11b") }; - assert_eq!(update.revision(), Revision(0x11c)); - assert!(update.data() == &CC02_FILE[HEADER..HEADER + 0x2878c], "the data is not the file's"); - let current = Cpu { revision: Revision(0x11c), ..below }; - assert_eq!(select(CC02_FILE, ¤t), Ok(Choice::Current(Revision(0x11c)))); - assert_eq!(select(CC02_FILE, &Cpu { platform: PlatformId(0), ..below }), Ok(Choice::NoMatch)); -} - -#[test] -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)); -} - -#[test] -fn a_flipped_bit_anywhere_in_header_or_data_is_refused() { - let mut file = T14_FILE.to_vec(); - for at in (0..HEADER).chain((HEADER..file.len()).step_by(4093)) { - file[at] ^= 1; - assert!(select(&file, &T14).is_err(), "a flip at byte {at} was accepted"); - file[at] ^= 1; - } - file[4096] ^= 1; - assert_eq!(refusal(&file), Refused { at: 0, why: Refusal::Checksum(u32::MAX) }); -} - -#[test] -fn a_file_cut_short_is_refused() { - for file in [T14_FILE, CC02_FILE] { - let len = file.len(); - assert_eq!(refusal(&file[..len - 4]).why, Refusal::Truncated { need: len, have: len - 4 }); - } - assert_eq!(refusal(&T14_FILE[..HEADER - 1]).why, Refusal::Truncated { need: HEADER, have: HEADER - 1 }); -} - -#[test] -fn an_empty_file_is_refused() { - assert_eq!(refusal(&[]), Refused { at: 0, why: Refusal::Truncated { need: HEADER, have: 0 } }); -} - -#[test] -fn a_malformed_update_after_a_good_one_refuses_the_file() { - let mut file = T14_FILE.to_vec(); - file.extend_from_slice(&[0; HEADER]); - assert_eq!(refusal(&file), Refused { at: T14_FILE.len(), why: Refusal::HeaderVersion(0) }); - for have in 1..HEADER { - let why = Refusal::Truncated { need: HEADER, have }; - assert_eq!(refusal(&file[..T14_FILE.len() + have]), Refused { at: T14_FILE.len(), why }); - } -} - -#[test] -fn a_header_or_loader_version_other_than_1_is_refused() { - for (at, why) in [(0, Refusal::HeaderVersion(2)), (20, Refusal::LoaderRevision(2))] { - let mut update = build(T14.signature.0, 0x80, 0xc0, &[]); - put(&mut update, at, 2); - reseal(&mut update, 2000); - assert_eq!(refusal(&update).why, why); - } -} - -#[test] -fn a_size_zero_off_its_granule_or_inside_out_is_refused() { - let cases: [(u32, u32, Refusal); 5] = [ - (0, 0, Refusal::ZeroDataSize), - (1998, 2048, Refusal::DataSize(1998)), - (2000, 2047, Refusal::TotalSize(2047)), - (2000, 2560, Refusal::TotalSize(2560)), - (3024, 2048, Refusal::TotalBelowData { total: 2048, data: 3024 }), - ]; - for (data, total, why) in cases { - let mut update = build(T14.signature.0, 0x80, 0xc0, &[]); - put(&mut update, 28, data); - put(&mut update, 32, total); - assert_eq!(refusal(&update).why, why); - } -} - -#[test] -fn any_multiple_of_1024_is_a_total_size() { - let mut update = build(T14.signature.0, 0x80, 0xc0, &[]); - update.resize(3072, 0); - put(&mut update, 28, 3072 - HEADER as u32); - put(&mut update, 32, 3072); - reseal(&mut update, 3072 - HEADER); - assert!(matches!(select(&update, &T14), Ok(Choice::Load(_)))); -} - -#[test] -fn an_extended_signature_names_a_cpu_the_header_does_not() { - let other = (0x0008_06c2, 0x01); - let t14 = (T14.signature.0, 0x80); - for extended in [[other, t14], [t14, other]] { - let update = build(0x0009_06a3, 0x80, 0xc0, &extended); - let Ok(Choice::Load(loaded)) = select(&update, &T14) else { panic!("an entry names the T14") }; - assert_eq!(loaded.data().len(), 2048 - HEADER - EXT_HEADER - 2 * EXT_SIGNATURE); - } - let wrong_platform = build(0x0009_06a3, 0x80, 0xc0, &[(T14.signature.0, 0x01)]); - assert_eq!(select(&wrong_platform, &T14), Ok(Choice::NoMatch)); -} - -#[test] -fn a_damaged_extended_table_is_refused() { - let at = 2048 - EXT_HEADER - 2 * EXT_SIGNATURE; - let good = build(0x0009_06a3, 0x80, 0xc0, &[(0x0008_06c2, 0x01), (T14.signature.0, 0x80)]); - - let mut entry = good.clone(); - let signature = at + EXT_HEADER + EXT_SIGNATURE; - let (sig, sum) = (get(&entry, signature), get(&entry, at + 4)); - put(&mut entry, signature, sig + 1); - put(&mut entry, at + 4, sum.wrapping_sub(1)); - assert_eq!(refusal(&entry).why, Refusal::ExtendedSignatureChecksum { index: 1 }); - - let mut table = good.clone(); - table[at + 8] ^= 1; - assert_eq!(refusal(&table).why, Refusal::ExtendedTableChecksum(1)); - - for count in [1, 3] { - let mut wrong = good.clone(); - put(&mut wrong, at, count); - let len = EXT_HEADER + 2 * EXT_SIGNATURE; - assert_eq!(refusal(&wrong).why, Refusal::ExtendedTableSize { len }, "count {count}"); - } - - let mut short = build(T14.signature.0, 0x80, 0xc0, &[]); - put(&mut short, 28, 1984); - short[2032..].fill(0); - reseal(&mut short, 1984); - assert_eq!(refusal(&short).why, Refusal::ExtendedTableSize { len: 16 }); -} - -#[test] -fn the_newest_update_naming_the_cpu_is_chosen_wherever_it_sits() { - let newer = build(T14.signature.0, 0x80, 0xc0, &[]); - let other = build(T14.signature.0, 0x01, 0xd0, &[]); - for file in [[T14_FILE, &newer[..], &other[..]].concat(), [&other[..], &newer[..], T14_FILE].concat()] { - let Ok(Choice::Load(update)) = select(&file, &T14) else { panic!("0xc0 is newer than 0xbe") }; - assert_eq!(update.revision(), Revision(0xc0)); - } -} diff --git a/kernel/microcode/src/lib.rs b/kernel/microcode/src/lib.rs index 8471c753558..c9afcd03120 100644 --- a/kernel/microcode/src/lib.rs +++ b/kernel/microcode/src/lib.rs @@ -1,23 +1,24 @@ -//! CPU microcode as Intel and AMD publish it, matched to one CPU, and the -//! files the kernel loads it from, pure. +//! CPU microcode as Intel publishes it, matched to one CPU, and the file the +//! kernel loads it from, pure. //! -//! [`intel`] reads Intel's update files as SDM Vol. 3A §12.11 defines them, -//! [`amd`] AMD's containers as AMD's own parser reads them. -//! Each validates the whole file, refuses it by name and offset where any -//! part is malformed, and answers with the update a CPU loads, or why it loads -//! none. Only the documented headers are read: an update's body is its -//! maker's signed payload, handed on whole and never interpreted. +//! A file Intel publishes for one processor signature is updates laid end to +//! 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. //! -//! Before any file is read, [`Maker::of`] says whose interface a CPU's -//! revision is read through and [`may_load`] whether it loads at all. +//! 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. //! -//! [`INTEL`] and [`AMD`] are the files the kernel embeds, one per CPU ToyOS -//! has a bench for, each as its maker publishes it and pinned by its digest -//! in `NOTICE`. **On an AMD CPU whose own signature check can be made to take -//! a patch AMD did not sign (CVE-2024-36347, AMD-SB-7033), that pin is the -//! only check**: the files are inside the kernel image, so no other patch -//! reaches the loader, and a microcode source outside the image brings a -//! check of its own. +//! 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. //! @@ -41,9 +42,6 @@ use core::fmt; use toyos_cpuvuln::Vendor; -pub mod amd; -pub mod intel; - #[cfg(test)] mod tests; @@ -58,17 +56,12 @@ pub struct Aligned(pub Bytes); /// `intel-ucode/06-8c-01`, the T14's i5-1135G7. pub static INTEL: &Aligned<[u8]> = &Aligned(*include_bytes!("../intel-ucode/06-8c-01")); -/// AMD's container for the CPUs ToyOS has a bench for: -/// `amd-ucode/microcode_amd_fam17h.bin`, the AMD bench's Ryzen 7 4800U among -/// them. -pub static AMD: &Aligned<[u8]> = &Aligned(*include_bytes!("../amd-ucode/microcode_amd_fam17h.bin")); - -/// CPUID.01H:EAX, which both makers' updates name their CPU by. +/// CPUID.01H:EAX, which Intel's updates name their CPU by. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Signature(pub u32); -/// A microcode revision: Intel's update revision, AMD's patch level, each the -/// dword its maker reports it in MSR 8BH, compared unsigned. +/// 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); @@ -76,16 +69,11 @@ impl Revision { /// The highest revision, which no update loads over. pub const DISABLED: Self = Self(u32::MAX); - /// Intel's: MSR 8BH read after writing it 0 and executing CPUID.01H, whose - /// upper dword the revision is (SDM §12.11.7.1). + /// 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) } - - /// AMD's: MSR 8BH's lower dword, the patch level (55922, MSR0000_008B). - pub const fn from_patch_level(msr: u64) -> Self { - Self(msr as u32) - } } impl fmt::LowerHex for Revision { @@ -96,33 +84,22 @@ impl fmt::LowerHex for Revision { /// What a valid file holds for one CPU. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Choice { - /// The newest update naming the CPU, which it loads. - Load(Update), - /// The newest update naming the CPU is this revision, and the CPU runs - /// one its maker's rule does not load it over. +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, } -/// A maker whose files the kernel embeds: whose file a CPU chooses from, and -/// whose half of MSR 8BH its revision is. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Maker { - Intel, - Amd, -} - -impl Maker { - /// The maker a CPU of `vendor` loads from, or why it loads nothing. - pub fn of(vendor: Vendor) -> Result { - match vendor { - Vendor::Intel => Ok(Self::Intel), - Vendor::Amd => Ok(Self::Amd), - Vendor::Hygon | Vendor::Centaur | Vendor::Zhaoxin | Vendor::Unknown => { - Err(Unloaded::Vendor(vendor)) - } +/// 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)) } } } @@ -130,7 +107,7 @@ impl Maker { /// Why a CPU loads nothing, said before any file is read. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Unloaded { - /// No embedded file is from this vendor, so nothing of its CPU is read. + /// 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 @@ -142,15 +119,15 @@ pub enum Unloaded { impl fmt::Display for Unloaded { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::Vendor(_) => "no embedded file is from this vendor", - Self::Hypervisor => "a hypervisor owns this CPU's microcode", - Self::Disabled => "revision 0xffffffff, which no update loads over", - }) + 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 whose maker [`Maker::of`] names may load, from its +/// 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> { @@ -163,6 +140,210 @@ pub fn may_load(cpuid_1_ecx: u32, found: Revision) -> Result<(), Unloaded> { 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 +/// (Table 12-9). +const EXT_HEADER: usize = 20; +/// One extended signature: signature, processor flags, checksum (Table 12-10). +const EXT_SIGNATURE: usize = 12; +/// Total Size is "always a multiple of 1024" (Table 12-7). +const GRANULE: usize = 1024; + +/// Which bit of an update's processor flags names this CPU's platform +/// (§12.11.4). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PlatformId(u8); + +impl PlatformId { + /// `IA32_PLATFORM_ID` (MSR 17H): bits 52:50 (Table 12-11). + pub const fn from_msr(msr: u64) -> Self { + Self((msr >> 50) as u8 & 7) + } + + const fn flag(self) -> u32 { + // `self.0` < 8: the field is private and `from_msr` masks it. + 1 << self.0 + } +} + +impl fmt::Display for PlatformId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +/// What one CPU reports, read on that CPU. +#[derive(Clone, Copy, Debug)] +pub struct Cpu { + pub signature: Signature, + pub platform: PlatformId, + pub revision: Revision, +} + +/// One update out of a file, validated. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Update<'a> { + revision: Revision, + signature: u32, + flags: u32, + data: &'a [u8], + extended: &'a [[u8; EXT_SIGNATURE]], +} + +impl<'a> Update<'a> { + pub fn revision(&self) -> Revision { + self.revision + } + + /// The update data. `IA32_BIOS_UPDT_TRIG` is written its linear address, + /// which must be 16-byte aligned and mapped present (§12.11.6). + pub fn data(&self) -> &'a [u8] { + self.data + } + + /// The header's signature and flags, then each extended signature's + /// (Examples 12-5 and 12-6). + fn names(&self, cpu: &Cpu) -> bool { + let extended = self.extended.iter().map(|entry| { + let [signature, flags, _] = dwords(entry); + (signature, flags) + }); + core::iter::once((self.signature, self.flags)) + .chain(extended) + .any(|(signature, flags)| signature == cpu.signature.0 && flags & cpu.platform.flag() != 0) + } +} + +/// Why a file is refused: the offset of the update that is malformed, and how. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Refused { + pub at: usize, + pub why: Refusal, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Refusal { + /// The bytes end before the header does, or before Total Size says the + /// update does. + Truncated { need: usize, have: usize }, + /// Header Version is not 1, the only version §12.11 defines. + HeaderVersion(u32), + /// Loader Revision is not 1, the only loader §12.11.6 describes. + LoaderRevision(u32), + /// Data Size is 0, which Table 12-7 reads as 2000 bytes: the form of + /// updates for CPUs older than any ToyOS runs on. + ZeroDataSize, + /// Data Size is not a multiple of a dword. + DataSize(usize), + /// Total Size is not a multiple of 1024. + TotalSize(usize), + /// Total Size is smaller than the header and data it holds. + TotalBelowData { total: usize, data: usize }, + /// The header's and data's dwords sum to this, not to 0 (§12.11.5). + Checksum(u32), + /// The extended signature table is not 20 bytes and 12 for each signature + /// its count names. + ExtendedTableSize { len: usize }, + /// The extended signature table's dwords sum to this, not to 0 (§12.11.2). + ExtendedTableChecksum(u32), + /// This extended signature's checksum does not stand in for the header's + /// (Table 12-10). + ExtendedSignatureChecksum { index: usize }, +} + +/// The update in `file` to load on `cpu`, or why there is none. A file holds +/// at least one update. +pub fn select<'a>(file: &'a [u8], cpu: &Cpu) -> Result, Refused> { + let mut newest: Option> = None; + let mut rest = file; + loop { + // `rest` is a suffix of `file`. + let at = file.len().wrapping_sub(rest.len()); + let (update, after) = parse(rest).map_err(|why| Refused { at, why })?; + if update.names(cpu) && newest.is_none_or(|n| update.revision > n.revision) { + newest = Some(update); + } + if after.is_empty() { + break; + } + rest = after; + } + Ok(match newest { + None => Choice::NoMatch, + Some(update) if update.revision > cpu.revision => Choice::Load(update), + Some(update) => Choice::Current(update.revision), + }) +} + +/// The update `bytes` starts with, and the bytes after it. +fn parse(bytes: &[u8]) -> Result<(Update<'_>, &[u8]), Refusal> { + let have = bytes.len(); + let (header, rest) = + bytes.split_first_chunk::
().ok_or(Refusal::Truncated { need: HEADER, have })?; + let [version, revision, _, signature, checksum, loader, flags, data, total, ..]: [u32; 12] = + dwords(header); + if version != 1 { + return Err(Refusal::HeaderVersion(version)); + } + if loader != 1 { + return Err(Refusal::LoaderRevision(loader)); + } + let (data, total) = (data as usize, total as usize); + if data == 0 { + return Err(Refusal::ZeroDataSize); + } + if !data.is_multiple_of(4) { + return Err(Refusal::DataSize(data)); + } + if !total.is_multiple_of(GRANULE) { + return Err(Refusal::TotalSize(total)); + } + let table = HEADER + .checked_add(data) + .and_then(|signed| total.checked_sub(signed)) + .ok_or(Refusal::TotalBelowData { total, data })?; + let truncated = Refusal::Truncated { need: total, have }; + let (data, rest) = rest.split_at_checked(data).ok_or(truncated)?; + let (table, rest) = rest.split_at_checked(table).ok_or(truncated)?; + + match sum(header).wrapping_add(sum(data)) { + 0 => {} + sum => return Err(Refusal::Checksum(sum)), + } + let extended = match table { + [] => &[], + table => extended(table, signature.wrapping_add(checksum).wrapping_add(flags))?, + }; + let update = Update { revision: Revision(revision), signature, flags, data, extended }; + Ok((update, rest)) +} + +/// The entries of an extended signature table whose update's header +/// signature, checksum and flags sum to `primary`. +fn extended(table: &[u8], primary: u32) -> Result<&[[u8; EXT_SIGNATURE]], Refusal> { + let size = Refusal::ExtendedTableSize { len: table.len() }; + let (header, entries) = table.split_first_chunk::().ok_or(size)?; + let [count, ..]: [u32; 5] = dwords(header); + if (count as usize).checked_mul(EXT_SIGNATURE) != Some(entries.len()) { + return Err(size); + } + match sum(table) { + 0 => {} + sum => return Err(Refusal::ExtendedTableChecksum(sum)), + } + let (entries, _) = entries.as_chunks::(); + // An extended signature, flags and checksum replace the header's three in + // the update it stands for, so the two triples sum alike. + for (index, entry) in entries.iter().enumerate() { + let [signature, flags, checksum] = dwords(entry); + if signature.wrapping_add(flags).wrapping_add(checksum) != primary { + return Err(Refusal::ExtendedSignatureChecksum { index }); + } + } + Ok(entries) +} + /// `bytes` as little-endian dwords; a `LEN` that is not `4 * N` does not build. fn dwords(bytes: &[u8; LEN]) -> [u32; N] { const { assert!(LEN == 4 * N) }; @@ -172,3 +353,8 @@ fn dwords(bytes: &[u8; LEN]) -> [u32; N] { } dwords } + +/// Every dword of `bytes` summed, unsigned, with wrap (§12.11.5). +fn sum(bytes: &[u8]) -> u32 { + bytes.as_chunks::<4>().0.iter().fold(0, |sum, &w| sum.wrapping_add(u32::from_le_bytes(w))) +} diff --git a/kernel/microcode/src/tests.rs b/kernel/microcode/src/tests.rs index 013169d4b08..2339ba4f922 100644 --- a/kernel/microcode/src/tests.rs +++ b/kernel/microcode/src/tests.rs @@ -1,15 +1,260 @@ -//! Whether a CPU loads at all, and the Intel file the kernel embeds. +//! 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::unwrap_used, clippy::panic, reason = "a test fails by panicking")] +#![allow( + clippy::indexing_slicing, + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + reason = "a test fails by panicking" +)] + +extern crate std; + +use std::vec; +use std::vec::Vec; use super::*; +/// `intel-ucode/06-8c-01`, pinned by its digest in `NOTICE`. +const T14_FILE: &[u8] = include_bytes!("../intel-ucode/06-8c-01"); + +/// `intel-ucode/06-cc-02`, pinned by its digest in `NOTICE`: one update, +/// revision 0x11c, Data Size 0x2878c, flags 0x94, and an extended signature +/// table whose last entry is 0xe0652 with flags 0x94. +const CC02_FILE: &[u8] = include_bytes!("../intel-ucode/06-cc-02"); + +/// The T14's i5-1135G7 at the revision its firmware loads. +const T14: Cpu = Cpu { + signature: Signature(0x0008_06c1), + platform: PlatformId(7), + revision: Revision(0xbe), +}; + +fn get(bytes: &[u8], at: usize) -> u32 { + u32::from_le_bytes(bytes[at..at + 4].try_into().unwrap()) +} + +fn put(bytes: &mut [u8], at: usize, value: u32) { + bytes[at..at + 4].copy_from_slice(&value.to_le_bytes()); +} + +/// Rewrite the header's checksum so the header and data sum to 0 again. +fn reseal(update: &mut [u8], data: usize) { + put(update, 16, 0); + let sum = sum(&update[..HEADER + data]); + put(update, 16, sum.wrapping_neg()); +} + +/// A 2048-byte update to Tables 12-7 to 12-10: `signature` and `flags` in the +/// header, `extended` as its extended signature table, every checksum true. +fn build(signature: u32, flags: u32, revision: u32, extended: &[(u32, u32)]) -> Vec { + let table = if extended.is_empty() { 0 } else { EXT_HEADER + EXT_SIGNATURE * extended.len() }; + let data = 2048 - HEADER - table; + let mut update = vec![0; 2048]; + for (i, word) in update[HEADER..HEADER + data].as_chunks_mut::<4>().0.iter_mut().enumerate() { + *word = (i as u32).wrapping_mul(0x9e37_79b9).to_le_bytes(); + } + for (at, value) in [(0, 1), (4, revision), (12, signature), (20, 1), (24, flags)] { + put(&mut update, at, value); + } + put(&mut update, 28, data as u32); + put(&mut update, 32, 2048); + reseal(&mut update, data); + if table != 0 { + let header = get(&update, 12).wrapping_add(get(&update, 16)).wrapping_add(get(&update, 24)); + let at = HEADER + data; + put(&mut update, at, extended.len() as u32); + for (i, &(sig, flags)) in extended.iter().enumerate() { + let entry = at + EXT_HEADER + EXT_SIGNATURE * i; + put(&mut update, entry, sig); + put(&mut update, entry + 4, flags); + put(&mut update, entry + 8, header.wrapping_sub(sig).wrapping_sub(flags)); + } + let sum = sum(&update[at..]); + put(&mut update, at + 4, sum.wrapping_neg()); + } + update +} + +fn refusal(file: &[u8]) -> Refused { + select(file, &T14).expect_err("a malformed file selects nothing") +} + +#[test] +fn the_t14_runs_intels_newest_update_for_its_cpu() { + assert_eq!(select(T14_FILE, &T14), Ok(Choice::Current(Revision(0xbe)))); +} + +#[test] +fn a_t14_below_it_loads_its_data_which_follows_the_header() { + let older = Cpu { revision: Revision(0xbd), ..T14 }; + let Ok(Choice::Load(update)) = select(T14_FILE, &older) else { panic!("0xbe is newer than 0xbd") }; + assert_eq!(update.revision(), Revision(0xbe)); + assert_eq!(update.data(), &T14_FILE[HEADER..]); +} + +#[test] +fn another_platform_or_stepping_is_not_named() { + for platform in 0..7 { + assert_eq!(select(T14_FILE, &Cpu { platform: PlatformId(platform), ..T14 }), Ok(Choice::NoMatch)); + } + let stepping_2 = Cpu { signature: Signature(0x0008_06c2), ..T14 }; + assert_eq!(select(T14_FILE, &stepping_2), Ok(Choice::NoMatch)); +} + +#[test] +fn an_extended_signature_in_intels_file_names_its_cpu() { + let below = Cpu { signature: Signature(0x000e_0652), platform: PlatformId(7), revision: Revision(0x11b) }; + let Ok(Choice::Load(update)) = select(CC02_FILE, &below) else { panic!("0x11c is newer than 0x11b") }; + assert_eq!(update.revision(), Revision(0x11c)); + assert!(update.data() == &CC02_FILE[HEADER..HEADER + 0x2878c], "the data is not the file's"); + let current = Cpu { revision: Revision(0x11c), ..below }; + assert_eq!(select(CC02_FILE, ¤t), Ok(Choice::Current(Revision(0x11c)))); + assert_eq!(select(CC02_FILE, &Cpu { platform: PlatformId(0), ..below }), Ok(Choice::NoMatch)); +} + +#[test] +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)); +} + +#[test] +fn a_flipped_bit_anywhere_in_header_or_data_is_refused() { + let mut file = T14_FILE.to_vec(); + for at in (0..HEADER).chain((HEADER..file.len()).step_by(4093)) { + file[at] ^= 1; + assert!(select(&file, &T14).is_err(), "a flip at byte {at} was accepted"); + file[at] ^= 1; + } + file[4096] ^= 1; + assert_eq!(refusal(&file), Refused { at: 0, why: Refusal::Checksum(u32::MAX) }); +} + +#[test] +fn a_file_cut_short_is_refused() { + for file in [T14_FILE, CC02_FILE] { + let len = file.len(); + assert_eq!(refusal(&file[..len - 4]).why, Refusal::Truncated { need: len, have: len - 4 }); + } + assert_eq!(refusal(&T14_FILE[..HEADER - 1]).why, Refusal::Truncated { need: HEADER, have: HEADER - 1 }); +} + +#[test] +fn an_empty_file_is_refused() { + assert_eq!(refusal(&[]), Refused { at: 0, why: Refusal::Truncated { need: HEADER, have: 0 } }); +} + +#[test] +fn a_malformed_update_after_a_good_one_refuses_the_file() { + let mut file = T14_FILE.to_vec(); + file.extend_from_slice(&[0; HEADER]); + assert_eq!(refusal(&file), Refused { at: T14_FILE.len(), why: Refusal::HeaderVersion(0) }); + for have in 1..HEADER { + let why = Refusal::Truncated { need: HEADER, have }; + assert_eq!(refusal(&file[..T14_FILE.len() + have]), Refused { at: T14_FILE.len(), why }); + } +} + +#[test] +fn a_header_or_loader_version_other_than_1_is_refused() { + for (at, why) in [(0, Refusal::HeaderVersion(2)), (20, Refusal::LoaderRevision(2))] { + let mut update = build(T14.signature.0, 0x80, 0xc0, &[]); + put(&mut update, at, 2); + reseal(&mut update, 2000); + assert_eq!(refusal(&update).why, why); + } +} + +#[test] +fn a_size_zero_off_its_granule_or_inside_out_is_refused() { + let cases: [(u32, u32, Refusal); 5] = [ + (0, 0, Refusal::ZeroDataSize), + (1998, 2048, Refusal::DataSize(1998)), + (2000, 2047, Refusal::TotalSize(2047)), + (2000, 2560, Refusal::TotalSize(2560)), + (3024, 2048, Refusal::TotalBelowData { total: 2048, data: 3024 }), + ]; + for (data, total, why) in cases { + let mut update = build(T14.signature.0, 0x80, 0xc0, &[]); + put(&mut update, 28, data); + put(&mut update, 32, total); + assert_eq!(refusal(&update).why, why); + } +} + +#[test] +fn any_multiple_of_1024_is_a_total_size() { + let mut update = build(T14.signature.0, 0x80, 0xc0, &[]); + update.resize(3072, 0); + put(&mut update, 28, 3072 - HEADER as u32); + put(&mut update, 32, 3072); + reseal(&mut update, 3072 - HEADER); + assert!(matches!(select(&update, &T14), Ok(Choice::Load(_)))); +} + +#[test] +fn an_extended_signature_names_a_cpu_the_header_does_not() { + let other = (0x0008_06c2, 0x01); + let t14 = (T14.signature.0, 0x80); + for extended in [[other, t14], [t14, other]] { + let update = build(0x0009_06a3, 0x80, 0xc0, &extended); + let Ok(Choice::Load(loaded)) = select(&update, &T14) else { panic!("an entry names the T14") }; + assert_eq!(loaded.data().len(), 2048 - HEADER - EXT_HEADER - 2 * EXT_SIGNATURE); + } + let wrong_platform = build(0x0009_06a3, 0x80, 0xc0, &[(T14.signature.0, 0x01)]); + assert_eq!(select(&wrong_platform, &T14), Ok(Choice::NoMatch)); +} + +#[test] +fn a_damaged_extended_table_is_refused() { + let at = 2048 - EXT_HEADER - 2 * EXT_SIGNATURE; + let good = build(0x0009_06a3, 0x80, 0xc0, &[(0x0008_06c2, 0x01), (T14.signature.0, 0x80)]); + + let mut entry = good.clone(); + let signature = at + EXT_HEADER + EXT_SIGNATURE; + let (sig, sum) = (get(&entry, signature), get(&entry, at + 4)); + put(&mut entry, signature, sig + 1); + put(&mut entry, at + 4, sum.wrapping_sub(1)); + assert_eq!(refusal(&entry).why, Refusal::ExtendedSignatureChecksum { index: 1 }); + + let mut table = good.clone(); + table[at + 8] ^= 1; + assert_eq!(refusal(&table).why, Refusal::ExtendedTableChecksum(1)); + + for count in [1, 3] { + let mut wrong = good.clone(); + put(&mut wrong, at, count); + let len = EXT_HEADER + 2 * EXT_SIGNATURE; + assert_eq!(refusal(&wrong).why, Refusal::ExtendedTableSize { len }, "count {count}"); + } + + let mut short = build(T14.signature.0, 0x80, 0xc0, &[]); + put(&mut short, 28, 1984); + short[2032..].fill(0); + reseal(&mut short, 1984); + assert_eq!(refusal(&short).why, Refusal::ExtendedTableSize { len: 16 }); +} + +#[test] +fn the_newest_update_naming_the_cpu_is_chosen_wherever_it_sits() { + let newer = build(T14.signature.0, 0x80, 0xc0, &[]); + let other = build(T14.signature.0, 0x01, 0xd0, &[]); + for file in [[T14_FILE, &newer[..], &other[..]].concat(), [&other[..], &newer[..], T14_FILE].concat()] { + let Ok(Choice::Load(update)) = select(&file, &T14) else { panic!("0xc0 is newer than 0xbe") }; + assert_eq!(update.revision(), Revision(0xc0)); + } +} + #[test] -fn a_vendor_no_embedded_file_is_from_loads_nothing() { - assert_eq!(Maker::of(Vendor::Intel), Ok(Maker::Intel)); - assert_eq!(Maker::of(Vendor::Amd), Ok(Maker::Amd)); - for vendor in [Vendor::Hygon, Vendor::Centaur, Vendor::Zhaoxin, Vendor::Unknown] { - assert_eq!(Maker::of(vendor), Err(Unloaded::Vendor(vendor))); +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))); } } @@ -29,12 +274,8 @@ fn revision_0xffffffff_loads_nothing() { /// 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 = intel::Cpu { - signature: Signature(0x0008_06c1), - platform: intel::PlatformId::from_msr(7 << 50), - revision: Revision(0xbd), - }; - let Ok(Choice::Load(update)) = intel::select(&INTEL.0, &below) else { panic!("0xbe is newer than 0xbd") }; + 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/arch/x86_64/microcode.rs b/kernel/src/arch/x86_64/microcode.rs index 31057d39c3e..a650b0413e9 100644 --- a/kernel/src/arch/x86_64/microcode.rs +++ b/kernel/src/arch/x86_64/microcode.rs @@ -1,4 +1,4 @@ -//! This CPU's microcode, loaded from the files `toyos_microcode` embeds +//! 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 @@ -9,24 +9,23 @@ //! //! **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. Neither maker documents a -//! fault for a valid update at a present, canonical address. +//! 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). AMD's PPR for family 17h model 60h -//! (55922 rev 3.06, MSR0000_008B) has it return "an identification number for -//! the microcode patch that has been loaded". So a CPU that does not take an -//! update runs the revision it reads back: its line says `refused` with the +//! 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. Every CPU that reads a revision -//! runs the one the first did, or the boot stops naming it: whatever reads the -//! revision assumes one. +//! 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. @@ -35,7 +34,7 @@ use core::arch::asm; use core::fmt; use core::sync::atomic::{AtomicU64, Ordering}; -use toyos_microcode::{amd, intel, Choice, Maker, Revision, Signature, Unloaded}; +use toyos_microcode::{Choice, Cpu, PlatformId, Revision, Signature, Unloaded}; use super::cpu; use crate::log; @@ -43,11 +42,9 @@ use crate::log; /// `IA32_PLATFORM_ID`. const PLATFORM_ID: u32 = 0x17; /// `IA32_BIOS_UPDT_TRIG`. -const INTEL_TRIGGER: u32 = 0x79; -/// `IA32_BIOS_SIGN_ID`; AMD's `PATCH_LEVEL` (55922, MSR0000_008B). +const TRIGGER: u32 = 0x79; +/// `IA32_BIOS_SIGN_ID`. const SIGN_ID: u32 = 0x8b; -/// AMD's patch loader, which no AMD document in hand describes. -const AMD_TRIGGER: u32 = 0xc001_0020; /// The revision every CPU runs, as the first to read one published it. static MACHINE: AtomicU64 = AtomicU64::new(UNPUBLISHED); @@ -58,25 +55,18 @@ const UNPUBLISHED: u64 = u64::MAX; pub fn load(cpu_id: u32) { let vendor = cpu::vendor(); let (eax, _, ecx, _) = cpu::cpuid(1, 0); - let signature = Signature(eax); - let maker = match Maker::of(vendor) { - Ok(maker) => maker, - Err(why) => { - log!("microcode: cpu{cpu_id} {vendor:?} signature {eax:#x} loads nothing: {why}"); - return; - } - }; - let found = revision(maker); + 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(()) => match maker { - Maker::Intel => { - let platform = intel::PlatformId::from_msr(cpu::rdmsr(PLATFORM_ID)); - let cpu = intel::Cpu { signature, platform, revision: found }; - (Some(platform), load_intel(cpu_id, &cpu)) - } - Maker::Amd => (None, load_amd(signature, found)), - }, + 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, @@ -88,7 +78,7 @@ pub fn load(cpu_id: u32) { agree(cpu_id, running); } -/// What one CPU did with the file its maker's is. +/// What one CPU did with the embedded file. enum Outcome { Unloaded(Unloaded), NoMatch, @@ -112,8 +102,8 @@ impl fmt::Display for Outcome { } } -/// An Intel CPU's platform, where it was read. -struct Platform(Option); +/// 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 { @@ -124,22 +114,17 @@ impl fmt::Display for Platform { } } -/// The revision this CPU runs, read as its maker reports it. -fn revision(maker: Maker) -> Revision { - match maker { - Maker::Intel => { - // 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)) - } - Maker::Amd => Revision::from_patch_level(cpu::rdmsr(SIGN_ID)), - } +/// 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: &intel::Cpu) -> Outcome { - let choice = intel::select(reseal::intel_file(cpu_id), cpu).unwrap_or_else(|refused| { +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 { @@ -149,51 +134,30 @@ fn load_intel(cpu_id: u32, cpu: &intel::Cpu) -> Outcome { // 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(INTEL_TRIGGER, update.data().as_ptr()) }; - taken(Maker::Intel, update.revision()) - } - } -} - -fn load_amd(signature: Signature, found: Revision) -> Outcome { - let choice = amd::select(&toyos_microcode::AMD.0, signature, found).unwrap_or_else(|refused| { - panic!("microcode: the embedded AMD container is refused at byte {}: {:?}", refused.at, refused.why) - }); - match choice { - Choice::NoMatch => Outcome::NoMatch, - Choice::Current(newest) => Outcome::Current(newest), - Choice::Load(patch) => { - let bytes = patch.bytes().as_ptr_range(); - // SAFETY: a validated patch inside the kernel image, so its - // address is canonical and present. - unsafe { trigger(AMD_TRIGGER, bytes.start) }; - if patch.leaves_its_mapping_uncached() { - cpu::invlpg(bytes.start as u64); - cpu::invlpg(bytes.end as u64 - 1); - } - taken(Maker::Amd, patch.revision()) + unsafe { trigger(update.data().as_ptr()) }; + taken(update.revision()) } } } -/// A trigger MSR written the address of the bytes the CPU loads from. Not +/// `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 -/// `msr` is a maker's trigger on a CPU of that maker, and `at` the start of -/// one of that maker's validated updates, placed as its trigger asks. -unsafe fn trigger(msr: u32, at: *const u8) { +/// 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") msr, in("eax") at as u32, in("edx") (at >> 32) as u32, options(nostack)); + 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(maker: Maker, update: Revision) -> Outcome { - match revision(maker) { +fn taken(update: Revision) -> Outcome { + match revision() { running if running == update => Outcome::Loaded(update), running => Outcome::Refused { update, running }, } diff --git a/licenses/AMD-ucode-license.txt b/licenses/AMD-ucode-license.txt deleted file mode 100644 index d882d266bb1..00000000000 --- a/licenses/AMD-ucode-license.txt +++ /dev/null @@ -1,64 +0,0 @@ -Copyright (C) 2010-2025 Advanced Micro Devices, Inc., All rights reserved. - -Permission is hereby granted by Advanced Micro Devices, Inc. ("AMD"), -free of any license fees, to any person obtaining a copy of this -microcode in binary form (the "Software") ("You"), to install, -reproduce, copy and distribute copies of the Software and to permit -persons to whom the Software is provided to do the same, subject to -the following terms and conditions. Your use of any portion of the -Software shall constitute Your acceptance of the following terms and -conditions. If You do not agree to the following terms and conditions, -do not use, retain or redistribute any portion of the Software. - -If You redistribute this Software, You must reproduce the above -copyright notice and this license with the Software. -Without specific, prior, written permission from AMD, You may not -reference AMD or AMD products in the promotion of any product derived -from or incorporating this Software in any manner that implies that -AMD endorses or has certified such product derived from or -incorporating this Software. - -You may not reverse engineer, decompile, or disassemble this Software -or any portion thereof. - -THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY EXPRESS OR IMPLIED -WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO WARRANTIES OF -MERCHANTABILITY, NONINFRINGEMENT, TITLE, FITNESS FOR ANY PARTICULAR -PURPOSE, OR WARRANTIES ARISING FROM CONDUCT, COURSE OF DEALING, OR -USAGE OF TRADE. IN NO EVENT SHALL AMD OR ITS LICENSORS BE LIABLE FOR -ANY DAMAGES WHATSOEVER (INCLUDING, WITHOUT LIMITATION, DAMAGES FOR -LOSS OF PROFITS, BUSINESS INTERRUPTION, OR LOSS OF DATA OR -INFORMATION) ARISING OUT OF AMD'S NEGLIGENCE, GROSS NEGLIGENCE, THE -USE OF OR INABILITY TO USE THE SOFTWARE, EVEN IF AMD HAS BEEN ADVISED -OF THE POSSIBILITY OF SUCH DAMAGES. BECAUSE SOME JURISDICTIONS -PROHIBIT THE EXCLUSION OR LIMITATION OF LIABILITY FOR CONSEQUENTIAL OR -INCIDENTAL DAMAGES OR THE EXCLUSION OF IMPLIED WARRANTIES, THE ABOVE -LIMITATION MAY NOT APPLY TO YOU. - -Without limiting the foregoing, the Software may implement third party -technologies for which You must obtain licenses from parties other -than AMD. You agree that AMD has not obtained or conveyed to You, and -that You shall be responsible for obtaining the rights to use and/or -distribute the applicable underlying intellectual property rights -related to the third party technologies. These third party -technologies are not licensed hereunder. - -If You use the Software (in whole or in part), You shall adhere to all -applicable U.S., European, and other export laws, including but not -limited to the U.S. Export Administration Regulations ("EAR"), (15 -C.F.R. Sections 730 through 774), and E.U. Council Regulation (EC) No -1334/2000 of 22 June 2000. Further, pursuant to Section 740.6 of the -EAR, You hereby certify that, except pursuant to a license granted by -the United States Department of Commerce Bureau of Industry and -Security or as otherwise permitted pursuant to a License Exception -under the U.S. Export Administration Regulations ("EAR"), You will not -(1) export, re-export or release to a national of a country in Country -Groups D:1, E:1 or E:2 any restricted technology, software, or source -code You receive hereunder, or (2) export to Country Groups D:1, E:1 -or E:2 the direct product of such technology or software, if such -foreign produced direct product is subject to national security -controls as identified on the Commerce Control List (currently found -in Supplement 1 to Part 774 of EAR). For the most current Country -Group listings, or for additional information about the EAR or Your -obligations under those regulations, please refer to the U.S. Bureau -of Industry and Security?s website at ttp://www.bis.doc.gov/. diff --git a/tests/checks.rs b/tests/checks.rs index 0de900b6ff8..a01cf929391 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -422,18 +422,17 @@ mod checks { } /// [`microcode_on_metal`] against both benches' machines and the ways a - /// boot of either could fall short of its embedded update, with no guest. + /// 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 BSP: &str = "Amd signature 0x860f01 found 0x8600106 loaded 0x860010d running 0x860010d"; - const SIBLING: &str = "Amd signature 0x860f01 found 0x860010d loaded 0x860010d running 0x860010d"; - const AMD_REFUSED: &str = "Amd signature 0x860f01 found 0x8600106 refused 0x860010d running 0x8600106"; + 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, if cpu.is_multiple_of(2) { BSP } else { SIBLING }); + 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")), @@ -443,8 +442,8 @@ mod checks { 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..16).map(amd).collect::>()), 16) - .map_err(|e| format!("the AMD bench at the container's 0x860010d 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(); @@ -453,16 +452,15 @@ mod checks { 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")?; - refused("an AMD bench that refused its patch", &(0..16).map(|cpu| (cpu, AMD_REFUSED)).collect::>(), 16, "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, "holds 0xbe")?; - let mut unloaded: Vec<_> = (0..16).map(amd).collect(); - unloaded[5] = (5, "Amd signature 0x860f01 found 0x8600106 current 0x860010d running 0x8600106"); - refused("an AMD CPU left on its firmware's", &unloaded, 16, "holds 0x860010d")?; + 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, "holds 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")?; @@ -472,7 +470,7 @@ mod checks { 2, "no bench", )?; - eprintln!(" [microcode] the verdict refuses 9 machines and accepts both benches"); + eprintln!(" [microcode] the verdict refuses 8 machines and accepts both benches"); Ok(()) } diff --git a/tests/toyos.rs b/tests/toyos.rs index a5ce1fc94dd..fc9062dabd5 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -940,8 +940,8 @@ const METAL: &[(&str, metal::Metal)] = &[ metal::Metal { arms: METALCASE, judge: |b| scanout_wc(b[0].kernel().text()) }, ), ( - // Every CPU's microcode, held to the update the embedded file holds - // for it: `microcode_on_metal` says which machine is held to what. + // 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()?) }, ), @@ -5494,14 +5494,16 @@ fn ioapic_topology(log: &str) -> Result<(), String> { } /// What each CPU's `microcode:` line says: one line from each CPU this -/// machine brought up, all of one signature, none refused, and each running -/// the update its maker's embedded file holds for a bench of that signature, -/// whether it found it `current` or `loaded` it. +/// machine brought up, all of one signature, none refused, and each saying +/// what a bench of that signature is held to. /// -/// - **The T14**, 0x806c1: Intel's `06-8c-01` holds 0xbe. -/// - **The AMD bench**, 0x860f01: the embedded container holds 0x0860010d. +/// - **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 BENCHES: [(u32, u32); 2] = [(0x0008_06c1, 0xbe), (0x0086_0f01, 0x0860_010d)]; + 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(); @@ -5512,15 +5514,21 @@ fn microcode_on_metal(log: &str, cpus: u32) -> Result<(), String> { if let Some(other) = lines.iter().find(|line| line.signature != signature) { return Err(format!("cpu0 is signature {signature:#x} and {other:?} is not")); } - let Some(&(_, file)) = BENCHES.iter().find(|(bench, _)| *bench == signature) else { - return Err(format!("no bench of signature {signature:#x} names the update it runs")); + 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| !["current", "loaded"].contains(&line.outcome.as_str()) || line.running != file) - { - return Err(format!("{line:?}: the embedded file holds {file:#x} for signature {signature:#x}")); + 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} run {file:#x}", lines.len()); + eprintln!(" [microcode] {} CPUs of signature {signature:#x}: {held_to}", lines.len()); Ok(()) } @@ -5529,9 +5537,16 @@ fn microcode_on_metal(log: &str, cpus: u32) -> Result<(), String> { struct MicrocodeLine { cpu: u32, signature: u32, - /// `loaded`, `current`, `refused`, or `loads` for one that loads nothing. - outcome: String, - running: 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 @@ -5544,19 +5559,22 @@ fn microcode_lines(log: &str) -> Result, String> { 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)?, - outcome: words - .iter() - .position(|w| *w == "found") - .and_then(|at| words.get(at + 2)) - .ok_or_else(unreadable)? - .to_string(), - running: hex("running").ok_or_else(unreadable)?, + said, }); - if let Some(line) = out.last().filter(|line| line.outcome == "refused") { - return Err(format!("cpu{} refused the update it was handed and runs {:#x}: {line:?}", line.cpu, line.running)); + 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) From 56f08c24ba0bae39dca199a921a52fd7234a617f Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 13 Oct 2026 07:48:48 +0200 Subject: [PATCH 6/6] issues: the microcode issue's slug says AMD, and its exit states the clean-room rule as root CLAUDE.md gives it The kernel loads Intel's microcode at this branch, so "the kernel loads no CPU microcode" is refuted; issues/README.md's "Slugs" has the work that takes an issue up rename it and move every citation, here the security track's list. The AMD paragraph and the exit said the reader must have read no other implementation. Root CLAUDE.md lets the source be read, and asks only that one agent write a specification of facts citing no implementation and another build from it alone. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015xhaydXCVRNscoccHYFUWy --- ...as-linux-on-every-machine-toyos-supports.md | 2 +- ...md => the-kernel-loads-no-amd-microcode.md} | 18 +++++++++++------- 2 files changed, 12 insertions(+), 8 deletions(-) rename issues/{the-kernel-loads-no-cpu-microcode.md => the-kernel-loads-no-amd-microcode.md} (72%) 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-cpu-microcode.md b/issues/the-kernel-loads-no-amd-microcode.md similarity index 72% rename from issues/the-kernel-loads-no-cpu-microcode.md rename to issues/the-kernel-loads-no-amd-microcode.md index f8ae64f6ae7..a60b6d6e8b1 100644 --- a/issues/the-kernel-loads-no-cpu-microcode.md +++ b/issues/the-kernel-loads-no-amd-microcode.md @@ -18,9 +18,12 @@ 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. A -loader built from AMD's own parser by somebody who also read another -implementation is not clean-room, and #886's was taken out for that. +**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. @@ -28,10 +31,11 @@ Owner: the orchestrator. 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 from a facts specification of the container format, - written by a separate reader of AMD's `amd_ucode_info` with no other - implementation read, its provenance kept in that reader's evidence and the - specification never committed; +- 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