quirks: ClocksourceQuirkFlags + 3-entry PMTMR blacklist (R15)

Phase R15 (2026-06-07) — timekeeping / TSC sync. The
data side lands now; TSC sync itself is algorithmic
(mark_tsc_unstable in the kernel) and is not represented.

Changes:

  1. ClocksourceQuirkFlags (mod.rs:415) with 4 bits:
     PMTMR_BLACKLIST, PMTMR_GRAYLIST, TSC_UNSTABLE,
     HPET_BROKEN. Only PMTMR bits fire today; TSC_UNSTABLE
     and HPET_BROKEN are reserved for future kernel-side
     use.

  2. ClocksourceQuirkEntry (mod.rs:445) — vendor / device /
     revision_lo / revision_hi / flags. matches() handles
     vendor / device wildcards (0xFFFF) and revision range
     (lo..=hi, with lo=0, hi=0xFF as the wildcard).

  3. CLOCKSOURCE_FLAG_NAMES + parse_clocksource_toml +
     load_clocksource_flags (toml_loader.rs) — new
     [[clocksource_quirk]] TOML table type with vendor +
     device + revision_lo + revision_hi + flags.

  4. 1 new unit test: phase_r15_clocksource_quirk_entry_matches
     exercises the range match + 4 wildcard combinations
     (vendor, device, revision out of range, revision
     wildcard). 126/126 tests pass.

  5. quirks.d/35-clocksource.toml (44 lines) — 3 entries
     sourced from Linux 7.1
     drivers/clocksource/acpi_pm.c:
       - Intel 82371AB_3 (PIIX4) 0x7113 rev 0..=2 → blacklist
       - Intel 82801DB_0 (ICH4) 0x24C0 → graylist
       - ServerWorks LE 0x0009 → graylist

cargo test: 126/126 (was 125, +1 for the new test).
cargo check: clean.

