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