Skip to main content

acpi_spec/
ivrs.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! IVRS (I/O Virtualization Reporting Structure) types for AMD IOMMU discovery.
5//!
6//! The IVRS ACPI table describes AMD IOMMU hardware to the guest OS. It contains
7//! one or more IVHD (I/O Virtualization Hardware Definition) blocks, each
8//! describing a single IOMMU instance: its PCI BDF, MMIO base, capabilities,
9//! and the set of devices behind it.
10//!
11//! Reference: AMD I/O Virtualization Technology (IOMMU) Specification,
12//! Doc #48882, Rev 3.11, §5.
13
14use super::Table;
15use crate::packed_nums::*;
16use bitfield_struct::bitfield;
17use core::mem::size_of;
18use static_assertions::const_assert_eq;
19use zerocopy::FromBytes;
20use zerocopy::Immutable;
21use zerocopy::IntoBytes;
22use zerocopy::KnownLayout;
23use zerocopy::Unaligned;
24
25/// IVRS table revision (AMD IOMMU spec §5.2.1).
26pub const IVRS_REVISION: u8 = 2;
27
28/// IVHD type 10h: Full-featured IVHD (§5.2.2.2).
29pub const IVHD_TYPE_10: u8 = 0x10;
30
31/// IVHD type 11h: Extended IVHD with EFR (§5.2.2.3).
32pub const IVHD_TYPE_11: u8 = 0x11;
33
34/// IVHD type 40h: Maximum performance IVHD with EFR (same layout as 11h).
35pub const IVHD_TYPE_40: u8 = 0x40;
36
37/// IVHD device entry type: all devices (§5.2.2.7, Table 93).
38pub const IVHD_DEV_ALL: u8 = 0x01;
39
40/// IVHD device entry type: select single device (§5.2.2.7, Table 93).
41pub const IVHD_DEV_SELECT: u8 = 0x02;
42
43/// IVHD device entry type: start of device range (§5.2.2.7, Table 93).
44pub const IVHD_DEV_RANGE_START: u8 = 0x03;
45
46/// IVHD device entry type: end of device range (§5.2.2.7, Table 93).
47pub const IVHD_DEV_RANGE_END: u8 = 0x04;
48
49/// IVHD device entry type: special device (§5.2.2.8, Table 96).
50///
51/// 8-byte entry used for IOAPIC, HPET, and other special devices that
52/// are not PCI devices but need IOMMU interrupt remapping.
53pub const IVHD_DEV_SPECIAL: u8 = 0x48;
54
55/// IVHD special device variety: IOAPIC (§5.2.2.8, Table 96).
56pub const IVHD_SPECIAL_IOAPIC: u8 = 0x01;
57
58/// IVHD special device variety: HPET (§5.2.2.8, Table 96).
59pub const IVHD_SPECIAL_HPET: u8 = 0x02;
60
61/// DTE setting: INITPass, EIntPass, NMIPass for fixed interrupt passthrough.
62pub const IVHD_DTE_SETTING_INIT_PASS: u8 = 0x01;
63pub const IVHD_DTE_SETTING_EINT_PASS: u8 = 0x02;
64pub const IVHD_DTE_SETTING_NMI_PASS: u8 = 0x04;
65
66/// IVinfo bitfield for the IVRS table header (AMD IOMMU spec §5.2.1, Table 84).
67#[bitfield(u32)]
68pub struct IvInfo {
69    /// EFRSup: Extended Feature Register supported (bit 0).
70    pub efr_sup: bool,
71    /// DMA remap support (bit 1).
72    pub dma_remap_sup: bool,
73    /// Reserved (bits 4:2).
74    #[bits(3)]
75    _reserved1: u32,
76    /// GVAsize: guest virtual address size (bits 7:5).
77    /// 0 = 48-bit, 1 = 57-bit.
78    #[bits(3)]
79    pub gva_size: u8,
80    /// PAsize: physical/guest-physical address size (bits 14:8).
81    /// Raw value, e.g. 48 for 48-bit.
82    #[bits(7)]
83    pub pa_size: u8,
84    /// VAsize: virtual address size (bits 21:15).
85    /// Raw value, e.g. 48 for 48-bit.
86    #[bits(7)]
87    pub va_size: u8,
88    /// HtAtsResv: HyperTransport ATS reserved (bit 22).
89    pub ht_ats_resv: bool,
90    /// Reserved (bits 31:23).
91    #[bits(9)]
92    _reserved2: u32,
93}
94
95/// IVRS fixed table header (follows the standard ACPI `Header`).
96///
97/// The IVRS table starts with the standard 36-byte ACPI header, followed by
98/// this 12-byte structure, followed by one or more IVHD/IVMD blocks.
99///
100/// Reference: AMD IOMMU spec §5.2.1, Table 83.
101#[repr(C)]
102#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes, Unaligned)]
103pub struct Ivrs {
104    /// IVinfo field (§5.2.1, Table 84). See [`IvInfo`] for the bitfield layout.
105    pub iv_info: u32_ne,
106    /// Reserved, must be zero.
107    pub reserved: [u8; 8],
108}
109
110impl Ivrs {
111    /// Create a new IVRS header with the given IVinfo value.
112    pub fn new(iv_info: u32) -> Self {
113        Self {
114            iv_info: iv_info.into(),
115            reserved: [0; 8],
116        }
117    }
118}
119
120impl Table for Ivrs {
121    const SIGNATURE: [u8; 4] = *b"IVRS";
122}
123
124const_assert_eq!(size_of::<Ivrs>(), 12);
125
126/// IVHD type 11h header (§5.2.2.3, Table 99).
127///
128/// Extended IVHD block with EFR image. Types 10h, 11h, and 40h all share
129/// the same first 24 bytes; types 11h and 40h extend this with EFR/EFR2
130/// register images (bytes 24..40) and are byte-identical apart from the
131/// type field. Type 40h additionally permits variable-length (ACPI
132/// HID-based) device entries, whereas 11h is limited to fixed-length
133/// (BDF-based) entries. Both require `IVinfo[EFRSup] = 1`.
134///
135/// We emit type 11h rather than 40h because it is the most broadly
136/// compatible extended format: the Microsoft hypervisor's boot-time IVRS
137/// parser (as shipped in Windows Server 2022 / build 20348) only
138/// recognizes types 10h and 11h and rejects the whole IVRS as a bad ACPI
139/// table if it finds no matching IVHD, whereas type 40h support is newer.
140/// Type 11h is accepted by that parser, newer hypervisor builds, and
141/// Linux. Our device entries are all BDF-based, so 40h buys us nothing.
142#[repr(C)]
143#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes, Unaligned)]
144pub struct IvhdType11 {
145    /// IVHD type: always [`IVHD_TYPE_11`].
146    pub ivhd_type: u8,
147    /// Flags (§5.2.2.2, Table 87).
148    pub flags: u8,
149    /// Length of the entire IVHD block in bytes (header + device entries).
150    pub length: u16_ne,
151    /// DeviceID (BDF) of the IOMMU itself.
152    pub device_id: u16_ne,
153    /// Offset of the IOMMU capability block in PCI config space.
154    pub capability_offset: u16_ne,
155    /// IOMMU MMIO base address (64-bit).
156    pub iommu_base_address: u64_ne,
157    /// PCI segment group number.
158    pub pci_segment: u16_ne,
159    /// IOMMU info field (§5.2.2.2).
160    pub iommu_info: u16_ne,
161    /// IOMMU attributes (§5.2.2.3). Repurposed from the type 10h
162    /// `iommu_feature_info` field.
163    pub iommu_attributes: u32_ne,
164    /// Extended Feature Register image (same layout as MMIO 0x0030).
165    pub efr_register: u64_ne,
166    /// Extended Feature Register 2 image.
167    pub efr_register2: u64_ne,
168}
169
170impl IvhdType11 {
171    /// Create a new IVHD type 11h header.
172    pub fn new(
173        device_id: u16,
174        capability_offset: u16,
175        iommu_base_address: u64,
176        pci_segment: u16,
177        efr: u64,
178    ) -> Self {
179        Self {
180            ivhd_type: IVHD_TYPE_11,
181            flags: 0,
182            length: (size_of::<Self>() as u16).into(),
183            device_id: device_id.into(),
184            capability_offset: capability_offset.into(),
185            iommu_base_address: iommu_base_address.into(),
186            pci_segment: pci_segment.into(),
187            iommu_info: 0.into(),
188            iommu_attributes: 0.into(),
189            efr_register: efr.into(),
190            efr_register2: 0.into(),
191        }
192    }
193
194    /// Set the total length (header + device entries).
195    pub fn with_length(mut self, length: u16) -> Self {
196        self.length = length.into();
197        self
198    }
199
200    /// Set the flags byte.
201    pub fn with_flags(mut self, flags: u8) -> Self {
202        self.flags = flags;
203        self
204    }
205}
206
207const_assert_eq!(size_of::<IvhdType11>(), 40);
208
209/// IVHD 4-byte device entry (§5.2.2.7, Table 93).
210///
211/// Used for: all-devices (type 01h), select (type 02h), start-of-range
212/// (type 03h), end-of-range (type 04h).
213#[repr(C)]
214#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes, Unaligned)]
215pub struct IvhdDeviceEntry4 {
216    /// Entry type (see IVHD_DEV_* constants).
217    pub entry_type: u8,
218    /// DeviceID (BDF) for select/range entries. Zero for type 01h (all).
219    pub device_id: u16_ne,
220    /// DTE setting applied to matching devices (§5.2.2.7, Table 94):
221    /// - Bit 0: INITPass
222    /// - Bit 1: EIntPass
223    /// - Bit 2: NMIPass
224    /// - Bit 3: Reserved
225    /// - Bits 5:4: SysMgt
226    /// - Bit 6: Lint0Pass
227    /// - Bit 7: Lint1Pass
228    pub dte_setting: u8,
229}
230
231impl IvhdDeviceEntry4 {
232    /// Create an "all devices" entry (type 01h).
233    pub fn all(dte_setting: u8) -> Self {
234        Self {
235            entry_type: IVHD_DEV_ALL,
236            device_id: 0.into(),
237            dte_setting,
238        }
239    }
240
241    /// Create a "select" entry (type 02h) for a single device.
242    pub fn select(device_id: u16, dte_setting: u8) -> Self {
243        Self {
244            entry_type: IVHD_DEV_SELECT,
245            device_id: device_id.into(),
246            dte_setting,
247        }
248    }
249
250    /// Create a "start of range" entry (type 03h).
251    pub fn range_start(device_id: u16, dte_setting: u8) -> Self {
252        Self {
253            entry_type: IVHD_DEV_RANGE_START,
254            device_id: device_id.into(),
255            dte_setting,
256        }
257    }
258
259    /// Create an "end of range" entry (type 04h).
260    pub fn range_end(device_id: u16) -> Self {
261        Self {
262            entry_type: IVHD_DEV_RANGE_END,
263            device_id: device_id.into(),
264            dte_setting: 0,
265        }
266    }
267}
268
269const_assert_eq!(size_of::<IvhdDeviceEntry4>(), 4);
270
271/// IVHD 8-byte special device entry (§5.2.2.8, Table 96).
272///
273/// Used for non-PCI devices (IOAPIC, HPET) that need IOMMU interrupt
274/// remapping. The `handle` identifies the device instance (e.g., IOAPIC ID
275/// or HPET number), and the source RID identifies the remapping context used
276/// for DTE/IRTE lookup.
277#[repr(C)]
278#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes, Unaligned)]
279pub struct IvhdSpecialDeviceEntry8 {
280    /// Entry type (0x48 for special device).
281    pub entry_type: u8,
282    /// Reserved, must be zero (bytes 1-2 per spec Table 107).
283    _reserved: u16_ne,
284    /// DTE setting (same as `IvhdDeviceEntry4::dte_setting`).
285    pub dte_setting: u8,
286    /// Device handle — interpretation depends on `variety`:
287    /// - IOAPIC: IOAPIC ID (from ACPI MADT)
288    /// - HPET: HPET number
289    pub handle: u8,
290    /// Source DeviceID — the BDF used by the IOMMU for DTE/IRTE lookup
291    /// when this special device issues an interrupt.
292    pub source_device_id: u16_ne,
293    /// Variety of special device (see `IVHD_SPECIAL_*` constants).
294    pub variety: u8,
295}
296
297impl IvhdSpecialDeviceEntry8 {
298    /// Create a special device entry for an IOAPIC.
299    ///
300    /// - `rid`: IOAPIC RID for IOMMU DTE/IRTE lookup
301    /// - `ioapic_id`: the IOAPIC ID (matches the MADT entry)
302    pub fn ioapic(rid: u16, ioapic_id: u8) -> Self {
303        Self {
304            entry_type: IVHD_DEV_SPECIAL,
305            _reserved: 0.into(),
306            dte_setting: 0,
307            handle: ioapic_id,
308            source_device_id: rid.into(),
309            variety: IVHD_SPECIAL_IOAPIC,
310        }
311    }
312}
313
314const_assert_eq!(size_of::<IvhdSpecialDeviceEntry8>(), 8);
315
316#[cfg(test)]
317mod tests {
318    extern crate alloc;
319
320    use super::*;
321    use alloc::vec::Vec;
322    use zerocopy::IntoBytes;
323
324    #[test]
325    fn test_ivrs_header() {
326        let ivrs = Ivrs::new(0x0040_3000);
327        assert_eq!(ivrs.iv_info.get(), 0x0040_3000);
328        assert_eq!(ivrs.reserved, [0; 8]);
329        let bytes = ivrs.as_bytes();
330        assert_eq!(bytes.len(), 12);
331    }
332
333    #[test]
334    fn test_iv_info_bitfield() {
335        let iv_info = IvInfo::new()
336            .with_efr_sup(true)
337            .with_pa_size(48)
338            .with_va_size(48);
339        let raw = u32::from(iv_info);
340        assert_eq!(raw & 1, 1); // EFRSup = bit 0
341        assert_eq!((raw >> 8) & 0x7F, 48); // PAsize = bits 14:8
342        assert_eq!((raw >> 15) & 0x7F, 48); // VAsize = bits 21:15
343    }
344
345    #[test]
346    fn test_ivrs_signature() {
347        assert_eq!(Ivrs::SIGNATURE, *b"IVRS");
348    }
349
350    #[test]
351    fn test_ivhd_type11_defaults() {
352        let ivhd = IvhdType11::new(0x0002, 0x40, 0xFD00_0000, 0, 0xC0);
353        assert_eq!(ivhd.ivhd_type, IVHD_TYPE_11);
354        assert_eq!(ivhd.flags, 0);
355        assert_eq!(ivhd.length.get(), size_of::<IvhdType11>() as u16);
356        assert_eq!(ivhd.device_id.get(), 0x0002);
357        assert_eq!(ivhd.capability_offset.get(), 0x40);
358        assert_eq!(ivhd.iommu_base_address.get(), 0xFD00_0000);
359        assert_eq!(ivhd.pci_segment.get(), 0);
360        assert_eq!(ivhd.iommu_info.get(), 0);
361        assert_eq!(ivhd.iommu_attributes.get(), 0);
362        assert_eq!(ivhd.efr_register.get(), 0xC0);
363        assert_eq!(ivhd.efr_register2.get(), 0);
364    }
365
366    #[test]
367    fn test_ivhd_type11_with_length() {
368        let ivhd = IvhdType11::new(0x0002, 0x40, 0xFD00_0000, 0, 0).with_length(48);
369        assert_eq!(ivhd.length.get(), 48);
370    }
371
372    #[test]
373    fn test_ivhd_device_entry_all() {
374        let entry = IvhdDeviceEntry4::all(0);
375        assert_eq!(entry.entry_type, IVHD_DEV_ALL);
376        assert_eq!(entry.device_id.get(), 0);
377        assert_eq!(entry.dte_setting, 0);
378    }
379
380    #[test]
381    fn test_ivhd_device_entry_select() {
382        let entry = IvhdDeviceEntry4::select(0x0108, 0x07);
383        assert_eq!(entry.entry_type, IVHD_DEV_SELECT);
384        assert_eq!(entry.device_id.get(), 0x0108);
385        assert_eq!(entry.dte_setting, 0x07);
386    }
387
388    #[test]
389    fn test_ivhd_device_entry_range() {
390        let start = IvhdDeviceEntry4::range_start(0x0001, 0);
391        let end = IvhdDeviceEntry4::range_end(0xFFFF);
392        assert_eq!(start.entry_type, IVHD_DEV_RANGE_START);
393        assert_eq!(start.device_id.get(), 0x0001);
394        assert_eq!(end.entry_type, IVHD_DEV_RANGE_END);
395        assert_eq!(end.device_id.get(), 0xFFFF);
396    }
397
398    #[test]
399    fn test_ivhd_device_entry_size() {
400        assert_eq!(size_of::<IvhdDeviceEntry4>(), 4);
401    }
402
403    #[test]
404    fn test_ivrs_round_trip() {
405        // Build a minimal IVRS: header + IVHD + two device entries
406        let ivrs = Ivrs::new(0x0040_3000);
407        let dev_entries_size = 2 * size_of::<IvhdDeviceEntry4>() as u16;
408        let ivhd_total = size_of::<IvhdType11>() as u16 + dev_entries_size;
409        let ivhd = IvhdType11::new(0x0002, 0x40, 0xFD00_0000, 0, 0).with_length(ivhd_total);
410
411        let range_start = IvhdDeviceEntry4::range_start(0x0001, 0);
412        let range_end = IvhdDeviceEntry4::range_end(0xFFFF);
413
414        // Serialize and verify offsets
415        let mut buf = Vec::new();
416        buf.extend_from_slice(ivrs.as_bytes());
417        buf.extend_from_slice(ivhd.as_bytes());
418        buf.extend_from_slice(range_start.as_bytes());
419        buf.extend_from_slice(range_end.as_bytes());
420
421        // IVRS header = 12 bytes
422        // IVHD header = 40 bytes
423        // 2 device entries = 8 bytes
424        // Total = 60 bytes
425        assert_eq!(buf.len(), 60);
426
427        // Verify IVHD starts at offset 12
428        assert_eq!(buf[12], IVHD_TYPE_11);
429        // Verify device entries start at offset 52 (12 + 40)
430        assert_eq!(buf[52], IVHD_DEV_RANGE_START);
431        assert_eq!(buf[56], IVHD_DEV_RANGE_END);
432    }
433}