quirks: universal consumption model — open driver-scoped domain channel

Redesign grounded in the Linux 7.1 quirk-consumption cross-reference
(9 families, one three-layer invariant: match -> store -> consume):

- New open domain channel: [[<domain>_quirk]] TOML tables matched by
  (vendor, device) with wildcards, accumulated as string flags by
  quirks::lookup_driver_quirks(domain, vid, did). The table name IS the
  domain key — new driver domains need zero registry code (Linux Type-C
  model: driver-owned fixup tables like HDA's per-codec lists).
- lookup_audio_quirks(vid, did) as the first domain convenience API for
  ihdad's future integration.
- quirks.d/15-audio.toml converted from [[pci_quirk]] to [[audio_quirk]]
  — the audio_* flags (force_eapd, single_cmd, position_fix_lpib,
  mirroring Linux sound/pci/hda) are now carried as data instead of
  warned-and-dropped on every probe cycle.
- Docs: three-layer model in QUIRKS-SYSTEM.md + consumption contract
  (bind-time / core-runtime / driver-runtime); QUIRKS-IMPROVEMENT-PLAN.md
  v2.0 with the redesign assessment and cutover-era reality (stale
  pcid-spawner broker references removed).
- 75/75 redox-driver-sys tests pass (new domain loader test).
This commit is contained in:
2026-07-24 08:55:49 +09:00
parent 326df7f02d
commit 6d978068f0
5 changed files with 267 additions and 17 deletions
+81 -5
View File
@@ -9,6 +9,79 @@ This plan replaces vague “quirks support” follow-up work with a concrete pat
3. reduce duplicated quirk logic,
4. leave DMI and USB device quirks in a maintainable state.
**v2.0 update (2026-07-24):** adds the **universal quirk-consumption model** (§ below) — a
redesign assessment grounded in a full cross-reference of Linux 7.1 quirk consumption across
all 9 quirk families (PCI, USB, HDA, DMI, ATA, NVMe, HID, ACPI, MMC), and the first
implementation of it: the open driver-scoped domain channel (`[[<domain>_quirk]]` tables +
`quirks::lookup_driver_quirks`), which also resolves the `audio_*` unknown-flag drift.
## Universal quirk-consumption model (v2.0 redesign assessment)
### What the Linux 7.1 cross-reference established
Linux has **no unified registry** — 9 separate per-subsystem tables. But every one of them
follows the same three-layer invariant:
```
[1] MATCH (typed keys: VID/DID, class, subsystem IDs, model strings, DMI, CID/dates)
[2] STORE (flag bits or fixup pointers attached to the device object)
[3] CONSUME (the subsystem CORE or the owning driver, at FIXED lifecycle points)
```
Three consumption classes exist:
| Class | Linux example | Mechanism |
|---|---|---|
| **A. Core lifecycle callbacks** | PCI `DECLARE_PCI_FIXUP_*` (8 passes: early/header/final/enable/resume/...) | Core invokes quirk *functions* at lifecycle points; drivers never look anything up |
| **B. Core flag checks** | USB `usb_detect_quirks`, NVMe `.driver_data`, HID `hid_lookup_quirk`, ATA `ata_dev_quirks` | Flags set on the device object at enumeration; the subsystem core checks them at behavior points |
| **C. Driver-owned domain tables** | HDA `snd_hda_pick_fixup` (5 actions: PRE_PROBE/PROBE/BUILD/INIT/FREE) | The driver matches its OWN table at its own lifecycle points |
### Red Bear's position (post-cutover, 2026-07-24)
Red Bear diverges from Linux deliberately and correctly by having **one unified registry**
(`redox-driver-sys`: compiled tables + TOML + DMI) instead of N tables. The registry is the
right base; what was missing is the lifecycle model and the Type-C driver-domain channel.
**The redesigned model — one registry, three consumption classes:**
1. **Bind-time (Class B, owned by driver-manager).** Spawn-time quirk decisions: env hints
(`REDBEAR_DRIVER_PCI_IRQ_MODE`, `DISABLE_ACCEL`, `IOMMU_GROUP`, `NUMA_NODE`,
`MSIX_VECTORS`), `NEED_FIRMWARE` deferral, blacklist interplay. Implemented and
QEMU-validated in the cutover.
2. **Core-runtime (Class B, owned by subsystem daemons).** Each subsystem daemon queries the
registry at its own behavior points: `xhcid` (51-flag xHCI table), `redox-drm`
(IRQ/accel), `linux-kpi` C drivers (`pci_has_quirk()`), `acpid` (system quirks), and —
planned — `pcid` for config-space behaviors and `evdevd`/HID for input quirks.
3. **Driver-runtime (Class C, NEW — open domain channel).** Driver-owned domains carried as
string flags keyed by domain name: `[[<domain>_quirk]]` TOML tables, matched by
`(vendor, device)` with wildcards, accumulated by
`quirks::lookup_driver_quirks(domain, vid, did) -> Vec<String>`. **New domains need zero
registry code — the table name is the domain key.** First instance: `[[audio_quirk]]`
(`quirks.d/15-audio.toml`) with `quirks::lookup_audio_quirks()` for ihdad's future
integration, mirroring Linux's per-codec HDA fixup model.
### What this changes vs the v1 plan
- The v1 plan's "brokered quirk bits from **pcid-spawner**" is **stale** — pcid-spawner is
retired (gated fallback); driver-manager is the bind-time consumer now, and the broker
concept is subsumed by Class 1 above.
- The v1 plan had no answer for driver-specific quirks (audio) — they warned as unknown
flags and were dropped. Class 3 is the answer: carried, queryable, zero-warning.
- Lifecycle beyond spawn (Linux Class A phases: early pre-claim vs enable vs suspend/resume)
remains a **next-milestone** item: `phase = "early"|"enable"` on entries, with
driver-manager consulting the registry during enumeration (early) in addition to spawn
(enable). PM phases wait for the power-management roadmap.
### Consumption contract per class (who may query what)
| Consumer | API | Class |
|---|---|---|
| driver-manager (spawn) | `lookup_pci_quirks` → env hints, deferrals | 1 |
| Rust drivers/daemons | `info.quirks()`, `lookup_usb_quirks`, `lookup_xhci_controller_quirks_full`, `lookup_hid_quirks` | 2 |
| C drivers via linux-kpi | `pci_has_quirk()` / `pci_get_quirk_flags()` | 2 |
| Driver-owned domains | `lookup_driver_quirks(domain, vid, did)`, `lookup_audio_quirks(vid, did)` | 3 |
| Tooling (lspci/lsusb/info) | same lookups, display-only | — |
## Current status snapshot
Completed from this plan:
@@ -43,9 +116,11 @@ shipped paths.
It is based on the current in-tree state of:
- `redox-driver-sys` as the canonical quirks library,
- `pcid-spawner` as an upstream-owned PCI launch broker that now brokers canonical quirks,
- `redox-drm`, `xhcid`, and the amdgpu Redox glue/runtime path as real runtime PCI quirk consumers,
- `redox-driver-sys` as the canonical quirks library (three-layer model: match → store →
consume; typed bitflag domains plus the open driver-scoped domain channel),
- `driver-manager` as the bind-time quirk consumer (spawn env hints, deferrals) — validated
in the 2026-07-23/24 QEMU gate,
- `redox-drm`, `xhcid`, and the amdgpu Redox glue/runtime path as runtime quirk consumers,
- `lspci`, `lsusb`, and `redbear-info` as reporting surfaces.
## Reassessment Summary
@@ -63,7 +138,6 @@ It is based on the current in-tree state of:
- USB quirks now have a first real runtime consumer in `xhcid`, but broader USB-driver adoption is still missing.
- The `linux-kpi` bridge now has a first real in-tree C consumer: amdgpu uses it for quirk-aware IRQ expectation logging. Broader C-driver adoption is still missing.
- `pcid-spawner` still synthesizes a partial `PciDeviceInfo` instead of reusing a richer canonical PCI object, because it operates as an upstream-owned broker with a narrow interface.
### What should not be “fixed” in the wrong layer
@@ -97,7 +171,9 @@ All other code should either:
- **Rust PCI drivers using `redox-driver-sys`** should call `info.quirks()` directly.
- **C drivers using `linux-kpi`** should call `pci_has_quirk()` / `pci_get_quirk_flags()` directly in probe/init paths.
- **Upstream base drivers that cannot depend on `redox-driver-sys`** may continue using brokered quirk bits from `pcid-spawner`, but only if that broker is made semantically identical to the canonical library.
- **Driver-owned domain quirks** (audio, future nvme/ata) use
`lookup_driver_quirks(domain, vendor, device)` / `lookup_audio_quirks(vendor, device)`
see the universal model § above.
- **USB device quirks** should be consumed inside `xhcid` device enumeration/configuration logic, not only in tooling.
## Concrete Work Plan
+50 -5
View File
@@ -15,6 +15,25 @@ For the current follow-up cleanup and integration roadmap, see
## Architecture
The system follows the same three-layer invariant as every Linux quirk family
(cross-referenced 2026-07-24 against Linux 7.1 PCI/USB/HDA/DMI/ATA/NVMe/HID/ACPI/MMC):
```
[1] MATCH typed keys — (vendor, device, subvendor/subdevice, class, revision, DMI)
[2] STORE flag bits (typed domains) or string flags (open driver domains)
[3] CONSUME a fixed consumer at a fixed point — never ad-hoc lookups
```
Consumption contract (who consumes, at which point):
| Class | Consumer | Point | Mechanism |
|---|---|---|---|
| Bind-time | `driver-manager` | probe/spawn | env hints (`REDBEAR_DRIVER_*`), `NEED_FIRMWARE` deferral, blacklist |
| Core-runtime | subsystem daemons (`xhcid`, `redox-drm`, `acpid`, linux-kpi C drivers) | their own behavior points | `info.quirks()`, `lookup_usb_quirks`, `lookup_xhci_controller_quirks_full`, `pci_has_quirk()` |
| Driver-runtime | the owning driver (e.g. ihdad, future nvme/ata) | driver init | `lookup_driver_quirks(domain, vid, did)` — open `[[<domain>_quirk]]` tables |
Lookup flow for typed PCI flags:
```
Driver probes device
└─ PciDeviceInfo::quirks()
@@ -28,6 +47,29 @@ All matching entries accumulate via bitwise OR, so broad rules (e.g., "all AMD G
need firmware") and narrow rules (e.g., "this specific revision has broken MSI-X")
compose naturally.
## Open driver-scoped domains (Type C, 2026-07-24)
Driver-owned quirk domains follow Linux's Type-C model (driver-owned fixup tables,
e.g. HDA's per-codec fixup lists). The registry matches and carries plain string
flags; the owning driver is the sole consumer and semantic owner:
```toml
[[audio_quirk]]
vendor = 0x8086
device = 0x284b
flags = ["audio_force_eapd"]
```
- Table name = domain key: any `[[<domain>_quirk]]` table works with **zero registry
code** for a new driver domain.
- Match keys: `vendor`, `device` (hex, `0xFFFF` wildcard).
- Query: `redox_driver_sys::quirks::lookup_driver_quirks(domain, vendor, device)
-> Vec<String>`; convenience: `lookup_audio_quirks(vendor, device)`.
- First instance: `quirks.d/15-audio.toml` (HDA quirks: force-EAPD, single-command,
position-fix-LPIB — mirrors Linux `sound/pci/hda` device quirks). ihdad consumes
these when its quirk integration lands; the flags were previously dropped with
"unknown flag" warnings.
## Quirk Sources
### 1. Compiled-in Tables
@@ -470,7 +512,7 @@ cargo test
| Q3 | xhcid PCI controller quirks (interrupt + reset delay) | ✅ Done |
| Q3 | xhcid xHCI controller quirks — canonical 51-flag table (Linux 7.1 port, ~85 entries) | ✅ Done (2026-07-18, P2-A) |
| Q3 | xhcid USB device quirks (descriptor/configuration/BOS handling) | ✅ Done |
| Q3 | pcid-spawner quirk passthrough | ✅ Done |
| Q3 | driver-manager bind-time consumption (spawn env hints, NEED_FIRMWARE deferral) | ✅ Done — QEMU-validated 2026-07-23/24 |
| Q3 | linux-kpi quirk flag bridge | ✅ Done |
| Q3 | amdgpu linux-kpi quirk consumption | ✅ Done |
| Q3 | redbear-info --quirks display | ✅ Done |
@@ -497,7 +539,11 @@ the honest breakdown.
- amdgpu: startup firmware requirement enforced at the Rust DRM boundary, with real quirk-aware runtime logging for `NO_ASPM`, `NEED_IOMMU`, `NO_MSI`, `NO_MSIX`
**Infrastructure (data flows, reporting, and partial integration):**
- pcid-spawner: computes `PCI_QUIRK_FLAGS` by calling the canonical `redox-driver-sys` lookup on synthesized `PciDeviceInfo`, then passes the env var onward
- driver-manager: bind-time consumption — spawn env hints
(`REDBEAR_DRIVER_PCI_IRQ_MODE`, `REDBEAR_DRIVER_DISABLE_ACCEL`,
`REDBEAR_DRIVER_IOMMU_GROUP`, `REDBEAR_DRIVER_NUMA_NODE`,
`REDBEAR_DRIVER_MSIX_VECTORS`) and `NEED_FIRMWARE` probe deferral, validated in
the 2026-07-23/24 QEMU gate
- linux-kpi: `pci_get_quirk_flags()` / `pci_has_quirk()` C FFI is available for C drivers and is now consumed by the Red Bear amdgpu path
- redbear-info: `--quirks` reads `/etc/quirks.d/*.toml` and reports configured PCI/USB/DMI entries
- lspci: shows active quirk flags per PCI device (via redox-driver-sys lookup)
@@ -523,6 +569,5 @@ Those selectors are used only for pre-descriptor timing flags (`RESET_DELAY`, `H
`SHORT_SET_ADDR_TIMEOUT`) where vendor/product IDs are not yet available.
**Remaining infrastructure work:**
- none in the current quirks scope
`pcid-spawner` now brokers quirks through the canonical `redox-driver-sys` lookup instead of carrying a separate in-tree PCI quirk engine.
- lifecycle phases beyond spawn (`phase = "early"|"enable"` on entries — early
pre-claim vs enable; PM phases follow the power-management roadmap)
@@ -361,6 +361,24 @@ pub fn lookup_usb_quirks(vendor: u16, product: u16) -> UsbQuirkFlags {
flags
}
/// Look up driver-scoped quirk flags for a domain (`"audio"`, `"nvme"`, ...)
/// from open `[[<domain>_quirk]]` TOML tables. The registry matches and
/// carries string flags; the owning driver interprets them. This is the
/// Linux Type-C model (driver-owned fixup tables such as HDA's per-codec
/// fixup lists) — the table name is the domain key, so new driver domains
/// need no registry code.
pub fn lookup_driver_quirks(domain: &str, vendor: u16, device: u16) -> Vec<String> {
toml_loader::load_domain_quirks(domain, vendor, device).unwrap_or_default()
}
/// Audio (HDA) driver quirk flags for a PCI vendor/device pair, from
/// `[[audio_quirk]]` entries (`quirks.d/15-audio.toml`). Mirrors Linux's
/// `sound/pci/hda` per-device quirk model: force-EAPD, single-command mode,
/// position-fix-LPIB and friends are interpreted by the audio driver itself.
pub fn lookup_audio_quirks(vendor: u16, device: u16) -> Vec<String> {
lookup_driver_quirks("audio", vendor, device)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -61,6 +61,71 @@ pub fn load_dmi_system_quirks(dmi_info: &DmiInfo) -> Result<SystemQuirkFlags, ()
Ok(flags)
}
/// A driver-scoped quirk entry from an open `[[<domain>_quirk]]` TOML table.
///
/// Domain tables carry plain string flags; the owning driver interprets
/// them. This is the Linux Type-C model (driver-owned fixup tables such as
/// HDA's per-codec fixup lists): the registry matches and carries the data,
/// the driver is the sole consumer and semantic owner. New driver domains
/// need no registry code — the table name IS the domain key.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DomainQuirkEntry {
pub vendor: u16,
pub device: u16,
pub flags: Vec<String>,
}
/// Accumulate string flags from all `[[<domain>_quirk]]` entries matching
/// `(vendor, device)` (`0xFFFF` wildcards match everything).
pub fn load_domain_quirks(domain: &str, vendor: u16, device: u16) -> Result<Vec<String>, ()> {
load_domain_quirks_from(QUIRKS_DIR, domain, vendor, device).map_err(|_| ())
}
fn load_domain_quirks_from(
dir: &str,
domain: &str,
vendor: u16,
device: u16,
) -> std::io::Result<Vec<String>> {
let table_name = format!("{domain}_quirk");
let mut out = Vec::new();
for path in sorted_toml_files(dir)? {
let path_str = path.display().to_string();
let Ok(content) = std::fs::read_to_string(&path) else {
continue;
};
let Ok(doc) = content.parse::<toml::Value>() else {
continue;
};
let Some(arr) = doc.get(&table_name).and_then(|v| v.as_array()) else {
continue;
};
for item in arr {
let Some(table) = item.as_table() else {
continue;
};
let entry_vendor = table
.get("vendor")
.and_then(|v| bounded_u16(v, "vendor", &path_str))
.unwrap_or(PCI_QUIRK_ANY_ID);
let entry_device = table
.get("device")
.and_then(|v| bounded_u16(v, "device", &path_str))
.unwrap_or(PCI_QUIRK_ANY_ID);
let vendor_matches = entry_vendor == vendor || entry_vendor == PCI_QUIRK_ANY_ID;
let device_matches = entry_device == device || entry_device == PCI_QUIRK_ANY_ID;
if vendor_matches && device_matches {
if let Some(names) = table.get("flags").and_then(|v| v.as_array()) {
for name in names.iter().filter_map(toml::Value::as_str) {
out.push(name.to_string());
}
}
}
}
}
Ok(out)
}
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(|| {
@@ -703,4 +768,50 @@ mod tests {
assert_eq!(rules[0].flags, PciQuirkFlags::NO_ASPM);
assert_eq!(system_rules[0].flags, SystemQuirkFlags::KBD_DEACTIVATE_FIXUP);
}
#[test]
fn domain_quirks_match_and_accumulate() {
let dir = std::env::temp_dir().join(format!(
"rb-quirks-domain-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("15-audio.toml"),
r#"
[[audio_quirk]]
vendor = 0x8086
device = 0x284b
flags = ["audio_force_eapd"]
[[audio_quirk]]
vendor = 0x8086
flags = ["audio_single_cmd"]
[[audio_quirk]]
vendor = 0x1002
device = 0x4370
flags = ["audio_position_fix_lpib"]
"#,
)
.unwrap();
let exact = load_domain_quirks_from(dir.to_str().unwrap(), "audio", 0x8086, 0x284b).unwrap();
assert_eq!(exact, vec!["audio_force_eapd", "audio_single_cmd"]);
let other_device = load_domain_quirks_from(dir.to_str().unwrap(), "audio", 0x8086, 0x9999).unwrap();
assert_eq!(other_device, vec!["audio_single_cmd"]);
let other_vendor = load_domain_quirks_from(dir.to_str().unwrap(), "audio", 0x1002, 0x284b).unwrap();
assert!(other_vendor.is_empty());
let wrong_domain = load_domain_quirks_from(dir.to_str().unwrap(), "nvme", 0x8086, 0x284b).unwrap();
assert!(wrong_domain.is_empty());
let _ = std::fs::remove_dir_all(&dir);
}
}
@@ -2,43 +2,43 @@
# These apply to HDA controllers and codec devices.
# Intel ICH8 HDA — force EAPD on outputs
[[pci_quirk]]
[[audio_quirk]]
vendor = 0x8086
device = 0x284b
flags = ["audio_force_eapd"]
# Intel ICH9 HDA (QEMU) — use immediate command interface
[[pci_quirk]]
[[audio_quirk]]
vendor = 0x8086
device = 0x293e
flags = ["audio_single_cmd"]
# Intel 6-series PCH HDA — position fix LPIB
[[pci_quirk]]
[[audio_quirk]]
vendor = 0x8086
device = 0x1c20
flags = ["audio_position_fix_lpib"]
# Intel 7-series PCH HDA — position fix LPIB
[[pci_quirk]]
[[audio_quirk]]
vendor = 0x8086
device = 0x1e20
flags = ["audio_position_fix_lpib"]
# Intel Sunrise Point HDA — position fix LPIB
[[pci_quirk]]
[[audio_quirk]]
vendor = 0x8086
device = 0xa170
flags = ["audio_position_fix_lpib"]
# Intel Cannon Point HDA — position fix LPIB
[[pci_quirk]]
[[audio_quirk]]
vendor = 0x8086
device = 0x9dc8
flags = ["audio_position_fix_lpib"]
# AMD FCH HDA — single command fallback
[[pci_quirk]]
[[audio_quirk]]
vendor = 0x1022
device = 0x1457
flags = ["audio_single_cmd"]