Skip to main content

memf_core/
test_builders.rs

1//! Test builders for synthetic page tables and kernel structs.
2//!
3//! Test-fixture infrastructure (a `pub mod` reachable from integration tests);
4//! panic-on-failure via `unwrap`/`expect` is intended — a broken fixture
5//! should fail loudly, not silently degrade.
6#![allow(clippy::unwrap_used, clippy::expect_used)]
7
8use memf_format::{PhysicalMemoryProvider, PhysicalRange};
9
10/// A synthetic physical memory image for testing.
11#[derive(Debug, Clone)]
12pub struct SyntheticPhysMem {
13    data: Vec<u8>,
14}
15
16impl SyntheticPhysMem {
17    /// Create a new synthetic image of `size` bytes, zero-filled.
18    pub fn new(size: usize) -> Self {
19        Self {
20            data: vec![0u8; size],
21        }
22    }
23
24    /// Write bytes at a physical address.
25    pub fn write_bytes(&mut self, addr: u64, bytes: &[u8]) {
26        let start = addr as usize;
27        self.data[start..start + bytes.len()].copy_from_slice(bytes);
28    }
29
30    /// Write a u64 value at a physical address (little-endian).
31    pub fn write_u64(&mut self, addr: u64, value: u64) {
32        self.write_bytes(addr, &value.to_le_bytes());
33    }
34
35    /// Read a u64 from a physical address (little-endian).
36    pub fn read_u64(&self, addr: u64) -> u64 {
37        let start = addr as usize;
38        u64::from_le_bytes(self.data[start..start + 8].try_into().unwrap())
39    }
40
41    /// Return the raw data slice.
42    pub fn data(&self) -> &[u8] {
43        &self.data
44    }
45}
46
47impl PhysicalMemoryProvider for SyntheticPhysMem {
48    fn read_phys(&self, addr: u64, buf: &mut [u8]) -> memf_format::Result<usize> {
49        if buf.is_empty() {
50            return Ok(0);
51        }
52        let start = addr as usize;
53        if start >= self.data.len() {
54            return Ok(0);
55        }
56        let available = self.data.len() - start;
57        let to_read = buf.len().min(available);
58        buf[..to_read].copy_from_slice(&self.data[start..start + to_read]);
59        Ok(to_read)
60    }
61
62    fn ranges(&self) -> &[PhysicalRange] {
63        &[]
64    }
65    fn format_name(&self) -> &str {
66        "Synthetic"
67    }
68}
69
70/// Page table entry flags for x86_64.
71pub mod flags {
72    /// Page is present in physical memory.
73    pub const PRESENT: u64 = 1 << 0;
74    /// Page is writable.
75    pub const WRITABLE: u64 = 1 << 1;
76    /// Page is accessible from user mode.
77    pub const USER: u64 = 1 << 2;
78    /// Page Size bit: indicates a large/huge page at PD/PDPT level.
79    pub const PS: u64 = 1 << 7;
80}
81
82/// Builder for x86_64 4-level page tables.
83pub struct PageTableBuilder {
84    mem: SyntheticPhysMem,
85    next_page: u64,
86    cr3: u64,
87}
88
89impl PageTableBuilder {
90    /// Physical address of the PML4 table (CR3).
91    pub const CR3: u64 = 0x0000_0000;
92    const ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000;
93
94    /// Create a new builder with a 16 MB synthetic memory image.
95    pub fn new() -> Self {
96        let mut mem = SyntheticPhysMem::new(16 * 1024 * 1024);
97        let cr3 = Self::CR3;
98        let next_page = 0x1000;
99        // Zero-initialize the PML4 table at CR3
100        for i in 0..512 {
101            mem.write_u64(cr3 + i * 8, 0);
102        }
103        Self {
104            mem,
105            next_page,
106            cr3,
107        }
108    }
109
110    fn alloc_page(&mut self) -> u64 {
111        let addr = self.next_page;
112        self.next_page += 0x1000;
113        // Zero-initialize the new page
114        for i in 0..512 {
115            self.mem.write_u64(addr + i * 8, 0);
116        }
117        addr
118    }
119
120    /// Map a 4K virtual address to a physical address with given flags.
121    pub fn map_4k(mut self, vaddr: u64, paddr: u64, page_flags: u64) -> Self {
122        let pml4_idx = (vaddr >> 39) & 0x1FF;
123        let pdpt_idx = (vaddr >> 30) & 0x1FF;
124        let pd_idx = (vaddr >> 21) & 0x1FF;
125        let pt_idx = (vaddr >> 12) & 0x1FF;
126
127        // PML4 -> PDPT
128        let pml4e_addr = self.cr3 + pml4_idx * 8;
129        let mut pml4e = self.mem.read_u64(pml4e_addr);
130        if pml4e & flags::PRESENT == 0 {
131            let pdpt_page = self.alloc_page();
132            pml4e = pdpt_page | flags::PRESENT | flags::WRITABLE;
133            self.mem.write_u64(pml4e_addr, pml4e);
134        }
135        let pdpt_base = pml4e & Self::ADDR_MASK;
136
137        // PDPT -> PD
138        let pdpte_addr = pdpt_base + pdpt_idx * 8;
139        let mut pdpte = self.mem.read_u64(pdpte_addr);
140        if pdpte & flags::PRESENT == 0 {
141            let pd_page = self.alloc_page();
142            pdpte = pd_page | flags::PRESENT | flags::WRITABLE;
143            self.mem.write_u64(pdpte_addr, pdpte);
144        }
145        let pd_base = pdpte & Self::ADDR_MASK;
146
147        // PD -> PT
148        let pde_addr = pd_base + pd_idx * 8;
149        let mut pde = self.mem.read_u64(pde_addr);
150        if pde & flags::PRESENT == 0 {
151            let pt_page = self.alloc_page();
152            pde = pt_page | flags::PRESENT | flags::WRITABLE;
153            self.mem.write_u64(pde_addr, pde);
154        }
155        let pt_base = pde & Self::ADDR_MASK;
156
157        // PT entry -> physical page
158        let pte_addr = pt_base + pt_idx * 8;
159        let pte = (paddr & Self::ADDR_MASK) | page_flags | flags::PRESENT;
160        self.mem.write_u64(pte_addr, pte);
161        self
162    }
163
164    /// Map a 2MB large page (sets PS bit at PD level).
165    pub fn map_2m(mut self, vaddr: u64, paddr: u64, page_flags: u64) -> Self {
166        let pml4_idx = (vaddr >> 39) & 0x1FF;
167        let pdpt_idx = (vaddr >> 30) & 0x1FF;
168        let pd_idx = (vaddr >> 21) & 0x1FF;
169
170        // PML4 -> PDPT
171        let pml4e_addr = self.cr3 + pml4_idx * 8;
172        let mut pml4e = self.mem.read_u64(pml4e_addr);
173        if pml4e & flags::PRESENT == 0 {
174            let pdpt_page = self.alloc_page();
175            pml4e = pdpt_page | flags::PRESENT | flags::WRITABLE;
176            self.mem.write_u64(pml4e_addr, pml4e);
177        }
178        let pdpt_base = pml4e & Self::ADDR_MASK;
179
180        // PDPT -> PD
181        let pdpte_addr = pdpt_base + pdpt_idx * 8;
182        let mut pdpte = self.mem.read_u64(pdpte_addr);
183        if pdpte & flags::PRESENT == 0 {
184            let pd_page = self.alloc_page();
185            pdpte = pd_page | flags::PRESENT | flags::WRITABLE;
186            self.mem.write_u64(pdpte_addr, pdpte);
187        }
188        let pd_base = pdpte & Self::ADDR_MASK;
189
190        // PD entry with PS bit set for 2MB page
191        let pde_addr = pd_base + pd_idx * 8;
192        let pde = (paddr & 0x000F_FFFF_FFE0_0000) | page_flags | flags::PRESENT | flags::PS;
193        self.mem.write_u64(pde_addr, pde);
194        self
195    }
196
197    /// Map a 1GB huge page (sets PS bit at PDPT level).
198    pub fn map_1g(mut self, vaddr: u64, paddr: u64, page_flags: u64) -> Self {
199        let pml4_idx = (vaddr >> 39) & 0x1FF;
200        let pdpt_idx = (vaddr >> 30) & 0x1FF;
201
202        // PML4 -> PDPT
203        let pml4e_addr = self.cr3 + pml4_idx * 8;
204        let mut pml4e = self.mem.read_u64(pml4e_addr);
205        if pml4e & flags::PRESENT == 0 {
206            let pdpt_page = self.alloc_page();
207            pml4e = pdpt_page | flags::PRESENT | flags::WRITABLE;
208            self.mem.write_u64(pml4e_addr, pml4e);
209        }
210        let pdpt_base = pml4e & Self::ADDR_MASK;
211
212        // PDPT entry with PS bit set for 1GB page
213        let pdpte_addr = pdpt_base + pdpt_idx * 8;
214        let pdpte = (paddr & 0x000F_FFFF_C000_0000) | page_flags | flags::PRESENT | flags::PS;
215        self.mem.write_u64(pdpte_addr, pdpte);
216        self
217    }
218
219    /// Ensure PML4→PDPT→PD→PT hierarchy exists for a 4K vaddr, returning the PT entry address.
220    fn ensure_pt_entry(&mut self, vaddr: u64) -> u64 {
221        let pml4_idx = (vaddr >> 39) & 0x1FF;
222        let pdpt_idx = (vaddr >> 30) & 0x1FF;
223        let pd_idx = (vaddr >> 21) & 0x1FF;
224        let pt_idx = (vaddr >> 12) & 0x1FF;
225
226        // PML4 -> PDPT
227        let pml4e_addr = self.cr3 + pml4_idx * 8;
228        let mut pml4e = self.mem.read_u64(pml4e_addr);
229        if pml4e & flags::PRESENT == 0 {
230            let pdpt_page = self.alloc_page();
231            pml4e = pdpt_page | flags::PRESENT | flags::WRITABLE;
232            self.mem.write_u64(pml4e_addr, pml4e);
233        }
234        let pdpt_base = pml4e & Self::ADDR_MASK;
235
236        // PDPT -> PD
237        let pdpte_addr = pdpt_base + pdpt_idx * 8;
238        let mut pdpte = self.mem.read_u64(pdpte_addr);
239        if pdpte & flags::PRESENT == 0 {
240            let pd_page = self.alloc_page();
241            pdpte = pd_page | flags::PRESENT | flags::WRITABLE;
242            self.mem.write_u64(pdpte_addr, pdpte);
243        }
244        let pd_base = pdpte & Self::ADDR_MASK;
245
246        // PD -> PT
247        let pde_addr = pd_base + pd_idx * 8;
248        let mut pde = self.mem.read_u64(pde_addr);
249        if pde & flags::PRESENT == 0 {
250            let pt_page = self.alloc_page();
251            pde = pt_page | flags::PRESENT | flags::WRITABLE;
252            self.mem.write_u64(pde_addr, pde);
253        }
254        let pt_base = pde & Self::ADDR_MASK;
255
256        pt_base + pt_idx * 8
257    }
258
259    /// Map a demand-zero PTE: upper levels present, PT entry = 0.
260    pub fn map_demand_zero(mut self, vaddr: u64) -> Self {
261        let pte_addr = self.ensure_pt_entry(vaddr);
262        // PT entry is already zero from page allocation; write explicitly for clarity
263        self.mem.write_u64(pte_addr, 0);
264        self
265    }
266
267    /// Map a transition PTE: PRESENT=0, bit 11 (TRANSITION)=1, PFN in bits [12..48].
268    pub fn map_transition(mut self, vaddr: u64, pfn: u64) -> Self {
269        let pte_addr = self.ensure_pt_entry(vaddr);
270        let pte = (pfn << 12) | (1 << 11);
271        self.mem.write_u64(pte_addr, pte);
272        self
273    }
274
275    /// Map a pagefile PTE: PRESENT=0, pagefile_num in bits [1..5], page_offset in bits [12..48].
276    pub fn map_pagefile(mut self, vaddr: u64, pagefile_num: u8, page_offset: u64) -> Self {
277        let pte_addr = self.ensure_pt_entry(vaddr);
278        let pte = ((u64::from(pagefile_num) & 0xF) << 1) | (page_offset << 12);
279        self.mem.write_u64(pte_addr, pte);
280        self
281    }
282
283    /// Map a prototype PTE: PRESENT=0, bit 10 (PROTOTYPE)=1.
284    pub fn map_prototype(mut self, vaddr: u64) -> Self {
285        let pte_addr = self.ensure_pt_entry(vaddr);
286        let pte: u64 = 1 << 10;
287        self.mem.write_u64(pte_addr, pte);
288        self
289    }
290
291    /// Map a prototype PTE with a specific raw PTE value. Bit 10 must be set.
292    pub fn map_prototype_raw(mut self, vaddr: u64, raw_pte: u64) -> Self {
293        assert!(raw_pte & (1 << 10) != 0, "prototype bit (10) must be set");
294        assert!(raw_pte & 1 == 0, "PRESENT bit must be clear");
295        let pte_addr = self.ensure_pt_entry(vaddr);
296        self.mem.write_u64(pte_addr, raw_pte);
297        self
298    }
299
300    /// Write data bytes at a physical address in the synthetic memory.
301    pub fn write_phys(mut self, addr: u64, data: &[u8]) -> Self {
302        self.mem.write_bytes(addr, data);
303        self
304    }
305
306    /// Write a u64 value at a physical address.
307    pub fn write_phys_u64(mut self, addr: u64, value: u64) -> Self {
308        self.mem.write_u64(addr, value);
309        self
310    }
311
312    /// Consume the builder and return the CR3 value + synthetic memory.
313    pub fn build(self) -> (u64, SyntheticPhysMem) {
314        (self.cr3, self.mem)
315    }
316}
317
318impl Default for PageTableBuilder {
319    fn default() -> Self {
320        Self::new()
321    }
322}
323
324/// Mock pagefile source for testing pagefile PTE resolution.
325pub struct MockPagefileSource {
326    pagefile_num: u8,
327    pages: std::collections::HashMap<u64, [u8; 4096]>,
328}
329
330impl MockPagefileSource {
331    /// Create a mock with the given pagefile number and pre-loaded pages.
332    /// Each tuple is `(page_offset, page_data)`.
333    pub fn new(pagefile_num: u8, pages: Vec<(u64, [u8; 4096])>) -> Self {
334        Self {
335            pagefile_num,
336            pages: pages.into_iter().collect(),
337        }
338    }
339}
340
341/// Mock prototype PTE source for testing prototype PTE resolution.
342pub struct MockPrototypePteSource {
343    /// Maps raw PTE value -> resolved physical address.
344    entries: std::collections::HashMap<u64, u64>,
345}
346
347impl MockPrototypePteSource {
348    /// Create a mock with the given `(pte_value, phys_addr)` pairs.
349    pub fn new(entries: Vec<(u64, u64)>) -> Self {
350        Self {
351            entries: entries.into_iter().collect(),
352        }
353    }
354}
355
356impl crate::proto_pte::PrototypePteSource for MockPrototypePteSource {
357    fn resolve(&self, pte_value: u64) -> Option<u64> {
358        self.entries.get(&pte_value).copied()
359    }
360}
361
362impl crate::pagefile::PagefileSource for MockPagefileSource {
363    fn pagefile_number(&self) -> u8 {
364        self.pagefile_num
365    }
366
367    fn read_page(&self, page_offset: u64) -> crate::Result<Option<[u8; 4096]>> {
368        Ok(self.pages.get(&page_offset).copied())
369    }
370}
371
372/// Build an `ObjectReader` from a pre-built ISF JSON value and an empty page table.
373///
374/// Eliminates the four-line setup boilerplate that repeats across every walker test:
375/// resolver construction, page table build, VAS construction, and reader assembly.
376pub fn make_reader(
377    isf: &memf_symbols::test_builders::IsfBuilder,
378) -> crate::object_reader::ObjectReader<SyntheticPhysMem> {
379    let json = isf.build_json();
380    let resolver = memf_symbols::isf::IsfResolver::from_value(&json).unwrap();
381    let (cr3, mem) = PageTableBuilder::new().build();
382    let vas = crate::vas::VirtualAddressSpace::new(
383        mem,
384        cr3,
385        crate::vas::TranslationMode::X86_64FourLevel,
386    );
387    crate::object_reader::ObjectReader::new(vas, Box::new(resolver))
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn synthetic_mem_read_write() {
396        let mut mem = SyntheticPhysMem::new(4096);
397        mem.write_bytes(0x100, &[0xAA, 0xBB, 0xCC, 0xDD]);
398        let mut buf = [0u8; 4];
399        let n = mem.read_phys(0x100, &mut buf).unwrap();
400        assert_eq!(n, 4);
401        assert_eq!(buf, [0xAA, 0xBB, 0xCC, 0xDD]);
402    }
403
404    #[test]
405    fn synthetic_mem_u64() {
406        let mut mem = SyntheticPhysMem::new(4096);
407        mem.write_u64(0x200, 0xDEAD_BEEF_CAFE_BABE);
408        assert_eq!(mem.read_u64(0x200), 0xDEAD_BEEF_CAFE_BABE);
409    }
410
411    #[test]
412    fn page_table_builder_creates_pml4() {
413        let (cr3, mem) = PageTableBuilder::new().build();
414        assert_eq!(cr3, 0);
415        for i in 0..512 {
416            assert_eq!(mem.read_u64(cr3 + i * 8), 0);
417        }
418    }
419
420    #[test]
421    fn page_table_builder_map_4k() {
422        let vaddr: u64 = 0xFFFF_8000_0010_0000;
423        let paddr: u64 = 0x0080_0000;
424        let (cr3, mem) = PageTableBuilder::new()
425            .map_4k(vaddr, paddr, flags::WRITABLE)
426            .write_phys(paddr, &[0x42; 64])
427            .build();
428        let pml4_idx = (vaddr >> 39) & 0x1FF;
429        let pml4e = mem.read_u64(cr3 + pml4_idx * 8);
430        assert_ne!(pml4e & flags::PRESENT, 0);
431        let mut buf = [0u8; 4];
432        mem.read_phys(paddr, &mut buf).unwrap();
433        assert_eq!(buf, [0x42; 4]);
434    }
435
436    #[test]
437    fn page_table_builder_map_2m() {
438        let vaddr: u64 = 0xFFFF_8000_0020_0000;
439        let paddr: u64 = 0x0100_0000;
440        let (cr3, mem) = PageTableBuilder::new()
441            .map_2m(vaddr, paddr, flags::WRITABLE)
442            .build();
443        let pml4_idx = (vaddr >> 39) & 0x1FF;
444        let pml4e = mem.read_u64(cr3 + pml4_idx * 8);
445        assert_ne!(pml4e & flags::PRESENT, 0);
446    }
447
448    #[test]
449    fn mock_pagefile_source_read_page() {
450        use crate::pagefile::PagefileSource;
451
452        let mut page_data = [0xABu8; 4096];
453        page_data[0] = 0x42;
454        let mock = MockPagefileSource::new(0, vec![(0x10, page_data)]);
455        assert_eq!(mock.pagefile_number(), 0);
456        let page = mock.read_page(0x10).unwrap().unwrap();
457        assert_eq!(page[0], 0x42);
458        assert_eq!(page[1], 0xAB);
459    }
460
461    #[test]
462    fn mock_pagefile_source_missing_page() {
463        use crate::pagefile::PagefileSource;
464
465        let mock = MockPagefileSource::new(1, vec![]);
466        assert_eq!(mock.pagefile_number(), 1);
467        assert!(mock.read_page(0x999).unwrap().is_none());
468    }
469
470    #[test]
471    fn page_table_builder_map_demand_zero() {
472        let vaddr: u64 = 0xFFFF_8000_0010_0000;
473        let (cr3, mem) = PageTableBuilder::new().map_demand_zero(vaddr).build();
474        let pml4_idx = (vaddr >> 39) & 0x1FF;
475        let pml4e = mem.read_u64(cr3 + pml4_idx * 8);
476        assert_ne!(pml4e & flags::PRESENT, 0, "PML4 entry should be present");
477        let pdpt_base = pml4e & PageTableBuilder::ADDR_MASK;
478        let pdpt_idx = (vaddr >> 30) & 0x1FF;
479        let pdpte = mem.read_u64(pdpt_base + pdpt_idx * 8);
480        assert_ne!(pdpte & flags::PRESENT, 0, "PDPT entry should be present");
481        let pd_base = pdpte & PageTableBuilder::ADDR_MASK;
482        let pd_idx = (vaddr >> 21) & 0x1FF;
483        let pde = mem.read_u64(pd_base + pd_idx * 8);
484        assert_ne!(pde & flags::PRESENT, 0, "PD entry should be present");
485        let pt_base = pde & PageTableBuilder::ADDR_MASK;
486        let pt_idx = (vaddr >> 12) & 0x1FF;
487        let pte = mem.read_u64(pt_base + pt_idx * 8);
488        assert_eq!(pte, 0, "demand-zero PTE must be all zeros");
489    }
490
491    #[test]
492    fn page_table_builder_map_transition_pte() {
493        let vaddr: u64 = 0xFFFF_8000_0010_0000;
494        let pfn: u64 = 0x800;
495        let (cr3, mem) = PageTableBuilder::new().map_transition(vaddr, pfn).build();
496        let pml4_idx = (vaddr >> 39) & 0x1FF;
497        let pml4e = mem.read_u64(cr3 + pml4_idx * 8);
498        let pdpt_base = pml4e & PageTableBuilder::ADDR_MASK;
499        let pdpt_idx = (vaddr >> 30) & 0x1FF;
500        let pdpte = mem.read_u64(pdpt_base + pdpt_idx * 8);
501        let pd_base = pdpte & PageTableBuilder::ADDR_MASK;
502        let pd_idx = (vaddr >> 21) & 0x1FF;
503        let pde = mem.read_u64(pd_base + pd_idx * 8);
504        let pt_base = pde & PageTableBuilder::ADDR_MASK;
505        let pt_idx = (vaddr >> 12) & 0x1FF;
506        let pte = mem.read_u64(pt_base + pt_idx * 8);
507        assert_eq!(pte & 1, 0, "PRESENT must be clear");
508        assert_ne!(pte & (1 << 11), 0, "TRANSITION bit must be set");
509        assert_eq!((pte >> 12) & 0xF_FFFF_FFFF, pfn, "PFN must match");
510    }
511
512    #[test]
513    fn page_table_builder_map_pagefile_pte() {
514        let vaddr: u64 = 0xFFFF_8000_0010_0000;
515        let pagefile_num: u8 = 0;
516        let page_offset: u64 = 0x5678;
517        let (cr3, mem) = PageTableBuilder::new()
518            .map_pagefile(vaddr, pagefile_num, page_offset)
519            .build();
520        let pml4_idx = (vaddr >> 39) & 0x1FF;
521        let pml4e = mem.read_u64(cr3 + pml4_idx * 8);
522        let pdpt_base = pml4e & PageTableBuilder::ADDR_MASK;
523        let pdpt_idx = (vaddr >> 30) & 0x1FF;
524        let pdpte = mem.read_u64(pdpt_base + pdpt_idx * 8);
525        let pd_base = pdpte & PageTableBuilder::ADDR_MASK;
526        let pd_idx = (vaddr >> 21) & 0x1FF;
527        let pde = mem.read_u64(pd_base + pd_idx * 8);
528        let pt_base = pde & PageTableBuilder::ADDR_MASK;
529        let pt_idx = (vaddr >> 12) & 0x1FF;
530        let pte = mem.read_u64(pt_base + pt_idx * 8);
531        assert_eq!(pte & 1, 0, "PRESENT must be clear");
532        assert_eq!((pte >> 1) & 0xF, u64::from(pagefile_num), "pagefile_num");
533        assert_eq!(pte & (1 << 10), 0, "prototype bit must be clear");
534        assert_eq!(pte & (1 << 11), 0, "transition bit must be clear");
535        assert_eq!((pte >> 12) & 0xF_FFFF_FFFF, page_offset, "page_offset");
536    }
537
538    #[test]
539    fn page_table_builder_map_prototype_pte() {
540        let vaddr: u64 = 0xFFFF_8000_0010_0000;
541        let (cr3, mem) = PageTableBuilder::new().map_prototype(vaddr).build();
542        let pml4_idx = (vaddr >> 39) & 0x1FF;
543        let pml4e = mem.read_u64(cr3 + pml4_idx * 8);
544        let pdpt_base = pml4e & PageTableBuilder::ADDR_MASK;
545        let pdpt_idx = (vaddr >> 30) & 0x1FF;
546        let pdpte = mem.read_u64(pdpt_base + pdpt_idx * 8);
547        let pd_base = pdpte & PageTableBuilder::ADDR_MASK;
548        let pd_idx = (vaddr >> 21) & 0x1FF;
549        let pde = mem.read_u64(pd_base + pd_idx * 8);
550        let pt_base = pde & PageTableBuilder::ADDR_MASK;
551        let pt_idx = (vaddr >> 12) & 0x1FF;
552        let pte = mem.read_u64(pt_base + pt_idx * 8);
553        assert_eq!(pte & 1, 0, "PRESENT must be clear");
554        assert_ne!(pte & (1 << 10), 0, "PROTOTYPE bit must be set");
555    }
556}