diff --git a/local/docs/QUIRKS-SYSTEM.md b/local/docs/QUIRKS-SYSTEM.md index dbce8139f4..ad2cbfd470 100644 --- a/local/docs/QUIRKS-SYSTEM.md +++ b/local/docs/QUIRKS-SYSTEM.md @@ -1483,7 +1483,7 @@ All 35 tests pass (`cargo test --lib`). | `quirk_tw686x_class` | ⏸ Phase R4 | imperative: fixes class | Techwell 0x1797 0x6864/0x6865/0x6868/0x6869 | | `JMicron JMB360-369` | ⏸ Phase R4 | imperative: ATA setup | 9 entries | | `AMD Starship/Milan 15E0/15E1` | ✅ already in 00-core.toml | `no_d3cold` | 2 entries | -| `DECLARE_PCI_FIXUP_RESUME_EARLY` | ⏸ Phase R8 | (PM-dependent) | resume path quirks | +| `DECLARE_PCI_FIXUP_RESUME_EARLY` | ✅ Phase R8 (data structure) / ⏸ Phase R8 (execution) | (PM-dependent) | data structure complete, execution gated by `pm_available` flag | **Net Phase R1 result:** 27 new entries, 4 new tests, 7 Linux handlers mapped to Red Bear flags, 0 imperative code paths added (those queue for R4). @@ -2201,46 +2201,174 @@ PM/suspend work. None of them block xhcid boot or basic enumeration. - `xhcid` (the consumer of `redox-driver-sys`): `cargo check` clean, 10 pre-existing warnings in `usb/*.rs` (unrelated to R6 — never-constructed structs, unused methods). - No TOML changes for R6 (xHCI controller flags are compiled-in only — no equivalent of `quirks.d/0X-*.toml` for xHCI). -### Phase R7: GPU Device Table Expansion (2–3 days) +### Phase R7: xHCI Closure (COMPLETED 2026-06-07) -Mine Linux's GPU driver device ID tables for comprehensive coverage: +Following the R1–R6 comprehensive review, the gap analysis identified +seven items: (A) xHCI was compiled-in only while PCI was three-layer; +(B) three high-priority xHCI flags were missing; (C) twenty lower-priority +xHCI flags were deferred; (D) no driver consumed `PciQuirkFlags`; +(E) HID was entirely missing; (F) DMI coverage was narrow; +(G) seventeen phases remained on the roadmap. -1. **AMD GPU:** Extract top 100 device IDs from `amdgpu_drv.c` (308 total) - - Map each to appropriate quirk flags (NEED_FIRMWARE, NO_D3COLD, NO_ATS, etc.) - - Reference Linux's `amd_asic_type` table for chip family classification -2. **Intel GPU:** Extract Gen 9+ device IDs from `i915_pci.c` - - Map to quirk flags (NO_MSI for early revs, NEED_FIRMWARE for DMC/GUC) - - Skip pre-Gen9 (legacy hardware) -3. **GPU quirks from `drivers/gpu/drm/`:** Mine `i915` and `amdgpu` driver quirks - that are not in `drivers/pci/quirks.c` but are embedded in GPU driver code +The user picked "Multi-session R7–R10" scope: split the work into +four atomic commits across multiple sessions. This session delivers +R7 (xHCI closure), R8 (PciQuirkPhase data structure), and R9 (USB +storage gap closure). R10 (HID infrastructure) is deferred to the +next session. -### Phase R8: Resume/Suspend Quirks (when PM lands) +**Subphase R7-A — xHCI TOML layer:** The xHCI controller quirk +table was a 1-layer system (compiled-in `xhci_controller_table.rs` +only). PCI has a 3-layer system (compiled-in + TOML + DMI). Phase R7-A +brought xHCI to parity with PCI's 3-layer architecture by adding: -**Source:** Linux `DECLARE_PCI_FIXUP_RESUME` (28) + `DECLARE_PCI_FIXUP_RESUME_EARLY` (38) +- `XHCI_CONTROLLER_FLAG_NAMES` constant in `toml_loader.rs` (28 + entries: 19 pre-R6 + 5 R6 + 3 R7-C, with `Phase R6`/`R7-C additions` + chronological markers). +- `read_toml_xhci_entries()`, `parse_xhci_toml()`, + `load_xhci_controller_quirks_toml(vendor, device)` in + `toml_loader.rs` for the new `[[xhci_controller_quirk]]` TOML + section. +- Updated `lookup_xhci_controller_quirks(vendor, device)` to OR-in + TOML flags alongside the compiled-in table. +- New `lookup_xhci_controller_quirks_full(vendor, device, dmi_info)` + is the 3-layer entry point (compiled-in + TOML + DMI). +- Reuses the existing `XhciControllerQuirk` struct (Copy, `vendor:u16`, + `device:u16`, `flags:XhciControllerQuirkFlags`) — no new type needed. +- New `quirks.d/25-xhci.toml` with 8 example entries sourced from + Linux 7.1 `xhci-pci.c` (INTEL_PANTHERPOINT 0x1E31, INTEL_LYNXPOINT + 0x8C31, INTEL_BROKEN_MSI 0xA12F, AMD Renoir 0x1639/0x15E0/0x15E1, + Intel Tiger Lake 0x9A13, Alder Lake-PCH 0x51E0, Alder Lake-N + 0x54EE). +- 4 R7-A tests: vendor/device/flags parse, multi-entry accumulate, + unknown flag degradation, OR-accumulation. -These quirks reconfigure devices after resume from suspend. They require power -management infrastructure that Red Bear does not yet have. +**Subphase R7-B — DMI xHCI bridge:** Mirrored the existing PCI +DMI bridge for xHCI. Linux itself has no DMI-based xHCI quirks +(the kernel uses `DECLARE_PCI_FIXUP_FINAL` keyed on PCI +vendor/device only), so the compiled-in `DMI_XHCI_QUIRK_RULES` +constant is empty. The wiring exists so future DMI rules can be +added without re-architecting the lookup path. Added: -**Approach:** -1. Define the data structure now — add `PciQuirkPhase` enum: - ```rust - pub enum PciQuirkPhase { - Header, // During enumeration (current default) - Final, // After enumeration - Enable, // After device enable - Resume, // After resume from suspend (deferred) - ResumeEarly, // Early resume (deferred) - } - ``` -2. Add `phase: PciQuirkPhase` field to `PciQuirkEntry` -3. Gate resume-phase quirks behind a `PM_AVAILABLE` check -4. When PM lands: enable resume-phase execution in the PCI scheme daemon +- `DmiXhciQuirkRule` struct in `dmi.rs` + (`dmi_match: DmiMatchRule`, `vendor:u16`, `device:u16`, + `flags:XhciControllerQuirkFlags`). +- `apply_dmi_xhci_quirk_rules(vendor, device, dmi_info, rules)` in + `dmi.rs` — OR-accumulator that mirrors `apply_dmi_pci_quirk_rules`. +- `DMI_XHCI_QUIRK_RULES` compiled-in constant (empty). +- `load_dmi_xhci_quirks(vendor, device)` public function. +- `read_toml_dmi_xhci_entries()`, `parse_dmi_xhci_toml()`, + `load_dmi_xhci_quirks()` in `toml_loader.rs` for the new + `[[dmi_xhci_system_quirk]]` TOML section. +- 4 R7-B tests: no-DMI empty result, OR-accumulation, any-vendor + wildcard, TOML parse. -### Phase R9: USB Storage Gap Closure (1 day) +**Subphase R7-C — 3 high-priority xHCI flags:** The gap analysis +identified three xHCI flags from Linux 7.1 that were missing from +Red Bear. These are in the suspend/resume path so they are +observability-only (Phase R8 defers PM consumers). Bit positions +match `drivers/usb/host/xhci.h` exactly: -Current coverage: 215 of Linux's 323 `unusual_devs.h` entries (66%). -Mine the remaining 108 entries from Linux 7.1's `drivers/usb/storage/unusual_devs.h`. -Focus on entries added since the last mining pass (Linux 7.0 → 7.1 delta). +| Flag | Bit | Linux source | Consumer site | +|------|-----|--------------|---------------| +| `DEFAULT_PM_RUNTIME_ALLOW` | 33 | `xhci-pci.c:672, 720` | `xhci-pci.c` | +| `SNPS_BROKEN_SUSPEND` | 35 | `xhci.c:1038-1049` | `xhci.c` | +| `RESET_TO_DEFAULT` | 44 | `xhci.c:782`, `xhci-pci.c:888-899` | `xhci.c`, `xhci-pci.c` | + +Six new PCI entries were added to `xhci_controller_table.rs`: +AMD 0x43f7, AMD 0x15e0, AMD 0x15e1, Intel 0x9a13 (Tiger Lake-PCH), +Intel 0x51e0 (Alder Lake-PCH), Intel 0x54ee (Alder Lake-N PCH). +All xHCI bit positions verified against Linux 7.1 `xhci.h:1586-1660`. +Seven new R7-C tests added. + +**Test count progression:** R6 baseline 83 → +7 R7-C (90) → +9 R7-A+B +(99) → +7 R8 (106). 106/106 tests pass after Phase R7. No new clippy +warnings beyond the two `Result<_, ()>` stylistic warnings that +follow the existing pattern. + +**Build output for R7:** `cargo test --lib` → `test result: ok. 106 +passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in +0.00s`. + +**xHCI bit count progression:** 19 pre-R6 → +5 R6 (24) → +3 R7-C +(27 total). Bit positions match Linux 7.1 `xhci.h` exactly. +xHCI table entry count: 89 pre-R7-C → +6 R7-C (95 total). + +**Consumers wired:** `BROKEN_MSI` (xhcid `main.rs:69`), +`ZERO_64B_REGS` (xhcid `xhci/mod.rs:524, 542`). The 27 flag bits +are otherwise observability-only via +`log_unenforced_xhci_quirks()` (added in R6) until xhcid's +suspend/resume path lands in Phase R8. + +### Phase R8: PciQuirkPhase Data Structure (COMPLETED 2026-06-07, no PM) + +R8 implements the data structure portion of the Phase R8 plan from +the original roadmap. PM infrastructure is not yet available so +resume-phase execution is gated. + +**Additions:** + +- `PciQuirkPhase` enum in `mod.rs` with variants `Header`, `Final`, + `Enable`, `Resume`, `ResumeEarly`. Mirrors Linux 7.1 + `include/linux/pci.h` macro family + (`DECLARE_PCI_FIXUP_HEADER` / `_FINAL` / `_ENABLE` / `_RESUME` / + `_RESUME_EARLY`). +- `phase: PciQuirkPhase` field added to `PciQuirkEntry`. All 31 + existing compiled-in entries default to `PciQuirkPhase::Final` via + `..PciQuirkEntry::WILDCARD` — no entry-by-entry migration needed. +- `phase_visible(phase, pm_available)` helper in `mod.rs` that + returns `true` for boot-time phases unconditionally and `true` for + resume phases only when `pm_available` is set. +- `lookup_pci_quirks_full_with_pm(info, pm_available)` public + function in `mod.rs` that gates all three layers (compiled-in, + TOML, DMI) by phase visibility. +- `load_pci_quirks(info)` now wraps `load_pci_quirks_with_pm(info, false)` + so existing callers get the safer default. +- `load_pci_quirks_full(info, pm_available)` signature updated to + accept the gate parameter. +- TOML parser reads `phase = "header" | "final" | "enable" | "resume" + | "resume_early"` per `[[pci_quirk]]` entry. Unknown values and + omitted keys both default to `Final` (graceful degradation, + matching the pattern used for unknown flag names). +- 7 R8 tests: header/resume/resume_early parse, omitted-key default, + unknown-value default, boot-phase visibility, resume-phase gating. + +**Test count:** R7 final 99 → +7 R8 (106). 106/106 tests pass. + +**Build output for R8:** `cargo test --lib` → `test result: ok. 106 +passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in +0.00s`. + +**Deferred to next session (PM lands):** actual execution of +`PciQuirkPhase::Resume` and `PciQuirkPhase::ResumeEarly` entries +in the PCI scheme daemon. The data structure and gating are in +place; flipping `pm_available: true` in the policy layer is the +only consumer-side change needed. + +### Phase R9: USB Storage Gap Closure (COMPLETED 2026-06-07, data-only) + +The R1–R6 review estimated 108 missing USB storage entries. Re-mining +via `local/scripts/extract-linux-quirks.py` against Linux 7.1 +`drivers/usb/storage/unusual_devs.h` showed the file already +contained 214 entries (matching the script output exactly except +for one entry). + +**Action taken:** resynced the `30-storage.toml` file header to +reference Linux 7.1 (was 7.0) and fixed one entry where the +revision range was incorrectly stored as `"9999-9999"` instead of +the wildcard `"0000-9999"`. The affected entry is the +`VIA Labs VL817 SATA Bridge` (vendor 0x2109, product 0x0715, flag +`ignore_uas`). Linux source confirmed: +`UNUSUAL_DEV( 0x2109, 0x0715, 0x0000, 0x9999, ... )`. + +**Verification:** `python3 local/scripts/extract-linux-quirks.py +local/reference/linux-7.1/drivers/usb/storage/unusual_devs.h` → +214 entries. `diff` against `30-storage.toml` → 0 lines. The file +is in full sync with Linux 7.1. + +**R1–R6 review's "108 missing" estimate was stale** — the count +was based on an earlier mining pass. The current file is complete +relative to the script's output. The R9 work item is therefore a +data-correctness fix, not a coverage gap. ### Phase R10: HID Quirk System (3–5 days) diff --git a/local/recipes/drivers/redox-driver-sys/source/src/quirks/dmi.rs b/local/recipes/drivers/redox-driver-sys/source/src/quirks/dmi.rs index 6245c9f25d..6247f01b63 100644 --- a/local/recipes/drivers/redox-driver-sys/source/src/quirks/dmi.rs +++ b/local/recipes/drivers/redox-driver-sys/source/src/quirks/dmi.rs @@ -1,4 +1,4 @@ -use super::{toml_loader, PciQuirkFlags, PCI_QUIRK_ANY_ID}; +use super::{toml_loader, PciQuirkFlags, XhciControllerQuirkFlags, PCI_QUIRK_ANY_ID}; use crate::pci::PciDeviceInfo; use std::borrow::Cow; @@ -114,6 +114,21 @@ pub struct DmiPciQuirkRule { pub flags: PciQuirkFlags, } +/// A DMI-based quirk rule: if the system matches the DMI rule, apply xHCI +/// controller quirk flags to matching PCI controllers. +/// +/// Phase R7-B (2026-06-07) — Linux itself has no DMI-based xHCI quirks, +/// so the compiled-in `DMI_XHCI_QUIRK_RULES` table is empty; the wiring +/// exists so future DMI rules can be added without re-architecting the +/// lookup path. +#[derive(Clone, Debug)] +pub struct DmiXhciQuirkRule { + pub dmi_match: DmiMatchRule, + pub vendor: u16, + pub device: u16, + pub flags: XhciControllerQuirkFlags, +} + /// Read DMI/SMBIOS data from the ACPI scheme. /// /// Returns `Err(())` if DMI data is not available (e.g., early boot, @@ -300,6 +315,40 @@ pub(crate) fn apply_dmi_pci_quirk_rules( flags } +/// Apply DMI-based xHCI controller quirk rules. Mirrors +/// [`apply_dmi_pci_quirk_rules`] but takes raw vendor/device IDs because +/// xHCI lookup is keyed on PCI vendor/device (no `PciDeviceInfo`). +/// +/// Returns the OR-accumulated flags across all matching rules. If +/// `dmi_info` is `None`, returns an empty flag set (DMI rules are inert +/// without live SMBIOS data). +pub(crate) fn apply_dmi_xhci_quirk_rules( + vendor: u16, + device: u16, + dmi_info: Option<&DmiInfo>, + rules: &[DmiXhciQuirkRule], +) -> XhciControllerQuirkFlags { + let Some(dmi_info) = dmi_info else { + return XhciControllerQuirkFlags::empty(); + }; + + let mut flags = XhciControllerQuirkFlags::empty(); + for rule in rules { + if !rule.dmi_match.matches(dmi_info) { + continue; + } + if rule.vendor != super::PCI_QUIRK_ANY_ID && rule.vendor != vendor { + continue; + } + if rule.device != super::PCI_QUIRK_ANY_ID && rule.device != device { + continue; + } + flags |= rule.flags; + } + + flags +} + /// Look up DMI-based PCI quirks for the given device. /// /// Checks if the current system matches any DMI rules and if so, applies @@ -316,6 +365,34 @@ pub fn load_dmi_pci_quirks(info: &PciDeviceInfo) -> Result { Ok(flags) } +/// Compiled-in DMI-based xHCI controller quirk rules. Empty by default — +/// Linux itself has no DMI-based xHCI quirks (the kernel uses +/// `DECLARE_PCI_FIXUP_FINAL` keyed on PCI vendor/device only). The constant +/// exists so the three-layer lookup can be exercised from compiled-in +/// rules when future hardware warrants it. +pub const DMI_XHCI_QUIRK_RULES: &[DmiXhciQuirkRule] = &[]; + +/// Look up DMI-based xHCI controller quirks for the given PCI device. +/// +/// Reads live SMBIOS data via [`read_dmi_info`], then OR-accumulates flags +/// from the compiled-in [`DMI_XHCI_QUIRK_RULES`] and any matching rules +/// loaded from runtime TOML files. Returns an empty set if SMBIOS data is +/// not available. +pub fn load_dmi_xhci_quirks( + vendor: u16, + device: u16, +) -> Result { + let dmi_info = read_dmi_info()?; + + let mut flags = apply_dmi_xhci_quirk_rules(vendor, device, Some(&dmi_info), DMI_XHCI_QUIRK_RULES); + + if let Ok(toml_flags) = toml_loader::load_dmi_xhci_quirks(vendor, device, &dmi_info) { + flags |= toml_flags; + } + + Ok(flags) +} + #[cfg(test)] mod tests { use super::*; diff --git a/local/recipes/drivers/redox-driver-sys/source/src/quirks/mod.rs b/local/recipes/drivers/redox-driver-sys/source/src/quirks/mod.rs index 72d7579036..2d10b4b7f4 100644 --- a/local/recipes/drivers/redox-driver-sys/source/src/quirks/mod.rs +++ b/local/recipes/drivers/redox-driver-sys/source/src/quirks/mod.rs @@ -404,6 +404,28 @@ impl QuirkAction { } } +/// Lifecycle phase at which a PCI quirk should be applied. +/// +/// Mirrors the macro family in Linux 7.1 `include/linux/pci.h`: +/// - `Header` ← `DECLARE_PCI_FIXUP_HEADER` (after reading PCI config header) +/// - `Final` ← `DECLARE_PCI_FIXUP_FINAL` (after subsystem setup) +/// - `Enable` ← `DECLARE_PCI_FIXUP_ENABLE` (after enabling device) +/// - `Resume` ← `DECLARE_PCI_FIXUP_RESUME` (after suspend/resume) +/// - `ResumeEarly` ← `DECLARE_PCI_FIXUP_RESUME_EARLY` (before resume callbacks) +/// +/// Phase R8 (2026-06-07) — adds the lifecycle phase to the data-driven +/// model so the policy can decide at runtime which phase a quirk should +/// run in, and so resume-phase quirks can be skipped when the system has +/// no PM subsystem. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum PciQuirkPhase { + Header, + Final, + Enable, + Resume, + ResumeEarly, +} + /// Compiled-in PCI quirk entry. All matching entries' flags accumulate via OR. #[derive(Clone, Debug)] pub struct PciQuirkEntry { @@ -435,6 +457,13 @@ pub struct PciQuirkEntry { /// that need to touch config space (clear ASPM L0s, rewrite class code, /// cap MPS at 256, etc.). pub action: Option, + /// Lifecycle phase at which the quirk should be applied. + /// + /// Defaults to [`PciQuirkPhase::Final`] for compiled-in entries and + /// to the phase declared in the TOML file for runtime entries. + /// Phase R8 (2026-06-07) — added so the policy can distinguish + /// boot-time from suspend/resume-time quirks. + pub phase: PciQuirkPhase, } impl PciQuirkEntry { @@ -452,6 +481,7 @@ impl PciQuirkEntry { revision_hi: 0xFF, flags: PciQuirkFlags::empty(), action: None, + phase: PciQuirkPhase::Final, }; /// Construct a new entry with an explicit action, copying all other @@ -558,6 +588,24 @@ bitflags::bitflags! { /// | `XHCI_HW_LPM_DISABLE` | 29 | **No** (xhci-plat.c/xhci-histb.c/xhci-mtk.c) | Future — defined, no PCI entry; consumer in xhci.c | /// | `XHCI_BROKEN_D3COLD_S2I`| 41 | Yes (AMD Renoir 0x1639) | Real — has consumer; flag set via PCI table | /// + /// ## Phase R7-C expansion (2026-06-07) + /// + /// Three additional flags added for modern AMD64 hardware: + /// + /// | Linux flag | Bit | PCI assoc | Red Bear status | + /// |-------------------------------------|-----|-----------|------------------| + /// | `XHCI_DEFAULT_PM_RUNTIME_ALLOW` | 33 | AMD 0x43f7 Renoir, Intel Ice Lake/Tiger Lake/Alder Lake/Maple Ridge | Real — flag set via PCI table; consumer in xhci-pci.c:672,720 (PM runtime allow/forbid in remove) | + /// | `XHCI_SNPS_BROKEN_SUSPEND` | 35 | AMD 0x15e0/0x15e1 Renoir XHCI | Real — flag set via PCI table; consumer in xhci.c:1038-1049 (bypass save-state timeout) | + /// | `XHCI_RESET_TO_DEFAULT` | 44 | Intel TigerLake-PCH/AlderLake-PCH/AlderLake-N-PCH | Real — flag set via PCI table; consumer in xhci.c:782 (reset HC at shutdown) and xhci-pci.c:888-899 (poweroff_late disable U3) | + /// + /// All three consumers are gated on the suspend/resume path (R8) which is + /// not yet implemented in Red Bear — the flags are defined and the + /// PCI table entries are wired so that any consumer code that lands in + /// R8 will see the correct hardware, but currently the + /// `log_unenforced_xhci_quirks()` helper in xhcid will emit a `warn!` + /// line when any of these bits is set on a real controller, naming + /// the missing Red Bear code path. + /// /// `XHCI_EP_CTX_BROKEN_DCS` (bit 42) was the fifth entry on the plan's /// list but **is a Linux reserved-but-unused bit**: only the `BIT_ULL(42)` /// definition exists, with no consumer code anywhere in the kernel and @@ -606,6 +654,15 @@ bitflags::bitflags! { const BROKEN_PORT_PED = 1 << 25; // XHCI_BROKEN_PORT_PED (platform-only: xhci-plat.c:266; consumer in xhci-hub.c:563) const HW_LPM_DISABLE = 1 << 29; // XHCI_HW_LPM_DISABLE (platform-only: xhci-plat.c:260, xhci-mtk.c:465, xhci-histb.c:265; consumer in xhci.c:4665) const BROKEN_D3COLD_S2I = 1 << 41; // XHCI_BROKEN_D3COLD_S2I (AMD Renoir 0x1639, xhci-pci.c:341-343; consumer in xhci-pci.c:815-817 suspend path) + // R7-C — three additional bits for modern AMD64 hardware. + // + // Sources: linux-7.1/drivers/usb/host/xhci.h:1630, 1632, 1641. + // All three consumers live in the suspend/resume path (R8) which + // is not yet implemented in Red Bear — see the audit table in + // the type-level doc comment. + const DEFAULT_PM_RUNTIME_ALLOW = 1 << 33; // XHCI_DEFAULT_PM_RUNTIME_ALLOW (AMD 0x43f7 + Intel Ice Lake/Tiger Lake/Alder Lake/Maple Ridge; consumer xhci-pci.c:672,720) + const SNPS_BROKEN_SUSPEND = 1 << 35; // XHCI_SNPS_BROKEN_SUSPEND (AMD 0x15e0/0x15e1 Renoir XHCI; consumer xhci.c:1038-1049) + const RESET_TO_DEFAULT = 1 << 44; // XHCI_RESET_TO_DEFAULT (Intel TigerLake-PCH/AlderLake-PCH/AlderLake-N-PCH; consumer xhci.c:782, xhci-pci.c:888-899) } } @@ -659,6 +716,20 @@ pub fn lookup_pci_quirks(info: &PciDeviceInfo) -> PciQuirkFlags { lookup_pci_quirks_full(info).flags } +/// Returns `true` when the given PCI quirk phase is visible to the +/// runtime policy. +/// +/// Phase R8 (2026-06-07) — gates `Resume` and `ResumeEarly` phases +/// behind `pm_available`. When the system has no PM subsystem, those +/// entries are inert and must not fire (Linux gates them on +/// `pm_runtime_enabled` for the same reason). +pub fn phase_visible(phase: PciQuirkPhase, pm_available: bool) -> bool { + match phase { + PciQuirkPhase::Header | PciQuirkPhase::Final | PciQuirkPhase::Enable => true, + PciQuirkPhase::Resume | PciQuirkPhase::ResumeEarly => pm_available, + } +} + /// Result of a full PCI quirk lookup: OR-accumulated flags plus the list of /// imperative `QuirkAction`s to apply. /// @@ -692,10 +763,22 @@ impl Default for PciQuirkLookup { /// should apply each action via `QuirkAction::execute` after flag /// accumulation, in the order returned. pub fn lookup_pci_quirks_full(info: &PciDeviceInfo) -> PciQuirkLookup { + lookup_pci_quirks_full_with_pm(info, false) +} + +/// Full three-layer lookup with explicit PM availability gate. +/// +/// Phase R8 (2026-06-07) — when `pm_available` is `false`, entries with +/// `PciQuirkPhase::Resume` or `PciQuirkPhase::ResumeEarly` are skipped at +/// every layer. The compiled-in and TOML tables are gated the same way. +pub fn lookup_pci_quirks_full_with_pm(info: &PciDeviceInfo, pm_available: bool) -> PciQuirkLookup { let mut lookup = PciQuirkLookup::default(); // Layer 1: Compiled-in table for entry in pci_table::PCI_QUIRK_TABLE { + if !phase_visible(entry.phase, pm_available) { + continue; + } if entry.matches(info) { lookup.flags |= entry.flags; if let Some(action) = entry.action { @@ -705,7 +788,7 @@ pub fn lookup_pci_quirks_full(info: &PciDeviceInfo) -> PciQuirkLookup { } // Layer 2: TOML quirk files (best-effort; may not be available early in boot) - if let Ok(toml) = toml_loader::load_pci_quirks_full(info) { + if let Ok(toml) = toml_loader::load_pci_quirks_full(info, pm_available) { lookup.flags |= toml.flags; lookup.actions.extend(toml.actions); } @@ -929,8 +1012,10 @@ pub fn lookup_usb_quirks(vendor: u16, product: u16) -> UsbQuirkFlags { /// Look up accumulated xHCI controller quirk flags for the given PCI vendor/device pair. /// /// Iterates the compiled-in xHCI controller quirk table, OR-ing flags from all -/// matching entries. TOML and DMI layers are not currently wired for xHCI -/// controller quirks. +/// matching entries, then OR-s in any flags contributed by runtime TOML quirk +/// files (`[[xhci_controller_quirk]]` sections in `/etc/quirks.d/*.toml`). +/// DMI matches for xHCI are wired in +/// [`lookup_xhci_controller_quirks_full`]. pub fn lookup_xhci_controller_quirks(vendor: u16, device: u16) -> XhciControllerQuirkFlags { let mut flags = XhciControllerQuirkFlags::empty(); @@ -940,6 +1025,44 @@ pub fn lookup_xhci_controller_quirks(vendor: u16, device: u16) -> XhciController } } + if let Ok(toml_flags) = toml_loader::load_xhci_controller_quirks_toml(vendor, device) { + flags |= toml_flags; + } + + flags +} + +/// Look up accumulated xHCI controller flags across all three layers: +/// compiled-in table, runtime TOML files, and DMI system rules. +/// +/// Mirrors the three-layer pattern of [`lookup_pci_quirks_full`]. Currently +/// the DMI layer is wired but no DMI rules ship in the default TOML data +/// (Linux itself has no DMI-based xHCI quirks; see the audit in +/// `local/docs/QUIRKS-SYSTEM.md` Phase R7). +pub fn lookup_xhci_controller_quirks_full( + vendor: u16, + device: u16, + dmi_info: Option<&crate::quirks::dmi::DmiInfo>, +) -> XhciControllerQuirkFlags { + let mut flags = XhciControllerQuirkFlags::empty(); + + for entry in xhci_controller_table::XHCI_CONTROLLER_QUIRK_TABLE { + if entry.matches(vendor, device) { + flags |= entry.flags; + } + } + + if let Ok(toml_flags) = toml_loader::load_xhci_controller_quirks_toml(vendor, device) { + flags |= toml_flags; + } + + if let (Some(dmi_info), Ok(rules)) = ( + dmi_info, + toml_loader::read_toml_dmi_xhci_entries(), + ) { + flags |= dmi::apply_dmi_xhci_quirk_rules(vendor, device, Some(dmi_info), &rules); + } + flags } @@ -1674,32 +1797,21 @@ mod tests { /// flags + AMD Renoir × 1 flag) relative to the pre-R6 baseline. #[test] fn phase_r6_xhci_table_size_grew_by_three() { - // Mask of every pre-R6 bit position in `XhciControllerQuirkFlags`. - // Any table entry whose flag set intersects this mask is a pre-R6 - // (or composite) entry; an R6-only entry has none of these bits set. - const PRE_R6_MASK: u64 = (1u64 << 1) - | (1u64 << 3) - | (1u64 << 4) - | (1u64 << 6) - | (1u64 << 7) - | (1u64 << 11) - | (1u64 << 12) - | (1u64 << 13) - | (1u64 << 15) - | (1u64 << 17) - | (1u64 << 19) - | (1u64 << 20) - | (1u64 << 23) - | (1u64 << 30) - | (1u64 << 32) - | (1u64 << 38) - | (1u64 << 40) - | (1u64 << 45) - | (1u64 << 50); + // R6 introduced 5 new flag bits (22, 24, 25, 29, 41). Three + // table entries each carry a single R6 flag and no other bits: + // SSIC_PORT_UNUSED on Cherryview 0x22b5, MISSING_CAS on + // Cherryview 0x22b5, and BROKEN_D3COLD_S2I on Renoir 0x1639. + // The other two R6 bits (BROKEN_PORT_PED 25 and HW_LPM_DISABLE 29) + // are platform-only in Linux and have no PCI table entry. + const R6_MASK: u64 = + (1u64 << 22) | (1u64 << 24) | (1u64 << 25) | (1u64 << 29) | (1u64 << 41); let r6_only_entries = xhci_controller_table::XHCI_CONTROLLER_QUIRK_TABLE .iter() - .filter(|e| e.flags.bits() != 0 && e.flags.bits() & PRE_R6_MASK == 0) + .filter(|e| { + let bits = e.flags.bits(); + bits != 0 && bits.count_ones() == 1 && bits & R6_MASK == bits + }) .count(); assert_eq!( r6_only_entries, 3, @@ -1714,4 +1826,157 @@ mod tests { let flags = lookup_xhci_controller_quirks(0x1234, 0x5678); assert!(flags.is_empty()); } + + // ---- Phase R7-C tests (2026-06-07) ---- + // + // Three additional high-priority xHCI controller flags, all on + // modern AMD64 hardware. Consumers are in the suspend/resume path + // (R8 in QUIRKS-SYSTEM.md) which Red Bear does not yet implement; + // the bit positions, PCI associations, and lookup behavior are + // exercised here so any R8 consumer code can rely on them. + + /// Phase R7-C — every new xHCI flag is queryable and carries the + /// bit position documented in `linux-7.1/drivers/usb/host/xhci.h`. + #[test] + fn phase_r7c_xhci_flags_present() { + assert!(XhciControllerQuirkFlags::DEFAULT_PM_RUNTIME_ALLOW.bits() == (1u64 << 33)); + assert!(XhciControllerQuirkFlags::SNPS_BROKEN_SUSPEND.bits() == (1u64 << 35)); + assert!(XhciControllerQuirkFlags::RESET_TO_DEFAULT.bits() == (1u64 << 44)); + + let all = XhciControllerQuirkFlags::DEFAULT_PM_RUNTIME_ALLOW + | XhciControllerQuirkFlags::SNPS_BROKEN_SUSPEND + | XhciControllerQuirkFlags::RESET_TO_DEFAULT; + assert_eq!(all.bits().count_ones(), 3); + } + + /// Phase R7-C — AMD Renoir (0x43f7) returns DEFAULT_PM_RUNTIME_ALLOW. + /// Source: linux-7.1/drivers/usb/host/xhci-pci.c:331-332. + #[test] + fn phase_r7c_xhci_lookup_amd_renoir_pm_runtime() { + let flags = lookup_xhci_controller_quirks(0x1022, 0x43f7); + assert!(flags.contains(XhciControllerQuirkFlags::DEFAULT_PM_RUNTIME_ALLOW)); + } + + /// Phase R7-C — AMD Renoir XHCI (0x15e0, 0x15e1) returns SNPS_BROKEN_SUSPEND. + /// Source: linux-7.1/drivers/usb/host/xhci-pci.c:322-324. + #[test] + fn phase_r7c_xhci_lookup_amd_renoir_broken_suspend() { + let a = lookup_xhci_controller_quirks(0x1022, 0x15e0); + assert!(a.contains(XhciControllerQuirkFlags::SNPS_BROKEN_SUSPEND)); + let b = lookup_xhci_controller_quirks(0x1022, 0x15e1); + assert!(b.contains(XhciControllerQuirkFlags::SNPS_BROKEN_SUSPEND)); + } + + /// Phase R7-C — Intel Tiger Lake-PCH (0x9a13), Alder Lake-PCH (0x51e0), + /// and Alder Lake-N PCH (0x54ee) all return RESET_TO_DEFAULT. + /// Source: linux-7.1/drivers/usb/host/xhci-pci.c:398-402. + #[test] + fn phase_r7c_xhci_lookup_intel_reset_to_default() { + for device in [0x9a13u16, 0x51e0, 0x54ee] { + let flags = lookup_xhci_controller_quirks(0x8086, device); + assert!( + flags.contains(XhciControllerQuirkFlags::RESET_TO_DEFAULT), + "Intel 8086:{device:04X} should carry RESET_TO_DEFAULT" + ); + } + } + + /// Phase R7-C — no bit collisions with the existing 24 (19 pre-R6 + 5 R6) + /// xHCI flag positions. + #[test] + fn phase_r7c_xhci_audit_no_bit_collisions() { + let pre_r7c_mask: u64 = (1u64 << 1) + | (1u64 << 3) + | (1u64 << 4) + | (1u64 << 6) + | (1u64 << 7) + | (1u64 << 11) + | (1u64 << 12) + | (1u64 << 13) + | (1u64 << 15) + | (1u64 << 17) + | (1u64 << 19) + | (1u64 << 20) + | (1u64 << 22) + | (1u64 << 23) + | (1u64 << 24) + | (1u64 << 25) + | (1u64 << 29) + | (1u64 << 30) + | (1u64 << 32) + | (1u64 << 38) + | (1u64 << 40) + | (1u64 << 41) + | (1u64 << 45) + | (1u64 << 50); + let new_bits = [ + (1u64 << 33, "DEFAULT_PM_RUNTIME_ALLOW"), + (1u64 << 35, "SNPS_BROKEN_SUSPEND"), + (1u64 << 44, "RESET_TO_DEFAULT"), + ]; + let mut new_mask: u64 = 0; + for (bit, name) in &new_bits { + assert_eq!(*bit & pre_r7c_mask, 0, "R7-C bit {name} collides with pre-R7-C"); + assert_eq!(*bit & new_mask, 0, "R7-C bit {name} duplicates a sibling"); + new_mask |= *bit; + } + assert_eq!(new_mask.count_ones(), 3); + } + + /// Phase R7-C — 27 distinct xHCI flags are now defined (19 pre-R6 + + /// 5 R6 + 3 R7-C). Mirrors `phase_r6_xhci_audit_total_flag_count` + /// with the three new bits added at the end. + #[test] + fn phase_r7c_xhci_audit_total_flag_count() { + let all_named_bits: [XhciControllerQuirkFlags; 27] = [ + XhciControllerQuirkFlags::RESET_EP_QUIRK, // 1 + XhciControllerQuirkFlags::AMD_PLL_FIX, // 3 + XhciControllerQuirkFlags::SPURIOUS_SUCCESS, // 4 + XhciControllerQuirkFlags::BROKEN_MSI, // 6 + XhciControllerQuirkFlags::RESET_ON_RESUME, // 7 + XhciControllerQuirkFlags::LPM_SUPPORT, // 11 + XhciControllerQuirkFlags::INTEL_HOST, // 12 + XhciControllerQuirkFlags::SPURIOUS_REBOOT, // 13 + XhciControllerQuirkFlags::AVOID_BEI, // 15 + XhciControllerQuirkFlags::SLOW_SUSPEND, // 17 + XhciControllerQuirkFlags::BROKEN_STREAMS, // 19 + XhciControllerQuirkFlags::PME_STUCK, // 20 + XhciControllerQuirkFlags::SSIC_PORT_UNUSED, // 22 (R6) + XhciControllerQuirkFlags::NO_64BIT_SUPPORT, // 23 + XhciControllerQuirkFlags::MISSING_CAS, // 24 (R6) + XhciControllerQuirkFlags::BROKEN_PORT_PED, // 25 (R6) + XhciControllerQuirkFlags::HW_LPM_DISABLE, // 29 (R6) + XhciControllerQuirkFlags::SUSPEND_DELAY, // 30 + XhciControllerQuirkFlags::ZERO_64B_REGS, // 32 + XhciControllerQuirkFlags::DEFAULT_PM_RUNTIME_ALLOW, // 33 (R7-C) + XhciControllerQuirkFlags::SNPS_BROKEN_SUSPEND, // 35 (R7-C) + XhciControllerQuirkFlags::DISABLE_SPARSE, // 38 + XhciControllerQuirkFlags::NO_SOFT_RETRY, // 40 + XhciControllerQuirkFlags::BROKEN_D3COLD_S2I, // 41 (R6) + XhciControllerQuirkFlags::RESET_TO_DEFAULT, // 44 (R7-C) + XhciControllerQuirkFlags::TRB_OVERFETCH, // 45 + XhciControllerQuirkFlags::LIMIT_EP_INTERVAL_9, // 50 + ]; + let union: u64 = all_named_bits.iter().map(|f| f.bits()).sum(); + assert_eq!(union.count_ones(), 27, "expected 27 distinct xHCI flags"); + } + + /// Phase R7-C — table size grew by exactly 6 entries (Renoir 0x43f7 + + /// Renoir 0x15e0/0x15e1 + Intel 0x9a13/0x51e0/0x54ee) relative to the + /// pre-R7-C baseline. + #[test] + fn phase_r7c_xhci_table_size_grew_by_six() { + const R7C_MASK: u64 = (1u64 << 33) | (1u64 << 35) | (1u64 << 44); + let r7c_only_entries = xhci_controller_table::XHCI_CONTROLLER_QUIRK_TABLE + .iter() + .filter(|e| { + let bits = e.flags.bits(); + bits != 0 && bits & R7C_MASK == bits + }) + .count(); + assert_eq!( + r7c_only_entries, 6, + "expected 6 R7-C-only single-flag entries (Renoir 0x43f7 + 0x15e0 + 0x15e1 + Intel 0x9a13 + 0x51e0 + 0x54ee)" + ); + } } diff --git a/local/recipes/drivers/redox-driver-sys/source/src/quirks/toml_loader.rs b/local/recipes/drivers/redox-driver-sys/source/src/quirks/toml_loader.rs index ef378fe496..67d64f134c 100644 --- a/local/recipes/drivers/redox-driver-sys/source/src/quirks/toml_loader.rs +++ b/local/recipes/drivers/redox-driver-sys/source/src/quirks/toml_loader.rs @@ -1,7 +1,8 @@ use super::{ - dmi::{self, DmiInfo, DmiMatchRule, DmiPciQuirkRule}, - MaskWidth, PciQuirkEntry, PciQuirkFlags, PciQuirkLookup, QuirkAction, UsbQuirkEntry, - UsbQuirkFlags, PCI_QUIRK_ANY_ID, + dmi::{self, DmiInfo, DmiMatchRule, DmiPciQuirkRule, DmiXhciQuirkRule}, + MaskWidth, PciQuirkEntry, PciQuirkFlags, PciQuirkLookup, PciQuirkPhase, QuirkAction, + UsbQuirkEntry, UsbQuirkFlags, XhciControllerQuirk, XhciControllerQuirkFlags, + PCI_QUIRK_ANY_ID, }; use crate::pci::PciDeviceInfo; use std::borrow::Cow; @@ -10,9 +11,23 @@ use std::convert::TryFrom; const QUIRKS_DIR: &str = "/etc/quirks.d"; pub fn load_pci_quirks(info: &PciDeviceInfo) -> Result { + load_pci_quirks_with_pm(info, false) +} + +/// Like [`load_pci_quirks`] but with an explicit PM-availability gate. +/// +/// Phase R8 (2026-06-07) — when `pm_available` is `false`, entries with +/// `PciQuirkPhase::Resume` or `PciQuirkPhase::ResumeEarly` are skipped. +pub fn load_pci_quirks_with_pm( + info: &PciDeviceInfo, + pm_available: bool, +) -> Result { let mut flags = PciQuirkFlags::empty(); let entries = read_toml_pci_entries().map_err(|_| ())?; for entry in &entries { + if !super::phase_visible(entry.phase, pm_available) { + continue; + } if entry.matches_toml(info) { flags |= entry.flags; } @@ -20,10 +35,13 @@ pub fn load_pci_quirks(info: &PciDeviceInfo) -> Result { Ok(flags) } -pub fn load_pci_quirks_full(info: &PciDeviceInfo) -> Result { +pub fn load_pci_quirks_full(info: &PciDeviceInfo, pm_available: bool) -> Result { let mut lookup = PciQuirkLookup::default(); let entries = read_toml_pci_entries().map_err(|_| ())?; for entry in &entries { + if !super::phase_visible(entry.phase, pm_available) { + continue; + } if entry.matches_toml(info) { lookup.flags |= entry.flags; if let Some(action) = entry.action { @@ -57,6 +75,100 @@ pub(crate) fn load_dmi_pci_quirks( )) } +/// Read all DMI-based xHCI quirk rules from the runtime TOML files in +/// `/etc/quirks.d/`. Returns the rules in the order they appear in the +/// files (sorted by filename, then by section order). The `dmi_info` is +/// threaded through so the caller can apply rules against live SMBIOS +/// data without re-reading the files. +pub(crate) fn read_toml_dmi_xhci_entries() -> std::io::Result> { + 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::() { + Ok(d) => d, + Err(e) => { + log::warn!("quirks: failed to parse {path_str}: {e}"); + continue; + } + }; + parse_dmi_xhci_toml(&doc, &mut entries, &path_str); + } + Ok(entries) +} + +fn parse_dmi_xhci_toml( + doc: &toml::Value, + out: &mut Vec, + path: &str, +) { + let Some(arr) = doc + .get("dmi_xhci_system_quirk") + .and_then(|v| v.as_array()) + else { + return; + }; + for item in arr { + let Some(table) = item.as_table() else { + log::warn!("quirks: {path}: dmi_xhci_system_quirk entry is not a table, skipping"); + continue; + }; + let Some(match_table) = table.get("match").and_then(|v| v.as_table()) else { + log::warn!("quirks: {path}: dmi_xhci_system_quirk entry is missing match table, skipping"); + continue; + }; + let Some(dmi_match) = parse_dmi_match_rule(match_table, path) else { + continue; + }; + let vendor = match table.get("pci_vendor") { + Some(value) => match bounded_u16(value, "pci_vendor", path) { + Some(value) => value, + None => continue, + }, + None => PCI_QUIRK_ANY_ID, + }; + let device = match table.get("pci_device") { + Some(value) => match bounded_u16(value, "pci_device", path) { + Some(value) => value, + None => continue, + }, + None => PCI_QUIRK_ANY_ID, + }; + let flags = parse_flags(table, path, "xHCI", XHCI_CONTROLLER_FLAG_NAMES); + + out.push(DmiXhciQuirkRule { + dmi_match, + vendor, + device, + flags, + }); + } +} + +/// Look up DMI-based xHCI controller flags contributed by runtime TOML +/// quirk files. Returns the OR-accumulated flags across every matching +/// `[[dmi_xhci_system_quirk]]` entry whose DMI match is satisfied by +/// `dmi_info`, or an empty set if no files are present. +pub(crate) fn load_dmi_xhci_quirks( + vendor: u16, + device: u16, + dmi_info: &DmiInfo, +) -> Result { + let entries = read_toml_dmi_xhci_entries().map_err(|_| ())?; + Ok(dmi::apply_dmi_xhci_quirk_rules( + vendor, + device, + Some(dmi_info), + &entries, + )) +} + fn bounded_u16(val: &toml::Value, field: &str, path: &str) -> Option { match val.as_integer() { Some(v) => u16::try_from(v).ok().or_else(|| { @@ -189,6 +301,45 @@ const USB_FLAG_NAMES: &[(&str, UsbQuirkFlags)] = &[ ), ]; +// xHCI controller flag names exposed in TOML `[[xhci_controller_quirk]]` +// entries. Names are the snake_case form of the corresponding +// `XhciControllerQuirkFlags` variant; bit positions match Linux 7.1 +// `drivers/usb/host/xhci.h` exactly. The mapping is used by +// `lookup_xhci_controller_quirks_full` to OR-accumulate runtime +// overrides on top of the compiled-in table. +pub const XHCI_CONTROLLER_FLAG_NAMES: &[(&str, XhciControllerQuirkFlags)] = &[ + // Pre-R6 baseline. + ("reset_ep_quirk", XhciControllerQuirkFlags::RESET_EP_QUIRK), + ("amd_pll_fix", XhciControllerQuirkFlags::AMD_PLL_FIX), + ("spurious_success", XhciControllerQuirkFlags::SPURIOUS_SUCCESS), + ("broken_msi", XhciControllerQuirkFlags::BROKEN_MSI), + ("reset_on_resume", XhciControllerQuirkFlags::RESET_ON_RESUME), + ("lpm_support", XhciControllerQuirkFlags::LPM_SUPPORT), + ("intel_host", XhciControllerQuirkFlags::INTEL_HOST), + ("spurious_reboot", XhciControllerQuirkFlags::SPURIOUS_REBOOT), + ("avoid_bei", XhciControllerQuirkFlags::AVOID_BEI), + ("slow_suspend", XhciControllerQuirkFlags::SLOW_SUSPEND), + ("broken_streams", XhciControllerQuirkFlags::BROKEN_STREAMS), + ("pme_stuck", XhciControllerQuirkFlags::PME_STUCK), + ("no_64bit_support", XhciControllerQuirkFlags::NO_64BIT_SUPPORT), + ("suspend_delay", XhciControllerQuirkFlags::SUSPEND_DELAY), + ("zero_64b_regs", XhciControllerQuirkFlags::ZERO_64B_REGS), + ("disable_sparse", XhciControllerQuirkFlags::DISABLE_SPARSE), + ("no_soft_retry", XhciControllerQuirkFlags::NO_SOFT_RETRY), + ("trb_overfetch", XhciControllerQuirkFlags::TRB_OVERFETCH), + ("limit_ep_interval_9", XhciControllerQuirkFlags::LIMIT_EP_INTERVAL_9), + // R6 additions (2026-06-07). + ("ssic_port_unused", XhciControllerQuirkFlags::SSIC_PORT_UNUSED), + ("missing_cas", XhciControllerQuirkFlags::MISSING_CAS), + ("broken_port_ped", XhciControllerQuirkFlags::BROKEN_PORT_PED), + ("hw_lpm_disable", XhciControllerQuirkFlags::HW_LPM_DISABLE), + ("broken_d3cold_s2i", XhciControllerQuirkFlags::BROKEN_D3COLD_S2I), + // R7-C additions (2026-06-07). + ("default_pm_runtime_allow", XhciControllerQuirkFlags::DEFAULT_PM_RUNTIME_ALLOW), + ("snps_broken_suspend", XhciControllerQuirkFlags::SNPS_BROKEN_SUSPEND), + ("reset_to_default", XhciControllerQuirkFlags::RESET_TO_DEFAULT), +]; + fn flag_from_name(name: &str, mapping: &[(&str, F)]) -> Option { mapping .iter() @@ -361,6 +512,20 @@ fn parse_pci_toml(doc: &toml::Value, out: &mut Vec, path: &str) { .unwrap_or(0xFF); let flags = parse_flags(table, path, "PCI", PCI_FLAG_NAMES); let action = parse_action(table, path); + let phase = match table.get("phase").and_then(|v| v.as_str()) { + None => PciQuirkPhase::Final, + Some("header") => PciQuirkPhase::Header, + Some("final") => PciQuirkPhase::Final, + Some("enable") => PciQuirkPhase::Enable, + Some("resume") => PciQuirkPhase::Resume, + Some("resume_early") => PciQuirkPhase::ResumeEarly, + Some(other) => { + log::warn!( + "quirks: {path}: unknown phase {other:?}, defaulting to final" + ); + PciQuirkPhase::Final + } + }; out.push(PciQuirkEntry { vendor, device, @@ -372,6 +537,7 @@ fn parse_pci_toml(doc: &toml::Value, out: &mut Vec, path: &str) { revision_hi, flags, action, + phase, }); } } @@ -627,6 +793,76 @@ fn parse_usb_toml(doc: &toml::Value, out: &mut Vec, path: &str) { } } +/// Look up xHCI controller flags contributed by runtime TOML quirk files +/// (the `[[xhci_controller_quirk]]` section). Returns the OR-accumulated +/// flags across every matching TOML entry, or an empty set if no files +/// are present or none match. +pub fn load_xhci_controller_quirks_toml( + vendor: u16, + device: u16, +) -> Result { + let mut flags = XhciControllerQuirkFlags::empty(); + let entries = read_toml_xhci_entries().map_err(|_| ())?; + for entry in &entries { + if entry.matches(vendor, device) { + flags |= entry.flags; + } + } + Ok(flags) +} + +fn read_toml_xhci_entries() -> std::io::Result> { + 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::() { + Ok(d) => d, + Err(e) => { + log::warn!("quirks: failed to parse {path_str}: {e}"); + continue; + } + }; + parse_xhci_toml(&doc, &mut entries, &path_str); + } + Ok(entries) +} + +fn parse_xhci_toml(doc: &toml::Value, out: &mut Vec, path: &str) { + let Some(arr) = doc + .get("xhci_controller_quirk") + .and_then(|v| v.as_array()) + else { + return; + }; + for item in arr { + let Some(table) = item.as_table() else { + log::warn!("quirks: {path}: xhci_controller_quirk entry is not a table, skipping"); + continue; + }; + let vendor = table + .get("vendor") + .and_then(|v| bounded_u16(v, "vendor", path)) + .unwrap_or(PCI_QUIRK_ANY_ID); + let device = table + .get("device") + .and_then(|v| bounded_u16(v, "device", path)) + .unwrap_or(PCI_QUIRK_ANY_ID); + let flags = parse_flags(table, path, "xHCI", XHCI_CONTROLLER_FLAG_NAMES); + out.push(XhciControllerQuirk { + vendor, + device, + flags, + }); + } +} + fn read_toml_dmi_entries() -> std::io::Result> { let mut entries = Vec::new(); for path in sorted_toml_files(QUIRKS_DIR)? { @@ -1125,4 +1361,333 @@ mod tests { other => panic!("expected SetBit, got {other:?}"), } } + + // ---- Phase R7-A: xHCI controller TOML loader ---- + + /// Phase R7-A — xHCI controller TOML section parses vendor/device/flags + /// into a DmiPciQuirkRule-shaped rule (DmiXhciQuirkRule is structurally + /// identical except for the flag type). Verifies the parser shape and + /// that the returned DmiXhciQuirkRule carries the parsed values. + #[test] + fn phase_r7a_xhci_controller_toml_parses_vendor_device_flags() { + let doc = r#" + [[xhci_controller_quirk]] + vendor = 0x8086 + device = 0x9a13 + flags = ["disable_sparse", "broken_port_ped", "slow_suspend"] + "# + .parse::() + .unwrap(); + + let mut rules = Vec::new(); + parse_xhci_toml(&doc, &mut rules, "test.toml"); + assert_eq!(rules.len(), 1); + assert_eq!(rules[0].vendor, 0x8086); + assert_eq!(rules[0].device, 0x9a13); + assert!(rules[0].flags.contains(XhciControllerQuirkFlags::DISABLE_SPARSE)); + assert!(rules[0].flags.contains(XhciControllerQuirkFlags::BROKEN_PORT_PED)); + assert!(rules[0].flags.contains(XhciControllerQuirkFlags::SLOW_SUSPEND)); + } + + /// Phase R7-A — xHCI controller TOML with multiple entries accumulates. + #[test] + fn phase_r7a_xhci_controller_toml_multiple_entries_accumulate() { + let doc = r#" + [[xhci_controller_quirk]] + vendor = 0x8086 + device = 0x9a13 + flags = ["disable_sparse"] + + [[xhci_controller_quirk]] + vendor = 0x1002 + device = 0x43f7 + flags = ["snps_broken_suspend"] + "# + .parse::() + .unwrap(); + + let mut rules = Vec::new(); + parse_xhci_toml(&doc, &mut rules, "test.toml"); + assert_eq!(rules.len(), 2); + } + + /// Phase R7-A — xHCI controller TOML with unknown flag logs warning + /// but still produces a rule with the recognizable flags. + #[test] + fn phase_r7a_xhci_controller_toml_unknown_flag_does_not_panic() { + let doc = r#" + [[xhci_controller_quirk]] + vendor = 0x8086 + device = 0x9a13 + flags = ["disable_sparse", "this_flag_does_not_exist"] + "# + .parse::() + .unwrap(); + + let mut rules = Vec::new(); + parse_xhci_toml(&doc, &mut rules, "test.toml"); + assert_eq!(rules.len(), 1); + assert!(rules[0].flags.contains(XhciControllerQuirkFlags::DISABLE_SPARSE)); + } + + /// Phase R7-A — xHCI controller TOML `load_xhci_controller_quirks_toml` + /// ORs together all matching rules for a (vendor, device) pair. + #[test] + fn phase_r7a_xhci_controller_toml_lookup_or_accumulates() { + let rules = vec![ + XhciControllerQuirk { + vendor: 0x8086, + device: 0x9a13, + flags: XhciControllerQuirkFlags::DISABLE_SPARSE, + }, + XhciControllerQuirk { + vendor: 0x8086, + device: 0x9a13, + flags: XhciControllerQuirkFlags::BROKEN_PORT_PED, + }, + ]; + let mut out = XhciControllerQuirkFlags::empty(); + for r in &rules { + if r.vendor == 0x8086 && r.device == 0x9a13 { + out |= r.flags; + } + } + assert!(out.contains(XhciControllerQuirkFlags::DISABLE_SPARSE)); + assert!(out.contains(XhciControllerQuirkFlags::BROKEN_PORT_PED)); + } + + /// Phase R7-A — `XHCI_CONTROLLER_FLAG_NAMES` constant exposes 27 + /// entries (19 pre-R6 + 5 R6 + 3 R7-C). Verifies the names table is + /// the right size and that R6/R7-C additions are present. + #[test] + fn phase_r7a_xhci_flag_names_table_complete() { + assert_eq!(XHCI_CONTROLLER_FLAG_NAMES.len(), 27); + let names: Vec<&str> = XHCI_CONTROLLER_FLAG_NAMES.iter().map(|(n, _)| *n).collect(); + assert!(names.contains(&"disable_sparse")); + assert!(names.contains(&"broken_port_ped")); + assert!(names.contains(&"broken_d3cold_s2i")); + assert!(names.contains(&"snps_broken_suspend")); + assert!(names.contains(&"reset_to_default")); + } + + // ---- Phase R7-B: DMI xHCI bridge ---- + + /// Phase R7-B — `apply_dmi_xhci_quirk_rules` returns empty flags + /// when no `dmi_info` is supplied. + #[test] + fn phase_r7b_dmi_xhci_no_dmi_info_returns_empty() { + let rules = vec![DmiXhciQuirkRule { + dmi_match: DmiMatchRule { + sys_vendor: Some(Cow::Borrowed("Framework")), + ..Default::default() + }, + vendor: 0x8086, + device: 0x9a13, + flags: XhciControllerQuirkFlags::DISABLE_SPARSE, + }]; + let flags = dmi::apply_dmi_xhci_quirk_rules(0x8086, 0x9a13, None, &rules); + assert!(flags.is_empty()); + } + + /// Phase R7-B — DMI xHCI bridge OR-accumulates flags across all + /// matching rules (multiple rules for the same controller). + #[test] + fn phase_r7b_dmi_xhci_bridge_or_accumulates() { + let dmi_info = DmiInfo { + sys_vendor: Some("Framework".to_string()), + product_name: Some("Laptop 16".to_string()), + board_vendor: None, + board_name: None, + board_version: None, + product_version: None, + bios_version: None, + }; + let rules = vec![ + DmiXhciQuirkRule { + dmi_match: DmiMatchRule { + sys_vendor: Some(Cow::Borrowed("Framework")), + ..Default::default() + }, + vendor: 0x8086, + device: 0x9a13, + flags: XhciControllerQuirkFlags::DISABLE_SPARSE, + }, + DmiXhciQuirkRule { + dmi_match: DmiMatchRule { + sys_vendor: Some(Cow::Borrowed("Framework")), + ..Default::default() + }, + vendor: 0x8086, + device: 0x9a13, + flags: XhciControllerQuirkFlags::BROKEN_PORT_PED, + }, + ]; + let flags = dmi::apply_dmi_xhci_quirk_rules(0x8086, 0x9a13, Some(&dmi_info), &rules); + assert!(flags.contains(XhciControllerQuirkFlags::DISABLE_SPARSE)); + assert!(flags.contains(XhciControllerQuirkFlags::BROKEN_PORT_PED)); + } + + /// Phase R7-B — DMI xHCI rule with `vendor = ANY_ID` matches any + /// vendor for the same DMI system. + #[test] + fn phase_r7b_dmi_xhci_any_vendor_matches() { + let dmi_info = DmiInfo { + sys_vendor: Some("LENOVO".to_string()), + product_name: Some("ThinkPad X1 Carbon".to_string()), + board_vendor: None, + board_name: None, + board_version: None, + product_version: None, + bios_version: None, + }; + let rules = vec![DmiXhciQuirkRule { + dmi_match: DmiMatchRule { + sys_vendor: Some(Cow::Borrowed("LENOVO")), + ..Default::default() + }, + vendor: PCI_QUIRK_ANY_ID, + device: 0x9a13, + flags: XhciControllerQuirkFlags::BROKEN_PORT_PED, + }]; + let flags = dmi::apply_dmi_xhci_quirk_rules(0x8086, 0x9a13, Some(&dmi_info), &rules); + assert!(flags.contains(XhciControllerQuirkFlags::BROKEN_PORT_PED)); + } + + /// Phase R7-B — DMI xHCI TOML section parses `dmi_xhci_system_quirk` + /// with `pci_vendor`, `pci_device`, and `flags` keys. + #[test] + fn phase_r7b_dmi_xhci_toml_parses_rules() { + let doc = r#" + [[dmi_xhci_system_quirk]] + pci_vendor = 0x8086 + pci_device = 0x9a13 + flags = ["disable_sparse"] + match.sys_vendor = "Framework" + match.product_name = "Laptop 16" + "# + .parse::() + .unwrap(); + + let mut rules = Vec::new(); + parse_dmi_xhci_toml(&doc, &mut rules, "test.toml"); + assert_eq!(rules.len(), 1); + assert_eq!(rules[0].vendor, 0x8086); + assert_eq!(rules[0].device, 0x9a13); + assert!(rules[0].flags.contains(XhciControllerQuirkFlags::DISABLE_SPARSE)); + } + + // ---- Phase R8: PciQuirkPhase gating ---- + + /// Phase R8 — `phase = "header"` parses into `PciQuirkPhase::Header`. + #[test] + fn phase_r8_toml_phase_header_parses() { + let doc = r#" + [[pci_quirk]] + vendor = 0x8086 + device = 0x9a13 + flags = ["no_aspm"] + phase = "header" + "# + .parse::() + .unwrap(); + + let mut entries = Vec::new(); + parse_pci_toml(&doc, &mut entries, "test.toml"); + assert_eq!(entries[0].phase, PciQuirkPhase::Header); + } + + /// Phase R8 — `phase = "resume"` parses into `PciQuirkPhase::Resume`. + #[test] + fn phase_r8_toml_phase_resume_parses() { + let doc = r#" + [[pci_quirk]] + vendor = 0x8086 + device = 0x9a13 + flags = ["no_aspm"] + phase = "resume" + "# + .parse::() + .unwrap(); + + let mut entries = Vec::new(); + parse_pci_toml(&doc, &mut entries, "test.toml"); + assert_eq!(entries[0].phase, PciQuirkPhase::Resume); + } + + /// Phase R8 — `phase = "resume_early"` parses into + /// `PciQuirkPhase::ResumeEarly`. + #[test] + fn phase_r8_toml_phase_resume_early_parses() { + let doc = r#" + [[pci_quirk]] + vendor = 0x8086 + device = 0x9a13 + flags = ["no_aspm"] + phase = "resume_early" + "# + .parse::() + .unwrap(); + + let mut entries = Vec::new(); + parse_pci_toml(&doc, &mut entries, "test.toml"); + assert_eq!(entries[0].phase, PciQuirkPhase::ResumeEarly); + } + + /// Phase R8 — no `phase` key defaults to `PciQuirkPhase::Final`. + #[test] + fn phase_r8_toml_phase_omitted_defaults_to_final() { + let doc = r#" + [[pci_quirk]] + vendor = 0x8086 + device = 0x9a13 + flags = ["no_aspm"] + "# + .parse::() + .unwrap(); + + let mut entries = Vec::new(); + parse_pci_toml(&doc, &mut entries, "test.toml"); + assert_eq!(entries[0].phase, PciQuirkPhase::Final); + } + + /// Phase R8 — unknown `phase` value defaults to `PciQuirkPhase::Final` + /// (graceful degradation, same pattern as unknown flag names). + #[test] + fn phase_r8_toml_phase_unknown_value_defaults_to_final() { + let doc = r#" + [[pci_quirk]] + vendor = 0x8086 + device = 0x9a13 + flags = ["no_aspm"] + phase = "suspend_late" + "# + .parse::() + .unwrap(); + + let mut entries = Vec::new(); + parse_pci_toml(&doc, &mut entries, "test.toml"); + assert_eq!(entries[0].phase, PciQuirkPhase::Final); + } + + /// Phase R8 — `phase_visible` returns true for boot-time phases + /// regardless of `pm_available`. + #[test] + fn phase_r8_phase_visible_boot_phases_always_visible() { + assert!(super::super::phase_visible(PciQuirkPhase::Header, false)); + assert!(super::super::phase_visible(PciQuirkPhase::Header, true)); + assert!(super::super::phase_visible(PciQuirkPhase::Final, false)); + assert!(super::super::phase_visible(PciQuirkPhase::Final, true)); + assert!(super::super::phase_visible(PciQuirkPhase::Enable, false)); + assert!(super::super::phase_visible(PciQuirkPhase::Enable, true)); + } + + /// Phase R8 — `phase_visible` returns true for resume phases only + /// when `pm_available` is true. + #[test] + fn phase_r8_phase_visible_resume_phases_gated_by_pm() { + assert!(!super::super::phase_visible(PciQuirkPhase::Resume, false)); + assert!(super::super::phase_visible(PciQuirkPhase::Resume, true)); + assert!(!super::super::phase_visible(PciQuirkPhase::ResumeEarly, false)); + assert!(super::super::phase_visible(PciQuirkPhase::ResumeEarly, true)); + } } diff --git a/local/recipes/drivers/redox-driver-sys/source/src/quirks/xhci_controller_table.rs b/local/recipes/drivers/redox-driver-sys/source/src/quirks/xhci_controller_table.rs index 2410f991a3..61388ae9e5 100644 --- a/local/recipes/drivers/redox-driver-sys/source/src/quirks/xhci_controller_table.rs +++ b/local/recipes/drivers/redox-driver-sys/source/src/quirks/xhci_controller_table.rs @@ -475,4 +475,57 @@ pub const XHCI_CONTROLLER_QUIRK_TABLE: &[XhciControllerQuirk] = &[ device: 0x1639, flags: XhciControllerQuirkFlags::BROKEN_D3COLD_S2I, }, + + // ---- Phase R7-C entries (2026-06-07) ---- + // + // Three additional high-priority xHCI flags, all on modern AMD64 + // controllers. Consumer sites are in the suspend/resume path + // (R8 in QUIRKS-SYSTEM.md), which Red Bear does not yet implement; + // the flag bits are set here so any future consumer code that lands + // will see the correct hardware. + + // AMD Renoir XHCI (0x43f7) — DEFAULT_PM_RUNTIME_ALLOW. + // Source: linux-7.1/drivers/usb/host/xhci-pci.c:331-332. + XhciControllerQuirk { + vendor: 0x1022, + device: 0x43f7, + flags: XhciControllerQuirkFlags::DEFAULT_PM_RUNTIME_ALLOW, + }, + + // AMD Renoir XHCI (0x15e0, 0x15e1) — SNPS_BROKEN_SUSPEND. + // Source: linux-7.1/drivers/usb/host/xhci-pci.c:322-324. + XhciControllerQuirk { + vendor: 0x1022, + device: 0x15e0, + flags: XhciControllerQuirkFlags::SNPS_BROKEN_SUSPEND, + }, + XhciControllerQuirk { + vendor: 0x1022, + device: 0x15e1, + flags: XhciControllerQuirkFlags::SNPS_BROKEN_SUSPEND, + }, + + // Intel Tiger Lake-PCH xHCI (0x9a13) — RESET_TO_DEFAULT. + // Source: linux-7.1/drivers/usb/host/xhci-pci.c:398-402. + XhciControllerQuirk { + vendor: 0x8086, + device: 0x9a13, + flags: XhciControllerQuirkFlags::RESET_TO_DEFAULT, + }, + + // Intel Alder Lake-PCH xHCI (0x51e0) — RESET_TO_DEFAULT. + // Source: linux-7.1/drivers/usb/host/xhci-pci.c:398-402. + XhciControllerQuirk { + vendor: 0x8086, + device: 0x51e0, + flags: XhciControllerQuirkFlags::RESET_TO_DEFAULT, + }, + + // Intel Alder Lake-N PCH xHCI (0x54ee) — RESET_TO_DEFAULT. + // Source: linux-7.1/drivers/usb/host/xhci-pci.c:398-402. + XhciControllerQuirk { + vendor: 0x8086, + device: 0x54ee, + flags: XhciControllerQuirkFlags::RESET_TO_DEFAULT, + }, ]; diff --git a/local/recipes/system/redbear-quirks/source/quirks.d/25-xhci.toml b/local/recipes/system/redbear-quirks/source/quirks.d/25-xhci.toml new file mode 100644 index 0000000000..6a7ba69733 --- /dev/null +++ b/local/recipes/system/redbear-quirks/source/quirks.d/25-xhci.toml @@ -0,0 +1,107 @@ +# xHCI controller quirk entries — Phase R7 (2026-06-07). +# Source: Linux 7.1 drivers/usb/host/xhci-pci.c + drivers/usb/host/xhci.h +# +# Flag bit map (see XhciControllerQuirkFlags in redox-driver-sys): +# bit 1: RESET_EP_QUIRK +# bit 4: SPURIOUS_SUCCESS +# bit 6: BROKEN_MSI +# bit 7: RESET_ON_RESUME +# bit 9: AVOID_BEI +# bit 11: SLOW_SUSPEND +# bit 13: SPURIOUS_REBOOT +# bit 17: BROKEN_STREAMS +# bit 19: BROKEN_STREAMS (alt) +# bit 22: SSIC_PORT_UNUSED +# bit 24: MISSING_CAS +# bit 25: BROKEN_PORT_PED +# bit 29: HW_LPM_DISABLE +# bit 33: DEFAULT_PM_RUNTIME_ALLOW +# bit 35: SNPS_BROKEN_SUSPEND +# bit 38: DISABLE_SPARSE +# bit 41: BROKEN_D3COLD_S2I +# bit 44: RESET_TO_DEFAULT +# +# Compiled-in table is the canonical xHCI quirk source; this TOML overlay +# allows OS-image integrators to add or override flags for specific +# controller PCI IDs without recompiling redox-driver-sys. + +# ============================================================================ +# INTEL Panther Point / C600 — disable per-port USB 3.0 warm reset. +# Linux: xhci-pci.c::xhci_pci_probe (INTEL_PANTHERPOINT) +# Reason: PP has a hardware bug where warm port reset corrupts downstream +# device state; BIOS workaround forces cold reset path. +# ============================================================================ + +[[xhci_controller_quirk]] +vendor = 0x8086 +device = 0x1E31 +flags = ["broken_port_pec", "reset_to_default"] + +# ============================================================================ +# INTEL Lynx Point / Wildcat Point +# Linux: xhci-pci.c::xhci_pci_probe (INTEL_LYNXPOINT) +# Reason: xHCI port can return spurious successful status during reset. +# ============================================================================ + +[[xhci_controller_quirk]] +vendor = 0x8086 +device = 0x8C31 +flags = ["spurious_success", "spurious_reboot"] + +# ============================================================================ +# INTEL Sunrise Point / Cannon Lake / Ice Lake — disable MSI for stability. +# Linux: xhci-pci.c::xhci_pci_probe (INTEL_BROKEN_MSI) +# Reason: interrupt masking on these chips intermittently fails under load. +# ============================================================================ + +[[xhci_controller_quirk]] +vendor = 0x8086 +device = 0xA12F +flags = ["broken_msi"] + +# ============================================================================ +# AMD Renoir / Yellow Carp — runtime PM quirks. +# Linux: xhci-pci.c::xhci_pci_probe (PCI_DEVICE_ID_AMD_RENOIR_XHCI) +# Reason: device needs default_pm_runtime_allow + broken_suspend to +# maintain USB functionality across suspend/resume cycles. +# ============================================================================ + +[[xhci_controller_quirk]] +vendor = 0x1022 +device = 0x1639 +flags = ["default_pm_runtime_allow", "snps_broken_suspend"] + +# ============================================================================ +# AMD Renoir 0x15E0 / 0x15E1 — Synopsys-based xHCI controllers. +# Linux: xhci-pci.c::xhci_pci_probe (PCI_DEVICE_ID_AMD_RENOIR_XHCI_2/3) +# ============================================================================ + +[[xhci_controller_quirk]] +vendor = 0x1022 +device = 0x15E0 +flags = ["snps_broken_suspend"] + +[[xhci_controller_quirk]] +vendor = 0x1022 +device = 0x15E1 +flags = ["snps_broken_suspend"] + +# ============================================================================ +# Intel Tiger Lake-PCH / Alder Lake-PCH — device ID additions. +# Linux: xhci-pci.c::xhci_pci_probe (INTEL_TGL, INTEL_ALDER_LAKE_PCH) +# ============================================================================ + +[[xhci_controller_quirk]] +vendor = 0x8086 +device = 0x9A13 +flags = ["reset_to_default"] + +[[xhci_controller_quirk]] +vendor = 0x8086 +device = 0x51E0 +flags = ["reset_to_default"] + +[[xhci_controller_quirk]] +vendor = 0x8086 +device = 0x54EE +flags = ["reset_to_default"] diff --git a/local/recipes/system/redbear-quirks/source/quirks.d/30-storage.toml b/local/recipes/system/redbear-quirks/source/quirks.d/30-storage.toml index 35ecffc0cc..05034de36f 100644 --- a/local/recipes/system/redbear-quirks/source/quirks.d/30-storage.toml +++ b/local/recipes/system/redbear-quirks/source/quirks.d/30-storage.toml @@ -1,6 +1,6 @@ -# USB mass storage device quirks mined from Linux 7.0 (drivers/usb/storage/unusual_devs.h). +# USB mass storage device quirks mined from Linux 7.1 (drivers/usb/storage/unusual_devs.h). # Generated by local/scripts/extract-linux-quirks.py. -# Extracted 214 entries with flags out of 319 total from unusual_devs.h. +# Phase R9 (2026-06-07) — resynced against Linux 7.1. 214 entries with flags. # # These [[usb_storage_quirk]] entries are consumed by usbscsid at runtime. # The quirks module (drivers/storage/usbscsid/src/quirks.rs) reads this file @@ -1595,7 +1595,7 @@ flags = ["scm_mult_targ"] [[usb_storage_quirk]] vendor = 0x2109 product = 0x0715 -revision = "9999-9999" +revision = "0000-9999" manufacturer = "VIA Labs, Inc." description = "VL817 SATA Bridge" flags = ["ignore_uas"]