diff --git a/local/recipes/system/iommu/source/Cargo.toml b/local/recipes/system/iommu/source/Cargo.toml index 9d046a1190..c53ad8ab29 100644 --- a/local/recipes/system/iommu/source/Cargo.toml +++ b/local/recipes/system/iommu/source/Cargo.toml @@ -3,10 +3,22 @@ name = "iommu" version = "0.3.2" edition = "2021" +[lib] +name = "iommu" +path = "src/lib_core.rs" + +[[bin]] +name = "iommu" +path = "src/main.rs" + +[features] +default = ["redox"] +redox = ["dep:redox-driver-sys", "dep:redox-scheme", "dep:syscall"] + [dependencies] -redox-driver-sys = { version = "0.3", path = "../../../drivers/redox-driver-sys/source" } -redox-scheme = { path = "../../../../../local/sources/redox-scheme" } -syscall = { path = "../../../../../local/sources/syscall", package = "redox_syscall" } +redox-driver-sys = { version = "0.3", path = "../../../drivers/redox-driver-sys/source", optional = true } +redox-scheme = { path = "../../../../../local/sources/redox-scheme", optional = true } +syscall = { path = "../../../../../local/sources/syscall", package = "redox_syscall", optional = true } log = { version = "0.4", features = ["std"] } [patch.crates-io] diff --git a/local/recipes/system/iommu/source/src/acpi.rs b/local/recipes/system/iommu/source/src/acpi.rs index c1004d26ea..9a76e719d6 100644 --- a/local/recipes/system/iommu/source/src/acpi.rs +++ b/local/recipes/system/iommu/source/src/acpi.rs @@ -7,8 +7,10 @@ const IVHD_HEADER_BYTES: usize = 0x18; const IVHD_TYPE_10: u8 = 0x10; const IVHD_TYPE_11: u8 = 0x11; +const IVHD_TYPE_40: u8 = 0x40; const IVMD_TYPE_20: u8 = 0x20; const IVMD_TYPE_21: u8 = 0x21; +const IVMD_TYPE_22: u8 = 0x22; const IVHD_ALL: u8 = 0x00; const IVHD_SEL: u8 = 0x01; @@ -149,6 +151,35 @@ pub struct IvrsInfo { pub revision: u8, pub iv_info: u32, pub units: Vec, + pub ivmd_entries: Vec, +} + +/// IVMD (I/O Virtualization Memory Definition) block. +/// +/// Scope is determined by the entry type (0x20=all, 0x21=single, 0x22=range). +/// Exclusion semantics come from the flags field (bit 4 = `IVMD_FLAG_EXCL_RANGE`, +/// with UNITY read/write bits applied independently). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct IvmdInfo { + pub entry_type: u8, + pub flags: u8, + pub start_bdf: Bdf, + pub end_bdf: Bdf, + pub pci_segment_group: u16, +} + +impl IvmdInfo { + pub const FLAG_UNITY_READ: u8 = 0x01; + pub const FLAG_UNITY_WRITE: u8 = 0x02; + pub const FLAG_EXCL_RANGE: u8 = 0x10; + + pub fn is_exclusion(&self) -> bool { + self.flags & Self::FLAG_EXCL_RANGE != 0 + } + + pub fn is_unity(&self) -> bool { + self.flags & (Self::FLAG_UNITY_READ | Self::FLAG_UNITY_WRITE) != 0 + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -228,6 +259,7 @@ pub fn parse_ivrs(bytes: &[u8]) -> Result { let iv_info = read_u32(table, ACPI_HEADER_BYTES).ok_or(IvrsError::TooShort)?; let mut units = Vec::new(); + let mut ivmd_entries = Vec::new(); let mut offset = IVRS_HEADER_BYTES; let mut skipped_qemu_padding = false; while offset < table.len() { @@ -260,13 +292,10 @@ pub fn parse_ivrs(bytes: &[u8]) -> Result { } let entry = &table[offset..offset + entry_length]; - if matches!(entry_type, IVHD_TYPE_10 | IVHD_TYPE_11) { + if matches!(entry_type, IVHD_TYPE_10 | IVHD_TYPE_11 | IVHD_TYPE_40) { units.push(parse_ivhd(entry, offset)?); - } - - if matches!(entry_type, IVMD_TYPE_20 | IVMD_TYPE_21) { - offset += entry_length; - continue; + } else if matches!(entry_type, IVMD_TYPE_20 | IVMD_TYPE_21 | IVMD_TYPE_22) { + ivmd_entries.push(parse_ivmd(entry, offset)?); } offset += entry_length; @@ -276,11 +305,17 @@ pub fn parse_ivrs(bytes: &[u8]) -> Result { revision, iv_info, units, + ivmd_entries, }) } fn parse_ivhd(entry: &[u8], table_offset: usize) -> Result { - if entry.len() < IVHD_HEADER_BYTES { + let header_size = match entry.first().copied() { + Some(IVHD_TYPE_10) => 24usize, + Some(IVHD_TYPE_11 | IVHD_TYPE_40) => 40, + _ => IVHD_HEADER_BYTES, + }; + if entry.len() < header_size { return Err(IvrsError::InvalidIvhdLength { offset: table_offset, length: entry.len(), @@ -288,7 +323,7 @@ fn parse_ivhd(entry: &[u8], table_offset: usize) -> Result Result Result { + if entry.len() < 24 { + return Err(IvrsError::TruncatedEntry { + offset: table_offset, + }); + } + Ok(IvmdInfo { + entry_type: entry[0], + flags: entry[1], + start_bdf: Bdf( + read_u16(entry, 4).ok_or(IvrsError::TruncatedEntry { + offset: table_offset + 4, + })?, + ), + end_bdf: Bdf( + read_u16(entry, 6).ok_or(IvrsError::TruncatedEntry { + offset: table_offset + 6, + })?, + ), + pci_segment_group: read_u16(entry, 8).ok_or(IvrsError::TruncatedEntry { + offset: table_offset + 8, + })?, + }) +} + fn ensure_remaining( entry: &[u8], offset: usize, @@ -490,7 +550,8 @@ mod tests { } fn build_ivhd(mmio_base: u64, iommu_bdf: Bdf, entries: &[u8]) -> Vec { - let length = (0x18 + entries.len()) as u16; + let header = 40usize; // IVHD type 0x11/0x40 uses 40-byte header + let length = (header + entries.len()) as u16; let mut bytes = vec![0u8; length as usize]; bytes[0] = 0x11; bytes[1] = 0xA0; @@ -501,7 +562,7 @@ mod tests { bytes[16..18].copy_from_slice(&0u16.to_le_bytes()); bytes[18..20].copy_from_slice(&0x01c2u16.to_le_bytes()); bytes[20..24].copy_from_slice(&0x00aa_5500u32.to_le_bytes()); - bytes[24..].copy_from_slice(entries); + bytes[header..].copy_from_slice(entries); bytes } @@ -571,4 +632,100 @@ mod tests { assert!(unit.handles_device(Bdf::new(0x80, 0x1f, 7))); } + + // ── IVHD type 0x40 (40-byte header, same layout as 0x11) ────────── + + fn build_ivhd_type40(mmio_base: u64, iommu_bdf: Bdf, entries: &[u8]) -> Vec { + let length = (40 + entries.len()) as u16; + let mut bytes = vec![0u8; length as usize]; + bytes[0] = 0x40; + bytes[1] = 0xA0; + bytes[2..4].copy_from_slice(&length.to_le_bytes()); + bytes[4..6].copy_from_slice(&iommu_bdf.raw().to_le_bytes()); + bytes[6..8].copy_from_slice(&0x0040u16.to_le_bytes()); + bytes[8..16].copy_from_slice(&mmio_base.to_le_bytes()); + bytes[16..18].copy_from_slice(&0u16.to_le_bytes()); + bytes[18..20].copy_from_slice(&0x01c2u16.to_le_bytes()); + bytes[20..24].copy_from_slice(&0x0033_cc00u32.to_le_bytes()); + bytes[40..].copy_from_slice(entries); + bytes + } + + #[test] + fn parses_ivhd_type_0x40() { + // Given: IVRS with type 0x40 IVHD block + let entries = [0x00, 0x00, 0x00, 0x00]; + let table = build_ivrs(&[build_ivhd_type40( + 0xfee0_0000, + Bdf::new(0, 0x18, 2), + &entries, + )]); + // When: parsed + let parsed = parse_ivrs(&table).unwrap_or_else(|err| panic!("IVRS parse failed: {err}")); + // Then: unit has correct MMIO base, EFR, and device entries + assert_eq!(parsed.units.len(), 1); + assert_eq!(parsed.units[0].entry_type, 0x40); + assert_eq!(parsed.units[0].mmio_base, 0xfee0_0000); + assert_eq!(parsed.units[0].iommu_efr, 0x0033_cc00); + assert_eq!(parsed.units[0].device_entries.len(), 1); + assert!(parsed.units[0].handles_device(Bdf::new(0x42, 0x0f, 0))); + } + + // ── IVMD parsing ──────────────────────────────────────────────────── + + fn build_ivmd(entry_type: u8, flags: u8, start_bdf: Bdf, end_bdf: Bdf, segment: u16) -> Vec { + let mut bytes = vec![0u8; 24]; + bytes[0] = entry_type; + bytes[1] = flags; + bytes[2..4].copy_from_slice(&(24u16).to_le_bytes()); + bytes[4..6].copy_from_slice(&start_bdf.raw().to_le_bytes()); + bytes[6..8].copy_from_slice(&end_bdf.raw().to_le_bytes()); + bytes[8..10].copy_from_slice(&segment.to_le_bytes()); + bytes + } + + #[test] + fn parses_ivmd_type_0x20_all_devices() { + let ivmd = build_ivmd(0x20, 0x10, Bdf::new(0, 1, 0), Bdf::new(0, 3, 7), 0); + let ivhd = build_ivhd(0xfee0_0000, Bdf::new(0, 0x18, 2), &[0x00, 0, 0, 0]); + let table = build_ivrs(&[ivhd, ivmd]); + let parsed = parse_ivrs(&table).unwrap_or_else(|err| panic!("IVRS parse failed: {err}")); + assert!(parsed.units.len() >= 1); + } + + #[test] + fn parses_ivmd_type_0x21_single_device() { + let ivmd = build_ivmd(0x21, 0, Bdf::new(0, 5, 0), Bdf::new(0, 5, 0), 0); + let ivhd = build_ivhd(0xfee0_0000, Bdf::new(0, 0x18, 2), &[0x00, 0, 0, 0]); + let table = build_ivrs(&[ivhd, ivmd]); + let parsed = parse_ivrs(&table).unwrap_or_else(|err| panic!("IVRS parse failed: {err}")); + assert!(parsed.units.len() >= 1); + } + + #[test] + fn parses_ivmd_type_0x22_device_range() { + let ivmd = build_ivmd(0x22, 0, Bdf::new(0, 4, 0), Bdf::new(0, 6, 7), 0); + let ivhd = build_ivhd(0xfee0_0000, Bdf::new(0, 0x18, 2), &[0x00, 0, 0, 0]); + let table = build_ivrs(&[ivhd, ivmd]); + let parsed = parse_ivrs(&table).unwrap_or_else(|err| panic!("IVRS parse failed: {err}")); + assert!(parsed.units.len() >= 1); + } + + #[test] + fn ivmd_exclusion_flag_is_parsed_separately_from_type() { + let ivmd = build_ivmd(0x20, 0x11, Bdf::new(0, 1, 0), Bdf::new(0, 1, 7), 0); + let ivhd = build_ivhd(0xfee0_0000, Bdf::new(0, 0x18, 2), &[0x00, 0, 0, 0]); + let table = build_ivrs(&[ivhd, ivmd]); + let parsed = parse_ivrs(&table).unwrap_or_else(|err| panic!("IVRS parse failed: {err}")); + assert!(parsed.units.len() >= 1); + } + + #[test] + fn ivmd_type_0x20_not_exclusion_by_default() { + let ivmd = build_ivmd(0x20, 0x00, Bdf::new(0, 1, 0), Bdf::new(0, 1, 7), 0); + let ivhd = build_ivhd(0xfee0_0000, Bdf::new(0, 0x18, 2), &[0x00, 0, 0, 0]); + let table = build_ivrs(&[ivhd, ivmd]); + let parsed = parse_ivrs(&table).unwrap_or_else(|err| panic!("IVRS parse failed: {err}")); + assert!(parsed.units.len() >= 1); + } } diff --git a/local/recipes/system/iommu/source/src/amd_vi.rs b/local/recipes/system/iommu/source/src/amd_vi.rs index 6680208b78..921627958c 100644 --- a/local/recipes/system/iommu/source/src/amd_vi.rs +++ b/local/recipes/system/iommu/source/src/amd_vi.rs @@ -4,7 +4,7 @@ use redox_driver_sys::memory::{CacheType, MmioProt, MmioRegion}; use crate::acpi::{parse_ivrs, Bdf, IommuUnitInfo, IvrsError}; use crate::command_buffer::{CommandBuffer, CommandEntry, EventLog, EventLogEntry}; use crate::device_table::{DeviceTable, DeviceTableEntry}; -use crate::interrupt::InterruptRemapTable; +use crate::interrupt::{irt_format_from_efr, InterruptRemapTable}; use crate::mmio::{control, ext_feature, offsets, status, AMD_VI_MMIO_BYTES}; use crate::page_table::DomainPageTables; @@ -98,8 +98,10 @@ impl AmdViUnit { let command_buffer = CommandBuffer::new(DEFAULT_CMD_ENTRIES).map_err(|err| err.to_string())?; let event_log = EventLog::new(DEFAULT_EVT_ENTRIES).map_err(|err| err.to_string())?; + let irt_format = irt_format_from_efr(self.info.iommu_efr, self.info.entry_type); let interrupt_table = - InterruptRemapTable::new_allocated(DEFAULT_IRT_ENTRIES).map_err(|err| err.to_string())?; + InterruptRemapTable::new_allocated(DEFAULT_IRT_ENTRIES, irt_format) + .map_err(|err| err.to_string())?; self.program_bars(&device_table, &command_buffer, &event_log)?; self.reset_ring_pointers()?; @@ -188,6 +190,22 @@ impl AmdViUnit { Ok(()) } + /// Program a test IRTE (source 0, vector 0x20) into the interrupt remap + /// table and return the allocated index. Used by the self-test to verify + /// IRT hardware functionality. + pub fn program_test_irte(&self) -> Result { + let table = self + .interrupt_table + .as_ref() + .ok_or_else(|| "interrupt remap table not initialized".to_string())?; + let irte = crate::interrupt::Irte::new(0, 0, 0x20); + let index = table + .find_free() + .ok_or_else(|| "no free IRTE entries".to_string())?; + table.program_entry(index, &irte); + Ok(index) + } + pub fn drain_events(&mut self) -> Result, String> { let mut drained = Vec::new(); if !self.initialized { @@ -391,9 +409,9 @@ mod tests { use super::AmdViUnit; fn build_ivrs_with_unit() -> Vec { - let mut table = vec![0u8; 40 + 28]; + let mut table = vec![0u8; 40 + 44]; table[0..4].copy_from_slice(b"IVRS"); - table[4..8].copy_from_slice(&(68u32).to_le_bytes()); + table[4..8].copy_from_slice(&(84u32).to_le_bytes()); table[8] = 3; table[10..16].copy_from_slice(b"RDBEAR"); table[16..24].copy_from_slice(b"AMDVI "); @@ -401,14 +419,14 @@ mod tests { let offset = 40; table[offset] = 0x11; table[offset + 1] = 0x20; - table[offset + 2..offset + 4].copy_from_slice(&(28u16).to_le_bytes()); + table[offset + 2..offset + 4].copy_from_slice(&(44u16).to_le_bytes()); table[offset + 4..offset + 6].copy_from_slice(&Bdf::new(0, 0x18, 2).raw().to_le_bytes()); table[offset + 6..offset + 8].copy_from_slice(&0x40u16.to_le_bytes()); table[offset + 8..offset + 16].copy_from_slice(&0xfee0_0000u64.to_le_bytes()); table[offset + 16..offset + 18].copy_from_slice(&0u16.to_le_bytes()); table[offset + 18..offset + 20].copy_from_slice(&0x0081u16.to_le_bytes()); table[offset + 20..offset + 24].copy_from_slice(&0u32.to_le_bytes()); - table[offset + 24..offset + 28].copy_from_slice(&[0x00, 0, 0, 0]); + table[offset + 40..offset + 44].copy_from_slice(&[0x00, 0, 0, 0]); let checksum = (!table.iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte))).wrapping_add(1); diff --git a/local/recipes/system/iommu/source/src/interrupt.rs b/local/recipes/system/iommu/source/src/interrupt.rs index 2058f6561a..5ceea72d74 100644 --- a/local/recipes/system/iommu/source/src/interrupt.rs +++ b/local/recipes/system/iommu/source/src/interrupt.rs @@ -1,11 +1,59 @@ -use log::info; +//! AMD-Vi Interrupt Remapping — IRTE, IRT format selection, remapped message encoding. +//! +//! The IRT format is selected per IOMMU unit from the IVHD EFR field: +//! - Legacy 32-bit (IVHD type 0x10, or EFR GA bit not set) — 4-byte entries. +//! - GA 128-bit (IVHD type 0x11/0x40 with EFR GA bit set) — 16-byte entries. +//! +//! DTE programming, table sizing, and MSI message address encoding all depend on the format. + +#[cfg(feature = "redox")] use redox_driver_sys::dma::DmaBuffer; -pub const IRTE_SIZE: usize = 16; -pub const IRTE_PRESENT: u64 = 1 << 0; -const DMA_ALIGNMENT: usize = 4096; +/// Interrupt Remapping Table entry format. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum IrtFormat { + /// 32-bit IRTE (legacy — 4 bytes). Used for IVHD type 0x10 or when EFR GA is absent. + Legacy32, + /// Guest-Addressing 128-bit IRTE (16 bytes). Used for IVHD type 0x11/0x40 with EFR GA bit. + GA128, +} -#[derive(Debug, Clone, Copy)] +impl IrtFormat { + /// Number of bytes per IRTE. + pub const fn entry_size(self) -> usize { + match self { + Self::Legacy32 => 4, + Self::GA128 => 16, + } + } +} + +/// EFR bit 7: Guest-Addressing Supported. +const EFR_GASUP: u32 = 1 << 7; + +/// Select the IRT format from an IVHD's EFR field and IVHD type. +/// +/// - IVHD type 0x10: always Legacy32. +/// - IVHD type 0x11/0x40: GA128 if EFR bit 7 (GASup) is set, else Legacy32. +pub const fn irt_format_from_efr(efr: u32, ivhd_type: u8) -> IrtFormat { + match ivhd_type { + 0x10 => IrtFormat::Legacy32, + _ => { + if efr & EFR_GASUP != 0 { + IrtFormat::GA128 + } else { + IrtFormat::Legacy32 + } + } + } +} + +// ── IRTE encoding / decoding ─────────────────────────────────────────── + +/// An Interrupt Remapping Table Entry (IRTE). +/// +/// Fields are format-independent; `encode` / `decode` dispatch on format. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Irte { pub present: bool, pub source_id: u16, @@ -16,128 +64,402 @@ pub struct Irte { } impl Irte { - pub fn new(source_id: u16, dest_id: u32, vector: u8) -> Self { - Self { present: true, source_id, dest_id, vector, delivery_mode: 0, trigger_mode: 0 } - } - - pub fn encode(&self) -> [u64; 2] { - let mut low: u64 = IRTE_PRESENT; - low |= u64::from(self.vector) & 0xFF; - low |= (u64::from(self.delivery_mode) & 0x7) << 8; - low |= u64::from(self.dest_id) << 40; - let high: u64 = u64::from(self.source_id); - [low, high] - } - - pub fn decode(low: u64, high: u64) -> Self { + pub const fn new(source_id: u16, dest_id: u32, vector: u8) -> Self { Self { - present: low & IRTE_PRESENT != 0, - vector: (low & 0xFF) as u8, - delivery_mode: ((low >> 8) & 0x7) as u8, - trigger_mode: ((low >> 15) & 0x1) as u8, - dest_id: ((low >> 40) & 0xFFFF_FFFF) as u32, - source_id: (high & 0xFFFF) as u16, + present: true, + source_id, + dest_id, + vector, + delivery_mode: 0, + trigger_mode: 0, + } + } + + /// Encode this IRTE into raw dwords in the given format. + pub fn encode(&self, format: IrtFormat) -> Vec { + match format { + IrtFormat::Legacy32 => { + // 32-bit IRTE (AMD IOMMU spec §3.4.1, Table 23) + let mut dword: u32 = 0; + if self.present { + dword |= 1 << 0; + } + dword |= (u32::from(self.vector) & 0xFF) << 16; + dword |= (u32::from(self.dest_id) & 0xFF) << 8; + dword |= (u32::from(self.delivery_mode) & 0x7) << 4; + vec![dword] + } + IrtFormat::GA128 => { + // 128-bit IRTE (AMD IOMMU spec §3.4.2, Table 25) + // DWord 0: RemapEnable, IntType, DM, Destination[12:5], Vector[7:0] + let mut dw0: u32 = 0; + if self.present { + dw0 |= 1 << 0; + } + dw0 |= (u32::from(self.vector) & 0xFF) << 16; + dw0 |= (u32::from(self.dest_id) & 0x1FE0) << (5 - 5); // dest bits 12:5 at bits 12:5 + dw0 |= (u32::from(self.delivery_mode) & 0x7) << 8; + // DWord 1: GuestMode(0), Destination[31:13] (bits 18:0) + let mut dw1: u32 = 0; + dw1 |= (self.dest_id >> 13) & 0x7_FFFF; + // DWord 2: GA Tag[15:0] from source_id (bits 31:16) + let dw2: u32 = u32::from(self.source_id) << 16; + // DWord 3: GA Address[31:0] (unused) + let dw3: u32 = 0; + vec![dw0, dw1, dw2, dw3] + } + } + } + + /// Decode raw dwords back into an IRTE. + pub fn decode(dwords: &[u32], format: IrtFormat) -> Self { + match format { + IrtFormat::Legacy32 => { + let dw = dwords.first().copied().unwrap_or(0); + Self { + present: dw & 1 != 0, + vector: ((dw >> 16) & 0xFF) as u8, + delivery_mode: ((dw >> 4) & 0x7) as u8, + trigger_mode: 0, + dest_id: ((dw >> 8) & 0xFF) as u32, + source_id: 0, + } + } + IrtFormat::GA128 => { + let dw0 = dwords.first().copied().unwrap_or(0); + let dw1 = dwords.get(1).copied().unwrap_or(0); + let dw2 = dwords.get(2).copied().unwrap_or(0); + Self { + present: dw0 & 1 != 0, + vector: ((dw0 >> 16) & 0xFF) as u8, + delivery_mode: ((dw0 >> 8) & 0x7) as u8, + trigger_mode: 0, + dest_id: (dw0 & 0x1FE0) | ((dw1 & 0x7_FFFF) << 13), + source_id: ((dw2 >> 16) & 0xFFFF) as u16, + } + } } } } +// ── Remapped MSI message address encoding ────────────────────────────── + +/// Compute the remapped MSI message address for a given IRTE index and format. +/// +/// AMD IOMMU spec §3.4.3: the address encodes the IRTE index and the interrupt +/// remapping table pointer so the IOMMU can look up the correct entry. +pub const fn remapped_msi_address(irte_index: u16, format: IrtFormat) -> u64 { + match format { + IrtFormat::Legacy32 => { + // Legacy format: address = 0xFEEX_XXXX where bits encode the index. + // AMD IOMMU spec: address[31:20] = 0xFEE, address[19:5] = IRTE index. + 0xFEE0_0000u64 | ((irte_index as u64 & 0x7FFF) << 5) + } + IrtFormat::GA128 => { + // GA 128-bit format: similar encoding, uses 0xFEE0_0000 base. + // The IOMMU distinguishes GA vs legacy via DTE.IV field. + 0xFEE0_0000u64 | ((irte_index as u64 & 0x7FFF) << 5) + } + } +} + +/// When the IOMMU is NOT used, direct-APIC message address for a given destination APIC ID. +pub const fn direct_apic_address(destination_id: u8) -> u64 { + 0xFEE0_0000u64 | ((destination_id as u64) << 12) +} + +/// Direct-APIC message data encoding (no remapping). +pub const fn direct_apic_data(vector: u8, delivery_mode: u8) -> u32 { + (vector as u32) | ((delivery_mode as u32 & 0x7) << 8) +} + +// ── Interrupt Remap Table (DMA-backed, redox-only) ───────────────────── + +/// In-memory descriptor for a DMA-allocated interrupt remap table. +#[cfg(feature = "redox")] pub struct InterruptRemapTable { pub entries: usize, - /// RAII holder for the DMA buffer allocated by `new_allocated`. - /// The field is never read; the buffer is kept alive so the - /// allocation lives as long as the table. Dropping the table - /// releases the underlying DMA pages. + pub format: IrtFormat, + /// RAII holder for the DMA buffer. #[allow(dead_code)] buffer: Option, base: usize, } +#[cfg(feature = "redox")] impl InterruptRemapTable { - pub fn new(base_addr: usize, size: usize) -> Self { - Self { base: base_addr, entries: size / IRTE_SIZE, buffer: None } + pub fn new(base_addr: usize, size: usize, format: IrtFormat) -> Self { + let entry_size = format.entry_size(); + Self { + base: base_addr, + entries: if entry_size > 0 { size / entry_size } else { 0 }, + buffer: None, + format, + } } - pub fn new_allocated(entry_count: usize) -> Result { + pub fn new_allocated( + entry_count: usize, + format: IrtFormat, + ) -> Result { + let entry_size = format.entry_size(); let byte_len = entry_count - .checked_mul(IRTE_SIZE) + .checked_mul(entry_size) .ok_or("IRTE table size overflow")?; - let buffer = DmaBuffer::allocate(byte_len, DMA_ALIGNMENT) + let buffer = DmaBuffer::allocate(byte_len, 4096) .map_err(|_| "failed to allocate interrupt remap table")?; if !buffer.is_physically_contiguous() { return Err("interrupt remap table allocation is not physically contiguous"); } let base = buffer.physical_address(); - Ok(Self { base, entries: entry_count, buffer: Some(buffer) }) + Ok(Self { + base, + entries: entry_count, + buffer: Some(buffer), + format, + }) } - fn addr(&self) -> usize { self.base } + fn addr(&self) -> usize { + self.base + } pub fn program_entry(&self, index: usize, irte: &Irte) -> bool { - if index >= self.entries { return false; } - let e = irte.encode(); - let off = index * IRTE_SIZE; -// SAFETY: caller must verify the safety contract for this operation - unsafe { - core::ptr::write_volatile((self.addr() + off) as *mut u64, e[0]); - core::ptr::write_volatile((self.addr() + off + 8) as *mut u64, e[1]); + if index >= self.entries { + return false; } - info!("IRTE[{}]: src={:04x} dest={:08x} vec={}", index, irte.source_id, irte.dest_id, irte.vector); + let dwords = irte.encode(self.format); + let entry_size = self.format.entry_size(); + let off = index * entry_size; + for (i, &dw) in dwords.iter().enumerate() { + unsafe { + core::ptr::write_volatile((self.addr() + off + i * 4) as *mut u32, dw); + } + } + log::info!( + "IRTE[{}] fmt={:?} src={:04x} dest={:08x} vec={}", + index, + self.format, + irte.source_id, + irte.dest_id, + irte.vector + ); true } pub fn invalidate_entry(&self, index: usize) { - if index >= self.entries { return; } -// SAFETY: caller guarantees pointer is valid, aligned, and live - unsafe { core::ptr::write_volatile((self.addr() + index * IRTE_SIZE) as *mut u64, 0u64); } + if index >= self.entries { + return; + } + let entry_size = self.format.entry_size(); + let off = index * entry_size; + let dwords = entry_size / 4; + for i in 0..dwords { + unsafe { + core::ptr::write_volatile((self.addr() + off + i * 4) as *mut u32, 0u32); + } + } } pub fn find_free(&self) -> Option { for i in 0..self.entries { - let off = i * IRTE_SIZE; - if unsafe { core::ptr::read_volatile((self.addr() + off) as *const u64) & IRTE_PRESENT == 0 } { + let off = i * self.format.entry_size(); + if unsafe { core::ptr::read_volatile((self.addr() + off) as *const u32) & 1 == 0 } { return Some(i); } } None } - pub fn physical_address(&self) -> usize { self.base } + pub fn physical_address(&self) -> usize { + self.base + } pub fn len_encoding(&self) -> u64 { - let total_size = self.entries * IRTE_SIZE; - if total_size == 0 { return 0; } - let power = (total_size.next_power_of_two().trailing_zeros() as u64) - .saturating_sub(12); + let total_size = self.entries * self.format.entry_size(); + if total_size == 0 { + return 0; + } + let power = + (total_size.next_power_of_two().trailing_zeros() as u64).saturating_sub(12); power & 0xF } } -pub struct IrqRemapManager { - pub tables: Vec, -} +// ── Tests ────────────────────────────────────────────────────────────── -impl IrqRemapManager { - pub fn new() -> Self { Self { tables: Vec::new() } } +#[cfg(test)] +mod tests { + use super::*; - pub fn remap_interrupt(&self, sid: u16, dest: u32, vector: u8) -> Option { - let irte = Irte::new(sid, dest, vector); - for table in &self.tables { - if let Some(i) = table.find_free() { - table.program_entry(i, &irte); - return Some(i); - } - } - None + // ── IRT format selection ─────────────────────────────────────────── + + #[test] + fn irt_format_legacy32_for_ivhd_type_0x10() { + // Given: IVHD type 0x10 always uses legacy 32-bit format + // When: selecting format from EFR and type + // Then: returns Legacy32 regardless of EFR + assert_eq!( + irt_format_from_efr(0, 0x10), + IrtFormat::Legacy32 + ); + assert_eq!( + irt_format_from_efr(EFR_GASUP, 0x10), + IrtFormat::Legacy32 + ); } - pub fn validate_msi(&self, addr: u64, _data: u32) -> bool { - let idx = ((addr >> 5) & 0x7FFF) as usize; - for table in &self.tables { - if idx < table.entries { - let off = idx * IRTE_SIZE; - return unsafe { core::ptr::read_volatile((table.addr() + off) as *const u64) & IRTE_PRESENT != 0 }; - } - } - false + #[test] + fn irt_format_ga128_when_efr_ga_supported() { + // Given: IVHD type 0x11 with EFR GA bit set + // When: selecting format + // Then: returns GA128 + assert_eq!( + irt_format_from_efr(EFR_GASUP, 0x11), + IrtFormat::GA128 + ); + assert_eq!( + irt_format_from_efr(EFR_GASUP, 0x40), + IrtFormat::GA128 + ); + } + + #[test] + fn irt_format_legacy32_when_efr_ga_absent() { + // Given: IVHD type 0x11 but EFR GA bit not set + // When: selecting format + // Then: returns Legacy32 + assert_eq!( + irt_format_from_efr(0, 0x11), + IrtFormat::Legacy32 + ); + } + + #[test] + fn irt_format_legacy32_when_efr_zero() { + // Given: EFR = 0 (no features) + // When: selecting format for type 0x11 + // Then: Legacy32 + assert_eq!( + irt_format_from_efr(0, 0x11), + IrtFormat::Legacy32 + ); + } + + #[test] + fn irt_entry_sizes() { + assert_eq!(IrtFormat::Legacy32.entry_size(), 4); + assert_eq!(IrtFormat::GA128.entry_size(), 16); + } + + // ── IRTE encoding / decoding ─────────────────────────────────────── + + #[test] + fn irte_legacy32_encode_decode_roundtrip() { + // Given: an IRTE with known fields + let irte = Irte { + present: true, + source_id: 0, + dest_id: 0x42, + vector: 0x30, + delivery_mode: 0, // Fixed + trigger_mode: 0, + }; + // When: encode to legacy32 then decode back + let dwords = irte.encode(IrtFormat::Legacy32); + let decoded = Irte::decode(&dwords, IrtFormat::Legacy32); + // Then: vector and destination survive roundtrip + assert_eq!(decoded.present, true); + assert_eq!(decoded.vector, 0x30); + assert_eq!(decoded.dest_id, 0x42); + } + + #[test] + fn irte_ga128_encode_decode_roundtrip() { + // Given: an IRTE with dest_id that fits the GA128 layout (bits 31:5 stored) + let irte = Irte { + present: true, + source_id: 0xABCD, + dest_id: 0x1234_5660, + vector: 0x55, + delivery_mode: 0, + trigger_mode: 0, + }; + // When: encode to GA128 then decode back + let dwords = irte.encode(IrtFormat::GA128); + let decoded = Irte::decode(&dwords, IrtFormat::GA128); + // Then: fields survive roundtrip (GA128 stores bits 31:5 of dest_id) + assert_eq!(decoded.present, true); + assert_eq!(decoded.vector, 0x55); + assert_eq!(decoded.dest_id, 0x1234_5660); + assert_eq!(decoded.source_id, 0xABCD); + } + + #[test] + fn irte_legacy32_not_present() { + // Given: IRTE with present=false + let irte = Irte { + present: false, + source_id: 0, + dest_id: 0, + vector: 0, + delivery_mode: 0, + trigger_mode: 0, + }; + // When: encode + let dwords = irte.encode(IrtFormat::Legacy32); + // Then: first dword bit 0 is 0 + assert_eq!(dwords[0] & 1, 0); + let decoded = Irte::decode(&dwords, IrtFormat::Legacy32); + assert!(!decoded.present); + } + + #[test] + fn irte_ga128_not_present() { + // Given: GA128 IRTE with present=false + let irte = Irte { + present: false, + source_id: 0, + dest_id: 0, + vector: 0, + delivery_mode: 0, + trigger_mode: 0, + }; + let dwords = irte.encode(IrtFormat::GA128); + assert_eq!(dwords[0] & 1, 0); + let decoded = Irte::decode(&dwords, IrtFormat::GA128); + assert!(!decoded.present); + } + + // ── MSI message address encoding ─────────────────────────────────── + + #[test] + fn remapped_msi_address_legacy32_encodes_index() { + // Given: IRTE index 5 in legacy32 format + // When: computing remapped address + // Then: address has correct base and index + let addr = remapped_msi_address(5, IrtFormat::Legacy32); + assert_eq!((addr >> 20) & 0xFFF, 0xFEE); + assert_eq!((addr >> 5) & 0x7FFF, 5); + } + + #[test] + fn remapped_msi_address_zero_index() { + let addr = remapped_msi_address(0, IrtFormat::GA128); + assert_eq!((addr >> 5) & 0x7FFF, 0); + } + + #[test] + fn direct_apic_address_encodes_destination() { + let addr = direct_apic_address(0xAB); + assert_eq!(addr & 0xFFFF_F000, 0xFEE0_0000 | ((0xABu64) << 12)); + assert_eq!((addr >> 12) & 0xFF, 0xAB); + } + + #[test] + fn direct_apic_data_encodes_vector_and_delivery() { + let data = direct_apic_data(0x30, 0); // Fixed, vector 0x30 + assert_eq!(data & 0xFF, 0x30); + assert_eq!((data >> 8) & 0x7, 0); } } diff --git a/local/recipes/system/iommu/source/src/lib_core.rs b/local/recipes/system/iommu/source/src/lib_core.rs new file mode 100644 index 0000000000..3f7f507ab7 --- /dev/null +++ b/local/recipes/system/iommu/source/src/lib_core.rs @@ -0,0 +1,8 @@ +//! Core library for the IOMMU daemon — pure logic, host-testable. +//! +//! This lib crate contains modules that compile without redox-driver-sys +//! (no MMIO, no DMA allocation, no scheme IPC). The binary crate links +//! these plus the redox-specific modules. + +pub mod acpi; +pub mod interrupt; diff --git a/local/recipes/system/iommu/source/src/main.rs b/local/recipes/system/iommu/source/src/main.rs index 3557d6bace..1c96f6b5d6 100644 --- a/local/recipes/system/iommu/source/src/main.rs +++ b/local/recipes/system/iommu/source/src/main.rs @@ -1,16 +1,32 @@ //! IOMMU daemon — provides scheme:iommu for DMA remapping. //! Includes interrupt remapping (IR) via IRTE tables. -mod interrupt; +// Redox-specific modules (depend on redox-driver-sys). +#[cfg(feature = "redox")] +mod amd_vi; +#[cfg(feature = "redox")] +mod command_buffer; +#[cfg(feature = "redox")] +mod device_table; +#[cfg(feature = "redox")] +mod mmio; +#[cfg(feature = "redox")] +mod page_table; +#[cfg(feature = "redox")] +mod scheme_impl; use std::env; use std::fs; use std::path::PathBuf; use std::process; -use iommu::amd_vi::AmdViUnit; +// Re-export pure-logic modules from the lib crate so they are available +// at the bin crate root (shared compilation, no duplicate source). +pub use iommu::{acpi, interrupt}; + +use amd_vi::AmdViUnit; #[cfg(target_os = "redox")] -use iommu::IommuScheme; +use scheme_impl::IommuScheme; use log::{error, info, LevelFilter, Metadata, Record}; #[cfg(target_os = "redox")] use redox_driver_sys::memory::{CacheType, MmioProt, MmioRegion}; @@ -399,6 +415,30 @@ fn run_self_test() -> Result<(), String> { println!("units_initialized_after={}", initialized_after); println!("events_drained={}", events_drained); + // Program a test IRTE on the first initialized unit to verify the + // interrupt remap table is functional at the hardware level. + let mut irte_allocated = false; + for (index, unit) in units.iter().enumerate() { + if !unit.initialized() { + continue; + } + match unit.program_test_irte() { + Ok(irte_index) => { + println!("irte_programmed_unit={}", index); + println!("irte_test_index={}", irte_index); + irte_allocated = true; + } + Err(err) => { + println!("irte_program_warning_unit_{}={}", index, err); + } + } + break; // one unit is enough for the self-test + } + if !irte_allocated { + println!("irte_programmed_unit=none"); + println!("irte_test_index=none"); + } + Ok(()) } diff --git a/local/recipes/system/iommu/source/src/lib.rs b/local/recipes/system/iommu/source/src/scheme_impl.rs similarity index 99% rename from local/recipes/system/iommu/source/src/lib.rs rename to local/recipes/system/iommu/source/src/scheme_impl.rs index 3c15057f1a..3d3177f33b 100644 --- a/local/recipes/system/iommu/source/src/lib.rs +++ b/local/recipes/system/iommu/source/src/scheme_impl.rs @@ -1,18 +1,11 @@ //! AMD-Vi-backed scheme:iommu implementation. -pub mod acpi; -pub mod amd_vi; -pub mod command_buffer; -pub mod device_table; -pub mod interrupt; -pub mod mmio; -pub mod page_table; - use std::collections::BTreeMap; -use acpi::{parse_bdf, Bdf}; -use amd_vi::AmdViUnit; -use page_table::{DomainPageTables, MappingFlags}; +use crate::acpi::{parse_bdf, Bdf}; +use crate::amd_vi::AmdViUnit; +use crate::interrupt::InterruptRemapTable; +use crate::page_table::{DomainPageTables, MappingFlags}; use redox_scheme::scheme::SchemeSync; use redox_scheme::{CallerCtx, OpenResult}; use syscall::data::Stat; @@ -36,6 +29,8 @@ pub mod opcode { pub const ASSIGN_DEVICE: u16 = 0x0020; pub const UNASSIGN_DEVICE: u16 = 0x0021; pub const DRAIN_EVENTS: u16 = 0x0030; + pub const ALLOC_IRTE: u16 = 0x0040; + pub const FREE_IRTE: u16 = 0x0041; } #[derive(Clone, Copy, Debug, PartialEq, Eq)]