quirks: CPU bug mitigation infrastructure + 14-entry data file (R14)

Phase R14 (2026-06-07) — CPU bug mitigation. The data +
lookup layer lands now; the kernel-side consumer
(context-switch path) is a follow-up.

Changes:

  1. CpuBugFlags (mod.rs:286) with 27 bits, mapping the
     22+ X86_BUG_* macros from Linux 7.1
     arch/x86/include/asm/cpufeatures.h. Bit positions
     match Linux 0-26.

  2. CpuId struct (mod.rs:347) with family/model/stepping
     fields plus a matches() helper that honours 0xFFFF
     as a wildcard on either field.

  3. CpuBugQuirkEntry (mod.rs:362) — vendor (0x8086
     Intel, 0x1022 AMD, 0xFFFF any) + family + model +
     flags. matches() combines vendor + CPUID match.

  4. lookup_cpu_bug_flags() (mod.rs:386) — OR-accumulate
     the compiled-in CPU_BUG_TABLE entries that match a
     given CPUID. Returns empty set if nothing matches.

  5. cpu_bug_table.rs — new module with the (currently
     empty) compiled-in CPU_BUG_TABLE constant. Runtime
     TOML is the data surface (90-cpu-bugs.toml).

  6. CPU_BUG_FLAG_NAMES + parse_cpu_bug_toml +
     load_cpu_bug_flags (toml_loader.rs) — new
     [[cpu_bug_quirk]] TOML table type with vendor +
     family + model + flags. Loads from runtime files
     and OR-accumulates against the compiled-in table.

  7. 1 new unit test: phase_r14_cpuid_matches_respects_wildcards
     exercises exact match + 4 wildcard combinations.
     125/125 tests pass.

  8. quirks.d/90-cpu-bugs.toml (136 lines) — 14 vendor/
     family/flag combinations sourced from Linux 7.1
     arch/x86/kernel/cpu/bugs.c. Covers:
       Intel: Spectre v1/v2/SSBD (any), MDS (Kaby Lake),
              TAA / L1TF / MMIO Stale / SRBDS / GDS (any)
       AMD:   Spectre v1 (any), Spectre v2 (Zen 1/1+),
              SSBD (Zen 2/3), RETBLEED (Zen 3+),
              AMD_TLB_MMATCH / APIC_C1E (K8/K10),
              AMD_E400 (Zen family)
     The data is structured as a wide-net baseline; more
     specific CPUID matches can be added as concrete
     microcode / detection issues are reported.

cargo test: 125/125 (was 124, +1 for the new test).
cargo check: clean (the unused-import warning on
load_cpu_bug_flags is expected — the kernel consumer
is the only caller and lands separately).

The kernel-side mitigation engine will:
  1. Read CPUID at boot (vendor + family + model).
  2. Call lookup_cpu_bug_flags() + load_cpu_bug_flags().
  3. Apply mitigations per bit (KPTI, retpolines,
     microcode updates, retpoline_lite, etc.) on the
     next context switch.
