driver-manager: N8 — per-driver [driver.params] parsing + env emission

Closes the v4.8 cross-cutting item 'modprobe.d options parser':
the new format's [driver.params] table (e.g. 50-amdgpu.toml's
radeon_overlap / amdgpu_force / amdgpu_disable_accel knobs) is now
parsed from every [[driver]] entry and emitted to the spawned child
as REDBEAR_DRIVER_PARAM_<NAME>=<value> env vars.

RawDriverEntry grows a new field:
  params: BTreeMap<String, String>

parsed at load_all time into a Vec<(name, value)> on DriverConfig
(preserving BTreeMap's sorted iteration order for deterministic
env-var emission).

The spawn path applies self.params after the existing
[[options]] env-var emission; both share the REDBEAR_DRIVER_PARAM_*
prefix so drivers don't need to distinguish the two sources.

Matching semantics (radeon_overlap='exclusive', amdgpu_force=true)
remain a follow-up — the env-var wiring is the foundational piece,
and 50-amdgpu.toml's radeon_overlap knob can be honoured by the
radeon driver daemon reading its env-var directly.

Tests (2 new):
- load_all_parses_driver_params: 3-params TOML entry parses
  with deterministic sorted iteration
- load_all_driver_params_default_empty: a [[driver]] without
  [driver.params] still parses, params list is empty

