From f7fd8f8eeeedf278718067fd7385ab5cc8465dc2 Mon Sep 17 00:00:00 2001 From: vasilito Date: Tue, 28 Jul 2026 06:24:08 +0900 Subject: [PATCH] driver-manager: N20 audit - aer BDF validation (A4) AerEvent::parse now rejects malformed device= tokens at parse time via the new is_valid_pci_bdf helper (SSSS:BB:DD.F format matching the kernel's pci_uevent). Garbled transport yields a real AER for a real device with the wrong address; route_to_driver would match no driver and silently drop the event to RecoveryAction::Handled. Three routing tests retargeted from "d" to "0000:aa:00.0" (both the binds key and the AerEvent::parse(...) string). 2 new tests added: parse_rejects_garbled_bdf (parse-level) and is_valid_pci_bdf_cases (unit-level). --- .../system/driver-manager/source/src/aer.rs | 78 +++++++++++++++++-- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/local/recipes/system/driver-manager/source/src/aer.rs b/local/recipes/system/driver-manager/source/src/aer.rs index c32786a672..cd7acd36fd 100644 --- a/local/recipes/system/driver-manager/source/src/aer.rs +++ b/local/recipes/system/driver-manager/source/src/aer.rs @@ -25,6 +25,12 @@ impl AerEvent { }; } else if let Some(d) = token.strip_prefix("device=") { device = d.to_string(); + // N20 A4: validate BDF format. Garbled transport yields a real + // AER for a real device with the wrong address; the routing logic + // would match no driver and silently drop the event. Reject early. + if !is_valid_pci_bdf(&device) { + return None; + } } } if device.is_empty() { @@ -142,6 +148,34 @@ pub(crate) fn severity_default(severity: ErrorSeverity) -> RecoveryAction { } } +/// Check whether a string is a valid PCI BDF (SSSS:BB:DD.F). Returns +/// `true` if it parses as 4 hex groups (2 colon-separated + 1 dot). +/// Matches the kernel's pci_uevent format +/// (drivers/pci/pci-driver.c:pci_uevent). +fn is_valid_pci_bdf(s: &str) -> bool { + if s.is_empty() { + return false; + } + let colon_count = s.bytes().filter(|&b| b == b':').count(); + if colon_count != 2 || s.bytes().filter(|&b| b == b'.').count() != 1 { + return false; + } + let mut parts = s.split(':'); + let seg = parts.next().unwrap_or(""); + let bus = parts.next().unwrap_or(""); + let dev_func = parts.next().unwrap_or(""); + let func = match dev_func.split('.').nth(1) { + Some(f) => f, + None => return false, + }; + let dev = match dev_func.split('.').next() { + Some(d) => d, + None => return false, + }; + let is_hex = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_hexdigit()); + is_hex(seg) && is_hex(bus) && is_hex(dev) && is_hex(func) +} + #[cfg(test)] mod tests { use super::*; @@ -194,10 +228,10 @@ mod tests { #[test] fn routing_severity_mapping() { - let binds = vec![("d".to_string(), "n".to_string())]; + let binds = vec![("0000:aa:00.0".to_string(), "n".to_string())]; assert_eq!( route_to_driver( - &AerEvent::parse("severity=Correctable device=d").unwrap(), + &AerEvent::parse("severity=Correctable device=0000:aa:00.0").unwrap(), &binds, |_, s| Some(severity_default(s)) ), @@ -205,7 +239,7 @@ mod tests { ); assert_eq!( route_to_driver( - &AerEvent::parse("severity=NonFatal device=d").unwrap(), + &AerEvent::parse("severity=NonFatal device=0000:aa:00.0").unwrap(), &binds, |_, s| Some(severity_default(s)) ), @@ -213,7 +247,7 @@ mod tests { ); assert_eq!( route_to_driver( - &AerEvent::parse("severity=Fatal device=d").unwrap(), + &AerEvent::parse("severity=Fatal device=0000:aa:00.0").unwrap(), &binds, |_, s| Some(severity_default(s)) ), @@ -223,9 +257,9 @@ mod tests { #[test] fn driver_callback_overrides_severity_default() { - let binds = vec![("d".to_string(), "n".to_string())]; + let binds = vec![("0000:aa:00.0".to_string(), "n".to_string())]; let action = route_to_driver( - &AerEvent::parse("severity=Fatal device=d").unwrap(), + &AerEvent::parse("severity=Fatal device=0000:aa:00.0").unwrap(), &binds, |_, _| Some(RecoveryAction::Handled), ); @@ -234,9 +268,9 @@ mod tests { #[test] fn driver_callback_none_falls_back_to_severity_default() { - let binds = vec![("d".to_string(), "n".to_string())]; + let binds = vec![("0000:aa:00.0".to_string(), "n".to_string())]; let action = route_to_driver( - &AerEvent::parse("severity=Fatal device=d").unwrap(), + &AerEvent::parse("severity=Fatal device=0000:aa:00.0").unwrap(), &binds, |_, _| None, ); @@ -318,4 +352,32 @@ mod tests { let _ = std::fs::remove_file(&path); } + + #[test] + fn parse_rejects_garbled_bdf() { + // N20 A4: a garbled device token must be rejected by parse so + // it never reaches route_to_driver where it would silently fall to + // RecoveryAction::Handled via the no-driver-found default. + assert!(AerEvent::parse("severity=NonFatal device=garbage").is_none()); + assert!(AerEvent::parse("severity=NonFatal device=").is_none()); + assert!(AerEvent::parse("severity=NonFatal").is_none()); + // Sanity: a valid BDF parses. + let event = AerEvent::parse("severity=NonFatal device=0000:00:1f.2"); + assert!(event.is_some()); + } + + #[test] + fn is_valid_pci_bdf_cases() { + // Valid BDFs. + assert!(is_valid_pci_bdf("0000:00:1f.2")); + assert!(is_valid_pci_bdf("0000:aa:00.0")); + // Invalid: empty, single char, no colon, no dot, extra colons. + assert!(!is_valid_pci_bdf("")); + assert!(!is_valid_pci_bdf("d")); + assert!(!is_valid_pci_bdf("no-colon")); + assert!(!is_valid_pci_bdf("0000:00:1f")); // missing dot + assert!(!is_valid_pci_bdf("0000:00:00:1f.2")); // 3 colons (off-by-one) + assert!(!is_valid_pci_bdf("0000.00.1f.2")); // dots in wrong places + assert!(!is_valid_pci_bdf("xxxx:yy:zz.w")); // non-hex segments + } }