driver-manager: accept device id list in [[driver.match]]

The shipped /lib/drivers.d/10-network.toml scopes vendor+class matches to
specific device ids with `device = [0x8168, 0x8169]` (rtl8168d) and a
42-id ixgbed list — the documented, intended form. But RawDriverMatch
parsed `device` as a single u16, so the list form failed to deserialize
("invalid type: sequence, expected u16") and took the whole file down,
leaving driver-manager with zero network drivers loaded — including
virtio-netd, so no network came up at all even in QEMU.

Parse `device` as scalar-or-list (U16OrList) and fan each raw match out
to one DriverMatch per id (an OR alternative carrying the shared
vendor/class/subclass constraints). Scalar and device-less matches keep
their previous 1:1 behaviour. Adds regression tests for both forms.
This commit is contained in:
2026-07-29 23:21:54 +09:00
parent 2b19f2ace0
commit 7eefe35b6f
@@ -117,10 +117,34 @@ impl Clone for DriverConfig {
}
}
/// A PCI id field (`device`) that the config may write either as a single id
/// (`device = 0x8139`) or as a list of ids (`device = [0x8168, 0x8169]`). A
/// list means "match any of these ids" — an OR alternative — and is the form
/// the network/graphics driver configs use to scope a vendor+class match to the
/// specific device ids a driver's register layout supports. Without this, the
/// list form fails to deserialize (`invalid type: sequence, expected u16`) and
/// takes the entire file down with it, so driver-manager loads zero drivers
/// from it (observed: no network drivers, including virtio-netd, get bound).
#[derive(Deserialize, Clone)]
#[serde(untagged)]
enum U16OrList {
Scalar(u16),
List(Vec<u16>),
}
impl U16OrList {
fn into_vec(self) -> Vec<u16> {
match self {
U16OrList::Scalar(v) => vec![v],
U16OrList::List(v) => v,
}
}
}
#[derive(Deserialize)]
struct RawDriverMatch {
vendor: Option<u16>,
device: Option<u16>,
device: Option<U16OrList>,
class: Option<u8>,
subclass: Option<u8>,
prog_if: Option<u8>,
@@ -128,6 +152,40 @@ struct RawDriverMatch {
subsystem_device: Option<u16>,
}
impl RawDriverMatch {
/// Expand one raw match into one `DriverMatch` per listed `device` id.
/// No `device` (or an empty list) yields a single device-less match,
/// preserving the previous 1:1 behaviour; a scalar yields one match; a
/// list fans out to one match per id, each an independent OR alternative
/// carrying the same vendor/class/subclass constraints.
fn expand(self) -> Vec<DriverMatch> {
let RawDriverMatch {
vendor,
device,
class,
subclass,
prog_if,
subsystem_vendor,
subsystem_device,
} = self;
// All captured fields are Copy, so this closure is reusable (Fn).
let mk = |dev: Option<u16>| DriverMatch {
vendor,
device: dev,
class,
subclass,
prog_if,
subsystem_vendor,
subsystem_device,
};
match device.map(U16OrList::into_vec) {
None => vec![mk(None)],
Some(ids) if ids.is_empty() => vec![mk(None)],
Some(ids) => ids.into_iter().map(|id| mk(Some(id))).collect(),
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
@@ -465,6 +523,63 @@ command = ["/usr/bin/redbear-acmd"]
});
}
#[test]
fn new_format_device_list_expands_to_one_match_per_id() {
// Reproduces the shipped 10-network.toml rtl8168d block: a vendor+class
// match scoped to two device ids via `device = [0x8168, 0x8169]`. Before
// the U16OrList fix this failed with `invalid type: sequence, expected
// u16` and took the whole file (incl. virtio-netd) down with it.
let toml_str = r#"
[[driver]]
name = "rtl8168d"
command = ["/usr/lib/drivers/rtl8168d"]
[[driver.match]]
vendor = 0x10EC
class = 2
subclass = 0
device = [0x8168, 0x8169]
"#;
let parsed: super::RawDriverToml = toml::from_str(toml_str).expect("parse [[driver]]");
let entry = parsed.driver.into_iter().next().unwrap();
let matches: Vec<super::DriverMatch> = entry
.r#match
.into_iter()
.flat_map(super::RawDriverMatch::expand)
.collect();
assert_eq!(matches.len(), 2, "device list must fan out to one match per id");
assert_eq!(matches[0].device, Some(0x8168));
assert_eq!(matches[1].device, Some(0x8169));
// Shared constraints are carried onto every expanded alternative.
for m in &matches {
assert_eq!(m.vendor, Some(0x10EC));
assert_eq!(m.class, Some(2));
assert_eq!(m.subclass, Some(0));
}
}
#[test]
fn new_format_scalar_device_is_single_match() {
let toml_str = r#"
[[driver]]
name = "rtl8139d"
command = ["/usr/lib/drivers/rtl8139d"]
[[driver.match]]
vendor = 0x10EC
device = 0x8139
"#;
let parsed: super::RawDriverToml = toml::from_str(toml_str).expect("parse [[driver]]");
let entry = parsed.driver.into_iter().next().unwrap();
let matches: Vec<super::DriverMatch> = entry
.r#match
.into_iter()
.flat_map(super::RawDriverMatch::expand)
.collect();
assert_eq!(matches.len(), 1);
assert_eq!(matches[0].device, Some(0x8139));
}
#[test]
fn legacy_vendor_plus_device_converts_to_one_match() {
let toml_str = r#"
@@ -1364,20 +1479,6 @@ pub fn spawn_decision_gate() -> SpawnDecision {
SpawnDecision::Spawn
}
impl From<RawDriverMatch> for DriverMatch {
fn from(r: RawDriverMatch) -> Self {
DriverMatch {
vendor: r.vendor,
device: r.device,
class: r.class,
subclass: r.subclass,
prog_if: r.prog_if,
subsystem_vendor: r.subsystem_vendor,
subsystem_device: r.subsystem_device,
}
}
}
impl DriverConfig {
/// Load every `.toml` config file in `dir` and return the merged,
/// priority-sorted list of driver configs.
@@ -1408,7 +1509,7 @@ impl DriverConfig {
Ok(parsed) => {
for driver in parsed.driver {
let matches: Vec<DriverMatch> =
driver.r#match.into_iter().map(DriverMatch::from).collect();
driver.r#match.into_iter().flat_map(RawDriverMatch::expand).collect();
let initial_power_state = match &driver.initial_power_state {
Some(s) => match PciPowerState::from_toml(s) {
Ok(state) => state,