From ae4cc707d5a4b9e683d623f0714bc9de91471f2e Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 29 Sep 2026 08:58:29 +0200 Subject: [PATCH 1/6] toyos-cpuvuln: Linux's speculation verdicts and mitigation choice, pure (S1) Stage S1 of issues/kernel/the-kernel-mitigates-what-linux-mitigates-on-the-t14.md: a no_std, unsafe-free crate the kernel can depend on later, mapping a CPU's CPUID and MSR facts to the line Ubuntu-6.8.0-142.142 (commit 53e5d07aac02) prints in each of its 17 /sys/devices/system/cpu/vulnerabilities files, and to the mitigation state bugs.c selects under the default command line and the pinned config (/boot/config-6.8.0-142-generic, sha256 3b8533dd...5890, extracted from linux-modules-6.8.0-142-generic_6.8.0-142.142_amd64.deb and checked). Carried from the tag: cpu_vuln_whitelist and cpu_vuln_blacklist (common.c:1182-1344) row for row with x86_match_cpu's first-match rule, cpu_set_bug_bits (common.c:1414-1578), init_speculation_control (common.c:973-1012), spectre_bad_microcodes (intel.c:141-180), tsx_init's state under TSX_MODE_OFF (tsx.c:158-229), every *_select_mitigation in cpu_select_mitigations (bugs.c:149-199) and cpu_show_common's strings (bugs.c:3305-3371). Refused rather than approximated: AMD family 0x15 on and Hygon, whose amd.c/hygon.c derive speculation features from microcode tables and MSR probes the facts do not carry; and the l1tf and itlb_multihit lines of an affected CPU, which read the e820 map, kvm_intel and IA32_FEAT_CTL. Recorded in issues/kernel/the-speculation-decision-refuses-amd-from-family-0x15-and-two-lines.md. Both fixtures are provisional until S0 captures them: the T14's facts are its documented identity (i5-1135G7, family 6 model 0x8C stepping 1) and the TCG model's are read off QEMU v11.1.1's target/i386/cpu.c; the expected lines are Linux's source read by hand. Negative control, applied as a checked patch and restored: deleting GDS from the TIGERLAKE_L blacklist row builds (exit 0) and reds the T14 test (exit 101) with gather_data_sampling "Not affected" against "Mitigation: Microcode". Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 4 + Cargo.toml | 1 + ...uses-amd-from-family-0x15-and-two-lines.md | 30 + toyos-cpuvuln/Cargo.toml | 9 + toyos-cpuvuln/fixtures/provisional/t14.txt | 17 + toyos-cpuvuln/fixtures/provisional/tcg.txt | 17 + toyos-cpuvuln/src/lib.rs | 987 ++++++++++++++++++ toyos-cpuvuln/src/table.rs | 317 ++++++ toyos-cpuvuln/src/tests.rs | 139 +++ 9 files changed, 1521 insertions(+) create mode 100644 issues/kernel/the-speculation-decision-refuses-amd-from-family-0x15-and-two-lines.md create mode 100644 toyos-cpuvuln/Cargo.toml create mode 100644 toyos-cpuvuln/fixtures/provisional/t14.txt create mode 100644 toyos-cpuvuln/fixtures/provisional/tcg.txt create mode 100644 toyos-cpuvuln/src/lib.rs create mode 100644 toyos-cpuvuln/src/table.rs create mode 100644 toyos-cpuvuln/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 1e18b1d023b..affecfe3f50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1019,6 +1019,10 @@ dependencies = [ "uuid", ] +[[package]] +name = "toyos-cpuvuln" +version = "0.1.0" + [[package]] name = "toyos-desktop" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 661c2ad735b..ccad00e1e1a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "toyos-blockhold", "toyos-blockring", "toyos-bootmap", + "toyos-cpuvuln", "toyos-desktop", "toyos-dns", "toyos-dma", diff --git a/issues/kernel/the-speculation-decision-refuses-amd-from-family-0x15-and-two-lines.md b/issues/kernel/the-speculation-decision-refuses-amd-from-family-0x15-and-two-lines.md new file mode 100644 index 00000000000..77cd89a5fcc --- /dev/null +++ b/issues/kernel/the-speculation-decision-refuses-amd-from-family-0x15-and-two-lines.md @@ -0,0 +1,30 @@ +--- +status: open +kind: defect +opened: 2026-09-29 +--- + +# The speculation decision refuses AMD from family 0x15, Hygon, and two lines + +`toyos_cpuvuln::decide` answers only from its facts, and three inputs rest on +state they do not carry, at `Ubuntu-6.8.0-142.142`: + +- **AMD family 0x15 and later, and Hygon**, refused whole. `amd.c` derives + `LS_CFG_SSBD` (576-594), the Zen generations (600-640), `TSA_SQ_NO`, + `TSA_L1_NO` and `VERW_CLEAR` from a microcode table (517-530) and + `IBPB_BRTYPE`/`SBPB` from a `PRED_CMD` write probe (799-805); `hygon.c` + derives `LS_CFG_SSBD` (228-238). Each feeds SSB, RETBLEED, SRSO or TSA. +- **`l1tf` on an affected CPU**: the line reads the e820 map against + `x86_cache_bits` (`bugs.c:2538-2583`) and `kvm_intel`'s state + (`bugs.c:3074-3090`). +- **`itlb_multihit` on an affected CPU**: the line reads `IA32_FEAT_CTL` and + `CR4.VMXE` (`bugs.c:3091-3102`). + +The T14 and the TCG model reach none of them. The KVM arms of +`issues/kernel/the-kernel-mitigates-what-linux-mitigates-on-the-t14.md` (S2, +S3, S5, S6) expect "S1 over the reported facts" on the nightly's `-cpu host` +runners, and on an AMD Zen runner `decide` has no answer. + +**Exit**: each is decided from facts the kernel reads — for AMD, the family, +the microcode revision and the probe's result — and held by a fixture captured +under the pinned Linux on such a CPU. diff --git a/toyos-cpuvuln/Cargo.toml b/toyos-cpuvuln/Cargo.toml new file mode 100644 index 00000000000..ae53301094c --- /dev/null +++ b/toyos-cpuvuln/Cargo.toml @@ -0,0 +1,9 @@ +# A member of the host workspace (root `Cargo.toml`), like toyos-userbound: +# `no_std` so the kernel can depend on it by path, and its tests run on the host. + +[package] +name = "toyos-cpuvuln" +description = "Linux's verdict on each speculative-execution vulnerability and its mitigation choice, from a CPU's CPUID and MSR facts, pure." +version = "0.1.0" +edition = "2021" +license = "MIT OR Apache-2.0" diff --git a/toyos-cpuvuln/fixtures/provisional/t14.txt b/toyos-cpuvuln/fixtures/provisional/t14.txt new file mode 100644 index 00000000000..15ba1120f37 --- /dev/null +++ b/toyos-cpuvuln/fixtures/provisional/t14.txt @@ -0,0 +1,17 @@ +/sys/devices/system/cpu/vulnerabilities/gather_data_sampling:Mitigation: Microcode +/sys/devices/system/cpu/vulnerabilities/indirect_target_selection:Mitigation: Aligned branch/return thunks +/sys/devices/system/cpu/vulnerabilities/itlb_multihit:Not affected +/sys/devices/system/cpu/vulnerabilities/l1tf:Not affected +/sys/devices/system/cpu/vulnerabilities/mds:Not affected +/sys/devices/system/cpu/vulnerabilities/meltdown:Not affected +/sys/devices/system/cpu/vulnerabilities/mmio_stale_data:Not affected +/sys/devices/system/cpu/vulnerabilities/reg_file_data_sampling:Not affected +/sys/devices/system/cpu/vulnerabilities/retbleed:Not affected +/sys/devices/system/cpu/vulnerabilities/spec_rstack_overflow:Not affected +/sys/devices/system/cpu/vulnerabilities/spec_store_bypass:Mitigation: Speculative Store Bypass disabled via prctl +/sys/devices/system/cpu/vulnerabilities/spectre_v1:Mitigation: usercopy/swapgs barriers and __user pointer sanitization +/sys/devices/system/cpu/vulnerabilities/spectre_v2:Mitigation: Enhanced / Automatic IBRS; IBPB: conditional; PBRSB-eIBRS: SW sequence; BHI: SW loop, KVM: SW loop +/sys/devices/system/cpu/vulnerabilities/srbds:Not affected +/sys/devices/system/cpu/vulnerabilities/tsa:Not affected +/sys/devices/system/cpu/vulnerabilities/tsx_async_abort:Not affected +/sys/devices/system/cpu/vulnerabilities/vmscape:Not affected diff --git a/toyos-cpuvuln/fixtures/provisional/tcg.txt b/toyos-cpuvuln/fixtures/provisional/tcg.txt new file mode 100644 index 00000000000..1019ef0e0c1 --- /dev/null +++ b/toyos-cpuvuln/fixtures/provisional/tcg.txt @@ -0,0 +1,17 @@ +/sys/devices/system/cpu/vulnerabilities/gather_data_sampling:Not affected +/sys/devices/system/cpu/vulnerabilities/indirect_target_selection:Not affected +/sys/devices/system/cpu/vulnerabilities/itlb_multihit:Not affected +/sys/devices/system/cpu/vulnerabilities/l1tf:Not affected +/sys/devices/system/cpu/vulnerabilities/mds:Not affected +/sys/devices/system/cpu/vulnerabilities/meltdown:Not affected +/sys/devices/system/cpu/vulnerabilities/mmio_stale_data:Not affected +/sys/devices/system/cpu/vulnerabilities/reg_file_data_sampling:Not affected +/sys/devices/system/cpu/vulnerabilities/retbleed:Not affected +/sys/devices/system/cpu/vulnerabilities/spec_rstack_overflow:Not affected +/sys/devices/system/cpu/vulnerabilities/spec_store_bypass:Not affected +/sys/devices/system/cpu/vulnerabilities/spectre_v1:Mitigation: usercopy/swapgs barriers and __user pointer sanitization +/sys/devices/system/cpu/vulnerabilities/spectre_v2:Mitigation: Retpolines; STIBP: disabled; RSB filling; PBRSB-eIBRS: Not affected; BHI: Not affected +/sys/devices/system/cpu/vulnerabilities/srbds:Not affected +/sys/devices/system/cpu/vulnerabilities/tsa:Not affected +/sys/devices/system/cpu/vulnerabilities/tsx_async_abort:Not affected +/sys/devices/system/cpu/vulnerabilities/vmscape:Not affected diff --git a/toyos-cpuvuln/src/lib.rs b/toyos-cpuvuln/src/lib.rs new file mode 100644 index 00000000000..f91715d9517 --- /dev/null +++ b/toyos-cpuvuln/src/lib.rs @@ -0,0 +1,987 @@ +//! Linux's decision about a CPU's speculative-execution vulnerabilities, pure. +//! +//! [`decide`] maps what a CPU reports about itself ([`Facts`]) to the line the +//! pinned Linux prints in each `/sys/devices/system/cpu/vulnerabilities/*` file +//! ([`Decision::line`]) and to the mitigation state it selects ([`Decision`]'s +//! fields), under the default command line and the pinned config. That Linux is +//! tag `Ubuntu-6.8.0-142.142` (commit 53e5d07aac02) of Ubuntu's noble kernel +//! with `/boot/config-6.8.0-142-generic`; every `path:line` here is read there, +//! and `common.c`, `bugs.c`, `intel.c` and `tsx.c` are in `arch/x86/kernel/cpu/`. +//! +//! Parity is exact or refused: an input whose answer rests on something the +//! facts do not carry is [`Refused`] whole, and a line that does is +//! [`Unmodelled`], never approximated. +//! +//! Pure: no I/O, no allocation, no `unsafe`. The caller reads the facts. + +#![no_std] +#![forbid(unsafe_code)] + +use core::fmt; + +mod table; +#[cfg(test)] +mod tests; + +use table::{ + blacklisted, whitelisted, GDS, ITS, MMIO, MMIO_SBDS, MSBDS_ONLY, NO_BHI, + NO_EIBRS_PBRSB, NO_ITLB_MULTIHIT, NO_L1TF, NO_MDS, NO_MELTDOWN, NO_MMIO, NO_SPECTRE_V2, + NO_SPECULATION, NO_SSB, RETBLEED, RFDS, SRBDS, VMSCAPE, +}; + +/// The vendors Linux's tables name, by CPUID.0's identification string +/// (`get_cpu_vendor`, `common.c:912-935`, over each `cpu_dev`'s `c_ident`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Vendor { + Intel, + Amd, + Hygon, + Centaur, + Zhaoxin, + Unknown, +} + +impl Vendor { + /// CPUID.0's EBX, EDX and ECX bytes, in that order. + pub const fn from_id(id: &[u8; 12]) -> Self { + match id { + b"GenuineIntel" => Self::Intel, + b"AuthenticAMD" => Self::Amd, + b"HygonGenuine" => Self::Hygon, + b"CentaurHauls" => Self::Centaur, + b" Shanghai " => Self::Zhaoxin, + _ => Self::Unknown, + } + } +} + +/// What the boot CPU reports, read the way Linux reads it. +#[derive(Clone, Copy, Debug)] +pub struct Facts { + pub vendor: Vendor, + /// CPUID.1:EAX; family, model and stepping are derived from it as + /// `arch/x86/lib/cpu.c` derives them. + pub signature: u32, + /// The microcode revision as `/proc/cpuinfo` reports it. + pub microcode: u32, + pub cpuid_1_ecx: u32, + pub cpuid_7_0_ebx: u32, + pub cpuid_7_0_edx: u32, + /// Read wherever leaf 7 exists (`scattered.c:59-79`). + pub cpuid_7_2_edx: u32, + /// 0 unless CPUID.0x80000000:EAX reaches 0x80000008 (`common.c:1055-1084`). + pub cpuid_8000_0008_ebx: u32, + /// 0 unless CPUID.0x80000000:EAX reaches 0x80000021 (`common.c:1055-1084`). + pub cpuid_8000_0021_eax: u32, + /// `IA32_ARCH_CAPABILITIES` (0x10A), 0 unless CPUID.(7,0):EDX[29] + /// enumerates it (`common.c:1353-1361`): the read is `#GP` there. + pub arch_capabilities: u64, + /// `IA32_MCU_OPT_CTRL` (0x123), present exactly where + /// `ARCH_CAPABILITIES.GDS_CTRL` enumerates it. + pub mcu_opt_ctrl: Option, + /// More than one thread per core, every sibling online: `sched_smt_active()`, + /// which under the default command line is also `cpu_smt_possible()`. + pub smt: bool, +} + +// Linux's feature bits (`arch/x86/include/asm/cpufeatures.h`, +// `scattered.c:26-57`) at their CPUID positions. +const CPUID_1_ECX_AVX: u32 = 1 << 28; +const CPUID_1_ECX_RDRAND: u32 = 1 << 30; +const CPUID_1_ECX_HYPERVISOR: u32 = 1 << 31; +const CPUID_7_0_EBX_RTM: u32 = 1 << 11; +const CPUID_7_0_EBX_RDSEED: u32 = 1 << 18; +const CPUID_7_0_EDX_SRBDS_CTRL: u32 = 1 << 9; +const CPUID_7_0_EDX_MD_CLEAR: u32 = 1 << 10; +const CPUID_7_0_EDX_RTM_ALWAYS_ABORT: u32 = 1 << 11; +const CPUID_7_0_EDX_SPEC_CTRL: u32 = 1 << 26; +const CPUID_7_0_EDX_INTEL_STIBP: u32 = 1 << 27; +const CPUID_7_0_EDX_FLUSH_L1D: u32 = 1 << 28; +const CPUID_7_0_EDX_ARCH_CAPABILITIES: u32 = 1 << 29; +const CPUID_7_0_EDX_SPEC_CTRL_SSBD: u32 = 1 << 31; +const CPUID_7_2_EDX_RRSBA_CTRL: u32 = 1 << 2; +const CPUID_7_2_EDX_BHI_CTRL: u32 = 1 << 4; +const CPUID_8000_0008_EBX_AMD_IBPB: u32 = 1 << 12; +const CPUID_8000_0008_EBX_AMD_IBRS: u32 = 1 << 14; +const CPUID_8000_0008_EBX_AMD_STIBP: u32 = 1 << 15; +const CPUID_8000_0008_EBX_AMD_STIBP_ALWAYS_ON: u32 = 1 << 17; +const CPUID_8000_0008_EBX_AMD_SSBD: u32 = 1 << 24; +const CPUID_8000_0008_EBX_VIRT_SSBD: u32 = 1 << 25; +const CPUID_8000_0008_EBX_AMD_SSB_NO: u32 = 1 << 26; +const CPUID_8000_0008_EBX_BTC_NO: u32 = 1 << 29; +const CPUID_8000_0021_EAX_AUTOIBRS: u32 = 1 << 8; + +// `arch/x86/include/asm/msr-index.h:46-54,101-183,215`. +const ARCH_CAP_RDCL_NO: u64 = 1 << 0; +const ARCH_CAP_IBRS_ALL: u64 = 1 << 1; +const ARCH_CAP_RSBA: u64 = 1 << 2; +const ARCH_CAP_SSB_NO: u64 = 1 << 4; +const ARCH_CAP_MDS_NO: u64 = 1 << 5; +const ARCH_CAP_PSCHANGE_MC_NO: u64 = 1 << 6; +const ARCH_CAP_TSX_CTRL_MSR: u64 = 1 << 7; +const ARCH_CAP_TAA_NO: u64 = 1 << 8; +const ARCH_CAP_SBDR_SSDP_NO: u64 = 1 << 13; +const ARCH_CAP_FBSDP_NO: u64 = 1 << 14; +const ARCH_CAP_PSDP_NO: u64 = 1 << 15; +const ARCH_CAP_FB_CLEAR: u64 = 1 << 17; +const ARCH_CAP_RRSBA: u64 = 1 << 19; +const ARCH_CAP_PBRSB_NO: u64 = 1 << 24; +const ARCH_CAP_GDS_CTRL: u64 = 1 << 25; +const ARCH_CAP_GDS_NO: u64 = 1 << 26; +const ARCH_CAP_RFDS_NO: u64 = 1 << 27; +const ARCH_CAP_RFDS_CLEAR: u64 = 1 << 28; +const ARCH_CAP_ITS_NO: u64 = 1 << 62; +const GDS_MITG_LOCKED: u64 = 1 << 5; +pub const SPEC_CTRL_IBRS: u64 = 1 << 0; +pub const SPEC_CTRL_STIBP: u64 = 1 << 1; +pub const SPEC_CTRL_RRSBA_DIS_S: u64 = 1 << 6; +pub const SPEC_CTRL_BHI_DIS_S: u64 = 1 << 10; + +/// `boot_cpu_data`'s identity: `x86_vendor`, `x86`, `x86_model`, +/// `x86_stepping`. +pub(crate) struct Ident { + pub(crate) vendor: Vendor, + pub(crate) family: u32, + pub(crate) model: u32, + pub(crate) stepping: u32, +} + +impl Ident { + /// `x86_family`, `x86_model` and `x86_stepping` (`arch/x86/lib/cpu.c:6-37`). + fn new(vendor: Vendor, sig: u32) -> Self { + let mut family = (sig >> 8) & 0xf; + if family == 0xf { + family += (sig >> 20) & 0xff; + } + let mut model = (sig >> 4) & 0xf; + if family >= 0x6 { + model += ((sig >> 16) & 0xf) << 4; + } + Self { vendor, family, model, stepping: sig & 0xf } + } +} + +/// An input whose every line rests on features Linux's vendor code synthesizes +/// from state the facts do not carry. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Refused { + /// `amd.c` derives `LS_CFG_SSBD` (576-594), the Zen generations (600-640), + /// `TSA_*_NO` and `VERW_CLEAR` (517-530) and `IBPB_BRTYPE` (799-805) from + /// the family, the microcode revision and MSR probes, from family 0x15 on. + AmdFamily(u32), + /// `hygon.c` derives `LS_CFG_SSBD` (228-238) from an MSR probe. + Hygon, +} + +/// An affected CPU's line that rests on state the facts do not carry. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Unmodelled { + /// The e820 map against `x86_cache_bits` (`bugs.c:2538-2583`) and + /// `kvm_intel`'s state (`bugs.c:3074-3090`). + L1tf, + /// `IA32_FEAT_CTL` and `CR4.VMXE` (`bugs.c:3091-3102`). + ItlbMultihit, +} + +/// The vulnerabilities files, in the order `drivers/base/cpu.c:615-631` lists +/// them. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Vuln { + Meltdown, + SpectreV1, + SpectreV2, + SpecStoreBypass, + L1tf, + Mds, + TsxAsyncAbort, + ItlbMultihit, + Srbds, + MmioStaleData, + Retbleed, + SpecRstackOverflow, + GatherDataSampling, + RegFileDataSampling, + IndirectTargetSelection, + Tsa, + Vmscape, +} + +impl Vuln { + pub const ALL: [Self; 17] = [ + Self::Meltdown, + Self::SpectreV1, + Self::SpectreV2, + Self::SpecStoreBypass, + Self::L1tf, + Self::Mds, + Self::TsxAsyncAbort, + Self::ItlbMultihit, + Self::Srbds, + Self::MmioStaleData, + Self::Retbleed, + Self::SpecRstackOverflow, + Self::GatherDataSampling, + Self::RegFileDataSampling, + Self::IndirectTargetSelection, + Self::Tsa, + Self::Vmscape, + ]; + + /// The file's name under `/sys/devices/system/cpu/vulnerabilities/`. + pub const fn name(self) -> &'static str { + match self { + Self::Meltdown => "meltdown", + Self::SpectreV1 => "spectre_v1", + Self::SpectreV2 => "spectre_v2", + Self::SpecStoreBypass => "spec_store_bypass", + Self::L1tf => "l1tf", + Self::Mds => "mds", + Self::TsxAsyncAbort => "tsx_async_abort", + Self::ItlbMultihit => "itlb_multihit", + Self::Srbds => "srbds", + Self::MmioStaleData => "mmio_stale_data", + Self::Retbleed => "retbleed", + Self::SpecRstackOverflow => "spec_rstack_overflow", + Self::GatherDataSampling => "gather_data_sampling", + Self::RegFileDataSampling => "reg_file_data_sampling", + Self::IndirectTargetSelection => "indirect_target_selection", + Self::Tsa => "tsa", + Self::Vmscape => "vmscape", + } + } +} + +/// `tsx_ctrl_state` after `tsx_init` (`tsx.c:158-229`) under +/// `CONFIG_X86_INTEL_TSX_MODE_OFF=y`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Tsx { + /// No `IA32_TSX_CTRL`: RTM stays as CPUID enumerates it. + NotSupported, + /// RTM always aborts; its enumeration is cleared. + RtmAlwaysAbort, + /// `IA32_TSX_CTRL` disables RTM and clears its enumeration. + Disable, +} + +/// `spectre_v2_enabled` (`bugs.c:1860-1946`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SpectreV2 { + None, + Retpoline, + Eibrs, + Ibrs, +} + +/// What `bhi_select_mitigation` (`bugs.c:1831-1858`) chose, as +/// `spectre_bhi_state` (`bugs.c:3220-3236`) reads it. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Bhi { + NotAffected, + /// `SPEC_CTRL.BHI_DIS_S`. + BhiDisS, + /// `clear_bhb_loop` on syscall entry and VM exit. + SwLoop, + /// Retpolines with RRSBA disabled. + Retpoline, + Vulnerable, +} + +/// Whether and how `switch_mm` issues IBPB (`bugs.c:1531-1552`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Ibpb { + /// The CPU has no IBPB. + Absent, + Disabled, + /// Between two processes when either asked for it by prctl. + Conditional, +} + +/// `spectre_v2_user_stibp` (`bugs.c:1492-1591`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Stibp { + None, + Prctl, + StrictPreferred, +} + +/// `retbleed_mitigation` (`bugs.c:1054-1206`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Retbleed { + None, + Unret, + Ibrs, + Eibrs, +} + +/// `ssb_mode` (`bugs.c:2172-2238`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Ssb { + None, + Prctl, +} + +/// `mds_mitigation` (`bugs.c:266-289`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Mds { + Off, + Full, + Vmwerv, +} + +/// `taa_mitigation` (`bugs.c:327-386`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Taa { + Off, + UcodeNeeded, + Verw, + TsxDisabled, +} + +/// `mmio_mitigation` (`bugs.c:424-480`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Mmio { + Off, + UcodeNeeded, + Verw, +} + +/// `rfds_mitigation` (`bugs.c:520-535`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Rfds { + Off, + Verw, + UcodeNeeded, +} + +/// `srbds_mitigation` where the CPU has SRBDS (`bugs.c:678-697`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Srbds { + UcodeNeeded, + Full, + TsxOff, + Hypervisor, +} + +/// `gds_mitigation` where the CPU has GDS (`bugs.c:814-866`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Gds { + UcodeNeeded, + /// `IA32_MCU_OPT_CTRL.GDS_MITG_DIS` cleared on every CPU. + Full, + /// Firmware locked the mitigation on. + FullLocked, + Hypervisor, +} + +/// `its_mitigation` (`bugs.c:1254-1334`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Its { + Off, + AlignedThunks, +} + +/// `vmscape_mitigation` (`bugs.c:2849-2869`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Vmscape { + None, + IbpbExitToUser, +} + +/// The bugs `cpu_set_bug_bits` (`common.c:1414-1578`) sets that a line or a +/// selection reads. +#[derive(Clone, Copy, Debug, Default)] +struct Bugs { + itlb_multihit: bool, + spectre_v1: bool, + spectre_v2: bool, + ssb: bool, + ibrs_enhanced: bool, + eibrs_pbrsb: bool, + mds: bool, + msbds_only: bool, + taa: bool, + srbds: bool, + mmio_stale_data: bool, + mmio_unknown: bool, + retbleed: bool, + gds: bool, + rfds: bool, + bhi: bool, + vmscape: bool, + its: bool, + meltdown: bool, + l1tf: bool, +} + +/// The features Linux has when it selects, after `init_speculation_control` +/// (`common.c:973-1012`) and `early_init_intel`'s microcode check +/// (`intel.c:296-309`). +struct Caps { + hypervisor: bool, + ibrs: bool, + ibpb: bool, + stibp: bool, + ssbd: bool, + autoibrs: bool, +} + +/// Linux's selection for one machine. +#[derive(Clone, Copy, Debug)] +pub struct Decision { + pub tsx: Tsx, + pub spectre_v2: SpectreV2, + pub bhi: Bhi, + pub ibpb: Ibpb, + /// `USE_IBRS_FW`: IBRS around firmware calls. + pub ibrs_fw: bool, + pub stibp: Stibp, + pub retbleed: Retbleed, + pub ssb: Ssb, + pub mds: Mds, + pub taa: Taa, + pub mmio: Mmio, + pub rfds: Rfds, + pub srbds: Option, + pub gds: Option, + pub its: Its, + pub vmscape: Vmscape, + /// `CLEAR_CPU_BUF`: `verw` on every return to user. + pub clear_cpu_buf: bool, + /// `EFER.AUTOIBRS` in place of `SPEC_CTRL.IBRS`. + pub efer_autoibrs: bool, + /// The bits `bugs.c` ORs into `x86_spec_ctrl_base`; the MSR's others are + /// the hardware's. + pub spec_ctrl: u64, + bugs: Bugs, + hypervisor: bool, + autoibrs: bool, + smt: bool, +} + +/// `cpu_set_bug_bits` (`common.c:1414-1578`). SRSO and TSA are set only on the +/// AMD families and Hygon [`decide`] refuses, and `SWAPGS`, `SMT_RSB` and +/// `IBPB_NO_RET` reach no line and no selection here. +fn bug_bits(f: &Facts, id: &Ident, caps: &Caps, arch: u64) -> Bugs { + let mut b = Bugs { + itlb_multihit: !whitelisted(id, NO_ITLB_MULTIHIT) && arch & ARCH_CAP_PSCHANGE_MC_NO == 0, + ..Bugs::default() + }; + if whitelisted(id, NO_SPECULATION) { + return b; + } + b.spectre_v1 = true; + b.spectre_v2 = !whitelisted(id, NO_SPECTRE_V2); + b.ssb = !whitelisted(id, NO_SSB) + && arch & ARCH_CAP_SSB_NO == 0 + && f.cpuid_8000_0008_ebx & CPUID_8000_0008_EBX_AMD_SSB_NO == 0; + b.ibrs_enhanced = arch & ARCH_CAP_IBRS_ALL != 0 || caps.autoibrs; + b.eibrs_pbrsb = + b.ibrs_enhanced && !whitelisted(id, NO_EIBRS_PBRSB) && arch & ARCH_CAP_PBRSB_NO == 0; + b.mds = !whitelisted(id, NO_MDS) && arch & ARCH_CAP_MDS_NO == 0; + b.msbds_only = b.mds && whitelisted(id, MSBDS_ONLY); + b.taa = arch & ARCH_CAP_TAA_NO == 0 + && (f.cpuid_7_0_ebx & CPUID_7_0_EBX_RTM != 0 || arch & ARCH_CAP_TSX_CTRL_MSR != 0); + b.srbds = (f.cpuid_1_ecx & CPUID_1_ECX_RDRAND != 0 || f.cpuid_7_0_ebx & CPUID_7_0_EBX_RDSEED != 0) + && blacklisted(id, SRBDS | MMIO_SBDS); + let mmio_immune = arch & (ARCH_CAP_FBSDP_NO | ARCH_CAP_PSDP_NO | ARCH_CAP_SBDR_SSDP_NO) + == ARCH_CAP_FBSDP_NO | ARCH_CAP_PSDP_NO | ARCH_CAP_SBDR_SSDP_NO; + if !mmio_immune { + if blacklisted(id, MMIO) { + b.mmio_stale_data = true; + } else if !whitelisted(id, NO_MMIO) { + b.mmio_unknown = true; + } + } + b.retbleed = f.cpuid_8000_0008_ebx & CPUID_8000_0008_EBX_BTC_NO == 0 + && (blacklisted(id, RETBLEED) || arch & ARCH_CAP_RSBA != 0); + b.gds = blacklisted(id, GDS) + && arch & ARCH_CAP_GDS_NO == 0 + && f.cpuid_1_ecx & CPUID_1_ECX_AVX != 0; + // `vulnerable_to_rfds` (`common.c:1370-1386`). + b.rfds = if arch & ARCH_CAP_RFDS_NO != 0 { + false + } else { + arch & ARCH_CAP_RFDS_CLEAR != 0 || blacklisted(id, RFDS) + }; + b.bhi = !whitelisted(id, NO_BHI) && (b.ibrs_enhanced || caps.hypervisor); + b.vmscape = blacklisted(id, VMSCAPE) && !caps.hypervisor; + // `vulnerable_to_its` (`common.c:1388-1412`). `ITS_NATIVE_ONLY` reaches no + // line under the default command line. + b.its = arch & ARCH_CAP_ITS_NO == 0 + && id.vendor == Vendor::Intel + && f.cpuid_7_2_edx & CPUID_7_2_EDX_BHI_CTRL == 0 + && (caps.hypervisor || blacklisted(id, ITS)); + if whitelisted(id, NO_MELTDOWN) || arch & ARCH_CAP_RDCL_NO != 0 { + return b; + } + b.meltdown = true; + b.l1tf = !whitelisted(id, NO_L1TF); + b +} + +/// `spec_ctrl_disable_kernel_rrsba` (`bugs.c:1723-1739`). +fn disable_kernel_rrsba(f: &Facts, arch: u64, spec_ctrl: &mut u64, rrsba_disabled: &mut bool) { + if *rrsba_disabled { + return; + } + if arch & ARCH_CAP_RRSBA == 0 { + *rrsba_disabled = true; + return; + } + if f.cpuid_7_2_edx & CPUID_7_2_EDX_RRSBA_CTRL == 0 { + return; + } + *spec_ctrl |= SPEC_CTRL_RRSBA_DIS_S; + *rrsba_disabled = true; +} + +/// Linux's selection over `facts`: `cpu_select_mitigations` (`bugs.c:149-199`) +/// and what it reads. +/// +/// Panics where the facts contradict their own documentation: an +/// `arch_capabilities` read without CPUID.(7,0):EDX[29], or an `mcu_opt_ctrl` +/// present where `GDS_CTRL` does not say so, or absent where it does. +pub fn decide(facts: &Facts) -> Result { + let f = facts; + let id = Ident::new(f.vendor, f.signature); + match id.vendor { + Vendor::Amd if id.family >= 0x15 => return Err(Refused::AmdFamily(id.family)), + Vendor::Hygon => return Err(Refused::Hygon), + _ => {} + } + assert!( + f.cpuid_7_0_edx & CPUID_7_0_EDX_ARCH_CAPABILITIES != 0 || f.arch_capabilities == 0, + "ARCH_CAPABILITIES {:#x} without CPUID.(7,0):EDX[29]", + f.arch_capabilities + ); + let arch = f.arch_capabilities; + assert_eq!( + f.mcu_opt_ctrl.is_some(), + arch & ARCH_CAP_GDS_CTRL != 0, + "IA32_MCU_OPT_CTRL is read exactly where ARCH_CAPABILITIES.GDS_CTRL" + ); + + let edx7 = f.cpuid_7_0_edx; + let ebx8 = f.cpuid_8000_0008_ebx; + let hypervisor = f.cpuid_1_ecx & CPUID_1_ECX_HYPERVISOR != 0; + let spec_ctrl_bit = edx7 & CPUID_7_0_EDX_SPEC_CTRL != 0; + let intel_stibp = edx7 & CPUID_7_0_EDX_INTEL_STIBP != 0; + let mut caps = Caps { + hypervisor, + ibrs: spec_ctrl_bit || ebx8 & CPUID_8000_0008_EBX_AMD_IBRS != 0, + ibpb: spec_ctrl_bit || ebx8 & CPUID_8000_0008_EBX_AMD_IBPB != 0, + stibp: intel_stibp || ebx8 & CPUID_8000_0008_EBX_AMD_STIBP != 0, + ssbd: edx7 & CPUID_7_0_EDX_SPEC_CTRL_SSBD != 0 + || ebx8 & (CPUID_8000_0008_EBX_VIRT_SSBD | CPUID_8000_0008_EBX_AMD_SSBD) != 0, + autoibrs: f.cpuid_8000_0021_eax & CPUID_8000_0021_EAX_AUTOIBRS != 0, + }; + if id.vendor == Vendor::Intel + && !hypervisor + && (spec_ctrl_bit || intel_stibp || caps.ibrs || caps.ibpb || caps.stibp) + && table::bad_spectre_microcode(&id, f.microcode) + { + caps.ibrs = false; + caps.ibpb = false; + caps.stibp = false; + caps.ssbd = false; + } + let bugs = bug_bits(f, &id, &caps, arch); + + let tsx = if edx7 & CPUID_7_0_EDX_RTM_ALWAYS_ABORT != 0 { + Tsx::RtmAlwaysAbort + } else if arch & ARCH_CAP_TSX_CTRL_MSR != 0 { + Tsx::Disable + } else { + Tsx::NotSupported + }; + let rtm = f.cpuid_7_0_ebx & CPUID_7_0_EBX_RTM != 0 && tsx == Tsx::NotSupported; + + let mut spec_ctrl = 0; + let mut rrsba_disabled = false; + let mut efer_autoibrs = false; + + // `spectre_v2_select_mitigation` (`bugs.c:1860-2046`). Where the CPU is + // unaffected it returns before any of it, leaving `spectre_v2_cmd` at + // `SPECTRE_V2_CMD_NONE`. + let mut spectre_v2 = SpectreV2::None; + let mut bhi = if bugs.bhi { Bhi::Vulnerable } else { Bhi::NotAffected }; + let mut ibrs_fw = false; + if bugs.spectre_v2 { + spectre_v2 = if bugs.ibrs_enhanced { + SpectreV2::Eibrs + } else if bugs.retbleed && caps.ibrs && id.vendor == Vendor::Intel { + SpectreV2::Ibrs + } else { + SpectreV2::Retpoline + }; + let ibrs_mode = matches!(spectre_v2, SpectreV2::Eibrs | SpectreV2::Ibrs); + if ibrs_mode { + if caps.autoibrs { + efer_autoibrs = true; + } else { + spec_ctrl |= SPEC_CTRL_IBRS; + } + } + if spectre_v2 == SpectreV2::Retpoline { + disable_kernel_rrsba(f, arch, &mut spec_ctrl, &mut rrsba_disabled); + } + // `bhi_select_mitigation` (`bugs.c:1831-1858`); its own call of + // `spec_ctrl_disable_kernel_rrsba` repeats the one just above. + if bugs.bhi { + bhi = if spectre_v2 == SpectreV2::Retpoline && rrsba_disabled { + Bhi::Retpoline + } else if f.cpuid_7_2_edx & CPUID_7_2_EDX_BHI_CTRL != 0 { + spec_ctrl |= SPEC_CTRL_BHI_DIS_S; + Bhi::BhiDisS + } else { + Bhi::SwLoop + }; + } + ibrs_fw = + !(bugs.retbleed && caps.ibpb && id.vendor == Vendor::Amd) && caps.ibrs && !ibrs_mode; + } + + // `retbleed_select_mitigation` (`bugs.c:1054-1206`). + let retbleed = if !bugs.retbleed { + Retbleed::None + } else if id.vendor == Vendor::Intel { + match spectre_v2 { + SpectreV2::Ibrs => Retbleed::Ibrs, + SpectreV2::Eibrs => Retbleed::Eibrs, + SpectreV2::None | SpectreV2::Retpoline => Retbleed::None, + } + } else if id.vendor == Vendor::Amd { + Retbleed::Unret + } else { + Retbleed::None + }; + + // `spectre_v2_user_select_mitigation` (`bugs.c:1492-1591`) and + // `cpu_bugs_smt_update` (`bugs.c:2964-3060`). + let user_cmd_none = !bugs.spectre_v2; + let ibpb = if !caps.ibpb { + Ibpb::Absent + } else if user_cmd_none { + Ibpb::Disabled + } else { + Ibpb::Conditional + }; + let stibp = if user_cmd_none + || !caps.stibp + || !f.smt + || (matches!(spectre_v2, SpectreV2::Eibrs) && !caps.autoibrs) + { + Stibp::None + } else if ebx8 & CPUID_8000_0008_EBX_AMD_STIBP_ALWAYS_ON != 0 || retbleed == Retbleed::Unret { + spec_ctrl |= SPEC_CTRL_STIBP; + Stibp::StrictPreferred + } else { + Stibp::Prctl + }; + + let ssb = if caps.ssbd && bugs.ssb { Ssb::Prctl } else { Ssb::None }; + + // `md_clear_select_mitigation` (`bugs.c:603-615`). Its update pass + // (`bugs.c:555-601`) changes nothing under the default command line: each + // bug it selects for again is already selected. + let md_clear = edx7 & CPUID_7_0_EDX_MD_CLEAR != 0; + let mut clear_cpu_buf = false; + let mds = if !bugs.mds { + Mds::Off + } else { + clear_cpu_buf = true; + if md_clear { Mds::Full } else { Mds::Vmwerv } + }; + let taa = if !bugs.taa { + Taa::Off + } else if !rtm { + Taa::TsxDisabled + } else { + clear_cpu_buf = true; + if md_clear && !(arch & ARCH_CAP_MDS_NO != 0 && arch & ARCH_CAP_TSX_CTRL_MSR == 0) { + Taa::Verw + } else { + Taa::UcodeNeeded + } + }; + let mmio = if !bugs.mmio_stale_data || bugs.mmio_unknown { + Mmio::Off + } else { + if bugs.mds || (bugs.taa && rtm) { + clear_cpu_buf = true; + } + let flush_l1d = edx7 & CPUID_7_0_EDX_FLUSH_L1D != 0; + if arch & ARCH_CAP_FB_CLEAR != 0 || (md_clear && flush_l1d && arch & ARCH_CAP_MDS_NO == 0) { + Mmio::Verw + } else { + Mmio::UcodeNeeded + } + }; + let rfds = if !bugs.rfds { + Rfds::Off + } else if arch & ARCH_CAP_RFDS_CLEAR != 0 { + clear_cpu_buf = true; + Rfds::Verw + } else { + Rfds::UcodeNeeded + }; + + let srbds = bugs.srbds.then_some( + if arch & ARCH_CAP_MDS_NO != 0 && !rtm && !bugs.mmio_stale_data { + Srbds::TsxOff + } else if hypervisor { + Srbds::Hypervisor + } else if edx7 & CPUID_7_0_EDX_SRBDS_CTRL == 0 { + Srbds::UcodeNeeded + } else { + Srbds::Full + }, + ); + + let gds = bugs.gds.then(|| { + if hypervisor { + Gds::Hypervisor + } else if arch & ARCH_CAP_GDS_CTRL == 0 { + Gds::UcodeNeeded + } else if f.mcu_opt_ctrl.expect("asserted above with GDS_CTRL") & GDS_MITG_LOCKED != 0 { + Gds::FullLocked + } else { + Gds::Full + } + }); + + let its = if bugs.its && spectre_v2 != SpectreV2::None { Its::AlignedThunks } else { Its::Off }; + + let vmscape = + if bugs.vmscape && caps.ibpb { Vmscape::IbpbExitToUser } else { Vmscape::None }; + + Ok(Decision { + tsx, + spectre_v2, + bhi, + ibpb, + ibrs_fw, + stibp, + retbleed, + ssb, + mds, + taa, + mmio, + rfds, + srbds, + gds, + its, + vmscape, + clear_cpu_buf, + efer_autoibrs, + spec_ctrl, + bugs, + hypervisor, + autoibrs: caps.autoibrs, + smt: f.smt, + }) +} + +impl Decision { + /// The file's line, without its newline. + pub fn line(&self, v: Vuln) -> Result, Unmodelled> { + match v { + Vuln::L1tf if self.bugs.l1tf => Err(Unmodelled::L1tf), + Vuln::ItlbMultihit if self.bugs.itlb_multihit => Err(Unmodelled::ItlbMultihit), + _ => Ok(Line { d: self, v }), + } + } +} + +/// One vulnerabilities file's line. +pub struct Line<'a> { + d: &'a Decision, + v: Vuln, +} + +impl Line<'_> { + fn smt(&self) -> &'static str { + if self.d.smt { "vulnerable" } else { "disabled" } + } + + /// `spectre_v2_show_state` (`bugs.c:3238-3257`); unprivileged eBPF is off + /// under `CONFIG_BPF_UNPRIV_DEFAULT_OFF=y` and no module is loaded. + fn spectre_v2(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let d = self.d; + f.write_str(match d.spectre_v2 { + SpectreV2::None => "Vulnerable", + SpectreV2::Retpoline => "Mitigation: Retpolines", + SpectreV2::Eibrs => "Mitigation: Enhanced / Automatic IBRS", + SpectreV2::Ibrs => "Mitigation: IBRS", + })?; + f.write_str(match d.ibpb { + Ibpb::Absent => "", + Ibpb::Disabled => "; IBPB: disabled", + Ibpb::Conditional => "; IBPB: conditional", + })?; + if d.ibrs_fw { + f.write_str("; IBRS_FW")?; + } + // `stibp_state` (`bugs.c:3174-3193`). + if !(d.spectre_v2 == SpectreV2::Eibrs && !d.autoibrs) { + f.write_str(match d.stibp { + Stibp::None => "; STIBP: disabled", + Stibp::StrictPreferred => "; STIBP: always-on", + Stibp::Prctl if d.smt => "; STIBP: conditional", + Stibp::Prctl => "", + })?; + } + // `RSB_CTXSW` (`bugs.c:1741-1789`). + if matches!(d.spectre_v2, SpectreV2::Retpoline | SpectreV2::Ibrs) { + f.write_str("; RSB filling")?; + } + // `pbrsb_eibrs_state` (`bugs.c:3207-3218`): every mode but `None` + // sets `RSB_VMEXIT` or `RSB_VMEXIT_LITE`. + f.write_str(match (d.bugs.eibrs_pbrsb, d.spectre_v2) { + (false, _) => "; PBRSB-eIBRS: Not affected", + (true, SpectreV2::None) => "; PBRSB-eIBRS: Vulnerable", + (true, _) => "; PBRSB-eIBRS: SW sequence", + })?; + f.write_str(match d.bhi { + Bhi::NotAffected => "; BHI: Not affected", + Bhi::BhiDisS => "; BHI: BHI_DIS_S", + Bhi::SwLoop => "; BHI: SW loop, KVM: SW loop", + Bhi::Retpoline => "; BHI: Retpoline", + Bhi::Vulnerable => "; BHI: Vulnerable", + }) + } +} + +impl fmt::Display for Line<'_> { + /// `cpu_show_common` (`bugs.c:3305-3371`) and the `*_show_state` it calls. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let d = self.d; + let b = &d.bugs; + let affected = match self.v { + Vuln::Meltdown => b.meltdown, + Vuln::SpectreV1 => b.spectre_v1, + Vuln::SpectreV2 => b.spectre_v2, + Vuln::SpecStoreBypass => b.ssb, + Vuln::L1tf => b.l1tf, + Vuln::Mds => b.mds, + Vuln::TsxAsyncAbort => b.taa, + Vuln::ItlbMultihit => b.itlb_multihit, + Vuln::Srbds => b.srbds, + Vuln::MmioStaleData => b.mmio_stale_data || b.mmio_unknown, + Vuln::Retbleed => b.retbleed, + Vuln::GatherDataSampling => b.gds, + Vuln::RegFileDataSampling => b.rfds, + Vuln::IndirectTargetSelection => b.its, + Vuln::Vmscape => b.vmscape, + // Set only on the CPUs `decide` refuses. + Vuln::SpecRstackOverflow | Vuln::Tsa => false, + }; + if !affected { + return f.write_str("Not affected"); + } + match self.v { + // `pti_check_boottime_disable` (`arch/x86/mm/pti.c:79-101`) sets PTI + // wherever Meltdown is; a Xen PV guest is no machine ToyOS boots. + Vuln::Meltdown => f.write_str("Mitigation: PTI"), + Vuln::SpectreV1 => { + f.write_str("Mitigation: usercopy/swapgs barriers and __user pointer sanitization") + } + Vuln::SpectreV2 => self.spectre_v2(f), + Vuln::SpecStoreBypass => f.write_str(match d.ssb { + Ssb::None => "Vulnerable", + Ssb::Prctl => "Mitigation: Speculative Store Bypass disabled via prctl", + }), + Vuln::Mds => { + f.write_str(match d.mds { + Mds::Off => "Vulnerable", + Mds::Full => "Mitigation: Clear CPU buffers", + Mds::Vmwerv => "Vulnerable: Clear CPU buffers attempted, no microcode", + })?; + if d.hypervisor { + f.write_str("; SMT Host state unknown") + } else if b.msbds_only { + let smt = match (d.mds, d.smt) { + (Mds::Off, _) => "vulnerable", + (_, true) => "mitigated", + (_, false) => "disabled", + }; + write!(f, "; SMT {smt}") + } else { + write!(f, "; SMT {}", self.smt()) + } + } + Vuln::TsxAsyncAbort => { + f.write_str(match d.taa { + Taa::Off => "Vulnerable", + Taa::UcodeNeeded => "Vulnerable: Clear CPU buffers attempted, no microcode", + Taa::Verw => "Mitigation: Clear CPU buffers", + Taa::TsxDisabled => "Mitigation: TSX disabled", + })?; + match d.taa { + Taa::Off | Taa::TsxDisabled => Ok(()), + _ if d.hypervisor => f.write_str("; SMT Host state unknown"), + _ => write!(f, "; SMT {}", self.smt()), + } + } + Vuln::Srbds => f.write_str(match d.srbds.expect("set with the bug") { + Srbds::UcodeNeeded => "Vulnerable: No microcode", + Srbds::Full => "Mitigation: Microcode", + Srbds::TsxOff => "Mitigation: TSX disabled", + Srbds::Hypervisor => "Unknown: Dependent on hypervisor status", + }), + Vuln::MmioStaleData => { + if b.mmio_unknown { + return f.write_str("Unknown: No mitigations"); + } + f.write_str(match d.mmio { + Mmio::Off => return f.write_str("Vulnerable"), + Mmio::UcodeNeeded => "Vulnerable: Clear CPU buffers attempted, no microcode", + Mmio::Verw => "Mitigation: Clear CPU buffers", + })?; + if d.hypervisor { + f.write_str("; SMT Host state unknown") + } else { + write!(f, "; SMT {}", self.smt()) + } + } + Vuln::Retbleed => match d.retbleed { + Retbleed::None => f.write_str("Vulnerable"), + Retbleed::Ibrs => f.write_str("Mitigation: IBRS"), + Retbleed::Eibrs => f.write_str("Mitigation: Enhanced IBRS"), + // Selected on AMD alone, so never "on non-AMD based uarch". + Retbleed::Unret => { + let smt = if !d.smt { + "disabled" + } else if d.stibp == Stibp::StrictPreferred { + "enabled with STIBP protection" + } else { + "vulnerable" + }; + write!(f, "Mitigation: untrained return thunk; SMT {smt}") + } + }, + Vuln::GatherDataSampling => f.write_str(match d.gds.expect("set with the bug") { + Gds::UcodeNeeded => "Vulnerable: No microcode", + Gds::Full => "Mitigation: Microcode", + Gds::FullLocked => "Mitigation: Microcode (locked)", + Gds::Hypervisor => "Unknown: Dependent on hypervisor status", + }), + Vuln::RegFileDataSampling => f.write_str(match d.rfds { + Rfds::Off => "Vulnerable", + Rfds::Verw => "Mitigation: Clear Register File", + Rfds::UcodeNeeded => "Vulnerable: No microcode", + }), + Vuln::IndirectTargetSelection => f.write_str(match d.its { + Its::Off => "Vulnerable", + Its::AlignedThunks => "Mitigation: Aligned branch/return thunks", + }), + Vuln::Vmscape => f.write_str(match d.vmscape { + Vmscape::None => "Vulnerable", + Vmscape::IbpbExitToUser => "Mitigation: IBPB before exit to userspace", + }), + Vuln::L1tf | Vuln::ItlbMultihit | Vuln::SpecRstackOverflow | Vuln::Tsa => { + unreachable!("{} is refused or never affected above", self.v.name()) + } + } + } +} diff --git a/toyos-cpuvuln/src/table.rs b/toyos-cpuvuln/src/table.rs new file mode 100644 index 00000000000..2aa801b51cb --- /dev/null +++ b/toyos-cpuvuln/src/table.rs @@ -0,0 +1,317 @@ +//! Linux's three CPU tables, row for row: `cpu_vuln_whitelist` and +//! `cpu_vuln_blacklist` (`common.c:1182-1344`) and `spectre_bad_microcodes` +//! (`intel.c:141-163`). A lookup is `x86_match_cpu` (`match.c:36-57`): the +//! first row whose vendor, family, model and stepping all match, `0` matching +//! any. +//! +//! The whitelist's `NSC` and `VORTEX` rows are left out: x86-64 Linux builds +//! neither vendor (`CONFIG_CPU_SUP_CYRIX_32`, `CONFIG_CPU_SUP_VORTEX_32`), so +//! no CPU it identifies matches them. + +use crate::{Ident, Vendor}; + +/// A whitelist row's flags (`common.c:1157-1168`). +#[derive(Clone, Copy)] +pub(crate) struct Wl(u16); +/// A blacklist row's flags (`common.c:1260-1282`). +#[derive(Clone, Copy)] +pub(crate) struct Bl(u16); + +impl core::ops::BitOr for Bl { + type Output = Self; + fn bitor(self, o: Self) -> Self { + Self(self.0 | o.0) + } +} + +pub(crate) const NO_SPECULATION: Wl = Wl(1 << 0); +pub(crate) const NO_MELTDOWN: Wl = Wl(1 << 1); +pub(crate) const NO_SSB: Wl = Wl(1 << 2); +pub(crate) const NO_L1TF: Wl = Wl(1 << 3); +pub(crate) const NO_MDS: Wl = Wl(1 << 4); +pub(crate) const MSBDS_ONLY: Wl = Wl(1 << 5); +pub(crate) const NO_SWAPGS: Wl = Wl(1 << 6); +pub(crate) const NO_ITLB_MULTIHIT: Wl = Wl(1 << 7); +pub(crate) const NO_SPECTRE_V2: Wl = Wl(1 << 8); +pub(crate) const NO_MMIO: Wl = Wl(1 << 9); +pub(crate) const NO_EIBRS_PBRSB: Wl = Wl(1 << 10); +pub(crate) const NO_BHI: Wl = Wl(1 << 11); + +pub(crate) const SRBDS: Bl = Bl(1 << 0); +pub(crate) const MMIO: Bl = Bl(1 << 1); +pub(crate) const MMIO_SBDS: Bl = Bl(1 << 2); +pub(crate) const RETBLEED: Bl = Bl(1 << 3); +pub(crate) const SMT_RSB: Bl = Bl(1 << 4); +pub(crate) const SRSO: Bl = Bl(1 << 5); +pub(crate) const GDS: Bl = Bl(1 << 6); +pub(crate) const RFDS: Bl = Bl(1 << 7); +pub(crate) const ITS: Bl = Bl(1 << 8); +pub(crate) const ITS_NATIVE_ONLY: Bl = Bl(1 << 9); +pub(crate) const TSA: Bl = Bl(1 << 10); +pub(crate) const VMSCAPE: Bl = Bl(1 << 11); + +macro_rules! wl { ($($f:ident)|+) => { Wl(0 $(| $f.0)+) }; } +macro_rules! bl { ($($f:ident)|+) => { Bl(0 $(| $f.0)+) }; } + +/// `X86_FAMILY_ANY`, `X86_MODEL_ANY` and `X86_STEPPING_ANY` +/// (`include/linux/mod_devicetable.h:700-702`). +const ANY: u32 = 0; + +/// `X86_STEPPINGS(mins, maxs)`, `GENMASK(maxs, mins)` (`asm/cpu_device_id.h:59`). +const fn steppings(min: u32, max: u32) -> u32 { + ((1 << (max + 1)) - 1) & !((1 << min) - 1) +} + +pub(crate) struct Row { + vendor: Option, + family: u32, + model: u32, + steppings: u32, + flags: F, +} + +const fn row(vendor: Option, family: u32, model: u32, flags: F) -> Row { + Row { vendor, family, model, steppings: ANY, flags } +} +const fn intel(model: u32, flags: F) -> Row { + row(Some(Vendor::Intel), 6, model, flags) +} +const fn intel_steppings(model: u32, steppings: u32, flags: Bl) -> Row { + Row { vendor: Some(Vendor::Intel), family: 6, model, steppings, flags } +} +const fn amd(family: u32, flags: F) -> Row { + row(Some(Vendor::Amd), family, ANY, flags) +} +const fn hygon(family: u32, flags: F) -> Row { + row(Some(Vendor::Hygon), family, ANY, flags) +} + +fn lookup(rows: &[Row], id: &Ident) -> Option { + rows.iter() + .find(|r| { + r.vendor.is_none_or(|v| v == id.vendor) + && (r.family == ANY || r.family == id.family) + && (r.model == ANY || r.model == id.model) + && (r.steppings == ANY || r.steppings & (1 << id.stepping) != 0) + }) + .map(|r| r.flags) +} + +/// `cpu_matches(cpu_vuln_whitelist, which)` (`common.c:1346-1351`). +pub(crate) fn whitelisted(id: &Ident, which: Wl) -> bool { + lookup(WHITELIST, id).is_some_and(|f| f.0 & which.0 != 0) +} + +/// `cpu_matches(cpu_vuln_blacklist, which)` (`common.c:1346-1351`). +pub(crate) fn blacklisted(id: &Ident, which: Bl) -> bool { + lookup(BLACKLIST, id).is_some_and(|f| f.0 & which.0 != 0) +} + +// `arch/x86/include/asm/intel-family.h`, family 6, the models the tables name. +const CORE_YONAH: u32 = 0x0E; +const SANDYBRIDGE: u32 = 0x2A; +const SANDYBRIDGE_X: u32 = 0x2D; +const IVYBRIDGE: u32 = 0x3A; +const IVYBRIDGE_X: u32 = 0x3E; +const HASWELL: u32 = 0x3C; +const HASWELL_X: u32 = 0x3F; +const HASWELL_L: u32 = 0x45; +const HASWELL_G: u32 = 0x46; +const BROADWELL: u32 = 0x3D; +const BROADWELL_G: u32 = 0x47; +const BROADWELL_X: u32 = 0x4F; +const BROADWELL_D: u32 = 0x56; +const SKYLAKE_L: u32 = 0x4E; +const SKYLAKE: u32 = 0x5E; +const SKYLAKE_X: u32 = 0x55; +const KABYLAKE_L: u32 = 0x8E; +const KABYLAKE: u32 = 0x9E; +const COMETLAKE: u32 = 0xA5; +const COMETLAKE_L: u32 = 0xA6; +const CANNONLAKE_L: u32 = 0x66; +const ICELAKE_X: u32 = 0x6A; +const ICELAKE_D: u32 = 0x6C; +const ICELAKE_L: u32 = 0x7E; +const ROCKETLAKE: u32 = 0xA7; +const TIGERLAKE_L: u32 = 0x8C; +const TIGERLAKE: u32 = 0x8D; +const SAPPHIRERAPIDS_X: u32 = 0x8F; +const EMERALDRAPIDS_X: u32 = 0xCF; +const GRANITERAPIDS_X: u32 = 0xAD; +const LAKEFIELD: u32 = 0x8A; +const ALDERLAKE: u32 = 0x97; +const ALDERLAKE_L: u32 = 0x9A; +const RAPTORLAKE: u32 = 0xB7; +const RAPTORLAKE_P: u32 = 0xBA; +const RAPTORLAKE_S: u32 = 0xBF; +const METEORLAKE_L: u32 = 0xAA; +const ARROWLAKE_H: u32 = 0xC5; +const ARROWLAKE: u32 = 0xC6; +const ARROWLAKE_U: u32 = 0xB5; +const LUNARLAKE_M: u32 = 0xBD; +const ATOM_BONNELL: u32 = 0x1C; +const ATOM_BONNELL_MID: u32 = 0x26; +const ATOM_SALTWELL: u32 = 0x36; +const ATOM_SALTWELL_MID: u32 = 0x27; +const ATOM_SALTWELL_TABLET: u32 = 0x35; +const ATOM_SILVERMONT: u32 = 0x37; +const ATOM_SILVERMONT_D: u32 = 0x4D; +const ATOM_SILVERMONT_MID: u32 = 0x4A; +const ATOM_AIRMONT: u32 = 0x4C; +const ATOM_AIRMONT_MID: u32 = 0x5A; +const ATOM_AIRMONT_NP: u32 = 0x75; +const ATOM_GOLDMONT: u32 = 0x5C; +const ATOM_GOLDMONT_D: u32 = 0x5F; +const ATOM_GOLDMONT_PLUS: u32 = 0x7A; +const ATOM_TREMONT_D: u32 = 0x86; +const ATOM_TREMONT: u32 = 0x96; +const ATOM_TREMONT_L: u32 = 0x9C; +const ATOM_GRACEMONT: u32 = 0xBE; +const ATOM_CRESTMONT_X: u32 = 0xAF; +const XEON_PHI_KNL: u32 = 0x57; +const XEON_PHI_KNM: u32 = 0x85; + +/// `cpu_vuln_whitelist` (`common.c:1182-1244`). +const WHITELIST: &[Row] = &[ + row(None, 4, ANY, wl!(NO_SPECULATION)), + row(Some(Vendor::Centaur), 5, ANY, wl!(NO_SPECULATION)), + row(Some(Vendor::Intel), 5, ANY, wl!(NO_SPECULATION)), + + intel(TIGERLAKE, wl!(NO_MMIO)), + intel(TIGERLAKE_L, wl!(NO_MMIO)), + intel(ALDERLAKE, wl!(NO_MMIO)), + intel(ALDERLAKE_L, wl!(NO_MMIO)), + + intel(ATOM_SALTWELL, wl!(NO_SPECULATION | NO_ITLB_MULTIHIT)), + intel(ATOM_SALTWELL_TABLET, wl!(NO_SPECULATION | NO_ITLB_MULTIHIT)), + intel(ATOM_SALTWELL_MID, wl!(NO_SPECULATION | NO_ITLB_MULTIHIT)), + intel(ATOM_BONNELL, wl!(NO_SPECULATION | NO_ITLB_MULTIHIT)), + intel(ATOM_BONNELL_MID, wl!(NO_SPECULATION | NO_ITLB_MULTIHIT)), + + intel(ATOM_SILVERMONT, wl!(NO_SSB | NO_L1TF | MSBDS_ONLY | NO_SWAPGS | NO_ITLB_MULTIHIT)), + intel(ATOM_SILVERMONT_D, wl!(NO_SSB | NO_L1TF | MSBDS_ONLY | NO_SWAPGS | NO_ITLB_MULTIHIT)), + intel(ATOM_SILVERMONT_MID, wl!(NO_SSB | NO_L1TF | MSBDS_ONLY | NO_SWAPGS | NO_ITLB_MULTIHIT)), + intel(ATOM_AIRMONT, wl!(NO_SSB | NO_L1TF | MSBDS_ONLY | NO_SWAPGS | NO_ITLB_MULTIHIT)), + intel(XEON_PHI_KNL, wl!(NO_SSB | NO_L1TF | MSBDS_ONLY | NO_SWAPGS | NO_ITLB_MULTIHIT)), + intel(XEON_PHI_KNM, wl!(NO_SSB | NO_L1TF | MSBDS_ONLY | NO_SWAPGS | NO_ITLB_MULTIHIT)), + + intel(CORE_YONAH, wl!(NO_SSB)), + + intel(ATOM_AIRMONT_MID, wl!(NO_SSB | NO_L1TF | NO_SWAPGS | NO_ITLB_MULTIHIT | MSBDS_ONLY)), + intel(ATOM_AIRMONT_NP, wl!(NO_SSB | NO_L1TF | NO_SWAPGS | NO_ITLB_MULTIHIT)), + + intel(ATOM_GOLDMONT, wl!(NO_MDS | NO_L1TF | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO)), + intel(ATOM_GOLDMONT_D, wl!(NO_MDS | NO_L1TF | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO)), + intel(ATOM_GOLDMONT_PLUS, wl!(NO_MDS | NO_L1TF | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO | NO_EIBRS_PBRSB)), + + intel(ATOM_TREMONT, wl!(NO_EIBRS_PBRSB)), + intel(ATOM_TREMONT_L, wl!(NO_EIBRS_PBRSB)), + intel(ATOM_TREMONT_D, wl!(NO_ITLB_MULTIHIT | NO_EIBRS_PBRSB)), + + amd(0x0f, wl!(NO_MELTDOWN | NO_SSB | NO_L1TF | NO_MDS | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO | NO_BHI)), + amd(0x10, wl!(NO_MELTDOWN | NO_SSB | NO_L1TF | NO_MDS | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO | NO_BHI)), + amd(0x11, wl!(NO_MELTDOWN | NO_SSB | NO_L1TF | NO_MDS | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO | NO_BHI)), + amd(0x12, wl!(NO_MELTDOWN | NO_SSB | NO_L1TF | NO_MDS | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO | NO_BHI)), + + amd(ANY, wl!(NO_MELTDOWN | NO_L1TF | NO_MDS | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO | NO_EIBRS_PBRSB | NO_BHI)), + hygon(ANY, wl!(NO_MELTDOWN | NO_L1TF | NO_MDS | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO | NO_EIBRS_PBRSB | NO_BHI)), + + row(Some(Vendor::Centaur), 7, ANY, wl!(NO_SPECTRE_V2 | NO_SWAPGS | NO_MMIO | NO_BHI)), + row(Some(Vendor::Zhaoxin), 7, ANY, wl!(NO_SPECTRE_V2 | NO_SWAPGS | NO_MMIO | NO_BHI)), +]; + +/// `cpu_vuln_blacklist` (`common.c:1284-1344`). +const BLACKLIST: &[Row] = &[ + intel_steppings(SANDYBRIDGE_X, ANY, bl!(VMSCAPE)), + intel_steppings(SANDYBRIDGE, ANY, bl!(VMSCAPE)), + intel_steppings(IVYBRIDGE_X, ANY, bl!(VMSCAPE)), + intel_steppings(IVYBRIDGE, ANY, bl!(SRBDS | VMSCAPE)), + intel_steppings(HASWELL, ANY, bl!(SRBDS | VMSCAPE)), + intel_steppings(HASWELL_L, ANY, bl!(SRBDS | VMSCAPE)), + intel_steppings(HASWELL_G, ANY, bl!(SRBDS | VMSCAPE)), + intel_steppings(HASWELL_X, ANY, bl!(MMIO | VMSCAPE)), + intel_steppings(BROADWELL_D, ANY, bl!(MMIO | VMSCAPE)), + intel_steppings(BROADWELL_X, ANY, bl!(MMIO | VMSCAPE)), + intel_steppings(BROADWELL_G, ANY, bl!(SRBDS | VMSCAPE)), + intel_steppings(BROADWELL, ANY, bl!(SRBDS | VMSCAPE)), + intel_steppings(SKYLAKE_X, steppings(0x0, 0x5), bl!(MMIO | RETBLEED | GDS | VMSCAPE)), + intel_steppings(SKYLAKE_X, ANY, bl!(MMIO | RETBLEED | GDS | ITS | VMSCAPE)), + intel_steppings(SKYLAKE_L, ANY, bl!(MMIO | RETBLEED | GDS | SRBDS | VMSCAPE)), + intel_steppings(SKYLAKE, ANY, bl!(MMIO | RETBLEED | GDS | SRBDS | VMSCAPE)), + intel_steppings(KABYLAKE_L, steppings(0x0, 0xb), bl!(MMIO | RETBLEED | GDS | SRBDS | VMSCAPE)), + intel_steppings(KABYLAKE_L, ANY, bl!(MMIO | RETBLEED | GDS | SRBDS | ITS | VMSCAPE)), + intel_steppings(KABYLAKE, steppings(0x0, 0xc), bl!(MMIO | RETBLEED | GDS | SRBDS | VMSCAPE)), + intel_steppings(KABYLAKE, ANY, bl!(MMIO | RETBLEED | GDS | SRBDS | ITS | VMSCAPE)), + intel_steppings(CANNONLAKE_L, ANY, bl!(RETBLEED | VMSCAPE)), + intel_steppings(ICELAKE_L, ANY, bl!(MMIO | MMIO_SBDS | RETBLEED | GDS | ITS | ITS_NATIVE_ONLY)), + intel_steppings(ICELAKE_D, ANY, bl!(MMIO | GDS | ITS | ITS_NATIVE_ONLY)), + intel_steppings(ICELAKE_X, ANY, bl!(MMIO | GDS | ITS | ITS_NATIVE_ONLY)), + intel_steppings(COMETLAKE, ANY, bl!(MMIO | MMIO_SBDS | RETBLEED | GDS | ITS | VMSCAPE)), + intel_steppings(COMETLAKE_L, steppings(0x0, 0x0), bl!(MMIO | RETBLEED | ITS | VMSCAPE)), + intel_steppings(COMETLAKE_L, ANY, bl!(MMIO | MMIO_SBDS | RETBLEED | GDS | ITS | VMSCAPE)), + intel_steppings(TIGERLAKE_L, ANY, bl!(GDS | ITS | ITS_NATIVE_ONLY)), + intel_steppings(TIGERLAKE, ANY, bl!(GDS | ITS | ITS_NATIVE_ONLY)), + intel_steppings(LAKEFIELD, ANY, bl!(MMIO | MMIO_SBDS | RETBLEED)), + intel_steppings(ROCKETLAKE, ANY, bl!(MMIO | RETBLEED | GDS | ITS | ITS_NATIVE_ONLY)), + intel_steppings(ALDERLAKE, ANY, bl!(RFDS | VMSCAPE)), + intel_steppings(ALDERLAKE_L, ANY, bl!(RFDS | VMSCAPE)), + intel_steppings(RAPTORLAKE, ANY, bl!(RFDS | VMSCAPE)), + intel_steppings(RAPTORLAKE_P, ANY, bl!(RFDS | VMSCAPE)), + intel_steppings(RAPTORLAKE_S, ANY, bl!(RFDS | VMSCAPE)), + intel_steppings(METEORLAKE_L, ANY, bl!(VMSCAPE)), + intel_steppings(ARROWLAKE_H, ANY, bl!(VMSCAPE)), + intel_steppings(ARROWLAKE, ANY, bl!(VMSCAPE)), + intel_steppings(ARROWLAKE_U, ANY, bl!(VMSCAPE)), + intel_steppings(LUNARLAKE_M, ANY, bl!(VMSCAPE)), + intel_steppings(SAPPHIRERAPIDS_X, ANY, bl!(VMSCAPE)), + intel_steppings(GRANITERAPIDS_X, ANY, bl!(VMSCAPE)), + intel_steppings(EMERALDRAPIDS_X, ANY, bl!(VMSCAPE)), + intel_steppings(ATOM_GRACEMONT, ANY, bl!(RFDS | VMSCAPE)), + intel_steppings(ATOM_TREMONT, ANY, bl!(MMIO | MMIO_SBDS | RFDS)), + intel_steppings(ATOM_TREMONT_D, ANY, bl!(MMIO | RFDS)), + intel_steppings(ATOM_TREMONT_L, ANY, bl!(MMIO | MMIO_SBDS | RFDS)), + intel_steppings(ATOM_GOLDMONT, ANY, bl!(RFDS)), + intel_steppings(ATOM_GOLDMONT_D, ANY, bl!(RFDS)), + intel_steppings(ATOM_GOLDMONT_PLUS, ANY, bl!(RFDS)), + intel_steppings(ATOM_CRESTMONT_X, ANY, bl!(VMSCAPE)), + + amd(0x15, bl!(RETBLEED)), + amd(0x16, bl!(RETBLEED)), + amd(0x17, bl!(RETBLEED | SMT_RSB | SRSO | VMSCAPE)), + hygon(0x18, bl!(RETBLEED | SMT_RSB | SRSO | VMSCAPE)), + amd(0x19, bl!(SRSO | TSA | VMSCAPE)), +]; + +/// `bad_spectre_microcode` (`intel.c:165-180`) over `spectre_bad_microcodes` +/// (`intel.c:141-163`): an Intel CPU outside a hypervisor whose microcode is +/// at or below its row's broke the speculation controls. +pub(crate) fn bad_spectre_microcode(id: &Ident, microcode: u32) -> bool { + const ROWS: &[(u32, u32, u32)] = &[ + (KABYLAKE, 0x0B, 0x80), + (KABYLAKE, 0x0A, 0x80), + (KABYLAKE, 0x09, 0x80), + (KABYLAKE_L, 0x0A, 0x80), + (KABYLAKE_L, 0x09, 0x80), + (SKYLAKE_X, 0x03, 0x0100013e), + (SKYLAKE_X, 0x04, 0x0200003c), + (BROADWELL, 0x04, 0x28), + (BROADWELL_G, 0x01, 0x1b), + (BROADWELL_D, 0x02, 0x14), + (BROADWELL_D, 0x03, 0x07000011), + (BROADWELL_X, 0x01, 0x0b000025), + (HASWELL_L, 0x01, 0x21), + (HASWELL_G, 0x01, 0x18), + (HASWELL, 0x03, 0x23), + (HASWELL_X, 0x02, 0x3b), + (HASWELL_X, 0x04, 0x10), + (IVYBRIDGE_X, 0x04, 0x42a), + (SANDYBRIDGE_X, 0x06, 0x61b), + (SANDYBRIDGE_X, 0x07, 0x712), + ]; + id.vendor == Vendor::Intel + && id.family == 6 + && ROWS + .iter() + .find(|&&(model, stepping, _)| model == id.model && stepping == id.stepping) + .is_some_and(|&(_, _, bad)| microcode <= bad) +} diff --git a/toyos-cpuvuln/src/tests.rs b/toyos-cpuvuln/src/tests.rs new file mode 100644 index 00000000000..90479fe4b90 --- /dev/null +++ b/toyos-cpuvuln/src/tests.rs @@ -0,0 +1,139 @@ +//! Facts in, Linux's lines out, judged against `grep .` over the +//! vulnerabilities directory. +//! +//! Both fixtures are provisional: their facts are read off documents and their +//! lines are Linux's source read by hand, so they hold this crate to one reading +//! of that source and not yet to the oracle. S0 of +//! `issues/kernel/the-kernel-mitigates-what-linux-mitigates-on-the-t14.md` +//! captures both machines under the pinned Linux; its facts replace these and +//! its lines replace `fixtures/provisional/`. + +extern crate std; + +use std::string::ToString; +use std::vec::Vec; + +use super::*; + +const PREFIX: &str = "/sys/devices/system/cpu/vulnerabilities/"; + +/// The T14's i5-1135G7 as documented: `GenuineIntel`, family 6 model 0x8C +/// (TIGERLAKE_L) stepping 1, four cores of two threads, eIBRS, not affected by +/// Meltdown, L1TF, MDS, TAA or ITLB multihit, GDS microcode present and +/// unlocked. Only the bits `decide` reads are set, and the microcode revision, +/// which no `spectre_bad_microcodes` row for this model reads, is unknown here. +const T14: Facts = Facts { + vendor: Vendor::from_id(b"GenuineIntel"), + signature: 0x0008_06C1, + microcode: 0, + cpuid_1_ecx: CPUID_1_ECX_AVX | CPUID_1_ECX_RDRAND, + cpuid_7_0_ebx: CPUID_7_0_EBX_RDSEED, + cpuid_7_0_edx: CPUID_7_0_EDX_MD_CLEAR + | CPUID_7_0_EDX_SPEC_CTRL + | CPUID_7_0_EDX_INTEL_STIBP + | CPUID_7_0_EDX_FLUSH_L1D + | CPUID_7_0_EDX_ARCH_CAPABILITIES + | CPUID_7_0_EDX_SPEC_CTRL_SSBD, + cpuid_7_2_edx: 0, + cpuid_8000_0008_ebx: 0, + cpuid_8000_0021_eax: 0, + arch_capabilities: ARCH_CAP_RDCL_NO + | ARCH_CAP_IBRS_ALL + | ARCH_CAP_MDS_NO + | ARCH_CAP_PSCHANGE_MC_NO + | ARCH_CAP_TAA_NO + | ARCH_CAP_GDS_CTRL, + mcu_opt_ctrl: Some(0), + smt: true, +}; + +/// `-cpu qemu64,+rdrand,+smap,+fsgsbase,+x2apic,+smep` under TCG at the QEMU +/// `.github/qemu-version` pins, v11.1.1, read off its `target/i386/cpu.c`: the +/// `qemu64` model is `AuthenticAMD` family 15 model 107 stepping 1 with +/// `xlevel` 0x8000000A, so leaf 0x80000021 is out of reach (3545-3563); every +/// model gets the hypervisor bit (8486); system-mode TCG enumerates no +/// SPEC_CTRL, ARCH_CAPABILITIES, SSBD or 0x80000008:EBX speculation bit +/// (996-1001, 1024-1034); the microcode revision is its AMD default +/// (10187-10197). `-smp 2` gives one thread per core. Only the bits `decide` +/// reads are set. +const TCG: Facts = Facts { + vendor: Vendor::from_id(b"AuthenticAMD"), + signature: 0x0006_0FB1, + microcode: 0x0100_0065, + cpuid_1_ecx: CPUID_1_ECX_RDRAND | CPUID_1_ECX_HYPERVISOR, + cpuid_7_0_ebx: 0, + cpuid_7_0_edx: 0, + cpuid_7_2_edx: 0, + cpuid_8000_0008_ebx: 0, + cpuid_8000_0021_eax: 0, + arch_capabilities: 0, + mcu_opt_ctrl: None, + smt: false, +}; + +/// `facts`' line for every file `capture` names, and `capture` names each +/// file once. +fn assert_capture(facts: &Facts, capture: &str) { + let d = decide(facts).expect("decided"); + let mut named = Vec::new(); + for l in capture.lines() { + let (file, want) = l + .strip_prefix(PREFIX) + .and_then(|rest| rest.split_once(':')) + .unwrap_or_else(|| panic!("{l:?} is not `grep .` over {PREFIX}")); + let v = *Vuln::ALL + .iter() + .find(|v| v.name() == file) + .unwrap_or_else(|| panic!("{file}: a file Linux does not have")); + let got = d.line(v).map(|line| line.to_string()); + assert_eq!(got.as_deref(), Ok(want), "{file}"); + named.push(file); + } + named.sort_unstable(); + let mut all: Vec<_> = Vuln::ALL.iter().map(|v| v.name()).collect(); + all.sort_unstable(); + assert_eq!(named, all, "the capture names every file once"); +} + +#[test] +fn the_t14s_facts_give_its_lines() { + assert_eq!(Ident::new(T14.vendor, T14.signature).model, 0x8C); + assert_capture(&T14, include_str!("../fixtures/provisional/t14.txt")); +} + +#[test] +fn the_tcg_models_facts_give_its_lines() { + let id = Ident::new(TCG.vendor, TCG.signature); + assert_eq!((id.family, id.model, id.stepping), (15, 107, 1)); + assert_capture(&TCG, include_str!("../fixtures/provisional/tcg.txt")); +} + +/// The track's exit control: the T14 with `IA32_ARCH_CAPABILITIES` read as 0. +/// Without `IBRS_ALL` it is in retpoline mode, without `GDS_CTRL` it has no GDS +/// microcode, and without `RDCL_NO` and `PSCHANGE_MC_NO` Linux's L1TF and +/// ITLB multihit lines turn on state the facts do not carry. +#[test] +fn the_t14_with_arch_capabilities_read_as_zero() { + let d = decide(&Facts { arch_capabilities: 0, mcu_opt_ctrl: None, ..T14 }).expect("decided"); + let line = |v| d.line(v).map(|l| l.to_string()); + assert_eq!( + line(Vuln::SpectreV2).as_deref(), + Ok( + "Mitigation: Retpolines; IBPB: conditional; IBRS_FW; STIBP: conditional; RSB filling; \ + PBRSB-eIBRS: Not affected; BHI: Not affected" + ) + ); + assert_eq!(line(Vuln::GatherDataSampling).as_deref(), Ok("Vulnerable: No microcode")); + assert_eq!(line(Vuln::Meltdown).as_deref(), Ok("Mitigation: PTI")); + assert_eq!(line(Vuln::Mds).as_deref(), Ok("Mitigation: Clear CPU buffers; SMT vulnerable")); + assert_eq!(line(Vuln::L1tf).err(), Some(Unmodelled::L1tf)); + assert_eq!(line(Vuln::ItlbMultihit).err(), Some(Unmodelled::ItlbMultihit)); +} + +#[test] +fn amd_from_family_0x15_and_hygon_are_refused() { + let zen3 = Facts { signature: 0x00A0_0F11, ..TCG }; + assert_eq!(decide(&zen3).err(), Some(Refused::AmdFamily(0x19))); + let hygon = Facts { vendor: Vendor::from_id(b"HygonGenuine"), ..TCG }; + assert_eq!(decide(&hygon).err(), Some(Refused::Hygon)); +} From a749acb6175968715618bfe81b8064bf2a893ed8 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 29 Sep 2026 08:59:59 +0200 Subject: [PATCH 2/6] toyos-cpuvuln: cite each Linux function to its closing line Every bugs.c, common.c, amd.c, hygon.c, intel.c and tsx.c range the crate and its issue cite now ends on the cited function's or block's closing line at Ubuntu-6.8.0-142.142, checked by printing both boundary lines of each. The previous commit's message cites intel.c:141-180 and tsx.c:158-229; the functions end at intel.c:182 and tsx.c:245. Co-Authored-By: Claude Opus 5.5 --- ...uses-amd-from-family-0x15-and-two-lines.md | 8 ++-- toyos-cpuvuln/src/lib.rs | 42 +++++++++---------- toyos-cpuvuln/src/table.rs | 2 +- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/issues/kernel/the-speculation-decision-refuses-amd-from-family-0x15-and-two-lines.md b/issues/kernel/the-speculation-decision-refuses-amd-from-family-0x15-and-two-lines.md index 77cd89a5fcc..422d0eda023 100644 --- a/issues/kernel/the-speculation-decision-refuses-amd-from-family-0x15-and-two-lines.md +++ b/issues/kernel/the-speculation-decision-refuses-amd-from-family-0x15-and-two-lines.md @@ -10,13 +10,13 @@ opened: 2026-09-29 state they do not carry, at `Ubuntu-6.8.0-142.142`: - **AMD family 0x15 and later, and Hygon**, refused whole. `amd.c` derives - `LS_CFG_SSBD` (576-594), the Zen generations (600-640), `TSA_SQ_NO`, + `LS_CFG_SSBD` (576-596), the Zen generations (600-648), `TSA_SQ_NO`, `TSA_L1_NO` and `VERW_CLEAR` from a microcode table (517-530) and - `IBPB_BRTYPE`/`SBPB` from a `PRED_CMD` write probe (799-805); `hygon.c` - derives `LS_CFG_SSBD` (228-238). Each feeds SSB, RETBLEED, SRSO or TSA. + `IBPB_BRTYPE`/`SBPB` from a `PRED_CMD` write probe (799-806); `hygon.c` + derives `LS_CFG_SSBD` (228-239). Each feeds SSB, SRSO or TSA. - **`l1tf` on an affected CPU**: the line reads the e820 map against `x86_cache_bits` (`bugs.c:2538-2583`) and `kvm_intel`'s state - (`bugs.c:3074-3090`). + (`bugs.c:3074-3089`). - **`itlb_multihit` on an affected CPU**: the line reads `IA32_FEAT_CTL` and `CR4.VMXE` (`bugs.c:3091-3102`). diff --git a/toyos-cpuvuln/src/lib.rs b/toyos-cpuvuln/src/lib.rs index f91715d9517..4f799de656e 100644 --- a/toyos-cpuvuln/src/lib.rs +++ b/toyos-cpuvuln/src/lib.rs @@ -30,7 +30,7 @@ use table::{ }; /// The vendors Linux's tables name, by CPUID.0's identification string -/// (`get_cpu_vendor`, `common.c:912-935`, over each `cpu_dev`'s `c_ident`). +/// (`get_cpu_vendor`, `common.c:912-936`, over each `cpu_dev`'s `c_ident`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Vendor { Intel, @@ -165,11 +165,11 @@ impl Ident { /// from state the facts do not carry. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Refused { - /// `amd.c` derives `LS_CFG_SSBD` (576-594), the Zen generations (600-640), - /// `TSA_*_NO` and `VERW_CLEAR` (517-530) and `IBPB_BRTYPE` (799-805) from + /// `amd.c` derives `LS_CFG_SSBD` (576-596), the Zen generations (600-648), + /// `TSA_*_NO` and `VERW_CLEAR` (517-530) and `IBPB_BRTYPE` (799-806) from /// the family, the microcode revision and MSR probes, from family 0x15 on. AmdFamily(u32), - /// `hygon.c` derives `LS_CFG_SSBD` (228-238) from an MSR probe. + /// `hygon.c` derives `LS_CFG_SSBD` (228-239) from an MSR probe. Hygon, } @@ -177,7 +177,7 @@ pub enum Refused { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Unmodelled { /// The e820 map against `x86_cache_bits` (`bugs.c:2538-2583`) and - /// `kvm_intel`'s state (`bugs.c:3074-3090`). + /// `kvm_intel`'s state (`bugs.c:3074-3089`). L1tf, /// `IA32_FEAT_CTL` and `CR4.VMXE` (`bugs.c:3091-3102`). ItlbMultihit, @@ -251,7 +251,7 @@ impl Vuln { } } -/// `tsx_ctrl_state` after `tsx_init` (`tsx.c:158-229`) under +/// `tsx_ctrl_state` after `tsx_init` (`tsx.c:158-245`) under /// `CONFIG_X86_INTEL_TSX_MODE_OFF=y`. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Tsx { @@ -263,7 +263,7 @@ pub enum Tsx { Disable, } -/// `spectre_v2_enabled` (`bugs.c:1860-1946`). +/// `spectre_v2_enabled` (`bugs.c:1860-2045`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SpectreV2 { None, @@ -286,7 +286,7 @@ pub enum Bhi { Vulnerable, } -/// Whether and how `switch_mm` issues IBPB (`bugs.c:1531-1552`). +/// Whether and how `switch_mm` issues IBPB (`bugs.c:1527-1550`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Ibpb { /// The CPU has no IBPB. @@ -304,7 +304,7 @@ pub enum Stibp { StrictPreferred, } -/// `retbleed_mitigation` (`bugs.c:1054-1206`). +/// `retbleed_mitigation` (`bugs.c:1054-1194`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Retbleed { None, @@ -320,7 +320,7 @@ pub enum Ssb { Prctl, } -/// `mds_mitigation` (`bugs.c:266-289`). +/// `mds_mitigation` (`bugs.c:266-283`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Mds { Off, @@ -328,7 +328,7 @@ pub enum Mds { Vmwerv, } -/// `taa_mitigation` (`bugs.c:327-386`). +/// `taa_mitigation` (`bugs.c:327-382`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Taa { Off, @@ -337,7 +337,7 @@ pub enum Taa { TsxDisabled, } -/// `mmio_mitigation` (`bugs.c:424-480`). +/// `mmio_mitigation` (`bugs.c:424-478`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Mmio { Off, @@ -345,7 +345,7 @@ pub enum Mmio { Verw, } -/// `rfds_mitigation` (`bugs.c:520-535`). +/// `rfds_mitigation` (`bugs.c:520-533`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Rfds { Off, @@ -353,7 +353,7 @@ pub enum Rfds { UcodeNeeded, } -/// `srbds_mitigation` where the CPU has SRBDS (`bugs.c:678-697`). +/// `srbds_mitigation` where the CPU has SRBDS (`bugs.c:678-700`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Srbds { UcodeNeeded, @@ -362,7 +362,7 @@ pub enum Srbds { Hypervisor, } -/// `gds_mitigation` where the CPU has GDS (`bugs.c:814-866`). +/// `gds_mitigation` where the CPU has GDS (`bugs.c:814-867`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Gds { UcodeNeeded, @@ -373,7 +373,7 @@ pub enum Gds { Hypervisor, } -/// `its_mitigation` (`bugs.c:1254-1334`). +/// `its_mitigation` (`bugs.c:1254-1335`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Its { Off, @@ -600,7 +600,7 @@ pub fn decide(facts: &Facts) -> Result { let mut rrsba_disabled = false; let mut efer_autoibrs = false; - // `spectre_v2_select_mitigation` (`bugs.c:1860-2046`). Where the CPU is + // `spectre_v2_select_mitigation` (`bugs.c:1860-2045`). Where the CPU is // unaffected it returns before any of it, leaving `spectre_v2_cmd` at // `SPECTRE_V2_CMD_NONE`. let mut spectre_v2 = SpectreV2::None; @@ -641,7 +641,7 @@ pub fn decide(facts: &Facts) -> Result { !(bugs.retbleed && caps.ibpb && id.vendor == Vendor::Amd) && caps.ibrs && !ibrs_mode; } - // `retbleed_select_mitigation` (`bugs.c:1054-1206`). + // `retbleed_select_mitigation` (`bugs.c:1054-1194`). let retbleed = if !bugs.retbleed { Retbleed::None } else if id.vendor == Vendor::Intel { @@ -657,7 +657,7 @@ pub fn decide(facts: &Facts) -> Result { }; // `spectre_v2_user_select_mitigation` (`bugs.c:1492-1591`) and - // `cpu_bugs_smt_update` (`bugs.c:2964-3060`). + // `cpu_bugs_smt_update` (`bugs.c:2964-3058`). let user_cmd_none = !bugs.spectre_v2; let ibpb = if !caps.ibpb { Ibpb::Absent @@ -681,7 +681,7 @@ pub fn decide(facts: &Facts) -> Result { let ssb = if caps.ssbd && bugs.ssb { Ssb::Prctl } else { Ssb::None }; - // `md_clear_select_mitigation` (`bugs.c:603-615`). Its update pass + // `md_clear_select_mitigation` (`bugs.c:603-616`). Its update pass // (`bugs.c:555-601`) changes nothing under the default command line: each // bug it selects for again is already selected. let md_clear = edx7 & CPUID_7_0_EDX_MD_CLEAR != 0; @@ -853,7 +853,7 @@ impl Line<'_> { } impl fmt::Display for Line<'_> { - /// `cpu_show_common` (`bugs.c:3305-3371`) and the `*_show_state` it calls. + /// `cpu_show_common` (`bugs.c:3305-3377`) and the `*_show_state` it calls. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let d = self.d; let b = &d.bugs; diff --git a/toyos-cpuvuln/src/table.rs b/toyos-cpuvuln/src/table.rs index 2aa801b51cb..7cfa1738132 100644 --- a/toyos-cpuvuln/src/table.rs +++ b/toyos-cpuvuln/src/table.rs @@ -282,7 +282,7 @@ const BLACKLIST: &[Row] = &[ amd(0x19, bl!(SRSO | TSA | VMSCAPE)), ]; -/// `bad_spectre_microcode` (`intel.c:165-180`) over `spectre_bad_microcodes` +/// `bad_spectre_microcode` (`intel.c:165-182`) over `spectre_bad_microcodes` /// (`intel.c:141-163`): an Intel CPU outside a hypervisor whose microcode is /// at or below its row's broke the speculation controls. pub(crate) fn bad_spectre_microcode(id: &Ident, microcode: u32) -> bool { From ca476ba517546be42ee4588c523f6b7044d9f81b Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 29 Sep 2026 10:08:45 +0200 Subject: [PATCH 3/6] toyos-cpuvuln: S0's T14, every mitigation field tested, AMD 0x17/0x19/0x1A Answers the review of #602 at a749acb6. The T14's facts are S0's words, from #601's toyos-t14linux/s0/t14/ at commit 44eb3c2e: CPUID.1:EAX 0x806c1, 1:ECX 0x7ffafbbf, (7,0) EBX 0xf3bfa7eb and EDX 0xfc100710 (SRBDS_CTRL set), (7,2):EDX 0x1, 0x10A 0x0a005c6b (FBSDP_NO and RFDS_NO set, TAA_NO clear), 0x123 0, microcode 0xbe. fixtures/provisional/ is gone: fixtures/t14.txt is byte-identical to s0/t14/vulnerabilities.txt, and fixtures/tcg.txt is s0/tcg/console.txt lines 379-395 with the serial console's CRs stripped. Every public Decision field is now asserted. The tests read them through an exhaustive destructuring, so a field added to Decision does not build until a test names it. The T14 asserts spec_ctrl == 0x1 against S0's MSR 0x48, which reads 0x1 on all eight CPUs, and the arch_capabilities-zero control asserts clear_cpu_buf. New host tests hold the x86_match_cpu stepping mask (KABYLAKE_L stepping 0xB against 0xC), the first-match rule (SKYLAKE_X stepping 5 against 6), the bad-microcode bound (KABYLAKE stepping 0xA at 0x80 against 0x81) and the hypervisor GDS answer. AMD families 0x17, 0x19 and 0x1A are decided, per the owner's ruling. That takes three new Facts fields. cpuid_8000_0021_ecx carries TSA_SQ_NO and TSA_L1_NO. ls_cfg_readable and sbpb_write_accepted carry the results of bsp_init_amd's LS_CFG read and early_init_amd's PRED_CMD.SBPB write; each is asserted present exactly where Linux probes. The new selections are SRSO under safe-ret, TSA with amd_check_tsa_microcode's table, VMSCAPE's IBPB-on-VMEXIT, and x86_pred_cmd's SBPB. The code the review found unreachable (AUTOIBRS, Retbleed Unret, STIBP always-on and the AMD IBRS_FW clause) is now reached, and Stibp::Prctl's empty arm is gone. X86_FEATURE_ZEN is set by init_amd from identify_cpu (amd.c:1039, common.c:1997), after early_identify_cpu has run cpu_set_bug_bits (common.c:1732). So the "Zen guest" TSA clause never fires on the boot CPU, and a family 0x1A guest is not TSA-affected. The Milan and Turin KVM-guest fixtures take their CPUID from InstLatx64's bare-metal dumps (EPYC 7713 at 2dc186e9, EPYC 9655 at b499237d) with the hypervisor bit set. Their lines are this crate's reading of the tag. They sit in fixtures/awaiting-capture/, and issues/build/no-nightly-runner-has-had-its-cpuid-and-vulnerability-lines-captured.md owes the real capture. In nightly run 36496779560 the 14 KVM shards ran on EPYC 7763 (11), 9V74 (2) and 9V45 (1). The refusal issue is renamed to what still stands: issues/kernel/the-speculation-decision-refuses-amd-0x15-0x16-hygon-and-two-lines.md. The TCG test now asserts its lines over both values of every fact S0 does not verify: the hypervisor bit, RDRAND, the microcode and SMT. The QEMU source reading behind the old constant was qemu64 at v11.1.1's target/i386/cpu.c: xlevel 0x8000000A at 3545-3563, the hypervisor bit at 8486, no speculation bits at 996-1001 and 1024-1034, and the microcode default at 10187-10197. Co-Authored-By: Claude Opus 5.5 --- ...-cpuid-and-vulnerability-lines-captured.md | 21 + ...fuses-amd-0x15-0x16-hygon-and-two-lines.md | 26 + ...uses-amd-from-family-0x15-and-two-lines.md | 30 -- .../fixtures/awaiting-capture/milan-kvm.txt | 17 + .../fixtures/awaiting-capture/turin-kvm.txt | 17 + .../fixtures/{provisional => }/t14.txt | 0 .../fixtures/{provisional => }/tcg.txt | 0 toyos-cpuvuln/src/lib.rs | 188 ++++++- toyos-cpuvuln/src/table.rs | 37 +- toyos-cpuvuln/src/tests.rs | 464 ++++++++++++++++-- 10 files changed, 689 insertions(+), 111 deletions(-) create mode 100644 issues/build/no-nightly-runner-has-had-its-cpuid-and-vulnerability-lines-captured.md create mode 100644 issues/kernel/the-speculation-decision-refuses-amd-0x15-0x16-hygon-and-two-lines.md delete mode 100644 issues/kernel/the-speculation-decision-refuses-amd-from-family-0x15-and-two-lines.md create mode 100644 toyos-cpuvuln/fixtures/awaiting-capture/milan-kvm.txt create mode 100644 toyos-cpuvuln/fixtures/awaiting-capture/turin-kvm.txt rename toyos-cpuvuln/fixtures/{provisional => }/t14.txt (100%) rename toyos-cpuvuln/fixtures/{provisional => }/tcg.txt (100%) diff --git a/issues/build/no-nightly-runner-has-had-its-cpuid-and-vulnerability-lines-captured.md b/issues/build/no-nightly-runner-has-had-its-cpuid-and-vulnerability-lines-captured.md new file mode 100644 index 00000000000..426e1f94360 --- /dev/null +++ b/issues/build/no-nightly-runner-has-had-its-cpuid-and-vulnerability-lines-captured.md @@ -0,0 +1,21 @@ +--- +status: open +kind: tooling +opened: 2026-09-29 +--- + +# No nightly runner has had its CPUID and vulnerability lines captured + +`toyos-cpuvuln`'s `fixtures/awaiting-capture/` holds the lines this crate +reads out of the pinned Linux for an EPYC Milan and an EPYC Turin as KVM +guests, from bare-metal CPUID dumps with the hypervisor bit set. Which bits +KVM passes through, whether it enumerates `ARCH_CAPABILITIES`, and whether the +host's microcode gives `IBPB_BRTYPE` are guesses there. In nightly run +36496779560 the 14 KVM shards ran on EPYC 7763 (11), 9V74 (2) and 9V45 (1), +each as `-cpu host,+rdrand,+smap,+fsgsbase,+x2apic,+smep` with `-smp cores=N`. + +**Exit**: a nightly step boots the pinned Linux under that command line on +the runner and records CPUID leaves 0, 1, 7.0, 7.2, 0x80000000, 0x80000008 and +0x80000021, MSR 0x10A where enumerated, the microcode revision, and `grep .` over +`/sys/devices/system/cpu/vulnerabilities/`; the two fixtures are replaced by +what it records, and one is added per CPU model it meets. diff --git a/issues/kernel/the-speculation-decision-refuses-amd-0x15-0x16-hygon-and-two-lines.md b/issues/kernel/the-speculation-decision-refuses-amd-0x15-0x16-hygon-and-two-lines.md new file mode 100644 index 00000000000..3e56b9afd2f --- /dev/null +++ b/issues/kernel/the-speculation-decision-refuses-amd-0x15-0x16-hygon-and-two-lines.md @@ -0,0 +1,26 @@ +--- +status: open +kind: defect +opened: 2026-09-29 +--- + +# The speculation decision refuses AMD 0x15, 0x16, Hygon, and two lines + +`toyos_cpuvuln::decide` answers only from its facts, and at +`Ubuntu-6.8.0-142.142` these inputs are not decided: + +- **AMD family 0x15, 0x16, 0x18 and from 0x1B, and Hygon**, refused whole. + The pinned Linux names 0x15 and 0x16 for Retbleed (`common.c:1338-1339`), + and `hygon.c` derives `LS_CFG_SSBD` from its own probe (228-239); no fixture + holds a reading of any of them. +- **`l1tf` on an affected CPU**: the line reads the e820 map against + `x86_cache_bits` (`bugs.c:2538-2583`) and `kvm_intel`'s state + (`bugs.c:3074-3089`). +- **`itlb_multihit` on an affected CPU**: the line reads `IA32_FEAT_CTL` and + `CR4.VMXE` (`bugs.c:3091-3102`). + +No machine ToyOS is tested on reaches one: the T14 is Intel, the TCG model is +AMD family 0xF, and the nightly's KVM runners are EPYC 7763, 9V74 and 9V45. + +**Exit**: each is decided from facts the kernel reads and held by a fixture +captured under the pinned Linux on such a CPU. diff --git a/issues/kernel/the-speculation-decision-refuses-amd-from-family-0x15-and-two-lines.md b/issues/kernel/the-speculation-decision-refuses-amd-from-family-0x15-and-two-lines.md deleted file mode 100644 index 422d0eda023..00000000000 --- a/issues/kernel/the-speculation-decision-refuses-amd-from-family-0x15-and-two-lines.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-29 ---- - -# The speculation decision refuses AMD from family 0x15, Hygon, and two lines - -`toyos_cpuvuln::decide` answers only from its facts, and three inputs rest on -state they do not carry, at `Ubuntu-6.8.0-142.142`: - -- **AMD family 0x15 and later, and Hygon**, refused whole. `amd.c` derives - `LS_CFG_SSBD` (576-596), the Zen generations (600-648), `TSA_SQ_NO`, - `TSA_L1_NO` and `VERW_CLEAR` from a microcode table (517-530) and - `IBPB_BRTYPE`/`SBPB` from a `PRED_CMD` write probe (799-806); `hygon.c` - derives `LS_CFG_SSBD` (228-239). Each feeds SSB, SRSO or TSA. -- **`l1tf` on an affected CPU**: the line reads the e820 map against - `x86_cache_bits` (`bugs.c:2538-2583`) and `kvm_intel`'s state - (`bugs.c:3074-3089`). -- **`itlb_multihit` on an affected CPU**: the line reads `IA32_FEAT_CTL` and - `CR4.VMXE` (`bugs.c:3091-3102`). - -The T14 and the TCG model reach none of them. The KVM arms of -`issues/kernel/the-kernel-mitigates-what-linux-mitigates-on-the-t14.md` (S2, -S3, S5, S6) expect "S1 over the reported facts" on the nightly's `-cpu host` -runners, and on an AMD Zen runner `decide` has no answer. - -**Exit**: each is decided from facts the kernel reads — for AMD, the family, -the microcode revision and the probe's result — and held by a fixture captured -under the pinned Linux on such a CPU. diff --git a/toyos-cpuvuln/fixtures/awaiting-capture/milan-kvm.txt b/toyos-cpuvuln/fixtures/awaiting-capture/milan-kvm.txt new file mode 100644 index 00000000000..379df7eefec --- /dev/null +++ b/toyos-cpuvuln/fixtures/awaiting-capture/milan-kvm.txt @@ -0,0 +1,17 @@ +/sys/devices/system/cpu/vulnerabilities/gather_data_sampling:Not affected +/sys/devices/system/cpu/vulnerabilities/indirect_target_selection:Not affected +/sys/devices/system/cpu/vulnerabilities/itlb_multihit:Not affected +/sys/devices/system/cpu/vulnerabilities/l1tf:Not affected +/sys/devices/system/cpu/vulnerabilities/mds:Not affected +/sys/devices/system/cpu/vulnerabilities/meltdown:Not affected +/sys/devices/system/cpu/vulnerabilities/mmio_stale_data:Not affected +/sys/devices/system/cpu/vulnerabilities/reg_file_data_sampling:Not affected +/sys/devices/system/cpu/vulnerabilities/retbleed:Not affected +/sys/devices/system/cpu/vulnerabilities/spec_rstack_overflow:Vulnerable: Safe RET, no microcode +/sys/devices/system/cpu/vulnerabilities/spec_store_bypass:Mitigation: Speculative Store Bypass disabled via prctl +/sys/devices/system/cpu/vulnerabilities/spectre_v1:Mitigation: usercopy/swapgs barriers and __user pointer sanitization +/sys/devices/system/cpu/vulnerabilities/spectre_v2:Mitigation: Retpolines; IBPB: conditional; IBRS_FW; STIBP: disabled; RSB filling; PBRSB-eIBRS: Not affected; BHI: Not affected +/sys/devices/system/cpu/vulnerabilities/srbds:Not affected +/sys/devices/system/cpu/vulnerabilities/tsa:Vulnerable: Clear CPU buffers attempted, no microcode +/sys/devices/system/cpu/vulnerabilities/tsx_async_abort:Not affected +/sys/devices/system/cpu/vulnerabilities/vmscape:Not affected diff --git a/toyos-cpuvuln/fixtures/awaiting-capture/turin-kvm.txt b/toyos-cpuvuln/fixtures/awaiting-capture/turin-kvm.txt new file mode 100644 index 00000000000..338f6c44fde --- /dev/null +++ b/toyos-cpuvuln/fixtures/awaiting-capture/turin-kvm.txt @@ -0,0 +1,17 @@ +/sys/devices/system/cpu/vulnerabilities/gather_data_sampling:Not affected +/sys/devices/system/cpu/vulnerabilities/indirect_target_selection:Not affected +/sys/devices/system/cpu/vulnerabilities/itlb_multihit:Not affected +/sys/devices/system/cpu/vulnerabilities/l1tf:Not affected +/sys/devices/system/cpu/vulnerabilities/mds:Not affected +/sys/devices/system/cpu/vulnerabilities/meltdown:Not affected +/sys/devices/system/cpu/vulnerabilities/mmio_stale_data:Not affected +/sys/devices/system/cpu/vulnerabilities/reg_file_data_sampling:Not affected +/sys/devices/system/cpu/vulnerabilities/retbleed:Not affected +/sys/devices/system/cpu/vulnerabilities/spec_rstack_overflow:Not affected +/sys/devices/system/cpu/vulnerabilities/spec_store_bypass:Mitigation: Speculative Store Bypass disabled via prctl +/sys/devices/system/cpu/vulnerabilities/spectre_v1:Mitigation: usercopy/swapgs barriers and __user pointer sanitization +/sys/devices/system/cpu/vulnerabilities/spectre_v2:Mitigation: Enhanced / Automatic IBRS; IBPB: conditional; STIBP: disabled; PBRSB-eIBRS: Not affected; BHI: Not affected +/sys/devices/system/cpu/vulnerabilities/srbds:Not affected +/sys/devices/system/cpu/vulnerabilities/tsa:Not affected +/sys/devices/system/cpu/vulnerabilities/tsx_async_abort:Not affected +/sys/devices/system/cpu/vulnerabilities/vmscape:Not affected diff --git a/toyos-cpuvuln/fixtures/provisional/t14.txt b/toyos-cpuvuln/fixtures/t14.txt similarity index 100% rename from toyos-cpuvuln/fixtures/provisional/t14.txt rename to toyos-cpuvuln/fixtures/t14.txt diff --git a/toyos-cpuvuln/fixtures/provisional/tcg.txt b/toyos-cpuvuln/fixtures/tcg.txt similarity index 100% rename from toyos-cpuvuln/fixtures/provisional/tcg.txt rename to toyos-cpuvuln/fixtures/tcg.txt diff --git a/toyos-cpuvuln/src/lib.rs b/toyos-cpuvuln/src/lib.rs index 4f799de656e..befd2163af8 100644 --- a/toyos-cpuvuln/src/lib.rs +++ b/toyos-cpuvuln/src/lib.rs @@ -6,7 +6,11 @@ //! fields), under the default command line and the pinned config. That Linux is //! tag `Ubuntu-6.8.0-142.142` (commit 53e5d07aac02) of Ubuntu's noble kernel //! with `/boot/config-6.8.0-142-generic`; every `path:line` here is read there, -//! and `common.c`, `bugs.c`, `intel.c` and `tsx.c` are in `arch/x86/kernel/cpu/`. +//! and `common.c`, `bugs.c`, `intel.c`, `amd.c` and `tsx.c` are in +//! `arch/x86/kernel/cpu/`. The config's `CONFIG_CPU_UNRET_ENTRY`, +//! `CONFIG_CPU_IBPB_ENTRY`, `CONFIG_CPU_SRSO`, `CONFIG_MITIGATION_TSA` and +//! `CONFIG_MITIGATION_VMSCAPE` are `y` +//! (`debian.master/config/annotations:3348,3319,3339,8048,411`). //! //! Parity is exact or refused: an input whose answer rests on something the //! facts do not carry is [`Refused`] whole, and a line that does is @@ -26,7 +30,7 @@ mod tests; use table::{ blacklisted, whitelisted, GDS, ITS, MMIO, MMIO_SBDS, MSBDS_ONLY, NO_BHI, NO_EIBRS_PBRSB, NO_ITLB_MULTIHIT, NO_L1TF, NO_MDS, NO_MELTDOWN, NO_MMIO, NO_SPECTRE_V2, - NO_SPECULATION, NO_SSB, RETBLEED, RFDS, SRBDS, VMSCAPE, + NO_SPECULATION, NO_SSB, RETBLEED, RFDS, SRBDS, SRSO, TSA, VMSCAPE, }; /// The vendors Linux's tables name, by CPUID.0's identification string @@ -73,12 +77,23 @@ pub struct Facts { pub cpuid_8000_0008_ebx: u32, /// 0 unless CPUID.0x80000000:EAX reaches 0x80000021 (`common.c:1055-1084`). pub cpuid_8000_0021_eax: u32, + /// 0 unless CPUID.0x80000000:EAX reaches 0x80000021 (`scattered.c:52-53`). + pub cpuid_8000_0021_ecx: u32, /// `IA32_ARCH_CAPABILITIES` (0x10A), 0 unless CPUID.(7,0):EDX[29] /// enumerates it (`common.c:1353-1361`): the read is `#GP` there. pub arch_capabilities: u64, /// `IA32_MCU_OPT_CTRL` (0x123), present exactly where /// `ARCH_CAPABILITIES.GDS_CTRL` enumerates it. pub mcu_opt_ctrl: Option, + /// Whether `rdmsr` of `MSR_AMD64_LS_CFG` (0xC0011020) completes without + /// `#GP`, present exactly where `bsp_init_amd` probes it (`amd.c:576-596`): + /// AMD family 0x15 to 0x17 enumerating neither `AMD_SSBD` nor `VIRT_SSBD`. + pub ls_cfg_readable: Option, + /// Whether `wrmsr` of `IA32_PRED_CMD` with `SBPB` (bit 7) completes without + /// `#GP`, present exactly where `early_init_amd` probes it + /// (`amd.c:799-806`): AMD from family 0x19, outside a hypervisor, without + /// CPUID's `IBPB_BRTYPE`. + pub sbpb_write_accepted: Option, /// More than one thread per core, every sibling online: `sched_smt_active()`, /// which under the default command line is also `cpu_smt_possible()`. pub smt: bool, @@ -109,7 +124,15 @@ const CPUID_8000_0008_EBX_AMD_SSBD: u32 = 1 << 24; const CPUID_8000_0008_EBX_VIRT_SSBD: u32 = 1 << 25; const CPUID_8000_0008_EBX_AMD_SSB_NO: u32 = 1 << 26; const CPUID_8000_0008_EBX_BTC_NO: u32 = 1 << 29; +const CPUID_8000_0021_EAX_VERW_CLEAR: u32 = 1 << 5; const CPUID_8000_0021_EAX_AUTOIBRS: u32 = 1 << 8; +const CPUID_8000_0021_EAX_SBPB: u32 = 1 << 27; +const CPUID_8000_0021_EAX_IBPB_BRTYPE: u32 = 1 << 28; +const CPUID_8000_0021_EAX_SRSO_NO: u32 = 1 << 29; +const CPUID_8000_0021_EAX_SRSO_USER_KERNEL_NO: u32 = 1 << 30; +const CPUID_8000_0021_EAX_SRSO_BP_SPEC_REDUCE: u32 = 1 << 31; +const CPUID_8000_0021_ECX_TSA_SQ_NO: u32 = 1 << 1; +const CPUID_8000_0021_ECX_TSA_L1_NO: u32 = 1 << 2; // `arch/x86/include/asm/msr-index.h:46-54,101-183,215`. const ARCH_CAP_RDCL_NO: u64 = 1 << 0; @@ -161,13 +184,10 @@ impl Ident { } } -/// An input whose every line rests on features Linux's vendor code synthesizes -/// from state the facts do not carry. +/// An input this crate does not decide. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Refused { - /// `amd.c` derives `LS_CFG_SSBD` (576-596), the Zen generations (600-648), - /// `TSA_*_NO` and `VERW_CLEAR` (517-530) and `IBPB_BRTYPE` (799-806) from - /// the family, the microcode revision and MSR probes, from family 0x15 on. + /// Every AMD family from 0x15 but 0x17, 0x19 and 0x1A. AmdFamily(u32), /// `hygon.c` derives `LS_CFG_SSBD` (228-239) from an MSR probe. Hygon, @@ -385,6 +405,29 @@ pub enum Its { pub enum Vmscape { None, IbpbExitToUser, + IbpbOnVmexit, +} + +/// `srso_mitigation` where the CPU has SRSO (`bugs.c:2670-2807`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Srso { + /// `SRSO_NO` forced: Zen1/2 with the IBPB microcode and no SMT. + SmtDisabled, + UcodeNeeded, + /// The safe-RET return thunk without the IBPB microcode. + SafeRetUcodeNeeded, + /// The safe-RET return thunk: `srso_alias_return_thunk` on family 0x19, + /// `srso_return_thunk` otherwise. + SafeRet, + IbpbOnVmexit, + BpSpecReduce, +} + +/// `tsa_mitigation` where the CPU has TSA (`bugs.c:2918-2962`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Tsa { + UcodeNeeded, + Full, } /// The bugs `cpu_set_bug_bits` (`common.c:1414-1578`) sets that a line or a @@ -409,13 +452,15 @@ struct Bugs { bhi: bool, vmscape: bool, its: bool, + srso: bool, + tsa: bool, meltdown: bool, l1tf: bool, } /// The features Linux has when it selects, after `init_speculation_control` -/// (`common.c:973-1012`) and `early_init_intel`'s microcode check -/// (`intel.c:296-309`). +/// (`common.c:973-1012`), `early_init_intel`'s microcode check +/// (`intel.c:296-309`) and `bsp_init_amd`'s `LS_CFG_SSBD` (`amd.c:576-596`). struct Caps { hypervisor: bool, ibrs: bool, @@ -445,6 +490,11 @@ pub struct Decision { pub gds: Option, pub its: Its, pub vmscape: Vmscape, + pub srso: Option, + pub tsa: Option, + /// `x86_pred_cmd` is `PRED_CMD_SBPB` rather than `PRED_CMD_IBPB` + /// (`bugs.c:2677-2678`). + pub sbpb: bool, /// `CLEAR_CPU_BUF`: `verw` on every return to user. pub clear_cpu_buf: bool, /// `EFER.AUTOIBRS` in place of `SPEC_CTRL.IBRS`. @@ -458,8 +508,7 @@ pub struct Decision { smt: bool, } -/// `cpu_set_bug_bits` (`common.c:1414-1578`). SRSO and TSA are set only on the -/// AMD families and Hygon [`decide`] refuses, and `SWAPGS`, `SMT_RSB` and +/// `cpu_set_bug_bits` (`common.c:1414-1578`). `SWAPGS`, `SMT_RSB` and /// `IBPB_NO_RET` reach no line and no selection here. fn bug_bits(f: &Facts, id: &Ident, caps: &Caps, arch: u64) -> Bugs { let mut b = Bugs { @@ -511,6 +560,14 @@ fn bug_bits(f: &Facts, id: &Ident, caps: &Caps, arch: u64) -> Bugs { && id.vendor == Vendor::Intel && f.cpuid_7_2_edx & CPUID_7_2_EDX_BHI_CTRL == 0 && (caps.hypervisor || blacklisted(id, ITS)); + b.srso = f.cpuid_8000_0021_eax & CPUID_8000_0021_EAX_SRSO_NO == 0 && blacklisted(id, SRSO); + // `tsa_init`'s forced `TSA_*_NO` (`amd.c:517-530`) reaches no family the + // TSA row names, and the Zen-guest clause reads `X86_FEATURE_ZEN`, which + // `init_amd` sets (`amd.c:1039`) from `identify_cpu` (`common.c:1997-1998`), + // after `early_identify_cpu` has run this (`common.c:1732`). + b.tsa = f.cpuid_8000_0021_ecx & (CPUID_8000_0021_ECX_TSA_SQ_NO | CPUID_8000_0021_ECX_TSA_L1_NO) + != CPUID_8000_0021_ECX_TSA_SQ_NO | CPUID_8000_0021_ECX_TSA_L1_NO + && blacklisted(id, TSA); if whitelisted(id, NO_MELTDOWN) || arch & ARCH_CAP_RDCL_NO != 0 { return b; } @@ -539,16 +596,20 @@ fn disable_kernel_rrsba(f: &Facts, arch: u64, spec_ctrl: &mut u64, rrsba_disable /// and what it reads. /// /// Panics where the facts contradict their own documentation: an -/// `arch_capabilities` read without CPUID.(7,0):EDX[29], or an `mcu_opt_ctrl` -/// present where `GDS_CTRL` does not say so, or absent where it does. +/// `arch_capabilities` read without CPUID.(7,0):EDX[29], or an `mcu_opt_ctrl`, +/// `ls_cfg_readable` or `sbpb_write_accepted` present where Linux does not read +/// it, or absent where it does. pub fn decide(facts: &Facts) -> Result { let f = facts; let id = Ident::new(f.vendor, f.signature); match id.vendor { - Vendor::Amd if id.family >= 0x15 => return Err(Refused::AmdFamily(id.family)), + Vendor::Amd if id.family >= 0x15 && !matches!(id.family, 0x17 | 0x19 | 0x1A) => { + return Err(Refused::AmdFamily(id.family)); + } Vendor::Hygon => return Err(Refused::Hygon), _ => {} } + let amd = id.vendor == Vendor::Amd; assert!( f.cpuid_7_0_edx & CPUID_7_0_EDX_ARCH_CAPABILITIES != 0 || f.arch_capabilities == 0, "ARCH_CAPABILITIES {:#x} without CPUID.(7,0):EDX[29]", @@ -563,7 +624,20 @@ pub fn decide(facts: &Facts) -> Result { let edx7 = f.cpuid_7_0_edx; let ebx8 = f.cpuid_8000_0008_ebx; + let eax21 = f.cpuid_8000_0021_eax; let hypervisor = f.cpuid_1_ecx & CPUID_1_ECX_HYPERVISOR != 0; + let amd_ssbd = ebx8 & (CPUID_8000_0008_EBX_VIRT_SSBD | CPUID_8000_0008_EBX_AMD_SSBD) != 0; + assert_eq!( + f.ls_cfg_readable.is_some(), + amd && (0x15..=0x17).contains(&id.family) && !amd_ssbd, + "MSR_AMD64_LS_CFG is probed exactly where amd.c:576-578 probes it" + ); + let brtype_enumerated = eax21 & CPUID_8000_0021_EAX_IBPB_BRTYPE != 0; + assert_eq!( + f.sbpb_write_accepted.is_some(), + amd && id.family >= 0x19 && !hypervisor && !brtype_enumerated, + "PRED_CMD.SBPB is probed exactly where amd.c:799-802 probes it" + ); let spec_ctrl_bit = edx7 & CPUID_7_0_EDX_SPEC_CTRL != 0; let intel_stibp = edx7 & CPUID_7_0_EDX_INTEL_STIBP != 0; let mut caps = Caps { @@ -572,9 +646,17 @@ pub fn decide(facts: &Facts) -> Result { ibpb: spec_ctrl_bit || ebx8 & CPUID_8000_0008_EBX_AMD_IBPB != 0, stibp: intel_stibp || ebx8 & CPUID_8000_0008_EBX_AMD_STIBP != 0, ssbd: edx7 & CPUID_7_0_EDX_SPEC_CTRL_SSBD != 0 - || ebx8 & (CPUID_8000_0008_EBX_VIRT_SSBD | CPUID_8000_0008_EBX_AMD_SSBD) != 0, - autoibrs: f.cpuid_8000_0021_eax & CPUID_8000_0021_EAX_AUTOIBRS != 0, + || amd_ssbd + || f.ls_cfg_readable == Some(true), + autoibrs: eax21 & CPUID_8000_0021_EAX_AUTOIBRS != 0, }; + // `early_init_amd` (`amd.c:799-806`). + let native_brtype = amd + && !hypervisor + && ((id.family == 0x17 && ebx8 & CPUID_8000_0008_EBX_AMD_IBPB != 0) + || f.sbpb_write_accepted == Some(true)); + let ibpb_brtype = brtype_enumerated || native_brtype; + let sbpb = eax21 & CPUID_8000_0021_EAX_SBPB != 0 || f.sbpb_write_accepted == Some(true); if id.vendor == Vendor::Intel && !hypervisor && (spec_ctrl_bit || intel_stibp || caps.ibrs || caps.ibpb || caps.stibp) @@ -750,10 +832,50 @@ pub fn decide(facts: &Facts) -> Result { } }); + // `srso_select_mitigation` (`bugs.c:2670-2807`) under its default + // `SRSO_CMD_SAFE_RET`; `retbleed` is never `IBPB` here. + let srso = bugs.srso.then_some( + if ibpb_brtype && id.family < 0x19 && !f.smt { + Srso::SmtDisabled + } else if eax21 & CPUID_8000_0021_EAX_SRSO_USER_KERNEL_NO != 0 { + if eax21 & CPUID_8000_0021_EAX_SRSO_BP_SPEC_REDUCE != 0 { + Srso::BpSpecReduce + } else if ibpb_brtype { + Srso::IbpbOnVmexit + } else { + Srso::UcodeNeeded + } + } else if ibpb_brtype { + Srso::SafeRet + } else { + Srso::SafeRetUcodeNeeded + }, + ); + let sbpb = !bugs.srso && sbpb; + let its = if bugs.its && spectre_v2 != SpectreV2::None { Its::AlignedThunks } else { Its::Off }; - let vmscape = - if bugs.vmscape && caps.ibpb { Vmscape::IbpbExitToUser } else { Vmscape::None }; + // `tsa_select_mitigation` (`bugs.c:2918-2962`) and `tsa_init`'s + // `VERW_CLEAR` (`amd.c:517-530`). + let tsa = bugs.tsa.then(|| { + let verw_clear = eax21 & CPUID_8000_0021_EAX_VERW_CLEAR != 0 + || (!hypervisor && table::tsa_microcode(&id, f.microcode)); + if verw_clear { + clear_cpu_buf = true; + Tsa::Full + } else { + clear_cpu_buf |= hypervisor; + Tsa::UcodeNeeded + } + }); + + let vmscape = if !(bugs.vmscape && caps.ibpb) { + Vmscape::None + } else if srso == Some(Srso::IbpbOnVmexit) { + Vmscape::IbpbOnVmexit + } else { + Vmscape::IbpbExitToUser + }; Ok(Decision { tsx, @@ -772,6 +894,9 @@ pub fn decide(facts: &Facts) -> Result { gds, its, vmscape, + srso, + tsa, + sbpb, clear_cpu_buf, efer_autoibrs, spec_ctrl, @@ -822,13 +947,13 @@ impl Line<'_> { if d.ibrs_fw { f.write_str("; IBRS_FW")?; } - // `stibp_state` (`bugs.c:3174-3193`). + // `stibp_state` (`bugs.c:3174-3193`); `Prctl` is selected only with SMT, + // which is what enables `switch_to_cond_stibp`. if !(d.spectre_v2 == SpectreV2::Eibrs && !d.autoibrs) { f.write_str(match d.stibp { Stibp::None => "; STIBP: disabled", Stibp::StrictPreferred => "; STIBP: always-on", - Stibp::Prctl if d.smt => "; STIBP: conditional", - Stibp::Prctl => "", + Stibp::Prctl => "; STIBP: conditional", })?; } // `RSB_CTXSW` (`bugs.c:1741-1789`). @@ -873,8 +998,8 @@ impl fmt::Display for Line<'_> { Vuln::RegFileDataSampling => b.rfds, Vuln::IndirectTargetSelection => b.its, Vuln::Vmscape => b.vmscape, - // Set only on the CPUs `decide` refuses. - Vuln::SpecRstackOverflow | Vuln::Tsa => false, + Vuln::SpecRstackOverflow => b.srso, + Vuln::Tsa => b.tsa, }; if !affected { return f.write_str("Not affected"); @@ -978,9 +1103,22 @@ impl fmt::Display for Line<'_> { Vuln::Vmscape => f.write_str(match d.vmscape { Vmscape::None => "Vulnerable", Vmscape::IbpbExitToUser => "Mitigation: IBPB before exit to userspace", + Vmscape::IbpbOnVmexit => "Mitigation: IBPB on VMEXIT", + }), + Vuln::SpecRstackOverflow => f.write_str(match d.srso.expect("set with the bug") { + Srso::SmtDisabled => "Mitigation: SMT disabled", + Srso::UcodeNeeded => "Vulnerable: No microcode", + Srso::SafeRetUcodeNeeded => "Vulnerable: Safe RET, no microcode", + Srso::SafeRet => "Mitigation: Safe RET", + Srso::IbpbOnVmexit => "Mitigation: IBPB on VMEXIT only", + Srso::BpSpecReduce => "Mitigation: Reduced Speculation", + }), + Vuln::Tsa => f.write_str(match d.tsa.expect("set with the bug") { + Tsa::UcodeNeeded => "Vulnerable: Clear CPU buffers attempted, no microcode", + Tsa::Full => "Mitigation: Clear CPU buffers", }), - Vuln::L1tf | Vuln::ItlbMultihit | Vuln::SpecRstackOverflow | Vuln::Tsa => { - unreachable!("{} is refused or never affected above", self.v.name()) + Vuln::L1tf | Vuln::ItlbMultihit => { + unreachable!("{} is unmodelled wherever affected", self.v.name()) } } } diff --git a/toyos-cpuvuln/src/table.rs b/toyos-cpuvuln/src/table.rs index 7cfa1738132..be5fc990d68 100644 --- a/toyos-cpuvuln/src/table.rs +++ b/toyos-cpuvuln/src/table.rs @@ -1,6 +1,7 @@ -//! Linux's three CPU tables, row for row: `cpu_vuln_whitelist` and -//! `cpu_vuln_blacklist` (`common.c:1182-1344`) and `spectre_bad_microcodes` -//! (`intel.c:141-163`). A lookup is `x86_match_cpu` (`match.c:36-57`): the +//! Linux's four CPU tables, row for row: `cpu_vuln_whitelist` and +//! `cpu_vuln_blacklist` (`common.c:1182-1344`), `spectre_bad_microcodes` +//! (`intel.c:141-163`) and `amd_check_tsa_microcode`'s (`amd.c:472-515`). A +//! lookup in the first two is `x86_match_cpu` (`match.c:36-57`): the //! first row whose vendor, family, model and stepping all match, `0` matching //! any. //! @@ -315,3 +316,33 @@ pub(crate) fn bad_spectre_microcode(id: &Ident, microcode: u32) -> bool { .find(|&&(model, stepping, _)| model == id.model && stepping == id.stepping) .is_some_and(|&(_, _, bad)| microcode <= bad) } + +/// `amd_check_tsa_microcode` (`amd.c:472-515`): a Zen3 or Zen4 CPU, which +/// `bsp_init_amd` names from family 0x19 models 0x00 to 0xAF (`amd.c:619-632`), +/// whose microcode is at or above its row's. +pub(crate) fn tsa_microcode(id: &Ident, microcode: u32) -> bool { + const ROWS: &[(u32, u32)] = &[ + (0xa0011, 0x0a0011d7), + (0xa0012, 0x0a00123b), + (0xa0082, 0x0a00820d), + (0xa1011, 0x0a10114c), + (0xa1012, 0x0a10124c), + (0xa1081, 0x0a108109), + (0xa2010, 0x0a20102e), + (0xa2012, 0x0a201211), + (0xa4041, 0x0a404108), + (0xa5000, 0x0a500012), + (0xa6012, 0x0a60120a), + (0xa7041, 0x0a704108), + (0xa7052, 0x0a705208), + (0xa7080, 0x0a708008), + (0xa70c0, 0x0a70c008), + (0xaa002, 0x0aa00216), + ]; + if !(id.vendor == Vendor::Amd && id.family == 0x19 && id.model <= 0xAF) { + return false; + } + // `union zen_patch_rev` (`asm/cpu.h:80-90`) above its `rev` byte. + let key = (id.family - 0xf) << 16 | (id.model >> 4) << 12 | (id.model & 0xf) << 4 | id.stepping; + ROWS.iter().find(|&&(k, _)| k == key).is_some_and(|&(_, min)| microcode >= min) +} diff --git a/toyos-cpuvuln/src/tests.rs b/toyos-cpuvuln/src/tests.rs index 90479fe4b90..ca81a086297 100644 --- a/toyos-cpuvuln/src/tests.rs +++ b/toyos-cpuvuln/src/tests.rs @@ -1,12 +1,5 @@ -//! Facts in, Linux's lines out, judged against `grep .` over the -//! vulnerabilities directory. -//! -//! Both fixtures are provisional: their facts are read off documents and their -//! lines are Linux's source read by hand, so they hold this crate to one reading -//! of that source and not yet to the oracle. S0 of -//! `issues/kernel/the-kernel-mitigates-what-linux-mitigates-on-the-t14.md` -//! captures both machines under the pinned Linux; its facts replace these and -//! its lines replace `fixtures/provisional/`. +//! Facts in, Linux's lines and selection out, the lines judged against +//! `grep .` over the vulnerabilities directory. extern crate std; @@ -17,63 +10,207 @@ use super::*; const PREFIX: &str = "/sys/devices/system/cpu/vulnerabilities/"; -/// The T14's i5-1135G7 as documented: `GenuineIntel`, family 6 model 0x8C -/// (TIGERLAKE_L) stepping 1, four cores of two threads, eIBRS, not affected by -/// Meltdown, L1TF, MDS, TAA or ITLB multihit, GDS microcode present and -/// unlocked. Only the bits `decide` reads are set, and the microcode revision, -/// which no `spectre_bad_microcodes` row for this model reads, is unknown here. +/// The T14's i5-1135G7 as S0 read it under the pinned Linux: PR #601's +/// `toyos-t14linux/s0/t14/cpuid.txt`, `msr.txt` and `cpuinfo.txt` at commit +/// 44eb3c2e. Every MSR reads the same on all eight CPUs. const T14: Facts = Facts { vendor: Vendor::from_id(b"GenuineIntel"), - signature: 0x0008_06C1, - microcode: 0, - cpuid_1_ecx: CPUID_1_ECX_AVX | CPUID_1_ECX_RDRAND, - cpuid_7_0_ebx: CPUID_7_0_EBX_RDSEED, - cpuid_7_0_edx: CPUID_7_0_EDX_MD_CLEAR - | CPUID_7_0_EDX_SPEC_CTRL - | CPUID_7_0_EDX_INTEL_STIBP - | CPUID_7_0_EDX_FLUSH_L1D - | CPUID_7_0_EDX_ARCH_CAPABILITIES - | CPUID_7_0_EDX_SPEC_CTRL_SSBD, - cpuid_7_2_edx: 0, + signature: 0x0008_06c1, + microcode: 0xbe, + cpuid_1_ecx: 0x7ffa_fbbf, + cpuid_7_0_ebx: 0xf3bf_a7eb, + cpuid_7_0_edx: 0xfc10_0710, + cpuid_7_2_edx: 0x0000_0001, cpuid_8000_0008_ebx: 0, + // CPUID.0x80000000:EAX is 0x80000008. cpuid_8000_0021_eax: 0, - arch_capabilities: ARCH_CAP_RDCL_NO - | ARCH_CAP_IBRS_ALL - | ARCH_CAP_MDS_NO - | ARCH_CAP_PSCHANGE_MC_NO - | ARCH_CAP_TAA_NO - | ARCH_CAP_GDS_CTRL, + cpuid_8000_0021_ecx: 0, + arch_capabilities: 0x0a00_5c6b, mcu_opt_ctrl: Some(0), + ls_cfg_readable: None, + sbpb_write_accepted: None, + // `siblings 8`, `cpu cores 4`. smt: true, }; -/// `-cpu qemu64,+rdrand,+smap,+fsgsbase,+x2apic,+smep` under TCG at the QEMU -/// `.github/qemu-version` pins, v11.1.1, read off its `target/i386/cpu.c`: the -/// `qemu64` model is `AuthenticAMD` family 15 model 107 stepping 1 with -/// `xlevel` 0x8000000A, so leaf 0x80000021 is out of reach (3545-3563); every -/// model gets the hypervisor bit (8486); system-mode TCG enumerates no -/// SPEC_CTRL, ARCH_CAPABILITIES, SSBD or 0x80000008:EBX speculation bit -/// (996-1001, 1024-1034); the microcode revision is its AMD default -/// (10187-10197). `-smp 2` gives one thread per core. Only the bits `decide` -/// reads are set. +/// The TCG model: `qemu64`, `AuthenticAMD` family 0xF model 0x6B stepping 1, +/// the signature S0's capture verifies; the bits `decide` reads in leaves 7 +/// and 0x80000008 are clear in that model, and it has no leaf 0x80000021. const TCG: Facts = Facts { vendor: Vendor::from_id(b"AuthenticAMD"), - signature: 0x0006_0FB1, - microcode: 0x0100_0065, - cpuid_1_ecx: CPUID_1_ECX_RDRAND | CPUID_1_ECX_HYPERVISOR, + signature: 0x0006_0fb1, + microcode: 0, + cpuid_1_ecx: 0, cpuid_7_0_ebx: 0, cpuid_7_0_edx: 0, cpuid_7_2_edx: 0, cpuid_8000_0008_ebx: 0, cpuid_8000_0021_eax: 0, + cpuid_8000_0021_ecx: 0, + arch_capabilities: 0, + mcu_opt_ctrl: None, + ls_cfg_readable: None, + sbpb_write_accepted: None, + smt: false, +}; + +/// An EPYC 7713 (Milan, family 0x19 model 1 stepping 1, the family and model +/// of the nightly's EPYC 7763) as a KVM guest: InstLatx64's bare-metal dump +/// `AuthenticAMD/AuthenticAMD0A00F11_K19_Milan_CPUID1.txt` (commit 2dc186e9) +/// with CPUID.1:ECX[31] set, `-smp cores=N`, and no microcode revision, which +/// no line reads under a hypervisor. It does not enumerate ARCH_CAPABILITIES. +const MILAN_GUEST: Facts = Facts { + vendor: Vendor::from_id(b"AuthenticAMD"), + signature: 0x00a0_0f11, + microcode: 0, + cpuid_1_ecx: 0x7eda_320b | CPUID_1_ECX_HYPERVISOR, + cpuid_7_0_ebx: 0x219c_97a9, + cpuid_7_0_edx: 0x0000_0010, + cpuid_7_2_edx: 0, + cpuid_8000_0008_ebx: 0x91be_f75f, + cpuid_8000_0021_eax: 0x0000_204d, + cpuid_8000_0021_ecx: 0, + arch_capabilities: 0, + mcu_opt_ctrl: None, + ls_cfg_readable: None, + sbpb_write_accepted: None, + smt: false, +}; + +/// An EPYC 9655 (Turin, family 0x1A model 2 stepping 1, the family of the +/// nightly's EPYC 9V45) as a KVM guest: InstLatx64's bare-metal dump +/// `AuthenticAMD/AuthenticAMD0B00F21_K20_Turin_01_CPUID.txt` (commit b499237d), +/// shaped as [`MILAN_GUEST`] is. +const TURIN_GUEST: Facts = Facts { + vendor: Vendor::from_id(b"AuthenticAMD"), + signature: 0x00b0_0f21, + microcode: 0, + cpuid_1_ecx: 0x7efa_320b | CPUID_1_ECX_HYPERVISOR, + cpuid_7_0_ebx: 0xf1bf_97ab, + cpuid_7_0_edx: 0x1000_0110, + cpuid_7_2_edx: 0, + cpuid_8000_0008_ebx: 0x79be_f25f, + cpuid_8000_0021_eax: 0xd93f_ffcf, + cpuid_8000_0021_ecx: 0, arch_capabilities: 0, mcu_opt_ctrl: None, + ls_cfg_readable: None, + sbpb_write_accepted: None, smt: false, }; +/// [`Decision`]'s selection, every public field: the destructuring in +/// [`state`] names each, so a field added there fails to build here. +#[derive(Debug, PartialEq)] +struct State { + tsx: Tsx, + spectre_v2: SpectreV2, + bhi: Bhi, + ibpb: Ibpb, + ibrs_fw: bool, + stibp: Stibp, + retbleed: Retbleed, + ssb: Ssb, + mds: Mds, + taa: Taa, + mmio: Mmio, + rfds: Rfds, + srbds: Option, + gds: Option, + its: Its, + vmscape: Vmscape, + srso: Option, + tsa: Option, + sbpb: bool, + clear_cpu_buf: bool, + efer_autoibrs: bool, + spec_ctrl: u64, +} + +fn state(d: &Decision) -> State { + let Decision { + tsx, + spectre_v2, + bhi, + ibpb, + ibrs_fw, + stibp, + retbleed, + ssb, + mds, + taa, + mmio, + rfds, + srbds, + gds, + its, + vmscape, + srso, + tsa, + sbpb, + clear_cpu_buf, + efer_autoibrs, + spec_ctrl, + bugs: _, + hypervisor: _, + autoibrs: _, + smt: _, + } = *d; + State { + tsx, + spectre_v2, + bhi, + ibpb, + ibrs_fw, + stibp, + retbleed, + ssb, + mds, + taa, + mmio, + rfds, + srbds, + gds, + its, + vmscape, + srso, + tsa, + sbpb, + clear_cpu_buf, + efer_autoibrs, + spec_ctrl, + } +} + +/// Nothing selected: a CPU no vulnerability reaches. +const NONE: State = State { + tsx: Tsx::NotSupported, + spectre_v2: SpectreV2::None, + bhi: Bhi::NotAffected, + ibpb: Ibpb::Absent, + ibrs_fw: false, + stibp: Stibp::None, + retbleed: Retbleed::None, + ssb: Ssb::None, + mds: Mds::Off, + taa: Taa::Off, + mmio: Mmio::Off, + rfds: Rfds::Off, + srbds: None, + gds: None, + its: Its::Off, + vmscape: Vmscape::None, + srso: None, + tsa: None, + sbpb: false, + clear_cpu_buf: false, + efer_autoibrs: false, + spec_ctrl: 0, +}; + /// `facts`' line for every file `capture` names, and `capture` names each /// file once. -fn assert_capture(facts: &Facts, capture: &str) { +fn assert_capture(facts: &Facts, capture: &str) -> Decision { let d = decide(facts).expect("decided"); let mut named = Vec::new(); for l in capture.lines() { @@ -93,25 +230,58 @@ fn assert_capture(facts: &Facts, capture: &str) { let mut all: Vec<_> = Vuln::ALL.iter().map(|v| v.name()).collect(); all.sort_unstable(); assert_eq!(named, all, "the capture names every file once"); + d +} + +fn line(facts: &Facts, v: Vuln) -> std::string::String { + decide(facts).expect("decided").line(v).expect("modelled").to_string() } #[test] -fn the_t14s_facts_give_its_lines() { +fn the_t14s_facts_give_its_lines_and_its_spec_ctrl() { assert_eq!(Ident::new(T14.vendor, T14.signature).model, 0x8C); - assert_capture(&T14, include_str!("../fixtures/provisional/t14.txt")); + let d = assert_capture(&T14, include_str!("../fixtures/t14.txt")); + // `spec_ctrl` against S0's `msr.txt`: 0x48 reads 0x1 on every CPU. + assert_eq!( + state(&d), + State { + spectre_v2: SpectreV2::Eibrs, + bhi: Bhi::SwLoop, + ibpb: Ibpb::Conditional, + ssb: Ssb::Prctl, + gds: Some(Gds::Full), + its: Its::AlignedThunks, + spec_ctrl: 0x1, + ..NONE + } + ); } +/// S0 verifies the TCG model's signature and nothing else it could read, so +/// its lines are held over either value of every other fact they could rest on. #[test] -fn the_tcg_models_facts_give_its_lines() { +fn the_tcg_models_signature_gives_its_lines() { let id = Ident::new(TCG.vendor, TCG.signature); assert_eq!((id.family, id.model, id.stepping), (15, 107, 1)); - assert_capture(&TCG, include_str!("../fixtures/provisional/tcg.txt")); + for hypervisor in [0, CPUID_1_ECX_HYPERVISOR] { + for rdrand in [0, CPUID_1_ECX_RDRAND] { + for microcode in [0, u32::MAX] { + for smt in [false, true] { + let facts = + Facts { cpuid_1_ecx: hypervisor | rdrand, microcode, smt, ..TCG }; + let d = assert_capture(&facts, include_str!("../fixtures/tcg.txt")); + assert_eq!(state(&d), State { spectre_v2: SpectreV2::Retpoline, ..NONE }); + } + } + } + } } /// The track's exit control: the T14 with `IA32_ARCH_CAPABILITIES` read as 0. /// Without `IBRS_ALL` it is in retpoline mode, without `GDS_CTRL` it has no GDS -/// microcode, and without `RDCL_NO` and `PSCHANGE_MC_NO` Linux's L1TF and -/// ITLB multihit lines turn on state the facts do not carry. +/// microcode, without `MDS_NO` it clears CPU buffers, and without `RDCL_NO` and +/// `PSCHANGE_MC_NO` Linux's L1TF and ITLB multihit lines turn on state the +/// facts do not carry. #[test] fn the_t14_with_arch_capabilities_read_as_zero() { let d = decide(&Facts { arch_capabilities: 0, mcu_opt_ctrl: None, ..T14 }).expect("decided"); @@ -128,12 +298,200 @@ fn the_t14_with_arch_capabilities_read_as_zero() { assert_eq!(line(Vuln::Mds).as_deref(), Ok("Mitigation: Clear CPU buffers; SMT vulnerable")); assert_eq!(line(Vuln::L1tf).err(), Some(Unmodelled::L1tf)); assert_eq!(line(Vuln::ItlbMultihit).err(), Some(Unmodelled::ItlbMultihit)); + assert_eq!( + state(&d), + State { + spectre_v2: SpectreV2::Retpoline, + ibpb: Ibpb::Conditional, + ibrs_fw: true, + stibp: Stibp::Prctl, + ssb: Ssb::Prctl, + mds: Mds::Full, + gds: Some(Gds::UcodeNeeded), + its: Its::AlignedThunks, + clear_cpu_buf: true, + ..NONE + } + ); +} + +/// `x86_match_cpu` matches a row's steppings as a mask: KABYLAKE_L's first row +/// covers steppings 0 to 0xB and leaves ITS out. +#[test] +fn a_blacklist_row_matches_only_its_steppings() { + let at = |stepping: u32| { + line(&Facts { signature: 0x0008_06e0 | stepping, ..T14 }, Vuln::IndirectTargetSelection) + }; + assert_eq!(at(0xB), "Not affected"); + assert_eq!(at(0xC), "Mitigation: Aligned branch/return thunks"); +} + +/// The first matching row decides: SKYLAKE_X's steppings 0 to 5 row, without +/// ITS, stands before its any-stepping row, with it. +#[test] +fn the_first_matching_blacklist_row_decides() { + let at = |stepping: u32| { + line(&Facts { signature: 0x0005_0650 | stepping, ..T14 }, Vuln::IndirectTargetSelection) + }; + assert_eq!(at(5), "Not affected"); + assert_eq!(at(6), "Mitigation: Aligned branch/return thunks"); } +/// KABYLAKE stepping 0xA's bad microcode is 0x80 and below: there Linux drops +/// IBRS, IBPB, STIBP and SSBD. #[test] -fn amd_from_family_0x15_and_hygon_are_refused() { - let zen3 = Facts { signature: 0x00A0_0F11, ..TCG }; - assert_eq!(decide(&zen3).err(), Some(Refused::AmdFamily(0x19))); +fn spectre_bad_microcode_includes_its_bound() { + let kabylake = |microcode| { + decide(&Facts { + signature: 0x0009_06ea, + microcode, + arch_capabilities: 0, + mcu_opt_ctrl: None, + ..T14 + }) + .expect("decided") + }; + let bad = kabylake(0x80); + assert_eq!((bad.ibpb, bad.ssb, bad.spectre_v2), (Ibpb::Absent, Ssb::None, SpectreV2::Retpoline)); + let good = kabylake(0x81); + assert_eq!((good.ibpb, good.ssb, good.spectre_v2), (Ibpb::Conditional, Ssb::Prctl, SpectreV2::Ibrs)); +} + +#[test] +fn a_guest_cannot_know_its_gds_mitigation() { + let guest = Facts { cpuid_1_ecx: T14.cpuid_1_ecx | CPUID_1_ECX_HYPERVISOR, ..T14 }; + assert_eq!(line(&guest, Vuln::GatherDataSampling), "Unknown: Dependent on hypervisor status"); +} + +// The two fixtures below are this crate's reading of the pinned Linux, awaiting +// a capture on a nightly runner: +// `issues/build/no-nightly-runner-has-had-its-cpuid-and-vulnerability-lines-captured.md`. + +#[test] +fn a_milan_kvm_guest_gives_the_tags_lines() { + let d = assert_capture(&MILAN_GUEST, include_str!("../fixtures/awaiting-capture/milan-kvm.txt")); + assert_eq!( + state(&d), + State { + spectre_v2: SpectreV2::Retpoline, + ibpb: Ibpb::Conditional, + ibrs_fw: true, + ssb: Ssb::Prctl, + srso: Some(Srso::SafeRetUcodeNeeded), + tsa: Some(Tsa::UcodeNeeded), + clear_cpu_buf: true, + ..NONE + } + ); +} + +#[test] +fn a_turin_kvm_guest_gives_the_tags_lines() { + let d = assert_capture(&TURIN_GUEST, include_str!("../fixtures/awaiting-capture/turin-kvm.txt")); + assert_eq!( + state(&d), + State { + spectre_v2: SpectreV2::Eibrs, + ibpb: Ibpb::Conditional, + ssb: Ssb::Prctl, + sbpb: true, + efer_autoibrs: true, + ..NONE + } + ); +} + +/// A Rome (Zen2, family 0x17 model 0x31) outside a hypervisor: its IBPB +/// implies `IBPB_BRTYPE`, so without SMT it is SRSO-immune, and with SMT the +/// untrained return thunk forces STIBP always on. +#[test] +fn zen2_selects_by_smt() { + let rome = |smt| Facts { + signature: 0x0083_0f10, + cpuid_1_ecx: 0, + cpuid_8000_0021_eax: 0, + smt, + ..MILAN_GUEST + }; + let d = decide(&rome(false)).expect("decided"); + assert_eq!( + d.line(Vuln::SpecRstackOverflow).expect("modelled").to_string(), + "Mitigation: SMT disabled" + ); + assert_eq!( + d.line(Vuln::Retbleed).expect("modelled").to_string(), + "Mitigation: untrained return thunk; SMT disabled" + ); + let d = decide(&rome(true)).expect("decided"); + assert_eq!( + d.line(Vuln::Retbleed).expect("modelled").to_string(), + "Mitigation: untrained return thunk; SMT enabled with STIBP protection" + ); + assert_eq!( + state(&d), + State { + spectre_v2: SpectreV2::Retpoline, + ibpb: Ibpb::Conditional, + stibp: Stibp::StrictPreferred, + retbleed: Retbleed::Unret, + ssb: Ssb::Prctl, + vmscape: Vmscape::IbpbExitToUser, + srso: Some(Srso::SafeRet), + spec_ctrl: SPEC_CTRL_STIBP, + ..NONE + } + ); +} + +/// A Milan outside a hypervisor: its `PRED_CMD.SBPB` probe decides SRSO's +/// microcode, and `amd_check_tsa_microcode`'s row for 0xA0011 decides TSA's. +#[test] +fn zen3_native_reads_its_probe_and_its_tsa_microcode() { + let milan = |microcode, sbpb| Facts { + microcode, + cpuid_1_ecx: MILAN_GUEST.cpuid_1_ecx & !CPUID_1_ECX_HYPERVISOR, + sbpb_write_accepted: Some(sbpb), + ..MILAN_GUEST + }; + let native = State { + spectre_v2: SpectreV2::Retpoline, + ibpb: Ibpb::Conditional, + ibrs_fw: true, + ssb: Ssb::Prctl, + vmscape: Vmscape::IbpbExitToUser, + ..NONE + }; + let d = decide(&milan(0x0a00_11d6, false)).expect("decided"); + assert_eq!( + state(&d), + State { + srso: Some(Srso::SafeRetUcodeNeeded), + tsa: Some(Tsa::UcodeNeeded), + ..native + } + ); + let d = decide(&milan(0x0a00_11d7, true)).expect("decided"); + assert_eq!( + state(&d), + State { srso: Some(Srso::SafeRet), tsa: Some(Tsa::Full), clear_cpu_buf: true, ..native } + ); + assert_eq!(d.line(Vuln::Tsa).expect("modelled").to_string(), "Mitigation: Clear CPU buffers"); +} + +#[test] +#[should_panic(expected = "PRED_CMD.SBPB is probed exactly where")] +fn a_native_zen3_without_its_probe_is_a_contradiction() { + let _ = decide(&Facts { cpuid_1_ecx: 0, ..MILAN_GUEST }); +} + +#[test] +fn amd_outside_0x17_0x19_0x1a_from_0x15_and_hygon_are_refused() { + for (signature, family) in [(0x0060_0f00, 0x15), (0x0070_0f00, 0x16), (0x00c0_0f00, 0x1B)] { + assert_eq!( + decide(&Facts { signature, ..TCG }).err(), + Some(Refused::AmdFamily(family)) + ); + } let hygon = Facts { vendor: Vendor::from_id(b"HygonGenuine"), ..TCG }; assert_eq!(decide(&hygon).err(), Some(Refused::Hygon)); } From 465b9e98a5087ddb078b6dfda31166ede31dffcc Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 29 Sep 2026 11:33:53 +0200 Subject: [PATCH 4/6] toyos-cpuvuln: a failing test for each AMD arm and Intel guest arm; decide every AMD family Review round 2 of #602 named nine mutations that passed every test. Each now has a host test that goes red, built from the pinned tag's logic (Ubuntu-6.8.0-142.142, 53e5d07a) and citing the lines behind each expected line: - STIBP always-on from AMD_STIBP_ALWAYS_ON without Unret: a native Milan with SMT (bugs.c:1575-1577, 2054-2068, 3185-3186). - The LS_CFG probe as the source of SSBD, and the probe's family bound: AMD 0x15, 0x16 and Zen1 0x17 without AMD_SSBD/VIRT_SSBD (amd.c:576-596). - SRSO_USER_KERNEL_NO's three branches and VMSCAPE's IBPB on VM exit (bugs.c:2703, 2715-2716, 2771-2781, 2861-2863). - The TSA_SQ_NO/TSA_L1_NO decoding (common.c:1555-1561, scattered.c:52-53). - A Zen4 row of the TSA microcode table: Genoa at 0x0a10114c and 0x0a10114b (amd.c:491, 522-525). - SRBDS and MDS inside an Intel guest: KABYLAKE without ARCH_CAPABILITIES (bugs.c:691-692, 3117-3119). The refusals now follow the module's single rule: an input is refused only when its answer rests on something the facts do not carry. AMD 0x15, 0x16, 0x18 and 0x1B and later rest on nothing beyond the facts: the LS_CFG probe is carried, and bugs.c's only family tests are SRSO's. So they are decided, and Refused::AmdFamily is deleted. Hygon stays refused because bsp_init_hygon's own LS_CFG probe (hygon.c:228-239) is not carried. Its table rows could never be reached and are deleted. The refusal issue is renamed to match. amd_check_tsa_microcode's `model <= 0xAF` bound changed no answer, because every row keys a model that bsp_init_amd names Zen3 or Zen4, so it is deleted. The false "does not enumerate ARCH_CAPABILITIES" comment is deleted: KVM enumerates it at the tag (kvm/cpuid.c:691). Co-Authored-By: Claude Opus 5.5 --- ...n-decision-refuses-hygon-and-two-lines.md} | 8 +- toyos-cpuvuln/src/lib.rs | 13 +- toyos-cpuvuln/src/table.rs | 16 +- toyos-cpuvuln/src/tests.rs | 204 +++++++++++++++--- 4 files changed, 191 insertions(+), 50 deletions(-) rename issues/kernel/{the-speculation-decision-refuses-amd-0x15-0x16-hygon-and-two-lines.md => the-speculation-decision-refuses-hygon-and-two-lines.md} (68%) diff --git a/issues/kernel/the-speculation-decision-refuses-amd-0x15-0x16-hygon-and-two-lines.md b/issues/kernel/the-speculation-decision-refuses-hygon-and-two-lines.md similarity index 68% rename from issues/kernel/the-speculation-decision-refuses-amd-0x15-0x16-hygon-and-two-lines.md rename to issues/kernel/the-speculation-decision-refuses-hygon-and-two-lines.md index 3e56b9afd2f..15e5506123b 100644 --- a/issues/kernel/the-speculation-decision-refuses-amd-0x15-0x16-hygon-and-two-lines.md +++ b/issues/kernel/the-speculation-decision-refuses-hygon-and-two-lines.md @@ -4,15 +4,13 @@ kind: defect opened: 2026-09-29 --- -# The speculation decision refuses AMD 0x15, 0x16, Hygon, and two lines +# The speculation decision refuses Hygon and two lines `toyos_cpuvuln::decide` answers only from its facts, and at `Ubuntu-6.8.0-142.142` these inputs are not decided: -- **AMD family 0x15, 0x16, 0x18 and from 0x1B, and Hygon**, refused whole. - The pinned Linux names 0x15 and 0x16 for Retbleed (`common.c:1338-1339`), - and `hygon.c` derives `LS_CFG_SSBD` from its own probe (228-239); no fixture - holds a reading of any of them. +- **Hygon**, refused whole: `bsp_init_hygon` sets `LS_CFG_SSBD` from its own + `MSR_AMD64_LS_CFG` probe (`hygon.c:228-239`), which `Facts` does not carry. - **`l1tf` on an affected CPU**: the line reads the e820 map against `x86_cache_bits` (`bugs.c:2538-2583`) and `kvm_intel`'s state (`bugs.c:3074-3089`). diff --git a/toyos-cpuvuln/src/lib.rs b/toyos-cpuvuln/src/lib.rs index befd2163af8..01cb8b44c55 100644 --- a/toyos-cpuvuln/src/lib.rs +++ b/toyos-cpuvuln/src/lib.rs @@ -187,9 +187,8 @@ impl Ident { /// An input this crate does not decide. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Refused { - /// Every AMD family from 0x15 but 0x17, 0x19 and 0x1A. - AmdFamily(u32), - /// `hygon.c` derives `LS_CFG_SSBD` (228-239) from an MSR probe. + /// `bsp_init_hygon`'s `MSR_AMD64_LS_CFG` probe (`hygon.c:228-239`), which + /// [`Facts::ls_cfg_readable`] does not carry. Hygon, } @@ -602,12 +601,8 @@ fn disable_kernel_rrsba(f: &Facts, arch: u64, spec_ctrl: &mut u64, rrsba_disable pub fn decide(facts: &Facts) -> Result { let f = facts; let id = Ident::new(f.vendor, f.signature); - match id.vendor { - Vendor::Amd if id.family >= 0x15 && !matches!(id.family, 0x17 | 0x19 | 0x1A) => { - return Err(Refused::AmdFamily(id.family)); - } - Vendor::Hygon => return Err(Refused::Hygon), - _ => {} + if id.vendor == Vendor::Hygon { + return Err(Refused::Hygon); } let amd = id.vendor == Vendor::Amd; assert!( diff --git a/toyos-cpuvuln/src/table.rs b/toyos-cpuvuln/src/table.rs index be5fc990d68..410cfb160b7 100644 --- a/toyos-cpuvuln/src/table.rs +++ b/toyos-cpuvuln/src/table.rs @@ -7,7 +7,8 @@ //! //! The whitelist's `NSC` and `VORTEX` rows are left out: x86-64 Linux builds //! neither vendor (`CONFIG_CPU_SUP_CYRIX_32`, `CONFIG_CPU_SUP_VORTEX_32`), so -//! no CPU it identifies matches them. +//! no CPU it identifies matches them. The Hygon rows are left out: `decide` +//! refuses Hygon. use crate::{Ident, Vendor}; @@ -83,9 +84,6 @@ const fn intel_steppings(model: u32, steppings: u32, flags: Bl) -> Row { const fn amd(family: u32, flags: F) -> Row { row(Some(Vendor::Amd), family, ANY, flags) } -const fn hygon(family: u32, flags: F) -> Row { - row(Some(Vendor::Hygon), family, ANY, flags) -} fn lookup(rows: &[Row], id: &Ident) -> Option { rows.iter() @@ -215,7 +213,6 @@ const WHITELIST: &[Row] = &[ amd(0x12, wl!(NO_MELTDOWN | NO_SSB | NO_L1TF | NO_MDS | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO | NO_BHI)), amd(ANY, wl!(NO_MELTDOWN | NO_L1TF | NO_MDS | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO | NO_EIBRS_PBRSB | NO_BHI)), - hygon(ANY, wl!(NO_MELTDOWN | NO_L1TF | NO_MDS | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO | NO_EIBRS_PBRSB | NO_BHI)), row(Some(Vendor::Centaur), 7, ANY, wl!(NO_SPECTRE_V2 | NO_SWAPGS | NO_MMIO | NO_BHI)), row(Some(Vendor::Zhaoxin), 7, ANY, wl!(NO_SPECTRE_V2 | NO_SWAPGS | NO_MMIO | NO_BHI)), @@ -279,7 +276,6 @@ const BLACKLIST: &[Row] = &[ amd(0x15, bl!(RETBLEED)), amd(0x16, bl!(RETBLEED)), amd(0x17, bl!(RETBLEED | SMT_RSB | SRSO | VMSCAPE)), - hygon(0x18, bl!(RETBLEED | SMT_RSB | SRSO | VMSCAPE)), amd(0x19, bl!(SRSO | TSA | VMSCAPE)), ]; @@ -317,9 +313,9 @@ pub(crate) fn bad_spectre_microcode(id: &Ident, microcode: u32) -> bool { .is_some_and(|&(_, _, bad)| microcode <= bad) } -/// `amd_check_tsa_microcode` (`amd.c:472-515`): a Zen3 or Zen4 CPU, which -/// `bsp_init_amd` names from family 0x19 models 0x00 to 0xAF (`amd.c:619-632`), -/// whose microcode is at or above its row's. +/// `amd_check_tsa_microcode` (`amd.c:472-515`): a CPU whose microcode is at or +/// above its row's. Every row keys a family 0x19 model `bsp_init_amd` names +/// Zen3 or Zen4 (`amd.c:619-632`). pub(crate) fn tsa_microcode(id: &Ident, microcode: u32) -> bool { const ROWS: &[(u32, u32)] = &[ (0xa0011, 0x0a0011d7), @@ -339,7 +335,7 @@ pub(crate) fn tsa_microcode(id: &Ident, microcode: u32) -> bool { (0xa70c0, 0x0a70c008), (0xaa002, 0x0aa00216), ]; - if !(id.vendor == Vendor::Amd && id.family == 0x19 && id.model <= 0xAF) { + if !(id.vendor == Vendor::Amd && id.family == 0x19) { return false; } // `union zen_patch_rev` (`asm/cpu.h:80-90`) above its `rev` byte. diff --git a/toyos-cpuvuln/src/tests.rs b/toyos-cpuvuln/src/tests.rs index ca81a086297..dedd5e16798 100644 --- a/toyos-cpuvuln/src/tests.rs +++ b/toyos-cpuvuln/src/tests.rs @@ -58,7 +58,7 @@ const TCG: Facts = Facts { /// of the nightly's EPYC 7763) as a KVM guest: InstLatx64's bare-metal dump /// `AuthenticAMD/AuthenticAMD0A00F11_K19_Milan_CPUID1.txt` (commit 2dc186e9) /// with CPUID.1:ECX[31] set, `-smp cores=N`, and no microcode revision, which -/// no line reads under a hypervisor. It does not enumerate ARCH_CAPABILITIES. +/// no line reads under a hypervisor. const MILAN_GUEST: Facts = Facts { vendor: Vendor::from_id(b"AuthenticAMD"), signature: 0x00a0_0f11, @@ -443,55 +443,207 @@ fn zen2_selects_by_smt() { ); } -/// A Milan outside a hypervisor: its `PRED_CMD.SBPB` probe decides SRSO's -/// microcode, and `amd_check_tsa_microcode`'s row for 0xA0011 decides TSA's. -#[test] -fn zen3_native_reads_its_probe_and_its_tsa_microcode() { - let milan = |microcode, sbpb| Facts { +/// [`MILAN_GUEST`] outside a hypervisor, its `PRED_CMD.SBPB` probe answering +/// `sbpb`. +const fn native_milan(microcode: u32, sbpb: bool) -> Facts { + Facts { microcode, cpuid_1_ecx: MILAN_GUEST.cpuid_1_ecx & !CPUID_1_ECX_HYPERVISOR, sbpb_write_accepted: Some(sbpb), ..MILAN_GUEST - }; - let native = State { - spectre_v2: SpectreV2::Retpoline, - ibpb: Ibpb::Conditional, - ibrs_fw: true, - ssb: Ssb::Prctl, - vmscape: Vmscape::IbpbExitToUser, - ..NONE - }; - let d = decide(&milan(0x0a00_11d6, false)).expect("decided"); + } +} + +/// What [`native_milan`] selects whatever its microcode and probe. +const NATIVE_MILAN: State = State { + spectre_v2: SpectreV2::Retpoline, + ibpb: Ibpb::Conditional, + ibrs_fw: true, + ssb: Ssb::Prctl, + vmscape: Vmscape::IbpbExitToUser, + ..NONE +}; + +/// A Milan outside a hypervisor: its `PRED_CMD.SBPB` probe decides SRSO's +/// microcode, and `amd_check_tsa_microcode`'s row for 0xA0011 decides TSA's. +#[test] +fn zen3_native_reads_its_probe_and_its_tsa_microcode() { + let d = decide(&native_milan(0x0a00_11d6, false)).expect("decided"); assert_eq!( state(&d), State { srso: Some(Srso::SafeRetUcodeNeeded), tsa: Some(Tsa::UcodeNeeded), - ..native + ..NATIVE_MILAN } ); - let d = decide(&milan(0x0a00_11d7, true)).expect("decided"); + let d = decide(&native_milan(0x0a00_11d7, true)).expect("decided"); assert_eq!( state(&d), - State { srso: Some(Srso::SafeRet), tsa: Some(Tsa::Full), clear_cpu_buf: true, ..native } + State { srso: Some(Srso::SafeRet), tsa: Some(Tsa::Full), clear_cpu_buf: true, ..NATIVE_MILAN } ); assert_eq!(d.line(Vuln::Tsa).expect("modelled").to_string(), "Mitigation: Clear CPU buffers"); } +/// A Genoa, family 0x19 model 0x11 stepping 1, which `bsp_init_amd` names Zen4 +/// (`amd.c:625-627`): `tsa_init` sets `VERW_CLEAR` (`amd.c:522-525`) from row +/// 0xa1011's microcode 0x0a10114c up (`amd.c:491,514`), and without it TSA's +/// microcode is missing (`bugs.c:2928-2929`; `tsa_strings`, 2889 and 2892). #[test] -#[should_panic(expected = "PRED_CMD.SBPB is probed exactly where")] -fn a_native_zen3_without_its_probe_is_a_contradiction() { - let _ = decide(&Facts { cpuid_1_ecx: 0, ..MILAN_GUEST }); +fn zen4_reads_its_row_of_the_tsa_microcode_table() { + let genoa = |microcode| Facts { signature: 0x00a1_0f11, ..native_milan(microcode, true) }; + let d = decide(&genoa(0x0a10_114c)).expect("decided"); + assert_eq!((d.tsa, d.clear_cpu_buf), (Some(Tsa::Full), true)); + assert_eq!(line(&genoa(0x0a10_114c), Vuln::Tsa), "Mitigation: Clear CPU buffers"); + let d = decide(&genoa(0x0a10_114b)).expect("decided"); + assert_eq!((d.tsa, d.clear_cpu_buf), (Some(Tsa::UcodeNeeded), false)); + assert_eq!( + line(&genoa(0x0a10_114b), Vuln::Tsa), + "Vulnerable: Clear CPU buffers attempted, no microcode" + ); } +/// An AMD CPU is TSA-affected unless it reports both `TSA_SQ_NO` and +/// `TSA_L1_NO` (`common.c:1555-1561`), CPUID.0x80000021:ECX bits 1 and 2 +/// (`scattered.c:52-53`). A guest's TSA microcode is missing +/// (`bugs.c:2928-2929`, `tsa_strings` 2889). #[test] -fn amd_outside_0x17_0x19_0x1a_from_0x15_and_hygon_are_refused() { - for (signature, family) in [(0x0060_0f00, 0x15), (0x0070_0f00, 0x16), (0x00c0_0f00, 0x1B)] { +fn tsa_is_ruled_out_only_by_both_its_no_bits() { + let at = |ecx| line(&Facts { cpuid_8000_0021_ecx: ecx, ..MILAN_GUEST }, Vuln::Tsa); + let affected = "Vulnerable: Clear CPU buffers attempted, no microcode"; + assert_eq!(at(CPUID_8000_0021_ECX_TSA_SQ_NO), affected); + assert_eq!(at(CPUID_8000_0021_ECX_TSA_L1_NO), affected); + assert_eq!(at(CPUID_8000_0021_ECX_TSA_SQ_NO | CPUID_8000_0021_ECX_TSA_L1_NO), "Not affected"); +} + +/// A native Milan with SMT: no retbleed row names family 0x19, so STIBP +/// always-on comes from `AMD_STIBP_ALWAYS_ON` alone (`bugs.c:1575-1577`); +/// `update_stibp_strict` sets `SPEC_CTRL.STIBP` for it (`bugs.c:2054-2068`, +/// 2976-2977) and `stibp_state` prints it (`bugs.c:3185-3186`). +#[test] +fn amd_stibp_always_on_is_strict_without_the_untrained_return_thunk() { + assert_ne!(MILAN_GUEST.cpuid_8000_0008_ebx & CPUID_8000_0008_EBX_AMD_STIBP_ALWAYS_ON, 0); + let milan = Facts { smt: true, ..native_milan(0x0a00_11d7, true) }; + let d = decide(&milan).expect("decided"); + assert_eq!( + state(&d), + State { + stibp: Stibp::StrictPreferred, + srso: Some(Srso::SafeRet), + tsa: Some(Tsa::Full), + clear_cpu_buf: true, + spec_ctrl: SPEC_CTRL_STIBP, + ..NATIVE_MILAN + } + ); + assert_eq!( + line(&milan, Vuln::SpectreV2), + "Mitigation: Retpolines; IBPB: conditional; IBRS_FW; STIBP: always-on; RSB filling; \ + PBRSB-eIBRS: Not affected; BHI: Not affected" + ); +} + +/// AMD families 0x15 to 0x17 enumerating neither `AMD_SSBD` nor `VIRT_SSBD` +/// have SSBD exactly where `MSR_AMD64_LS_CFG` reads (`amd.c:576-596`), and SSB's +/// prctl mode needs SSBD (`bugs.c:2177-2178,2200-2202`; `ssb_strings`, 2123 and +/// 2125). Family 0x17 model 1 is Zen1 (`amd.c:602-607`). +#[test] +fn amd_0x15_to_0x17_take_ssbd_from_the_ls_cfg_probe() { + for signature in [0x0060_0f00, 0x0070_0f00, 0x0080_0f11] { + let at = |readable| Facts { + signature, + cpuid_1_ecx: 0, + cpuid_8000_0008_ebx: MILAN_GUEST.cpuid_8000_0008_ebx + & !(CPUID_8000_0008_EBX_AMD_SSBD | CPUID_8000_0008_EBX_VIRT_SSBD), + cpuid_8000_0021_eax: 0, + ls_cfg_readable: Some(readable), + ..MILAN_GUEST + }; + let d = decide(&at(true)).expect("decided"); + assert_eq!(d.ssb, Ssb::Prctl, "{signature:#x}"); assert_eq!( - decide(&Facts { signature, ..TCG }).err(), - Some(Refused::AmdFamily(family)) + line(&at(true), Vuln::SpecStoreBypass), + "Mitigation: Speculative Store Bypass disabled via prctl" ); + let d = decide(&at(false)).expect("decided"); + assert_eq!(d.ssb, Ssb::None, "{signature:#x}"); + assert_eq!(line(&at(false), Vuln::SpecStoreBypass), "Vulnerable"); } +} + +/// A native family 0x19 reporting `SRSO_USER_KERNEL_NO` leaves safe RET for +/// the VM-exit arm (`bugs.c:2715-2716`): `SRSO_BP_SPEC_REDUCE` first +/// (2771-2775), then IBPB on VM exit given `IBPB_BRTYPE` (2777-2781), which +/// VMSCAPE takes up (2861-2863); without either the microcode is missing +/// (2703). `srso_strings` 2634, 2639 and 2640; `vmscape_strings` 2822-2823. +#[test] +fn srso_user_kernel_no_mitigates_only_the_vm_exit() { + let at = |bp_spec_reduce, sbpb| { + let facts = Facts { + cpuid_8000_0021_eax: MILAN_GUEST.cpuid_8000_0021_eax + | CPUID_8000_0021_EAX_SRSO_USER_KERNEL_NO + | bp_spec_reduce, + ..native_milan(0x0a00_11d7, sbpb) + }; + let d = decide(&facts).expect("decided"); + (d.srso, d.vmscape, line(&facts, Vuln::SpecRstackOverflow), line(&facts, Vuln::Vmscape)) + }; + let exit_to_user = "Mitigation: IBPB before exit to userspace"; + assert_eq!( + at(CPUID_8000_0021_EAX_SRSO_BP_SPEC_REDUCE, true), + ( + Some(Srso::BpSpecReduce), + Vmscape::IbpbExitToUser, + "Mitigation: Reduced Speculation".into(), + exit_to_user.into() + ) + ); + assert_eq!( + at(0, true), + ( + Some(Srso::IbpbOnVmexit), + Vmscape::IbpbOnVmexit, + "Mitigation: IBPB on VMEXIT only".into(), + "Mitigation: IBPB on VMEXIT".into() + ) + ); + assert_eq!( + at(0, false), + ( + Some(Srso::UcodeNeeded), + Vmscape::IbpbExitToUser, + "Vulnerable: No microcode".into(), + exit_to_user.into() + ) + ); +} + +/// A KABYLAKE guest, family 6 model 0x9E stepping 0xA, without +/// `ARCH_CAPABILITIES`: its blacklist row gives SRBDS (`common.c:1303`), whose +/// mitigation a guest cannot know (`bugs.c:691-692`, `srbds_strings` 636), and +/// no `MDS_NO` gives MDS (`common.c:1447-1449`), whose host SMT state a guest +/// cannot know (`bugs.c:3117-3119`). +#[test] +fn an_intel_guest_cannot_know_its_srbds_mitigation_or_its_hosts_smt() { + let guest = Facts { + signature: 0x0009_06ea, + cpuid_1_ecx: T14.cpuid_1_ecx | CPUID_1_ECX_HYPERVISOR, + arch_capabilities: 0, + mcu_opt_ctrl: None, + ..T14 + }; + assert_eq!(line(&guest, Vuln::Srbds), "Unknown: Dependent on hypervisor status"); + assert_eq!(line(&guest, Vuln::Mds), "Mitigation: Clear CPU buffers; SMT Host state unknown"); +} + +#[test] +#[should_panic(expected = "PRED_CMD.SBPB is probed exactly where")] +fn a_native_zen3_without_its_probe_is_a_contradiction() { + let _ = decide(&Facts { cpuid_1_ecx: 0, ..MILAN_GUEST }); +} + +#[test] +fn hygon_is_refused() { let hygon = Facts { vendor: Vendor::from_id(b"HygonGenuine"), ..TCG }; assert_eq!(decide(&hygon).err(), Some(Refused::Hygon)); } From e4d40141bf4efd54cb7ce120e2af333e6c9a2068 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 29 Sep 2026 14:19:47 +0200 Subject: [PATCH 5/6] toyos-cpuvuln: decide Hygon and itlb_multihit, drop unreachable states, a fixture for every sweep survivor Round 3's review found decision arms no test could fail. This round closes its five blocker groups with fixtures whose expected state and lines are read from the tag's source, and then sweeps the crate for the rest. - Hygon is decided rather than refused: its LS_CFG probe is the same fact as AMD's (hygon.c:228-239, every family), the untrained return thunk and IBPB around firmware take Hygon as bugs.c does, and its whitelist and blacklist rows are back. `decide` returns a `Decision`; `Refused` is gone. - itlb_multihit is decided: `Facts::feat_ctl` carries IA32_FEAT_CTL where Intel, Centaur and Zhaoxin read it, and the line is "VMX unsupported" or, with VMX kept by init_ia32_feat_ctl and no VM running, "VMX disabled". Only an affected CPU's l1tf line stays unmodelled; its issue now names the two facts it lacks. - States no input reaches are unrepresentable: `spectre_v2`, `bhi`, `mds`, `taa`, `mmio` and `rfds` are `Option`s without a "not affected" variant, `ibpb` is a bool (IBPB is conditional wherever Spectre v2 is), and `its` is a bool. MMIO's dead CLEAR_CPU_BUF block, TAA's always-true TSX_CTRL test, the RRSBA helper's dead guard, TSA microcode's dead vendor guard, the brtype probe's redundant vendor test and the doubled Intel check at the bad-microcode call site are deleted. - New fixtures: Zen1 with and without AMD_STIBP, Milan guest at microcode u32::MAX, a KABYLAKE guest at its bad microcode, Alder Lake with BHI_CTRL, retpoline guests with and without RRSBA_CTRL and RRSBA, Comet Lake across MDS_NO, FB_CLEAR, TSX_CTRL, RTM, MD_CLEAR, FLUSH_L1D, SRBDS_CTRL, RDRAND and RDSEED, RTM_ALWAYS_ABORT, GDS locked, Silvermont's MSBDS_ONLY SMT line, the T14's *_NO bits, RFDS by RFDS_CLEAR and by row, Hygon, Zhaoxin, Centaur, an unknown vendor, native Turin, a Zen2 guest, and IA32_FEAT_CTL's cases. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs --- ...es-not-model-an-affected-cpus-l1tf-line.md | 32 + ...on-decision-refuses-hygon-and-two-lines.md | 24 - toyos-cpuvuln/src/lib.rs | 374 +++-- toyos-cpuvuln/src/table.rs | 18 +- toyos-cpuvuln/src/tests.rs | 1206 +++++++++++++++-- 5 files changed, 1329 insertions(+), 325 deletions(-) create mode 100644 issues/kernel/the-speculation-decision-does-not-model-an-affected-cpus-l1tf-line.md delete mode 100644 issues/kernel/the-speculation-decision-refuses-hygon-and-two-lines.md diff --git a/issues/kernel/the-speculation-decision-does-not-model-an-affected-cpus-l1tf-line.md b/issues/kernel/the-speculation-decision-does-not-model-an-affected-cpus-l1tf-line.md new file mode 100644 index 00000000000..1fa0f6fc125 --- /dev/null +++ b/issues/kernel/the-speculation-decision-does-not-model-an-affected-cpus-l1tf-line.md @@ -0,0 +1,32 @@ +--- +status: open +kind: defect +opened: 2026-09-29 +--- + +# The speculation decision does not model an affected CPU's l1tf line + +`toyos_cpuvuln::Decision::line` answers `Unmodelled::L1tf` for a CPU with +`X86_BUG_L1TF`, because at `Ubuntu-6.8.0-142.142` that line rests on two +things `Facts` does not carry: + +- **Whether RAM lies above `MAX_PA/2`**: `l1tf_select_mitigation` sets + `L1TF_PTEINV` only if no e820 RAM range reaches `l1tf_pfn_limit()` + (`bugs.c:2571-2583`; `asm/processor.h:217`), which is `x86_cache_bits` (CPUID 0x80000008:EAX, + raised to 44 for the models `override_cache_bits` names, `bugs.c:2513-2536`) + against the firmware's memory map. Without `L1TF_PTEINV` the line is + "Vulnerable" (`bugs.c:3330-3333,3376`). +- **`kvm_intel`'s `l1tf_vmx_mitigation`**: the line's `VMX:` part is + `VMENTER_L1D_FLUSH_AUTO` until `kvm_intel` loads (`bugs.c:3074-3089`), a + module Linux's userspace loads, not a CPU fact. + +The first is readable by the kernel (the loader's memory map, CPUID); the +second has no counterpart in a kernel that runs no VM, so parity needs a ruling +on which `VMX:` state ToyOS is held to. + +No machine ToyOS is tested on reaches it: the T14 has `RDCL_NO`, and the TCG +model and the nightly's KVM runners are AMD. + +**Exit**: the line is decided from the memory map and CPUID and the owner's +`VMX:` ruling, and held by a fixture captured under the pinned Linux on such a +CPU. diff --git a/issues/kernel/the-speculation-decision-refuses-hygon-and-two-lines.md b/issues/kernel/the-speculation-decision-refuses-hygon-and-two-lines.md deleted file mode 100644 index 15e5506123b..00000000000 --- a/issues/kernel/the-speculation-decision-refuses-hygon-and-two-lines.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-29 ---- - -# The speculation decision refuses Hygon and two lines - -`toyos_cpuvuln::decide` answers only from its facts, and at -`Ubuntu-6.8.0-142.142` these inputs are not decided: - -- **Hygon**, refused whole: `bsp_init_hygon` sets `LS_CFG_SSBD` from its own - `MSR_AMD64_LS_CFG` probe (`hygon.c:228-239`), which `Facts` does not carry. -- **`l1tf` on an affected CPU**: the line reads the e820 map against - `x86_cache_bits` (`bugs.c:2538-2583`) and `kvm_intel`'s state - (`bugs.c:3074-3089`). -- **`itlb_multihit` on an affected CPU**: the line reads `IA32_FEAT_CTL` and - `CR4.VMXE` (`bugs.c:3091-3102`). - -No machine ToyOS is tested on reaches one: the T14 is Intel, the TCG model is -AMD family 0xF, and the nightly's KVM runners are EPYC 7763, 9V74 and 9V45. - -**Exit**: each is decided from facts the kernel reads and held by a fixture -captured under the pinned Linux on such a CPU. diff --git a/toyos-cpuvuln/src/lib.rs b/toyos-cpuvuln/src/lib.rs index 01cb8b44c55..51f544b41af 100644 --- a/toyos-cpuvuln/src/lib.rs +++ b/toyos-cpuvuln/src/lib.rs @@ -6,14 +6,13 @@ //! fields), under the default command line and the pinned config. That Linux is //! tag `Ubuntu-6.8.0-142.142` (commit 53e5d07aac02) of Ubuntu's noble kernel //! with `/boot/config-6.8.0-142-generic`; every `path:line` here is read there, -//! and `common.c`, `bugs.c`, `intel.c`, `amd.c` and `tsx.c` are in -//! `arch/x86/kernel/cpu/`. The config's `CONFIG_CPU_UNRET_ENTRY`, +//! and `common.c`, `bugs.c`, `intel.c`, `amd.c`, `hygon.c`, `feat_ctl.c` and +//! `tsx.c` are in `arch/x86/kernel/cpu/`. The config's `CONFIG_CPU_UNRET_ENTRY`, //! `CONFIG_CPU_IBPB_ENTRY`, `CONFIG_CPU_SRSO`, `CONFIG_MITIGATION_TSA` and -//! `CONFIG_MITIGATION_VMSCAPE` are `y` -//! (`debian.master/config/annotations:3348,3319,3339,8048,411`). +//! `CONFIG_MITIGATION_VMSCAPE` are `y`, and `CONFIG_KVM_INTEL` is `m` +//! (`debian.master/config/annotations:3348,3319,3339,8048,411,7262`). //! -//! Parity is exact or refused: an input whose answer rests on something the -//! facts do not carry is [`Refused`] whole, and a line that does is +//! Parity is exact: a line that rests on something the facts do not carry is //! [`Unmodelled`], never approximated. //! //! Pure: no I/O, no allocation, no `unsafe`. The caller reads the facts. @@ -85,9 +84,14 @@ pub struct Facts { /// `IA32_MCU_OPT_CTRL` (0x123), present exactly where /// `ARCH_CAPABILITIES.GDS_CTRL` enumerates it. pub mcu_opt_ctrl: Option, + /// `IA32_FEAT_CTL` (0x3A) where `init_ia32_feat_ctl` reads it, for Intel, + /// Centaur and Zhaoxin (`intel.c:715`, `centaur.c:217`, `zhaoxin.c:97`), and + /// the read completes; `None` where it is `#GP` (`feat_ctl.c:119-123`). + pub feat_ctl: Option, /// Whether `rdmsr` of `MSR_AMD64_LS_CFG` (0xC0011020) completes without - /// `#GP`, present exactly where `bsp_init_amd` probes it (`amd.c:576-596`): - /// AMD family 0x15 to 0x17 enumerating neither `AMD_SSBD` nor `VIRT_SSBD`. + /// `#GP`, present exactly where it is probed: AMD family 0x15 to 0x17 + /// (`amd.c:576-596`) and Hygon (`hygon.c:228-239`), enumerating neither + /// `AMD_SSBD` nor `VIRT_SSBD`. pub ls_cfg_readable: Option, /// Whether `wrmsr` of `IA32_PRED_CMD` with `SBPB` (bit 7) completes without /// `#GP`, present exactly where `early_init_amd` probes it @@ -101,6 +105,7 @@ pub struct Facts { // Linux's feature bits (`arch/x86/include/asm/cpufeatures.h`, // `scattered.c:26-57`) at their CPUID positions. +const CPUID_1_ECX_VMX: u32 = 1 << 5; const CPUID_1_ECX_AVX: u32 = 1 << 28; const CPUID_1_ECX_RDRAND: u32 = 1 << 30; const CPUID_1_ECX_HYPERVISOR: u32 = 1 << 31; @@ -134,7 +139,7 @@ const CPUID_8000_0021_EAX_SRSO_BP_SPEC_REDUCE: u32 = 1 << 31; const CPUID_8000_0021_ECX_TSA_SQ_NO: u32 = 1 << 1; const CPUID_8000_0021_ECX_TSA_L1_NO: u32 = 1 << 2; -// `arch/x86/include/asm/msr-index.h:46-54,101-183,215`. +// `arch/x86/include/asm/msr-index.h:46-54,101-183,215,821-823`. const ARCH_CAP_RDCL_NO: u64 = 1 << 0; const ARCH_CAP_IBRS_ALL: u64 = 1 << 1; const ARCH_CAP_RSBA: u64 = 1 << 2; @@ -155,6 +160,8 @@ const ARCH_CAP_RFDS_NO: u64 = 1 << 27; const ARCH_CAP_RFDS_CLEAR: u64 = 1 << 28; const ARCH_CAP_ITS_NO: u64 = 1 << 62; const GDS_MITG_LOCKED: u64 = 1 << 5; +const FEAT_CTL_LOCKED: u64 = 1 << 0; +const FEAT_CTL_VMX_ENABLED_OUTSIDE_SMX: u64 = 1 << 2; pub const SPEC_CTRL_IBRS: u64 = 1 << 0; pub const SPEC_CTRL_STIBP: u64 = 1 << 1; pub const SPEC_CTRL_RRSBA_DIS_S: u64 = 1 << 6; @@ -184,22 +191,12 @@ impl Ident { } } -/// An input this crate does not decide. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Refused { - /// `bsp_init_hygon`'s `MSR_AMD64_LS_CFG` probe (`hygon.c:228-239`), which - /// [`Facts::ls_cfg_readable`] does not carry. - Hygon, -} - /// An affected CPU's line that rests on state the facts do not carry. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Unmodelled { /// The e820 map against `x86_cache_bits` (`bugs.c:2538-2583`) and /// `kvm_intel`'s state (`bugs.c:3074-3089`). L1tf, - /// `IA32_FEAT_CTL` and `CR4.VMXE` (`bugs.c:3091-3102`). - ItlbMultihit, } /// The vulnerabilities files, in the order `drivers/base/cpu.c:615-631` lists @@ -282,37 +279,24 @@ pub enum Tsx { Disable, } -/// `spectre_v2_enabled` (`bugs.c:1860-2045`). +/// `spectre_v2_enabled` where the CPU has Spectre v2 (`bugs.c:1860-2045`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SpectreV2 { - None, Retpoline, Eibrs, Ibrs, } -/// What `bhi_select_mitigation` (`bugs.c:1831-1858`) chose, as -/// `spectre_bhi_state` (`bugs.c:3220-3236`) reads it. +/// What `bhi_select_mitigation` (`bugs.c:1831-1858`) chose where the CPU has +/// BHI, as `spectre_bhi_state` (`bugs.c:3220-3236`) reads it. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Bhi { - NotAffected, /// `SPEC_CTRL.BHI_DIS_S`. BhiDisS, /// `clear_bhb_loop` on syscall entry and VM exit. SwLoop, /// Retpolines with RRSBA disabled. Retpoline, - Vulnerable, -} - -/// Whether and how `switch_mm` issues IBPB (`bugs.c:1527-1550`). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Ibpb { - /// The CPU has no IBPB. - Absent, - Disabled, - /// Between two processes when either asked for it by prctl. - Conditional, } /// `spectre_v2_user_stibp` (`bugs.c:1492-1591`). @@ -339,35 +323,31 @@ pub enum Ssb { Prctl, } -/// `mds_mitigation` (`bugs.c:266-283`). +/// `mds_mitigation` where the CPU has MDS (`bugs.c:266-283`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Mds { - Off, Full, Vmwerv, } -/// `taa_mitigation` (`bugs.c:327-382`). +/// `taa_mitigation` where the CPU has TAA (`bugs.c:327-382`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Taa { - Off, UcodeNeeded, Verw, TsxDisabled, } -/// `mmio_mitigation` (`bugs.c:424-478`). +/// `mmio_mitigation` where the CPU has MMIO Stale Data (`bugs.c:424-478`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Mmio { - Off, UcodeNeeded, Verw, } -/// `rfds_mitigation` (`bugs.c:520-533`). +/// `rfds_mitigation` where the CPU has RFDS (`bugs.c:520-533`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Rfds { - Off, Verw, UcodeNeeded, } @@ -392,13 +372,6 @@ pub enum Gds { Hypervisor, } -/// `its_mitigation` (`bugs.c:1254-1335`). -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Its { - Off, - AlignedThunks, -} - /// `vmscape_mitigation` (`bugs.c:2849-2869`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Vmscape { @@ -459,7 +432,7 @@ struct Bugs { /// The features Linux has when it selects, after `init_speculation_control` /// (`common.c:973-1012`), `early_init_intel`'s microcode check -/// (`intel.c:296-309`) and `bsp_init_amd`'s `LS_CFG_SSBD` (`amd.c:576-596`). +/// (`intel.c:296-309`) and the `LS_CFG_SSBD` probe. struct Caps { hypervisor: bool, ibrs: bool, @@ -473,21 +446,25 @@ struct Caps { #[derive(Clone, Copy, Debug)] pub struct Decision { pub tsx: Tsx, - pub spectre_v2: SpectreV2, - pub bhi: Bhi, - pub ibpb: Ibpb, + pub spectre_v2: Option, + pub bhi: Option, + /// `USE_IBPB`, with `switch_mm` issuing it conditionally + /// (`bugs.c:1527-1550`): the CPU has IBPB and Spectre v2. + pub ibpb: bool, /// `USE_IBRS_FW`: IBRS around firmware calls. pub ibrs_fw: bool, pub stibp: Stibp, pub retbleed: Retbleed, pub ssb: Ssb, - pub mds: Mds, - pub taa: Taa, - pub mmio: Mmio, - pub rfds: Rfds, + pub mds: Option, + pub taa: Option, + pub mmio: Option, + pub rfds: Option, pub srbds: Option, pub gds: Option, - pub its: Its, + /// ITS's aligned branch and return thunks (`bugs.c:1254-1335`), wherever + /// the CPU has ITS. + pub its: bool, pub vmscape: Vmscape, pub srso: Option, pub tsa: Option, @@ -505,6 +482,8 @@ pub struct Decision { hypervisor: bool, autoibrs: bool, smt: bool, + /// `X86_FEATURE_MSR_IA32_FEAT_CTL` and `X86_FEATURE_VMX`. + vmx: bool, } /// `cpu_set_bug_bits` (`common.c:1414-1578`). `SWAPGS`, `SMT_RSB` and @@ -526,7 +505,9 @@ fn bug_bits(f: &Facts, id: &Ident, caps: &Caps, arch: u64) -> Bugs { b.eibrs_pbrsb = b.ibrs_enhanced && !whitelisted(id, NO_EIBRS_PBRSB) && arch & ARCH_CAP_PBRSB_NO == 0; b.mds = !whitelisted(id, NO_MDS) && arch & ARCH_CAP_MDS_NO == 0; - b.msbds_only = b.mds && whitelisted(id, MSBDS_ONLY); + // `MSBDS_ONLY` is set within MDS (`common.c:1447-1451`), and only MDS's line + // reads it. + b.msbds_only = whitelisted(id, MSBDS_ONLY); b.taa = arch & ARCH_CAP_TAA_NO == 0 && (f.cpuid_7_0_ebx & CPUID_7_0_EBX_RTM != 0 || arch & ARCH_CAP_TSX_CTRL_MSR != 0); b.srbds = (f.cpuid_1_ecx & CPUID_1_ECX_RDRAND != 0 || f.cpuid_7_0_ebx & CPUID_7_0_EBX_RDSEED != 0) @@ -563,7 +544,8 @@ fn bug_bits(f: &Facts, id: &Ident, caps: &Caps, arch: u64) -> Bugs { // `tsa_init`'s forced `TSA_*_NO` (`amd.c:517-530`) reaches no family the // TSA row names, and the Zen-guest clause reads `X86_FEATURE_ZEN`, which // `init_amd` sets (`amd.c:1039`) from `identify_cpu` (`common.c:1997-1998`), - // after `early_identify_cpu` has run this (`common.c:1732`). + // after `early_identify_cpu` has run this (`common.c:1732`). Only AMD rows + // name TSA, so the AMD vendor test (`common.c:1555`) is the table's. b.tsa = f.cpuid_8000_0021_ecx & (CPUID_8000_0021_ECX_TSA_SQ_NO | CPUID_8000_0021_ECX_TSA_L1_NO) != CPUID_8000_0021_ECX_TSA_SQ_NO | CPUID_8000_0021_ECX_TSA_L1_NO && blacklisted(id, TSA); @@ -575,20 +557,17 @@ fn bug_bits(f: &Facts, id: &Ident, caps: &Caps, arch: u64) -> Bugs { b } -/// `spec_ctrl_disable_kernel_rrsba` (`bugs.c:1723-1739`). -fn disable_kernel_rrsba(f: &Facts, arch: u64, spec_ctrl: &mut u64, rrsba_disabled: &mut bool) { - if *rrsba_disabled { - return; - } +/// `spec_ctrl_disable_kernel_rrsba` (`bugs.c:1723-1739`), its first call: +/// whether the kernel's RET predictions are RSB-only after it. +fn disable_kernel_rrsba(f: &Facts, arch: u64, spec_ctrl: &mut u64) -> bool { if arch & ARCH_CAP_RRSBA == 0 { - *rrsba_disabled = true; - return; + return true; } if f.cpuid_7_2_edx & CPUID_7_2_EDX_RRSBA_CTRL == 0 { - return; + return false; } *spec_ctrl |= SPEC_CTRL_RRSBA_DIS_S; - *rrsba_disabled = true; + true } /// Linux's selection over `facts`: `cpu_select_mitigations` (`bugs.c:149-199`) @@ -596,15 +575,15 @@ fn disable_kernel_rrsba(f: &Facts, arch: u64, spec_ctrl: &mut u64, rrsba_disable /// /// Panics where the facts contradict their own documentation: an /// `arch_capabilities` read without CPUID.(7,0):EDX[29], or an `mcu_opt_ctrl`, -/// `ls_cfg_readable` or `sbpb_write_accepted` present where Linux does not read -/// it, or absent where it does. -pub fn decide(facts: &Facts) -> Result { +/// `feat_ctl`, `ls_cfg_readable` or `sbpb_write_accepted` present where Linux +/// does not read it, or one of the first, third or fourth absent where it +/// does. +pub fn decide(facts: &Facts) -> Decision { let f = facts; let id = Ident::new(f.vendor, f.signature); - if id.vendor == Vendor::Hygon { - return Err(Refused::Hygon); - } let amd = id.vendor == Vendor::Amd; + // `X86_VENDOR_AMD || X86_VENDOR_HYGON` (`bugs.c:1103-1104,2030-2031,3268-3269`). + let amd_or_hygon = amd || id.vendor == Vendor::Hygon; assert!( f.cpuid_7_0_edx & CPUID_7_0_EDX_ARCH_CAPABILITIES != 0 || f.arch_capabilities == 0, "ARCH_CAPABILITIES {:#x} without CPUID.(7,0):EDX[29]", @@ -616,6 +595,11 @@ pub fn decide(facts: &Facts) -> Result { arch & ARCH_CAP_GDS_CTRL != 0, "IA32_MCU_OPT_CTRL is read exactly where ARCH_CAPABILITIES.GDS_CTRL" ); + assert!( + f.feat_ctl.is_none() + || matches!(id.vendor, Vendor::Intel | Vendor::Centaur | Vendor::Zhaoxin), + "IA32_FEAT_CTL is read only where intel.c:715, centaur.c:217 and zhaoxin.c:97 read it" + ); let edx7 = f.cpuid_7_0_edx; let ebx8 = f.cpuid_8000_0008_ebx; @@ -624,8 +608,8 @@ pub fn decide(facts: &Facts) -> Result { let amd_ssbd = ebx8 & (CPUID_8000_0008_EBX_VIRT_SSBD | CPUID_8000_0008_EBX_AMD_SSBD) != 0; assert_eq!( f.ls_cfg_readable.is_some(), - amd && (0x15..=0x17).contains(&id.family) && !amd_ssbd, - "MSR_AMD64_LS_CFG is probed exactly where amd.c:576-578 probes it" + !amd_ssbd && ((amd && (0x15..=0x17).contains(&id.family)) || id.vendor == Vendor::Hygon), + "MSR_AMD64_LS_CFG is probed exactly where amd.c:576-578 and hygon.c:228-229 probe it" ); let brtype_enumerated = eax21 & CPUID_8000_0021_EAX_IBPB_BRTYPE != 0; assert_eq!( @@ -633,28 +617,27 @@ pub fn decide(facts: &Facts) -> Result { amd && id.family >= 0x19 && !hypervisor && !brtype_enumerated, "PRED_CMD.SBPB is probed exactly where amd.c:799-802 probes it" ); - let spec_ctrl_bit = edx7 & CPUID_7_0_EDX_SPEC_CTRL != 0; - let intel_stibp = edx7 & CPUID_7_0_EDX_INTEL_STIBP != 0; let mut caps = Caps { hypervisor, - ibrs: spec_ctrl_bit || ebx8 & CPUID_8000_0008_EBX_AMD_IBRS != 0, - ibpb: spec_ctrl_bit || ebx8 & CPUID_8000_0008_EBX_AMD_IBPB != 0, - stibp: intel_stibp || ebx8 & CPUID_8000_0008_EBX_AMD_STIBP != 0, + ibrs: edx7 & CPUID_7_0_EDX_SPEC_CTRL != 0 || ebx8 & CPUID_8000_0008_EBX_AMD_IBRS != 0, + ibpb: edx7 & CPUID_7_0_EDX_SPEC_CTRL != 0 || ebx8 & CPUID_8000_0008_EBX_AMD_IBPB != 0, + stibp: edx7 & CPUID_7_0_EDX_INTEL_STIBP != 0 || ebx8 & CPUID_8000_0008_EBX_AMD_STIBP != 0, ssbd: edx7 & CPUID_7_0_EDX_SPEC_CTRL_SSBD != 0 || amd_ssbd || f.ls_cfg_readable == Some(true), autoibrs: eax21 & CPUID_8000_0021_EAX_AUTOIBRS != 0, }; - // `early_init_amd` (`amd.c:799-806`). - let native_brtype = amd - && !hypervisor + // `early_init_amd` (`amd.c:799-806`). Only SRSO reads it, and of SRSO's rows + // (`common.c:1340-1342`) only AMD's names family 0x17. + let native_brtype = !hypervisor && ((id.family == 0x17 && ebx8 & CPUID_8000_0008_EBX_AMD_IBPB != 0) || f.sbpb_write_accepted == Some(true)); let ibpb_brtype = brtype_enumerated || native_brtype; let sbpb = eax21 & CPUID_8000_0021_EAX_SBPB != 0 || f.sbpb_write_accepted == Some(true); - if id.vendor == Vendor::Intel - && !hypervisor - && (spec_ctrl_bit || intel_stibp || caps.ibrs || caps.ibpb || caps.stibp) + // `SPEC_CTRL` and `INTEL_STIBP` are in the test at `intel.c:296-299` too, + // but each already set `IBRS` or `STIBP` (`common.c:981-988`). + if !hypervisor + && (caps.ibrs || caps.ibpb || caps.stibp) && table::bad_spectre_microcode(&id, f.microcode) { caps.ibrs = false; @@ -673,25 +656,32 @@ pub fn decide(facts: &Facts) -> Result { }; let rtm = f.cpuid_7_0_ebx & CPUID_7_0_EBX_RTM != 0 && tsx == Tsx::NotSupported; + // `X86_FEATURE_MSR_IA32_FEAT_CTL` and `X86_FEATURE_VMX` after + // `init_ia32_feat_ctl` (`feat_ctl.c:112-184`), which without tboot locks an + // unlocked MSR with VMX enabled for `CONFIG_KVM_INTEL`. + let vmx = f.cpuid_1_ecx & CPUID_1_ECX_VMX != 0 + && f.feat_ctl.is_some_and(|m| { + m & FEAT_CTL_LOCKED == 0 || m & FEAT_CTL_VMX_ENABLED_OUTSIDE_SMX != 0 + }); + let mut spec_ctrl = 0; - let mut rrsba_disabled = false; let mut efer_autoibrs = false; // `spectre_v2_select_mitigation` (`bugs.c:1860-2045`). Where the CPU is // unaffected it returns before any of it, leaving `spectre_v2_cmd` at // `SPECTRE_V2_CMD_NONE`. - let mut spectre_v2 = SpectreV2::None; - let mut bhi = if bugs.bhi { Bhi::Vulnerable } else { Bhi::NotAffected }; + let mut spectre_v2 = None; + let mut bhi = None; let mut ibrs_fw = false; if bugs.spectre_v2 { - spectre_v2 = if bugs.ibrs_enhanced { + let mode = if bugs.ibrs_enhanced { SpectreV2::Eibrs } else if bugs.retbleed && caps.ibrs && id.vendor == Vendor::Intel { SpectreV2::Ibrs } else { SpectreV2::Retpoline }; - let ibrs_mode = matches!(spectre_v2, SpectreV2::Eibrs | SpectreV2::Ibrs); + let ibrs_mode = matches!(mode, SpectreV2::Eibrs | SpectreV2::Ibrs); if ibrs_mode { if caps.autoibrs { efer_autoibrs = true; @@ -699,23 +689,22 @@ pub fn decide(facts: &Facts) -> Result { spec_ctrl |= SPEC_CTRL_IBRS; } } - if spectre_v2 == SpectreV2::Retpoline { - disable_kernel_rrsba(f, arch, &mut spec_ctrl, &mut rrsba_disabled); - } + let rrsba_disabled = + mode == SpectreV2::Retpoline && disable_kernel_rrsba(f, arch, &mut spec_ctrl); // `bhi_select_mitigation` (`bugs.c:1831-1858`); its own call of // `spec_ctrl_disable_kernel_rrsba` repeats the one just above. if bugs.bhi { - bhi = if spectre_v2 == SpectreV2::Retpoline && rrsba_disabled { + bhi = Some(if rrsba_disabled { Bhi::Retpoline } else if f.cpuid_7_2_edx & CPUID_7_2_EDX_BHI_CTRL != 0 { spec_ctrl |= SPEC_CTRL_BHI_DIS_S; Bhi::BhiDisS } else { Bhi::SwLoop - }; + }); } - ibrs_fw = - !(bugs.retbleed && caps.ibpb && id.vendor == Vendor::Amd) && caps.ibrs && !ibrs_mode; + ibrs_fw = !(bugs.retbleed && caps.ibpb && amd_or_hygon) && caps.ibrs && !ibrs_mode; + spectre_v2 = Some(mode); } // `retbleed_select_mitigation` (`bugs.c:1054-1194`). @@ -723,30 +712,24 @@ pub fn decide(facts: &Facts) -> Result { Retbleed::None } else if id.vendor == Vendor::Intel { match spectre_v2 { - SpectreV2::Ibrs => Retbleed::Ibrs, - SpectreV2::Eibrs => Retbleed::Eibrs, - SpectreV2::None | SpectreV2::Retpoline => Retbleed::None, + Some(SpectreV2::Ibrs) => Retbleed::Ibrs, + Some(SpectreV2::Eibrs) => Retbleed::Eibrs, + Some(SpectreV2::Retpoline) | None => Retbleed::None, } - } else if id.vendor == Vendor::Amd { + } else if amd_or_hygon { Retbleed::Unret } else { Retbleed::None }; // `spectre_v2_user_select_mitigation` (`bugs.c:1492-1591`) and - // `cpu_bugs_smt_update` (`bugs.c:2964-3058`). - let user_cmd_none = !bugs.spectre_v2; - let ibpb = if !caps.ibpb { - Ibpb::Absent - } else if user_cmd_none { - Ibpb::Disabled - } else { - Ibpb::Conditional - }; - let stibp = if user_cmd_none + // `cpu_bugs_smt_update` (`bugs.c:2964-3058`). Without Spectre v2 the user + // command is `NONE`, which skips both IBPB and STIBP (`bugs.c:1507-1508`). + let ibpb = caps.ibpb && bugs.spectre_v2; + let stibp = if !bugs.spectre_v2 || !caps.stibp || !f.smt - || (matches!(spectre_v2, SpectreV2::Eibrs) && !caps.autoibrs) + || (spectre_v2 == Some(SpectreV2::Eibrs) && !caps.autoibrs) { Stibp::None } else if ebx8 & CPUID_8000_0008_EBX_AMD_STIBP_ALWAYS_ON != 0 || retbleed == Retbleed::Unret { @@ -763,45 +746,40 @@ pub fn decide(facts: &Facts) -> Result { // bug it selects for again is already selected. let md_clear = edx7 & CPUID_7_0_EDX_MD_CLEAR != 0; let mut clear_cpu_buf = false; - let mds = if !bugs.mds { - Mds::Off - } else { + let mds = bugs.mds.then(|| { clear_cpu_buf = true; if md_clear { Mds::Full } else { Mds::Vmwerv } - }; - let taa = if !bugs.taa { - Taa::Off - } else if !rtm { - Taa::TsxDisabled - } else { - clear_cpu_buf = true; - if md_clear && !(arch & ARCH_CAP_MDS_NO != 0 && arch & ARCH_CAP_TSX_CTRL_MSR == 0) { - Taa::Verw + }); + // With RTM left on, `tsx_init` found no `TSX_CTRL` (`tsx.c:188-192`), so + // the `MDS_NO && !TSX_CTRL_MSR` test (`bugs.c:367-369`) is `MDS_NO` here. + let taa = bugs.taa.then(|| { + if !rtm { + Taa::TsxDisabled } else { - Taa::UcodeNeeded - } - }; - let mmio = if !bugs.mmio_stale_data || bugs.mmio_unknown { - Mmio::Off - } else { - if bugs.mds || (bugs.taa && rtm) { clear_cpu_buf = true; + if md_clear && arch & ARCH_CAP_MDS_NO == 0 { Taa::Verw } else { Taa::UcodeNeeded } } - let flush_l1d = edx7 & CPUID_7_0_EDX_FLUSH_L1D != 0; - if arch & ARCH_CAP_FB_CLEAR != 0 || (md_clear && flush_l1d && arch & ARCH_CAP_MDS_NO == 0) { + }); + // `MMIO_UNKNOWN` is set only without `MMIO_STALE_DATA` (`common.c:1495-1500`), + // and the `CLEAR_CPU_BUF` MDS or TAA would set here (`bugs.c:440-442`) they + // have set already. + let mmio = bugs.mmio_stale_data.then_some( + if arch & ARCH_CAP_FB_CLEAR != 0 + || (md_clear && edx7 & CPUID_7_0_EDX_FLUSH_L1D != 0 && arch & ARCH_CAP_MDS_NO == 0) + { Mmio::Verw } else { Mmio::UcodeNeeded + }, + ); + let rfds = bugs.rfds.then(|| { + if arch & ARCH_CAP_RFDS_CLEAR != 0 { + clear_cpu_buf = true; + Rfds::Verw + } else { + Rfds::UcodeNeeded } - }; - let rfds = if !bugs.rfds { - Rfds::Off - } else if arch & ARCH_CAP_RFDS_CLEAR != 0 { - clear_cpu_buf = true; - Rfds::Verw - } else { - Rfds::UcodeNeeded - }; + }); let srbds = bugs.srbds.then_some( if arch & ARCH_CAP_MDS_NO != 0 && !rtm && !bugs.mmio_stale_data { @@ -848,8 +826,6 @@ pub fn decide(facts: &Facts) -> Result { ); let sbpb = !bugs.srso && sbpb; - let its = if bugs.its && spectre_v2 != SpectreV2::None { Its::AlignedThunks } else { Its::Off }; - // `tsa_select_mitigation` (`bugs.c:2918-2962`) and `tsa_init`'s // `VERW_CLEAR` (`amd.c:517-530`). let tsa = bugs.tsa.then(|| { @@ -872,7 +848,7 @@ pub fn decide(facts: &Facts) -> Result { Vmscape::IbpbExitToUser }; - Ok(Decision { + Decision { tsx, spectre_v2, bhi, @@ -887,7 +863,7 @@ pub fn decide(facts: &Facts) -> Result { rfds, srbds, gds, - its, + its: bugs.its, vmscape, srso, tsa, @@ -899,7 +875,8 @@ pub fn decide(facts: &Facts) -> Result { hypervisor, autoibrs: caps.autoibrs, smt: f.smt, - }) + vmx, + } } impl Decision { @@ -907,7 +884,6 @@ impl Decision { pub fn line(&self, v: Vuln) -> Result, Unmodelled> { match v { Vuln::L1tf if self.bugs.l1tf => Err(Unmodelled::L1tf), - Vuln::ItlbMultihit if self.bugs.itlb_multihit => Err(Unmodelled::ItlbMultihit), _ => Ok(Line { d: self, v }), } } @@ -928,23 +904,21 @@ impl Line<'_> { /// under `CONFIG_BPF_UNPRIV_DEFAULT_OFF=y` and no module is loaded. fn spectre_v2(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let d = self.d; - f.write_str(match d.spectre_v2 { - SpectreV2::None => "Vulnerable", + let mode = d.spectre_v2.expect("set with the bug"); + f.write_str(match mode { SpectreV2::Retpoline => "Mitigation: Retpolines", SpectreV2::Eibrs => "Mitigation: Enhanced / Automatic IBRS", SpectreV2::Ibrs => "Mitigation: IBRS", })?; - f.write_str(match d.ibpb { - Ibpb::Absent => "", - Ibpb::Disabled => "; IBPB: disabled", - Ibpb::Conditional => "; IBPB: conditional", - })?; + if d.ibpb { + f.write_str("; IBPB: conditional")?; + } if d.ibrs_fw { f.write_str("; IBRS_FW")?; } // `stibp_state` (`bugs.c:3174-3193`); `Prctl` is selected only with SMT, // which is what enables `switch_to_cond_stibp`. - if !(d.spectre_v2 == SpectreV2::Eibrs && !d.autoibrs) { + if !(mode == SpectreV2::Eibrs && !d.autoibrs) { f.write_str(match d.stibp { Stibp::None => "; STIBP: disabled", Stibp::StrictPreferred => "; STIBP: always-on", @@ -952,22 +926,21 @@ impl Line<'_> { })?; } // `RSB_CTXSW` (`bugs.c:1741-1789`). - if matches!(d.spectre_v2, SpectreV2::Retpoline | SpectreV2::Ibrs) { + if matches!(mode, SpectreV2::Retpoline | SpectreV2::Ibrs) { f.write_str("; RSB filling")?; } - // `pbrsb_eibrs_state` (`bugs.c:3207-3218`): every mode but `None` - // sets `RSB_VMEXIT` or `RSB_VMEXIT_LITE`. - f.write_str(match (d.bugs.eibrs_pbrsb, d.spectre_v2) { - (false, _) => "; PBRSB-eIBRS: Not affected", - (true, SpectreV2::None) => "; PBRSB-eIBRS: Vulnerable", - (true, _) => "; PBRSB-eIBRS: SW sequence", + // `pbrsb_eibrs_state` (`bugs.c:3207-3218`): every mode sets + // `RSB_VMEXIT` or `RSB_VMEXIT_LITE`. + f.write_str(if d.bugs.eibrs_pbrsb { + "; PBRSB-eIBRS: SW sequence" + } else { + "; PBRSB-eIBRS: Not affected" })?; f.write_str(match d.bhi { - Bhi::NotAffected => "; BHI: Not affected", - Bhi::BhiDisS => "; BHI: BHI_DIS_S", - Bhi::SwLoop => "; BHI: SW loop, KVM: SW loop", - Bhi::Retpoline => "; BHI: Retpoline", - Bhi::Vulnerable => "; BHI: Vulnerable", + None => "; BHI: Not affected", + Some(Bhi::BhiDisS) => "; BHI: BHI_DIS_S", + Some(Bhi::SwLoop) => "; BHI: SW loop, KVM: SW loop", + Some(Bhi::Retpoline) => "; BHI: Retpoline", }) } } @@ -1012,37 +985,40 @@ impl fmt::Display for Line<'_> { Ssb::Prctl => "Mitigation: Speculative Store Bypass disabled via prctl", }), Vuln::Mds => { - f.write_str(match d.mds { - Mds::Off => "Vulnerable", + f.write_str(match d.mds.expect("set with the bug") { Mds::Full => "Mitigation: Clear CPU buffers", Mds::Vmwerv => "Vulnerable: Clear CPU buffers attempted, no microcode", })?; if d.hypervisor { f.write_str("; SMT Host state unknown") } else if b.msbds_only { - let smt = match (d.mds, d.smt) { - (Mds::Off, _) => "vulnerable", - (_, true) => "mitigated", - (_, false) => "disabled", - }; - write!(f, "; SMT {smt}") + f.write_str(if d.smt { "; SMT mitigated" } else { "; SMT disabled" }) } else { write!(f, "; SMT {}", self.smt()) } } Vuln::TsxAsyncAbort => { - f.write_str(match d.taa { - Taa::Off => "Vulnerable", + let taa = d.taa.expect("set with the bug"); + f.write_str(match taa { Taa::UcodeNeeded => "Vulnerable: Clear CPU buffers attempted, no microcode", Taa::Verw => "Mitigation: Clear CPU buffers", - Taa::TsxDisabled => "Mitigation: TSX disabled", + Taa::TsxDisabled => return f.write_str("Mitigation: TSX disabled"), })?; - match d.taa { - Taa::Off | Taa::TsxDisabled => Ok(()), - _ if d.hypervisor => f.write_str("; SMT Host state unknown"), - _ => write!(f, "; SMT {}", self.smt()), + if d.hypervisor { + f.write_str("; SMT Host state unknown") + } else { + write!(f, "; SMT {}", self.smt()) } } + // `itlb_multihit_show_state` under `CONFIG_KVM_INTEL` + // (`bugs.c:3091-3102`). No VM runs: KVM sets `CR4.VMXE` only while + // one exists (`arch/x86/kvm/vmx/vmx.c:2802`, from `KVM_CREATE_VM`, + // `virt/kvm/kvm_main.c:1272,5658-5660`). + Vuln::ItlbMultihit => f.write_str(if d.vmx { + "KVM: Mitigation: VMX disabled" + } else { + "KVM: Mitigation: VMX unsupported" + }), Vuln::Srbds => f.write_str(match d.srbds.expect("set with the bug") { Srbds::UcodeNeeded => "Vulnerable: No microcode", Srbds::Full => "Mitigation: Microcode", @@ -1050,11 +1026,10 @@ impl fmt::Display for Line<'_> { Srbds::Hypervisor => "Unknown: Dependent on hypervisor status", }), Vuln::MmioStaleData => { - if b.mmio_unknown { + let Some(mmio) = d.mmio else { return f.write_str("Unknown: No mitigations"); - } - f.write_str(match d.mmio { - Mmio::Off => return f.write_str("Vulnerable"), + }; + f.write_str(match mmio { Mmio::UcodeNeeded => "Vulnerable: Clear CPU buffers attempted, no microcode", Mmio::Verw => "Mitigation: Clear CPU buffers", })?; @@ -1068,7 +1043,8 @@ impl fmt::Display for Line<'_> { Retbleed::None => f.write_str("Vulnerable"), Retbleed::Ibrs => f.write_str("Mitigation: IBRS"), Retbleed::Eibrs => f.write_str("Mitigation: Enhanced IBRS"), - // Selected on AMD alone, so never "on non-AMD based uarch". + // Selected on AMD and Hygon alone, so never "on non-AMD based + // uarch". Retbleed::Unret => { let smt = if !d.smt { "disabled" @@ -1086,15 +1062,13 @@ impl fmt::Display for Line<'_> { Gds::FullLocked => "Mitigation: Microcode (locked)", Gds::Hypervisor => "Unknown: Dependent on hypervisor status", }), - Vuln::RegFileDataSampling => f.write_str(match d.rfds { - Rfds::Off => "Vulnerable", + Vuln::RegFileDataSampling => f.write_str(match d.rfds.expect("set with the bug") { Rfds::Verw => "Mitigation: Clear Register File", Rfds::UcodeNeeded => "Vulnerable: No microcode", }), - Vuln::IndirectTargetSelection => f.write_str(match d.its { - Its::Off => "Vulnerable", - Its::AlignedThunks => "Mitigation: Aligned branch/return thunks", - }), + Vuln::IndirectTargetSelection => { + f.write_str("Mitigation: Aligned branch/return thunks") + } Vuln::Vmscape => f.write_str(match d.vmscape { Vmscape::None => "Vulnerable", Vmscape::IbpbExitToUser => "Mitigation: IBPB before exit to userspace", @@ -1112,9 +1086,7 @@ impl fmt::Display for Line<'_> { Tsa::UcodeNeeded => "Vulnerable: Clear CPU buffers attempted, no microcode", Tsa::Full => "Mitigation: Clear CPU buffers", }), - Vuln::L1tf | Vuln::ItlbMultihit => { - unreachable!("{} is unmodelled wherever affected", self.v.name()) - } + Vuln::L1tf => unreachable!("l1tf is unmodelled wherever affected"), } } } diff --git a/toyos-cpuvuln/src/table.rs b/toyos-cpuvuln/src/table.rs index 410cfb160b7..ed284e7b698 100644 --- a/toyos-cpuvuln/src/table.rs +++ b/toyos-cpuvuln/src/table.rs @@ -7,8 +7,7 @@ //! //! The whitelist's `NSC` and `VORTEX` rows are left out: x86-64 Linux builds //! neither vendor (`CONFIG_CPU_SUP_CYRIX_32`, `CONFIG_CPU_SUP_VORTEX_32`), so -//! no CPU it identifies matches them. The Hygon rows are left out: `decide` -//! refuses Hygon. +//! no CPU it identifies matches them. use crate::{Ident, Vendor}; @@ -84,6 +83,9 @@ const fn intel_steppings(model: u32, steppings: u32, flags: Bl) -> Row { const fn amd(family: u32, flags: F) -> Row { row(Some(Vendor::Amd), family, ANY, flags) } +const fn hygon(family: u32, flags: F) -> Row { + row(Some(Vendor::Hygon), family, ANY, flags) +} fn lookup(rows: &[Row], id: &Ident) -> Option { rows.iter() @@ -213,6 +215,7 @@ const WHITELIST: &[Row] = &[ amd(0x12, wl!(NO_MELTDOWN | NO_SSB | NO_L1TF | NO_MDS | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO | NO_BHI)), amd(ANY, wl!(NO_MELTDOWN | NO_L1TF | NO_MDS | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO | NO_EIBRS_PBRSB | NO_BHI)), + hygon(ANY, wl!(NO_MELTDOWN | NO_L1TF | NO_MDS | NO_SWAPGS | NO_ITLB_MULTIHIT | NO_MMIO | NO_EIBRS_PBRSB | NO_BHI)), row(Some(Vendor::Centaur), 7, ANY, wl!(NO_SPECTRE_V2 | NO_SWAPGS | NO_MMIO | NO_BHI)), row(Some(Vendor::Zhaoxin), 7, ANY, wl!(NO_SPECTRE_V2 | NO_SWAPGS | NO_MMIO | NO_BHI)), @@ -276,12 +279,13 @@ const BLACKLIST: &[Row] = &[ amd(0x15, bl!(RETBLEED)), amd(0x16, bl!(RETBLEED)), amd(0x17, bl!(RETBLEED | SMT_RSB | SRSO | VMSCAPE)), + hygon(0x18, bl!(RETBLEED | SMT_RSB | SRSO | VMSCAPE)), amd(0x19, bl!(SRSO | TSA | VMSCAPE)), ]; /// `bad_spectre_microcode` (`intel.c:165-182`) over `spectre_bad_microcodes` -/// (`intel.c:141-163`): an Intel CPU outside a hypervisor whose microcode is -/// at or below its row's broke the speculation controls. +/// (`intel.c:141-163`), whose caller is Intel's (`intel.c:296-299`): a CPU +/// whose microcode is at or below its row's broke the speculation controls. pub(crate) fn bad_spectre_microcode(id: &Ident, microcode: u32) -> bool { const ROWS: &[(u32, u32, u32)] = &[ (KABYLAKE, 0x0B, 0x80), @@ -315,7 +319,8 @@ pub(crate) fn bad_spectre_microcode(id: &Ident, microcode: u32) -> bool { /// `amd_check_tsa_microcode` (`amd.c:472-515`): a CPU whose microcode is at or /// above its row's. Every row keys a family 0x19 model `bsp_init_amd` names -/// Zen3 or Zen4 (`amd.c:619-632`). +/// Zen3 or Zen4 (`amd.c:619-632`), the only family TSA's row names +/// (`common.c:1342`). pub(crate) fn tsa_microcode(id: &Ident, microcode: u32) -> bool { const ROWS: &[(u32, u32)] = &[ (0xa0011, 0x0a0011d7), @@ -335,9 +340,6 @@ pub(crate) fn tsa_microcode(id: &Ident, microcode: u32) -> bool { (0xa70c0, 0x0a70c008), (0xaa002, 0x0aa00216), ]; - if !(id.vendor == Vendor::Amd && id.family == 0x19) { - return false; - } // `union zen_patch_rev` (`asm/cpu.h:80-90`) above its `rev` byte. let key = (id.family - 0xf) << 16 | (id.model >> 4) << 12 | (id.model & 0xf) << 4 | id.stepping; ROWS.iter().find(|&&(k, _)| k == key).is_some_and(|&(_, min)| microcode >= min) diff --git a/toyos-cpuvuln/src/tests.rs b/toyos-cpuvuln/src/tests.rs index dedd5e16798..d7bf6bb13b3 100644 --- a/toyos-cpuvuln/src/tests.rs +++ b/toyos-cpuvuln/src/tests.rs @@ -1,9 +1,10 @@ //! Facts in, Linux's lines and selection out, the lines judged against -//! `grep .` over the vulnerabilities directory. +//! `grep .` over the vulnerabilities directory. Every expected value below the +//! two captures is read from the tag's source at the lines each test cites. extern crate std; -use std::string::ToString; +use std::string::{String, ToString}; use std::vec::Vec; use super::*; @@ -27,6 +28,10 @@ const T14: Facts = Facts { cpuid_8000_0021_ecx: 0, arch_capabilities: 0x0a00_5c6b, mcu_opt_ctrl: Some(0), + // S0 did not read 0x3A. `cpuinfo.txt` lists `vmx` and its `vmx flags`, so + // Linux kept VMX (`feat_ctl.c:171-184`): these are the two bits that decide + // that, and no other bit is read. + feat_ctl: Some(FEAT_CTL_LOCKED | FEAT_CTL_VMX_ENABLED_OUTSIDE_SMX), ls_cfg_readable: None, sbpb_write_accepted: None, // `siblings 8`, `cpu cores 4`. @@ -49,6 +54,7 @@ const TCG: Facts = Facts { cpuid_8000_0021_ecx: 0, arch_capabilities: 0, mcu_opt_ctrl: None, + feat_ctl: None, ls_cfg_readable: None, sbpb_write_accepted: None, smt: false, @@ -72,6 +78,7 @@ const MILAN_GUEST: Facts = Facts { cpuid_8000_0021_ecx: 0, arch_capabilities: 0, mcu_opt_ctrl: None, + feat_ctl: None, ls_cfg_readable: None, sbpb_write_accepted: None, smt: false, @@ -94,6 +101,7 @@ const TURIN_GUEST: Facts = Facts { cpuid_8000_0021_ecx: 0, arch_capabilities: 0, mcu_opt_ctrl: None, + feat_ctl: None, ls_cfg_readable: None, sbpb_write_accepted: None, smt: false, @@ -104,20 +112,20 @@ const TURIN_GUEST: Facts = Facts { #[derive(Debug, PartialEq)] struct State { tsx: Tsx, - spectre_v2: SpectreV2, - bhi: Bhi, - ibpb: Ibpb, + spectre_v2: Option, + bhi: Option, + ibpb: bool, ibrs_fw: bool, stibp: Stibp, retbleed: Retbleed, ssb: Ssb, - mds: Mds, - taa: Taa, - mmio: Mmio, - rfds: Rfds, + mds: Option, + taa: Option, + mmio: Option, + rfds: Option, srbds: Option, gds: Option, - its: Its, + its: bool, vmscape: Vmscape, srso: Option, tsa: Option, @@ -155,6 +163,7 @@ fn state(d: &Decision) -> State { hypervisor: _, autoibrs: _, smt: _, + vmx: _, } = *d; State { tsx, @@ -185,20 +194,20 @@ fn state(d: &Decision) -> State { /// Nothing selected: a CPU no vulnerability reaches. const NONE: State = State { tsx: Tsx::NotSupported, - spectre_v2: SpectreV2::None, - bhi: Bhi::NotAffected, - ibpb: Ibpb::Absent, + spectre_v2: None, + bhi: None, + ibpb: false, ibrs_fw: false, stibp: Stibp::None, retbleed: Retbleed::None, ssb: Ssb::None, - mds: Mds::Off, - taa: Taa::Off, - mmio: Mmio::Off, - rfds: Rfds::Off, + mds: None, + taa: None, + mmio: None, + rfds: None, srbds: None, gds: None, - its: Its::Off, + its: false, vmscape: Vmscape::None, srso: None, tsa: None, @@ -211,7 +220,7 @@ const NONE: State = State { /// `facts`' line for every file `capture` names, and `capture` names each /// file once. fn assert_capture(facts: &Facts, capture: &str) -> Decision { - let d = decide(facts).expect("decided"); + let d = decide(facts); let mut named = Vec::new(); for l in capture.lines() { let (file, want) = l @@ -233,8 +242,8 @@ fn assert_capture(facts: &Facts, capture: &str) -> Decision { d } -fn line(facts: &Facts, v: Vuln) -> std::string::String { - decide(facts).expect("decided").line(v).expect("modelled").to_string() +fn line(facts: &Facts, v: Vuln) -> String { + decide(facts).line(v).expect("modelled").to_string() } #[test] @@ -245,12 +254,12 @@ fn the_t14s_facts_give_its_lines_and_its_spec_ctrl() { assert_eq!( state(&d), State { - spectre_v2: SpectreV2::Eibrs, - bhi: Bhi::SwLoop, - ibpb: Ibpb::Conditional, + spectre_v2: Some(SpectreV2::Eibrs), + bhi: Some(Bhi::SwLoop), + ibpb: true, ssb: Ssb::Prctl, gds: Some(Gds::Full), - its: Its::AlignedThunks, + its: true, spec_ctrl: 0x1, ..NONE } @@ -270,7 +279,10 @@ fn the_tcg_models_signature_gives_its_lines() { let facts = Facts { cpuid_1_ecx: hypervisor | rdrand, microcode, smt, ..TCG }; let d = assert_capture(&facts, include_str!("../fixtures/tcg.txt")); - assert_eq!(state(&d), State { spectre_v2: SpectreV2::Retpoline, ..NONE }); + assert_eq!( + state(&d), + State { spectre_v2: Some(SpectreV2::Retpoline), ..NONE } + ); } } } @@ -279,12 +291,12 @@ fn the_tcg_models_signature_gives_its_lines() { /// The track's exit control: the T14 with `IA32_ARCH_CAPABILITIES` read as 0. /// Without `IBRS_ALL` it is in retpoline mode, without `GDS_CTRL` it has no GDS -/// microcode, without `MDS_NO` it clears CPU buffers, and without `RDCL_NO` and -/// `PSCHANGE_MC_NO` Linux's L1TF and ITLB multihit lines turn on state the -/// facts do not carry. +/// microcode, without `MDS_NO` it clears CPU buffers, without `PSCHANGE_MC_NO` +/// its kept VMX gives ITLB multihit's "VMX disabled" (`bugs.c:3096-3097`), and +/// without `RDCL_NO` Linux's L1TF line turns on state the facts do not carry. #[test] fn the_t14_with_arch_capabilities_read_as_zero() { - let d = decide(&Facts { arch_capabilities: 0, mcu_opt_ctrl: None, ..T14 }).expect("decided"); + let d = decide(&Facts { arch_capabilities: 0, mcu_opt_ctrl: None, ..T14 }); let line = |v| d.line(v).map(|l| l.to_string()); assert_eq!( line(Vuln::SpectreV2).as_deref(), @@ -296,19 +308,26 @@ fn the_t14_with_arch_capabilities_read_as_zero() { assert_eq!(line(Vuln::GatherDataSampling).as_deref(), Ok("Vulnerable: No microcode")); assert_eq!(line(Vuln::Meltdown).as_deref(), Ok("Mitigation: PTI")); assert_eq!(line(Vuln::Mds).as_deref(), Ok("Mitigation: Clear CPU buffers; SMT vulnerable")); + // Without SMT, "disabled" (`bugs.c:3128-3129`). + let no_smt = Facts { arch_capabilities: 0, mcu_opt_ctrl: None, smt: false, ..T14 }; + assert_eq!( + decide(&no_smt).line(Vuln::Mds).expect("modelled").to_string(), + "Mitigation: Clear CPU buffers; SMT disabled" + ); assert_eq!(line(Vuln::L1tf).err(), Some(Unmodelled::L1tf)); - assert_eq!(line(Vuln::ItlbMultihit).err(), Some(Unmodelled::ItlbMultihit)); + assert_eq!(line(Vuln::ItlbMultihit).as_deref(), Ok("KVM: Mitigation: VMX disabled")); + assert_eq!(line(Vuln::TsxAsyncAbort).as_deref(), Ok("Not affected")); assert_eq!( state(&d), State { - spectre_v2: SpectreV2::Retpoline, - ibpb: Ibpb::Conditional, + spectre_v2: Some(SpectreV2::Retpoline), + ibpb: true, ibrs_fw: true, stibp: Stibp::Prctl, ssb: Ssb::Prctl, - mds: Mds::Full, + mds: Some(Mds::Full), gds: Some(Gds::UcodeNeeded), - its: Its::AlignedThunks, + its: true, clear_cpu_buf: true, ..NONE } @@ -337,24 +356,99 @@ fn the_first_matching_blacklist_row_decides() { assert_eq!(at(6), "Mitigation: Aligned branch/return thunks"); } -/// KABYLAKE stepping 0xA's bad microcode is 0x80 and below: there Linux drops -/// IBRS, IBPB, STIBP and SSBD. +/// KABYLAKE (family 6 model 0x9E) stepping 0xA with [`T14`]'s CPUID and no +/// `ARCH_CAPABILITIES`, outside a hypervisor. +const KABYLAKE: Facts = Facts { signature: 0x0009_06ea, arch_capabilities: 0, mcu_opt_ctrl: None, ..T14 }; + +/// KABYLAKE stepping 0xA's bad microcode is 0x80 and below +/// (`intel.c:143,177-179`): there Linux drops IBRS, IBPB, STIBP and SSBD +/// (`intel.c:296-309`), so Spectre v2 falls to retpolines (`bugs.c:1884-1893`), +/// RETBleed to "Vulnerable" (`bugs.c:1175-1190`), STIBP to none +/// (`bugs.c:1564-1567`) and VMSCAPE to "Vulnerable" (`bugs.c:2851-2855`). #[test] fn spectre_bad_microcode_includes_its_bound() { - let kabylake = |microcode| { + let d = decide(&Facts { microcode: 0x80, ..KABYLAKE }); + assert_eq!( + state(&d), + State { + spectre_v2: Some(SpectreV2::Retpoline), + mds: Some(Mds::Full), + mmio: Some(Mmio::Verw), + srbds: Some(Srbds::Full), + gds: Some(Gds::UcodeNeeded), + clear_cpu_buf: true, + ..NONE + } + ); + let line = |v| d.line(v).expect("modelled").to_string(); + assert_eq!( + line(Vuln::SpectreV2), + "Mitigation: Retpolines; STIBP: disabled; RSB filling; PBRSB-eIBRS: Not affected; \ + BHI: Not affected" + ); + assert_eq!(line(Vuln::Retbleed), "Vulnerable"); + assert_eq!(line(Vuln::SpecStoreBypass), "Vulnerable"); + assert_eq!(line(Vuln::Vmscape), "Vulnerable"); + let d = decide(&Facts { microcode: 0x81, ..KABYLAKE }); + assert_eq!( + state(&d), + State { + spectre_v2: Some(SpectreV2::Ibrs), + ibpb: true, + stibp: Stibp::Prctl, + retbleed: Retbleed::Ibrs, + ssb: Ssb::Prctl, + mds: Some(Mds::Full), + mmio: Some(Mmio::Verw), + srbds: Some(Srbds::Full), + gds: Some(Gds::UcodeNeeded), + vmscape: Vmscape::IbpbExitToUser, + clear_cpu_buf: true, + spec_ctrl: SPEC_CTRL_IBRS, + ..NONE + } + ); + assert_eq!( + d.line(Vuln::SpectreV2).expect("modelled").to_string(), + "Mitigation: IBRS; IBPB: conditional; STIBP: conditional; RSB filling; \ + PBRSB-eIBRS: Not affected; BHI: Not affected" + ); + assert_eq!(d.line(Vuln::Retbleed).expect("modelled").to_string(), "Mitigation: IBRS"); +} + +/// `early_init_intel` drops the controls only where one of IBRS, IBPB or STIBP +/// is set (`intel.c:296-299`; `init_speculation_control` sets them from the +/// AMD bits too, `common.c:994-1005`): alone, `SPEC_CTRL_SSBD` keeps SSBD. +#[test] +fn bad_microcode_drops_the_controls_only_beside_one_of_them() { + let ssb = |ebx8| { decide(&Facts { - signature: 0x0009_06ea, - microcode, - arch_capabilities: 0, - mcu_opt_ctrl: None, - ..T14 + microcode: 0x80, + cpuid_7_0_edx: CPUID_7_0_EDX_SPEC_CTRL_SSBD, + cpuid_8000_0008_ebx: ebx8, + ..KABYLAKE }) - .expect("decided") + .ssb }; - let bad = kabylake(0x80); - assert_eq!((bad.ibpb, bad.ssb, bad.spectre_v2), (Ibpb::Absent, Ssb::None, SpectreV2::Retpoline)); - let good = kabylake(0x81); - assert_eq!((good.ibpb, good.ssb, good.spectre_v2), (Ibpb::Conditional, Ssb::Prctl, SpectreV2::Ibrs)); + assert_eq!(ssb(0), Ssb::Prctl); + assert_eq!(ssb(CPUID_8000_0008_EBX_AMD_IBRS), Ssb::None); + assert_eq!(ssb(CPUID_8000_0008_EBX_AMD_IBPB), Ssb::None); + assert_eq!(ssb(CPUID_8000_0008_EBX_AMD_STIBP), Ssb::None); +} + +/// `spectre_bad_microcodes` keys `x86_vfm`, vendor and family with the model +/// (`intel.c:141-143,177`), and only `early_init_intel` reads it: neither an +/// unknown vendor's nor an Intel family 0xF's model 0x9E stepping 0xA drops +/// the controls at microcode 0x80. +#[test] +fn bad_microcode_is_intel_family_6s() { + let unknown = Facts { vendor: Vendor::from_id(b"GenuineIotel"), feat_ctl: None, ..KABYLAKE }; + let family_f = Facts { signature: 0x0009_0fea, ..KABYLAKE }; + assert_eq!(Ident::new(family_f.vendor, family_f.signature).model, 0x9E); + for facts in [unknown, family_f] { + let d = decide(&Facts { microcode: 0x80, ..facts }); + assert_eq!((d.ibpb, d.ssb), (true, Ssb::Prctl), "{:?}", facts.vendor); + } } #[test] @@ -363,26 +457,54 @@ fn a_guest_cannot_know_its_gds_mitigation() { assert_eq!(line(&guest, Vuln::GatherDataSampling), "Unknown: Dependent on hypervisor status"); } +/// `gds_select_mitigation` reads `GDS_MITG_LOCKED` (`bugs.c:849-861`); `gds_strings` +/// 766. +#[test] +fn firmware_can_lock_the_gds_mitigation() { + let locked = Facts { mcu_opt_ctrl: Some(GDS_MITG_LOCKED), ..T14 }; + assert_eq!(decide(&locked).gds, Some(Gds::FullLocked)); + assert_eq!(line(&locked, Vuln::GatherDataSampling), "Mitigation: Microcode (locked)"); +} + +/// `RTM_ALWAYS_ABORT` sets `TSX_CTRL_RTM_ALWAYS_ABORT` before `TSX_CTRL` is +/// looked at (`tsx.c:170-176`). +#[test] +fn rtm_always_abort_decides_tsx_first() { + let edx7 = T14.cpuid_7_0_edx | CPUID_7_0_EDX_RTM_ALWAYS_ABORT; + let d = decide(&Facts { cpuid_7_0_edx: edx7, ..T14 }); + assert_eq!(d.tsx, Tsx::RtmAlwaysAbort); + let arch = T14.arch_capabilities | ARCH_CAP_TSX_CTRL_MSR; + assert_eq!(decide(&Facts { cpuid_7_0_edx: edx7, arch_capabilities: arch, ..T14 }).tsx, Tsx::RtmAlwaysAbort); + assert_eq!(decide(&Facts { arch_capabilities: arch, ..T14 }).tsx, Tsx::Disable); +} + // The two fixtures below are this crate's reading of the pinned Linux, awaiting // a capture on a nightly runner: // `issues/build/no-nightly-runner-has-had-its-cpuid-and-vulnerability-lines-captured.md`. +/// A guest's lines do not read its microcode: `tsa_init` returns under a +/// hypervisor (`amd.c:519-520`) before `amd_check_tsa_microcode`. #[test] fn a_milan_kvm_guest_gives_the_tags_lines() { - let d = assert_capture(&MILAN_GUEST, include_str!("../fixtures/awaiting-capture/milan-kvm.txt")); - assert_eq!( - state(&d), - State { - spectre_v2: SpectreV2::Retpoline, - ibpb: Ibpb::Conditional, - ibrs_fw: true, - ssb: Ssb::Prctl, - srso: Some(Srso::SafeRetUcodeNeeded), - tsa: Some(Tsa::UcodeNeeded), - clear_cpu_buf: true, - ..NONE - } - ); + for microcode in [0, u32::MAX] { + let d = assert_capture( + &Facts { microcode, ..MILAN_GUEST }, + include_str!("../fixtures/awaiting-capture/milan-kvm.txt"), + ); + assert_eq!( + state(&d), + State { + spectre_v2: Some(SpectreV2::Retpoline), + ibpb: true, + ibrs_fw: true, + ssb: Ssb::Prctl, + srso: Some(Srso::SafeRetUcodeNeeded), + tsa: Some(Tsa::UcodeNeeded), + clear_cpu_buf: true, + ..NONE + } + ); + } } #[test] @@ -391,8 +513,8 @@ fn a_turin_kvm_guest_gives_the_tags_lines() { assert_eq!( state(&d), State { - spectre_v2: SpectreV2::Eibrs, - ibpb: Ibpb::Conditional, + spectre_v2: Some(SpectreV2::Eibrs), + ibpb: true, ssb: Ssb::Prctl, sbpb: true, efer_autoibrs: true, @@ -413,7 +535,7 @@ fn zen2_selects_by_smt() { smt, ..MILAN_GUEST }; - let d = decide(&rome(false)).expect("decided"); + let d = decide(&rome(false)); assert_eq!( d.line(Vuln::SpecRstackOverflow).expect("modelled").to_string(), "Mitigation: SMT disabled" @@ -422,7 +544,7 @@ fn zen2_selects_by_smt() { d.line(Vuln::Retbleed).expect("modelled").to_string(), "Mitigation: untrained return thunk; SMT disabled" ); - let d = decide(&rome(true)).expect("decided"); + let d = decide(&rome(true)); assert_eq!( d.line(Vuln::Retbleed).expect("modelled").to_string(), "Mitigation: untrained return thunk; SMT enabled with STIBP protection" @@ -430,8 +552,8 @@ fn zen2_selects_by_smt() { assert_eq!( state(&d), State { - spectre_v2: SpectreV2::Retpoline, - ibpb: Ibpb::Conditional, + spectre_v2: Some(SpectreV2::Retpoline), + ibpb: true, stibp: Stibp::StrictPreferred, retbleed: Retbleed::Unret, ssb: Ssb::Prctl, @@ -443,6 +565,73 @@ fn zen2_selects_by_smt() { ); } +/// A native Zen1, family 0x17 model 1 stepping 1 (`amd.c:602-606`), with SMT +/// and no leaf 0x80000021; 0x80000008:EBX carries AMD_IBPB, AMD_IBRS and +/// AMD_SSBD, and not AMD_STIBP_ALWAYS_ON. +const ZEN1: Facts = Facts { + vendor: Vendor::from_id(b"AuthenticAMD"), + signature: 0x0080_0f11, + microcode: 0, + cpuid_1_ecx: 0, + cpuid_7_0_ebx: 0, + cpuid_7_0_edx: 0, + cpuid_7_2_edx: 0, + cpuid_8000_0008_ebx: CPUID_8000_0008_EBX_AMD_IBPB + | CPUID_8000_0008_EBX_AMD_IBRS + | CPUID_8000_0008_EBX_AMD_SSBD, + cpuid_8000_0021_eax: 0, + cpuid_8000_0021_ecx: 0, + arch_capabilities: 0, + mcu_opt_ctrl: None, + feat_ctl: None, + ls_cfg_readable: None, + sbpb_write_accepted: None, + smt: true, +}; + +/// Zen1 with SMT: the untrained return thunk (`bugs.c:1103-1106`) forces STIBP +/// always on without AMD_STIBP_ALWAYS_ON (`bugs.c:1579-1585`), which sets +/// `SPEC_CTRL.STIBP` (`bugs.c:2054-2068,2976-2977`) and gives retbleed's "SMT +/// enabled with STIBP protection" (`bugs.c:3271-3275`); IBPB-extending microcode +/// from `IBPB_BRTYPE` (`amd.c:799-801`) gives safe RET (`bugs.c:2733-2734`). +/// Without AMD_STIBP there is no STIBP to force (`bugs.c:1564-1567`) and +/// retbleed's SMT is "vulnerable". +#[test] +fn zen1_forces_stibp_from_the_untrained_return_thunk() { + let with = Facts { cpuid_8000_0008_ebx: ZEN1.cpuid_8000_0008_ebx | CPUID_8000_0008_EBX_AMD_STIBP, ..ZEN1 }; + let d = decide(&with); + let expected = State { + spectre_v2: Some(SpectreV2::Retpoline), + ibpb: true, + stibp: Stibp::StrictPreferred, + retbleed: Retbleed::Unret, + ssb: Ssb::Prctl, + vmscape: Vmscape::IbpbExitToUser, + srso: Some(Srso::SafeRet), + spec_ctrl: SPEC_CTRL_STIBP, + ..NONE + }; + assert_eq!(state(&d), expected); + assert_eq!( + line(&with, Vuln::SpectreV2), + "Mitigation: Retpolines; IBPB: conditional; STIBP: always-on; RSB filling; \ + PBRSB-eIBRS: Not affected; BHI: Not affected" + ); + assert_eq!( + line(&with, Vuln::Retbleed), + "Mitigation: untrained return thunk; SMT enabled with STIBP protection" + ); + assert_eq!(line(&with, Vuln::SpecRstackOverflow), "Mitigation: Safe RET"); + let d = decide(&ZEN1); + assert_eq!(state(&d), State { stibp: Stibp::None, spec_ctrl: 0, ..expected }); + assert_eq!( + line(&ZEN1, Vuln::SpectreV2), + "Mitigation: Retpolines; IBPB: conditional; STIBP: disabled; RSB filling; \ + PBRSB-eIBRS: Not affected; BHI: Not affected" + ); + assert_eq!(line(&ZEN1, Vuln::Retbleed), "Mitigation: untrained return thunk; SMT vulnerable"); +} + /// [`MILAN_GUEST`] outside a hypervisor, its `PRED_CMD.SBPB` probe answering /// `sbpb`. const fn native_milan(microcode: u32, sbpb: bool) -> Facts { @@ -456,8 +645,8 @@ const fn native_milan(microcode: u32, sbpb: bool) -> Facts { /// What [`native_milan`] selects whatever its microcode and probe. const NATIVE_MILAN: State = State { - spectre_v2: SpectreV2::Retpoline, - ibpb: Ibpb::Conditional, + spectre_v2: Some(SpectreV2::Retpoline), + ibpb: true, ibrs_fw: true, ssb: Ssb::Prctl, vmscape: Vmscape::IbpbExitToUser, @@ -468,7 +657,7 @@ const NATIVE_MILAN: State = State { /// microcode, and `amd_check_tsa_microcode`'s row for 0xA0011 decides TSA's. #[test] fn zen3_native_reads_its_probe_and_its_tsa_microcode() { - let d = decide(&native_milan(0x0a00_11d6, false)).expect("decided"); + let d = decide(&native_milan(0x0a00_11d6, false)); assert_eq!( state(&d), State { @@ -477,7 +666,7 @@ fn zen3_native_reads_its_probe_and_its_tsa_microcode() { ..NATIVE_MILAN } ); - let d = decide(&native_milan(0x0a00_11d7, true)).expect("decided"); + let d = decide(&native_milan(0x0a00_11d7, true)); assert_eq!( state(&d), State { srso: Some(Srso::SafeRet), tsa: Some(Tsa::Full), clear_cpu_buf: true, ..NATIVE_MILAN } @@ -492,10 +681,10 @@ fn zen3_native_reads_its_probe_and_its_tsa_microcode() { #[test] fn zen4_reads_its_row_of_the_tsa_microcode_table() { let genoa = |microcode| Facts { signature: 0x00a1_0f11, ..native_milan(microcode, true) }; - let d = decide(&genoa(0x0a10_114c)).expect("decided"); + let d = decide(&genoa(0x0a10_114c)); assert_eq!((d.tsa, d.clear_cpu_buf), (Some(Tsa::Full), true)); assert_eq!(line(&genoa(0x0a10_114c), Vuln::Tsa), "Mitigation: Clear CPU buffers"); - let d = decide(&genoa(0x0a10_114b)).expect("decided"); + let d = decide(&genoa(0x0a10_114b)); assert_eq!((d.tsa, d.clear_cpu_buf), (Some(Tsa::UcodeNeeded), false)); assert_eq!( line(&genoa(0x0a10_114b), Vuln::Tsa), @@ -524,7 +713,7 @@ fn tsa_is_ruled_out_only_by_both_its_no_bits() { fn amd_stibp_always_on_is_strict_without_the_untrained_return_thunk() { assert_ne!(MILAN_GUEST.cpuid_8000_0008_ebx & CPUID_8000_0008_EBX_AMD_STIBP_ALWAYS_ON, 0); let milan = Facts { smt: true, ..native_milan(0x0a00_11d7, true) }; - let d = decide(&milan).expect("decided"); + let d = decide(&milan); assert_eq!( state(&d), State { @@ -559,13 +748,13 @@ fn amd_0x15_to_0x17_take_ssbd_from_the_ls_cfg_probe() { ls_cfg_readable: Some(readable), ..MILAN_GUEST }; - let d = decide(&at(true)).expect("decided"); + let d = decide(&at(true)); assert_eq!(d.ssb, Ssb::Prctl, "{signature:#x}"); assert_eq!( line(&at(true), Vuln::SpecStoreBypass), "Mitigation: Speculative Store Bypass disabled via prctl" ); - let d = decide(&at(false)).expect("decided"); + let d = decide(&at(false)); assert_eq!(d.ssb, Ssb::None, "{signature:#x}"); assert_eq!(line(&at(false), Vuln::SpecStoreBypass), "Vulnerable"); } @@ -585,7 +774,7 @@ fn srso_user_kernel_no_mitigates_only_the_vm_exit() { | bp_spec_reduce, ..native_milan(0x0a00_11d7, sbpb) }; - let d = decide(&facts).expect("decided"); + let d = decide(&facts); (d.srso, d.vmscape, line(&facts, Vuln::SpecRstackOverflow), line(&facts, Vuln::Vmscape)) }; let exit_to_user = "Mitigation: IBPB before exit to userspace"; @@ -618,32 +807,865 @@ fn srso_user_kernel_no_mitigates_only_the_vm_exit() { ); } -/// A KABYLAKE guest, family 6 model 0x9E stepping 0xA, without -/// `ARCH_CAPABILITIES`: its blacklist row gives SRBDS (`common.c:1303`), whose -/// mitigation a guest cannot know (`bugs.c:691-692`, `srbds_strings` 636), and -/// no `MDS_NO` gives MDS (`common.c:1447-1449`), whose host SMT state a guest -/// cannot know (`bugs.c:3117-3119`). +/// [`KABYLAKE`] as a guest at its bad microcode, which a guest does not check +/// (`intel.c:173-174`): the hypervisor bit alone gives BHI (`common.c:1533-1536`) +/// and ITS (`common.c:1405-1406`; this stepping's row, `common.c:1303`, has no +/// ITS). IBRS stays, so Spectre v2 is IBRS (`bugs.c:1884-1891`) with the SW +/// BHB loop (`bugs.c:1851-1857`); SRBDS's and GDS's mitigations a guest cannot +/// know (`bugs.c:691-692,821-824`), nor its host's SMT (`bugs.c:3117-3119`, +/// 3155-3157). #[test] -fn an_intel_guest_cannot_know_its_srbds_mitigation_or_its_hosts_smt() { +fn an_intel_guest_cannot_know_its_mitigations_or_its_hosts_smt() { let guest = Facts { - signature: 0x0009_06ea, + microcode: 0x80, cpuid_1_ecx: T14.cpuid_1_ecx | CPUID_1_ECX_HYPERVISOR, + ..KABYLAKE + }; + let d = decide(&guest); + assert_eq!( + state(&d), + State { + spectre_v2: Some(SpectreV2::Ibrs), + bhi: Some(Bhi::SwLoop), + ibpb: true, + stibp: Stibp::Prctl, + retbleed: Retbleed::Ibrs, + ssb: Ssb::Prctl, + mds: Some(Mds::Full), + mmio: Some(Mmio::Verw), + srbds: Some(Srbds::Hypervisor), + gds: Some(Gds::Hypervisor), + its: true, + clear_cpu_buf: true, + spec_ctrl: SPEC_CTRL_IBRS, + ..NONE + } + ); + let line = |v| d.line(v).expect("modelled").to_string(); + assert_eq!( + line(Vuln::SpectreV2), + "Mitigation: IBRS; IBPB: conditional; STIBP: conditional; RSB filling; \ + PBRSB-eIBRS: Not affected; BHI: SW loop, KVM: SW loop" + ); + assert_eq!(line(Vuln::Srbds), "Unknown: Dependent on hypervisor status"); + assert_eq!(line(Vuln::Mds), "Mitigation: Clear CPU buffers; SMT Host state unknown"); + assert_eq!(line(Vuln::MmioStaleData), "Mitigation: Clear CPU buffers; SMT Host state unknown"); + assert_eq!(line(Vuln::IndirectTargetSelection), "Mitigation: Aligned branch/return thunks"); + assert_eq!(line(Vuln::Vmscape), "Not affected"); +} + +/// An Alder Lake (family 6 model 0x97) with [`T14`]'s CPUID and +/// `ARCH_CAPABILITIES` and with `BHI_CTRL`: eIBRS sets `SPEC_CTRL.IBRS` +/// (`bugs.c:1930-1936`) and BHI is mitigated in hardware by `BHI_DIS_S` +/// (`bugs.c:1795-1805,1847-1849`; `spectre_bhi_state` 3224-3225). STIBP is not +/// printed under eIBRS (`bugs.c:3176-3178`); its row gives VMSCAPE +/// (`common.c:1316`). +#[test] +fn alder_lake_mitigates_bhi_in_hardware() { + let adl = Facts { + signature: 0x0009_0672, + cpuid_7_2_edx: T14.cpuid_7_2_edx | CPUID_7_2_EDX_BHI_CTRL, + ..T14 + }; + let d = decide(&adl); + assert_eq!( + state(&d), + State { + spectre_v2: Some(SpectreV2::Eibrs), + bhi: Some(Bhi::BhiDisS), + ibpb: true, + ssb: Ssb::Prctl, + vmscape: Vmscape::IbpbExitToUser, + spec_ctrl: SPEC_CTRL_IBRS | SPEC_CTRL_BHI_DIS_S, + ..NONE + } + ); + assert_eq!( + line(&adl, Vuln::SpectreV2), + "Mitigation: Enhanced / Automatic IBRS; IBPB: conditional; PBRSB-eIBRS: SW sequence; \ + BHI: BHI_DIS_S" + ); + assert_eq!(line(&adl, Vuln::IndirectTargetSelection), "Not affected"); +} + +/// [`T14`] as a guest without `IBRS_ALL` and with `RRSBA`: retpolines +/// (`bugs.c:1893`) disable RRSBA through `RRSBA_DIS_S` where `RRSBA_CTRL` is +/// (`bugs.c:1723-1739,1966-1969`), which leaves BHI to them +/// (`bugs.c:1837-1841`; `spectre_bhi_state` 3228-3231); without `RRSBA_CTRL` +/// BHI takes the SW loop (`bugs.c:1851-1857`). +#[test] +fn a_retpoline_guest_leaves_bhi_to_retpolines_only_with_rrsba_disabled() { + let guest = |edx72| Facts { + cpuid_1_ecx: T14.cpuid_1_ecx | CPUID_1_ECX_HYPERVISOR, + cpuid_7_2_edx: edx72, + arch_capabilities: (T14.arch_capabilities & !ARCH_CAP_IBRS_ALL) | ARCH_CAP_RRSBA, + ..T14 + }; + let with = guest(T14.cpuid_7_2_edx | CPUID_7_2_EDX_RRSBA_CTRL); + let expected = State { + spectre_v2: Some(SpectreV2::Retpoline), + bhi: Some(Bhi::Retpoline), + ibpb: true, + ibrs_fw: true, + stibp: Stibp::Prctl, + ssb: Ssb::Prctl, + gds: Some(Gds::Hypervisor), + its: true, + spec_ctrl: SPEC_CTRL_RRSBA_DIS_S, + ..NONE + }; + assert_eq!(state(&decide(&with)), expected); + assert_eq!( + line(&with, Vuln::SpectreV2), + "Mitigation: Retpolines; IBPB: conditional; IBRS_FW; STIBP: conditional; RSB filling; \ + PBRSB-eIBRS: Not affected; BHI: Retpoline" + ); + let without = guest(T14.cpuid_7_2_edx); + assert_eq!(state(&decide(&without)), State { bhi: Some(Bhi::SwLoop), spec_ctrl: 0, ..expected }); + assert_eq!( + line(&without, Vuln::SpectreV2), + "Mitigation: Retpolines; IBPB: conditional; IBRS_FW; STIBP: conditional; RSB filling; \ + PBRSB-eIBRS: Not affected; BHI: SW loop, KVM: SW loop" + ); +} + +/// A native Comet Lake (family 6 model 0xA5) with RTM and `MD_CLEAR`, +/// `FLUSH_L1D` and `SRBDS_CTRL`, whose `ARCH_CAPABILITIES` the fixtures below +/// vary; its row (`common.c:1309`) gives MMIO, MMIO_SBDS, RETBLEED, GDS, ITS +/// and VMSCAPE. +const COMETLAKE: Facts = Facts { + vendor: Vendor::from_id(b"GenuineIntel"), + signature: 0x000a_0655, + microcode: 0xf8, + cpuid_1_ecx: CPUID_1_ECX_VMX | CPUID_1_ECX_AVX | CPUID_1_ECX_RDRAND, + cpuid_7_0_ebx: CPUID_7_0_EBX_RTM | CPUID_7_0_EBX_RDSEED, + cpuid_7_0_edx: CPUID_7_0_EDX_SRBDS_CTRL + | CPUID_7_0_EDX_MD_CLEAR + | CPUID_7_0_EDX_SPEC_CTRL + | CPUID_7_0_EDX_INTEL_STIBP + | CPUID_7_0_EDX_FLUSH_L1D + | CPUID_7_0_EDX_ARCH_CAPABILITIES + | CPUID_7_0_EDX_SPEC_CTRL_SSBD, + cpuid_7_2_edx: 0, + cpuid_8000_0008_ebx: 0, + cpuid_8000_0021_eax: 0, + cpuid_8000_0021_ecx: 0, + arch_capabilities: 0, + mcu_opt_ctrl: None, + feat_ctl: Some(FEAT_CTL_LOCKED | FEAT_CTL_VMX_ENABLED_OUTSIDE_SMX), + ls_cfg_readable: None, + sbpb_write_accepted: None, + smt: true, +}; + +/// What [`COMETLAKE`] selects outside its buffer-clearing bugs: IBRS for +/// RETBleed (`bugs.c:1884-1891,1175-1178`), no GDS microcode +/// (`bugs.c:831-842`), ITS's thunks and VMSCAPE's IBPB. +const COMETLAKE_STATE: State = State { + spectre_v2: Some(SpectreV2::Ibrs), + ibpb: true, + stibp: Stibp::Prctl, + retbleed: Retbleed::Ibrs, + ssb: Ssb::Prctl, + gds: Some(Gds::UcodeNeeded), + its: true, + vmscape: Vmscape::IbpbExitToUser, + spec_ctrl: SPEC_CTRL_IBRS, + ..NONE +}; + +/// Without `MDS_NO`, `MD_CLEAR` mitigates TAA (`bugs.c:353-356`) and, with +/// `FLUSH_L1D`, MMIO (`bugs.c:468-472`). With `MDS_NO` and without +/// `TSX_CTRL_MSR` TAA's microcode is missing (`bugs.c:367-369`), and so is +/// MMIO's unless `FB_CLEAR` (`bugs.c:468-474`). RTM on keeps SRBDS's +/// microcode mitigation (`bugs.c:688-695`). +#[test] +fn comet_lake_clears_cpu_buffers_by_mds_no_and_fb_clear() { + let at = |arch| Facts { arch_capabilities: arch, ..COMETLAKE }; + let d = decide(&at(0)); + assert_eq!( + state(&d), + State { + mds: Some(Mds::Full), + taa: Some(Taa::Verw), + mmio: Some(Mmio::Verw), + srbds: Some(Srbds::Full), + clear_cpu_buf: true, + ..COMETLAKE_STATE + } + ); + let line = |facts: &Facts, v| line(facts, v); + assert_eq!(line(&at(0), Vuln::TsxAsyncAbort), "Mitigation: Clear CPU buffers; SMT vulnerable"); + assert_eq!(line(&at(0), Vuln::MmioStaleData), "Mitigation: Clear CPU buffers; SMT vulnerable"); + assert_eq!(line(&at(0), Vuln::Srbds), "Mitigation: Microcode"); + let mds_no = at(ARCH_CAP_MDS_NO); + assert_eq!( + state(&decide(&mds_no)), + State { + taa: Some(Taa::UcodeNeeded), + mmio: Some(Mmio::UcodeNeeded), + srbds: Some(Srbds::Full), + clear_cpu_buf: true, + ..COMETLAKE_STATE + } + ); + let no_microcode = "Vulnerable: Clear CPU buffers attempted, no microcode; SMT vulnerable"; + assert_eq!(line(&mds_no, Vuln::TsxAsyncAbort), no_microcode); + assert_eq!(line(&mds_no, Vuln::MmioStaleData), no_microcode); + assert_eq!(line(&mds_no, Vuln::Mds), "Not affected"); + let fb_clear = at(ARCH_CAP_MDS_NO | ARCH_CAP_FB_CLEAR); + assert_eq!(decide(&fb_clear).mmio, Some(Mmio::Verw)); +} + +/// SRBDS is "TSX disabled" on an `MDS_NO` CPU only where RTM is off and MMIO +/// Stale Data is not (`bugs.c:688-690`): `TSX_CTRL` disables RTM under +/// `TSX_MODE_OFF` (`tsx.c:188-227`), as `RTM_ALWAYS_ABORT` does +/// (`tsx.c:170-176`), and TAA is then "TSX disabled" (`bugs.c:335-338`) +/// without clearing buffers; the three `*_NO` bits make MMIO immune +/// (`common.c:1363-1368,1495`). +#[test] +fn srbds_is_tsx_disabled_only_without_rtm_and_mmio() { + let immune = ARCH_CAP_MDS_NO | ARCH_CAP_FBSDP_NO | ARCH_CAP_PSDP_NO | ARCH_CAP_SBDR_SSDP_NO; + let tsx_ctrl = Facts { arch_capabilities: immune | ARCH_CAP_TSX_CTRL_MSR, ..COMETLAKE }; + let off = State { + tsx: Tsx::Disable, + taa: Some(Taa::TsxDisabled), + srbds: Some(Srbds::TsxOff), + ..COMETLAKE_STATE + }; + assert_eq!(state(&decide(&tsx_ctrl)), off); + assert_eq!(line(&tsx_ctrl, Vuln::Srbds), "Mitigation: TSX disabled"); + assert_eq!(line(&tsx_ctrl, Vuln::TsxAsyncAbort), "Mitigation: TSX disabled"); + assert_eq!(line(&tsx_ctrl, Vuln::MmioStaleData), "Not affected"); + let always_abort = Facts { + cpuid_7_0_edx: COMETLAKE.cpuid_7_0_edx | CPUID_7_0_EDX_RTM_ALWAYS_ABORT, + arch_capabilities: immune, + ..COMETLAKE + }; + assert_eq!(state(&decide(&always_abort)), State { tsx: Tsx::RtmAlwaysAbort, ..off }); + let rtm = Facts { arch_capabilities: immune, ..COMETLAKE }; + assert_eq!( + state(&decide(&rtm)), + State { + taa: Some(Taa::UcodeNeeded), + srbds: Some(Srbds::Full), + clear_cpu_buf: true, + ..COMETLAKE_STATE + } + ); + let mmio = Facts { arch_capabilities: ARCH_CAP_MDS_NO | ARCH_CAP_TSX_CTRL_MSR, ..COMETLAKE }; + assert_eq!( + state(&decide(&mmio)), + State { + tsx: Tsx::Disable, + taa: Some(Taa::TsxDisabled), + mmio: Some(Mmio::UcodeNeeded), + srbds: Some(Srbds::Full), + ..COMETLAKE_STATE + } + ); +} + +/// An affected CPU's ITLB multihit line reads `X86_FEATURE_MSR_IA32_FEAT_CTL` +/// and `X86_FEATURE_VMX` (`bugs.c:3093-3095`) as `init_ia32_feat_ctl` left them: +/// a faulting read clears VMX (`feat_ctl.c:119-123`), an unlocked MSR is locked +/// with VMX enabled (`feat_ctl.c:139-166`), and a locked one keeps VMX only +/// with `VMX_ENABLED_OUTSIDE_SMX` (`feat_ctl.c:174-179`). No VM runs, so a kept +/// VMX is "disabled" (`bugs.c:3096-3097`). +#[test] +fn itlb_multihit_reads_what_feat_ctl_left_of_vmx() { + let at = |ecx, feat_ctl| line(&Facts { cpuid_1_ecx: ecx, feat_ctl, ..COMETLAKE }, Vuln::ItlbMultihit); + let ecx = COMETLAKE.cpuid_1_ecx; + let (disabled, unsupported) = ("KVM: Mitigation: VMX disabled", "KVM: Mitigation: VMX unsupported"); + assert_eq!(at(ecx, Some(0)), disabled); + assert_eq!(at(ecx, Some(FEAT_CTL_LOCKED | FEAT_CTL_VMX_ENABLED_OUTSIDE_SMX)), disabled); + assert_eq!(at(ecx, Some(FEAT_CTL_LOCKED)), unsupported); + assert_eq!(at(ecx, Some(FEAT_CTL_LOCKED | 1 << 1)), unsupported); + assert_eq!(at(ecx, None), unsupported); + assert_eq!(at(ecx & !CPUID_1_ECX_VMX, Some(0)), unsupported); + assert_eq!(line(&COMETLAKE, Vuln::ItlbMultihit), disabled); + assert_eq!( + line(&Facts { arch_capabilities: ARCH_CAP_PSCHANGE_MC_NO, ..COMETLAKE }, Vuln::ItlbMultihit), + "Not affected" + ); +} + +#[test] +#[should_panic(expected = "IA32_FEAT_CTL is read only where")] +fn an_amd_cpu_with_feat_ctl_is_a_contradiction() { + decide(&Facts { feat_ctl: Some(0), ..TCG }); +} + +/// A Silvermont (family 6 model 0x37) is `MSBDS_ONLY` (`common.c:1202`), whose +/// SMT line is "mitigated" or "disabled" (`bugs.c:3122-3126`). Neither MMIO +/// row names it, so its MMIO is unknown (`common.c:1498-1499`). +#[test] +fn an_msbds_only_cpu_reports_its_smt_as_mitigated() { + let silvermont = |smt| Facts { + vendor: Vendor::from_id(b"GenuineIntel"), + signature: 0x0003_0678, + microcode: 0, + cpuid_1_ecx: 0, + cpuid_7_0_ebx: 0, + cpuid_7_0_edx: CPUID_7_0_EDX_MD_CLEAR | CPUID_7_0_EDX_SPEC_CTRL | CPUID_7_0_EDX_INTEL_STIBP, + cpuid_7_2_edx: 0, + cpuid_8000_0008_ebx: 0, + cpuid_8000_0021_eax: 0, + cpuid_8000_0021_ecx: 0, arch_capabilities: 0, mcu_opt_ctrl: None, - ..T14 + feat_ctl: None, + ls_cfg_readable: None, + sbpb_write_accepted: None, + smt, }; - assert_eq!(line(&guest, Vuln::Srbds), "Unknown: Dependent on hypervisor status"); - assert_eq!(line(&guest, Vuln::Mds), "Mitigation: Clear CPU buffers; SMT Host state unknown"); + assert_eq!(line(&silvermont(true), Vuln::Mds), "Mitigation: Clear CPU buffers; SMT mitigated"); + assert_eq!(line(&silvermont(false), Vuln::Mds), "Mitigation: Clear CPU buffers; SMT disabled"); + assert_eq!(line(&silvermont(true), Vuln::MmioStaleData), "Unknown: No mitigations"); + assert_eq!(line(&silvermont(true), Vuln::L1tf), "Not affected"); + assert_eq!( + state(&decide(&silvermont(true))), + State { + spectre_v2: Some(SpectreV2::Retpoline), + ibpb: true, + ibrs_fw: true, + stibp: Stibp::Prctl, + mds: Some(Mds::Full), + clear_cpu_buf: true, + ..NONE + } + ); +} + +/// A native Hygon Dhyana, family 0x18 model 0 stepping 1, with SMT: its +/// whitelist row is AMD's (`common.c:1238`) and its blacklist row gives +/// RETBLEED, SRSO and VMSCAPE (`common.c:1341`). Hygon probes `LS_CFG` for +/// SSBD in every family (`hygon.c:228-239`), takes the untrained return thunk +/// (`bugs.c:1103-1106`) and IBPB rather than IBRS around firmware +/// (`bugs.c:2028-2036`), and has no `early_init_amd` to set `IBPB_BRTYPE`, so +/// safe RET lacks its microcode (`bugs.c:2672,2735-2736`). +#[test] +fn hygon_decides_as_linux_does() { + let dhyana = |readable| Facts { + vendor: Vendor::from_id(b"HygonGenuine"), + signature: 0x0090_0f01, + cpuid_8000_0008_ebx: CPUID_8000_0008_EBX_AMD_IBPB + | CPUID_8000_0008_EBX_AMD_IBRS + | CPUID_8000_0008_EBX_AMD_STIBP, + ls_cfg_readable: Some(readable), + ..ZEN1 + }; + let d = assert_capture( + &dhyana(true), + "/sys/devices/system/cpu/vulnerabilities/gather_data_sampling:Not affected +/sys/devices/system/cpu/vulnerabilities/indirect_target_selection:Not affected +/sys/devices/system/cpu/vulnerabilities/itlb_multihit:Not affected +/sys/devices/system/cpu/vulnerabilities/l1tf:Not affected +/sys/devices/system/cpu/vulnerabilities/mds:Not affected +/sys/devices/system/cpu/vulnerabilities/meltdown:Not affected +/sys/devices/system/cpu/vulnerabilities/mmio_stale_data:Not affected +/sys/devices/system/cpu/vulnerabilities/reg_file_data_sampling:Not affected +/sys/devices/system/cpu/vulnerabilities/retbleed:Mitigation: untrained return thunk; SMT enabled with STIBP protection +/sys/devices/system/cpu/vulnerabilities/spec_rstack_overflow:Vulnerable: Safe RET, no microcode +/sys/devices/system/cpu/vulnerabilities/spec_store_bypass:Mitigation: Speculative Store Bypass disabled via prctl +/sys/devices/system/cpu/vulnerabilities/spectre_v1:Mitigation: usercopy/swapgs barriers and __user pointer sanitization +/sys/devices/system/cpu/vulnerabilities/spectre_v2:Mitigation: Retpolines; IBPB: conditional; STIBP: always-on; RSB filling; PBRSB-eIBRS: Not affected; BHI: Not affected +/sys/devices/system/cpu/vulnerabilities/srbds:Not affected +/sys/devices/system/cpu/vulnerabilities/tsa:Not affected +/sys/devices/system/cpu/vulnerabilities/tsx_async_abort:Not affected +/sys/devices/system/cpu/vulnerabilities/vmscape:Mitigation: IBPB before exit to userspace", + ); + let expected = State { + spectre_v2: Some(SpectreV2::Retpoline), + ibpb: true, + stibp: Stibp::StrictPreferred, + retbleed: Retbleed::Unret, + ssb: Ssb::Prctl, + vmscape: Vmscape::IbpbExitToUser, + srso: Some(Srso::SafeRetUcodeNeeded), + spec_ctrl: SPEC_CTRL_STIBP, + ..NONE + }; + assert_eq!(state(&d), expected); + assert_eq!(state(&decide(&dhyana(false))), State { ssb: Ssb::None, ..expected }); +} + +#[test] +#[should_panic(expected = "MSR_AMD64_LS_CFG is probed exactly where")] +fn a_hygon_cpu_without_its_ls_cfg_probe_is_a_contradiction() { + decide(&Facts { + vendor: Vendor::from_id(b"HygonGenuine"), + signature: 0x0090_0f01, + cpuid_8000_0008_ebx: 0, + ..ZEN1 + }); +} + +/// A Zhaoxin, family 7 model 0x1B, with SMT: its whitelist row rules out +/// Spectre v2 but not v1 (`common.c:1242,1426`), so neither IBPB, STIBP nor IBRS +/// around firmware is set up (`bugs.c:1869-1871,1507-1508`); it reads +/// `IA32_FEAT_CTL` (`zhaoxin.c:97`). An unknown vendor's family 7 matches no +/// row and has Spectre v2. +#[test] +fn zhaoxin_without_spectre_v2_sets_up_no_ibpb() { + let zhaoxin = Facts { + vendor: Vendor::from_id(b" Shanghai "), + signature: 0x0001_07b5, + microcode: 0, + cpuid_1_ecx: CPUID_1_ECX_VMX, + cpuid_7_0_ebx: 0, + cpuid_7_0_edx: CPUID_7_0_EDX_SPEC_CTRL + | CPUID_7_0_EDX_INTEL_STIBP + | CPUID_7_0_EDX_SPEC_CTRL_SSBD, + cpuid_7_2_edx: 0, + cpuid_8000_0008_ebx: 0, + cpuid_8000_0021_eax: 0, + cpuid_8000_0021_ecx: 0, + arch_capabilities: 0, + mcu_opt_ctrl: None, + feat_ctl: Some(0), + ls_cfg_readable: None, + sbpb_write_accepted: None, + smt: true, + }; + let d = decide(&zhaoxin); + assert_eq!( + state(&d), + State { ssb: Ssb::Prctl, mds: Some(Mds::Vmwerv), clear_cpu_buf: true, ..NONE } + ); + assert_eq!(line(&zhaoxin, Vuln::SpectreV2), "Not affected"); + assert_eq!( + line(&zhaoxin, Vuln::SpectreV1), + "Mitigation: usercopy/swapgs barriers and __user pointer sanitization" + ); + assert_eq!( + line(&zhaoxin, Vuln::Mds), + "Vulnerable: Clear CPU buffers attempted, no microcode; SMT vulnerable" + ); + assert_eq!(line(&zhaoxin, Vuln::ItlbMultihit), "KVM: Mitigation: VMX disabled"); + let unknown = Facts { vendor: Vendor::from_id(b"GenuineIotel"), feat_ctl: None, ..zhaoxin }; + assert_eq!(decide(&unknown).spectre_v2, Some(SpectreV2::Retpoline)); +} + +/// A Centaur family 5 is `NO_SPECULATION` (`common.c:1184`) and reads +/// `IA32_FEAT_CTL` (`centaur.c:217`); ITLB multihit, set before that return +/// (`common.c:1419-1424`), is all it has. +#[test] +fn centaur_family_5_has_only_itlb_multihit() { + let centaur = Facts { + vendor: Vendor::from_id(b"CentaurHauls"), + signature: 0x0000_0540, + feat_ctl: Some(0), + cpuid_1_ecx: CPUID_1_ECX_VMX, + ..TCG + }; + assert_eq!(state(&decide(¢aur)), NONE); + assert_eq!(line(¢aur, Vuln::ItlbMultihit), "KVM: Mitigation: VMX disabled"); + assert_eq!(line(¢aur, Vuln::SpectreV1), "Not affected"); } #[test] #[should_panic(expected = "PRED_CMD.SBPB is probed exactly where")] fn a_native_zen3_without_its_probe_is_a_contradiction() { - let _ = decide(&Facts { cpuid_1_ecx: 0, ..MILAN_GUEST }); + decide(&Facts { cpuid_1_ecx: 0, ..MILAN_GUEST }); +} + +/// `x86_family` adds the extended family only to family 0xF, and `x86_model` +/// the extended model only from family 6 (`arch/x86/lib/cpu.c:6-37`). +#[test] +fn family_and_model_are_derived_as_arch_x86_lib_derives_them() { + let at = |sig| { + let id = Ident::new(Vendor::Intel, sig); + (id.family, id.model, id.stepping) + }; + assert_eq!(at(0x00a0_0f11), (0x19, 0x01, 1)); + assert_eq!(at(0x0ff0_06c1), (6, 0x0c, 1)); + assert_eq!(at(0x000f_0521), (5, 0x02, 1)); +} + +/// Each `*_NO` bit and feature test in `cpu_set_bug_bits` rules its bug out on +/// [`T14`]: `SSB_NO` (`common.c:1431-1434`), `PBRSB_NO` (`common.c:1440-1445`), +/// `GDS_NO` and a missing AVX (`common.c:1521-1523`), `ITS_NO` and `BHI_CTRL` +/// (`common.c:1391-1398`); `RSBA` gives RETBleed (`common.c:1502-1505`), which +/// eIBRS mitigates (`bugs.c:1180-1183`, `retbleed_strings` 1003). +#[test] +fn the_t14s_no_bits_each_rule_out_their_bug() { + let t14 = state(&decide(&T14)); + let arch = |bit| Facts { arch_capabilities: T14.arch_capabilities | bit, ..T14 }; + assert_eq!(line(&arch(ARCH_CAP_SSB_NO), Vuln::SpecStoreBypass), "Not affected"); + assert_eq!(state(&decide(&arch(ARCH_CAP_SSB_NO))), State { ssb: Ssb::None, ..t14 }); + assert_eq!( + line(&arch(ARCH_CAP_PBRSB_NO), Vuln::SpectreV2), + "Mitigation: Enhanced / Automatic IBRS; IBPB: conditional; PBRSB-eIBRS: Not affected; \ + BHI: SW loop, KVM: SW loop" + ); + assert_eq!(line(&arch(ARCH_CAP_GDS_NO), Vuln::GatherDataSampling), "Not affected"); + let no_avx = Facts { cpuid_1_ecx: T14.cpuid_1_ecx & !CPUID_1_ECX_AVX, ..T14 }; + assert_eq!(line(&no_avx, Vuln::GatherDataSampling), "Not affected"); + assert_eq!(line(&arch(ARCH_CAP_ITS_NO), Vuln::IndirectTargetSelection), "Not affected"); + let rsba = arch(ARCH_CAP_RSBA); + assert_eq!(state(&decide(&rsba)), State { retbleed: Retbleed::Eibrs, ..t14 }); + assert_eq!(line(&rsba, Vuln::Retbleed), "Mitigation: Enhanced IBRS"); + // `BHI_CTRL` also puts BHI in hardware (`bugs.c:1847-1849`). + let bhi_ctrl = Facts { cpuid_7_2_edx: T14.cpuid_7_2_edx | CPUID_7_2_EDX_BHI_CTRL, ..T14 }; + assert_eq!( + state(&decide(&bhi_ctrl)), + State { + bhi: Some(Bhi::BhiDisS), + its: false, + spec_ctrl: SPEC_CTRL_IBRS | SPEC_CTRL_BHI_DIS_S, + ..t14 + } + ); +} + +/// RFDS by `RFDS_CLEAR` where no row names it, whose `VERW` clears CPU buffers +/// (`common.c:1370-1386`; `bugs.c:529-532`, `rfds_strings` 516), and by +/// Alder Lake's row without `RFDS_NO` or `RFDS_CLEAR`, whose microcode is +/// missing (`common.c:1316`; `bugs.c:531-532`, `rfds_strings` 517). +#[test] +fn rfds_is_cleared_only_with_rfds_clear() { + let clear = Facts { + arch_capabilities: (T14.arch_capabilities & !ARCH_CAP_RFDS_NO) | ARCH_CAP_RFDS_CLEAR, + ..T14 + }; + assert_eq!( + state(&decide(&clear)), + State { rfds: Some(Rfds::Verw), clear_cpu_buf: true, ..state(&decide(&T14)) } + ); + assert_eq!(line(&clear, Vuln::RegFileDataSampling), "Mitigation: Clear Register File"); + let adl = Facts { + signature: 0x0009_0672, + arch_capabilities: T14.arch_capabilities & !ARCH_CAP_RFDS_NO, + ..T14 + }; + assert_eq!(decide(&adl).rfds, Some(Rfds::UcodeNeeded)); + assert_eq!(line(&adl, Vuln::RegFileDataSampling), "Vulnerable: No microcode"); + assert!(!decide(&adl).clear_cpu_buf); +} + +/// [`COMETLAKE`] without `ARCH_CAPABILITIES` bits, varied one fact at a time: +/// `TAA_NO` rules TAA out (`common.c:1467-1470`); without `MD_CLEAR` MDS is +/// `VMWERV` (`bugs.c:274-275`) and TAA's and MMIO's microcode is missing +/// (`bugs.c:353-356,468-474`); without `FLUSH_L1D` MMIO's is (`bugs.c:468-474`); +/// without `SRBDS_CTRL` SRBDS's is (`bugs.c:693-694`, `srbds_strings` 633); +/// either RDRAND or RDSEED gives SRBDS (`common.c:1480-1483`). +#[test] +fn comet_lake_varies_one_fact_at_a_time() { + let all = State { + mds: Some(Mds::Full), + taa: Some(Taa::Verw), + mmio: Some(Mmio::Verw), + srbds: Some(Srbds::Full), + clear_cpu_buf: true, + ..COMETLAKE_STATE + }; + let taa_no = Facts { arch_capabilities: ARCH_CAP_TAA_NO, ..COMETLAKE }; + assert_eq!(state(&decide(&taa_no)), State { taa: None, ..all }); + assert_eq!(line(&taa_no, Vuln::TsxAsyncAbort), "Not affected"); + let edx7 = |clear: u32| Facts { cpuid_7_0_edx: COMETLAKE.cpuid_7_0_edx & !clear, ..COMETLAKE }; + assert_eq!( + state(&decide(&edx7(CPUID_7_0_EDX_MD_CLEAR))), + State { + mds: Some(Mds::Vmwerv), + taa: Some(Taa::UcodeNeeded), + mmio: Some(Mmio::UcodeNeeded), + ..all + } + ); + assert_eq!( + state(&decide(&edx7(CPUID_7_0_EDX_FLUSH_L1D))), + State { mmio: Some(Mmio::UcodeNeeded), ..all } + ); + let no_srbds_ctrl = edx7(CPUID_7_0_EDX_SRBDS_CTRL); + assert_eq!(state(&decide(&no_srbds_ctrl)), State { srbds: Some(Srbds::UcodeNeeded), ..all }); + assert_eq!(line(&no_srbds_ctrl, Vuln::Srbds), "Vulnerable: No microcode"); + let rdseed = Facts { cpuid_1_ecx: COMETLAKE.cpuid_1_ecx & !CPUID_1_ECX_RDRAND, ..COMETLAKE }; + assert_eq!(decide(&rdseed).srbds, Some(Srbds::Full)); + let rdrand = Facts { cpuid_7_0_ebx: COMETLAKE.cpuid_7_0_ebx & !CPUID_7_0_EBX_RDSEED, ..COMETLAKE }; + assert_eq!(decide(&rdrand).srbds, Some(Srbds::Full)); + let neither = Facts { cpuid_7_0_ebx: rdrand.cpuid_7_0_ebx, ..rdseed }; + assert_eq!(decide(&neither).srbds, None); + assert_eq!(line(&neither, Vuln::Srbds), "Not affected"); +} + +/// [`COMETLAKE`] as a guest: BHI from the hypervisor bit (`common.c:1533-1536`) +/// takes the SW loop under IBRS (`bugs.c:1851-1857`), VMSCAPE is bare-metal +/// only (`common.c:1545-1548`), and TAA's host SMT state is unknown +/// (`bugs.c:3138-3141`). +#[test] +fn a_comet_lake_guest_cannot_know_its_hosts_smt() { + let guest = Facts { cpuid_1_ecx: COMETLAKE.cpuid_1_ecx | CPUID_1_ECX_HYPERVISOR, ..COMETLAKE }; + assert_eq!( + state(&decide(&guest)), + State { + bhi: Some(Bhi::SwLoop), + mds: Some(Mds::Full), + taa: Some(Taa::Verw), + mmio: Some(Mmio::Verw), + srbds: Some(Srbds::Hypervisor), + gds: Some(Gds::Hypervisor), + vmscape: Vmscape::None, + clear_cpu_buf: true, + ..COMETLAKE_STATE + } + ); + assert_eq!( + line(&guest, Vuln::TsxAsyncAbort), + "Mitigation: Clear CPU buffers; SMT Host state unknown" + ); +} + +/// TAA without RTM in CPUID comes from `TSX_CTRL_MSR` (`common.c:1467-1470`) +/// and is "TSX disabled" (`bugs.c:335-338`). Without RTM and without `TSX_CTRL` +/// there is no TAA, and SRBDS is "TSX disabled" on an `MDS_NO` CPU without MMIO +/// (`bugs.c:688-690`) and the microcode's otherwise (`bugs.c:693-695`). +#[test] +fn rtm_absent_from_cpuid_is_rtm_off() { + let no_rtm = |arch| Facts { + cpuid_7_0_ebx: COMETLAKE.cpuid_7_0_ebx & !CPUID_7_0_EBX_RTM, + arch_capabilities: arch, + ..COMETLAKE + }; + assert_eq!( + state(&decide(&no_rtm(ARCH_CAP_MDS_NO | ARCH_CAP_TSX_CTRL_MSR))), + State { + tsx: Tsx::Disable, + taa: Some(Taa::TsxDisabled), + mmio: Some(Mmio::UcodeNeeded), + srbds: Some(Srbds::Full), + ..COMETLAKE_STATE + } + ); + let immune = ARCH_CAP_FBSDP_NO | ARCH_CAP_PSDP_NO | ARCH_CAP_SBDR_SSDP_NO; + assert_eq!( + state(&decide(&no_rtm(ARCH_CAP_MDS_NO | immune))), + State { srbds: Some(Srbds::TsxOff), ..COMETLAKE_STATE } + ); + assert_eq!( + state(&decide(&no_rtm(immune))), + State { + mds: Some(Mds::Full), + srbds: Some(Srbds::Full), + clear_cpu_buf: true, + ..COMETLAKE_STATE + } + ); +} + +/// The bad-microcode rows key model and stepping together (`intel.c:177-178`): +/// KABYLAKE stepping 8 has no row, and Comet Lake stepping 0xA is not +/// KABYLAKE's stepping 0xA. +#[test] +fn a_bad_microcode_row_is_its_model_and_its_stepping() { + for signature in [0x0009_06e8, 0x000a_065a] { + let d = decide(&Facts { signature, microcode: 0x80, ..KABYLAKE }); + assert_eq!((d.ibpb, d.ssb), (true, Ssb::Prctl), "{signature:#x}"); + } +} + +/// An unknown vendor's family 6 matches no row (`match.c:36-57`): `RSBA` gives +/// RETBleed (`common.c:1502-1505`), which neither the AMD nor the Intel +/// selection takes up (`bugs.c:1101-1106,1175`), and IBRS guards firmware +/// (`bugs.c:2038-2041`). Its MMIO is unknown (`common.c:1498-1499`). +#[test] +fn an_unknown_vendors_retbleed_is_unmitigated() { + let unknown = Facts { + vendor: Vendor::from_id(b"GenuineIotel"), + signature: 0x0009_06ea, + microcode: 0, + cpuid_1_ecx: 0, + cpuid_7_0_ebx: 0, + cpuid_7_0_edx: CPUID_7_0_EDX_SPEC_CTRL | CPUID_7_0_EDX_ARCH_CAPABILITIES, + cpuid_7_2_edx: 0, + cpuid_8000_0008_ebx: 0, + cpuid_8000_0021_eax: 0, + cpuid_8000_0021_ecx: 0, + arch_capabilities: ARCH_CAP_RSBA, + mcu_opt_ctrl: None, + feat_ctl: None, + ls_cfg_readable: None, + sbpb_write_accepted: None, + smt: false, + }; + assert_eq!( + state(&decide(&unknown)), + State { + spectre_v2: Some(SpectreV2::Retpoline), + ibpb: true, + ibrs_fw: true, + mds: Some(Mds::Vmwerv), + clear_cpu_buf: true, + ..NONE + } + ); + assert_eq!(line(&unknown, Vuln::Retbleed), "Vulnerable"); + assert_eq!(line(&unknown, Vuln::MmioStaleData), "Unknown: No mitigations"); +} + +/// [`ZEN1`] with `BTC_NO` has no RETBleed (`common.c:1502-1505`), so IBRS, not +/// IBPB, guards firmware (`bugs.c:2028-2041`); with `AMD_SSB_NO` it has no SSB +/// (`common.c:1431-1434`). Without AMD_IBPB there is no `IBPB_BRTYPE` +/// (`amd.c:800-801`), so safe RET lacks its microcode (`bugs.c:2735-2736`), +/// VMSCAPE has no IBPB (`bugs.c:2851-2855`), and IBRS guards firmware. +#[test] +fn zen1_by_btc_no_ssb_no_and_ibpb() { + let ebx8 = |set, clear: u32| Facts { + cpuid_8000_0008_ebx: (ZEN1.cpuid_8000_0008_ebx | set) & !clear, + ..ZEN1 + }; + let expected = State { + spectre_v2: Some(SpectreV2::Retpoline), + ibpb: true, + ibrs_fw: true, + ssb: Ssb::Prctl, + vmscape: Vmscape::IbpbExitToUser, + srso: Some(Srso::SafeRet), + ..NONE + }; + let btc_no = ebx8(CPUID_8000_0008_EBX_BTC_NO, 0); + assert_eq!(state(&decide(&btc_no)), expected); + assert_eq!(line(&btc_no, Vuln::Retbleed), "Not affected"); + assert_eq!(line(&ebx8(CPUID_8000_0008_EBX_AMD_SSB_NO, 0), Vuln::SpecStoreBypass), "Not affected"); + let no_ibpb = ebx8(0, CPUID_8000_0008_EBX_AMD_IBPB); + assert_eq!( + state(&decide(&no_ibpb)), + State { + ibpb: false, + retbleed: Retbleed::Unret, + vmscape: Vmscape::None, + srso: Some(Srso::SafeRetUcodeNeeded), + ..expected + } + ); + assert_eq!( + line(&no_ibpb, Vuln::SpectreV2), + "Mitigation: Retpolines; IBRS_FW; STIBP: disabled; RSB filling; PBRSB-eIBRS: Not affected; \ + BHI: Not affected" + ); + assert_eq!(line(&no_ibpb, Vuln::Vmscape), "Vulnerable"); } +/// A Rome (Zen2) guest: `early_init_amd` sets no `IBPB_BRTYPE` under a +/// hypervisor (`amd.c:799`), so SRSO is safe RET without its microcode even +/// without SMT (`bugs.c:2687-2703,2735-2736`); `VIRT_SSBD` alone gives SSBD +/// (`common.c:990-992`) and leaves `LS_CFG` unprobed (`amd.c:576-578`). #[test] -fn hygon_is_refused() { - let hygon = Facts { vendor: Vendor::from_id(b"HygonGenuine"), ..TCG }; - assert_eq!(decide(&hygon).err(), Some(Refused::Hygon)); +fn a_zen2_guest_has_no_ibpb_brtype() { + let rome = Facts { + signature: 0x0083_0f10, + cpuid_8000_0021_eax: 0, + ..MILAN_GUEST + }; + assert_eq!( + state(&decide(&rome)), + State { + spectre_v2: Some(SpectreV2::Retpoline), + ibpb: true, + retbleed: Retbleed::Unret, + ssb: Ssb::Prctl, + srso: Some(Srso::SafeRetUcodeNeeded), + ..NONE + } + ); + assert_eq!(line(&rome, Vuln::SpecRstackOverflow), "Vulnerable: Safe RET, no microcode"); + let virt_ssbd = Facts { + cpuid_8000_0008_ebx: (rome.cpuid_8000_0008_ebx & !CPUID_8000_0008_EBX_AMD_SSBD) + | CPUID_8000_0008_EBX_VIRT_SSBD, + ..rome + }; + assert_eq!(decide(&virt_ssbd).ssb, Ssb::Prctl); } + +/// A native family 0x19 with `SRSO_NO` keeps SBPB (`bugs.c:2674-2678`); with +/// `IBPB_BRTYPE` enumerated it is not probed (`amd.c:799`) and has safe RET's +/// microcode (`bugs.c:2672,2733-2734`). +#[test] +fn zen3_by_srso_no_and_an_enumerated_ibpb_brtype() { + let srso_no = Facts { + cpuid_8000_0021_eax: MILAN_GUEST.cpuid_8000_0021_eax | CPUID_8000_0021_EAX_SRSO_NO, + ..native_milan(0x0a00_11d7, true) + }; + assert_eq!( + state(&decide(&srso_no)), + State { sbpb: true, tsa: Some(Tsa::Full), clear_cpu_buf: true, ..NATIVE_MILAN } + ); + assert_eq!(line(&srso_no, Vuln::SpecRstackOverflow), "Not affected"); + let brtype = Facts { + cpuid_8000_0021_eax: MILAN_GUEST.cpuid_8000_0021_eax | CPUID_8000_0021_EAX_IBPB_BRTYPE, + sbpb_write_accepted: None, + ..native_milan(0x0a00_11d7, false) + }; + assert_eq!( + state(&decide(&brtype)), + State { srso: Some(Srso::SafeRet), tsa: Some(Tsa::Full), clear_cpu_buf: true, ..NATIVE_MILAN } + ); +} + +/// A native Turin with SMT: AutoIBRS leaves STIBP selectable +/// (`bugs.c:1564-1567`), and AMD_STIBP_ALWAYS_ON makes it strict +/// (`bugs.c:1575-1577`); `stibp_state` prints it (`bugs.c:3176-3186`). +#[test] +fn autoibrs_leaves_stibp_to_select() { + let turin = Facts { + cpuid_1_ecx: TURIN_GUEST.cpuid_1_ecx & !CPUID_1_ECX_HYPERVISOR, + smt: true, + ..TURIN_GUEST + }; + assert_eq!( + state(&decide(&turin)), + State { + spectre_v2: Some(SpectreV2::Eibrs), + ibpb: true, + stibp: Stibp::StrictPreferred, + ssb: Ssb::Prctl, + sbpb: true, + efer_autoibrs: true, + spec_ctrl: SPEC_CTRL_STIBP, + ..NONE + } + ); + assert_eq!( + line(&turin, Vuln::SpectreV2), + "Mitigation: Enhanced / Automatic IBRS; IBPB: conditional; STIBP: always-on; \ + PBRSB-eIBRS: Not affected; BHI: Not affected" + ); +} + +/// `VERW_CLEAR` enumerated to a guest is TSA's microcode (`bugs.c:2928-2929`, +/// `tsa_strings` 2892). +#[test] +fn a_guest_with_verw_clear_mitigates_tsa() { + let guest = Facts { + cpuid_8000_0021_eax: MILAN_GUEST.cpuid_8000_0021_eax | CPUID_8000_0021_EAX_VERW_CLEAR, + ..MILAN_GUEST + }; + assert_eq!(decide(&guest).tsa, Some(Tsa::Full)); + assert_eq!(line(&guest, Vuln::Tsa), "Mitigation: Clear CPU buffers"); +} + +/// Without `RRSBA` there is nothing to disable (`bugs.c:1728-1731`), so a +/// retpoline guest leaves BHI to retpolines without `RRSBA_DIS_S`. +#[test] +fn a_retpoline_guest_without_rrsba_leaves_bhi_to_retpolines() { + let guest = Facts { + cpuid_1_ecx: T14.cpuid_1_ecx | CPUID_1_ECX_HYPERVISOR, + arch_capabilities: T14.arch_capabilities & !ARCH_CAP_IBRS_ALL, + ..T14 + }; + let d = decide(&guest); + assert_eq!((d.bhi, d.spec_ctrl), (Some(Bhi::Retpoline), 0)); +} + +/// The two probes are AMD's and Hygon's: an Intel family 0x17 or 0x19 is +/// probed for neither. +#[test] +fn only_amd_and_hygon_are_probed() { + for signature in [0x0080_0f00, 0x00a0_0f00] { + let d = decide(&Facts { signature, ..TCG_INTEL }); + assert_eq!(d.spectre_v2, Some(SpectreV2::Retpoline), "{signature:#x}"); + } +} + +/// [`TCG`]'s facts under Intel's name. +const TCG_INTEL: Facts = Facts { vendor: Vendor::from_id(b"GenuineIntel"), ..TCG }; From e767338ca02fe695521db2aabac94122ed1f1e44 Mon Sep 17 00:00:00 2001 From: japabu Date: Tue, 29 Sep 2026 15:58:00 +0200 Subject: [PATCH 6/6] toyos-cpuvuln: tests for a TSA model nibble of 8+, COMETLAKE_L stepping 0 and Hygon without SMT Three tests, expected values from Ubuntu-6.8.0-142.142: - Zen3 0x00a00f82: microcode 0x0a00820d is Tsa::Full, 0x0a00820c UcodeNeeded (amd.c:479-490,621). - COMETLAKE_L 0x000a0660 has no SRBDS or GDS, 0x000a0661 has both (common.c:1310-1311,1480-1483,1521-1523). - A Dhyana without SMT and with IBPB_BRTYPE is Srso::SmtDisabled (bugs.c:2689-2691,3284-3285). Each of five mutations exits 101 on its test. The owed S0 capture reads MSR 0x3A. Cargo.toml's description no longer names another crate. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01U6SVYFkdvV2t38KzNrESxs --- ...t14-and-the-tcg-model-are-not-committed.md | 2 +- toyos-cpuvuln/Cargo.toml | 2 +- toyos-cpuvuln/src/tests.rs | 42 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/issues/hardware/linuxs-readings-of-the-t14-and-the-tcg-model-are-not-committed.md b/issues/hardware/linuxs-readings-of-the-t14-and-the-tcg-model-are-not-committed.md index 67f1be92423..d1b5db93f37 100644 --- a/issues/hardware/linuxs-readings-of-the-t14-and-the-tcg-model-are-not-committed.md +++ b/issues/hardware/linuxs-readings-of-the-t14-and-the-tcg-model-are-not-committed.md @@ -18,7 +18,7 @@ Pull request #601 holds it. Ubuntu leaves the T14 only after this issue and close. **Exit**: both captures committed, and no Linux image; before the wipe the -T14's adds CPUID 5, 0x19 and 0x80000001, MSR 0xCF, the split-lock line, and +T14's adds CPUID 5, 0x19 and 0x80000001, MSR 0xCF, MSR 0x3A (`rdmsr -a 0x3a`, IA32_FEAT_CTL), the split-lock line, and the config's `X86_KERNEL_IBT` and `X86_INTEL_MEMORY_PROTECTION_KEYS`. **Mutation**: a config hash off by one byte is refused. **Oracle**: that Linux. diff --git a/toyos-cpuvuln/Cargo.toml b/toyos-cpuvuln/Cargo.toml index ae53301094c..77f5b8f4f14 100644 --- a/toyos-cpuvuln/Cargo.toml +++ b/toyos-cpuvuln/Cargo.toml @@ -1,4 +1,4 @@ -# A member of the host workspace (root `Cargo.toml`), like toyos-userbound: +# 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] diff --git a/toyos-cpuvuln/src/tests.rs b/toyos-cpuvuln/src/tests.rs index d7bf6bb13b3..ae728c5e091 100644 --- a/toyos-cpuvuln/src/tests.rs +++ b/toyos-cpuvuln/src/tests.rs @@ -345,6 +345,18 @@ fn a_blacklist_row_matches_only_its_steppings() { assert_eq!(at(0xC), "Mitigation: Aligned branch/return thunks"); } +/// COMETLAKE_L stepping 0 has its own row without MMIO_SBDS and GDS, ahead of +/// its any-stepping row that carries both (`common.c:1310-1311`); SRBDS and GDS +/// are forced by those flags (`common.c:1480-1483`, `1521-1523`). +#[test] +fn a_blacklist_row_for_stepping_zero_stands_before_its_any_stepping_row() { + let at = |stepping: u32| decide(&Facts { signature: 0x000a_0660 | stepping, ..COMETLAKE }); + let d = at(0); + assert_eq!((d.srbds, d.gds), (None, None)); + let d = at(1); + assert_eq!((d.srbds, d.gds), (Some(Srbds::Full), Some(Gds::UcodeNeeded))); +} + /// The first matching row decides: SKYLAKE_X's steppings 0 to 5 row, without /// ITS, stands before its any-stepping row, with it. #[test] @@ -692,6 +704,16 @@ fn zen4_reads_its_row_of_the_tsa_microcode_table() { ); } +/// A Zen3 model 0x08 stepping 2 (signature `0x00a00f82`) keys `amd_check_tsa_microcode`'s +/// row 0xa0082 by the whole model byte (`amd.c:479-490`, `ZEN3` from `bsp_init_amd`, `amd.c:621`): +/// microcode 0x0a00820d and up is mitigated, below it is missing. +#[test] +fn a_tsa_row_keyed_by_a_model_nibble_of_8_or_more_is_read() { + let at = |microcode| Facts { signature: 0x00a0_0f82, ..native_milan(microcode, true) }; + assert_eq!(decide(&at(0x0a00_820d)).tsa, Some(Tsa::Full)); + assert_eq!(decide(&at(0x0a00_820c)).tsa, Some(Tsa::UcodeNeeded)); +} + /// An AMD CPU is TSA-affected unless it reports both `TSA_SQ_NO` and /// `TSA_L1_NO` (`common.c:1555-1561`), CPUID.0x80000021:ECX bits 1 and 2 /// (`scattered.c:52-53`). A guest's TSA microcode is missing @@ -1190,6 +1212,26 @@ fn hygon_decides_as_linux_does() { assert_eq!(state(&decide(&dhyana(false))), State { ssb: Ssb::None, ..expected }); } +/// Family 0x18 is below 0x19, so a Dhyana without SMT has SRSO ruled out by +/// `SRSO_NO` once `IBPB_BRTYPE` is set (`bugs.c:2689-2691`), shown as "SMT +/// disabled" (`bugs.c:3284-3285`). +#[test] +fn hygon_without_smt_and_with_ibpb_brtype_has_srso_smt_disabled() { + let dhyana = Facts { + vendor: Vendor::from_id(b"HygonGenuine"), + signature: 0x0090_0f01, + cpuid_8000_0008_ebx: CPUID_8000_0008_EBX_AMD_IBPB + | CPUID_8000_0008_EBX_AMD_IBRS + | CPUID_8000_0008_EBX_AMD_STIBP, + cpuid_8000_0021_eax: CPUID_8000_0021_EAX_IBPB_BRTYPE, + ls_cfg_readable: Some(true), + smt: false, + ..ZEN1 + }; + assert_eq!(decide(&dhyana).srso, Some(Srso::SmtDisabled)); + assert_eq!(line(&dhyana, Vuln::SpecRstackOverflow), "Mitigation: SMT disabled"); +} + #[test] #[should_panic(expected = "MSR_AMD64_LS_CFG is probed exactly where")] fn a_hygon_cpu_without_its_ls_cfg_probe_is_a_contradiction() {