driver-manager: F1 — crash counter + backoff + blacklist

Closes the HIGH-severity F1 finding: drivers that crash on every
spawn used to be respawned every hotplug poll (250 ms) indefinitely,
consuming process slots and cluttering logs.

New: per-BDF CrashTracker with:
- consecutive failure counter (HashMap<BDF, CrashState>)
- exponential backoff window: base_ms * 2^N, capped at cap_ms
- auto-blacklist WARN at threshold (default 5)

Env-var configuration:
- REDBEAR_DRIVER_CRASH_THRESHOLD       (default 5)
- REDBEAR_DRIVER_BACKOFF_BASE_MS       (default 100)
- REDBEAR_DRIVER_BACKOFF_CAP_MS        (default 30000)

Probe hot path integration (config.rs::DriverConfig::probe):
- is_in_backoff(bdf) -> ProbeResult::Deferred with reason
- record_success(bdf) on Bind (clears any prior failure state)
- record_failure(bdf) on spawn ENOENT or SIGCHLD reap
- WARN log on threshold-cross with override hint

Reap integration (reap_pid):
- every reap is treated as a spawn failure
- threshold-cross WARN identifies driver + BDF + override path

New /scheme/driver-manager endpoints:
- /crash_count                  (read): lines of '<bdf> <failures>'
- /crash_count/<bdf>            (read): '<bdf> <failures>' (0 if unknown)

Tests (7 new):
- crash_tracker_record_failure_increments_and_signals_threshold
- crash_tracker_record_success_clears_state
- crash_tracker_is_in_backoff_returns_true_within_window
- crash_tracker_snapshot_returns_per_bdf_counts
- crash_tracker_apply_env_overrides
- crash_tracker_apply_env_ignores_invalid_values
- crash_tracker_global_stub_returns_when_unset

