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}