Skip to main content

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