diff --git a/docs/07-RED-BEAR-OS-IMPLEMENTATION-PLAN.md b/docs/07-RED-BEAR-OS-IMPLEMENTATION-PLAN.md index 1992f740..13891768 100644 --- a/docs/07-RED-BEAR-OS-IMPLEMENTATION-PLAN.md +++ b/docs/07-RED-BEAR-OS-IMPLEMENTATION-PLAN.md @@ -251,11 +251,15 @@ Goal: - improve runtime trust in IRQ delivery, MSI/MSI-X, and IOMMU-adjacent infrastructure, - turn compile-oriented infrastructure into runtime-proven substrate. -Current state: +Current state (2026-04-29): -- source and build evidence are good, -- runtime validation is thinner than desired, -- this remains a blocker for USB, Wi-Fi, Bluetooth, and reliable device/runtime claims. +- 5 IRQ/low-level check binaries exist: PCI IRQ, IOMMU, DMA, PS/2, timer validation +- 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) +- 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: diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-iommu-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-iommu-check.rs index cfa95724..3ff87fef 100644 --- a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-iommu-check.rs +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-iommu-check.rs @@ -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 { + 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 { + 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, key: &str) -> Result { + let value = values + .get(key) + .ok_or_else(|| format!("iommu self-test did not report {key}"))?; + value + .parse::() + .map_err(|err| format!("invalid {key} value '{value}': {err}")) +} + +#[cfg(any(target_os = "redox", test))] +fn parse_bool_field(values: &BTreeMap, key: &str) -> Result { + 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 { + 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 { + 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 { + 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 + ); + } +} diff --git a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-pci-irq-check.rs b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-pci-irq-check.rs index 6574969f..976f4aa4 100644 --- a/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-pci-irq-check.rs +++ b/local/recipes/system/redbear-hwutils/source/src/bin/redbear-phase-pci-irq-check.rs @@ -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, + interrupt_support: String, + supports_msi: bool, + supports_msix: bool, + msix_table_size: Option, + msix_function_masked: Option, +} + +#[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 { let mut names = match fs::read_dir(path) { Ok(entries) => entries @@ -44,16 +97,7 @@ fn collect_irq_reports(root: &Path) -> Vec { 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 { reports } +fn parse_runtime_pci_location(device: &str) -> Option { + 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, 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 { + 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::() + .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 { + 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 { + 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 { + 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); + } } diff --git a/local/scripts/test-irq-runtime.sh b/local/scripts/test-irq-runtime.sh new file mode 100755 index 00000000..f5b2e1b1 --- /dev/null +++ b/local/scripts/test-irq-runtime.sh @@ -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 </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