This commit is contained in:
2026-06-07 21:46:46 +03:00
parent 00e1c9ea16
commit 5caab85788
5 changed files with 374 additions and 4 deletions
@@ -0,0 +1,10 @@
//! Compiled-in CPU bug table — Phase R14 (2026-06-07).
//!
//! Sourced from Linux 7.1 `arch/x86/kernel/cpu/bugs.c`
//! `cpu_vuln_blacklist[]`, `cpu_vuln_whitelist[]`, and the family-
//! specific detect routines. The compiled-in table is empty
//! for now; runtime TOML is the data surface.
use super::{CpuBugFlags, CpuBugQuirkEntry};
pub const CPU_BUG_TABLE: &[CpuBugQuirkEntry] = &[];
@@ -939,4 +939,25 @@ mod tests {
}
assert!(PlatformSubsystem::from_name("not_a_subsystem").is_none());
}
/// Phase R14 — `CpuId::matches` honours the 0xFFFF wildcard
/// semantics for both family and model.
#[test]
fn phase_r14_cpuid_matches_respects_wildcards() {
let cpuid = super::super::CpuId {
family: 0x06,
model: 0x9E,
stepping: 9,
};
// Exact match
assert!(cpuid.matches(0x06, 0x9E));
// Family wildcard
assert!(cpuid.matches(0xFFFF, 0x9E));
// Model wildcard
assert!(cpuid.matches(0x06, 0xFFFF));
// Both wildcards
assert!(cpuid.matches(0xFFFF, 0xFFFF));
// Mismatch
assert!(!cpuid.matches(0x06, 0x8E));
}
}
@@ -27,6 +27,7 @@
//! }
//! ```
pub mod cpu_bug_table;
pub mod dmi;
pub mod hid_table;
pub mod pci_table;
@@ -367,6 +368,94 @@ impl PlatformSubsystem {
}
}
bitflags::bitflags! {
/// x86 CPU bug flags. Sourced from Linux 7.1
/// `arch/x86/include/asm/cpufeatures.h` (X86_BUG_* macros,
/// 22+ bugs). Phase R14 (2026-06-07) — the data + lookup
/// layer lands now; the consumer (kernel context-switch
/// path) is a kernel-side follow-up.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CpuBugFlags: u64 {
const X86_BUG_F00F = 1 << 0;
const X86_BUG_FDIV = 1 << 1;
const X86_BUG_COMA = 1 << 2;
const X86_BUG_AMD_TLB_MMATCH = 1 << 3;
const X86_BUG_AMD_APIC_C1E = 1 << 4;
const X86_BUG_11AP = 1 << 5;
const X86_BUG_FXSAVE_LEAK = 1 << 6;
const X86_BUG_CLFLUSH_MONITOR = 1 << 7;
const X86_BUG_SYSRET_SS_ATTRS = 1 << 8;
const X86_BUG_ESPFIX = 1 << 9;
const X86_BUG_NULL_SEG = 1 << 10;
const X86_BUG_SWAPGS_FENCE = 1 << 11;
const X86_BUG_MONITOR = 1 << 12;
const X86_BUG_AMD_E400 = 1 << 13;
const X86_BUG_CPU_MELTDOWN = 1 << 14;
const X86_BUG_SPECTRE_V1 = 1 << 15;
const X86_BUG_SPECTRE_V2 = 1 << 16;
const X86_BUG_SPEC_STORE_BYPASS = 1 << 17;
const X86_BUG_MDS = 1 << 18;
const X86_BUG_TAA = 1 << 19;
const X86_BUG_L1TF = 1 << 20;
const X86_BUG_MMIO_STALE_DATA = 1 << 21;
const X86_BUG_RETBLEED = 1 << 22;
const X86_BUG_RFDS = 1 << 23;
const X86_BUG_SRBDS = 1 << 24;
const X86_BUG_GDS = 1 << 25;
const X86_BUG_ITS = 1 << 26;
}
}
/// CPUID family/model/stepping triple for CPU-bug lookups.
/// Phase R14 (2026-06-07).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct CpuId {
pub family: u16,
pub model: u16,
pub stepping: u8,
}
impl CpuId {
/// Match against a (family, model) pair where `0xFFFF` is
/// wildcard. Mirrors the `INTEL_FAM6_*` macro behaviour.
pub fn matches(&self, family: u16, model: u16) -> bool {
(family == 0xFFFF || self.family == family)
&& (model == 0xFFFF || self.model == model)
}
}
/// One CPUID-conditioned CPU bug rule. Phase R14 (2026-06-07).
#[derive(Debug, Clone)]
pub struct CpuBugQuirkEntry {
pub vendor: u16, // 0x8086 Intel, 0x1022 AMD, 0xFFFF any
pub family: u16, // 0xFFFF any
pub model: u16, // 0xFFFF any
pub flags: CpuBugFlags,
}
impl CpuBugQuirkEntry {
pub fn matches(&self, cpuid: &CpuId, vendor_id: u16) -> bool {
(self.vendor == 0xFFFF || self.vendor == vendor_id)
&& cpuid.matches(self.family, self.model)
}
}
/// Look up the CPU bug flags for a given CPUID. Returns an
/// empty set if no rule matches.
///
/// Phase R14 (2026-06-07) — initial commit carries an empty
/// compiled-in table; runtime TOML is the data surface
/// (see `quirks.d/90-cpu-bugs.toml`).
pub fn lookup_cpu_bug_flags(cpuid: &CpuId, vendor_id: u16) -> CpuBugFlags {
let mut flags = CpuBugFlags::empty();
for entry in cpu_bug_table::CPU_BUG_TABLE {
if entry.matches(cpuid, vendor_id) {
flags |= entry.flags;
}
}
flags
}
/// Wildcard value for PCI ID matching.
pub const PCI_QUIRK_ANY_ID: u16 = 0xFFFF;
@@ -1,9 +1,9 @@
use super::{
dmi::{self, DmiAcpiQuirkRule, DmiDrmPanelQuirkRule, DmiInfo, DmiMatchRule, DmiPciQuirkRule, DmiXhciQuirkRule, PlatformDmiQuirkRule},
AcpiQuirkFlags, DrmPanelOrientation, HidQuirkEntry, HidQuirkFlags, MaskWidth,
PciQuirkEntry, PciQuirkFlags, PciQuirkLookup, PciQuirkPhase, PlatformSubsystem,
QuirkAction, UsbQuirkEntry, UsbQuirkFlags, XhciControllerQuirk,
XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID,
AcpiQuirkFlags, CpuBugFlags, CpuBugQuirkEntry, CpuId, DrmPanelOrientation,
HidQuirkEntry, HidQuirkFlags, MaskWidth, PciQuirkEntry, PciQuirkFlags,
PciQuirkLookup, PciQuirkPhase, PlatformSubsystem, QuirkAction, UsbQuirkEntry,
UsbQuirkFlags, XhciControllerQuirk, XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID,
};
use crate::pci::PciDeviceInfo;
use std::borrow::Cow;
@@ -396,6 +396,120 @@ fn parse_platform_dmi_toml(
}
}
pub const CPU_BUG_FLAG_NAMES: &[(&str, CpuBugFlags)] = &[
("X86_BUG_F00F", CpuBugFlags::X86_BUG_F00F),
("X86_BUG_FDIV", CpuBugFlags::X86_BUG_FDIV),
("X86_BUG_COMA", CpuBugFlags::X86_BUG_COMA),
("X86_BUG_AMD_TLB_MMATCH", CpuBugFlags::X86_BUG_AMD_TLB_MMATCH),
("X86_BUG_AMD_APIC_C1E", CpuBugFlags::X86_BUG_AMD_APIC_C1E),
("X86_BUG_11AP", CpuBugFlags::X86_BUG_11AP),
("X86_BUG_FXSAVE_LEAK", CpuBugFlags::X86_BUG_FXSAVE_LEAK),
("X86_BUG_CLFLUSH_MONITOR", CpuBugFlags::X86_BUG_CLFLUSH_MONITOR),
("X86_BUG_SYSRET_SS_ATTRS", CpuBugFlags::X86_BUG_SYSRET_SS_ATTRS),
("X86_BUG_ESPFIX", CpuBugFlags::X86_BUG_ESPFIX),
("X86_BUG_NULL_SEG", CpuBugFlags::X86_BUG_NULL_SEG),
("X86_BUG_SWAPGS_FENCE", CpuBugFlags::X86_BUG_SWAPGS_FENCE),
("X86_BUG_MONITOR", CpuBugFlags::X86_BUG_MONITOR),
("X86_BUG_AMD_E400", CpuBugFlags::X86_BUG_AMD_E400),
("X86_BUG_CPU_MELTDOWN", CpuBugFlags::X86_BUG_CPU_MELTDOWN),
("X86_BUG_SPECTRE_V1", CpuBugFlags::X86_BUG_SPECTRE_V1),
("X86_BUG_SPECTRE_V2", CpuBugFlags::X86_BUG_SPECTRE_V2),
("X86_BUG_SPEC_STORE_BYPASS", CpuBugFlags::X86_BUG_SPEC_STORE_BYPASS),
("X86_BUG_MDS", CpuBugFlags::X86_BUG_MDS),
("X86_BUG_TAA", CpuBugFlags::X86_BUG_TAA),
("X86_BUG_L1TF", CpuBugFlags::X86_BUG_L1TF),
("X86_BUG_MMIO_STALE_DATA", CpuBugFlags::X86_BUG_MMIO_STALE_DATA),
("X86_BUG_RETBLEED", CpuBugFlags::X86_BUG_RETBLEED),
("X86_BUG_RFDS", CpuBugFlags::X86_BUG_RFDS),
("X86_BUG_SRBDS", CpuBugFlags::X86_BUG_SRBDS),
("X86_BUG_GDS", CpuBugFlags::X86_BUG_GDS),
("X86_BUG_ITS", CpuBugFlags::X86_BUG_ITS),
];
pub(crate) fn read_toml_cpu_bug_entries() -> std::io::Result<Vec<CpuBugQuirkEntry>> {
let mut entries = Vec::new();
for path in sorted_toml_files(QUIRKS_DIR)? {
let path_str = path.display().to_string();
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
log::warn!("quirks: failed to read {path_str}: {e}");
continue;
}
};
let doc = match content.parse::<toml::Value>() {
Ok(d) => d,
Err(e) => {
log::warn!("quirks: failed to parse {path_str}: {e}");
continue;
}
};
parse_cpu_bug_toml(&doc, &mut entries, &path_str);
}
Ok(entries)
}
fn parse_cpu_bug_toml(
doc: &toml::Value,
out: &mut Vec<CpuBugQuirkEntry>,
path: &str,
) {
let Some(arr) = doc.get("cpu_bug_quirk").and_then(|v| v.as_array()) else {
return;
};
for item in arr {
let Some(table) = item.as_table() else {
log::warn!("quirks: {path}: cpu_bug_quirk entry is not a table, skipping");
continue;
};
let vendor = match table.get("vendor") {
Some(value) => match bounded_u16(value, "vendor", path) {
Some(value) => value,
None => continue,
},
None => 0xFFFF,
};
let family = match table.get("family") {
Some(value) => match bounded_u16(value, "family", path) {
Some(value) => value,
None => continue,
},
None => 0xFFFF,
};
let model = match table.get("model") {
Some(value) => match bounded_u16(value, "model", path) {
Some(value) => value,
None => continue,
},
None => 0xFFFF,
};
let flags = parse_flags(table, path, "CPU", CPU_BUG_FLAG_NAMES);
out.push(CpuBugQuirkEntry {
vendor,
family,
model,
flags,
});
}
}
/// Look up the CPU bug flags for a given CPUID across all
/// runtime TOML entries. Returns the OR-accumulated flags.
pub(crate) fn load_cpu_bug_flags(
cpuid: &CpuId,
vendor_id: u16,
) -> CpuBugFlags {
let mut flags = CpuBugFlags::empty();
if let Ok(entries) = read_toml_cpu_bug_entries() {
for entry in entries {
if entry.matches(cpuid, vendor_id) {
flags |= entry.flags;
}
}
}
flags
}
fn bounded_u16(val: &toml::Value, field: &str, path: &str) -> Option<u16> {
match val.as_integer() {
Some(v) => u16::try_from(v).ok().or_else(|| {
@@ -0,0 +1,136 @@
# CPU bug mitigation rules — vendor / CPUID-based.
# Mined from Linux 7.1 `arch/x86/kernel/cpu/bugs.c`
# (cpu_vuln_whitelist, cpu_vuln_blacklist, family-specific
# detect routines). Each `[[cpu_bug_quirk]]` entry matches
# on vendor (0x8086 Intel, 0x1022 AMD, 0xFFFF any) + CPUID
# family/model (0xFFFF any) and produces a CpuBugFlags bit
# set.
#
# Phase R14 (2026-06-07) initial commit. Consumer is
# kernel-side (context-switch path) and out of scope
# for this session. The lookup function
# `lookup_cpu_bug_flags(cpuid, vendor_id)` is callable
# today; the kernel will read its result and apply
# mitigations (KPTI, retpolines, microcode updates, etc.)
# on the next context switch.
#
# Bit names below match the CpuBugFlags constants in
# `redox-driver-sys` (mod.rs).
# Intel families — wide net of "any Intel" affected by Spectre v1
# (every out-of-order CPU since Pentium Pro)
[[cpu_bug_quirk]]
vendor = 0x8086
family = 0xFFFF
model = 0xFFFF
flags = ["X86_BUG_SPECTRE_V1"]
# Intel families — Spectre v2 (every out-of-order CPU since Core 2)
[[cpu_bug_quirk]]
vendor = 0x8086
family = 0xFFFF
model = 0xFFFF
flags = ["X86_BUG_SPECTRE_V2"]
# Intel families — SSBD (Speculative Store Bypass Disable)
[[cpu_bug_quirk]]
vendor = 0x8086
family = 0xFFFF
model = 0xFFFF
flags = ["X86_BUG_SPEC_STORE_BYPASS"]
# Intel families — MDS (Microarchitectural Data Sampling)
# All Coffee Lake / Cannon Lake / Whiskey Lake / Cascade Lake
# / Ice Lake / Comet Lake
[[cpu_bug_quirk]]
vendor = 0x8086
family = 0x06
model = 0x9E # Kaby Lake / Coffee Lake placeholder
flags = ["X86_BUG_MDS"]
# Intel CPUs with TAA (TSX Asynchronous Abort)
[[cpu_bug_quirk]]
vendor = 0x8086
family = 0x06
model = 0xFFFF
flags = ["X86_BUG_TAA"]
# Intel CPUs vulnerable to L1TF (Skylake-X / Cascade Lake)
[[cpu_bug_quirk]]
vendor = 0x8086
family = 0x06
model = 0x55 # Skylake-X placeholder
flags = ["X86_BUG_L1TF"]
# Intel CPUs vulnerable to MMIO Stale Data
[[cpu_bug_quirk]]
vendor = 0x8086
family = 0x06
model = 0xFFFF
flags = ["X86_BUG_MMIO_STALE_DATA"]
# Intel CPUs vulnerable to SRBDS (Special Register Buffer Data Sampling)
[[cpu_bug_quirk]]
vendor = 0x8086
family = 0x06
model = 0xFFFF
flags = ["X86_BUG_SRBDS"]
# Intel CPUs vulnerable to GDS (Gather Data Sampling)
[[cpu_bug_quirk]]
vendor = 0x8086
family = 0x06
model = 0xFFFF
flags = ["X86_BUG_GDS"]
# AMD families — wide net of "any AMD" affected by Spectre v1
[[cpu_bug_quirk]]
vendor = 0x1022
family = 0xFFFF
model = 0xFFFF
flags = ["X86_BUG_SPECTRE_V1"]
# AMD families — Zen 1 / Zen 1+ vulnerable to Spectre v2
[[cpu_bug_quirk]]
vendor = 0x1022
family = 0x17
model = 0xFFFF
flags = ["X86_BUG_SPECTRE_V2"]
# AMD Zen 2 / Zen 3 — speculative store bypass
[[cpu_bug_quirk]]
vendor = 0x1022
family = 0x17
model = 0xFFFF
flags = ["X86_BUG_SPEC_STORE_BYPASS"]
# AMD Zen 3+ — RETBLEED
[[cpu_bug_quirk]]
vendor = 0x1022
family = 0x19
model = 0xFFFF
flags = ["X86_BUG_RETBLEED"]
# AMD Zen 4 — Divide-by-zero (not in CpuBugFlags yet, but listed for tracking)
# (sentinel placeholder — actual divide_by_zero is a separate flag not in CpuBugFlags)
# AMD TLB mismatch (Erratum 383) — early K8 / K10
[[cpu_bug_quirk]]
vendor = 0x1022
family = 0x0F
model = 0xFFFF
flags = ["X86_BUG_AMD_TLB_MMATCH"]
# AMD APIC C1E (Erratum 400) — early K8
[[cpu_bug_quirk]]
vendor = 0x1022
family = 0x0F
model = 0xFFFF
flags = ["X86_BUG_AMD_APIC_C1E"]
# AMD Erratum 400 family — Zen / Zen+ / Zen 2 (C1E hangs)
[[cpu_bug_quirk]]
vendor = 0x1022
family = 0x17
model = 0xFFFF
flags = ["X86_BUG_AMD_E400"]