Skip to main content

hyperlight_host/mem/
mgr.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
17use flatbuffers::FlatBufferBuilder;
18use hyperlight_common::flatbuffer_wrappers::function_call::{
19    FunctionCall, validate_guest_function_call_buffer,
20};
21use hyperlight_common::flatbuffer_wrappers::function_types::FunctionCallResult;
22use hyperlight_common::flatbuffer_wrappers::guest_log_data::GuestLogData;
23use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails;
24use hyperlight_common::vmem::{self, PAGE_TABLE_SIZE};
25#[cfg(crashdump)]
26use hyperlight_common::vmem::{BasicMapping, MappingKind};
27use tracing::{Span, instrument};
28
29use super::layout::SandboxMemoryLayout;
30use super::shared_mem::{
31    ExclusiveSharedMemory, GuestSharedMemory, HostSharedMemory, ReadonlySharedMemory, SharedMemory,
32};
33use crate::hypervisor::regs::CommonSpecialRegisters;
34use crate::mem::memory_region::MemoryRegion;
35#[cfg(crashdump)]
36use crate::mem::memory_region::{CrashDumpRegion, MemoryRegionFlags, MemoryRegionType};
37use crate::sandbox::snapshot::{NextAction, Snapshot};
38use crate::{Result, new_error};
39
40#[cfg(crashdump)]
41fn mapping_kind_to_flags(kind: &MappingKind) -> (MemoryRegionFlags, MemoryRegionType) {
42    match kind {
43        MappingKind::Basic(BasicMapping {
44            readable,
45            writable,
46            executable,
47        }) => {
48            let mut flags = MemoryRegionFlags::empty();
49            if *readable {
50                flags |= MemoryRegionFlags::READ;
51            }
52            if *writable {
53                flags |= MemoryRegionFlags::WRITE;
54            }
55            if *executable {
56                flags |= MemoryRegionFlags::EXECUTE;
57            }
58            (flags, MemoryRegionType::Snapshot)
59        }
60        MappingKind::Cow(cow) => {
61            let mut flags = MemoryRegionFlags::empty();
62            if cow.readable {
63                flags |= MemoryRegionFlags::READ;
64            }
65            if cow.executable {
66                flags |= MemoryRegionFlags::EXECUTE;
67            }
68            (flags, MemoryRegionType::Scratch)
69        }
70        MappingKind::Unmapped => (MemoryRegionFlags::empty(), MemoryRegionType::Snapshot),
71    }
72}
73
74/// Try to extend the last region in `regions` if the new page is contiguous
75/// in both guest and host address space and has the same flags.
76///
77/// Returns `true` if the region was coalesced, `false` if a new region is needed.
78#[cfg(crashdump)]
79fn try_coalesce_region(
80    regions: &mut [CrashDumpRegion],
81    virt_base: usize,
82    virt_end: usize,
83    host_base: usize,
84    flags: MemoryRegionFlags,
85) -> bool {
86    if let Some(last) = regions.last_mut()
87        && last.guest_region.end == virt_base
88        && last.host_region.end == host_base
89        && last.flags == flags
90    {
91        last.guest_region.end = virt_end;
92        last.host_region.end = host_base + (virt_end - virt_base);
93        return true;
94    }
95    false
96}
97
98// It would be nice to have a simple type alias
99// `SnapshotSharedMemory<S: SharedMemory>` that abstracts over the
100// fact that the snapshot shared memory is `ReadonlySharedMemory`
101// normally, but there is (temporary) support for writable
102// `GuestSharedMemory` with `#[cfg(gdb)]`. Unfortunately, rustc gets
103// annoyed about an unused type parameter, unless one goes to a little
104// bit of effort to trick it...
105mod unused_hack {
106    #[cfg(not(unshared_snapshot_mem))]
107    use crate::mem::shared_mem::ReadonlySharedMemory;
108    use crate::mem::shared_mem::SharedMemory;
109    pub trait SnapshotSharedMemoryT {
110        type T<S: SharedMemory>;
111    }
112    pub struct SnapshotSharedMemory_;
113    impl SnapshotSharedMemoryT for SnapshotSharedMemory_ {
114        #[cfg(not(unshared_snapshot_mem))]
115        type T<S: SharedMemory> = ReadonlySharedMemory;
116        #[cfg(unshared_snapshot_mem)]
117        type T<S: SharedMemory> = S;
118    }
119    pub type SnapshotSharedMemory<S> = <SnapshotSharedMemory_ as SnapshotSharedMemoryT>::T<S>;
120}
121impl ReadonlySharedMemory {
122    pub(crate) fn to_mgr_snapshot_mem(
123        &self,
124    ) -> Result<SnapshotSharedMemory<ExclusiveSharedMemory>> {
125        #[cfg(not(unshared_snapshot_mem))]
126        let ret = self.clone();
127        #[cfg(unshared_snapshot_mem)]
128        let ret = self.copy_to_writable()?;
129        Ok(ret)
130    }
131}
132pub(crate) use unused_hack::SnapshotSharedMemory;
133/// A struct that is responsible for laying out and managing the memory
134/// for a given `Sandbox`.
135#[derive(Clone)]
136pub(crate) struct SandboxMemoryManager<S: SharedMemory> {
137    /// Shared memory for the Sandbox
138    pub(crate) shared_mem: SnapshotSharedMemory<S>,
139    /// Scratch memory for the Sandbox
140    pub(crate) scratch_mem: S,
141    /// The memory layout of the underlying shared memory
142    pub(crate) layout: SandboxMemoryLayout,
143    /// Offset for the execution entrypoint from `load_addr`
144    pub(crate) entrypoint: NextAction,
145    /// Buffer for accumulating guest abort messages
146    pub(crate) abort_buffer: Vec<u8>,
147    /// Generation counter: how many snapshots have been taken from
148    /// this sandbox's execution path from init to here. Incremented
149    /// on each `snapshot` call; on `restore_snapshot` we inherit the
150    /// restored snapshot's own generation number so the guest-visible
151    /// counter tracks which snapshot the sandbox is a clone of.
152    pub(crate) snapshot_count: u64,
153}
154
155/// Buffer for building guest page tables during snapshot creation.
156/// `TableAddr` is an absolute GPA (u64) so the same address space is
157/// used regardless of entry size.
158pub(crate) struct GuestPageTableBuffer {
159    buffer: std::cell::RefCell<Vec<u8>>,
160    phys_base: usize,
161    /// Absolute GPA of the currently-active root table. For
162    /// multi-root guests, `set_root` switches which root subsequent
163    /// `vmem::map` / `vmem::space_aware_map` calls target — typically
164    /// to an address previously returned by `alloc_table`.
165    root: std::cell::Cell<u64>,
166}
167
168impl vmem::TableReadOps for GuestPageTableBuffer {
169    type TableAddr = u64;
170
171    fn entry_addr(addr: u64, offset: u64) -> u64 {
172        addr + offset
173    }
174
175    unsafe fn read_entry(&self, addr: u64) -> vmem::PageTableEntry {
176        let buffer = self.buffer.borrow();
177        let byte_offset = addr as usize - self.phys_base;
178        let pte_size = core::mem::size_of::<vmem::PageTableEntry>();
179        let Some(bytes) = buffer.get(byte_offset..byte_offset + pte_size) else {
180            return 0;
181        };
182        let mut buf = [0u8; 8];
183        buf[..pte_size].copy_from_slice(bytes);
184        vmem::PageTableEntry::from_le_bytes(buf[..pte_size].try_into().unwrap_or_default())
185    }
186
187    fn to_phys(addr: u64) -> vmem::PhysAddr {
188        addr as vmem::PhysAddr
189    }
190
191    fn from_phys(addr: vmem::PhysAddr) -> u64 {
192        #[allow(clippy::unnecessary_cast)]
193        {
194            addr as u64
195        }
196    }
197
198    fn root_table(&self) -> u64 {
199        self.root.get()
200    }
201}
202
203impl vmem::TableOps for GuestPageTableBuffer {
204    type TableMovability = vmem::MayNotMoveTable;
205
206    unsafe fn alloc_table(&self) -> u64 {
207        let mut b = self.buffer.borrow_mut();
208        let offset = b.len();
209        b.resize(offset + PAGE_TABLE_SIZE, 0);
210        (self.phys_base + offset) as u64
211    }
212
213    unsafe fn write_entry(&self, addr: u64, entry: vmem::PageTableEntry) -> Option<vmem::Void> {
214        let mut b = self.buffer.borrow_mut();
215        let byte_offset = addr as usize - self.phys_base;
216        let pte_size = core::mem::size_of::<vmem::PageTableEntry>();
217        if let Some(slice) = b.get_mut(byte_offset..byte_offset + pte_size) {
218            slice.copy_from_slice(&entry.to_le_bytes()[..pte_size]);
219        }
220        None
221    }
222
223    unsafe fn update_root(&self, impossible: vmem::Void) {
224        match impossible {}
225    }
226}
227
228impl core::convert::AsRef<GuestPageTableBuffer> for GuestPageTableBuffer {
229    fn as_ref(&self) -> &Self {
230        self
231    }
232}
233
234impl GuestPageTableBuffer {
235    /// Create a new buffer with an initial zeroed root table at
236    /// `phys_base`. The returned buffer's current root is `phys_base`;
237    /// additional roots can be obtained by calling `alloc_table`.
238    pub(crate) fn new(phys_base: usize) -> Self {
239        GuestPageTableBuffer {
240            buffer: std::cell::RefCell::new(vec![0u8; PAGE_TABLE_SIZE]),
241            phys_base,
242            root: std::cell::Cell::new(phys_base as u64),
243        }
244    }
245
246    /// Switch the active root. `addr` must have been obtained either
247    /// as the initial root GPA (`phys_base`) or via `alloc_table`.
248    pub(crate) fn set_root(&self, addr: u64) {
249        self.root.set(addr);
250    }
251
252    /// GPA of the initial root allocated by `new`.
253    pub(crate) fn initial_root(&self) -> u64 {
254        self.phys_base as u64
255    }
256
257    #[cfg(test)]
258    #[allow(dead_code)]
259    pub(crate) fn size(&self) -> usize {
260        self.buffer.borrow().len()
261    }
262
263    pub(crate) fn into_bytes(self) -> Box<[u8]> {
264        self.buffer.into_inner().into_boxed_slice()
265    }
266}
267
268impl<S> SandboxMemoryManager<S>
269where
270    S: SharedMemory,
271{
272    /// Create a new `SandboxMemoryManager` with the given parameters
273    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
274    pub(crate) fn new(
275        layout: SandboxMemoryLayout,
276        shared_mem: SnapshotSharedMemory<S>,
277        scratch_mem: S,
278        entrypoint: NextAction,
279    ) -> Self {
280        Self {
281            layout,
282            shared_mem,
283            scratch_mem,
284            entrypoint,
285            abort_buffer: Vec::new(),
286            snapshot_count: 0,
287        }
288    }
289
290    /// Get mutable access to the abort buffer
291    pub(crate) fn get_abort_buffer_mut(&mut self) -> &mut Vec<u8> {
292        &mut self.abort_buffer
293    }
294
295    /// Create a snapshot with the given mapped regions
296    #[allow(clippy::too_many_arguments)]
297    pub(crate) fn snapshot(
298        &mut self,
299        mapped_regions: Vec<MemoryRegion>,
300        root_pt_gpas: &[u64],
301        rsp_gva: u64,
302        sregs: CommonSpecialRegisters,
303        entrypoint: NextAction,
304        host_functions: HostFunctionDetails,
305    ) -> Result<Snapshot> {
306        self.snapshot_count += 1;
307        Snapshot::new(
308            &mut self.shared_mem,
309            &mut self.scratch_mem,
310            self.layout,
311            crate::mem::exe::LoadInfo::dummy(),
312            mapped_regions,
313            root_pt_gpas,
314            rsp_gva,
315            sregs,
316            entrypoint,
317            self.snapshot_count,
318            host_functions,
319        )
320    }
321}
322
323impl SandboxMemoryManager<ExclusiveSharedMemory> {
324    pub(crate) fn from_snapshot(s: &Snapshot) -> Result<Self> {
325        let layout = *s.layout();
326        let shared_mem = s.memory().to_mgr_snapshot_mem()?;
327        let scratch_mem = ExclusiveSharedMemory::new(s.layout().get_scratch_size())?;
328        let entrypoint = s.entrypoint();
329        let mut mgr = Self::new(layout, shared_mem, scratch_mem, entrypoint);
330        // Inherit the snapshot's generation number for the same
331        // reason `restore_snapshot` does: the guest-visible counter
332        // reflects "which snapshot is the sandbox currently a clone
333        // of", not "how many snapshots this partition has taken".
334        mgr.snapshot_count = s.snapshot_generation();
335        Ok(mgr)
336    }
337
338    /// Wraps ExclusiveSharedMemory::build
339    // Morally, this should not have to be a Result: this operation is
340    // infallible. The source of the Result is
341    // update_scratch_bookkeeping(), which calls functions that can
342    // fail due to bounds checks (which are statically known to be ok
343    // in this situation) or due to failing to take the scratch shared
344    // memory lock, but the scratch shared memory is built in this
345    // function, its lock does not escape before the end of the
346    // function, and the lock is taken by no other code path, so we
347    // know it is not contended.
348    pub fn build(
349        self,
350    ) -> Result<(
351        SandboxMemoryManager<HostSharedMemory>,
352        SandboxMemoryManager<GuestSharedMemory>,
353    )> {
354        let (hshm, gshm) = self.shared_mem.build();
355        let (hscratch, gscratch) = self.scratch_mem.build();
356        let mut host_mgr = SandboxMemoryManager {
357            shared_mem: hshm,
358            scratch_mem: hscratch,
359            layout: self.layout,
360            entrypoint: self.entrypoint,
361            abort_buffer: self.abort_buffer,
362            snapshot_count: self.snapshot_count,
363        };
364        let guest_mgr = SandboxMemoryManager {
365            shared_mem: gshm,
366            scratch_mem: gscratch,
367            layout: self.layout,
368            entrypoint: self.entrypoint,
369            abort_buffer: Vec::new(), // Guest doesn't need abort buffer
370            snapshot_count: self.snapshot_count,
371        };
372        host_mgr.update_scratch_bookkeeping()?;
373        Ok((host_mgr, guest_mgr))
374    }
375}
376
377impl SandboxMemoryManager<HostSharedMemory> {
378    /// Reads a host function call from memory
379    #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
380    pub(crate) fn get_host_function_call(&mut self) -> Result<FunctionCall> {
381        self.scratch_mem.try_pop_buffer_into::<FunctionCall>(
382            self.layout.get_output_data_buffer_scratch_host_offset(),
383            self.layout.output_data_size,
384        )
385    }
386
387    /// Writes a host function call result to memory
388    #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
389    pub(crate) fn write_response_from_host_function_call(
390        &mut self,
391        res: &FunctionCallResult,
392    ) -> Result<()> {
393        let mut builder = FlatBufferBuilder::new();
394        let data = res.encode(&mut builder);
395
396        self.scratch_mem.push_buffer(
397            self.layout.get_input_data_buffer_scratch_host_offset(),
398            self.layout.input_data_size,
399            data,
400        )
401    }
402
403    /// Writes a guest function call to memory
404    #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
405    pub(crate) fn write_guest_function_call(&mut self, buffer: &[u8]) -> Result<()> {
406        validate_guest_function_call_buffer(buffer).map_err(|e| {
407            new_error!(
408                "Guest function call buffer validation failed: {}",
409                e.to_string()
410            )
411        })?;
412
413        self.scratch_mem.push_buffer(
414            self.layout.get_input_data_buffer_scratch_host_offset(),
415            self.layout.input_data_size,
416            buffer,
417        )?;
418        Ok(())
419    }
420
421    /// Reads a function call result from memory.
422    /// A function call result can be either an error or a successful return value.
423    #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
424    pub(crate) fn get_guest_function_call_result(&mut self) -> Result<FunctionCallResult> {
425        self.scratch_mem.try_pop_buffer_into::<FunctionCallResult>(
426            self.layout.get_output_data_buffer_scratch_host_offset(),
427            self.layout.output_data_size,
428        )
429    }
430
431    /// Read guest log data from the `SharedMemory` contained within `self`
432    #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
433    pub(crate) fn read_guest_log_data(&mut self) -> Result<GuestLogData> {
434        self.scratch_mem.try_pop_buffer_into::<GuestLogData>(
435            self.layout.get_output_data_buffer_scratch_host_offset(),
436            self.layout.output_data_size,
437        )
438    }
439
440    pub(crate) fn clear_io_buffers(&mut self) {
441        // Clear the output data buffer
442        loop {
443            let Ok(_) = self.scratch_mem.try_pop_buffer_into::<Vec<u8>>(
444                self.layout.get_output_data_buffer_scratch_host_offset(),
445                self.layout.output_data_size,
446            ) else {
447                break;
448            };
449        }
450        // Clear the input data buffer
451        loop {
452            let Ok(_) = self.scratch_mem.try_pop_buffer_into::<Vec<u8>>(
453                self.layout.get_input_data_buffer_scratch_host_offset(),
454                self.layout.input_data_size,
455            ) else {
456                break;
457            };
458        }
459    }
460
461    /// This function restores a memory snapshot from a given snapshot.
462    pub(crate) fn restore_snapshot(
463        &mut self,
464        snapshot: &Snapshot,
465    ) -> Result<(
466        Option<SnapshotSharedMemory<GuestSharedMemory>>,
467        Option<GuestSharedMemory>,
468    )> {
469        let gsnapshot = if *snapshot.memory() == self.shared_mem {
470            // If the snapshot memory is already the correct memory,
471            // which is readonly, don't bother with restoring it,
472            // since its contents must be the same.  Note that in the
473            // #[cfg(unshared_snapshot_mem)] case, this condition will
474            // never be true, since even immediately after a restore,
475            // self.shared_mem is a (writable) copy, not the original
476            // shared_mem.
477            None
478        } else {
479            let new_snapshot_mem = snapshot.memory().to_mgr_snapshot_mem()?;
480            let (hsnapshot, gsnapshot) = new_snapshot_mem.build();
481            self.shared_mem = hsnapshot;
482            Some(gsnapshot)
483        };
484        let new_scratch_size = snapshot.layout().get_scratch_size();
485        let gscratch = if new_scratch_size == self.scratch_mem.mem_size() {
486            self.scratch_mem.zero()?;
487            None
488        } else {
489            let new_scratch_mem = ExclusiveSharedMemory::new(new_scratch_size)?;
490            let (hscratch, gscratch) = new_scratch_mem.build();
491            // Even though this destroys the reference to the host
492            // side of the old scratch mapping, the VM should still
493            // own the reference to the guest side of the old scratch
494            // mapping, so it won't actually be deallocated until it
495            // has been unmapped from the VM.
496            self.scratch_mem = hscratch;
497
498            Some(gscratch)
499        };
500        self.layout = *snapshot.layout();
501        // Inherit the snapshot's own generation number — the
502        // guest-visible counter reflects "which snapshot is the
503        // sandbox currently a clone of", not "how many restores have
504        // happened into this (possibly-reused) partition".
505        self.snapshot_count = snapshot.snapshot_generation();
506
507        self.update_scratch_bookkeeping()?;
508        Ok((gsnapshot, gscratch))
509    }
510
511    #[inline]
512    fn update_scratch_bookkeeping_item(&mut self, offset: u64, value: u64) -> Result<()> {
513        let scratch_size = self.scratch_mem.mem_size();
514        let base_offset = scratch_size - offset as usize;
515        self.scratch_mem.write::<u64>(base_offset, value)
516    }
517
518    fn update_scratch_bookkeeping(&mut self) -> Result<()> {
519        use hyperlight_common::layout::*;
520        let scratch_size = self.scratch_mem.mem_size();
521        self.update_scratch_bookkeeping_item(SCRATCH_TOP_SIZE_OFFSET, scratch_size as u64)?;
522        self.update_scratch_bookkeeping_item(
523            SCRATCH_TOP_ALLOCATOR_OFFSET,
524            self.layout.get_first_free_scratch_gpa(),
525        )?;
526        // Record the GPA of the snapshot's copy of the page tables.
527        // The copy lives at the tail of the snapshot blob; we copy it
528        // into scratch below so the guest walker can run against
529        // mutable, TLB-fresh tables. The guest reads this GPA during
530        // CoW fault-in to follow the original PTs on the first write
531        // — until the HV can execute directly out of the
532        // snapshot-resident PTs, at which point the whole split goes
533        // away.
534        self.update_scratch_bookkeeping_item(
535            SCRATCH_TOP_SNAPSHOT_PT_GPA_BASE_OFFSET,
536            self.layout.get_pt_base_gpa(),
537        )?;
538        self.update_scratch_bookkeeping_item(
539            SCRATCH_TOP_SNAPSHOT_GENERATION_OFFSET,
540            self.snapshot_count,
541        )?;
542
543        // Initialise the guest input and output data buffers in
544        // scratch memory. TODO: remove the need for this.
545        self.scratch_mem.write::<u64>(
546            self.layout.get_input_data_buffer_scratch_host_offset(),
547            SandboxMemoryLayout::STACK_POINTER_SIZE_BYTES,
548        )?;
549        self.scratch_mem.write::<u64>(
550            self.layout.get_output_data_buffer_scratch_host_offset(),
551            SandboxMemoryLayout::STACK_POINTER_SIZE_BYTES,
552        )?;
553
554        // Copy page tables from `shared_mem` into scratch. PT bytes
555        // are appended to the snapshot blob at build time and live
556        // just past the end of the guest-visible KVM slot (see
557        // `Snapshot::new`). Keeping them outside the KVM slot avoids
558        // overlapping with `map_file_cow` regions installed
559        // immediately after the snapshot in the guest PA space.
560        let snapshot_pt_end = self.shared_mem.mem_size();
561        let snapshot_pt_size = self.layout.get_pt_size();
562        let snapshot_pt_start = snapshot_pt_end - snapshot_pt_size;
563        self.scratch_mem.with_exclusivity(|scratch| {
564            #[cfg(not(unshared_snapshot_mem))]
565            let bytes = &self.shared_mem.as_slice()[snapshot_pt_start..snapshot_pt_end];
566            #[cfg(unshared_snapshot_mem)]
567            let bytes = {
568                let mut bytes = vec![0u8; snapshot_pt_size];
569                self.shared_mem
570                    .copy_to_slice(&mut bytes, snapshot_pt_start)?;
571                bytes
572            };
573            #[allow(clippy::needless_borrow)]
574            scratch.copy_from_slice(&bytes, self.layout.get_pt_base_scratch_offset())
575        })??;
576
577        Ok(())
578    }
579
580    /// Build the list of guest memory regions for a crash dump.
581    ///
582    /// By default, walks the guest page tables to discover
583    /// GVA→GPA mappings and translates them to host-backed regions.
584    #[cfg(crashdump)]
585    pub(crate) fn get_guest_memory_regions(
586        &mut self,
587        root_pt: u64,
588        mmap_regions: &[MemoryRegion],
589    ) -> Result<Vec<CrashDumpRegion>> {
590        use crate::sandbox::snapshot::SharedMemoryPageTableBuffer;
591
592        let len = hyperlight_common::layout::SCRATCH_TOP_GVA;
593
594        let regions = self.shared_mem.with_contents(|snapshot| {
595            self.scratch_mem.with_contents(|scratch| {
596                let pt_buf =
597                    SharedMemoryPageTableBuffer::new(snapshot, scratch, self.layout, root_pt);
598
599                let mappings: Vec<_> =
600                    unsafe { hyperlight_common::vmem::virt_to_phys(&pt_buf, 0, len as u64) }
601                        .collect();
602
603                if mappings.is_empty() {
604                    return Err(new_error!("No page table mappings found (len {len})",));
605                }
606
607                let mut regions: Vec<CrashDumpRegion> = Vec::new();
608                for mapping in &mappings {
609                    let virt_base = mapping.virt_base as usize;
610                    let virt_end = (mapping.virt_base + mapping.len) as usize;
611
612                    if let Some(resolved) = self.layout.resolve_gpa(mapping.phys_base, mmap_regions)
613                    {
614                        let (flags, region_type) = mapping_kind_to_flags(&mapping.kind);
615                        let resolved = resolved.with_memories(snapshot, scratch);
616                        let contents = resolved.as_ref();
617                        let host_base = contents.as_ptr() as usize;
618                        let host_len = (mapping.len as usize).min(contents.len());
619
620                        if try_coalesce_region(&mut regions, virt_base, virt_end, host_base, flags)
621                        {
622                            continue;
623                        }
624
625                        regions.push(CrashDumpRegion {
626                            guest_region: virt_base..virt_end,
627                            host_region: host_base..host_base + host_len,
628                            flags,
629                            region_type,
630                        });
631                    }
632                }
633
634                Ok(regions)
635            })
636        })???;
637
638        Ok(regions)
639    }
640
641    /// Read guest memory at a Guest Virtual Address (GVA) by walking the
642    /// page tables to translate GVA → GPA, then reading from the correct
643    /// backing memory (shared_mem or scratch_mem).
644    ///
645    /// This is necessary because with Copy-on-Write (CoW) the guest's
646    /// virtual pages are backed by physical pages in the scratch
647    /// region rather than being identity-mapped.
648    ///
649    /// # Arguments
650    /// * `gva` - The Guest Virtual Address to read from
651    /// * `len` - The number of bytes to read
652    /// * `root_pt` - The root page table physical address (CR3)
653    #[cfg(feature = "trace_guest")]
654    pub(crate) fn read_guest_memory_by_gva(
655        &mut self,
656        gva: u64,
657        len: usize,
658        root_pt: u64,
659    ) -> Result<Vec<u8>> {
660        use hyperlight_common::vmem::PAGE_SIZE;
661
662        use crate::sandbox::snapshot::{SharedMemoryPageTableBuffer, access_gpa};
663
664        self.shared_mem.with_contents(|snap| {
665            self.scratch_mem.with_contents(|scratch| {
666                let pt_buf = SharedMemoryPageTableBuffer::new(snap, scratch, self.layout, root_pt);
667
668                // Walk page tables to get all mappings that cover the GVA range
669                let mappings: Vec<_> = unsafe {
670                    hyperlight_common::vmem::virt_to_phys(&pt_buf, gva, len as u64)
671                }
672                .collect();
673
674                if mappings.is_empty() {
675                    return Err(new_error!(
676                        "No page table mappings found for GVA {:#x} (len {})",
677                        gva,
678                        len,
679                    ));
680                }
681
682                // Resulting vector of bytes to return
683                let mut result = Vec::with_capacity(len);
684                let mut current_gva = gva;
685
686                for mapping in &mappings {
687                    // The page table walker should only return valid mappings
688                    // that cover our current read position.
689                    if mapping.virt_base > current_gva {
690                        return Err(new_error!(
691                            "Page table walker returned mapping with virt_base {:#x} > current read position {:#x}",
692                            mapping.virt_base,
693                            current_gva,
694                        ));
695                    }
696
697                    // Calculate the offset within this page where to start copying
698                    let page_offset = (current_gva - mapping.virt_base) as usize;
699
700                    let bytes_remaining = len - result.len();
701                    let available_in_page = PAGE_SIZE - page_offset;
702                    let bytes_to_copy = bytes_remaining.min(available_in_page);
703
704                    // Translate the GPA to host memory
705                    let gpa = mapping.phys_base + page_offset as u64;
706                    let (mem, offset) = access_gpa(snap, scratch, self.layout, gpa)
707                        .ok_or_else(|| {
708                            new_error!(
709                                "Failed to resolve GPA {:#x} to host memory (GVA {:#x})",
710                                gpa,
711                                gva
712                            )
713                        })?;
714
715                    let slice = mem
716                        .get(offset..offset + bytes_to_copy)
717                        .ok_or_else(|| {
718                            new_error!(
719                                "GPA {:#x} resolved to out-of-bounds host offset {} (need {} bytes)",
720                                gpa,
721                                offset,
722                                bytes_to_copy
723                            )
724                        })?;
725
726                    result.extend_from_slice(slice);
727                    current_gva += bytes_to_copy as u64;
728                }
729
730                if result.len() != len {
731                    tracing::error!(
732                        "Page table walker returned mappings that don't cover the full requested length: got {}, expected {}",
733                        result.len(),
734                        len,
735                    );
736                    return Err(new_error!(
737                        "Could not read full GVA range: got {} of {} bytes {:?}",
738                        result.len(),
739                        len,
740                        mappings
741                    ));
742                }
743
744                Ok(result)
745            })
746        })??
747    }
748}
749
750#[cfg(test)]
751#[cfg(target_arch = "x86_64")]
752mod tests {
753    use hyperlight_testing::sandbox_sizes::{LARGE_HEAP_SIZE, MEDIUM_HEAP_SIZE, SMALL_HEAP_SIZE};
754    use hyperlight_testing::simple_guest_as_string;
755
756    use crate::GuestBinary;
757    use crate::sandbox::SandboxConfiguration;
758    use crate::sandbox::snapshot::Snapshot;
759
760    /// Build a Snapshot for the given configuration and verify the
761    /// NULL page is not mapped in its page tables.
762    fn verify_page_tables(name: &str, config: SandboxConfiguration) {
763        let path = simple_guest_as_string().expect("failed to get simple guest path");
764        let snapshot = Snapshot::from_env(GuestBinary::FilePath(path), config)
765            .unwrap_or_else(|e| panic!("{}: failed to create snapshot: {}", name, e));
766
767        // Verify NULL page (0x0) is NOT mapped
768        assert!(
769            unsafe { hyperlight_common::vmem::virt_to_phys(&snapshot, 0, 1) }
770                .next()
771                .is_none(),
772            "{}: NULL page (0x0) should NOT be mapped",
773            name
774        );
775    }
776
777    #[test]
778    fn test_page_tables_for_various_configurations() {
779        let test_cases: [(&str, SandboxConfiguration); 4] = [
780            ("default", { SandboxConfiguration::default() }),
781            ("small (8MB heap)", {
782                let mut cfg = SandboxConfiguration::default();
783                cfg.set_heap_size(SMALL_HEAP_SIZE);
784                cfg
785            }),
786            ("medium (64MB heap)", {
787                let mut cfg = SandboxConfiguration::default();
788                cfg.set_heap_size(MEDIUM_HEAP_SIZE);
789                cfg
790            }),
791            ("large (256MB heap)", {
792                let mut cfg = SandboxConfiguration::default();
793                cfg.set_heap_size(LARGE_HEAP_SIZE);
794                cfg.set_scratch_size(0x100000);
795                cfg
796            }),
797        ];
798
799        for (name, config) in test_cases {
800            verify_page_tables(name, config);
801        }
802    }
803}