v4.1: AER auto-dispatch, QuirkPhase::Early gating, IOMMU/NUMA honest absence

driver-manager v4.1 — closes the three remaining P3 items the v4.0 plan
declared open after the cutover:

* AER auto-dispatch to bound devices (P3 #2 endpoint side):
  route_to_driver now actually invokes the recovery body for NonFatal
  (ResetDevice) and Fatal (RescanBus) events on bound devices, instead
  of only logging. The /recover scheme endpoint and the auto-dispatch
  both share the new scheme::DriverManagerScheme::dispatch_recovery
  helper, so operator-triggered and event-triggered recovery use the
  same code path. Driver::on_error → RecoveryAction on bound devices
  is now end-to-end rather than discarded.

* Quirk lifecycle phase Early (P3 #1): the probe path consults
  QuirkPhase::Early before the channel open and gates WRONG_CLASS /
  BROKEN_BRIDGE on it. decide_for_phase in quirks.rs bridges to
  redox-driver-sys::quirks::lookup_pci_quirks_for_phase. Combined with
  Enable (spawn-time), this is the Linux 2-of-8 minimum split. PM
  phases remain out of scope per plan § P3.

* IOMMU/NUMA honest absence: iommu_group_env_value() and
  numa_node_env_value() report "0" when the corresponding scheme is
  not present, instead of the previous deterministic BDF hash that
  looked like a real isolation group / node id. Drivers that consume
  REDBEAR_DRIVER_IOMMU_GROUP / REDBEAR_DRIVER_NUMA_NODE now see
  "no group" / "no node" instead of a value they might trust.

Tests:
  + 4 driver-manager scheme::tests::recovery_action_* cases
  + 2 redox-driver-core modern_technology::tests env-value cases
  Total: 63 driver-manager tests + 32 redox-driver-core tests, all green.
This commit is contained in:
2026-07-24 23:27:13 +09:00
parent 8de4d0eea5
commit 31bbe2fa12
5 changed files with 192 additions and 37 deletions
@@ -86,6 +86,17 @@ fn hash_bdf(bdf: &str) -> u32 {
h
}
/// Env-var value for `REDBEAR_DRIVER_IOMMU_GROUP`. Real scheme groups are
/// passed as decimal strings; synthetic groups (no iommu daemon) are
/// reported as `"0"` so drivers do not trust a hash as a real
/// isolation group.
pub fn iommu_group_env_value(bdf: &str) -> String {
match iommu_group_for(bdf) {
IommuGroups::Available(g) => g.group.to_string(),
IommuGroups::Unavailable(_) => "0".to_string(),
}
}
/// MSI-X vector proposal for a child driver.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MsiXProposal {
@@ -178,6 +189,17 @@ pub fn numa_node_for(bdf: &str) -> NumaNode {
}
}
/// Env-var value for `REDBEAR_DRIVER_NUMA_NODE`. Real scheme nodes are
/// passed as decimal strings; synthetic nodes (no numad daemon) are
/// reported as `"0"` so drivers do not trust a hash as a real node id.
pub fn numa_node_env_value(bdf: &str) -> String {
if Path::new(&format!("/scheme/numad/device/{}", bdf)).exists() {
numa_node_for(bdf).node.to_string()
} else {
"0".to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -211,4 +233,18 @@ mod tests {
let n = numa_node_for("0000:00:00.0");
assert_eq!(n.source, NumaSource::Synthetic);
}
#[test]
fn iommu_group_env_value_is_zero_when_synthetic() {
// With no /scheme/iommu/domain/<hash> on the host test
// environment, the helper must report "0" rather than a fake
// hash, so drivers do not trust a synthetic value as a real
// isolation group.
assert_eq!(iommu_group_env_value("0000:99:99.9"), "0");
}
#[test]
fn numa_node_env_value_is_zero_when_synthetic() {
assert_eq!(numa_node_env_value("0000:99:99.9"), "0");
}
}
@@ -585,6 +585,26 @@ impl Driver for DriverConfig {
}
}
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
@@ -740,13 +760,14 @@ impl Driver for DriverConfig {
cmd.env("REDBEAR_DRIVER_DISABLE_ACCEL", "1");
}
let iommu_group = redox_driver_core::modern_technology::iommu_group_for(&info.id.path);
cmd.env(
"REDBEAR_DRIVER_IOMMU_GROUP",
iommu_group.into_inner().group.to_string(),
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 numa_node = redox_driver_core::modern_technology::numa_node_for(&info.id.path);
cmd.env("REDBEAR_DRIVER_NUMA_NODE", numa_node.node.to_string());
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());
@@ -383,6 +383,7 @@ fn main() {
));
let scheme_for_events = Arc::clone(&scheme);
let scheme_for_events_aer = Arc::clone(&scheme);
let _events_thread = unified_events::spawn_unified_listener(
std::path::PathBuf::from("/scheme/pci/aer"),
std::path::PathBuf::from("/scheme/pci/pciehp"),
@@ -390,7 +391,24 @@ fn main() {
move |event| {
match event {
unified_events::UnifiedEvent::Aer(e) => {
log::info!("AER: event device={} severity={:?}", e.device, e.severity);
let binds = scheme_for_events_aer.bound_device_pairs();
let action = crate::aer::route_to_driver(&e, &binds);
log::info!(
"AER: device={} severity={:?} action={:?}",
e.device,
e.severity,
action
);
#[cfg(target_os = "redox")]
if let Some(action_str) =
scheme::DriverManagerScheme::recovery_action_str(action)
{
scheme_for_events_aer.dispatch_recovery(&e.device, action_str);
}
#[cfg(not(target_os = "redox"))]
{
let _ = scheme::DriverManagerScheme::recovery_action_str(action);
}
}
unified_events::UnifiedEvent::Pciehp(e) => {
log::info!("pciehp: event kind={} device={}", e.kind.label(), e.device);
@@ -12,7 +12,7 @@ extern crate alloc;
use redox_driver_core::device::DeviceInfo;
use redox_driver_sys::pci::{PciDeviceInfo, PciLocation};
use redox_driver_sys::quirks::{lookup_pci_quirks, PciQuirkFlags};
use redox_driver_sys::quirks::{lookup_pci_quirks, lookup_pci_quirks_for_phase, PciQuirkFlags, QuirkPhase};
/// Convert the core device-info record to the redox-driver-sys quirk-lookup
/// input. Fields that core tracks as `Option<u16>` (vendor / device /
@@ -88,8 +88,16 @@ impl QuirkDecision {
/// Compute the manager's spawn-time decision for one device by querying
/// `redox-driver-sys`'s curated quirk tables.
pub fn decide(info: &DeviceInfo) -> QuirkDecision {
decide_for_phase(info, QuirkPhase::Enable)
}
pub fn decide_for_phase(info: &DeviceInfo, phase: QuirkPhase) -> QuirkDecision {
let input = to_quirks_input(info);
let flags = lookup_pci_quirks(&input);
let flags = if phase == QuirkPhase::Enable {
lookup_pci_quirks(&input)
} else {
lookup_pci_quirks_for_phase(&input, phase)
};
summarize(flags)
}
@@ -14,6 +14,7 @@ use redox_driver_core::manager::DeviceManager;
use redox_driver_core::manager::ProbeEvent;
#[cfg(target_os = "redox")]
use redox_driver_core::driver::ProbeResult;
use redox_driver_core::driver::RecoveryAction;
#[cfg(target_os = "redox")]
use redox_scheme::scheme::SchemeSync;
#[cfg(target_os = "redox")]
@@ -351,6 +352,72 @@ impl DriverManagerScheme {
self.push_event_line(format!("{event:?}\n"));
}
/// Execute a recovery action for a bound device. Called from:
/// - the `/recover` scheme endpoint (operator-initiated),
/// - the unified AER/pciehp listener (auto-dispatch on event).
///
/// Action vocabulary matches Linux's `pci_ers_result` outcomes:
/// - `reset_device` — remove the device, sleep 100 ms, rebind (FLR-style)
/// - `rescan_bus` — re-enumerate all buses (slot-level recovery)
/// - `disconnect` — remove the device and clear driver-params
///
/// Unknown actions log an error and are no-ops; AER events with no
/// bound driver are filtered upstream by `route_to_driver`.
#[cfg(target_os = "redox")]
pub fn dispatch_recovery(&self, addr: &str, action: &str) {
let id = DeviceId {
bus: "pci".to_string(),
path: addr.to_string(),
};
log::warn!("AER recovery: device={} action={}", id.path, action);
match action {
"reset_device" => {
if let Err(err) = self.with_manager(|mgr| mgr.remove_device(&id)) {
log::warn!("AER recovery: remove_device failed: {err:?}");
return;
}
std::thread::sleep(std::time::Duration::from_millis(100));
match self.with_manager(|mgr| mgr.bind_device(&id, "")) {
Ok(events) => {
for event in &events {
self.record_probe_event(event);
}
}
Err(err) => log::warn!("AER recovery: rebind failed: {err:?}"),
}
}
"rescan_bus" => match self.with_manager(|mgr| mgr.enumerate()) {
Ok(events) => {
for event in &events {
self.record_probe_event(event);
}
}
Err(err) => log::warn!("AER recovery: enumerate failed: {err:?}"),
},
"disconnect" => {
if let Err(err) = self.with_manager(|mgr| mgr.remove_device(&id)) {
log::warn!("AER recovery: remove_device failed: {err:?}");
return;
}
notify_unbind(self, addr);
}
_ => log::error!("AER recovery: unknown action '{}'", action),
}
}
/// Map a `RecoveryAction` (from `Driver::on_error` / AER routing)
/// to the action vocabulary accepted by `dispatch_recovery`.
/// Returns `None` for actions that don't need an auto-dispatch
/// (e.g. `Handled`, `Fatal` which is operator-only).
pub fn recovery_action_str(action: RecoveryAction) -> Option<&'static str> {
match action {
RecoveryAction::Handled => None,
RecoveryAction::ResetDevice => Some("reset_device"),
RecoveryAction::RescanBus => Some("rescan_bus"),
RecoveryAction::Fatal => None,
}
}
#[cfg(target_os = "redox")]
fn write_operator(&self, kind: HandleKind, line: &str) -> Result<()> {
match kind {
@@ -434,36 +501,7 @@ impl DriverManagerScheme {
let mut parts = line.split_whitespace();
let addr = parts.next().ok_or(Error::new(EINVAL))?;
let action = parts.next().unwrap_or("reset_device");
let id = DeviceId {
bus: "pci".to_string(),
path: addr.to_string(),
};
log::warn!(
"AER recovery: device={} action={}",
id.path,
action
);
match action {
"reset_device" => {
let _event = self.with_manager(|mgr| mgr.remove_device(&id))?;
thread::sleep(std::time::Duration::from_millis(100));
let events = self.with_manager(|mgr| mgr.bind_device(&id, ""))?;
for event in &events {
self.record_probe_event(event);
}
}
"rescan_bus" => {
let events = self.with_manager(|mgr| mgr.enumerate())?;
for event in &events {
self.record_probe_event(event);
}
}
"disconnect" => {
let _event = self.with_manager(|mgr| mgr.remove_device(&id))?;
notify_unbind(self, addr);
}
_ => return Err(Error::new(EINVAL)),
}
self.dispatch_recovery(addr, action);
Ok(())
}
_ => Err(Error::new(EACCES)),
@@ -778,6 +816,40 @@ pub fn start_scheme_server(_scheme: Arc<DriverManagerScheme>) -> std::result::Re
#[cfg(test)]
mod tests {
use super::parse_new_id;
use crate::DriverManagerScheme;
use redox_driver_core::driver::RecoveryAction;
#[test]
fn recovery_action_handled_is_noop() {
assert_eq!(
DriverManagerScheme::recovery_action_str(RecoveryAction::Handled),
None
);
}
#[test]
fn recovery_action_reset_device_maps_to_reset_device_str() {
assert_eq!(
DriverManagerScheme::recovery_action_str(RecoveryAction::ResetDevice),
Some("reset_device")
);
}
#[test]
fn recovery_action_rescan_bus_maps_to_rescan_bus_str() {
assert_eq!(
DriverManagerScheme::recovery_action_str(RecoveryAction::RescanBus),
Some("rescan_bus")
);
}
#[test]
fn recovery_action_fatal_is_operator_only() {
assert_eq!(
DriverManagerScheme::recovery_action_str(RecoveryAction::Fatal),
None
);
}
#[test]
fn new_id_parses_vendor_device() {