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).
This commit is contained in:
2026-07-28 06:24:08 +09:00
parent aa480f7ca3
commit f7fd8f8eee
@@ -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
}
}