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:
@@ -251,11 +251,15 @@ Goal:
|
|||||||
- improve runtime trust in IRQ delivery, MSI/MSI-X, and IOMMU-adjacent infrastructure,
|
- improve runtime trust in IRQ delivery, MSI/MSI-X, and IOMMU-adjacent infrastructure,
|
||||||
- turn compile-oriented infrastructure into runtime-proven substrate.
|
- turn compile-oriented infrastructure into runtime-proven substrate.
|
||||||
|
|
||||||
Current state:
|
Current state (2026-04-29):
|
||||||
|
|
||||||
- source and build evidence are good,
|
- 5 IRQ/low-level check binaries exist: PCI IRQ, IOMMU, DMA, PS/2, timer validation
|
||||||
- runtime validation is thinner than desired,
|
- 6 test scripts: test-msix-qemu.sh, test-iommu-qemu.sh, test-xhci-irq-qemu.sh, test-ps2-qemu.sh, test-timer-qemu.sh, test-lowlevel-controllers-qemu.sh (aggregate)
|
||||||
- this remains a blocker for USB, Wi-Fi, Bluetooth, and reliable device/runtime claims.
|
- redox-driver-sys: typed PCI/IRQ userspace substrate with host-runnable unit tests, quirk-aware interrupt-support reporting, MSI-X table helpers, affinity helpers
|
||||||
|
- redox-drm: shared interrupt abstraction with MSI-X-first and legacy-IRQ fallback
|
||||||
|
- iommu daemon: specification-rich IOMMU/interrupt-remapping direction
|
||||||
|
- Kernel: PIC, IOAPIC, LAPIC/x2APIC, IDT reservation, masking, EOI, spurious IRQ accounting
|
||||||
|
- Weakness: runtime validation thinner than desired, controller-specific characterization uneven, this remains a blocker for USB/Wi-Fi/Bluetooth reliability claims
|
||||||
|
|
||||||
Canonical plan:
|
Canonical plan:
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,285 @@
|
|||||||
use std::path::Path;
|
#[cfg(any(target_os = "redox", test))]
|
||||||
use std::process::{self, Command};
|
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;
|
use redbear_hwutils::parse_args;
|
||||||
|
|
||||||
const PROGRAM: &str = "redbear-phase-iommu-check";
|
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.";
|
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> {
|
fn require_path(path: &str) -> Result<(), String> {
|
||||||
if Path::new(path).exists() {
|
if Path::new(path).exists() {
|
||||||
println!("{path}");
|
println!("present={path}");
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
Err(format!("missing {path}"))
|
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> {
|
fn run() -> Result<(), String> {
|
||||||
parse_args(PROGRAM, USAGE, std::env::args()).map_err(|err| {
|
parse_args(PROGRAM, USAGE, std::env::args()).map_err(|err| {
|
||||||
if err.is_empty() {
|
if err.is_empty() {
|
||||||
@@ -25,6 +290,8 @@ fn run() -> Result<(), String> {
|
|||||||
|
|
||||||
println!("=== Red Bear OS IOMMU Runtime Check ===");
|
println!("=== Red Bear OS IOMMU Runtime Check ===");
|
||||||
require_path("/usr/bin/iommu")?;
|
require_path("/usr/bin/iommu")?;
|
||||||
|
require_path("/scheme/iommu")?;
|
||||||
|
require_path("/scheme/iommu/control")?;
|
||||||
|
|
||||||
let output = Command::new("/usr/bin/iommu")
|
let output = Command::new("/usr/bin/iommu")
|
||||||
.env("IOMMU_LOG", "info")
|
.env("IOMMU_LOG", "info")
|
||||||
@@ -43,31 +310,144 @@ fn run() -> Result<(), String> {
|
|||||||
output.status.code()
|
output.status.code()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if !stdout.contains("units_detected=") {
|
|
||||||
return Err("iommu self-test did not report detected unit count".to_string());
|
let summary = parse_self_test_summary(&stdout)?;
|
||||||
}
|
println!(
|
||||||
if !stdout.contains("discovery_source=") {
|
"amd_vi_present={}",
|
||||||
return Err("iommu self-test did not report discovery source".to_string());
|
if summary.units_detected > 0 { 1 } else { 0 }
|
||||||
}
|
);
|
||||||
if !stdout.contains("dmar_present=") {
|
println!(
|
||||||
return Err("iommu self-test did not report DMAR presence state".to_string());
|
"intel_vtd_present={}",
|
||||||
}
|
if summary.dmar_present { 1 } else { 0 }
|
||||||
if !stdout.contains("units_initialized_now=") {
|
);
|
||||||
return Err("iommu self-test did not report initialized unit count".to_string());
|
println!(
|
||||||
}
|
"iommu_vendor_detection={}",
|
||||||
if !stdout.contains("units_initialized_after=") {
|
iommu_vendor_detection(&summary)
|
||||||
return Err("iommu self-test did not report initialized-after count".to_string());
|
);
|
||||||
}
|
|
||||||
if !stdout.contains("events_drained=") {
|
if summary.units_detected == 0 && !summary.dmar_present {
|
||||||
return Err("iommu self-test did not report drained events".to_string());
|
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(())
|
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() {
|
fn main() {
|
||||||
if let Err(err) = run() {
|
if let Err(err) = run() {
|
||||||
eprintln!("{PROGRAM}: {err}");
|
eprintln!("{PROGRAM}: {err}");
|
||||||
process::exit(1);
|
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::{
|
use std::{
|
||||||
|
collections::BTreeSet,
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process,
|
process,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(target_os = "redox")]
|
||||||
|
use std::{fs::File, io::Read};
|
||||||
|
|
||||||
use redbear_hwutils::parse_args;
|
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 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 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)]
|
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||||
struct IrqReport {
|
struct IrqReport {
|
||||||
@@ -18,6 +35,32 @@ struct IrqReport {
|
|||||||
reason: String,
|
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 {
|
fn root_prefix() -> PathBuf {
|
||||||
std::env::var_os("REDBEAR_HWUTILS_ROOT")
|
std::env::var_os("REDBEAR_HWUTILS_ROOT")
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
@@ -28,6 +71,16 @@ fn resolve(root: &Path, absolute: &str) -> PathBuf {
|
|||||||
root.join(absolute.trim_start_matches('/'))
|
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> {
|
fn read_dir_names(path: &Path) -> Vec<String> {
|
||||||
let mut names = match fs::read_dir(path) {
|
let mut names = match fs::read_dir(path) {
|
||||||
Ok(entries) => entries
|
Ok(entries) => entries
|
||||||
@@ -44,16 +97,7 @@ fn collect_irq_reports(root: &Path) -> Vec<IrqReport> {
|
|||||||
let mut reports = Vec::new();
|
let mut reports = Vec::new();
|
||||||
let mut seen = std::collections::BTreeSet::new();
|
let mut seen = std::collections::BTreeSet::new();
|
||||||
|
|
||||||
for dir in [
|
for dir in IRQ_REPORT_DIRS {
|
||||||
"/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 name in read_dir_names(&resolve(root, dir))
|
for name in read_dir_names(&resolve(root, dir))
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|name| name.ends_with(".env"))
|
.filter(|name| name.ends_with(".env"))
|
||||||
@@ -111,9 +155,175 @@ fn collect_irq_reports(root: &Path) -> Vec<IrqReport> {
|
|||||||
reports
|
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> {
|
fn run() -> Result<(), String> {
|
||||||
parse_args(PROGRAM, USAGE, std::env::args())?;
|
parse_args(PROGRAM, USAGE, std::env::args())?;
|
||||||
let root = root_prefix();
|
let root = root_prefix();
|
||||||
|
|
||||||
|
println!("=== Red Bear OS PCI/IRQ Runtime Check ===");
|
||||||
|
require_path(&root, "/scheme/irq")?;
|
||||||
|
|
||||||
let reports = collect_irq_reports(&root);
|
let reports = collect_irq_reports(&root);
|
||||||
|
|
||||||
println!("PCI_IRQ_REPORTS={}", reports.len());
|
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());
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,4 +471,21 @@ mod tests {
|
|||||||
|
|
||||||
fs::remove_dir_all(root).unwrap();
|
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