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