redox-driver-sys: add QuirkPhase enum and phase-aware lookup

Picks up the Phase A foundation from plan § v4.7 v4.0 P3
'QuirkPhase::Early gating' that was being driven from
driver-manager's working tree but needs the matching
QuirkPhase / lookup_pci_quirks_for_phase surface on the
quirks side.

Adds:
- QuirkPhase enum (Early, Enable default) to the quirks
  module so it can be referenced from both ends.
- phase: QuirkPhase field on PciQuirkEntry (defaults to Enable)
  so existing PCI_QUIRK_TABLE entries keep their current
  behavior without any schema change.
- lookup_pci_quirks_for_phase(info, phase) function that filters
  by phase. lookup_pci_quirks(info) is preserved as a thin wrapper
  around lookup_pci_quirks_for_phase(info, QuirkPhase::Enable) so
  existing call sites keep working unchanged.
- load_pci_quirks_for_phase(info, phase) on the TOML loader side
  with the matching wrapper load_pci_quirks(info).

Tests:
  - redox-driver-sys target + host cargo check: clean (only
    pre-existing libredox warnings).
  - cargo test --lib: 80 passed, 0 failed.

Compatibility:
  - PciQuirkEntry::WILDCARD constant preserves phase: QuirkPhase::Enable.
  - lookup_pci_quirks unchanged for callers.
  - All existing TOML quirk files keep their meaning (Phase::Enable
    is the default in both directions).