141 driver-manager tests pass.
This commit is contained in:
2026-07-27 13:28:48 +09:00
parent a043a03c81
commit c80c23dbb1
4 changed files with 1435 additions and 4 deletions
File diff suppressed because it is too large Load Diff
@@ -9,6 +9,8 @@ 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};
@@ -22,6 +24,13 @@ 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,
@@ -121,6 +130,7 @@ mod tests {
};
use std::collections::BTreeMap;
use std::sync::Mutex;
use std::time::Instant;
static ENV_LOCK: Mutex<()> = Mutex::new(());
@@ -434,6 +444,111 @@ initial_power_state = "D2"
});
}
// ── 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() {
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() {
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(|| {
@@ -581,6 +696,192 @@ fn send_signal_to_spawned(
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.
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
@@ -794,12 +1095,34 @@ impl DriverConfig {
/// maps. Called by the SIGCHLD reaper when a spawned child has
/// been observed as exited. Idempotent — a no-op if the pid is not
/// present.
///
/// F1 integration: every reap is treated as a spawn failure for
/// crash-tracking purposes. If the failure count crosses the
/// threshold, emit a WARN with the BDF + last exit so operators
/// can correlate with kernel/daemon logs.
pub fn reap_pid(&self, pid: u32) {
if let Ok(mut p2d) = self.pid_to_device.lock() {
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 {
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
);
}
}
}
@@ -848,6 +1171,22 @@ impl Driver for DriverConfig {
}
}
// 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,
@@ -1119,11 +1458,31 @@ impl Driver for DriverConfig {
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) => ProbeResult::Fatal {
reason: format!("spawn failed: {}", e),
},
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),
}
}
}
}
@@ -570,6 +570,9 @@ fn main() {
// Apply deferred-retry config from env vars (defaults to 30 retries
// at 500 ms intervals if unset). See timing.rs::set_deferred_retry_config.
apply_deferred_retry_env();
// Wire the global CrashTracker (F1). Must come before the probe
// hot path is reachable.
apply_crash_tracker_env();
reset_timeline_log();
@@ -691,6 +694,24 @@ fn async_probe_from_env() -> bool {
}
}
/// Wire the global `CrashTracker` and apply env-var overrides. Called
/// once at startup after `apply_deferred_retry_env()` so the probe hot
/// path can consult a fully-configured tracker.
static CRASH_TRACKER: config::CrashTracker = config::CrashTracker::new();
fn apply_crash_tracker_env() {
CRASH_TRACKER.apply_env();
config::set_crash_tracker(&CRASH_TRACKER);
let threshold = CRASH_TRACKER.threshold();
let base_ms = CRASH_TRACKER.backoff_base_ms();
let cap_ms = CRASH_TRACKER.backoff_cap_ms();
log::info!(
"crash-tracker: threshold={} backoff_base_ms={} backoff_cap_ms={}",
threshold,
base_ms,
cap_ms
);
}
/// Apply the deferred-retry configuration from the
/// `REDBEAR_DRIVER_DEFERRED_RETRY_COUNT` and
/// `REDBEAR_DRIVER_DEFERRED_RETRY_INTERVAL_MS` env vars. Unparseable
@@ -59,6 +59,8 @@ enum HandleKind {
Recover,
Suspend,
Resume,
CrashCount,
CrashCountBdf(String),
Timing,
}
@@ -188,11 +190,15 @@ impl DriverManagerScheme {
["recover"] => Ok(HandleKind::Recover),
["suspend"] => Ok(HandleKind::Suspend),
["resume"] => Ok(HandleKind::Resume),
["crash_count"] => Ok(HandleKind::CrashCount),
["timing"] => Ok(HandleKind::Timing),
["devices", pci_addr] if Self::valid_pci_addr(pci_addr) => {
let _ = self.device_status(pci_addr)?;
Ok(HandleKind::Device((*pci_addr).to_string()))
}
["crash_count", pci_addr] if Self::valid_pci_addr(pci_addr) => {
Ok(HandleKind::CrashCountBdf((*pci_addr).to_string()))
}
_ => Err(Error::new(ENOENT)),
}
}
@@ -277,6 +283,23 @@ impl DriverManagerScheme {
HandleKind::Device(pci_addr) => self.device_status(pci_addr),
HandleKind::Bound => self.bound_output(),
HandleKind::Events => self.events_output(),
HandleKind::CrashCount => {
let entries = crate::config::crash_tracker().snapshot();
let mut out = String::new();
for (bdf, n) in entries {
out.push_str(&format!("{bdf} {n}\n"));
}
Ok(out)
}
HandleKind::CrashCountBdf(pci_addr) => Ok(format!(
"{pci_addr} {}\n",
crate::config::crash_tracker()
.snapshot()
.into_iter()
.find(|(b, _)| b.as_str() == pci_addr.as_str())
.map(|(_, n)| n)
.unwrap_or(0)
)),
HandleKind::Modalias => {
if let Ok(results) = self.modalias_results.lock() {
if let Some(result) = results.get(&id) {
@@ -338,6 +361,10 @@ impl DriverManagerScheme {
HandleKind::Recover => format!("{SCHEME_NAME}:/recover"),
HandleKind::Suspend => format!("{SCHEME_NAME}:/suspend"),
HandleKind::Resume => format!("{SCHEME_NAME}:/resume"),
HandleKind::CrashCount => format!("{SCHEME_NAME}:/crash_count"),
HandleKind::CrashCountBdf(pci_addr) => {
format!("{SCHEME_NAME}:/crash_count/{pci_addr}")
}
HandleKind::Timing => format!("{SCHEME_NAME}:/timing"),
}
}
@@ -359,6 +386,8 @@ impl DriverManagerScheme {
| HandleKind::Recover
| HandleKind::Suspend
| HandleKind::Resume
| HandleKind::CrashCount
| HandleKind::CrashCountBdf(_)
| HandleKind::Timing => MODE_FILE | 0o644,
}
}