Skip to main content

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