The kernel-side clocksource engine (R15 consumer) will
call load_clocksource_flags() at PMTMR probe time and
select / reject the PMTMR clocksource accordingly.
This commit is contained in:
2026-06-07 21:59:22 +03:00
parent f4ac668e78
commit 57778e7898
4 changed files with 235 additions and 4 deletions
@@ -960,4 +960,37 @@ mod tests {
// Mismatch // Mismatch
assert!(!cpuid.matches(0x06, 0x8E)); assert!(!cpuid.matches(0x06, 0x8E));
} }
/// Phase R15 — `ClocksourceQuirkEntry::matches` honours
/// vendor / device / revision wildcards.
#[test]
fn phase_r15_clocksource_quirk_entry_matches() {
use super::super::ClocksourceQuirkEntry;
// Range entry: vendor=0x8086, device=0x7110, revision in 0..=2
let entry = ClocksourceQuirkEntry {
vendor: 0x8086,
device: 0x7110,
revision_lo: 0,
revision_hi: 2,
flags: super::super::ClocksourceQuirkFlags::PMTMR_BLACKLIST,
};
assert!(entry.matches(0x8086, 0x7110, 0));
assert!(entry.matches(0x8086, 0x7110, 2));
// Out of range revision
assert!(!entry.matches(0x8086, 0x7110, 3));
// Wrong vendor
assert!(!entry.matches(0x1022, 0x7110, 1));
// Wrong device
assert!(!entry.matches(0x8086, 0x7111, 1));
// Revision wildcard (0..=0xFF)
let wildcard = ClocksourceQuirkEntry {
vendor: 0x8086,
device: 0x24C0,
revision_lo: 0,
revision_hi: 0xFF,
flags: super::super::ClocksourceQuirkFlags::PMTMR_GRAYLIST,
};
assert!(wildcard.matches(0x8086, 0x24C0, 0));
assert!(wildcard.matches(0x8086, 0x24C0, 200));
}
} }
@@ -456,6 +456,59 @@ pub fn lookup_cpu_bug_flags(cpuid: &CpuId, vendor_id: u16) -> CpuBugFlags {
flags flags
} }
bitflags::bitflags! {
/// Clocksource / timekeeping quirk flags. Phase R15
/// (2026-06-07) — initial bit set covers the PMTMR
/// blacklist from Linux 7.1
/// `drivers/clocksource/acpi_pm.c` (3 entries: Intel
/// PIIX4 rev<3 blacklisted, Intel ICH4 + ServerWorks
/// LE graylisted). TSC sync is handled algorithmically
/// in the kernel (mark_tsc_unstable), not by a data
/// table, so it is not represented here.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ClocksourceQuirkFlags: u64 {
/// The chipset is on the PMTMR blacklist; the
/// kernel should not use PMTMR as a clocksource.
const PMTMR_BLACKLIST = 1 << 0;
/// The chipset is on the PMTMR graylist; PMTMR
/// may be used with a degraded rating (120).
const PMTMR_GRAYLIST = 1 << 1;
/// The TSC on this CPU is known to be unstable.
const TSC_UNSTABLE = 1 << 2;
/// The HPET on this chipset is broken; the kernel
/// should not use HPET.
const HPET_BROKEN = 1 << 3;
}
}
/// One PCI-device entry in the clocksource quirk table.
/// Phase R15 (2026-06-07).
#[derive(Debug, Clone)]
pub struct ClocksourceQuirkEntry {
pub vendor: u16,
pub device: u16,
pub revision_lo: u8, // inclusive; 0 means "any"
pub revision_hi: u8, // inclusive; 0xFF means "any"
pub flags: ClocksourceQuirkFlags,
}
impl ClocksourceQuirkEntry {
/// Match against (vendor, device, revision). revision=0xFF
/// in the entry is a wildcard on revision.
pub fn matches(&self, vendor: u16, device: u16, revision: u8) -> bool {
if self.vendor != 0xFFFF && self.vendor != vendor {
return false;
}
if self.device != 0xFFFF && self.device != device {
return false;
}
if self.revision_lo == 0 && self.revision_hi == 0xFF {
return true;
}
revision >= self.revision_lo && revision <= self.revision_hi
}
}
/// Wildcard value for PCI ID matching. /// Wildcard value for PCI ID matching.
pub const PCI_QUIRK_ANY_ID: u16 = 0xFFFF; pub const PCI_QUIRK_ANY_ID: u16 = 0xFFFF;
@@ -1,9 +1,10 @@
use super::{ use super::{
dmi::{self, DmiAcpiQuirkRule, DmiDrmPanelQuirkRule, DmiInfo, DmiMatchRule, DmiPciQuirkRule, DmiXhciQuirkRule, PlatformDmiQuirkRule}, dmi::{self, DmiAcpiQuirkRule, DmiDrmPanelQuirkRule, DmiInfo, DmiMatchRule, DmiPciQuirkRule, DmiXhciQuirkRule, PlatformDmiQuirkRule},
AcpiQuirkFlags, CpuBugFlags, CpuBugQuirkEntry, CpuId, DrmPanelOrientation, AcpiQuirkFlags, ClocksourceQuirkEntry, ClocksourceQuirkFlags, CpuBugFlags,
HidQuirkEntry, HidQuirkFlags, MaskWidth, PciQuirkEntry, PciQuirkFlags, CpuBugQuirkEntry, CpuId, DrmPanelOrientation, HidQuirkEntry, HidQuirkFlags,
PciQuirkLookup, PciQuirkPhase, PlatformSubsystem, QuirkAction, UsbQuirkEntry, MaskWidth, PciQuirkEntry, PciQuirkFlags, PciQuirkLookup, PciQuirkPhase,
UsbQuirkFlags, XhciControllerQuirk, XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID, PlatformSubsystem, QuirkAction, UsbQuirkEntry, UsbQuirkFlags,
XhciControllerQuirk, XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID,
}; };
use crate::pci::PciDeviceInfo; use crate::pci::PciDeviceInfo;
use std::borrow::Cow; use std::borrow::Cow;
@@ -510,6 +511,108 @@ pub(crate) fn load_cpu_bug_flags(
flags flags
} }
pub const CLOCKSOURCE_FLAG_NAMES: &[(&str, ClocksourceQuirkFlags)] = &[
("pmtmr_blacklist", ClocksourceQuirkFlags::PMTMR_BLACKLIST),
("pmtmr_graylist", ClocksourceQuirkFlags::PMTMR_GRAYLIST),
("tsc_unstable", ClocksourceQuirkFlags::TSC_UNSTABLE),
("hpet_broken", ClocksourceQuirkFlags::HPET_BROKEN),
];
pub(crate) fn read_toml_clocksource_entries()
-> std::io::Result<Vec<ClocksourceQuirkEntry>> {
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_clocksource_toml(&doc, &mut entries, &path_str);
}
Ok(entries)
}
fn parse_clocksource_toml(
doc: &toml::Value,
out: &mut Vec<ClocksourceQuirkEntry>,
path: &str,
) {
let Some(arr) = doc.get("clocksource_quirk").and_then(|v| v.as_array()) else {
return;
};
for item in arr {
let Some(table) = item.as_table() else {
log::warn!("quirks: {path}: clocksource_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 device = match table.get("device") {
Some(value) => match bounded_u16(value, "device", path) {
Some(value) => value,
None => continue,
},
None => 0xFFFF,
};
let revision_lo = match table.get("revision_lo") {
Some(value) => match bounded_u8(value, "revision_lo", path) {
Some(value) => value,
None => continue,
},
None => 0,
};
let revision_hi = match table.get("revision_hi") {
Some(value) => match bounded_u8(value, "revision_hi", path) {
Some(value) => value,
None => continue,
},
None => 0xFF,
};
let flags = parse_flags(table, path, "Clocksource", CLOCKSOURCE_FLAG_NAMES);
out.push(ClocksourceQuirkEntry {
vendor,
device,
revision_lo,
revision_hi,
flags,
});
}
}
/// Look up the clocksource flags for the given PCI device
/// (vendor, device, revision) across all runtime TOML entries.
/// Returns the OR-accumulated flags.
pub(crate) fn load_clocksource_flags(
vendor: u16,
device: u16,
revision: u8,
) -> ClocksourceQuirkFlags {
let mut flags = ClocksourceQuirkFlags::empty();
if let Ok(entries) = read_toml_clocksource_entries() {
for entry in entries {
if entry.matches(vendor, device, revision) {
flags |= entry.flags;
}
}
}
flags
}
fn bounded_u16(val: &toml::Value, field: &str, path: &str) -> Option<u16> { fn bounded_u16(val: &toml::Value, field: &str, path: &str) -> Option<u16> {
match val.as_integer() { match val.as_integer() {
Some(v) => u16::try_from(v).ok().or_else(|| { Some(v) => u16::try_from(v).ok().or_else(|| {
@@ -0,0 +1,42 @@
# Clocksource / timekeeping quirks — PCI device-based.
# Mined from Linux 7.1
# `drivers/clocksource/acpi_pm.c` `DECLARE_PCI_FIXUP_EARLY`
# entries (3 total):
# - Intel 82371AB_3 (PIIX4) — blacklisted
# - Intel 82801DB_0 (ICH4) — graylisted
# - ServerWorks LE — graylisted
#
# The PMTMR workaround switches the kernel's clocksource
# choice. `blacklist` means PMTMR is rejected; `graylist`
# means PMTMR is allowed at a downgraded rating (120).
#
# Phase R15 (2026-06-07). The compiled-in table is empty
# (`clocksource_table.rs`); runtime TOML is the data
# surface. TSC sync detection is algorithmic in the
# kernel (mark_tsc_unstable), not table-driven, so it
# is not represented here.
# Intel 82371AB_3 (PIIX4) — blacklist, revisions 0..=2
# "Has a bug so severe that it causes random time jumps
# of up to 5 seconds." (Linux comment)
[[clocksource_quirk]]
vendor = 0x8086
device = 0x7113
revision_lo = 0
revision_hi = 2
flags = ["pmtmr_blacklist"]
# Intel 82801DB_0 (ICH4) — graylist
# "The chipset may have PM-Timer Bug. We can fall back to
# the slower read." (Linux comment)
[[clocksource_quirk]]
vendor = 0x8086
device = 0x24C0
flags = ["pmtmr_graylist"]
# ServerWorks LE — graylist
# Same as ICH4; degraded PMTMR rating 120.
[[clocksource_quirk]]
vendor = 0x1166
device = 0x0009
flags = ["pmtmr_graylist"]