diff --git a/Cargo.lock b/Cargo.lock index cc05a45c729..ad4d09f1f76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1083,6 +1083,10 @@ dependencies = [ "toyos-dns", ] +[[package]] +name = "toyos-microcode" +version = "0.1.0" + [[package]] name = "toyos-mixer" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9fbbcf0c5ca..d9e61734e07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ members = [ "toyos-logstream", "toyos-manifest", "toyos-mdns", + "toyos-microcode", "toyos-mixer", "toyos-net-ip", "toyos-net-tcp", diff --git a/NOTICE b/NOTICE index 517fd70bc36..224294bec76 100644 --- a/NOTICE +++ b/NOTICE @@ -394,3 +394,21 @@ a work of theirs. Two things the tools wrote are not: machine-written metadata and a short phrase, and no licence is claimed for it. It stays because the deleted entries and their long-name runs in front of them are input a reader has to skip. + + +toyos-microcode/intel-ucode/* — Intel microcode, redistributable unmodified +--------------------------------------------------------------------------- + + 06-8c-01 112,640 bytes, update revision 0xbe for processor signature + 0x806c1, processor flags 0x80 (Tiger Lake B0/B1, the T14's + i5-1135G7) + sha256 efe83e312b90f7fe4b8f75260087edf03e048d2f4f80caef2ef631c842714bb3 + 06-cc-02 165,888 bytes, update revision 0x11c for processor signature + 0xc06c1, processor flags 0x94, and four extended signatures + sha256 4e43bb4d23c3638f8c16967d61e8f6456ae0da2ddd1da82ab579a50715147bb1 + Upstream: https://github.com/intel/Intel-Linux-Processor-Microcode-Data-Files, + `intel-ucode/` at tag microcode-20260925 + (commit bdc92abe5c499c3fd7988b76a128d05c9120a520), byte for byte + Licence text: licenses/Intel-microcode-license.txt (upstream's `license`, + sha256 03efb1491c7e899feb2665fa299363e64035e5444c1b8bc1f6ebed30de964e12) + SPDX-License-Identifier: LicenseRef-Intel-Microcode diff --git a/issues/kernel/the-kernel-is-at-least-as-secure-as-linux-on-every-machine-toyos-supports.md b/issues/kernel/the-kernel-is-at-least-as-secure-as-linux-on-every-machine-toyos-supports.md index 46ee0798305..bce8666f792 100644 --- a/issues/kernel/the-kernel-is-at-least-as-secure-as-linux-on-every-machine-toyos-supports.md +++ b/issues/kernel/the-kernel-is-at-least-as-secure-as-linux-on-every-machine-toyos-supports.md @@ -23,7 +23,7 @@ probe is a `boot-actuators` arm or a `test-actuators` `SYS_DEBUG` action. - `issues/kernel/user-pointer-checks-have-no-spectre-v1-fence-and-smap-is-optional.md` - `issues/kernel/indirect-branches-and-returns-run-without-thunks.md` - `issues/kernel/tsx-stays-as-firmware-left-it.md` -- `issues/kernel/whether-the-kernel-loads-cpu-microcode-is-the-owners.md` +- `issues/kernel/the-kernel-loads-no-cpu-microcode.md` - `issues/kernel/no-user-address-is-drawn-per-spawn.md` - `issues/kernel/the-kernel-has-no-stack-protector.md` - `issues/kernel/no-kernel-address-is-drawn-per-boot.md` diff --git a/issues/kernel/the-kernel-loads-no-cpu-microcode.md b/issues/kernel/the-kernel-loads-no-cpu-microcode.md new file mode 100644 index 00000000000..3003800d615 --- /dev/null +++ b/issues/kernel/the-kernel-loads-no-cpu-microcode.md @@ -0,0 +1,77 @@ +--- +status: open +kind: defect +opened: 2026-09-29 +--- + +# The kernel loads no CPU microcode + +The kernel is to load CPU microcode signed by the CPU's maker, pinned by +version and hash the way vendor device firmware is (owner, 2026-09-30). It +loads none, so a CPU whose BIOS ships stale microcode stays below Linux at +`Ubuntu-6.8.0-142.142` on every line that rests on microcode. + +**Exit**: the kernel loads current microcode early on every CPU, at least as +current as Linux's. + +`toyos-microcode` validates an Intel update file and picks the update for one +CPU; nothing calls it. The T14's eight CPUs run 0xbe from its firmware, which +is Intel's newest for them at `microcode-20260925`, so a load there is a no-op. + +**Where.** The kernel, on every CPU, from a file it embeds per CPU ToyOS +supports on metal. Not the loader: it runs on the BSP alone, reaching the APs +takes EFI MP Services, and the kernel reads every CPU's revision anyway. In +`percpu::init_bsp` after `idt::init` and in `percpu::init_ap` after +`control_regs::init`, before `fpu::init` on each: after the IDT, so a load that +faults reports on this kernel's channels; before anything acts on an +enumeration an update changes (CPUID.7.0:EDX, `IA32_ARCH_CAPABILITIES`, RTM +and HLE); and before `ROSTER.echo`, so `boot_aps` starting one AP at a time is +the serialisation per core that SDM Vol. 3A §12.11.6.3 asks. A CPU with +CPUID.1:ECX[31] set loads nothing, as Linux does, and `IA32_PLATFORM_ID` is +read on a GenuineIntel CPU only. The update data starts 16-byte aligned (§12.11.6), +and the trigger is an `asm!` of its own: `cpu::wrmsr` is `nomem`, and the CPU +reads the update through this write. INIT keeps an update; a hard reset clears +it (§12.11.6.1). + +**Verified.** After the trigger the CPU writes 0 to `IA32_BIOS_SIGN_ID`, runs +CPUID.01H, and panics unless it reads back the update's revision (Example +12-10). After `boot_aps`, CPUs that report different revisions panic the boot. +Each CPU logs its platform, the revision it found and the one it runs. It +stops rather than run the firmware's revision behind a log line: the file is +pinned and chosen by the CPU's own signature, platform and revision, so a CPU +that does not take it means one of those is wrong, a ToyOS defect, and a boot +that went on would run below the revision its image claims with nothing red. + +**AMD.** linux-firmware's `amd-ucode/microcode_amd_fam{17,19,1a}h.bin` is a +container (magic 0x00414d44): an equivalence table from CPUID.01H:EAX to a +processor ID, then patches. MSR C001_0020 takes a patch's address, and MSR 8B +must then read back its patch ID; family 0x17 also invalidates the patch's +pages (Linux `amd.c`, `__apply_microcode_amd`). On families 0x17, 0x19 and part +of 0x1a below a per-CPU cutoff revision the CPU's own signature check is broken +(EntrySign; `cpu_has_entrysign`, `need_sha_check`), so the hash pin is the only +check, as `amd_shas.c` is Linux's. linux-firmware's `LICENSE.amd-ucode` is +unread. ToyOS has no AMD metal: the nightly's EPYCs are KVM guests, which load +nothing. + +**Licence.** `LicenseRef-Intel-Microcode` is in no `ALLOWED` row of +`src/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 `toyos-microcode/intel-ucode/`, the +test-only `06-cc-02` included, whatever the kernel embeds, until an exception +scoped to CPU microcode admits them. + +Each step's exit, in the testing ladder's order: + +- host: the step's decisions are pure in `toyos-microcode`, each refused by + name and each red under its mutation: a hypervisor or a vendor no file + covers, `select`, a read-back that is not the update's revision, CPUs that + disagree; +- metal: a T14 boot logs platform 7 and 0xbe current on all eight CPUs; a + `boot-actuators` arm that raises only the header's revision to 0xbf and + reseals its checksum stops that boot at the read-back or at a fault, since + Intel's payload still says 0xbe; +- metal, on no machine ToyOS has: the load arm, on a CPU whose firmware runs + older microcode than the embedded file; AMD's loader, on AMD metal; +- guest: every CPU logs why it loads nothing, a hypervisor under KVM and a + vendor no file covers under TCG's `AuthenticAMD` `qemu64`, and a guest test + asserts the line. diff --git a/issues/kernel/whether-the-kernel-loads-cpu-microcode-is-the-owners.md b/issues/kernel/whether-the-kernel-loads-cpu-microcode-is-the-owners.md deleted file mode 100644 index 1e6b8e4fef2..00000000000 --- a/issues/kernel/whether-the-kernel-loads-cpu-microcode-is-the-owners.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -status: owner -kind: question -opened: 2026-09-29 ---- - -# Whether the kernel loads CPU microcode is the owner's - -Microcode is firmware that runs on the CPU, which root `CLAUDE.md` does not -carve out: it admits only device firmware that never executes on the CPU. Until -the kernel loads it, a CPU whose BIOS ships stale microcode stays below Linux -at `Ubuntu-6.8.0-142.142` on every line that rests on microcode. - -**Exit**: the owner rules. diff --git a/licenses/Intel-microcode-license.txt b/licenses/Intel-microcode-license.txt new file mode 100644 index 00000000000..cb763c91a31 --- /dev/null +++ b/licenses/Intel-microcode-license.txt @@ -0,0 +1,37 @@ +Copyright (c) 2018-2021 Intel Corporation. +All rights reserved. + +Redistribution. + +Redistribution and use in binary form, without modification, are permitted, +provided that the following conditions are met: + +1. Redistributions must reproduce the above copyright notice and the + following disclaimer in the documentation and/or other materials provided + with the distribution. + +2. Neither the name of Intel Corporation nor the names of its suppliers may + be used to endorse or promote products derived from this software without + specific prior written permission. + +3. No reverse engineering, decompilation, or disassembly of this software + is permitted. + + +"Binary form" includes any format that is commonly used for electronic +conveyance that is a reversible, bit-exact translation of binary +representation to ASCII or ISO text, for example "uuencode". + +DISCLAIMER. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/src/licence.rs b/src/licence.rs index b19a2d51b52..7550705ef4c 100644 --- a/src/licence.rs +++ b/src/licence.rs @@ -464,6 +464,18 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ "ours: a ToyOS binary this build produced (toyos-symbols/tests/real.rs)", Terms::Spdx("MIT OR Apache-2.0"), ), + ( + "toyos-microcode/intel-ucode/06-8c-01", + "efe83e312b90f7fe4b8f75260087edf03e048d2f4f80caef2ef631c842714bb3", + "NOTICE", + Terms::Spdx("LicenseRef-Intel-Microcode"), + ), + ( + "toyos-microcode/intel-ucode/06-cc-02", + "4e43bb4d23c3638f8c16967d61e8f6456ae0da2ddd1da82ab579a50715147bb1", + "NOTICE", + Terms::Spdx("LicenseRef-Intel-Microcode"), + ), ]; /// `NOTICE` sections that name no files, and why. diff --git a/toyos-microcode/Cargo.toml b/toyos-microcode/Cargo.toml new file mode 100644 index 00000000000..5a259ff2508 --- /dev/null +++ b/toyos-microcode/Cargo.toml @@ -0,0 +1,10 @@ +# A member of the host workspace (root `Cargo.toml`): +# `no_std` so the kernel can depend on it by path, and its tests run on the host. + +[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." +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" +publish = false diff --git a/toyos-microcode/intel-ucode/06-8c-01 b/toyos-microcode/intel-ucode/06-8c-01 new file mode 100644 index 00000000000..6cbf8811ef0 Binary files /dev/null and b/toyos-microcode/intel-ucode/06-8c-01 differ diff --git a/toyos-microcode/intel-ucode/06-cc-02 b/toyos-microcode/intel-ucode/06-cc-02 new file mode 100644 index 00000000000..5ed9dad6f5f Binary files /dev/null and b/toyos-microcode/intel-ucode/06-cc-02 differ diff --git a/toyos-microcode/src/lib.rs b/toyos-microcode/src/lib.rs new file mode 100644 index 00000000000..0f5d0907164 --- /dev/null +++ b/toyos-microcode/src/lib.rs @@ -0,0 +1,274 @@ +//! Intel microcode update files, read as the SDM defines them, 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. +//! +//! Only the documented header, checksums and signature table are read. The +//! update data is the maker's signed payload: handed on whole, never +//! interpreted. +//! +//! Pure: no I/O, no allocation, no `unsafe`. The caller reads the CPU. +//! +//! The file is untrusted, so nothing here may panic on it. + +#![no_std] +#![forbid(unsafe_code)] +#![deny( + clippy::indexing_slicing, + clippy::arithmetic_side_effects, + clippy::unwrap_used, + clippy::expect_used, + clippy::panic, + clippy::panic_in_result_fn, + clippy::unreachable, + clippy::todo, + clippy::unimplemented +)] + +#[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); + +/// 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 + } +} + +/// An update revision. Signed (Table 12-7): one update is newer than another +/// only when numerically larger (Example 12-10). +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Revision(pub i32); + +impl Revision { + /// `IA32_BIOS_SIGN_ID` (MSR 8BH) read after writing it 0 and executing + /// CPUID.01H: the revision is its upper dword (§12.11.7.1). + pub const fn from_sign_id(msr: u64) -> Self { + Self((msr >> 32) as u32 as i32) + } +} + +/// 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) + } +} + +/// What a valid file holds for one CPU. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Choice<'a> { + /// The newest update naming the CPU, newer than the revision it runs. + Load(Update<'a>), + /// The newest update naming the CPU is this revision, and the CPU already + /// runs it or a newer one. + Current(Revision), + /// No update in the file names the CPU. + NoMatch, +} + +/// Why a file is refused: the offset of the update that is malformed, and how. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Refused { + 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 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); + } + 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) }; + let mut dwords = [0; N]; + for (dword, bytes) in dwords.iter_mut().zip(bytes.as_chunks::<4>().0) { + *dword = u32::from_le_bytes(*bytes); + } + 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/toyos-microcode/src/tests.rs b/toyos-microcode/src/tests.rs new file mode 100644 index 00000000000..506f1a5018b --- /dev/null +++ b/toyos-microcode/src/tests.rs @@ -0,0 +1,258 @@ +//! 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)); + 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; + } + 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_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)))); +}