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/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/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/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/toyos-cpuvuln/Cargo.toml b/toyos-cpuvuln/Cargo.toml new file mode 100644 index 00000000000..77f5b8f4f14 --- /dev/null +++ b/toyos-cpuvuln/Cargo.toml @@ -0,0 +1,9 @@ +# A member of the host workspace (root `Cargo.toml`): +# `no_std` so the kernel can depend on it by path, and its tests run on the host. + +[package] +name = "toyos-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/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/t14.txt b/toyos-cpuvuln/fixtures/t14.txt new file mode 100644 index 00000000000..15ba1120f37 --- /dev/null +++ b/toyos-cpuvuln/fixtures/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/tcg.txt b/toyos-cpuvuln/fixtures/tcg.txt new file mode 100644 index 00000000000..1019ef0e0c1 --- /dev/null +++ b/toyos-cpuvuln/fixtures/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..51f544b41af --- /dev/null +++ b/toyos-cpuvuln/src/lib.rs @@ -0,0 +1,1092 @@ +//! 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`, `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`, and `CONFIG_KVM_INTEL` is `m` +//! (`debian.master/config/annotations:3348,3319,3339,8048,411,7262`). +//! +//! 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. + +#![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, SRSO, TSA, VMSCAPE, +}; + +/// The vendors Linux's tables name, by CPUID.0's identification string +/// (`get_cpu_vendor`, `common.c:912-936`, 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, + /// 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, + /// `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 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 + /// (`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, +} + +// 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; +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_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,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; +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; +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; +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 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, +} + +/// 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-245`) 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` where the CPU has Spectre v2 (`bugs.c:1860-2045`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SpectreV2 { + Retpoline, + Eibrs, + Ibrs, +} + +/// 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 { + /// `SPEC_CTRL.BHI_DIS_S`. + BhiDisS, + /// `clear_bhb_loop` on syscall entry and VM exit. + SwLoop, + /// Retpolines with RRSBA disabled. + Retpoline, +} + +/// `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-1194`). +#[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` where the CPU has MDS (`bugs.c:266-283`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Mds { + Full, + Vmwerv, +} + +/// `taa_mitigation` where the CPU has TAA (`bugs.c:327-382`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Taa { + UcodeNeeded, + Verw, + TsxDisabled, +} + +/// `mmio_mitigation` where the CPU has MMIO Stale Data (`bugs.c:424-478`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Mmio { + UcodeNeeded, + Verw, +} + +/// `rfds_mitigation` where the CPU has RFDS (`bugs.c:520-533`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Rfds { + Verw, + UcodeNeeded, +} + +/// `srbds_mitigation` where the CPU has SRBDS (`bugs.c:678-700`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Srbds { + UcodeNeeded, + Full, + TsxOff, + Hypervisor, +} + +/// `gds_mitigation` where the CPU has GDS (`bugs.c:814-867`). +#[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, +} + +/// `vmscape_mitigation` (`bugs.c:2849-2869`). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +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 +/// 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, + srso: bool, + tsa: bool, + meltdown: bool, + l1tf: bool, +} + +/// 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 the `LS_CFG_SSBD` probe. +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: 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: Option, + pub taa: Option, + pub mmio: Option, + pub rfds: Option, + pub srbds: Option, + pub gds: Option, + /// 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, + /// `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`. + 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, + /// `X86_FEATURE_MSR_IA32_FEAT_CTL` and `X86_FEATURE_VMX`. + vmx: bool, +} + +/// `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 { + 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; + // `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) + && 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)); + 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`). 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); + 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`), 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 { + return true; + } + if f.cpuid_7_2_edx & CPUID_7_2_EDX_RRSBA_CTRL == 0 { + return false; + } + *spec_ctrl |= SPEC_CTRL_RRSBA_DIS_S; + 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`, +/// `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); + 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]", + 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" + ); + 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; + 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_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!( + 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 mut caps = Caps { + hypervisor, + 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`). 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); + // `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; + 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; + + // `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 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 = None; + let mut bhi = None; + let mut ibrs_fw = false; + if bugs.spectre_v2 { + 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!(mode, SpectreV2::Eibrs | SpectreV2::Ibrs); + if ibrs_mode { + if caps.autoibrs { + efer_autoibrs = true; + } else { + spec_ctrl |= SPEC_CTRL_IBRS; + } + } + 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 = 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 && amd_or_hygon) && caps.ibrs && !ibrs_mode; + spectre_v2 = Some(mode); + } + + // `retbleed_select_mitigation` (`bugs.c:1054-1194`). + let retbleed = if !bugs.retbleed { + Retbleed::None + } else if id.vendor == Vendor::Intel { + match spectre_v2 { + Some(SpectreV2::Ibrs) => Retbleed::Ibrs, + Some(SpectreV2::Eibrs) => Retbleed::Eibrs, + Some(SpectreV2::Retpoline) | None => Retbleed::None, + } + } 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`). 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 + || (spectre_v2 == Some(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-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; + let mut clear_cpu_buf = false; + let mds = bugs.mds.then(|| { + clear_cpu_buf = true; + if md_clear { Mds::Full } else { Mds::Vmwerv } + }); + // 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 { + clear_cpu_buf = true; + if md_clear && arch & ARCH_CAP_MDS_NO == 0 { Taa::Verw } else { Taa::UcodeNeeded } + } + }); + // `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 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 + } + }); + + // `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; + + // `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 + }; + + Decision { + tsx, + spectre_v2, + bhi, + ibpb, + ibrs_fw, + stibp, + retbleed, + ssb, + mds, + taa, + mmio, + rfds, + srbds, + gds, + its: bugs.its, + vmscape, + srso, + tsa, + sbpb, + clear_cpu_buf, + efer_autoibrs, + spec_ctrl, + bugs, + hypervisor, + autoibrs: caps.autoibrs, + smt: f.smt, + vmx, + } +} + +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), + _ => 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; + 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", + })?; + 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 !(mode == SpectreV2::Eibrs && !d.autoibrs) { + f.write_str(match d.stibp { + Stibp::None => "; STIBP: disabled", + Stibp::StrictPreferred => "; STIBP: always-on", + Stibp::Prctl => "; STIBP: conditional", + })?; + } + // `RSB_CTXSW` (`bugs.c:1741-1789`). + if matches!(mode, SpectreV2::Retpoline | SpectreV2::Ibrs) { + f.write_str("; RSB filling")?; + } + // `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 { + 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", + }) + } +} + +impl fmt::Display for Line<'_> { + /// `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; + 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, + Vuln::SpecRstackOverflow => b.srso, + Vuln::Tsa => b.tsa, + }; + 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.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 { + f.write_str(if d.smt { "; SMT mitigated" } else { "; SMT disabled" }) + } else { + write!(f, "; SMT {}", self.smt()) + } + } + Vuln::TsxAsyncAbort => { + 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 => return f.write_str("Mitigation: TSX disabled"), + })?; + 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", + Srbds::TsxOff => "Mitigation: TSX disabled", + Srbds::Hypervisor => "Unknown: Dependent on hypervisor status", + }), + Vuln::MmioStaleData => { + let Some(mmio) = d.mmio else { + return f.write_str("Unknown: No mitigations"); + }; + f.write_str(match mmio { + 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 and Hygon 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.expect("set with the bug") { + Rfds::Verw => "Mitigation: Clear Register File", + Rfds::UcodeNeeded => "Vulnerable: No microcode", + }), + 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", + 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 => unreachable!("l1tf is unmodelled wherever affected"), + } + } +} diff --git a/toyos-cpuvuln/src/table.rs b/toyos-cpuvuln/src/table.rs new file mode 100644 index 00000000000..ed284e7b698 --- /dev/null +++ b/toyos-cpuvuln/src/table.rs @@ -0,0 +1,346 @@ +//! 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. +//! +//! 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-182`) over `spectre_bad_microcodes` +/// (`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), + (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) +} + +/// `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`), 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), + (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), + ]; + // `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 new file mode 100644 index 00000000000..ae728c5e091 --- /dev/null +++ b/toyos-cpuvuln/src/tests.rs @@ -0,0 +1,1713 @@ +//! Facts in, Linux's lines and selection out, the lines judged against +//! `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::{String, ToString}; +use std::vec::Vec; + +use super::*; + +const PREFIX: &str = "/sys/devices/system/cpu/vulnerabilities/"; + +/// 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: 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, + 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`. + smt: true, +}; + +/// 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: 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, + feat_ctl: 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. +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, + feat_ctl: 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, + feat_ctl: 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: Option, + bhi: Option, + ibpb: bool, + ibrs_fw: bool, + stibp: Stibp, + retbleed: Retbleed, + ssb: Ssb, + mds: Option, + taa: Option, + mmio: Option, + rfds: Option, + srbds: Option, + gds: Option, + its: bool, + 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: _, + vmx: _, + } = *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: None, + bhi: None, + ibpb: false, + ibrs_fw: false, + stibp: Stibp::None, + retbleed: Retbleed::None, + ssb: Ssb::None, + mds: None, + taa: None, + mmio: None, + rfds: None, + srbds: None, + gds: None, + its: false, + 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) -> Decision { + let d = decide(facts); + 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"); + d +} + +fn line(facts: &Facts, v: Vuln) -> String { + decide(facts).line(v).expect("modelled").to_string() +} + +#[test] +fn the_t14s_facts_give_its_lines_and_its_spec_ctrl() { + assert_eq!(Ident::new(T14.vendor, T14.signature).model, 0x8C); + 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: Some(SpectreV2::Eibrs), + bhi: Some(Bhi::SwLoop), + ibpb: true, + ssb: Ssb::Prctl, + gds: Some(Gds::Full), + its: true, + 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_signature_gives_its_lines() { + let id = Ident::new(TCG.vendor, TCG.signature); + assert_eq!((id.family, id.model, id.stepping), (15, 107, 1)); + 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: Some(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, 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 }); + 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")); + // 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).as_deref(), Ok("KVM: Mitigation: VMX disabled")); + assert_eq!(line(Vuln::TsxAsyncAbort).as_deref(), Ok("Not affected")); + assert_eq!( + state(&d), + State { + spectre_v2: Some(SpectreV2::Retpoline), + ibpb: true, + ibrs_fw: true, + stibp: Stibp::Prctl, + ssb: Ssb::Prctl, + mds: Some(Mds::Full), + gds: Some(Gds::UcodeNeeded), + its: true, + 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"); +} + +/// 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] +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 (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 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 { + microcode: 0x80, + cpuid_7_0_edx: CPUID_7_0_EDX_SPEC_CTRL_SSBD, + cpuid_8000_0008_ebx: ebx8, + ..KABYLAKE + }) + .ssb + }; + 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] +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"); +} + +/// `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() { + 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] +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: Some(SpectreV2::Eibrs), + ibpb: true, + 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)); + 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)); + 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: 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 + } + ); +} + +/// 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 { + Facts { + microcode, + cpuid_1_ecx: MILAN_GUEST.cpuid_1_ecx & !CPUID_1_ECX_HYPERVISOR, + sbpb_write_accepted: Some(sbpb), + ..MILAN_GUEST + } +} + +/// What [`native_milan`] selects whatever its microcode and probe. +const NATIVE_MILAN: State = State { + spectre_v2: Some(SpectreV2::Retpoline), + ibpb: true, + 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)); + assert_eq!( + state(&d), + State { + srso: Some(Srso::SafeRetUcodeNeeded), + tsa: Some(Tsa::UcodeNeeded), + ..NATIVE_MILAN + } + ); + 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 } + ); + 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] +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)); + 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)); + 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" + ); +} + +/// 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 +/// (`bugs.c:2928-2929`, `tsa_strings` 2889). +#[test] +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); + 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)); + 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)); + 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); + (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() + ) + ); +} + +/// [`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_mitigations_or_its_hosts_smt() { + let guest = Facts { + 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, + feat_ctl: None, + ls_cfg_readable: None, + sbpb_write_accepted: None, + smt, + }; + 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 }); +} + +/// 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() { + 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() { + 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 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 };