iommu: AMD-Vi interrupt-remapping structural correction

IVRS parser (acpi.rs):
- Parse IVHD type 0x40 (40-byte header, same layout as 0x11)
- Parse IVMD types 0x20/0x21/0x22 into IvmdInfo struct
- IVMD scope determined by type, exclusion by flags (bit 4 = EXCL_RANGE)
- UNITY read/write flags applied independently
- Fix IVHD header size: 24 bytes for 0x10, 40 for 0x11/0x40

Interrupt remap table (interrupt.rs):
- Remove global 16-byte IRTE assumption
- IRT format selected per-unit from IVHD EFR field (GASup bit)
- Legacy32 (4-byte) vs GA128 (16-byte) IRTE formats
- EFR-driven encode/decode for both formats
- Remapped MSI address and direct-APIC message encoding helpers
- Restructure: lib crate (lib_core.rs) for host-testable pure logic

Build restructure:
- Split into lib (acpi, interrupt) and bin (redox-specific) targets
- Feature-gate redox-driver-sys behind 'redox' feature
- Host-runnable tests: cargo test --lib --no-default-features

Kernel: remove unconditional-true iommu_validate_msi_irq gate
pcid/msi: add remapped_message_address/data helpers for BDF-aware MSI
Self-test: IRTE programming verification on first device use

Tests: 23/23 pass (10 acpi + 13 interrupt)
This commit is contained in:
2026-08-05 15:34:14 +03:00
parent a0537c17ab
commit 1de0326398
7 changed files with 662 additions and 110 deletions
+15 -3
View File
@@ -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]
+167 -10
View File
@@ -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<IommuUnitInfo>,
pub ivmd_entries: Vec<IvmdInfo>,
}
/// 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<IvrsInfo, IvrsError> {
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<IvrsInfo, IvrsError> {
}
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<IvrsInfo, IvrsError> {
revision,
iv_info,
units,
ivmd_entries,
})
}
fn parse_ivhd(entry: &[u8], table_offset: usize) -> Result<IommuUnitInfo, IvrsError> {
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<IommuUnitInfo, IvrsEr
}
let mut device_entries = Vec::new();
let mut offset = IVHD_HEADER_BYTES;
let mut offset = header_size;
while offset < entry.len() {
let kind = entry[offset];
match kind {
@@ -399,6 +434,31 @@ fn parse_ivhd(entry: &[u8], table_offset: usize) -> Result<IommuUnitInfo, IvrsEr
})
}
fn parse_ivmd(entry: &[u8], table_offset: usize) -> Result<IvmdInfo, IvrsError> {
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<u8> {
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<u8> {
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<u8> {
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);
}
}
@@ -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<usize, String> {
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<Vec<AmdViEvent>, String> {
let mut drained = Vec::new();
if !self.initialized {
@@ -391,9 +409,9 @@ mod tests {
use super::AmdViUnit;
fn build_ivrs_with_unit() -> Vec<u8> {
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);
@@ -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<u32> {
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<DmaBuffer>,
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<Self, &'static str> {
pub fn new_allocated(
entry_count: usize,
format: IrtFormat,
) -> Result<Self, &'static str> {
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<usize> {
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<InterruptRemapTable>,
}
// ── 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<usize> {
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);
}
}
@@ -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;
+43 -3
View File
@@ -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(())
}
@@ -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)]