Skip to main content

hyperlight_common/arch/amd64/
vmem.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4//! x86-64 4-level page table manipulation code.
5//!
6//! This module implements page table setup for x86-64 long mode using 4-level paging:
7//! - PML4 (Page Map Level 4) - bits 47:39 - 512 entries, each covering 512GB
8//! - PDPT (Page Directory Pointer Table) - bits 38:30 - 512 entries, each covering 1GB
9//! - PD (Page Directory) - bits 29:21 - 512 entries, each covering 2MB
10//! - PT (Page Table) - bits 20:12 - 512 entries, each covering 4KB pages
11//!
12//! The code uses an iterator-based approach to walk the page table hierarchy,
13//! allocating intermediate tables as needed and setting appropriate flags on leaf PTEs
14
15use crate::vmem::{
16    BasicMapping, CowMapping, MapRequest, MapResponse, Mapping, MappingKind, TableMovabilityBase,
17    TableOps, TableReadOps, UpdateParent, UpdateParentNone, UpdateParentTable, Void, modify_ptes,
18    write_entry_updating,
19};
20
21#[derive(Copy, Clone)]
22pub(in crate::vmem) struct UpdateParentRoot {}
23
24/// Read a PTE and return it (widened to u64) if the present bit is
25/// set. The amd64 "present" encoding is a single bit (bit 0); other
26/// architectures may need richer semantics, which is why this lives
27/// per-arch rather than in the common module.
28///
29/// # Safety
30/// `entry_ptr` must point to a valid page table entry.
31#[inline(always)]
32#[allow(clippy::useless_conversion)]
33pub(super) unsafe fn read_pte_if_present<Op: TableReadOps>(
34    op: &Op,
35    entry_ptr: Op::TableAddr,
36) -> Option<u64> {
37    let pte: u64 = unsafe { op.read_entry(entry_ptr) }.into();
38    if (pte & PAGE_PRESENT) != 0 {
39        Some(pte)
40    } else {
41        None
42    }
43}
44
45/// Require that a PTE is present and descend to the next-level table.
46///
47/// # Safety
48/// `op` must provide valid page table memory.
49pub(super) unsafe fn require_pte_exist<Op: TableReadOps, P: UpdateParent<Op>>(
50    op: &Op,
51    x: MapResponse<Op, P>,
52) -> Option<MapRequest<Op, P::ChildType>>
53where
54    P::ChildType: UpdateParent<Op>,
55{
56    unsafe { read_pte_if_present(op, x.entry_ptr) }.map(|pte| MapRequest {
57        #[allow(clippy::unnecessary_cast)]
58        table_base: Op::from_phys((pte & PTE_ADDR_MASK) as PhysAddr),
59        vmin: x.vmin,
60        len: x.len,
61        update_parent: x.update_parent.for_child_at_entry(x.entry_ptr),
62    })
63}
64
65// Paging Flags
66//
67// See the following links explaining paging:
68//
69// * Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A: System Programming Guide, Part 1
70//  - Chapter 5 "Paging"
71//
72// https://cdrdv2.intel.com/v1/dl/getContent/671200
73//
74// * AMD64 Architecture Programmer’s Manual, Volume 2: System Programming, Section 5.3: Long-Mode Page Translation
75//
76// https://docs.amd.com/v/u/en-US/24593_3.43
77//
78// Or if you prefer something less formal:
79//
80// * Very basic description: https://stackoverflow.com/a/26945892
81// * More in-depth descriptions: https://wiki.osdev.org/Paging
82//
83
84/// Page is Present
85pub const PAGE_PRESENT: u64 = 1;
86/// Page is Read/Write (if not set page is read only so long as the WP bit in CR0 is set to 1 - which it is in Hyperlight)
87const PAGE_RW: u64 = 1 << 1;
88/// Execute Disable (if this bit is set then data in the page cannot be executed)`
89const PAGE_NX: u64 = 1 << 63;
90/// Mask to extract the physical address from a PTE (bits 51:12)
91/// This masks out the lower 12 flag bits AND the upper bits including NX (bit 63)
92pub const PTE_ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000;
93const PAGE_USER_ACCESS_DISABLED: u64 = 0 << 2; // U/S bit not set - supervisor mode only (no code runs in user mode for now)
94const PAGE_DIRTY_SET: u64 = 1 << 6; // D - dirty bit
95const PAGE_ACCESSED_SET: u64 = 1 << 5; // A - accessed bit
96const PAGE_CACHE_ENABLED: u64 = 0 << 4; // PCD - page cache disable bit not set (caching enabled)
97const PAGE_WRITE_BACK: u64 = 0 << 3; // PWT - page write-through bit not set (write-back caching)
98const PAGE_PAT_WB: u64 = 0 << 7; // PAT - page attribute table index bit (0 for write-back memory when PCD=0, PWT=0)
99
100// We use various patterns of the available-for-software-use bits to
101// represent certain special mappings.
102const PTE_AVL_MASK: u64 = 0x0000_0000_0000_0E00;
103const PAGE_AVL_COW: u64 = 1 << 9;
104
105/// Returns PAGE_RW if writable is true, 0 otherwise
106#[inline(always)]
107const fn page_rw_flag(writable: bool) -> u64 {
108    if writable { PAGE_RW } else { 0 }
109}
110
111/// Returns PAGE_NX if executable is false (NX = No Execute), 0 otherwise
112#[inline(always)]
113const fn page_nx_flag(executable: bool) -> u64 {
114    if executable { 0 } else { PAGE_NX }
115}
116
117/// Helper function to generate a page table entry that points to another table
118#[allow(clippy::identity_op)]
119#[allow(clippy::precedence)]
120fn pte_for_table<Op: TableOps>(table_addr: Op::TableAddr) -> u64 {
121    Op::to_phys(table_addr) |
122        PAGE_ACCESSED_SET | // prevent the CPU writing to the access flag
123        PAGE_CACHE_ENABLED | // leave caching enabled
124        PAGE_WRITE_BACK | // use write-back caching
125        PAGE_USER_ACCESS_DISABLED |// dont allow user access (no code runs in user mode for now)
126        PAGE_RW | // R/W - we don't use block-level permissions
127        PAGE_PRESENT // P   - this entry is present
128}
129
130/// This trait is used to select appropriate implementations of
131/// [`UpdateParent`] to be used, depending on whether a particular
132/// implementation needs the ability to move tables.
133pub(in crate::vmem) trait TableMovability<Op: TableReadOps + ?Sized, TableMoveInfo> {
134    type RootUpdateParent: UpdateParent<Op, TableMoveInfo = TableMoveInfo>;
135    fn root_update_parent() -> Self::RootUpdateParent;
136}
137impl<Op: TableOps<TableMovability = crate::vmem::MayMoveTable>> TableMovability<Op, Op::TableAddr>
138    for crate::vmem::MayMoveTable
139{
140    type RootUpdateParent = UpdateParentRoot;
141    fn root_update_parent() -> Self::RootUpdateParent {
142        UpdateParentRoot {}
143    }
144}
145impl<Op: TableReadOps> TableMovability<Op, Void> for crate::vmem::MayNotMoveTable {
146    type RootUpdateParent = UpdateParentNone;
147    fn root_update_parent() -> Self::RootUpdateParent {
148        UpdateParentNone {}
149    }
150}
151
152impl<
153    Op: TableOps<TableMovability = crate::vmem::MayMoveTable>,
154    P: UpdateParent<Op, TableMoveInfo = Op::TableAddr>,
155> UpdateParent<Op> for UpdateParentTable<Op, P>
156{
157    type TableMoveInfo = Op::TableAddr;
158    type ChildType = UpdateParentTable<Op, Self>;
159    fn update_parent(self, op: &Op, new_ptr: Op::TableAddr) {
160        let pte = pte_for_table::<Op>(new_ptr);
161        unsafe {
162            write_entry_updating(op, self.parent, self.entry_ptr, pte);
163        }
164    }
165    fn for_child_at_entry(self, entry_ptr: Op::TableAddr) -> Self::ChildType {
166        Self::ChildType::new(self, entry_ptr)
167    }
168}
169
170impl<Op: TableOps<TableMovability = crate::vmem::MayMoveTable>> UpdateParent<Op>
171    for UpdateParentRoot
172{
173    type TableMoveInfo = Op::TableAddr;
174    type ChildType = UpdateParentTable<Op, Self>;
175    fn update_parent(self, op: &Op, new_ptr: Op::TableAddr) {
176        unsafe {
177            op.update_root(new_ptr);
178        }
179    }
180    fn for_child_at_entry(self, entry_ptr: Op::TableAddr) -> Self::ChildType {
181        Self::ChildType::new(self, entry_ptr)
182    }
183}
184
185/// Page-mapping callback to allocate a next-level page table if necessary.
186/// # Safety
187/// This function modifies page table data structures, and should not be called concurrently
188/// with any other operations that modify the page tables.
189unsafe fn alloc_pte_if_needed<
190    Op: TableOps,
191    P: UpdateParent<
192            Op,
193            TableMoveInfo = <Op::TableMovability as TableMovabilityBase<Op>>::TableMoveInfo,
194        >,
195>(
196    op: &Op,
197    x: MapResponse<Op, P>,
198) -> MapRequest<Op, P::ChildType>
199where
200    P::ChildType: UpdateParent<Op>,
201{
202    let new_update_parent = x.update_parent.for_child_at_entry(x.entry_ptr);
203    if let Some(pte) = unsafe { read_pte_if_present(op, x.entry_ptr) } {
204        return MapRequest {
205            table_base: Op::from_phys(pte & PTE_ADDR_MASK),
206            vmin: x.vmin,
207            len: x.len,
208            update_parent: new_update_parent,
209        };
210    }
211
212    let page_addr = unsafe { op.alloc_table() };
213
214    let pte = pte_for_table::<Op>(page_addr);
215    unsafe {
216        write_entry_updating(op, x.update_parent, x.entry_ptr, pte);
217    };
218    MapRequest {
219        table_base: page_addr,
220        vmin: x.vmin,
221        len: x.len,
222        update_parent: new_update_parent,
223    }
224}
225
226/// Map a normal memory page
227/// # Safety
228/// This function modifies page table data structures, and should not be called concurrently
229/// with any other operations that modify the page tables.
230#[allow(clippy::identity_op)]
231#[allow(clippy::precedence)]
232unsafe fn map_page<
233    Op: TableOps,
234    P: UpdateParent<
235            Op,
236            TableMoveInfo = <Op::TableMovability as TableMovabilityBase<Op>>::TableMoveInfo,
237        >,
238>(
239    op: &Op,
240    mapping: &Mapping,
241    r: MapResponse<Op, P>,
242) {
243    let pte = match &mapping.kind {
244        MappingKind::Basic(bm) =>
245        // TODO: Support not readable
246        // NOTE: On x86-64, there is no separate "readable" bit in the page table entry.
247        // This means that pages cannot be made write-only or execute-only without also being readable.
248        // All pages that are mapped as writable or executable are also implicitly readable.
249        // If support for "not readable" mappings is required in the future, it would need to be
250        // implemented using additional mechanisms (e.g., page-fault handling or memory protection keys),
251        // but for now, this architectural limitation is accepted.
252        {
253            (mapping.phys_base + (r.vmin - mapping.virt_base)) |
254                page_nx_flag(bm.executable) | // NX - no execute unless allowed
255                PAGE_PAT_WB | // PAT index bit for write-back memory
256                PAGE_DIRTY_SET | // prevent the CPU writing to the dirty bit
257                PAGE_ACCESSED_SET | // prevent the CPU writing to the access flag
258                PAGE_CACHE_ENABLED | // leave caching enabled
259                PAGE_WRITE_BACK | // use write-back caching
260                PAGE_USER_ACCESS_DISABLED | // dont allow user access (no code runs in user mode for now)
261                page_rw_flag(bm.writable) | // R/W - set if writable
262                PAGE_PRESENT // P   - this entry is present
263        }
264        MappingKind::Cow(cm) => {
265            (mapping.phys_base + (r.vmin - mapping.virt_base)) |
266                page_nx_flag(cm.executable) | // NX - no execute unless allowed
267                PAGE_AVL_COW |
268                PAGE_PAT_WB | // PAT index bit for write-back memory
269                PAGE_DIRTY_SET | // prevent the CPU writing to the dirty bit
270                PAGE_ACCESSED_SET | // prevent the CPU writing to the access flag
271                PAGE_CACHE_ENABLED | // leave caching enabled
272                PAGE_WRITE_BACK | // use write-back caching
273                PAGE_USER_ACCESS_DISABLED | // dont allow user access (no code runs in user mode for now)
274                0 | // R/W - Cow page is never writable
275                PAGE_PRESENT // P   - this entry is present
276        }
277        MappingKind::Unmapped => 0,
278    };
279    unsafe {
280        write_entry_updating(op, r.update_parent, r.entry_ptr, pte);
281    }
282}
283
284// There are no notable architecture-specific safety considerations
285// here, and the general conditions are documented in the
286// architecture-independent re-export in vmem.rs
287
288/// Maps a contiguous virtual address range to physical memory.
289///
290/// This function walks the 4-level page table hierarchy (PML4 → PDPT → PD → PT),
291/// allocating intermediate tables as needed via `alloc_pte_if_needed`, and finally
292/// writing the leaf page table entries with the requested permissions via `map_page`.
293///
294/// The iterator chain processes each level:
295/// 1. PML4 (47:39) - allocate PDPT if needed
296/// 2. PDPT (38:30) - allocate PD if needed
297/// 3. PD (29:21) - allocate PT if needed
298/// 4. PT (20:12) - write final PTE with physical address and flags
299///
300/// Multi-space page-table walking on amd64: walks each root
301/// independently and emits all leaves as `ThisSpace`. Aliased
302/// intermediate-table detection is not implemented here because no
303/// current embedder exercises that pattern on amd64.
304#[allow(clippy::missing_safety_doc)]
305pub unsafe fn walk_va_spaces<Op: TableReadOps>(
306    op: &Op,
307    roots: &[Op::TableAddr],
308    address: u64,
309    len: u64,
310) -> ::alloc::vec::Vec<(
311    crate::vmem::SpaceId,
312    ::alloc::vec::Vec<crate::vmem::SpaceAwareMapping>,
313)> {
314    use ::alloc::vec::Vec;
315
316    let mut out: Vec<(crate::vmem::SpaceId, Vec<crate::vmem::SpaceAwareMapping>)> =
317        Vec::with_capacity(roots.len());
318
319    let addr = address & ((1u64 << VA_BITS) - 1);
320    let vmin = addr & !(PAGE_SIZE as u64 - 1);
321    let vmax = core::cmp::min(addr + len, 1u64 << VA_BITS);
322
323    for &root in roots {
324        #[allow(clippy::unnecessary_cast)]
325        let root_id: crate::vmem::SpaceId = Op::to_phys(root) as u64;
326        let mut mappings: Vec<crate::vmem::SpaceAwareMapping> = Vec::new();
327
328        let iter = modify_ptes::<47, 39, Op, _>(MapRequest {
329            table_base: root,
330            vmin,
331            len: vmax.saturating_sub(vmin),
332            update_parent: UpdateParentNone {},
333        })
334        .filter_map(|r| unsafe { require_pte_exist(op, r) })
335        .flat_map(modify_ptes::<38, 30, Op, _>)
336        .filter_map(|r| unsafe { require_pte_exist(op, r) })
337        .flat_map(modify_ptes::<29, 21, Op, _>)
338        .filter_map(|r| unsafe { require_pte_exist(op, r) })
339        .flat_map(modify_ptes::<20, 12, Op, _>);
340
341        for r in iter {
342            let Some(pte) = (unsafe { read_pte_if_present(op, r.entry_ptr) }) else {
343                continue;
344            };
345            let phys_addr = pte & PTE_ADDR_MASK;
346            let sgn_bit = r.vmin >> (VA_BITS - 1);
347            let sgn_bits = 0u64.wrapping_sub(sgn_bit) << VA_BITS;
348            let virt_addr = sgn_bits | r.vmin;
349
350            let executable = (pte & PAGE_NX) == 0;
351            let avl = pte & PTE_AVL_MASK;
352            let kind = if avl == PAGE_AVL_COW {
353                MappingKind::Cow(CowMapping {
354                    readable: true,
355                    executable,
356                })
357            } else {
358                MappingKind::Basic(BasicMapping {
359                    readable: true,
360                    writable: (pte & PAGE_RW) != 0,
361                    executable,
362                })
363            };
364            mappings.push(crate::vmem::SpaceAwareMapping::ThisSpace(Mapping {
365                phys_base: phys_addr,
366                virt_base: virt_addr,
367                len: PAGE_SIZE as u64,
368                kind,
369            }));
370        }
371
372        out.push((root_id, mappings));
373    }
374
375    out
376}
377
378/// See [`walk_va_spaces`]: amd64 never emits `AnotherSpace`, so this
379/// is unreachable in practice. It silently no-ops (rather than
380/// panicking) to keep the architecture-independent re-export usable.
381#[allow(clippy::missing_safety_doc)]
382pub unsafe fn space_aware_map<Op: TableOps>(
383    _op: &Op,
384    _ref_map: crate::vmem::SpaceReferenceMapping,
385    _built_roots: &::alloc::collections::BTreeMap<crate::vmem::SpaceId, Op::TableAddr>,
386) {
387}
388
389#[allow(clippy::missing_safety_doc)]
390pub unsafe fn map<Op: TableOps>(op: &Op, mapping: Mapping) {
391    modify_ptes::<47, 39, Op, _>(MapRequest {
392        table_base: op.root_table(),
393        vmin: mapping.virt_base,
394        len: mapping.len,
395        update_parent: Op::TableMovability::root_update_parent(),
396    })
397    .map(|r| unsafe { alloc_pte_if_needed(op, r) })
398    .flat_map(modify_ptes::<38, 30, Op, _>)
399    .map(|r| unsafe { alloc_pte_if_needed(op, r) })
400    .flat_map(modify_ptes::<29, 21, Op, _>)
401    .map(|r| unsafe { alloc_pte_if_needed(op, r) })
402    .flat_map(modify_ptes::<20, 12, Op, _>)
403    .map(|r| unsafe { map_page(op, &mapping, r) })
404    .for_each(drop);
405}
406
407// There are no notable architecture-specific safety considerations
408// here, and the general conditions are documented in the
409// architecture-independent re-export in vmem.rs
410
411/// Translates a virtual address range to the physical address pages
412/// that back it by walking the page tables.
413///
414/// Returns an iterator with an entry for each mapped page that
415/// intersects the given range.
416///
417/// This takes AsRef<Op> + Copy so that on targets where the
418/// operations have little state (e.g. the guest) the operations state
419/// can be copied into the closure(s) in the iterator, allowing for a
420/// nicer result lifetime.  On targets like the
421/// building-an-original-snapshot portion of the host, where the
422/// operations structure owns a large buffer, a reference can instead
423/// be passed.
424#[allow(clippy::missing_safety_doc)]
425pub unsafe fn virt_to_phys<'a, Op: TableReadOps + 'a>(
426    op: impl core::convert::AsRef<Op> + Copy + 'a,
427    address: u64,
428    len: u64,
429) -> impl Iterator<Item = Mapping> + 'a {
430    // Undo sign-extension
431    let addr = address & ((1u64 << VA_BITS) - 1);
432    // Mask off any sub-page bits
433    let vmin = addr & !(PAGE_SIZE as u64 - 1);
434    // Calculate the maximum virtual address we need to look at based on the starting
435    // address and length ensuring we don't go past the end of the address space
436    let vmax = core::cmp::min(addr + len, 1u64 << VA_BITS);
437    modify_ptes::<47, 39, Op, _>(MapRequest {
438        table_base: op.as_ref().root_table(),
439        vmin,
440        len: vmax - vmin,
441        update_parent: UpdateParentNone {},
442    })
443    .filter_map(move |r| unsafe { require_pte_exist(op.as_ref(), r) })
444    .flat_map(modify_ptes::<38, 30, Op, _>)
445    .filter_map(move |r| unsafe { require_pte_exist(op.as_ref(), r) })
446    .flat_map(modify_ptes::<29, 21, Op, _>)
447    .filter_map(move |r| unsafe { require_pte_exist(op.as_ref(), r) })
448    .flat_map(modify_ptes::<20, 12, Op, _>)
449    .filter_map(move |r| {
450        let pte = unsafe { read_pte_if_present(op.as_ref(), r.entry_ptr) }?;
451        let phys_addr = pte & PTE_ADDR_MASK;
452        // Re-do the sign extension
453        let sgn_bit = r.vmin >> (VA_BITS - 1);
454        let sgn_bits = 0u64.wrapping_sub(sgn_bit) << VA_BITS;
455        let virt_addr = sgn_bits | r.vmin;
456
457        let executable = (pte & PAGE_NX) == 0;
458        let avl = pte & PTE_AVL_MASK;
459        let kind = if avl == PAGE_AVL_COW {
460            MappingKind::Cow(CowMapping {
461                readable: true,
462                executable,
463            })
464        } else {
465            MappingKind::Basic(BasicMapping {
466                readable: true,
467                writable: (pte & PAGE_RW) != 0,
468                executable,
469            })
470        };
471        Some(Mapping {
472            phys_base: phys_addr,
473            virt_base: virt_addr,
474            len: PAGE_SIZE as u64,
475            kind,
476        })
477    })
478}
479
480const VA_BITS: usize = 48; // We use 48-bit virtual addresses at the moment.
481
482pub const PAGE_SIZE: usize = 4096;
483pub const PAGE_TABLE_SIZE: usize = 4096;
484pub type PageTableEntry = u64;
485pub type VirtAddr = u64;
486pub type PhysAddr = u64;
487
488#[cfg(test)]
489mod tests {
490    use alloc::vec;
491    use alloc::vec::Vec;
492    use core::cell::RefCell;
493
494    use super::*;
495    use crate::vmem::{
496        BasicMapping, Mapping, MappingKind, MayNotMoveTable, PAGE_TABLE_ENTRIES_PER_TABLE,
497        TableOps, TableReadOps, Void, bits,
498    };
499
500    /// A mock TableOps implementation for testing that stores page tables in memory
501    /// needed because the `GuestPageTableBuffer` is in hyperlight_host which would cause a circular dependency
502    struct MockTableOps {
503        tables: RefCell<Vec<[u64; PAGE_TABLE_ENTRIES_PER_TABLE]>>,
504    }
505
506    // for virt_to_phys
507    impl core::convert::AsRef<MockTableOps> for MockTableOps {
508        fn as_ref(&self) -> &Self {
509            self
510        }
511    }
512
513    impl MockTableOps {
514        fn new() -> Self {
515            // Start with one table (the root/PML4)
516            Self {
517                tables: RefCell::new(vec![[0u64; PAGE_TABLE_ENTRIES_PER_TABLE]]),
518            }
519        }
520
521        fn table_count(&self) -> usize {
522            self.tables.borrow().len()
523        }
524
525        fn get_entry(&self, table_idx: usize, entry_idx: usize) -> u64 {
526            self.tables.borrow()[table_idx][entry_idx]
527        }
528    }
529
530    impl TableReadOps for MockTableOps {
531        type TableAddr = (usize, usize); // (table_index, entry_index)
532
533        fn entry_addr(addr: Self::TableAddr, entry_offset: u64) -> Self::TableAddr {
534            // Convert to physical address, add offset, convert back
535            let phys = Self::to_phys(addr) + entry_offset;
536            Self::from_phys(phys)
537        }
538
539        unsafe fn read_entry(&self, addr: Self::TableAddr) -> u64 {
540            self.tables.borrow()[addr.0][addr.1]
541        }
542
543        fn to_phys(addr: Self::TableAddr) -> PhysAddr {
544            // Each table is 4KB, entries are 8 bytes
545            (addr.0 as u64 * PAGE_TABLE_SIZE as u64) + (addr.1 as u64 * 8)
546        }
547
548        fn from_phys(addr: PhysAddr) -> Self::TableAddr {
549            let table_idx = (addr / PAGE_TABLE_SIZE as u64) as usize;
550            let entry_idx = ((addr % PAGE_TABLE_SIZE as u64) / 8) as usize;
551            (table_idx, entry_idx)
552        }
553
554        fn root_table(&self) -> Self::TableAddr {
555            (0, 0)
556        }
557    }
558
559    impl TableOps for MockTableOps {
560        type TableMovability = MayNotMoveTable;
561
562        unsafe fn alloc_table(&self) -> Self::TableAddr {
563            let mut tables = self.tables.borrow_mut();
564            let idx = tables.len();
565            tables.push([0u64; PAGE_TABLE_ENTRIES_PER_TABLE]);
566            (idx, 0)
567        }
568
569        unsafe fn write_entry(&self, addr: Self::TableAddr, entry: u64) -> Option<Void> {
570            self.tables.borrow_mut()[addr.0][addr.1] = entry;
571            None
572        }
573
574        unsafe fn update_root(&self, impossible: Void) {
575            match impossible {}
576        }
577    }
578
579    // ==================== bits() function tests ====================
580
581    #[test]
582    fn test_bits_extracts_pml4_index() {
583        // PML4 uses bits 47:39
584        // Address 0x0000_0080_0000_0000 should have PML4 index 1
585        let addr: u64 = 0x0000_0080_0000_0000;
586        assert_eq!(bits::<47, 39>(addr), 1);
587    }
588
589    #[test]
590    fn test_bits_extracts_pdpt_index() {
591        // PDPT uses bits 38:30
592        // Address with PDPT index 1: bit 30 set = 0x4000_0000 (1GB)
593        let addr: u64 = 0x4000_0000;
594        assert_eq!(bits::<38, 30>(addr), 1);
595    }
596
597    #[test]
598    fn test_bits_extracts_pd_index() {
599        // PD uses bits 29:21
600        // Address 0x0000_0000_0020_0000 (2MB) should have PD index 1
601        let addr: u64 = 0x0000_0000_0020_0000;
602        assert_eq!(bits::<29, 21>(addr), 1);
603    }
604
605    #[test]
606    fn test_bits_extracts_pt_index() {
607        // PT uses bits 20:12
608        // Address 0x0000_0000_0000_1000 (4KB) should have PT index 1
609        let addr: u64 = 0x0000_0000_0000_1000;
610        assert_eq!(bits::<20, 12>(addr), 1);
611    }
612
613    #[test]
614    fn test_bits_max_index() {
615        // Maximum 9-bit index is 511
616        // PML4 index 511 = bits 47:39 all set = 0x0000_FF80_0000_0000
617        let addr: u64 = 0x0000_FF80_0000_0000;
618        assert_eq!(bits::<47, 39>(addr), 511);
619    }
620
621    // ==================== PTE flag tests ====================
622
623    #[test]
624    fn test_page_rw_flag_writable() {
625        assert_eq!(page_rw_flag(true), PAGE_RW);
626    }
627
628    #[test]
629    fn test_page_rw_flag_readonly() {
630        assert_eq!(page_rw_flag(false), 0);
631    }
632
633    #[test]
634    fn test_page_nx_flag_executable() {
635        assert_eq!(page_nx_flag(true), 0); // Executable = no NX bit
636    }
637
638    #[test]
639    fn test_page_nx_flag_not_executable() {
640        assert_eq!(page_nx_flag(false), PAGE_NX);
641    }
642
643    // ==================== map() function tests ====================
644
645    #[test]
646    fn test_map_single_page() {
647        let ops = MockTableOps::new();
648        let mapping = Mapping {
649            phys_base: 0x1000,
650            virt_base: 0x1000,
651            len: PAGE_SIZE as u64,
652            kind: MappingKind::Basic(BasicMapping {
653                readable: true,
654                writable: true,
655                executable: false,
656            }),
657        };
658
659        unsafe { map(&ops, mapping) };
660
661        // Should have allocated: PML4(exists) + PDPT + PD + PT = 4 tables
662        assert_eq!(ops.table_count(), 4);
663
664        // Check PML4 entry 0 points to PDPT (table 1) with correct flags
665        let pml4_entry = ops.get_entry(0, 0);
666        assert_ne!(pml4_entry & PAGE_PRESENT, 0, "PML4 entry should be present");
667        assert_ne!(pml4_entry & PAGE_RW, 0, "PML4 entry should be writable");
668
669        // Check the leaf PTE has correct flags
670        // PT is table 3, entry 1 (for virt_base 0x1000)
671        let pte = ops.get_entry(3, 1);
672        assert_ne!(pte & PAGE_PRESENT, 0, "PTE should be present");
673        assert_ne!(pte & PAGE_RW, 0, "PTE should be writable");
674        assert_ne!(pte & PAGE_NX, 0, "PTE should have NX set (not executable)");
675        assert_eq!(pte & PTE_ADDR_MASK, 0x1000, "PTE should map to phys 0x1000");
676    }
677
678    #[test]
679    fn test_map_executable_page() {
680        let ops = MockTableOps::new();
681        let mapping = Mapping {
682            phys_base: 0x2000,
683            virt_base: 0x2000,
684            len: PAGE_SIZE as u64,
685            kind: MappingKind::Basic(BasicMapping {
686                readable: true,
687                writable: false,
688                executable: true,
689            }),
690        };
691
692        unsafe { map(&ops, mapping) };
693
694        // PT is table 3, entry 2 (for virt_base 0x2000)
695        let pte = ops.get_entry(3, 2);
696        assert_ne!(pte & PAGE_PRESENT, 0, "PTE should be present");
697        assert_eq!(pte & PAGE_RW, 0, "PTE should be read-only");
698        assert_eq!(pte & PAGE_NX, 0, "PTE should NOT have NX set (executable)");
699    }
700
701    #[test]
702    fn test_map_multiple_pages() {
703        let ops = MockTableOps::new();
704        let mapping = Mapping {
705            phys_base: 0x10000,
706            virt_base: 0x10000,
707            len: 4 * PAGE_SIZE as u64, // 4 pages = 16KB
708            kind: MappingKind::Basic(BasicMapping {
709                readable: true,
710                writable: true,
711                executable: false,
712            }),
713        };
714
715        unsafe { map(&ops, mapping) };
716
717        // Check all 4 PTEs are present
718        for i in 0..4 {
719            let entry_idx = 16 + i; // 0x10000 / 0x1000 = 16
720            let pte = ops.get_entry(3, entry_idx);
721            assert_ne!(pte & PAGE_PRESENT, 0, "PTE {} should be present", i);
722            let expected_phys = 0x10000 + (i as u64 * PAGE_SIZE as u64);
723            assert_eq!(
724                pte & PTE_ADDR_MASK,
725                expected_phys,
726                "PTE {} should map to correct phys addr",
727                i
728            );
729        }
730    }
731
732    #[test]
733    fn test_map_reuses_existing_tables() {
734        let ops = MockTableOps::new();
735
736        // Map first region
737        let mapping1 = Mapping {
738            phys_base: 0x1000,
739            virt_base: 0x1000,
740            len: PAGE_SIZE as u64,
741            kind: MappingKind::Basic(BasicMapping {
742                readable: true,
743                writable: true,
744                executable: false,
745            }),
746        };
747        unsafe { map(&ops, mapping1) };
748        let tables_after_first = ops.table_count();
749
750        // Map second region in same PT (different page)
751        let mapping2 = Mapping {
752            phys_base: 0x5000,
753            virt_base: 0x5000,
754            len: PAGE_SIZE as u64,
755            kind: MappingKind::Basic(BasicMapping {
756                readable: true,
757                writable: true,
758                executable: false,
759            }),
760        };
761        unsafe { map(&ops, mapping2) };
762
763        // Should NOT allocate new tables (reuses existing hierarchy)
764        assert_eq!(
765            ops.table_count(),
766            tables_after_first,
767            "Should reuse existing page tables"
768        );
769    }
770
771    // ==================== virt_to_phys() tests ====================
772
773    #[test]
774    fn test_virt_to_phys_mapped_address() {
775        let ops = MockTableOps::new();
776        let mapping = Mapping {
777            phys_base: 0x1000,
778            virt_base: 0x1000,
779            len: PAGE_SIZE as u64,
780            kind: MappingKind::Basic(BasicMapping {
781                readable: true,
782                writable: true,
783                executable: false,
784            }),
785        };
786
787        unsafe { map(&ops, mapping) };
788
789        let result = unsafe { virt_to_phys(&ops, 0x1000, 1).next() };
790        assert!(result.is_some(), "Should find mapped address");
791        let mapping = result.unwrap();
792        assert_eq!(mapping.phys_base, 0x1000);
793    }
794
795    #[test]
796    fn test_virt_to_phys_unaligned_virt() {
797        let ops = MockTableOps::new();
798        let mapping = Mapping {
799            phys_base: 0x1000,
800            virt_base: 0x1000,
801            len: PAGE_SIZE as u64,
802            kind: MappingKind::Basic(BasicMapping {
803                readable: true,
804                writable: true,
805                executable: false,
806            }),
807        };
808
809        unsafe { map(&ops, mapping) };
810
811        let result = unsafe { virt_to_phys(&ops, 0x1234, 1).next() };
812        assert!(result.is_some(), "Should find mapped address");
813        let mapping = result.unwrap();
814        assert_eq!(mapping.phys_base, 0x1000);
815    }
816
817    #[test]
818    fn test_virt_to_phys_unaligned_virt_and_across_pages_len() {
819        let ops = MockTableOps::new();
820        let mapping = Mapping {
821            phys_base: 0x1000,
822            virt_base: 0x1000,
823            len: 2 * PAGE_SIZE as u64, // 2 page
824            kind: MappingKind::Basic(BasicMapping {
825                readable: true,
826                writable: true,
827                executable: false,
828            }),
829        };
830
831        unsafe { map(&ops, mapping) };
832
833        let mappings = unsafe { virt_to_phys(&ops, 0x1F00, 0x300).collect::<Vec<_>>() };
834        assert_eq!(mappings.len(), 2, "Should return 2 mappings for 2 pages");
835        assert_eq!(mappings[0].phys_base, 0x1000);
836        assert_eq!(mappings[1].phys_base, 0x2000);
837    }
838
839    #[test]
840    fn test_virt_to_phys_unaligned_virt_and_multiple_page_len() {
841        let ops = MockTableOps::new();
842        let mapping = Mapping {
843            phys_base: 0x1000,
844            virt_base: 0x1000,
845            len: PAGE_SIZE as u64 * 2 + 0x200, // 2 page + 512 bytes
846            kind: MappingKind::Basic(BasicMapping {
847                readable: true,
848                writable: true,
849                executable: false,
850            }),
851        };
852
853        unsafe { map(&ops, mapping) };
854
855        let mappings =
856            unsafe { virt_to_phys(&ops, 0x1234, PAGE_SIZE as u64 * 2 + 0x10).collect::<Vec<_>>() };
857        assert_eq!(mappings.len(), 3, "Should return 3 mappings for 3 pages");
858        assert_eq!(mappings[0].phys_base, 0x1000);
859        assert_eq!(mappings[1].phys_base, 0x2000);
860        assert_eq!(mappings[2].phys_base, 0x3000);
861    }
862
863    #[test]
864    fn test_virt_to_phys_perms() {
865        let test = |kind| {
866            let ops = MockTableOps::new();
867            let mapping = Mapping {
868                phys_base: 0x1000,
869                virt_base: 0x1000,
870                len: PAGE_SIZE as u64,
871                kind,
872            };
873            unsafe { map(&ops, mapping) };
874            let result = unsafe { virt_to_phys(&ops, 0x1000, 1).next() };
875            let mapping = result.unwrap();
876            assert_eq!(mapping.kind, kind);
877        };
878        test(MappingKind::Basic(BasicMapping {
879            readable: true,
880            writable: false,
881            executable: false,
882        }));
883        test(MappingKind::Basic(BasicMapping {
884            readable: true,
885            writable: false,
886            executable: true,
887        }));
888        test(MappingKind::Basic(BasicMapping {
889            readable: true,
890            writable: true,
891            executable: false,
892        }));
893        test(MappingKind::Basic(BasicMapping {
894            readable: true,
895            writable: true,
896            executable: true,
897        }));
898        test(MappingKind::Cow(CowMapping {
899            readable: true,
900            executable: false,
901        }));
902        test(MappingKind::Cow(CowMapping {
903            readable: true,
904            executable: true,
905        }));
906    }
907
908    #[test]
909    fn test_virt_to_phys_unmapped_address() {
910        let ops = MockTableOps::new();
911        // Don't map anything
912
913        let result = unsafe { virt_to_phys(&ops, 0x1000, 1).next() };
914        assert!(result.is_none(), "Should return None for unmapped address");
915    }
916
917    #[test]
918    fn test_virt_to_phys_partially_mapped() {
919        let ops = MockTableOps::new();
920        let mapping = Mapping {
921            phys_base: 0x1000,
922            virt_base: 0x1000,
923            len: PAGE_SIZE as u64,
924            kind: MappingKind::Basic(BasicMapping {
925                readable: true,
926                writable: true,
927                executable: false,
928            }),
929        };
930
931        unsafe { map(&ops, mapping) };
932
933        // Query an address in a different PT entry (unmapped)
934        let result = unsafe { virt_to_phys(&ops, 0x5000, 1).next() };
935        assert!(
936            result.is_none(),
937            "Should return None for unmapped address in same PT"
938        );
939    }
940
941    // ==================== ModifyPteIterator tests ====================
942
943    #[test]
944    fn test_modify_pte_iterator_single_page() {
945        let ops = MockTableOps::new();
946        let request = MapRequest {
947            table_base: ops.root_table(),
948            vmin: 0x1000,
949            len: PAGE_SIZE as u64,
950            update_parent: UpdateParentNone {},
951        };
952
953        let responses: Vec<_> = modify_ptes::<20, 12, MockTableOps, _>(request).collect();
954        assert_eq!(responses.len(), 1, "Single page should yield one response");
955        assert_eq!(responses[0].vmin, 0x1000);
956        assert_eq!(responses[0].len, PAGE_SIZE as u64);
957    }
958
959    #[test]
960    fn test_modify_pte_iterator_multiple_pages() {
961        let ops = MockTableOps::new();
962        let request = MapRequest {
963            table_base: ops.root_table(),
964            vmin: 0x1000,
965            len: 3 * PAGE_SIZE as u64,
966            update_parent: UpdateParentNone {},
967        };
968
969        let responses: Vec<_> = modify_ptes::<20, 12, MockTableOps, _>(request).collect();
970        assert_eq!(responses.len(), 3, "3 pages should yield 3 responses");
971    }
972
973    #[test]
974    fn test_modify_pte_iterator_zero_length() {
975        let ops = MockTableOps::new();
976        let request = MapRequest {
977            table_base: ops.root_table(),
978            vmin: 0x1000,
979            len: 0,
980            update_parent: UpdateParentNone {},
981        };
982
983        let responses: Vec<_> = modify_ptes::<20, 12, MockTableOps, _>(request).collect();
984        assert_eq!(responses.len(), 0, "Zero length should yield no responses");
985    }
986
987    #[test]
988    fn test_modify_pte_iterator_unaligned_start() {
989        let ops = MockTableOps::new();
990        // Start at 0x1800 (mid-page), map 0x1000 bytes
991        // Should cover 0x1800-0x1FFF (first page) and 0x2000-0x27FF (second page)
992        let request = MapRequest {
993            table_base: ops.root_table(),
994            vmin: 0x1800,
995            len: 0x1000,
996            update_parent: UpdateParentNone {},
997        };
998
999        let responses: Vec<_> = modify_ptes::<20, 12, MockTableOps, _>(request).collect();
1000        assert_eq!(
1001            responses.len(),
1002            2,
1003            "Unaligned mapping spanning 2 pages should yield 2 responses"
1004        );
1005        assert_eq!(responses[0].vmin, 0x1800);
1006        assert_eq!(responses[0].len, 0x800); // Remaining in first page
1007        assert_eq!(responses[1].vmin, 0x2000);
1008        assert_eq!(responses[1].len, 0x800); // Continuing in second page
1009    }
1010
1011    // ==================== TableOps entry_addr tests ====================
1012
1013    #[test]
1014    fn test_entry_addr_from_table_base() {
1015        // entry_addr is called with a table base (entry_index = 0) and a byte offset
1016        // offset = entry_index * 8, so offset 40 means entry 5
1017        let result = MockTableOps::entry_addr((2, 0), 40);
1018        assert_eq!(result, (2, 5), "Should return (table 2, entry 5)");
1019    }
1020
1021    #[test]
1022    fn test_entry_addr_with_nonzero_base_entry() {
1023        // Even though entry_addr is typically called with entry_index=0,
1024        // it should handle non-zero base correctly by adding the offset
1025        // Base: table 1, entry 10 (phys = 1*4096 + 10*8 = 4176)
1026        // Offset: 16 bytes (2 entries)
1027        // Result phys: 4176 + 16 = 4192 = 1*4096 + 12*8 → (1, 12)
1028        let result = MockTableOps::entry_addr((1, 10), 16);
1029        assert_eq!(result, (1, 12), "Should add offset to base entry");
1030    }
1031
1032    #[test]
1033    fn test_to_phys_from_phys_roundtrip() {
1034        // Verify to_phys and from_phys are inverses
1035        let addr = (3, 42);
1036        let phys = MockTableOps::to_phys(addr);
1037        let back = MockTableOps::from_phys(phys);
1038        assert_eq!(back, addr, "to_phys/from_phys should roundtrip");
1039    }
1040}