Files
RedBear-OS/local/recipes/system/driver-manager/source/src/config.rs
T
vasilito 509552725f driver-manager: crash-aware reap + error_channel fd cleanup (N18 Q2+Bug4)
Splits `reap_pid` into the `reap_pid_inner(crashed: bool)` private
helper plus two public entry points:

- `reap_pid(pid)` — crash path. Calls
  `crash_tracker().record_failure` after the maps are cleared,
  which advances the auto-blacklist counter.

- `reap_pid_clean(pid)` — clean exit path. Calls
  `crash_tracker().record_success` so the device's backoff is
  reset; a future crash starts the count from 1, not from
  where the previous clean-exit backoff ended.

Both paths also call `error_channel::global().remove(&key)` to
close the sidecar UnixStream fd (Bug4: each crash previously
leaked one fd; ~1024 crashes exhausted the manager's fd table).

This is the kernel of N18-Q2+Bug4; the call-site change in
`main.rs` that interprets the waitpid status was committed in
the prior Q1+Q2+S3 commit.
2026-07-27 21:56:37 +09:00

2051 lines
73 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::collections::HashMap;
use std::fs;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::path::Path;
use std::process::Command;
use std::string::String;
use std::sync::Mutex;
use std::thread;
use std::time::{Duration, Instant};
use std::vec::Vec;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use pcid_interface::PciFunctionHandle;
use redox_driver_core::device::DeviceInfo;
use redox_driver_core::driver::{Driver, DriverError, ErrorSeverity, ProbeResult, RecoveryAction};
use redox_driver_core::r#match::DriverMatch;
use redox_driver_core::params::{DriverParams, ParamValue};
use serde::Deserialize;
const SIGTERM: i32 = 15;
const SIGKILL: i32 = 9;
const GRACE_PERIOD_MS: u64 = 3000;
const POLL_INTERVAL_MS: u64 = 50;
/// Default crash-threshold (consecutive spawn failures before a driver
/// is auto-blacklisted for this BDF). Overridable via
/// `REDBEAR_DRIVER_CRASH_THRESHOLD`; see `crash_tracker` below.
const DEFAULT_CRASH_THRESHOLD: u32 = 5;
const DEFAULT_BACKOFF_BASE_MS: u64 = 100;
const DEFAULT_BACKOFF_CAP_MS: u64 = 30_000;
#[derive(Debug)]
struct SpawnedDriver {
pid: u32,
channel_fd: OwnedFd,
}
#[derive(Debug)]
pub struct DriverConfig {
pub name: String,
pub description: String,
pub priority: i32,
pub command: Vec<String>,
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 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,
spawned: Mutex<HashMap<String, SpawnedDriver>>,
pid_to_device: Mutex<HashMap<u32, String>>,
}
/// PCI power state for driver spawn. Mirrors the subset of
/// `pci_power_state` (include/linux/pci.h) that Red Bear OS uses at
/// boot: D0 (fully on, default) and D3hot (software-visible but
/// not consuming bus power).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PciPowerState {
D0,
D3hot,
}
impl PciPowerState {
pub fn from_toml(s: &str) -> Result<Self, String> {
match s {
"D0" => Ok(Self::D0),
"D3hot" => Ok(Self::D3hot),
other => Err(format!(
"invalid initial_power_state {:?}: expected D0 or D3hot",
other
)),
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::D0 => "D0",
Self::D3hot => "D3hot",
}
}
pub fn is_default(self) -> bool {
matches!(self, Self::D0)
}
}
impl Clone for DriverConfig {
fn clone(&self) -> Self {
DriverConfig {
name: self.name.clone(),
description: self.description.clone(),
priority: self.priority,
command: self.command.clone(),
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()),
}
}
}
#[derive(Deserialize)]
struct RawDriverMatch {
vendor: Option<u16>,
device: Option<u16>,
class: Option<u8>,
subclass: Option<u8>,
prog_if: Option<u8>,
subsystem_vendor: Option<u16>,
subsystem_device: Option<u16>,
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::{
is_process_alive, signal_all_spawned, signal_then_collect, spawned_pids_snapshot,
DriverConfig, PciPowerState,
};
use std::collections::BTreeMap;
use std::sync::Mutex;
use std::time::Instant;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn serialized<R>(body: impl FnOnce() -> R) -> R {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
body()
}
#[test]
fn load_all_accepts_matchless_driver_entries() {
serialized(|| {
let dir = std::env::temp_dir().join(format!(
"rb-dm-matchless-{}-{}",
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("70-usb-class.toml"),
r#"
[[driver]]
name = "redbear-acmd"
description = "USB CDC ACM serial driver"
priority = 70
command = ["/usr/bin/redbear-acmd"]
"#,
)
.unwrap();
let configs = DriverConfig::load_all(dir.to_str().unwrap())
.expect("matchless driver entries must load");
assert_eq!(configs.len(), 1);
assert_eq!(configs[0].name, "redbear-acmd");
assert!(configs[0].matches.is_empty());
let _ = std::fs::remove_dir_all(&dir);
});
}
#[test]
fn is_process_alive_returns_false_for_unused_pid() {
let dead = u32::MAX;
assert!(
!is_process_alive(dead),
"PID {} should not be alive; got true",
dead
);
}
#[test]
fn signal_then_collect_returns_ok_for_unused_pid() {
let r = signal_then_collect(u32::MAX, std::time::Duration::from_millis(100));
assert!(
r.is_ok(),
"signal_then_collect against an unused PID should converge immediately; got {:?}",
r
);
}
#[test]
fn spawned_pids_snapshot_returns_empty_for_unbound_driver() {
let cfg = DriverConfig {
name: "unbound".to_string(),
description: String::new(),
priority: 0,
command: Vec::new(),
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()),
};
let pids = spawned_pids_snapshot(&cfg);
assert!(pids.is_empty(), "no spawned children => empty snapshot");
}
#[test]
fn signal_all_spawned_returns_zero_for_unbound_driver() {
let cfg = DriverConfig {
name: "unbound".to_string(),
description: String::new(),
priority: 0,
command: Vec::new(),
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()),
};
let n = signal_all_spawned(&cfg, libc::SIGTERM);
assert_eq!(n, 0, "no spawned children => zero signals sent");
}
#[test]
fn legacy_vendor_plus_device_converts_to_one_match() {
let toml_str = r#"
[[drivers]]
name = "rtl"
vendor = 0x10ec
device = 0x8168
class = 0x02
command = ["rtl8168d"]
"#;
let parsed: super::RawLegacyToml = toml::from_str(toml_str).expect("parse legacy");
let cfg = super::convert_legacy(parsed.drivers.into_iter().next().unwrap());
assert_eq!(cfg.name, "rtl");
assert_eq!(cfg.command, vec!["rtl8168d"]);
assert_eq!(cfg.matches.len(), 1);
assert_eq!(cfg.matches[0].vendor, Some(0x10ec));
assert_eq!(cfg.matches[0].device, Some(0x8168));
assert_eq!(cfg.matches[0].class, Some(0x02));
}
#[test]
fn legacy_ids_map_expands_to_multiple_matches() {
let mut ids = BTreeMap::new();
ids.insert("0x8086".to_string(), vec![0x100E, 0x10D3]);
let entry = super::RawLegacyEntry {
name: Some("igb".to_string()),
class: Some(0x02),
subclass: Some(0x00),
interface: None,
ids: Some(ids),
vendor: None,
device: None,
device_id_range: None,
exclusive_with: Vec::new(),
command: vec!["e1000d".to_string()],
};
let cfg = super::convert_legacy(entry);
assert_eq!(cfg.matches.len(), 2);
assert_eq!(cfg.matches[0].vendor, Some(0x8086));
assert_eq!(cfg.matches[0].device, Some(0x100E));
assert_eq!(cfg.matches[1].device, Some(0x10D3));
}
#[test]
fn legacy_device_id_range_expands_to_multiple_matches() {
let entry = super::RawLegacyEntry {
name: Some("range_dr".to_string()),
class: None,
subclass: None,
interface: None,
ids: None,
vendor: None,
device: None,
device_id_range: Some(super::RawLegacyRange { start: 0x1000, end: 0x1002 }),
exclusive_with: Vec::new(),
command: vec!["test_dr".to_string()],
};
let cfg = super::convert_legacy(entry);
assert_eq!(cfg.matches.len(), 3);
assert_eq!(cfg.matches[0].device, Some(0x1000));
assert_eq!(cfg.matches[2].device, Some(0x1002));
}
#[test]
fn empty_legacy_does_not_panic_falls_back_to_class() {
let entry = super::RawLegacyEntry {
name: Some("emptily".to_string()),
class: Some(0x06),
subclass: None,
interface: None,
ids: None,
vendor: None,
device: None,
device_id_range: None,
exclusive_with: Vec::new(),
command: vec!["x".to_string()],
};
let cfg = super::convert_legacy(entry);
assert_eq!(cfg.matches.len(), 1);
assert_eq!(cfg.matches[0].class, Some(0x06));
}
#[test]
fn pci_power_state_from_toml_round_trips() {
assert_eq!(
super::PciPowerState::from_toml("D0").unwrap(),
super::PciPowerState::D0
);
assert_eq!(
super::PciPowerState::from_toml("D3hot").unwrap(),
super::PciPowerState::D3hot
);
}
#[test]
fn pci_power_state_from_toml_rejects_unknown_values() {
assert!(super::PciPowerState::from_toml("D1").is_err());
assert!(super::PciPowerState::from_toml("D2").is_err());
assert!(super::PciPowerState::from_toml("D3cold").is_err());
assert!(super::PciPowerState::from_toml("d0").is_err(), "case-sensitive");
assert!(super::PciPowerState::from_toml("").is_err());
}
#[test]
fn pci_power_state_is_default_for_d0_only() {
assert!(super::PciPowerState::D0.is_default());
assert!(!super::PciPowerState::D3hot.is_default());
}
#[test]
fn pci_power_state_as_str_matches_linux_pci_power_state_names() {
assert_eq!(super::PciPowerState::D0.as_str(), "D0");
assert_eq!(super::PciPowerState::D3hot.as_str(), "D3hot");
}
#[test]
fn load_all_parses_initial_power_state() {
serialized(|| {
let dir = std::env::temp_dir().join(format!(
"rb-dm-power-state-{}-{}",
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("20-pm.toml"),
r#"
[[driver]]
name = "watchdog"
priority = 50
command = ["/usr/bin/watchdogd"]
initial_power_state = "D3hot"
"#,
)
.unwrap();
let configs = DriverConfig::load_all(dir.to_str().unwrap())
.expect("initial_power_state must parse");
assert_eq!(configs.len(), 1);
assert_eq!(
configs[0].initial_power_state,
super::PciPowerState::D3hot
);
let _ = std::fs::remove_dir_all(&dir);
});
}
#[test]
fn load_all_defaults_to_d0_when_initial_power_state_absent() {
serialized(|| {
let dir = std::env::temp_dir().join(format!(
"rb-dm-power-default-{}-{}",
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-net.toml"),
r#"
[[driver]]
name = "e1000d"
priority = 80
command = ["e1000d"]
"#,
)
.unwrap();
let configs = DriverConfig::load_all(dir.to_str().unwrap())
.expect("missing initial_power_state must default");
assert_eq!(configs.len(), 1);
assert_eq!(
configs[0].initial_power_state,
super::PciPowerState::D0
);
let _ = std::fs::remove_dir_all(&dir);
});
}
#[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(|| {
let dir = std::env::temp_dir().join(format!(
"rb-dm-power-invalid-{}-{}",
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("30-bad.toml"),
r#"
[[driver]]
name = "broken"
priority = 50
command = ["/usr/bin/broken"]
initial_power_state = "D2"
"#,
)
.unwrap();
let configs = DriverConfig::load_all(dir.to_str().unwrap())
.expect("invalid initial_power_state must not abort config loading");
assert_eq!(configs.len(), 1);
assert_eq!(
configs[0].initial_power_state,
super::PciPowerState::D0,
"invalid value should default to D0 with a WARN log"
);
let _ = std::fs::remove_dir_all(&dir);
});
}
// ── F1 crash-tracker tests ──────────────────────────────────────
#[test]
fn crash_tracker_record_failure_increments_and_signals_threshold() {
let tracker = super::CrashTracker::new();
let bdf = "0000:00:1f.2";
assert_eq!(tracker.record_failure(bdf), None, "1st failure < 5 threshold");
assert_eq!(tracker.record_failure(bdf), None, "2nd failure");
assert_eq!(tracker.record_failure(bdf), None, "3rd failure");
assert_eq!(tracker.record_failure(bdf), None, "4th failure");
let count = tracker.record_failure(bdf).expect("5th failure >= threshold");
assert_eq!(count, 5);
}
#[test]
fn crash_tracker_record_success_clears_state() {
let tracker = super::CrashTracker::new();
let bdf = "0000:00:1f.2";
tracker.record_failure(bdf);
tracker.record_failure(bdf);
assert!(tracker.is_in_backoff(bdf, Instant::now()));
tracker.record_success(bdf);
assert!(!tracker.is_in_backoff(bdf, Instant::now()));
}
#[test]
fn crash_tracker_is_in_backoff_returns_true_within_window() {
let tracker = super::CrashTracker::new();
let bdf = "0000:00:1f.2";
tracker.record_failure(bdf);
// Just after the failure, backoff is active.
assert!(
tracker.is_in_backoff(bdf, Instant::now()),
"a just-recorded failure must trigger backoff"
);
}
#[test]
fn crash_tracker_snapshot_returns_per_bdf_counts() {
let tracker = super::CrashTracker::new();
tracker.record_failure("0000:00:1f.2");
tracker.record_failure("0000:00:1f.2");
tracker.record_failure("0000:01:00.0");
let snap = tracker.snapshot();
assert_eq!(snap.len(), 2);
let mut counts: std::collections::HashMap<String, u32> = snap.into_iter().collect();
assert_eq!(counts.remove("0000:00:1f.2").unwrap(), 2);
assert_eq!(counts.remove("0000:01:00.0").unwrap(), 1);
}
#[test]
fn crash_tracker_apply_env_overrides() {
serialized(|| {
let tracker = super::CrashTracker::new();
unsafe { std::env::set_var("REDBEAR_DRIVER_CRASH_THRESHOLD", "3") };
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_BASE_MS", "50") };
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_CAP_MS", "500") };
tracker.apply_env();
assert_eq!(tracker.threshold(), 3);
assert_eq!(tracker.backoff_base_ms(), 50);
assert_eq!(tracker.backoff_cap_ms(), 500);
unsafe { std::env::remove_var("REDBEAR_DRIVER_CRASH_THRESHOLD") };
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_BASE_MS") };
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_CAP_MS") };
});
}
#[test]
fn crash_tracker_apply_env_ignores_invalid_values() {
serialized(|| {
let tracker = super::CrashTracker::new();
let orig_threshold = tracker.threshold();
let orig_base = tracker.backoff_base_ms();
let orig_cap = tracker.backoff_cap_ms();
unsafe { std::env::set_var("REDBEAR_DRIVER_CRASH_THRESHOLD", "not-a-number") };
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_BASE_MS", "0") };
unsafe { std::env::set_var("REDBEAR_DRIVER_BACKOFF_CAP_MS", "-5") };
tracker.apply_env();
assert_eq!(
tracker.threshold(),
orig_threshold,
"invalid threshold must be ignored"
);
assert_eq!(
tracker.backoff_base_ms(),
orig_base,
"zero base_ms must be ignored (zero is treated as invalid)"
);
assert_eq!(
tracker.backoff_cap_ms(),
orig_cap,
"negative cap_ms must be ignored (parse fails, default kept)"
);
unsafe { std::env::remove_var("REDBEAR_DRIVER_CRASH_THRESHOLD") };
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_BASE_MS") };
unsafe { std::env::remove_var("REDBEAR_DRIVER_BACKOFF_CAP_MS") };
});
}
#[test]
fn crash_tracker_global_stub_returns_when_unset() {
// CrashTracker global pointer must not be set by other tests.
let snap = super::crash_tracker().snapshot();
assert!(
snap.is_empty(),
"fresh crash tracker snapshot should be empty"
);
}
#[test]
fn spawn_decision_defaults_to_spawn_without_env_or_flags() {
serialized(|| {
unsafe {
std::env::remove_var("REDBEAR_DRIVER_MANAGER_ACTIVE");
}
let prev: Vec<String> = std::env::args().collect();
if !prev.iter().any(|a| a == "--no-spawn") {
assert_eq!(super::spawn_decision_gate(), super::SpawnDecision::Spawn);
}
});
}
#[test]
fn spawn_decision_rejects_when_env_zero() {
serialized(|| {
unsafe {
std::env::set_var("REDBEAR_DRIVER_MANAGER_ACTIVE", "0");
}
let decision = super::spawn_decision_gate();
unsafe {
std::env::remove_var("REDBEAR_DRIVER_MANAGER_ACTIVE");
}
match decision {
super::SpawnDecision::ObserveOnly(reason) => {
assert!(reason.contains("REDBEAR_DRIVER_MANAGER_ACTIVE=0"));
}
super::SpawnDecision::Spawn => panic!(
"env REDBEAR_DRIVER_MANAGER_ACTIVE=0 should be observed-only"
),
}
});
}
#[test]
fn spawn_decision_accepts_env_one() {
serialized(|| {
unsafe {
std::env::set_var("REDBEAR_DRIVER_MANAGER_ACTIVE", "1");
}
let decision = super::spawn_decision_gate();
unsafe {
std::env::remove_var("REDBEAR_DRIVER_MANAGER_ACTIVE");
}
assert_eq!(decision, super::SpawnDecision::Spawn);
});
}
}
/// Send a Unix signal to a spawned child process by PID.
///
/// We invoke the external `kill(1)` utility rather than `libc::kill(2)` so
/// that this code compiles unchanged on Linux host (where we run unit tests
/// and the no-stubs audit) and Redox target (where the user-space kill
/// binary is provided by `redox-userspace` / busybox). Standard exit code:
/// `0` on success, non-zero (treated as Err) otherwise.
fn send_signal(pid: u32, signal: i32) -> Result<(), String> {
let status = Command::new("kill")
.arg(format!("-{}", signal))
.arg(pid.to_string())
.status();
match status {
Ok(s) if s.success() => Ok(()),
Ok(s) => Err(format!(
"kill -{} {} exited with status {}",
signal,
pid,
s.code().map(|c| c.to_string()).unwrap_or_else(|| "no-code".to_string())
)),
Err(e) => Err(format!("failed to spawn kill(1): {}", e)),
}
}
/// Probe whether a PID is still alive using `kill(pid, 0)` semantics. Returns
/// `true` if the process is alive, `false` if it has exited (or the kill probe
/// reports ESRCH). Other errors (e.g. EPERM) are conservatively treated as
/// `true` so we do not falsely report an exit.
fn is_process_alive(pid: u32) -> bool {
match Command::new("kill").arg("-0").arg(pid.to_string()).status() {
Ok(s) => s.success(),
Err(_) => true,
}
}
/// Send a SIGTERM and wait up to `grace` for the child to exit; if still
/// alive, follow up with SIGKILL. Returns Ok once `kill -0` reports the
/// process is gone.
fn signal_then_collect(pid: u32, grace: Duration) -> Result<(), String> {
if let Err(why) = send_signal(pid, SIGTERM) {
log::warn!("signal_then_collect: SIGTERM to pid {} failed: {}", pid, why);
}
let deadline = Instant::now() + grace;
while Instant::now() < deadline {
if !is_process_alive(pid) {
return Ok(());
}
thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
}
log::warn!(
"signal_then_collect: pid {} did not exit within {:?}; escalating to SIGKILL",
pid,
grace
);
if let Err(why) = send_signal(pid, SIGKILL) {
return Err(format!(
"pid {} did not exit on SIGTERM and SIGKILL also failed: {}",
pid, why
));
}
let kill_deadline = Instant::now() + Duration::from_millis(GRACE_PERIOD_MS);
while Instant::now() < kill_deadline {
if !is_process_alive(pid) {
return Ok(());
}
thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
}
Err(format!(
"pid {} survived SIGKILL after {:?} — zombie child left for init",
pid,
grace + Duration::from_millis(GRACE_PERIOD_MS)
))
}
fn send_signal_to_spawned(
spawned: &Mutex<HashMap<String, SpawnedDriver>>,
signal: i32,
device_key: &str,
) -> Result<(), DriverError> {
let pids: Vec<u32> = {
let map = spawned.lock().unwrap_or_else(|e| e.into_inner());
map.values().map(|sd| sd.pid).collect()
};
for pid in pids {
if let Err(why) = send_signal(pid, signal) {
log::error!(
"send_signal_to_spawned: device {} pid {} signal {} failed: {}",
device_key,
pid,
signal,
why
);
return Err(DriverError::IoError);
}
}
Ok(())
}
// ── Crash counter, exponential backoff, auto-blacklist ───────────
//
// Closes the F1 finding: a driver that crashes on every spawn used
// to be respawned every hotplug poll (250ms) indefinitely. The
// `CrashTracker` records consecutive spawn failures per BDF, applies
// exponential backoff (100 ms × 2^N, cap 30 s) before the next probe,
// and auto-blacklists after `threshold` consecutive failures.
//
// Configuration is read once at startup from env vars:
// REDBEAR_DRIVER_CRASH_THRESHOLD (default 5)
// REDBEAR_DRIVER_BACKOFF_BASE_MS (default 100)
// REDBEAR_DRIVER_BACKOFF_CAP_MS (default 30000)
//
// A successful probe (Bind) clears the failure state for that BDF so
// a transient crash followed by a fix doesn't permanently blacklist
// the device.
/// Per-BDF crash state.
#[derive(Debug, Clone, Copy)]
pub struct CrashState {
pub failures: u32,
pub next_retry_at: Instant,
}
/// Process-wide tracker. Process-global because the failure of one
/// driver's spawn is observable across all drivers (every probe cycle
/// sees the same BDF and must respect the backoff window).
pub struct CrashTracker {
failures: std::sync::OnceLock<Mutex<HashMap<String, CrashState>>>,
threshold: AtomicU32,
backoff_base_ms: AtomicU64,
backoff_cap_ms: AtomicU64,
}
impl CrashTracker {
pub const fn new() -> Self {
Self {
failures: std::sync::OnceLock::new(),
threshold: AtomicU32::new(DEFAULT_CRASH_THRESHOLD),
backoff_base_ms: AtomicU64::new(DEFAULT_BACKOFF_BASE_MS),
backoff_cap_ms: AtomicU64::new(DEFAULT_BACKOFF_CAP_MS),
}
}
fn failures_map(&self) -> &Mutex<HashMap<String, CrashState>> {
self.failures.get_or_init(|| Mutex::new(HashMap::new()))
}
pub fn threshold(&self) -> u32 {
self.threshold.load(Ordering::SeqCst)
}
pub fn backoff_base_ms(&self) -> u64 {
self.backoff_base_ms.load(Ordering::SeqCst)
}
pub fn backoff_cap_ms(&self) -> u64 {
self.backoff_cap_ms.load(Ordering::SeqCst)
}
/// Read the env-var overrides and apply them. Called once at
/// startup from `apply_crash_tracker_env()` (see main.rs).
pub fn apply_env(&self) {
if let Ok(s) = std::env::var("REDBEAR_DRIVER_CRASH_THRESHOLD") {
if let Ok(n) = s.parse::<u32>() {
if n > 0 {
self.threshold.store(n, Ordering::SeqCst);
}
}
}
if let Ok(s) = std::env::var("REDBEAR_DRIVER_BACKOFF_BASE_MS") {
if let Ok(n) = s.parse::<u64>() {
if n > 0 {
self.backoff_base_ms.store(n, Ordering::SeqCst);
}
}
}
if let Ok(s) = std::env::var("REDBEAR_DRIVER_BACKOFF_CAP_MS") {
if let Ok(n) = s.parse::<u64>() {
if n > 0 {
self.backoff_cap_ms.store(n, Ordering::SeqCst);
}
}
}
}
/// Return true if `bdf` has a recorded failure whose
/// `next_retry_at` is still in the future. Used by `probe` to
/// short-circuit before the spawn attempt.
pub fn is_in_backoff(&self, bdf: &str, now: Instant) -> bool {
if let Ok(map) = self.failures_map().lock() {
if let Some(state) = map.get(bdf) {
return state.next_retry_at > now;
}
}
false
}
/// Snapshot the current failures table for the
/// `/crash_count/<bdf>` and `/crash_count` (list) scheme endpoints.
#[allow(dead_code)] // public API used by tests
pub fn snapshot(&self) -> Vec<(String, u32)> {
self.failures_map()
.lock()
.map(|map| {
map.iter()
.map(|(k, v)| (k.clone(), v.failures))
.collect()
})
.unwrap_or_default()
}
/// Record a spawn failure for `bdf`. Updates the failure counter
/// and computes the next-retry instant via exponential backoff.
/// Returns `Some(threshold_exceeded)` if the BDF should be
/// auto-blacklisted (i.e. `failures >= threshold`), `None` otherwise.
pub fn record_failure(&self, bdf: &str) -> Option<u32> {
let threshold = self.threshold();
let base_ms = self.backoff_base_ms();
let cap_ms = self.backoff_cap_ms();
let map_mutex = self.failures_map();
let mut map = match map_mutex.lock() {
Ok(m) => m,
Err(_) => return None,
};
let entry = map.entry(bdf.to_string()).or_insert(CrashState {
failures: 0,
next_retry_at: Instant::now(),
});
entry.failures = entry.failures.saturating_add(1);
let n = entry.failures;
let backoff_ms = base_ms
.checked_shl(n.min(20))
.unwrap_or(cap_ms)
.min(cap_ms);
entry.next_retry_at = Instant::now() + Duration::from_millis(backoff_ms);
if n >= threshold {
Some(n)
} else {
None
}
}
/// Clear any failure state for `bdf`. Called from a successful
/// probe (Bind result).
pub fn record_success(&self, bdf: &str) {
if let Ok(mut map) = self.failures_map().lock() {
map.remove(bdf);
}
}
}
/// Global crash tracker. The pointer is set once at startup
/// (`set_crash_tracker`); reads in the probe hot path are non-blocking
/// only because the pointer is `static`, not because of any
/// `RwLock`. If unset, `crash_tracker()` returns a static stub with
/// the default threshold and no recorded failures — i.e. behavior
/// identical to pre-F1 (every probe runs, no backoff, no blacklist).
static mut CRASH_TRACKER: *const CrashTracker = std::ptr::null();
/// Returns a reference to the global crash tracker. If the manager
/// has not yet called `set_crash_tracker`, returns a static stub that
/// reports threshold=0 (i.e. never auto-blacklists) and accepts every
/// probe — this preserves the pre-F1 behaviour for early-init code
/// paths (config loading, env parsing) that run before the manager
/// wires the tracker.
pub fn crash_tracker() -> &'static CrashTracker {
unsafe {
if CRASH_TRACKER.is_null() {
&CRASH_TRACKER_STUB
} else {
&*CRASH_TRACKER
}
}
}
static CRASH_TRACKER_STUB: CrashTracker = CrashTracker::new();
/// Wire the global crash tracker. Called once at startup from
/// `apply_crash_tracker_env()` after env vars are read. The pointer
/// is process-lifetime and never reset.
pub fn set_crash_tracker(tracker: &'static CrashTracker) {
unsafe {
CRASH_TRACKER = tracker;
}
}
/// Snapshot the PIDs of every spawned child for this driver config.
/// Used by the system-wide `/suspend` scheme endpoint to walk drivers
/// in priority order and signal each in turn. Returns PIDs in
/// registration order; the manager iterates drivers in priority order
/// (highest first for resume, lowest first for suspend).
#[allow(dead_code)] // public API used by tests
pub fn spawned_pids_snapshot(cfg: &DriverConfig) -> Vec<u32> {
let map = cfg
.spawned
.lock()
.unwrap_or_else(|e| e.into_inner());
map.values().map(|sd| sd.pid).collect()
}
/// Send `signal` to every spawned child of this driver config. Returns
/// the count of PIDs successfully signalled (signal failures are
/// logged but do not abort the walk — system PM must attempt every
/// driver even if one fails).
#[allow(dead_code)] // public API used by tests
pub fn signal_all_spawned(cfg: &DriverConfig, signal: i32) -> usize {
let pids = spawned_pids_snapshot(cfg);
let mut ok = 0;
for pid in pids {
if send_signal(pid, signal).is_ok() {
ok += 1;
}
}
ok
}
/// Decision returned by [`spawn_decision_gate`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SpawnDecision {
/// Probe may spawn the driver daemon.
Spawn,
/// Probe must NOT spawn; the manager is in observe-only mode.
ObserveOnly(String),
}
/// Process-wide loaded blacklist. Initialised once via
/// [`set_global_shared_blacklist`] from `main()` and consulted by
/// [`spawn_decision_gate`]. A `None` value (e.g. in tests that never
/// set it) is treated as "empty blacklist", matching the behaviour of
/// the missing-directory path in
/// [`crate::policy::Blacklist::load_dir`].
static GLOBAL_BLACKLIST: std::sync::OnceLock<crate::policy::SharedBlacklist> =
std::sync::OnceLock::new();
/// Registered driver configs. Populated by `load_all()` at startup and
/// used by the `/modalias` scheme endpoint for lookup. A `None` value
/// (e.g. in tests that never call `load_all`) is treated as an empty set,
/// matching the behaviour of the missing-config path in `load_all`.
static REGISTERED_DRIVERS: std::sync::OnceLock<Vec<DriverConfig>> =
std::sync::OnceLock::new();
pub fn set_registered_drivers(drivers: Vec<DriverConfig>) {
let _ = REGISTERED_DRIVERS.set(drivers);
}
pub fn drivers_registered() -> &'static [DriverConfig] {
REGISTERED_DRIVERS
.get()
.map(|v| v.as_slice())
.unwrap_or(&[])
}
pub fn set_global_shared_blacklist(blacklist: crate::policy::SharedBlacklist) {
let _ = GLOBAL_BLACKLIST.set(blacklist);
}
pub fn blacklist_match(driver_name: &str) -> bool {
GLOBAL_BLACKLIST
.get()
.map(|b| b.is_blacklisted(driver_name))
.unwrap_or(false)
}
// ── Process-wide loaded driver options (N1 wiring) ──────────────────
//
// Initialised once via `set_global_shared_driver_options` from `main()`.
// Consulted by the spawn path to emit `REDBEAR_DRIVER_PARAM_<NAME>=<value>`
// env vars for each override declared for the spawned driver. A `None`
// value (e.g. in tests that never set it) is treated as "no overrides" —
// the spawn path simply emits no extra env vars.
static GLOBAL_DRIVER_OPTIONS: std::sync::OnceLock<crate::policy::SharedDriverOptions> =
std::sync::OnceLock::new();
pub fn set_global_shared_driver_options(options: crate::policy::SharedDriverOptions) {
let _ = GLOBAL_DRIVER_OPTIONS.set(options);
}
pub fn driver_options_for(driver_name: &str) -> Vec<(String, String)> {
GLOBAL_DRIVER_OPTIONS
.get()
.map(|o| o.for_driver(driver_name))
.unwrap_or_default()
}
// ── Process-wide loaded autoload list (N2 wiring) ───────────────────
static GLOBAL_AUTOLOAD: std::sync::OnceLock<crate::policy::SharedAutoloadList> =
std::sync::OnceLock::new();
pub fn set_global_shared_autoload_list(list: crate::policy::SharedAutoloadList) {
let _ = GLOBAL_AUTOLOAD.set(list);
}
#[allow(dead_code)] // public API used by future initfs manager (operator introspection)
pub fn autoload_modules() -> Vec<String> {
GLOBAL_AUTOLOAD
.get()
.map(|a| a.modules())
.unwrap_or_default()
}
// ── Process-wide loaded initfs manifest (N3 wiring) ─────────────────
static GLOBAL_INITFS_MANIFEST: std::sync::OnceLock<crate::policy::SharedInitfsManifest> =
std::sync::OnceLock::new();
pub fn set_global_shared_initfs_manifest(manifest: crate::policy::SharedInitfsManifest) {
let _ = GLOBAL_INITFS_MANIFEST.set(manifest);
}
#[allow(dead_code)] // public API used by initfs manager (operator introspection)
pub fn initfs_manifest_stages() -> Vec<(crate::policy::InitfsStageKind, Vec<String>)> {
GLOBAL_INITFS_MANIFEST
.get()
.map(|m| m.stages())
.unwrap_or_default()
}
/// The SpawnDecision committee: returns whether the manager should spawn the
/// driver daemon at this probe, given the current environment and CLI flags.
///
/// Per migration plan § 5.1 D1, the committee gates on five signals:
///
/// 1. The driver's `command` must be non-empty (handled in `probe` itself).
/// 2. `claim_pci_device` must have returned a bind handle (handled in `probe`).
/// 3. All `depends_on` schemes must be present (handled in `probe`).
/// 4. CLI flag `--no-spawn` (observation-only mode).
/// 5. Environment variable `REDBEAR_DRIVER_MANAGER_ACTIVE=0` (observation-only).
///
/// When any gate rejects, the committee returns [`SpawnDecision::ObserveOnly`]
/// with a short reason; the manager emits `ProbeResult::NotSupported`.
pub fn spawn_decision_gate() -> SpawnDecision {
let args: Vec<String> = std::env::args().collect();
if args.iter().any(|a| a == "--no-spawn" || a == "--observe") {
return SpawnDecision::ObserveOnly("cli flag --no-spawn/--observe".to_string());
}
if let Ok(val) = std::env::var("REDBEAR_DRIVER_MANAGER_ACTIVE") {
if val == "0" || val.eq_ignore_ascii_case("false") || val == "no" {
return SpawnDecision::ObserveOnly(format!(
"REDBEAR_DRIVER_MANAGER_ACTIVE={}",
val
));
}
}
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.
///
/// Format detection: each file is tried as a `[[driver]]` (new) first,
/// then as `[[drivers]]` (legacy pcid-spawner) if that fails. The
/// legacy form is normalised to one `DriverConfig` per legacy entry,
/// expanding `ids` and `device_id_range` into individual match entries.
/// Format errors in either form are reported with `parse <path> failed`.
pub fn load_all(dir: &str) -> Result<Vec<DriverConfig>, String> {
let entries = fs::read_dir(dir).map_err(|e| format!("read_dir failed: {}", e))?;
let mut configs = Vec::new();
for entry in entries {
let entry = entry.map_err(|e| format!("entry error: {}", e))?;
let path = entry.path();
if !path.is_file() {
continue;
}
let data = fs::read_to_string(&path)
.map_err(|e| format!("read {} failed: {}", path.display(), e))?;
let parsed_new: Result<RawDriverToml, _> = toml::from_str(&data);
match parsed_new {
Ok(parsed) => {
for driver in parsed.driver {
let matches: Vec<DriverMatch> =
driver.r#match.into_iter().map(DriverMatch::from).collect();
let initial_power_state = match &driver.initial_power_state {
Some(s) => match PciPowerState::from_toml(s) {
Ok(state) => state,
Err(e) => {
log::warn!(
"{}: driver `{}`: {} (defaulting to D0)",
path.display(),
driver.name,
e
);
PciPowerState::D0
}
},
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,
priority: driver.priority,
command: driver.command,
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()),
});
}
}
Err(new_err) => {
let parsed_legacy: Result<RawLegacyToml, _> = toml::from_str(&data);
match parsed_legacy {
Ok(parsed) => {
log::warn!(
"{}: legacy `[[drivers]]` format detected; \
convert to `[[driver]]` to silence this warning",
path.display()
);
for legacy in parsed.drivers {
configs.push(convert_legacy(legacy));
}
}
Err(legacy_err) => {
return Err(format!(
"{}: neither `[[driver]]` ({}) nor `[[drivers]]` ({}) \
parse succeeded",
path.display(),
new_err,
legacy_err
));
}
}
}
}
}
configs.sort_by(|a, b| b.priority.cmp(&a.priority));
Ok(configs)
}
/// Reap a child known to have exited via crash (non-zero exit code,
/// or a signal other than SIGTERM/SIGINT). Removes the device from
/// the spawned/pid_to_device maps, closes the error_channel fd, and
/// advances the CrashTracker. After `threshold` consecutive crashes
/// the device is auto-blacklisted.
pub fn reap_pid(&self, pid: u32) {
self.reap_pid_inner(pid, /*crashed=*/ true);
}
/// Reap a child known to have exited cleanly (exit code 0, or
/// SIGTERM/SIGINT from our own `remove()`). Removes the device from
/// the maps and closes the error_channel fd, but does NOT advance
/// the crash tracker — this prevents the 5-consecutive-exits
/// autoblacklist from triggering on intentional driver reloads.
pub fn reap_pid_clean(&self, pid: u32) {
self.reap_pid_inner(pid, /*crashed=*/ false);
}
fn reap_pid_inner(&self, pid: u32, crashed: bool) {
let device_key = if let Ok(mut p2d) = self.pid_to_device.lock() {
if let Some(key) = p2d.remove(&pid) {
if let Ok(mut spawned) = self.spawned.lock() {
spawned.remove(&key);
}
Some(key)
} else {
None
}
} else {
None
};
if let Some(key) = device_key {
// N18 Bug4: close the error_channel fd on every reap path so
// a crashed or cleanly-exited daemon doesn't leak a UnixStream
// until process exit.
crate::error_channel::global().remove(&key);
if crashed {
if let Some(count) = crash_tracker().record_failure(&key) {
log::warn!(
"crash-threshold: driver={} device={} consecutive_failures={} \
auto-blacklisted (override via /etc/driver-manager.d/disable-<bdf>.toml \
or REDBEAR_DRIVER_CRASH_THRESHOLD override)",
self.name,
key,
count
);
}
} else {
// Clean exit: clear any backoff the device was in so a
// later crash starts the count from 1, not from where the
// previous clean-exit backoff ended.
crash_tracker().record_success(&key);
}
}
}
}
fn pci_device_path(info: &DeviceInfo) -> String {
if info.raw_path.starts_with("/scheme/pci/") {
info.raw_path.clone()
} else {
format!("/scheme/pci/{}", info.id.path)
}
}
fn check_scheme_available(name: &str) -> bool {
if std::path::Path::new(&format!("/scheme/{}", name)).exists() {
return true;
}
false
}
impl Driver for DriverConfig {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
&self.description
}
fn priority(&self) -> i32 {
self.priority
}
fn match_table(&self) -> &[DriverMatch] {
&self.matches
}
fn probe(&self, info: &DeviceInfo) -> ProbeResult {
let device_key = info.id.path.clone();
{
let spawned = self.spawned.lock().unwrap_or_else(|e| e.into_inner());
if spawned.contains_key(&device_key) {
log::debug!("driver {} already bound to {}", self.name, device_key);
return ProbeResult::Bound;
}
}
// F1 crash tracker: if this BDF recently failed enough times
// to exceed the threshold, the policy blacklist has already
// absorbed the BDF (see `record_failure` integration below).
// For in-backoff state below the threshold, defer with a
// backoff reason so the next probe cycle respects it.
if crash_tracker().is_in_backoff(&device_key, Instant::now()) {
log::debug!(
"crash-backoff: driver={} device={} skipping until next retry",
self.name,
device_key
);
return ProbeResult::Deferred {
reason: format!("crash-backoff for {}", device_key),
};
}
let early_quirks = crate::quirks::decide_for_phase(
info,
redox_driver_sys::quirks::QuirkPhase::Early,
);
if early_quirks.has_flags() {
log::info!(
"early-quirks: driver={} device={} flags={}",
self.name,
device_key,
early_quirks.summary_short()
);
}
if early_quirks.flags.contains(redox_driver_sys::quirks::PciQuirkFlags::WRONG_CLASS)
|| early_quirks
.flags
.contains(redox_driver_sys::quirks::PciQuirkFlags::BROKEN_BRIDGE)
{
return ProbeResult::NotSupported;
}
// Claim the device by opening its pcid channel. The channel is
// exclusive: pcid returns ENOLCK to a second opener, so a
// concurrent probe of the same device loses atomically. This is
// the same model pcid-spawner uses — the channel fd doubles as
// the claim and is later handed to the spawned child.
let claim_start = Instant::now();
let device_path = pci_device_path(info);
let mut handle = match PciFunctionHandle::connect_by_path(Path::new(&device_path)) {
Ok(handle) => handle,
Err(err) => {
if err.raw_os_error() == Some(syscall::ENOLCK as i32) {
log::debug!("device {} already claimed (ENOLCK)", device_key);
return ProbeResult::NotSupported;
}
return ProbeResult::Deferred {
reason: format!("channel open for {} failed: {}", device_path, err),
};
}
};
// Mutual exclusion: if this driver is exclusive_with another driver
// and that other driver is already bound to this device, skip the
// spawn. Priority determines who wins — the higher-priority driver
// claims the device; the lower-priority one is deferred.
for excluded in &self.exclusive_with {
if let Some(other) = crate::config::drivers_registered()
.iter()
.find(|d| d.name == *excluded)
{
let other_bound = other
.spawned
.lock()
.unwrap_or_else(|e| e.into_inner())
.contains_key(&device_key);
if other_bound {
log::info!(
"exclusive_with: driver {} skips spawn of {} (already bound by {})",
self.name,
device_key,
other.name
);
return ProbeResult::Deferred {
reason: format!("exclusive_with {}: device {} already claimed by {}", self.name, device_key, other.name),
};
}
}
}
let quirk_decision = crate::quirks::decide(info);
if quirk_decision.has_flags() {
log::info!(
"quirks: driver={} device={} flags={}",
self.name,
device_key,
quirk_decision.summary_short()
);
}
if quirk_decision.needs_firmware {
if check_scheme_available("firmware") {
log::info!(
"firmware-ready: driver={} device={} firmware scheme available",
self.name,
device_key
);
crate::timing::record_firmware_ready_if_deferred(&device_key);
} else {
log::info!(
"quirk-defer: driver={} device={} waiting-for-firmware",
self.name,
device_key
);
crate::timing::record_firmware_defer_start(&device_key);
return ProbeResult::Deferred {
reason: format!(
"PciQuirkFlags::NEED_FIRMWARE set for {}",
device_key
),
};
}
}
if blacklist_match(&self.name) {
log::info!(
"policy: driver {} is on the blacklist; not spawning for {}",
self.name,
device_key
);
return ProbeResult::NotSupported;
}
match spawn_decision_gate() {
SpawnDecision::Spawn => {}
SpawnDecision::ObserveOnly(reason) => {
log::debug!(
"observe-only: skipping spawn of driver {} for {} ({})",
self.name,
device_key,
reason
);
return ProbeResult::NotSupported;
}
}
if self.command.is_empty() {
return ProbeResult::Fatal {
reason: String::from("empty command"),
};
}
let actual_path = if self.command[0].starts_with('/') {
self.command[0].clone()
} else {
format!("/usr/lib/drivers/{}", self.command[0])
};
if !std::path::Path::new(&actual_path).exists() {
return ProbeResult::Deferred {
reason: format!("driver binary not found: {}", actual_path),
};
}
let deps: Vec<String> = if !self.depends_on.is_empty() {
self.depends_on.clone()
} else {
guess_dependencies(&self.name)
};
for dep in &deps {
if !check_scheme_available(dep) {
return ProbeResult::Deferred {
reason: format!("dependency scheme not ready: {}", dep),
};
}
}
log::info!("probing {} with driver {}", device_key, self.name);
handle.enable_device();
let channel_fd = handle.into_inner_fd();
let channel_fd = unsafe { OwnedFd::from_raw_fd(channel_fd) };
let mut cmd = Command::new(&actual_path);
for arg in &self.command[1..] {
cmd.arg(arg);
}
// Sidecar error-report channel: a unix socketpair lets the
// spawned daemon receive AER notifications and return a
// RecoveryAction. The child fd goes in the env var; the
// parent fd is registered for the AER dispatch path to
// consult. Drivers that do not opt in ignore the fd.
//
// Initfs mode skips the socketpair entirely: the initfs
// kernel namespace does not support AF_UNIX socketpair (returns
// ENODEV) and the initfs driver-manager is transient — it
// exits after enumeration, so no AER dispatch ever runs. The
// initfs spawn log line is silent on this branch by design.
let error_channel = if crate::is_initfs_mode() {
None
} else {
match std::os::unix::net::UnixStream::pair() {
Ok((parent, child)) => {
cmd.env("REDBEAR_DRIVER_ERROR_FD", child.as_raw_fd().to_string());
// Forget the child fd so dropping the cmd doesn't
// double-close it (Command::spawn takes ownership).
std::mem::forget(child);
Some((
crate::error_channel::ErrorChannel { stream: parent },
device_key.clone(),
))
}
Err(err) => {
log::warn!(
"driver {} for device {} could not get sidecar error channel: {}",
self.name,
device_key,
err
);
None
}
}
};
cmd.env("PCID_CLIENT_CHANNEL", channel_fd.as_raw_fd().to_string());
cmd.env("PCID_DEVICE_PATH", &device_path);
// N1: apply per-driver parameter overrides from the policy
// directory's `[[options]]` entries. Each `(name, value)` pair
// becomes an env var `REDBEAR_DRIVER_PARAM_<NAME>=<value>` so
// the daemon can consume them without a separate IPC channel.
let overrides = driver_options_for(&self.name);
for (param_name, param_value) in &overrides {
cmd.env(
format!("REDBEAR_DRIVER_PARAM_{}", param_name.to_uppercase()),
param_value,
);
}
if !overrides.is_empty() {
log::info!(
"policy-options: driver={} applied {} override{}",
self.name,
overrides.len(),
if overrides.len() == 1 { "" } else { "s" }
);
}
// 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",
self.initial_power_state.as_str(),
);
log::info!(
"power-state: driver={} device={} initial={} (driver daemon will \
set this on its granted channel)",
self.name,
device_key,
self.initial_power_state.as_str()
);
}
let quirk_decision_at_spawn = crate::quirks::decide(info);
if quirk_decision_at_spawn.has_flags() {
log::info!(
"spawn-hints: driver={} device={} flags={}",
self.name,
device_key,
quirk_decision_at_spawn.summary_short()
);
}
if std::env::var("pci").map(|v| v == "nomsi" || v == "no_msi").unwrap_or(false) {
cmd.env("REDBEAR_DRIVER_PCI_IRQ_MODE", "intx_only");
} else if quirk_decision_at_spawn.request_intx_or_msi_only {
cmd.env("REDBEAR_DRIVER_PCI_IRQ_MODE", "intx_or_msi");
}
if quirk_decision_at_spawn.disable_accel {
cmd.env("REDBEAR_DRIVER_DISABLE_ACCEL", "1");
}
cmd.env(
"REDBEAR_DRIVER_IOMMU_GROUP",
redox_driver_core::modern_technology::iommu_group_env_value(&info.id.path),
);
cmd.env(
"REDBEAR_DRIVER_NUMA_NODE",
redox_driver_core::modern_technology::numa_node_env_value(&info.id.path),
);
let msix = redox_driver_core::modern_technology::propose_msix_vectors(&info.id.path, 1, 16);
cmd.env("REDBEAR_DRIVER_MSIX_VECTORS", msix.recommended.to_string());
match cmd.spawn() {
Ok(child) => {
let pid = child.id();
let claim_spawn_us = claim_start.elapsed().as_micros() as u64;
crate::timing::record("claim-spawn", claim_spawn_us);
log::info!(
"driver {} spawned (pid {}) for device {} (claim-spawn {}us)",
self.name,
pid,
device_key,
claim_spawn_us
);
crate::timing::record_firmware_ready_if_deferred(&device_key);
if let Some((channel, key)) = error_channel {
crate::error_channel::global()
.insert(key, std::sync::Arc::new(channel));
}
let mut spawned = self.spawned.lock().unwrap_or_else(|e| e.into_inner());
spawned.insert(device_key.clone(), SpawnedDriver { pid, channel_fd });
if let Ok(mut p2d) = self.pid_to_device.lock() {
p2d.insert(pid, device_key.clone());
}
// F1: a successful spawn clears any prior failure
// state for this BDF, so a transient crash followed by
// a working spawn does not leave the device
// permanently back-pressured.
crash_tracker().record_success(&device_key);
ProbeResult::Bound
}
Err(e) => {
// F1: an immediate spawn failure (e.g. ENOENT for a
// missing binary) counts toward the threshold just
// like a SIGCHLD reap would.
if let Some(count) = crash_tracker().record_failure(&device_key) {
log::warn!(
"crash-threshold: driver={} device={} consecutive_failures={} \
(spawn returned: {})",
self.name,
device_key,
count,
e
);
}
ProbeResult::Fatal {
reason: format!("spawn failed: {}", e),
}
}
}
}
fn remove(&self, info: &DeviceInfo) -> Result<(), DriverError> {
let device_key = info.id.path.clone();
let binding = {
let mut spawned = self
.spawned
.lock()
.unwrap_or_else(|e| e.into_inner());
spawned.remove(&device_key)
};
crate::error_channel::global().remove(&device_key);
if let Some(ref b) = binding {
if let Ok(mut p2d) = self.pid_to_device.lock() {
p2d.remove(&b.pid);
}
}
if let Some(ref b) = binding {
if let Ok(mut p2d) = self.pid_to_device.lock() {
p2d.remove(&b.pid);
}
}
match binding {
Some(binding) => {
let channel_fd = binding.channel_fd.as_raw_fd();
log::info!(
"unbind-request: device {} from driver {} (pid {}, channel fd {})",
device_key,
self.name,
binding.pid,
channel_fd
);
match signal_then_collect(binding.pid, Duration::from_millis(GRACE_PERIOD_MS)) {
Ok(()) => {
log::info!(
"unbound: device {} from driver {} (pid {} exited cleanly)",
device_key,
self.name,
binding.pid
);
Ok(())
}
Err(why) => {
log::error!(
"unbind: failed to stop pid {} for driver {} device {}: {}",
binding.pid,
self.name,
device_key,
why
);
Err(DriverError::IoError)
}
}
}
None => {
log::warn!("driver {} not bound to device {}", self.name, device_key);
Err(DriverError::Other("not bound"))
}
}
}
fn suspend(&self, info: &DeviceInfo) -> Result<(), DriverError> {
let device_key = info.id.path.clone();
let spawned_pids = {
let spawned = self
.spawned
.lock()
.unwrap_or_else(|e| e.into_inner());
spawned.keys().cloned().collect::<Vec<_>>()
};
log::info!(
"suspend-request: driver {} device {} ({} spawned child(ren))",
self.name,
device_key,
spawned_pids.len()
);
send_signal_to_spawned(&self.spawned, SIGTERM, &device_key)?;
Ok(())
}
fn resume(&self, info: &DeviceInfo) -> Result<(), DriverError> {
let device_key = info.id.path.clone();
let spawned_pids = {
let spawned = self
.spawned
.lock()
.unwrap_or_else(|e| e.into_inner());
spawned.keys().cloned().collect::<Vec<_>>()
};
log::info!(
"resume-request: driver {} device {} ({} spawned child(ren))",
self.name,
device_key,
spawned_pids.len()
);
// SIGCONT (signal 18 on Linux/Redox). The driver daemon is
// expected to honour SIGCONT by resuming normal operation;
// if the daemon has exited, the SIGCHLD reaper will reap it
// and a subsequent probe cycle will rebind the device.
send_signal_to_spawned(&self.spawned, libc::SIGCONT as i32, &device_key)?;
Ok(())
}
fn on_error(
&self,
info: &DeviceInfo,
severity: ErrorSeverity,
) -> Result<RecoveryAction, DriverError> {
let _ = info;
Ok(match severity {
ErrorSeverity::Correctable => RecoveryAction::Handled,
ErrorSeverity::NonFatal => RecoveryAction::ResetDevice,
ErrorSeverity::Fatal => RecoveryAction::RescanBus,
})
}
fn params(&self) -> DriverParams {
let mut p = DriverParams::new();
p.define(
"enabled",
"Whether this driver is active",
ParamValue::Bool(true),
true,
);
p.define(
"priority",
"Probe priority (higher = earlier)",
ParamValue::Int(self.priority as i64),
false,
);
p
}
}
/// Driver-specified dependencies. Parsed from [driver.depends] TOML field.
/// Example: depends_on = ["pci", "acpi"]
/// When specified, takes precedence over guess_dependencies().
fn guess_dependencies(driver_name: &str) -> Vec<String> {
match driver_name {
"xhcid" | "usbhubd" | "usbctl" | "usbhidd" | "usbscsid" => {
vec![String::from("pci")]
}
"nvmed" | "ahcid" | "ided" | "virtio-blkd" => {
vec![String::from("pci")]
}
"e1000d" | "rtl8168d" | "rtl8139d" | "ixgbed" | "virtio-netd" => {
vec![String::from("pci")]
}
"vesad" | "virtio-gpud" | "redox-drm" => {
vec![String::from("pci")]
}
"ihdad" | "ac97d" | "sb16d" => {
vec![String::from("pci")]
}
"ps2d" => vec![String::from("serio")],
"i2c-hidd" => vec![String::from("i2c")],
"dw-acpi-i2cd" | "amd-mp2-i2cd" | "intel-lpss-i2cd" => {
vec![String::from("acpi"), String::from("i2c")]
}
_ => vec![String::from("pci")],
}
}
#[derive(Deserialize)]
struct RawDriverToml {
driver: Vec<RawDriverEntry>,
}
#[derive(Deserialize)]
struct RawDriverEntry {
name: String,
#[serde(default)]
description: String,
#[serde(default)]
priority: i32,
#[serde(default)]
command: Vec<String>,
#[serde(rename = "match")]
#[serde(default)]
r#match: Vec<RawDriverMatch>,
#[serde(default)]
depends_on: Vec<String>,
#[serde(default)]
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)]
struct RawLegacyToml {
drivers: Vec<RawLegacyEntry>,
}
#[derive(Deserialize)]
struct RawLegacyEntry {
#[serde(default)]
name: Option<String>,
#[serde(default)]
class: Option<u8>,
#[serde(default)]
subclass: Option<u8>,
#[serde(default)]
interface: Option<u8>,
#[serde(default)]
ids: Option<std::collections::BTreeMap<String, Vec<u16>>>,
#[serde(default)]
vendor: Option<u16>,
#[serde(default)]
exclusive_with: Vec<String>,
#[serde(default)]
device: Option<u16>,
#[serde(default)]
device_id_range: Option<RawLegacyRange>,
#[serde(default)]
command: Vec<String>,
}
#[derive(Deserialize)]
struct RawLegacyRange {
start: u16,
end: u16,
}
fn convert_legacy(legacy: RawLegacyEntry) -> DriverConfig {
let name = legacy
.name
.unwrap_or_else(|| "(unnamed from legacy pcid.d)".to_string());
let mut matches: Vec<DriverMatch> = Vec::new();
if let (Some(v), Some(d)) = (legacy.vendor, legacy.device) {
matches.push(DriverMatch {
vendor: Some(v),
device: Some(d),
class: legacy.class,
subclass: legacy.subclass,
prog_if: legacy.interface,
subsystem_vendor: None,
subsystem_device: None,
});
} else if let Some(v) = legacy.vendor {
matches.push(DriverMatch {
vendor: Some(v),
device: None,
class: legacy.class,
subclass: legacy.subclass,
prog_if: legacy.interface,
subsystem_vendor: None,
subsystem_device: None,
});
}
if let Some(ids) = legacy.ids {
for (vendor_token, dev_list) in ids {
let vendor_u16 = match u16::from_str_radix(vendor_token.trim_start_matches("0x"), 16) {
Ok(v) => v,
Err(e) => {
log::warn!(
"convert_legacy: invalid vendor token {:?} for driver `{}`: {}",
vendor_token,
name,
e
);
continue;
}
};
for d in dev_list {
matches.push(DriverMatch {
vendor: Some(vendor_u16),
device: Some(d),
class: legacy.class,
subclass: legacy.subclass,
prog_if: legacy.interface,
subsystem_vendor: None,
subsystem_device: None,
});
}
}
}
if let Some(range) = legacy.device_id_range {
if range.end >= range.start {
for d in range.start..=range.end {
matches.push(DriverMatch {
vendor: None,
device: Some(d),
class: legacy.class,
subclass: legacy.subclass,
prog_if: legacy.interface,
subsystem_vendor: None,
subsystem_device: None,
});
}
} else {
log::warn!(
"convert_legacy: driver `{}` has degenerate device_id_range {}-{}; \
skipping",
name,
range.start,
range.end
);
}
}
if matches.is_empty() {
log::warn!(
"convert_legacy: driver `{}` has no matches after conversion; \
falling back to class-only",
name
);
matches.push(DriverMatch {
vendor: None,
device: None,
class: legacy.class,
subclass: legacy.subclass,
prog_if: legacy.interface,
subsystem_vendor: None,
subsystem_device: None,
});
}
DriverConfig {
name,
description: String::new(),
priority: 0,
command: legacy.command,
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()),
}
}