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);
|
||||
}
|
||||
}
|
||||
|
||||
Executable
+221
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env bash
|
||||
# IRQ and low-level controller runtime validation — automated QEMU test harness.
|
||||
#
|
||||
# Boots a Red Bear OS image in QEMU, logs in, and runs all IRQ runtime check
|
||||
# binaries to validate that each low-level controller surface is present and
|
||||
# actually functional at runtime, not just installed.
|
||||
#
|
||||
# Modes:
|
||||
# --guest Run inside a Red Bear OS guest
|
||||
# --qemu [CONFIG] Boot CONFIG in QEMU and run the same checks automatically
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — all checks passed
|
||||
# 1 — one or more checks failed
|
||||
# 2 — QEMU boot or login failure
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
find_uefi_firmware() {
|
||||
local candidates=(
|
||||
"/usr/share/ovmf/x64/OVMF.4m.fd"
|
||||
"/usr/share/OVMF/x64/OVMF.4m.fd"
|
||||
"/usr/share/ovmf/x64/OVMF_CODE.4m.fd"
|
||||
"/usr/share/OVMF/x64/OVMF_CODE.4m.fd"
|
||||
"/usr/share/qemu/edk2-x86_64-code.fd"
|
||||
)
|
||||
local path
|
||||
for path in "${candidates[@]}"; do
|
||||
if [[ -f "$path" ]]; then
|
||||
printf '%s\n' "$path"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
run_guest_checks() {
|
||||
echo "=== Red Bear OS IRQ Runtime Validation ==="
|
||||
echo
|
||||
|
||||
local failures=0
|
||||
|
||||
run_check() {
|
||||
local name="$1"
|
||||
local cmd="$2"
|
||||
local description="$3"
|
||||
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo " FAIL $name: $cmd not found — $description"
|
||||
failures=$((failures + 1))
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo " Running $name..."
|
||||
if "$cmd" >/dev/null 2>&1; then
|
||||
echo " PASS $name: $description"
|
||||
else
|
||||
echo " FAIL $name: $description (exit code non-zero)"
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "--- PCI IRQ ---"
|
||||
run_check "pci-irq" "redbear-phase-pci-irq-check" "/scheme/irq, MSI/MSI-X capability, affinity, and spurious IRQ routing quality"
|
||||
echo
|
||||
|
||||
echo "--- IOMMU ---"
|
||||
run_check "iommu" "redbear-phase-iommu-check" "/scheme/iommu, AMD-Vi/Intel VT-d detection, event log, and interrupt remap setup"
|
||||
echo
|
||||
|
||||
echo "--- DMA ---"
|
||||
run_check "dma" "redbear-phase-dma-check" "DMA buffer allocation and write/readback"
|
||||
echo
|
||||
|
||||
echo "--- PS/2 + serio ---"
|
||||
run_check "ps2" "redbear-phase-ps2-check" "/scheme/input/ps2 or serio runtime path"
|
||||
echo
|
||||
|
||||
echo "--- monotonic timer ---"
|
||||
run_check "timer" "redbear-phase-timer-check" "/scheme/time/CLOCK_MONOTONIC monotonic progress"
|
||||
echo
|
||||
|
||||
echo "=== IRQ Runtime Validation Summary ==="
|
||||
echo " Failure count: $failures"
|
||||
if [ "$failures" -gt 0 ]; then
|
||||
echo " Result: FAIL"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " Result: PASS"
|
||||
return 0
|
||||
}
|
||||
|
||||
run_qemu_checks() {
|
||||
local config="${1:-redbear-full}"
|
||||
local firmware
|
||||
firmware="$(find_uefi_firmware)" || {
|
||||
echo "ERROR: no usable x86_64 UEFI firmware found" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
local arch image extra
|
||||
arch="${ARCH:-$(uname -m)}"
|
||||
image="build/$arch/$config/harddrive.img"
|
||||
extra="build/$arch/$config/extra.img"
|
||||
|
||||
if [[ ! -f "$image" ]]; then
|
||||
echo "ERROR: missing image $image" >&2
|
||||
echo "Build it first with: ./local/scripts/build-redbear.sh $config" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ ! -f "$extra" ]]; then
|
||||
truncate -s 1g "$extra"
|
||||
fi
|
||||
|
||||
expect <<EXPECT_SCRIPT
|
||||
log_user 1
|
||||
set timeout 300
|
||||
|
||||
proc run_check {name cmd description ok_marker fail_marker missing_marker} {
|
||||
global failures
|
||||
|
||||
puts "--- \$name ---"
|
||||
send "if command -v \$cmd >/dev/null 2>&1; then if \$cmd >/dev/null 2>&1; then echo \$ok_marker; else echo \$fail_marker; fi; else echo \$missing_marker; fi\r"
|
||||
expect {
|
||||
\$ok_marker {
|
||||
puts " PASS \$name: \$description"
|
||||
}
|
||||
\$fail_marker {
|
||||
puts " FAIL \$name: \$description (exit code non-zero)"
|
||||
incr failures
|
||||
}
|
||||
\$missing_marker {
|
||||
puts " FAIL \$name: \$cmd not found — \$description"
|
||||
incr failures
|
||||
}
|
||||
timeout {
|
||||
puts " FAIL \$name: timed out"
|
||||
incr failures
|
||||
}
|
||||
eof {
|
||||
puts " FAIL \$name: guest exited before check completion"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
puts ""
|
||||
}
|
||||
|
||||
set failures 0
|
||||
spawn qemu-system-x86_64 -name {Red Bear OS x86_64} -device qemu-xhci -smp 4 -m 2048 -bios $firmware -chardev stdio,id=debug,signal=off,mux=on -serial chardev:debug -mon chardev=debug -machine q35 -device ich9-intel-hda -device hda-output -device virtio-net,netdev=net0 -netdev user,id=net0 -nographic -vga none -drive file=$image,format=raw,if=none,id=drv0 -device nvme,drive=drv0,serial=NVME_SERIAL -drive file=$extra,format=raw,if=none,id=drv1 -device nvme,drive=drv1,serial=NVME_EXTRA -enable-kvm -cpu host
|
||||
expect "login:"
|
||||
send "root\r"
|
||||
expect "assword:"
|
||||
send "password\r"
|
||||
expect "Type 'help' for available commands."
|
||||
send "echo __READY__\r"
|
||||
expect "__READY__"
|
||||
|
||||
puts "=== Red Bear OS IRQ Runtime Validation ==="
|
||||
puts ""
|
||||
|
||||
run_check "PCI IRQ" "redbear-phase-pci-irq-check" "/scheme/irq, MSI/MSI-X capability, affinity, and spurious IRQ routing quality" "__PCI_IRQ_OK__" "__PCI_IRQ_FAIL__" "__PCI_IRQ_MISSING__"
|
||||
run_check "IOMMU" "redbear-phase-iommu-check" "/scheme/iommu, AMD-Vi/Intel VT-d detection, event log, and interrupt remap setup" "__IOMMU_OK__" "__IOMMU_FAIL__" "__IOMMU_MISSING__"
|
||||
run_check "DMA" "redbear-phase-dma-check" "DMA buffer allocation and write/readback" "__DMA_OK__" "__DMA_FAIL__" "__DMA_MISSING__"
|
||||
run_check "PS/2 + serio" "redbear-phase-ps2-check" "/scheme/input/ps2 or serio runtime path" "__PS2_OK__" "__PS2_FAIL__" "__PS2_MISSING__"
|
||||
run_check "monotonic timer" "redbear-phase-timer-check" "/scheme/time/CLOCK_MONOTONIC monotonic progress" "__TIMER_OK__" "__TIMER_FAIL__" "__TIMER_MISSING__"
|
||||
|
||||
puts "=== IRQ Runtime Validation Summary ==="
|
||||
puts " Failure count: \$failures"
|
||||
if {\$failures == 0} {
|
||||
puts " Result: PASS"
|
||||
} else {
|
||||
puts " Result: FAIL"
|
||||
}
|
||||
|
||||
send "echo __IRQ_RUNTIME_DONE__\$failures__\r"
|
||||
expect "__IRQ_RUNTIME_DONE__\$failures__"
|
||||
if {\$failures != 0} {
|
||||
exit 1
|
||||
}
|
||||
|
||||
send "shutdown\r"
|
||||
expect eof
|
||||
EXPECT_SCRIPT
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
Usage:
|
||||
./local/scripts/test-irq-runtime.sh --guest
|
||||
./local/scripts/test-irq-runtime.sh --qemu [redbear-full]
|
||||
|
||||
This script validates the IRQ and low-level controller runtime substrate by
|
||||
running the guest-side check binaries and using their exit codes as the
|
||||
authoritative pass/fail signal.
|
||||
|
||||
Guest mode runs inside a Red Bear OS instance.
|
||||
QEMU mode boots an image and runs checks automatically.
|
||||
|
||||
Required binaries (must be in PATH inside the guest):
|
||||
redbear-phase-pci-irq-check — PCI IRQ runtime reports, MSI/MSI-X capability, affinity, spurious IRQs
|
||||
redbear-phase-iommu-check — IOMMU runtime self-test + scheme control probes
|
||||
redbear-phase-dma-check — DMA buffer allocation/runtime proof
|
||||
redbear-phase-ps2-check — PS/2 + serio runtime proof
|
||||
redbear-phase-timer-check — monotonic timer runtime proof
|
||||
USAGE
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--guest)
|
||||
run_guest_checks
|
||||
;;
|
||||
--qemu)
|
||||
run_qemu_checks "${2:-redbear-full}"
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in New Issue
Block a user