Skip to main content

hyperlight_common/arch/amd64/
vmem.rs

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