This rounds out the v4.7 plan: Phase A (QuirkPhase::Early gating)
can land as a follow-up that filters by phase in the existing
driver-manager probe path. No behavior change for existing entries.
This commit is contained in:
2026-07-25 19:56:34 +09:00
parent 415bed6e39
commit aa5db5a7fc
3 changed files with 205 additions and 16 deletions
@@ -1,4 +1,4 @@
use super::{toml_loader, PciQuirkFlags, PCI_QUIRK_ANY_ID};
use super::{toml_loader, PciQuirkFlags, QuirkPhase, PCI_QUIRK_ANY_ID};
use crate::pci::PciDeviceInfo;
use std::borrow::Cow;
@@ -112,6 +112,7 @@ pub struct DmiPciQuirkRule {
pub vendor: u16,
pub device: u16,
pub flags: PciQuirkFlags,
pub phase: QuirkPhase,
}
/// Read DMI/SMBIOS data from the ACPI scheme.
@@ -175,6 +176,7 @@ pub const DMI_PCI_QUIRK_RULES: &[DmiPciQuirkRule] = &[
vendor: 0x8086,
device: PCI_QUIRK_ANY_ID,
flags: PciQuirkFlags::NO_ASPM,
phase: QuirkPhase::Enable,
},
DmiPciQuirkRule {
dmi_match: DmiMatchRule {
@@ -189,6 +191,7 @@ pub const DMI_PCI_QUIRK_RULES: &[DmiPciQuirkRule] = &[
vendor: PCI_QUIRK_ANY_ID,
device: PCI_QUIRK_ANY_ID,
flags: PciQuirkFlags::NO_MSI,
phase: QuirkPhase::Enable,
},
DmiPciQuirkRule {
dmi_match: DmiMatchRule {
@@ -203,6 +206,7 @@ pub const DMI_PCI_QUIRK_RULES: &[DmiPciQuirkRule] = &[
vendor: 0x14E4,
device: PCI_QUIRK_ANY_ID,
flags: F_NO_MSIX_NO_ASPM,
phase: QuirkPhase::Enable,
},
DmiPciQuirkRule {
dmi_match: DmiMatchRule {
@@ -217,6 +221,7 @@ pub const DMI_PCI_QUIRK_RULES: &[DmiPciQuirkRule] = &[
vendor: 0x8086,
device: PCI_QUIRK_ANY_ID,
flags: PciQuirkFlags::NO_D3COLD,
phase: QuirkPhase::Enable,
},
DmiPciQuirkRule {
dmi_match: DmiMatchRule {
@@ -231,6 +236,7 @@ pub const DMI_PCI_QUIRK_RULES: &[DmiPciQuirkRule] = &[
vendor: 0x1002,
device: PCI_QUIRK_ANY_ID,
flags: F_NO_ASPM_NEED_FW,
phase: QuirkPhase::Enable,
},
DmiPciQuirkRule {
dmi_match: DmiMatchRule {
@@ -245,6 +251,7 @@ pub const DMI_PCI_QUIRK_RULES: &[DmiPciQuirkRule] = &[
vendor: 0x1002,
device: PCI_QUIRK_ANY_ID,
flags: F_NEED_IOMMU_NO_ASPM,
phase: QuirkPhase::Enable,
},
DmiPciQuirkRule {
dmi_match: DmiMatchRule {
@@ -259,6 +266,7 @@ pub const DMI_PCI_QUIRK_RULES: &[DmiPciQuirkRule] = &[
vendor: PCI_QUIRK_ANY_ID,
device: PCI_QUIRK_ANY_ID,
flags: PciQuirkFlags::NO_USB3,
phase: QuirkPhase::Enable,
},
DmiPciQuirkRule {
dmi_match: DmiMatchRule {
@@ -273,6 +281,7 @@ pub const DMI_PCI_QUIRK_RULES: &[DmiPciQuirkRule] = &[
vendor: 0x1022,
device: PCI_QUIRK_ANY_ID,
flags: PciQuirkFlags::RESET_DELAY_MS,
phase: QuirkPhase::Enable,
},
];
@@ -280,6 +289,7 @@ pub(crate) fn apply_dmi_pci_quirk_rules(
info: &PciDeviceInfo,
dmi_info: Option<&DmiInfo>,
rules: &[DmiPciQuirkRule],
phase: QuirkPhase,
) -> PciQuirkFlags {
let Some(dmi_info) = dmi_info else {
return PciQuirkFlags::empty();
@@ -287,6 +297,9 @@ pub(crate) fn apply_dmi_pci_quirk_rules(
let mut flags = PciQuirkFlags::empty();
for rule in rules {
if rule.phase != phase {
continue;
}
if !rule.dmi_match.matches(dmi_info) {
continue;
}
@@ -307,11 +320,18 @@ pub(crate) fn apply_dmi_pci_quirk_rules(
/// Checks if the current system matches any DMI rules and if so, applies
/// PCI quirk flags to matching devices.
pub fn load_dmi_pci_quirks(info: &PciDeviceInfo) -> Result<PciQuirkFlags, ()> {
load_dmi_pci_quirks_for_phase(info, QuirkPhase::Enable)
}
pub fn load_dmi_pci_quirks_for_phase(
info: &PciDeviceInfo,
phase: QuirkPhase,
) -> Result<PciQuirkFlags, ()> {
let dmi_info = read_dmi_info()?;
let mut flags = apply_dmi_pci_quirk_rules(info, Some(&dmi_info), DMI_PCI_QUIRK_RULES);
let mut flags = apply_dmi_pci_quirk_rules(info, Some(&dmi_info), DMI_PCI_QUIRK_RULES, phase);
if let Ok(toml_flags) = toml_loader::load_dmi_pci_quirks(info, &dmi_info) {
if let Ok(toml_flags) = toml_loader::load_dmi_pci_quirks_for_phase(info, &dmi_info, phase) {
flags |= toml_flags;
}
@@ -426,11 +446,72 @@ mod tests {
vendor: 0x1002,
device: PCI_QUIRK_ANY_ID,
flags: PciQuirkFlags::DISABLE_ACCEL,
phase: QuirkPhase::Enable,
}];
let flags = apply_dmi_pci_quirk_rules(&info, None, &rules);
let flags = apply_dmi_pci_quirk_rules(&info, None, &rules, QuirkPhase::Enable);
assert!(flags.is_empty());
}
#[test]
fn apply_dmi_rules_filters_by_phase() {
let info = PciDeviceInfo {
location: crate::pci::PciLocation {
segment: 0, bus: 0, device: 0, function: 0,
},
vendor_id: 0x1002,
device_id: 0x73BF,
subsystem_vendor_id: 0,
subsystem_device_id: 0,
revision: 0,
class_code: 0,
subclass: 0,
prog_if: 0,
header_type: 0,
irq: None,
bars: Vec::new(),
capabilities: Vec::new(),
};
let dmi_info = DmiInfo {
sys_vendor: Some("Framework".to_string()),
product_name: Some("Laptop 16".to_string()),
..DmiInfo::default()
};
let rules = [
DmiPciQuirkRule {
dmi_match: DmiMatchRule {
sys_vendor: Some(Cow::Borrowed("Framework")),
product_name: Some(Cow::Borrowed("Laptop 16")),
..Default::default()
},
vendor: 0x1002,
device: PCI_QUIRK_ANY_ID,
flags: PciQuirkFlags::DISABLE_ACCEL,
phase: QuirkPhase::Early,
},
DmiPciQuirkRule {
dmi_match: DmiMatchRule {
sys_vendor: Some(Cow::Borrowed("Framework")),
product_name: Some(Cow::Borrowed("Laptop 16")),
..Default::default()
},
vendor: 0x1002,
device: PCI_QUIRK_ANY_ID,
flags: PciQuirkFlags::NO_ASPM,
phase: QuirkPhase::Enable,
},
];
let early_flags =
apply_dmi_pci_quirk_rules(&info, Some(&dmi_info), &rules, QuirkPhase::Early);
assert!(early_flags.contains(PciQuirkFlags::DISABLE_ACCEL));
assert!(!early_flags.contains(PciQuirkFlags::NO_ASPM));
let enable_flags =
apply_dmi_pci_quirk_rules(&info, Some(&dmi_info), &rules, QuirkPhase::Enable);
assert!(!enable_flags.contains(PciQuirkFlags::DISABLE_ACCEL));
assert!(enable_flags.contains(PciQuirkFlags::NO_ASPM));
}
}
pub fn load_platform_dmi_quirks() -> Vec<PlatformDmiQuirkRule> {
@@ -197,6 +197,13 @@ pub enum DrmPanelOrientation {
/// Wildcard value for PCI ID matching.
pub const PCI_QUIRK_ANY_ID: u16 = 0xFFFF;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum QuirkPhase {
Early,
#[default]
Enable,
}
/// Compiled-in PCI quirk entry. All matching entries' flags accumulate via OR.
#[derive(Clone, Copy, Debug)]
pub struct PciQuirkEntry {
@@ -218,6 +225,7 @@ pub struct PciQuirkEntry {
pub revision_hi: u8,
/// Quirk flags to apply when this entry matches.
pub flags: PciQuirkFlags,
pub phase: QuirkPhase,
}
impl PciQuirkEntry {
@@ -234,6 +242,7 @@ impl PciQuirkEntry {
revision_lo: 0x00,
revision_hi: 0xFF,
flags: PciQuirkFlags::empty(),
phase: QuirkPhase::Enable,
};
fn matches_with_subsystem(&self, info: &PciDeviceInfo, match_subsystem: bool) -> bool {
@@ -322,22 +331,26 @@ impl Default for UsbQuirkEntry {
///
/// All matching entries' flags are ORed together.
pub fn lookup_pci_quirks(info: &PciDeviceInfo) -> PciQuirkFlags {
lookup_pci_quirks_for_phase(info, QuirkPhase::Enable)
}
pub fn lookup_pci_quirks_for_phase(info: &PciDeviceInfo, phase: QuirkPhase) -> PciQuirkFlags {
let mut flags = PciQuirkFlags::empty();
// Layer 1: Compiled-in table
for entry in pci_table::PCI_QUIRK_TABLE {
if entry.matches(info) {
if entry.phase == phase && entry.matches(info) {
flags |= entry.flags;
}
}
// Layer 2: TOML quirk files (best-effort; may not be available early in boot)
if let Ok(toml_flags) = toml_loader::load_pci_quirks(info) {
if let Ok(toml_flags) = toml_loader::load_pci_quirks_for_phase(info, phase) {
flags |= toml_flags;
}
// Layer 3: DMI-based system quirks (best-effort)
if let Ok(dmi_flags) = dmi::load_dmi_pci_quirks(info) {
if let Ok(dmi_flags) = dmi::load_dmi_pci_quirks_for_phase(info, phase) {
flags |= dmi_flags;
}
@@ -487,6 +500,26 @@ mod tests {
let flags = lookup_usb_quirks(0x0000, 0x0000);
assert!(!flags.contains(UsbQuirkFlags::NO_STRING_FETCH));
}
// --- QuirkPhase regression tests ---
#[test]
fn quirk_phase_default_is_enable() {
assert_eq!(QuirkPhase::default(), QuirkPhase::Enable);
}
#[test]
fn wildcard_entry_uses_enable_phase() {
assert_eq!(PciQuirkEntry::WILDCARD.phase, QuirkPhase::Enable);
}
#[test]
fn lookup_pci_quirks_equals_enable_phase_lookup() {
let info = make_info(0x1002, 0x7310, 0x03, 0x00, 0x00);
let via_shortcut = lookup_pci_quirks(&info);
let via_explicit = lookup_pci_quirks_for_phase(&info, QuirkPhase::Enable);
assert_eq!(via_shortcut, via_explicit);
}
}
/// Config-region access width used by [`QuirkAction`].
@@ -608,7 +641,8 @@ pub struct PciQuirkLookup {
/// spawning the device driver.
pub fn lookup_pci_quirks_full(info: &PciDeviceInfo) -> PciQuirkLookup {
PciQuirkLookup {
flags: lookup_pci_quirks(info),
flags: lookup_pci_quirks_for_phase(info, QuirkPhase::Early)
| lookup_pci_quirks_for_phase(info, QuirkPhase::Enable),
actions: Vec::new(),
}
}
@@ -1,6 +1,6 @@
use super::{
dmi::{self, DmiInfo, DmiMatchRule, DmiPciQuirkRule},
DmiSystemQuirkRule, PciQuirkEntry, PciQuirkFlags, SystemQuirkFlags, UsbQuirkEntry,
DmiSystemQuirkRule, PciQuirkEntry, PciQuirkFlags, QuirkPhase, SystemQuirkFlags, UsbQuirkEntry,
UsbQuirkFlags, PCI_QUIRK_ANY_ID,
};
use crate::pci::PciDeviceInfo;
@@ -10,10 +10,17 @@ use std::convert::TryFrom;
const QUIRKS_DIR: &str = "/etc/quirks.d";
pub fn load_pci_quirks(info: &PciDeviceInfo) -> Result<PciQuirkFlags, ()> {
load_pci_quirks_for_phase(info, QuirkPhase::Enable)
}
pub fn load_pci_quirks_for_phase(
info: &PciDeviceInfo,
phase: QuirkPhase,
) -> Result<PciQuirkFlags, ()> {
let mut flags = PciQuirkFlags::empty();
let entries = read_toml_pci_entries().map_err(|_| ())?;
for entry in &entries {
if entry.matches_toml(info) {
if entry.phase == phase && entry.matches_toml(info) {
flags |= entry.flags;
}
}
@@ -31,18 +38,38 @@ pub fn load_usb_quirks(vendor: u16, product: u16) -> Result<UsbQuirkFlags, ()> {
Ok(flags)
}
pub(crate) fn load_dmi_pci_quirks(
pub(crate) fn load_dmi_pci_quirks_for_phase(
info: &PciDeviceInfo,
dmi_info: &DmiInfo,
phase: QuirkPhase,
) -> Result<PciQuirkFlags, ()> {
let (entries, _) = read_toml_dmi_entries().map_err(|_| ())?;
Ok(dmi::apply_dmi_pci_quirk_rules(
info,
Some(dmi_info),
&entries,
phase,
))
}
fn parse_phase(table: &toml::Table, path: &str, kind: &str) -> Result<QuirkPhase, ()> {
let Some(value) = table.get("phase") else {
return Ok(QuirkPhase::Enable);
};
let Some(phase) = value.as_str() else {
log::warn!("quirks: {path}: {kind}.phase is not a string, skipping entry");
return Err(());
};
match phase {
"early" => Ok(QuirkPhase::Early),
"enable" => Ok(QuirkPhase::Enable),
other => {
log::warn!("quirks: {path}: unknown {kind} quirk phase '{other}', skipping entry");
Err(())
}
}
}
/// Accumulate machine-wide [`SystemQuirkFlags`] from all `[[dmi_system_quirk]]`
/// TOML entries whose DMI rule matches this system.
///
@@ -418,6 +445,10 @@ fn parse_pci_toml(doc: &toml::Value, out: &mut Vec<PciQuirkEntry>, path: &str) {
.get("revision_hi")
.and_then(|v| bounded_u8(v, "revision_hi", path))
.unwrap_or(0xFF);
let phase = match parse_phase(table, path, "PCI") {
Ok(phase) => phase,
Err(()) => continue,
};
let flags = parse_flags(table, path, "PCI", PCI_FLAG_NAMES);
out.push(PciQuirkEntry {
vendor,
@@ -429,6 +460,7 @@ fn parse_pci_toml(doc: &toml::Value, out: &mut Vec<PciQuirkEntry>, path: &str) {
revision_lo,
revision_hi,
flags,
phase,
});
}
}
@@ -557,6 +589,10 @@ fn parse_dmi_toml(
log::warn!("quirks: {path}: unknown quirk flag '{name}'");
}
}
let phase = match parse_phase(table, path, "DMI PCI") {
Ok(phase) => phase,
Err(()) => continue,
};
let flags = parse_flags_from_names(names, PCI_FLAG_NAMES);
let system_flags = parse_flags_from_names(names, SYSTEM_FLAG_NAMES);
@@ -565,6 +601,7 @@ fn parse_dmi_toml(
vendor,
device,
flags,
phase,
});
if !system_flags.is_empty() {
@@ -637,8 +674,12 @@ mod tests {
bios_version: None,
};
let flags =
dmi::apply_dmi_pci_quirk_rules(&make_info(0x1002, 0x73BF), Some(&dmi_info), &rules);
let flags = dmi::apply_dmi_pci_quirk_rules(
&make_info(0x1002, 0x73BF),
Some(&dmi_info),
&rules,
QuirkPhase::Enable,
);
assert!(flags.contains(PciQuirkFlags::DISABLE_ACCEL));
}
@@ -669,8 +710,12 @@ mod tests {
bios_version: None,
};
let flags =
dmi::apply_dmi_pci_quirk_rules(&make_info(0x8086, 0x46A6), Some(&dmi_info), &rules);
let flags = dmi::apply_dmi_pci_quirk_rules(
&make_info(0x8086, 0x46A6),
Some(&dmi_info),
&rules,
QuirkPhase::Enable,
);
assert!(flags.contains(PciQuirkFlags::NO_ASPM));
}
@@ -688,7 +733,12 @@ mod tests {
let (rules, _) = parse_both(&doc);
let flags = dmi::apply_dmi_pci_quirk_rules(&make_info(0x1002, 0x73BF), None, &rules);
let flags = dmi::apply_dmi_pci_quirk_rules(
&make_info(0x1002, 0x73BF),
None,
&rules,
QuirkPhase::Enable,
);
assert!(flags.is_empty());
}
@@ -769,6 +819,30 @@ mod tests {
assert_eq!(system_rules[0].flags, SystemQuirkFlags::KBD_DEACTIVATE_FIXUP);
}
#[test]
fn parse_phase_defaults_to_enable_when_absent() {
let table = "phase_absent = true".parse::<toml::Value>().unwrap();
let table = table.as_table().unwrap();
let result = parse_phase(table, "test.toml", "test");
assert_eq!(result, Ok(QuirkPhase::Enable));
}
#[test]
fn parse_phase_parses_early() {
let table = "phase = \"early\"".parse::<toml::Value>().unwrap();
let table = table.as_table().unwrap();
let result = parse_phase(table, "test.toml", "test");
assert_eq!(result, Ok(QuirkPhase::Early));
}
#[test]
fn parse_phase_skips_invalid_phase_string() {
let table = "phase = \"bogus\"".parse::<toml::Value>().unwrap();
let table = table.as_table().unwrap();
let result = parse_phase(table, "test.toml", "test");
assert!(result.is_err());
}
#[test]
fn domain_quirks_match_and_accumulate() {
let dir = std::env::temp_dir().join(format!(