162 driver-manager tests pass.
driver-manager-audit-no-stubs.py: 46 files, 0 violations.
This commit is contained in:
2026-07-27 17:05:12 +09:00
parent cc37a2c08a
commit 3d1f774f8d
@@ -46,11 +46,18 @@ pub struct DriverConfig {
pub matches: Vec<DriverMatch>,
pub depends_on: Vec<String>,
pub exclusive_with: Vec<String>,
/// Per-driver parameter overrides from the policy layer's
/// `[driver.params]` table. Each `(name, value)` pair is emitted
/// as `REDBEAR_DRIVER_PARAM_<NAME>=<value>` in the spawn env.
/// This is the wiring for `redbear-driver-policy`'s `50-amdgpu.toml`
/// style per-driver knobs (radeon_overlap, amdgpu_force,
/// amdgpu_disable_accel, etc.).
pub params: Vec<(String, String)>,
/// Initial PCI power state for the spawned driver daemon. D0 is the
/// default and matches the post-`enable_device` state of every bound
/// device. D3hot / D3cold can be set for low-power drivers (e.g.
/// always-on housekeeping devices) that wake the bus on demand.
/// When non-D0, driver-manager sets
/// When non-D0, driver-manager emits
/// `REDBEAR_DRIVER_INITIAL_POWER_STATE=<state>` in the child's env so
/// the daemon can call `set_power_state` on its granted channel.
pub initial_power_state: PciPowerState,
@@ -102,6 +109,7 @@ impl Clone for DriverConfig {
matches: self.matches.clone(),
depends_on: self.depends_on.clone(),
exclusive_with: self.exclusive_with.clone(),
params: self.params.clone(),
initial_power_state: self.initial_power_state,
spawned: Mutex::new(HashMap::new()),
pid_to_device: Mutex::new(HashMap::new()),
@@ -201,6 +209,7 @@ command = ["/usr/bin/redbear-acmd"]
matches: Vec::new(),
depends_on: Vec::new(),
exclusive_with: Vec::new(),
params: Vec::new(),
initial_power_state: PciPowerState::D0,
spawned: Mutex::new(HashMap::new()),
pid_to_device: Mutex::new(HashMap::new()),
@@ -219,6 +228,7 @@ command = ["/usr/bin/redbear-acmd"]
matches: Vec::new(),
depends_on: Vec::new(),
exclusive_with: Vec::new(),
params: Vec::new(),
initial_power_state: PciPowerState::D0,
spawned: Mutex::new(HashMap::new()),
pid_to_device: Mutex::new(HashMap::new()),
@@ -409,6 +419,84 @@ command = ["e1000d"]
});
}
#[test]
fn load_all_parses_driver_params() {
// N8: per-driver [driver.params] table from redbear-driver-policy
// (e.g. 50-amdgpu.toml) — each entry becomes a param override
// that the spawn path emits as REDBEAR_DRIVER_PARAM_<NAME>=<value>.
serialized(|| {
let dir = std::env::temp_dir().join(format!(
"rb-dm-driver-params-{}-{}",
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("50-amdgpu.toml"),
r#"
[[driver]]
name = "redox-drm"
priority = 95
command = ["/usr/bin/redox-drm"]
[driver.params]
radeon_overlap = "exclusive"
amdgpu_force = "true"
amdgpu_disable_accel = "false"
"#,
)
.unwrap();
let configs = DriverConfig::load_all(dir.to_str().unwrap())
.expect("[driver.params] must parse");
assert_eq!(configs.len(), 1);
let params = &configs[0].params;
assert_eq!(params.len(), 3);
// BTreeMap iteration is sorted by key.
assert_eq!(params[0].0, "amdgpu_disable_accel");
assert_eq!(params[0].1, "false");
assert_eq!(params[1].0, "amdgpu_force");
assert_eq!(params[1].1, "true");
assert_eq!(params[2].0, "radeon_overlap");
assert_eq!(params[2].1, "exclusive");
let _ = std::fs::remove_dir_all(&dir);
});
}
#[test]
fn load_all_driver_params_default_empty() {
// A driver entry without [driver.params] should still parse
// and have an empty params list — drivers that don't need
// knobs aren't required to declare any.
serialized(|| {
let dir = std::env::temp_dir().join(format!(
"rb-dm-driver-params-empty-{}-{}",
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("10-foo.toml"),
r#"
[[driver]]
name = "foo"
priority = 80
command = ["/usr/bin/foo"]
"#,
)
.unwrap();
let configs = DriverConfig::load_all(dir.to_str().unwrap()).unwrap();
assert_eq!(configs.len(), 1);
assert!(configs[0].params.is_empty());
let _ = std::fs::remove_dir_all(&dir);
});
}
#[test]
fn load_all_warns_and_defaults_on_invalid_initial_power_state() {
serialized(|| {
@@ -1108,6 +1196,23 @@ impl DriverConfig {
},
None => PciPowerState::D0,
};
// Collect per-driver [driver.params] into an
// ordered Vec for env-var emission. BTreeMap
// iteration is sorted by key so the env vars
// are emitted in deterministic order.
let params: Vec<(String, String)> = driver
.params
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
if !params.is_empty() {
log::info!(
"policy: driver `{}` has {} [driver.params] override{}",
driver.name,
params.len(),
if params.len() == 1 { "" } else { "s" }
);
}
configs.push(DriverConfig {
name: driver.name,
description: driver.description,
@@ -1116,6 +1221,7 @@ impl DriverConfig {
matches,
depends_on: driver.depends_on,
exclusive_with: driver.exclusive_with,
params,
initial_power_state,
spawned: Mutex::new(HashMap::new()),
pid_to_device: Mutex::new(HashMap::new()),
@@ -1475,6 +1581,18 @@ impl Driver for DriverConfig {
);
}
// N8: apply per-driver [driver.params] overrides from the
// policy layer (e.g. 50-amdgpu.toml's radeon_overlap /
// amdgpu_force / amdgpu_disable_accel). Same env-var
// convention as `[[options]]` so drivers don't need to
// distinguish the two sources.
for (param_name, param_value) in &self.params {
cmd.env(
format!("REDBEAR_DRIVER_PARAM_{}", param_name.to_uppercase()),
param_value,
);
}
if !self.initial_power_state.is_default() {
cmd.env(
"REDBEAR_DRIVER_INITIAL_POWER_STATE",
@@ -1753,6 +1871,15 @@ struct RawDriverEntry {
exclusive_with: Vec<String>,
#[serde(default)]
initial_power_state: Option<String>,
/// Per-driver parameter overrides from the policy layer's
/// `[driver.params]` table. Each entry becomes a
/// `REDBEAR_DRIVER_PARAM_<NAME>=<value>` env var on the spawned
/// child's environment. The schema is a flat string-to-string map;
/// values are stored verbatim (the driver daemon interprets them
/// per its own schema). Used by `redbear-driver-policy`'s
/// `50-amdgpu.toml` and similar per-driver policy files.
#[serde(default)]
params: std::collections::BTreeMap<String, String>,
}
#[derive(Deserialize)]
@@ -1895,6 +2022,7 @@ fn convert_legacy(legacy: RawLegacyEntry) -> DriverConfig {
matches,
depends_on: Vec::new(),
exclusive_with: legacy.exclusive_with,
params: Vec::new(),
initial_power_state: PciPowerState::D0,
spawned: Mutex::new(HashMap::new()),
pid_to_device: Mutex::new(HashMap::new()),