milestone: IRQ & low-level controllers — enhanced checkers + unified harness
PCI IRQ checker: MSI-X capability detection, spurious IRQ accounting, interrupt affinity probe using redox-driver-sys IrqHandle IOMMU checker: vendor detection (AMD-Vi/Intel VT-d), control-scheme unit initialization probe, event drain probe, wire-format tests Unified harness: test-irq-runtime.sh (guest + QEMU modes) following Phase 1-5 pattern — exit-code-based, explicit binary checks Zero warnings, all tests pass.
This commit is contained in:
@@ -1,20 +1,285 @@
|
||||
use std::path::Path;
|
||||
use std::process::{self, Command};
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
use std::collections::BTreeMap;
|
||||
use std::process;
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::{
|
||||
fs::{File, OpenOptions},
|
||||
io::{Read, Write},
|
||||
path::Path,
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use redbear_hwutils::parse_args;
|
||||
|
||||
const PROGRAM: &str = "redbear-phase-iommu-check";
|
||||
const USAGE: &str = "Usage: redbear-phase-iommu-check\n\nShow the installed IOMMU validation surface inside the guest.";
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
const IOMMU_PROTOCOL_VERSION: u16 = 1;
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
const IOMMU_REQUEST_SIZE: usize = 32;
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
const IOMMU_RESPONSE_SIZE: usize = 36;
|
||||
#[cfg(target_os = "redox")]
|
||||
const IOMMU_ALL_UNITS: u32 = u32::MAX;
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
const OPCODE_QUERY: u16 = 0x0000;
|
||||
#[cfg(target_os = "redox")]
|
||||
const OPCODE_INIT_UNITS: u16 = 0x0003;
|
||||
#[cfg(target_os = "redox")]
|
||||
const OPCODE_DRAIN_EVENTS: u16 = 0x0030;
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
struct SelfTestSummary {
|
||||
discovery_source: String,
|
||||
kernel_acpi_status: String,
|
||||
dmar_present: bool,
|
||||
units_detected: u64,
|
||||
units_initialized_now: u64,
|
||||
units_initialized_after: u64,
|
||||
events_drained: u64,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
struct IommuRequest {
|
||||
opcode: u16,
|
||||
version: u16,
|
||||
arg0: u32,
|
||||
arg1: u64,
|
||||
arg2: u64,
|
||||
arg3: u64,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
struct IommuResponse {
|
||||
status: i32,
|
||||
kind: u16,
|
||||
version: u16,
|
||||
arg0: u32,
|
||||
arg1: u64,
|
||||
arg2: u64,
|
||||
arg3: u64,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
struct SchemeProbe {
|
||||
units_detected: u32,
|
||||
domains: u64,
|
||||
device_assignments: u64,
|
||||
units_initialized_before: u64,
|
||||
units_initialized_now: u32,
|
||||
units_attempted: u64,
|
||||
units_initialized_after: u64,
|
||||
events_drained: u32,
|
||||
first_event_code: u64,
|
||||
first_event_device: u64,
|
||||
first_event_address: u64,
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
impl IommuRequest {
|
||||
const fn new(opcode: u16, arg0: u32, arg1: u64, arg2: u64, arg3: u64) -> Self {
|
||||
Self {
|
||||
opcode,
|
||||
version: IOMMU_PROTOCOL_VERSION,
|
||||
arg0,
|
||||
arg1,
|
||||
arg2,
|
||||
arg3,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_bytes(self) -> [u8; IOMMU_REQUEST_SIZE] {
|
||||
let mut bytes = [0u8; IOMMU_REQUEST_SIZE];
|
||||
bytes[0..2].copy_from_slice(&self.opcode.to_le_bytes());
|
||||
bytes[2..4].copy_from_slice(&self.version.to_le_bytes());
|
||||
bytes[4..8].copy_from_slice(&self.arg0.to_le_bytes());
|
||||
bytes[8..16].copy_from_slice(&self.arg1.to_le_bytes());
|
||||
bytes[16..24].copy_from_slice(&self.arg2.to_le_bytes());
|
||||
bytes[24..32].copy_from_slice(&self.arg3.to_le_bytes());
|
||||
bytes
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
impl IommuResponse {
|
||||
fn from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
let header = bytes.get(..IOMMU_RESPONSE_SIZE)?;
|
||||
Some(Self {
|
||||
status: i32::from_le_bytes(header.get(0..4)?.try_into().ok()?),
|
||||
kind: u16::from_le_bytes(header.get(4..6)?.try_into().ok()?),
|
||||
version: u16::from_le_bytes(header.get(6..8)?.try_into().ok()?),
|
||||
arg0: u32::from_le_bytes(header.get(8..12)?.try_into().ok()?),
|
||||
arg1: u64::from_le_bytes(header.get(12..20)?.try_into().ok()?),
|
||||
arg2: u64::from_le_bytes(header.get(20..28)?.try_into().ok()?),
|
||||
arg3: u64::from_le_bytes(header.get(28..36)?.try_into().ok()?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn require_path(path: &str) -> Result<(), String> {
|
||||
if Path::new(path).exists() {
|
||||
println!("{path}");
|
||||
println!("present={path}");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("missing {path}"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
fn parse_key_value_output(text: &str) -> BTreeMap<String, String> {
|
||||
let mut values = BTreeMap::new();
|
||||
for line in text.lines() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
values.insert(key.trim().to_string(), value.trim().to_string());
|
||||
}
|
||||
}
|
||||
values
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
fn parse_u64_field(values: &BTreeMap<String, String>, key: &str) -> Result<u64, String> {
|
||||
let value = values
|
||||
.get(key)
|
||||
.ok_or_else(|| format!("iommu self-test did not report {key}"))?;
|
||||
value
|
||||
.parse::<u64>()
|
||||
.map_err(|err| format!("invalid {key} value '{value}': {err}"))
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
fn parse_bool_field(values: &BTreeMap<String, String>, key: &str) -> Result<bool, String> {
|
||||
let value = values
|
||||
.get(key)
|
||||
.ok_or_else(|| format!("iommu self-test did not report {key}"))?;
|
||||
match value.as_str() {
|
||||
"1" | "true" | "yes" => Ok(true),
|
||||
"0" | "false" | "no" => Ok(false),
|
||||
_ => Err(format!("invalid {key} value '{value}'")),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
fn parse_self_test_summary(stdout: &str) -> Result<SelfTestSummary, String> {
|
||||
let values = parse_key_value_output(stdout);
|
||||
|
||||
Ok(SelfTestSummary {
|
||||
discovery_source: values
|
||||
.get("discovery_source")
|
||||
.cloned()
|
||||
.ok_or_else(|| "iommu self-test did not report discovery_source".to_string())?,
|
||||
kernel_acpi_status: values
|
||||
.get("kernel_acpi_status")
|
||||
.cloned()
|
||||
.ok_or_else(|| "iommu self-test did not report kernel_acpi_status".to_string())?,
|
||||
dmar_present: parse_bool_field(&values, "dmar_present")?,
|
||||
units_detected: parse_u64_field(&values, "units_detected")?,
|
||||
units_initialized_now: parse_u64_field(&values, "units_initialized_now")?,
|
||||
units_initialized_after: parse_u64_field(&values, "units_initialized_after")?,
|
||||
events_drained: parse_u64_field(&values, "events_drained")?,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "redox", test))]
|
||||
fn iommu_vendor_detection(summary: &SelfTestSummary) -> &'static str {
|
||||
match (summary.units_detected > 0, summary.dmar_present) {
|
||||
(true, true) => "amd-vi+intel-vt-d",
|
||||
(true, false) => "amd-vi",
|
||||
(false, true) => "intel-vt-d",
|
||||
(false, false) => "none",
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn send_request(control: &mut File, request: IommuRequest) -> Result<IommuResponse, String> {
|
||||
control.write_all(&request.to_bytes()).map_err(|err| {
|
||||
format!(
|
||||
"failed to write IOMMU request opcode {:#06x}: {err}",
|
||||
request.opcode
|
||||
)
|
||||
})?;
|
||||
control.flush().map_err(|err| {
|
||||
format!(
|
||||
"failed to flush IOMMU request opcode {:#06x}: {err}",
|
||||
request.opcode
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut response_bytes = [0u8; IOMMU_RESPONSE_SIZE];
|
||||
control.read_exact(&mut response_bytes).map_err(|err| {
|
||||
format!(
|
||||
"failed to read IOMMU response for opcode {:#06x}: {err}",
|
||||
request.opcode
|
||||
)
|
||||
})?;
|
||||
|
||||
let response = IommuResponse::from_bytes(&response_bytes).ok_or_else(|| {
|
||||
format!(
|
||||
"failed to decode IOMMU response for opcode {:#06x}",
|
||||
request.opcode
|
||||
)
|
||||
})?;
|
||||
|
||||
if response.version != IOMMU_PROTOCOL_VERSION {
|
||||
return Err(format!(
|
||||
"IOMMU response version mismatch for opcode {:#06x}: expected {} got {}",
|
||||
request.opcode, IOMMU_PROTOCOL_VERSION, response.version
|
||||
));
|
||||
}
|
||||
if response.kind != request.opcode {
|
||||
return Err(format!(
|
||||
"IOMMU response kind mismatch for opcode {:#06x}: got {:#06x}",
|
||||
request.opcode, response.kind
|
||||
));
|
||||
}
|
||||
if response.status != 0 {
|
||||
return Err(format!(
|
||||
"IOMMU request opcode {:#06x} failed with status {}",
|
||||
request.opcode, response.status
|
||||
));
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn probe_iommu_scheme() -> Result<SchemeProbe, String> {
|
||||
let mut control = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/scheme/iommu/control")
|
||||
.map_err(|err| format!("failed to open /scheme/iommu/control: {err}"))?;
|
||||
|
||||
let query = send_request(&mut control, IommuRequest::new(OPCODE_QUERY, 0, 0, 0, 0))?;
|
||||
let init = send_request(
|
||||
&mut control,
|
||||
IommuRequest::new(OPCODE_INIT_UNITS, IOMMU_ALL_UNITS, 0, 0, 0),
|
||||
)?;
|
||||
let drain = send_request(
|
||||
&mut control,
|
||||
IommuRequest::new(OPCODE_DRAIN_EVENTS, IOMMU_ALL_UNITS, 0, 0, 0),
|
||||
)?;
|
||||
|
||||
Ok(SchemeProbe {
|
||||
units_detected: query.arg0,
|
||||
domains: query.arg1,
|
||||
device_assignments: query.arg2,
|
||||
units_initialized_before: query.arg3,
|
||||
units_initialized_now: init.arg0,
|
||||
units_attempted: init.arg1,
|
||||
units_initialized_after: init.arg2,
|
||||
events_drained: drain.arg0,
|
||||
first_event_code: drain.arg1,
|
||||
first_event_device: drain.arg2,
|
||||
first_event_address: drain.arg3,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn run() -> Result<(), String> {
|
||||
parse_args(PROGRAM, USAGE, std::env::args()).map_err(|err| {
|
||||
if err.is_empty() {
|
||||
@@ -25,6 +290,8 @@ fn run() -> Result<(), String> {
|
||||
|
||||
println!("=== Red Bear OS IOMMU Runtime Check ===");
|
||||
require_path("/usr/bin/iommu")?;
|
||||
require_path("/scheme/iommu")?;
|
||||
require_path("/scheme/iommu/control")?;
|
||||
|
||||
let output = Command::new("/usr/bin/iommu")
|
||||
.env("IOMMU_LOG", "info")
|
||||
@@ -43,31 +310,144 @@ fn run() -> Result<(), String> {
|
||||
output.status.code()
|
||||
));
|
||||
}
|
||||
if !stdout.contains("units_detected=") {
|
||||
return Err("iommu self-test did not report detected unit count".to_string());
|
||||
}
|
||||
if !stdout.contains("discovery_source=") {
|
||||
return Err("iommu self-test did not report discovery source".to_string());
|
||||
}
|
||||
if !stdout.contains("dmar_present=") {
|
||||
return Err("iommu self-test did not report DMAR presence state".to_string());
|
||||
}
|
||||
if !stdout.contains("units_initialized_now=") {
|
||||
return Err("iommu self-test did not report initialized unit count".to_string());
|
||||
}
|
||||
if !stdout.contains("units_initialized_after=") {
|
||||
return Err("iommu self-test did not report initialized-after count".to_string());
|
||||
}
|
||||
if !stdout.contains("events_drained=") {
|
||||
return Err("iommu self-test did not report drained events".to_string());
|
||||
|
||||
let summary = parse_self_test_summary(&stdout)?;
|
||||
println!(
|
||||
"amd_vi_present={}",
|
||||
if summary.units_detected > 0 { 1 } else { 0 }
|
||||
);
|
||||
println!(
|
||||
"intel_vtd_present={}",
|
||||
if summary.dmar_present { 1 } else { 0 }
|
||||
);
|
||||
println!(
|
||||
"iommu_vendor_detection={}",
|
||||
iommu_vendor_detection(&summary)
|
||||
);
|
||||
|
||||
if summary.units_detected == 0 && !summary.dmar_present {
|
||||
return Err("iommu self-test did not detect AMD-Vi or Intel VT-d presence".to_string());
|
||||
}
|
||||
|
||||
let scheme = probe_iommu_scheme()?;
|
||||
println!(
|
||||
"IOMMU_SCHEME_QUERY units_detected={} domains={} device_assignments={} units_initialized_before={}",
|
||||
scheme.units_detected,
|
||||
scheme.domains,
|
||||
scheme.device_assignments,
|
||||
scheme.units_initialized_before
|
||||
);
|
||||
println!(
|
||||
"IOMMU_INIT_UNITS units_initialized_now={} units_attempted={} units_initialized_after={}",
|
||||
scheme.units_initialized_now, scheme.units_attempted, scheme.units_initialized_after
|
||||
);
|
||||
println!(
|
||||
"IOMMU_EVENT_LOG drained_events={} first_code={} first_device={:#x} first_address={:#x}",
|
||||
scheme.events_drained,
|
||||
scheme.first_event_code,
|
||||
scheme.first_event_device,
|
||||
scheme.first_event_address
|
||||
);
|
||||
println!("iommu_event_log_probe=ok");
|
||||
|
||||
if u64::from(scheme.units_detected) == 0 {
|
||||
return Err("scheme:iommu reported zero units".to_string());
|
||||
}
|
||||
if scheme.units_initialized_after == 0 {
|
||||
return Err(
|
||||
"scheme:iommu did not leave any units initialized after INIT_UNITS".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
println!(
|
||||
"interrupt_remap_table_probe=ok initialized_units={}",
|
||||
scheme.units_initialized_after
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
fn run() -> Result<(), String> {
|
||||
parse_args(PROGRAM, USAGE, std::env::args()).map_err(|err| {
|
||||
if err.is_empty() {
|
||||
process::exit(0);
|
||||
}
|
||||
err
|
||||
})?;
|
||||
Err("redbear-phase-iommu-check requires target_os=redox".to_string())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if let Err(err) = run() {
|
||||
eprintln!("{PROGRAM}: {err}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_self_test_summary_reads_required_fields() {
|
||||
let summary = parse_self_test_summary(
|
||||
"discovery_source=kernel-acpi\nkernel_acpi_status=ok\ndmar_present=0\nunits_detected=1\nunits_initialized_now=1\nunits_initialized_after=1\nevents_drained=0\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(summary.discovery_source, "kernel-acpi");
|
||||
assert_eq!(summary.kernel_acpi_status, "ok");
|
||||
assert!(summary.units_detected > 0);
|
||||
assert!(!summary.dmar_present);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iommu_vendor_detection_prefers_combined_label_when_both_are_visible() {
|
||||
let summary = SelfTestSummary {
|
||||
discovery_source: "test".to_string(),
|
||||
kernel_acpi_status: "ok".to_string(),
|
||||
dmar_present: true,
|
||||
units_detected: 1,
|
||||
units_initialized_now: 1,
|
||||
units_initialized_after: 1,
|
||||
events_drained: 0,
|
||||
};
|
||||
|
||||
assert_eq!(iommu_vendor_detection(&summary), "amd-vi+intel-vt-d");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iommu_response_decodes_wire_format() {
|
||||
let mut bytes = [0u8; IOMMU_RESPONSE_SIZE];
|
||||
bytes[0..4].copy_from_slice(&0i32.to_le_bytes());
|
||||
bytes[4..6].copy_from_slice(&OPCODE_QUERY.to_le_bytes());
|
||||
bytes[6..8].copy_from_slice(&IOMMU_PROTOCOL_VERSION.to_le_bytes());
|
||||
bytes[8..12].copy_from_slice(&2u32.to_le_bytes());
|
||||
bytes[12..20].copy_from_slice(&3u64.to_le_bytes());
|
||||
bytes[20..28].copy_from_slice(&4u64.to_le_bytes());
|
||||
bytes[28..36].copy_from_slice(&5u64.to_le_bytes());
|
||||
|
||||
let response = IommuResponse::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(response.kind, OPCODE_QUERY);
|
||||
assert_eq!(response.arg0, 2);
|
||||
assert_eq!(response.arg3, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iommu_request_encodes_wire_format() {
|
||||
let request = IommuRequest::new(OPCODE_QUERY, 7, 8, 9, 10);
|
||||
let bytes = request.to_bytes();
|
||||
|
||||
assert_eq!(bytes.len(), IOMMU_REQUEST_SIZE);
|
||||
assert_eq!(u16::from_le_bytes([bytes[0], bytes[1]]), OPCODE_QUERY);
|
||||
assert_eq!(
|
||||
u16::from_le_bytes([bytes[2], bytes[3]]),
|
||||
IOMMU_PROTOCOL_VERSION
|
||||
);
|
||||
assert_eq!(
|
||||
u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]),
|
||||
7
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+301
-10
@@ -1,13 +1,30 @@
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
use std::{fs::File, io::Read};
|
||||
|
||||
use redbear_hwutils::parse_args;
|
||||
#[cfg(target_os = "redox")]
|
||||
use redox_driver_sys::irq::IrqHandle;
|
||||
use redox_driver_sys::pci::{PciLocation, parse_device_info_from_config_space};
|
||||
|
||||
const PROGRAM: &str = "redbear-phase-pci-irq-check";
|
||||
const USAGE: &str = "Usage: redbear-phase-pci-irq-check\n\nShow bounded live PCI/IRQ runtime reporting from the current target runtime.";
|
||||
const IRQ_REPORT_DIRS: &[&str] = &[
|
||||
"/tmp/redbear-irq-report",
|
||||
"/tmp/run/redbear-irq-report",
|
||||
"/run/redbear-irq-report",
|
||||
"/var/run/redbear-irq-report",
|
||||
"/scheme/initfs/tmp/redbear-irq-report",
|
||||
"/scheme/initfs/tmp/run/redbear-irq-report",
|
||||
"/scheme/initfs/run/redbear-irq-report",
|
||||
"/scheme/initfs/var/run/redbear-irq-report",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
struct IrqReport {
|
||||
@@ -18,6 +35,32 @@ struct IrqReport {
|
||||
reason: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
struct PciDeviceProbe {
|
||||
device: String,
|
||||
irq_line: Option<u32>,
|
||||
interrupt_support: String,
|
||||
supports_msi: bool,
|
||||
supports_msix: bool,
|
||||
msix_table_size: Option<u16>,
|
||||
msix_function_masked: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
|
||||
struct SpuriousIrqStats {
|
||||
irq7: u64,
|
||||
irq15: u64,
|
||||
total: u64,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
struct AffinityProbe {
|
||||
irq: u32,
|
||||
cpu_id: u8,
|
||||
cpu_mask: u64,
|
||||
}
|
||||
|
||||
fn root_prefix() -> PathBuf {
|
||||
std::env::var_os("REDBEAR_HWUTILS_ROOT")
|
||||
.map(PathBuf::from)
|
||||
@@ -28,6 +71,16 @@ fn resolve(root: &Path, absolute: &str) -> PathBuf {
|
||||
root.join(absolute.trim_start_matches('/'))
|
||||
}
|
||||
|
||||
fn require_path(root: &Path, absolute: &str) -> Result<(), String> {
|
||||
let path = resolve(root, absolute);
|
||||
if path.exists() {
|
||||
println!("present={absolute}");
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("missing {absolute}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn read_dir_names(path: &Path) -> Vec<String> {
|
||||
let mut names = match fs::read_dir(path) {
|
||||
Ok(entries) => entries
|
||||
@@ -44,16 +97,7 @@ fn collect_irq_reports(root: &Path) -> Vec<IrqReport> {
|
||||
let mut reports = Vec::new();
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
|
||||
for dir in [
|
||||
"/tmp/redbear-irq-report",
|
||||
"/tmp/run/redbear-irq-report",
|
||||
"/run/redbear-irq-report",
|
||||
"/var/run/redbear-irq-report",
|
||||
"/scheme/initfs/tmp/redbear-irq-report",
|
||||
"/scheme/initfs/tmp/run/redbear-irq-report",
|
||||
"/scheme/initfs/run/redbear-irq-report",
|
||||
"/scheme/initfs/var/run/redbear-irq-report",
|
||||
] {
|
||||
for dir in IRQ_REPORT_DIRS {
|
||||
for name in read_dir_names(&resolve(root, dir))
|
||||
.into_iter()
|
||||
.filter(|name| name.ends_with(".env"))
|
||||
@@ -111,9 +155,175 @@ fn collect_irq_reports(root: &Path) -> Vec<IrqReport> {
|
||||
reports
|
||||
}
|
||||
|
||||
fn parse_runtime_pci_location(device: &str) -> Option<PciLocation> {
|
||||
let (segment, rest) = device.split_once(':')?;
|
||||
let (bus, rest) = rest.split_once(':')?;
|
||||
let (slot, function) = rest.split_once('.')?;
|
||||
|
||||
Some(PciLocation {
|
||||
segment: u16::from_str_radix(segment, 16).ok()?,
|
||||
bus: u8::from_str_radix(bus, 16).ok()?,
|
||||
device: u8::from_str_radix(slot, 16).ok()?,
|
||||
function: function.parse().ok()?,
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_device_probes(
|
||||
root: &Path,
|
||||
reports: &[IrqReport],
|
||||
) -> Result<Vec<PciDeviceProbe>, String> {
|
||||
let mut probes = Vec::new();
|
||||
let mut seen = BTreeSet::new();
|
||||
|
||||
for report in reports {
|
||||
if !seen.insert(report.device.clone()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let location = parse_runtime_pci_location(&report.device)
|
||||
.ok_or_else(|| format!("invalid PCI location {} in IRQ report", report.device))?;
|
||||
let config_path = resolve(root, &format!("{}/config", location.scheme_path()));
|
||||
let config = fs::read(&config_path)
|
||||
.map_err(|err| format!("failed to read {}: {err}", config_path.display()))?;
|
||||
|
||||
if config.len() < 64 {
|
||||
return Err(format!(
|
||||
"PCI config space for {} was too short: {} bytes",
|
||||
report.device,
|
||||
config.len()
|
||||
));
|
||||
}
|
||||
|
||||
let info = parse_device_info_from_config_space(location, &config).ok_or_else(|| {
|
||||
format!(
|
||||
"failed to parse PCI config space for {} from {}",
|
||||
report.device,
|
||||
config_path.display()
|
||||
)
|
||||
})?;
|
||||
let msix = info.find_msix();
|
||||
|
||||
probes.push(PciDeviceProbe {
|
||||
device: report.device.clone(),
|
||||
irq_line: info.irq,
|
||||
interrupt_support: info.interrupt_support().as_str().to_string(),
|
||||
supports_msi: info.supports_msi(),
|
||||
supports_msix: info.supports_msix(),
|
||||
msix_table_size: msix.as_ref().map(|cap| cap.table_size),
|
||||
msix_function_masked: msix.as_ref().map(|cap| cap.masked),
|
||||
});
|
||||
}
|
||||
|
||||
probes.sort_by(|left, right| left.device.cmp(&right.device));
|
||||
Ok(probes)
|
||||
}
|
||||
|
||||
fn parse_spurious_irq_stats(text: &str) -> Result<SpuriousIrqStats, String> {
|
||||
let mut stats = SpuriousIrqStats::default();
|
||||
let mut saw_irq7 = false;
|
||||
let mut saw_irq15 = false;
|
||||
let mut saw_total = false;
|
||||
|
||||
for line in text.lines() {
|
||||
let mut parts = line.split_whitespace();
|
||||
let Some(value) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
let Some(label) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
let count = value
|
||||
.parse::<u64>()
|
||||
.map_err(|err| format!("invalid spurious IRQ count '{value}': {err}"))?;
|
||||
|
||||
match label {
|
||||
"IRQ7" => {
|
||||
stats.irq7 = count;
|
||||
saw_irq7 = true;
|
||||
}
|
||||
"IRQ15" => {
|
||||
stats.irq15 = count;
|
||||
saw_irq15 = true;
|
||||
}
|
||||
"total" => {
|
||||
stats.total = count;
|
||||
saw_total = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if !saw_irq7 || !saw_irq15 || !saw_total {
|
||||
return Err("spurious IRQ report was missing IRQ7, IRQ15, or total counters".to_string());
|
||||
}
|
||||
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
fn probe_spurious_irqs(root: &Path) -> Result<SpuriousIrqStats, String> {
|
||||
let path = resolve(root, "/scheme/sys/spurious_irq");
|
||||
let content = fs::read_to_string(&path)
|
||||
.map_err(|err| format!("failed to read {}: {err}", path.display()))?;
|
||||
parse_spurious_irq_stats(&content)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn read_bsp_cpu_id() -> Result<u8, String> {
|
||||
let mut file = File::open("/scheme/irq/bsp")
|
||||
.map_err(|err| format!("failed to open /scheme/irq/bsp: {err}"))?;
|
||||
let mut buf = [0u8; 8];
|
||||
let bytes_read = file
|
||||
.read(&mut buf)
|
||||
.map_err(|err| format!("failed to read /scheme/irq/bsp: {err}"))?;
|
||||
|
||||
let raw = match bytes_read {
|
||||
8 => u64::from_le_bytes(buf),
|
||||
4 => u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as u64,
|
||||
_ => {
|
||||
return Err(format!(
|
||||
"unexpected /scheme/irq/bsp payload size {bytes_read}"
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
u8::try_from(raw).map_err(|_| format!("BSP CPU id {raw} does not fit in u8"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
fn probe_interrupt_affinity(probes: &[PciDeviceProbe]) -> Result<AffinityProbe, String> {
|
||||
let irq = probes
|
||||
.iter()
|
||||
.filter_map(|probe| probe.irq_line)
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
"no active PCI device exposed a legacy IRQ line for affinity validation".to_string()
|
||||
})?;
|
||||
|
||||
let cpu_id = read_bsp_cpu_id()?;
|
||||
let cpu_mask = 1u64
|
||||
.checked_shl(u32::from(cpu_id))
|
||||
.ok_or_else(|| format!("BSP CPU id {cpu_id} exceeds 64-bit affinity mask width"))?;
|
||||
|
||||
let handle = IrqHandle::request(irq)
|
||||
.map_err(|err| format!("failed to request IRQ {irq} for affinity validation: {err}"))?;
|
||||
handle
|
||||
.set_affinity(cpu_mask)
|
||||
.map_err(|err| format!("failed to set IRQ {irq} affinity to mask {cpu_mask:#x}: {err}"))?;
|
||||
|
||||
Ok(AffinityProbe {
|
||||
irq,
|
||||
cpu_id,
|
||||
cpu_mask,
|
||||
})
|
||||
}
|
||||
|
||||
fn run() -> Result<(), String> {
|
||||
parse_args(PROGRAM, USAGE, std::env::args())?;
|
||||
let root = root_prefix();
|
||||
|
||||
println!("=== Red Bear OS PCI/IRQ Runtime Check ===");
|
||||
require_path(&root, "/scheme/irq")?;
|
||||
|
||||
let reports = collect_irq_reports(&root);
|
||||
|
||||
println!("PCI_IRQ_REPORTS={}", reports.len());
|
||||
@@ -128,6 +338,70 @@ fn run() -> Result<(), String> {
|
||||
return Err("no live PCI/IRQ runtime reports found".to_string());
|
||||
}
|
||||
|
||||
let probes = collect_device_probes(&root, &reports)?;
|
||||
println!("PCI_IRQ_ACTIVE_DEVICES={}", probes.len());
|
||||
|
||||
let msi_capable = probes.iter().filter(|probe| probe.supports_msi).count();
|
||||
let msix_capable = probes.iter().filter(|probe| probe.supports_msix).count();
|
||||
println!("PCI_IRQ_MSI_CAPABLE={msi_capable}");
|
||||
println!("PCI_IRQ_MSIX_CAPABLE={msix_capable}");
|
||||
|
||||
for probe in &probes {
|
||||
let irq_line = probe
|
||||
.irq_line
|
||||
.map(|irq| irq.to_string())
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
let msix_table_size = probe
|
||||
.msix_table_size
|
||||
.map(|size| size.to_string())
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
let msix_function_masked = probe
|
||||
.msix_function_masked
|
||||
.map(|masked| if masked { "1" } else { "0" }.to_string())
|
||||
.unwrap_or_else(|| "none".to_string());
|
||||
|
||||
println!(
|
||||
"PCI_IRQ_CAPABILITY={} irq_line={} interrupt_support={} msi_capable={} msix_capable={} msix_table_size={} msix_function_masked={}",
|
||||
probe.device,
|
||||
irq_line,
|
||||
probe.interrupt_support,
|
||||
if probe.supports_msi { 1 } else { 0 },
|
||||
if probe.supports_msix { 1 } else { 0 },
|
||||
msix_table_size,
|
||||
msix_function_masked
|
||||
);
|
||||
}
|
||||
|
||||
if msi_capable == 0 && msix_capable == 0 {
|
||||
return Err("no live PCI device exposed MSI/MSI-X capability".to_string());
|
||||
}
|
||||
|
||||
let spurious = probe_spurious_irqs(&root)?;
|
||||
println!("PCI_IRQ_SPURIOUS_IRQ7={}", spurious.irq7);
|
||||
println!("PCI_IRQ_SPURIOUS_IRQ15={}", spurious.irq15);
|
||||
println!("PCI_IRQ_SPURIOUS_TOTAL={}", spurious.total);
|
||||
|
||||
if spurious.total > 0 {
|
||||
return Err(format!(
|
||||
"spurious IRQs observed (irq7={} irq15={} total={})",
|
||||
spurious.irq7, spurious.irq15, spurious.total
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "redox")]
|
||||
{
|
||||
let affinity = probe_interrupt_affinity(&probes)?;
|
||||
println!(
|
||||
"PCI_IRQ_AFFINITY=ok irq={} cpu={} mask={:#x}",
|
||||
affinity.irq, affinity.cpu_id, affinity.cpu_mask
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "redox"))]
|
||||
{
|
||||
println!("PCI_IRQ_AFFINITY=host_build_stub");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -197,4 +471,21 @@ mod tests {
|
||||
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_runtime_pci_location_accepts_standard_bdf_string() {
|
||||
let location = parse_runtime_pci_location("0000:00:14.0").unwrap();
|
||||
assert_eq!(location.segment, 0);
|
||||
assert_eq!(location.bus, 0);
|
||||
assert_eq!(location.device, 0x14);
|
||||
assert_eq!(location.function, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_spurious_irq_stats_reads_all_counters() {
|
||||
let stats = parse_spurious_irq_stats("0\tIRQ7\n1\tIRQ15\n1\ttotal\n").unwrap();
|
||||
assert_eq!(stats.irq7, 0);
|
||||
assert_eq!(stats.irq15, 1);
|
||||
assert_eq!(stats.total, 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user