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:
@@ -960,4 +960,37 @@ mod tests {
|
||||
// Mismatch
|
||||
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
|
||||
}
|
||||
|
||||
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.
|
||||
pub const PCI_QUIRK_ANY_ID: u16 = 0xFFFF;
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::{
|
||||
dmi::{self, DmiAcpiQuirkRule, DmiDrmPanelQuirkRule, DmiInfo, DmiMatchRule, DmiPciQuirkRule, DmiXhciQuirkRule, PlatformDmiQuirkRule},
|
||||
AcpiQuirkFlags, CpuBugFlags, CpuBugQuirkEntry, CpuId, DrmPanelOrientation,
|
||||
HidQuirkEntry, HidQuirkFlags, MaskWidth, PciQuirkEntry, PciQuirkFlags,
|
||||
PciQuirkLookup, PciQuirkPhase, PlatformSubsystem, QuirkAction, UsbQuirkEntry,
|
||||
UsbQuirkFlags, XhciControllerQuirk, XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID,
|
||||
AcpiQuirkFlags, ClocksourceQuirkEntry, ClocksourceQuirkFlags, 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;
|
||||
@@ -510,6 +511,108 @@ pub(crate) fn load_cpu_bug_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> {
|
||||
match val.as_integer() {
|
||||
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"]
|
||||
Reference in New Issue
Block a user