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:
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user