Skip to main content

hyperlight_host/mem/
mgr.rs

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