Skip to main content

intel_vtd/spec/
pte.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Second-level page table entry types for the Intel VT-d IOMMU.
5//!
6//! Based on Intel VT-d Specification Rev 4.1, §9.8. The second-level page
7//! table format is EPT-like: 64-bit entries, 9 bits per level, with R/W
8//! permission bits in bits 1:0 (bit 2 is ignored) and a PS bit at bit 7 for large
9//! pages at levels 2 (2MB) and 3 (1GB).
10//! Accessed (bit 8) and leaf dirty (bit 9) flags are ignored unless Second Stage
11//! Access/Dirty bit Enable (SSADE) is set in the referencing scalable-mode
12//! PASID-table entry (§3.7.2).
13//!
14//! Page table level parameters:
15//! - Level 4: VA\[47:39\], 9 bits → 512GB region
16//! - Level 3: VA\[38:30\], 9 bits → 1GB page (if PS=1)
17//! - Level 2: VA\[29:21\], 9 bits → 2MB page (if PS=1)
18//! - Level 1: VA\[20:12\], 9 bits → 4KB page
19
20use bitfield_struct::bitfield;
21use inspect::Inspect;
22use zerocopy::FromBytes;
23use zerocopy::Immutable;
24use zerocopy::IntoBytes;
25use zerocopy::KnownLayout;
26
27/// A 64-bit second-level page table entry (PTE or PDE).
28///
29/// EPT-like format used by Intel VT-d for DMA address translation.
30/// Permissions (R/W) are AND-accumulated across all levels per §3.7.1.
31#[bitfield(u64)]
32#[derive(IntoBytes, Immutable, KnownLayout, FromBytes, Inspect)]
33#[rustfmt::skip]
34pub struct SlPte {
35    /// Read permission — 1 = DMA reads allowed through this entry.
36    pub r: bool,
37    /// Write permission — 1 = DMA writes allowed through this entry.
38    pub w: bool,
39    /// EPT execute bit; ignored in VT-d second-stage entries.
40    pub x: bool,
41    #[bits(4)]
42    _ignored1: u64,
43    /// Page Size — 1 = this is a leaf entry for a large page.
44    /// Valid at level 3 (1GB) and level 2 (2MB). Must be 0 at level 4.
45    pub ps: bool,
46    /// Accessed (bit 8): hardware sets this when using the entry for translation.
47    /// Enabled by Second Stage Access/Dirty bit Enable (SSADE) in the
48    /// referencing scalable-mode PASID-table entry; otherwise ignored.
49    pub a: bool,
50    /// Dirty (bit 9): hardware sets this on the leaf entry for a DMA write.
51    /// Ignored in non-leaf entries or when the PASID-table SSADE field is clear.
52    pub d: bool,
53    #[bits(1)]
54    _ignored2: u64,
55    /// Snoop behavior (bit 11, ignored by emulator).
56    pub snp: bool,
57    /// Address bits [51:12] — next-level table or page frame address.
58    #[bits(40)]
59    pub address: u64,
60    #[bits(10)]
61    _ignored3: u64,
62    #[bits(1)]
63    _reserved: u64,
64    #[bits(1)]
65    _ignored4: u64,
66}
67
68/// Number of entries per page table page (4KB / 8 bytes = 512).
69pub const ENTRIES_PER_TABLE: usize = 512;
70
71/// Bits per page table level index (log2 of 512 = 9).
72pub const BITS_PER_LEVEL: u32 = 9;
73
74/// Page size at level 1 (4KB).
75pub const PAGE_SIZE_4K: u64 = 4096;
76
77/// Page size at level 2 (2MB).
78pub const PAGE_SIZE_2M: u64 = 1 << 21;
79
80/// Page size at level 3 (1GB).
81pub const PAGE_SIZE_1G: u64 = 1 << 30;
82
83impl SlPte {
84    /// Check if this entry is present (R or W is set).
85    ///
86    /// For DMA remapping, an entry is present if any of R or W is set.
87    /// The X bit is ignored for second-level entries.
88    pub fn is_present(&self) -> bool {
89        self.r() || self.w()
90    }
91
92    /// Get the full physical address from the address field (bits 51:12 shifted).
93    pub fn phys_address(&self) -> u64 {
94        self.address() << 12
95    }
96
97    /// Compute the page table entry index for a given IOVA and level.
98    ///
99    /// Extracts the 9-bit index from `iova` at the bit range for `level`:
100    /// `(iova >> shift) & 0x1FF`, where `shift = 12 + (level - 1) * 9`.
101    ///
102    /// Level 1: bits 20:12 (shift=12)
103    /// Level 2: bits 29:21 (shift=21)
104    /// Level 3: bits 38:30 (shift=30)
105    /// Level 4: bits 47:39 (shift=39)
106    pub fn iova_index(iova: u64, level: u8) -> usize {
107        let shift = 12 + (level as u32 - 1) * BITS_PER_LEVEL;
108        ((iova >> shift) & 0x1FF) as usize
109    }
110
111    /// Get the page size at a given level (when PS=1 for large pages).
112    ///
113    /// Level 1: 4KB, Level 2: 2MB, Level 3: 1GB.
114    pub fn page_size_at_level(level: u8) -> u64 {
115        1u64 << (12 + (level as u32 - 1) * BITS_PER_LEVEL)
116    }
117
118    /// Compute the GPA for a large page mapping at the given level.
119    ///
120    /// Masks the page frame address to the level alignment and adds the
121    /// offset from the IOVA within the page.
122    pub fn large_page_gpa(&self, iova: u64, level: u8) -> u64 {
123        let page_size = Self::page_size_at_level(level);
124        let mask = !(page_size - 1);
125        (self.phys_address() & mask) | (iova & (page_size - 1))
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use test_with_tracing::test;
133
134    #[test]
135    fn test_accessed_dirty_literal_layout() {
136        assert_eq!(size_of::<SlPte>(), 8);
137        assert_eq!(align_of::<SlPte>(), 8);
138        for (raw, accessed, dirty) in [
139            (0x100, true, false),
140            (0x200, false, true),
141            (0x400, false, false),
142        ] {
143            let pte = SlPte::from(raw);
144            assert_eq!(pte.a(), accessed);
145            assert_eq!(pte.d(), dirty);
146            assert!(!pte.is_present());
147        }
148        assert_eq!(SlPte::new().with_a(true).into_bits(), 1 << 8);
149        assert_eq!(SlPte::new().with_d(true).into_bits(), 1 << 9);
150        assert_eq!(
151            SlPte::new().with_a(true).with_d(true).as_bytes(),
152            &[0, 3, 0, 0, 0, 0, 0, 0]
153        );
154    }
155
156    #[test]
157    fn test_accessed_dirty_preserve_other_bits() {
158        // Include every unrelated bit, including ignored and reserved bits.
159        for bit in 0..64 {
160            for raw in [1u64 << bit, !(1u64 << bit)] {
161                for accessed in [false, true] {
162                    for dirty in [false, true] {
163                        let pte = SlPte::from(raw).with_a(accessed).with_d(dirty);
164                        assert_eq!(
165                            pte.into_bits(),
166                            (raw & !0x300) | ((accessed as u64) << 8) | ((dirty as u64) << 9)
167                        );
168                    }
169                }
170            }
171        }
172    }
173
174    #[test]
175    fn test_pte_present_requires_r_or_w() {
176        assert!(SlPte::new().with_r(true).is_present());
177        assert!(SlPte::new().with_w(true).is_present());
178        assert!(!SlPte::new().is_present());
179        // X=1 alone does NOT make it present for DMA
180        assert!(!SlPte::new().with_x(true).is_present());
181    }
182
183    #[test]
184    fn test_iova_index() {
185        assert_eq!(SlPte::iova_index(0x7F80_0000_0000, 4), 0xFF);
186    }
187
188    #[test]
189    fn test_page_size_at_level() {
190        assert_eq!(SlPte::page_size_at_level(1), PAGE_SIZE_4K);
191        assert_eq!(SlPte::page_size_at_level(2), PAGE_SIZE_2M);
192        assert_eq!(SlPte::page_size_at_level(3), PAGE_SIZE_1G);
193    }
194
195    #[test]
196    fn test_large_page_gpa() {
197        let pte = SlPte::new()
198            .with_r(true)
199            .with_w(true)
200            .with_ps(true)
201            .with_address(0x40000); // 0x40000 << 12 = 0x4000_0000
202        // 1GB page: base 0x4000_0000, offset 0x1234
203        assert_eq!(pte.large_page_gpa(0x4000_1234, 3), 0x4000_1234);
204        // 2MB page
205        let pte2 = SlPte::new()
206            .with_r(true)
207            .with_w(true)
208            .with_ps(true)
209            .with_address(0x200); // 0x200 << 12 = 0x20_0000
210        assert_eq!(pte2.large_page_gpa(0x0020_1000, 2), 0x0020_1000);
211    }
212}