Skip to main content

hyperlight_host/sandbox/snapshot/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4mod file;
5mod file_tests;
6mod tripwires;
7
8use std::collections::{BTreeMap, HashMap};
9
10pub(crate) use file::host_cpu_vendor_golden_tag;
11pub use file::reference::{OciDigest, OciReference, OciTag};
12use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails;
13use hyperlight_common::layout::{io_page, scratch_base_gpa, scratch_base_gva};
14use hyperlight_common::vmem;
15use hyperlight_common::vmem::{
16    BasicMapping, CowMapping, Mapping, MappingKind, PAGE_SIZE, SpaceAwareMapping, SpaceId, TableOps,
17};
18use tracing::{Span, instrument};
19
20use crate::Result;
21use crate::hypervisor::regs::CommonSpecialRegisters;
22#[cfg(target_arch = "x86_64")]
23use crate::hypervisor::regs::MsrEntry;
24use crate::mem::exe::{ExeInfo, LoadInfo};
25use crate::mem::layout::SandboxMemoryLayout;
26use crate::mem::memory_region::{GuestMemoryRegion, MemoryRegion, MemoryRegionFlags};
27use crate::mem::mgr::{GuestPageTableBuffer, SnapshotSharedMemory};
28use crate::mem::shared_mem::{ReadonlySharedMemory, SharedMemory};
29use crate::sandbox::SandboxConfiguration;
30use crate::sandbox::uninitialized::{GuestBinary, GuestEnvironment};
31
32const PTE_SIZE: usize = size_of::<vmem::PageTableEntry>();
33
34/// Presently, a snapshot can be of a preinitialised sandbox, which
35/// still needs an initialise function called in order to determine
36/// how to call into it, or of an already-properly-initialised sandbox
37/// which can be immediately called into. This keeps track of the
38/// difference.
39///
40/// TODO: this should not necessarily be around in the long term:
41/// ideally we would just preinitialise earlier in the snapshot
42/// creation process and never need this.
43#[derive(Copy, Clone, PartialEq, Eq)]
44pub enum NextAction {
45    /// A sandbox in the preinitialise state still needs to be
46    /// initialised by calling the initialise function
47    Initialise(u64),
48    /// A sandbox in the ready state can immediately be called into,
49    /// using the dispatch function pointer.
50    Call(u64),
51    /// Only when compiling for tests: a sandbox that cannot actually
52    /// be used
53    #[cfg(test)]
54    None,
55}
56
57/// A wrapper around a `SharedMemory` reference and a snapshot
58/// of the memory therein
59pub struct Snapshot {
60    /// Layout object for the sandbox. TODO: get rid of this and
61    /// replace with something saner and set up from the guest (early
62    /// on?).
63    layout: crate::mem::layout::SandboxMemoryLayout,
64    /// Memory of the sandbox at the time this snapshot was taken
65    memory: ReadonlySharedMemory,
66    /// Extra debug information about the binary in this snapshot,
67    /// from when the binary was first loaded into the snapshot.
68    ///
69    /// This information is provided on a best-effort basis, and there
70    /// is a pretty good chance that it does not exist; generally speaking,
71    /// things like persisting a snapshot and reloading it are likely
72    /// to destroy this information.
73    load_info: LoadInfo,
74    /// The address of the top of the guest stack
75    stack_top_gva: u64,
76
77    /// Special register state captured from the vCPU during snapshot.
78    /// None for snapshots created directly from a binary (before
79    /// guest runs).  Some for snapshots taken from a running sandbox.
80    /// Note: CR3 in this struct is NOT used on restore, since page
81    /// tables are relocated during snapshot.
82    sregs: Option<CommonSpecialRegisters>,
83
84    /// The MSRs saved in this snapshot. None before the guest has run.
85    #[cfg(target_arch = "x86_64")]
86    msrs: Option<Vec<MsrEntry>>,
87
88    /// The next action that should be performed on this snapshot
89    next_action: NextAction,
90
91    /// Guest virtual address of the guest binary's ELF entry point
92    /// (`load_addr + e_entry - base_va`). Unlike `next_action`, which
93    /// transitions to `Call(dispatch_addr)` once the guest has run,
94    /// this preserves the original entry across that transition. Used
95    /// to fill `AT_ENTRY` in guest core dumps so a debugger can
96    /// compute the PIE load bias. 0 if unknown (e.g. an older
97    /// on-disk snapshot that predates this field).
98    original_entrypoint: u64,
99
100    /// The generation number assigned to this snapshot when it was
101    /// taken — i.e. "this is the Nth snapshot taken from the sandbox's
102    /// execution path from init to here". Propagated into the
103    /// restored sandbox's guest-visible counter so the guest can tell
104    /// which snapshot it is currently a clone of.
105    snapshot_generation: u64,
106
107    /// Names and signatures of host functions registered on the
108    /// sandbox at the time this snapshot was taken. Used by
109    /// [`crate::MultiUseSandbox::from_snapshot`] to reject a
110    /// `HostFunctions` set that is missing required functions or
111    /// has mismatched signatures.
112    host_functions: HostFunctionDetails,
113}
114impl core::convert::AsRef<Snapshot> for Snapshot {
115    fn as_ref(&self) -> &Self {
116        self
117    }
118}
119impl hyperlight_common::vmem::TableReadOps for Snapshot {
120    type TableAddr = u64;
121    fn entry_addr(addr: u64, offset: u64) -> u64 {
122        addr + offset
123    }
124    unsafe fn read_entry(&self, addr: u64) -> vmem::PageTableEntry {
125        let addr = addr as usize;
126        let Some(pte_bytes) = self.memory.as_slice().get(addr..addr + PTE_SIZE) else {
127            // Attacker-controlled data pointed out-of-bounds. We'll
128            // default to returning 0 in this case, which, for most
129            // architectures (including x86-64 and arm64, the ones we
130            // care about presently) will be a not-present entry.
131            return 0;
132        };
133        // The `get()` above ensures exactly PTE_SIZE bytes.
134        #[allow(clippy::unwrap_used)]
135        vmem::PageTableEntry::from_le_bytes(pte_bytes.try_into().unwrap())
136    }
137    #[allow(clippy::unnecessary_cast)]
138    fn to_phys(addr: u64) -> vmem::PhysAddr {
139        addr as vmem::PhysAddr
140    }
141    #[allow(clippy::unnecessary_cast)]
142    fn from_phys(addr: vmem::PhysAddr) -> u64 {
143        addr as u64
144    }
145    fn root_table(&self) -> u64 {
146        self.root_pt_gpa()
147    }
148}
149
150pub(crate) fn access_gpa<'a>(
151    snap: &'a [u8],
152    scratch: &'a [u8],
153    layout: SandboxMemoryLayout,
154    gpa: u64,
155) -> Option<(&'a [u8], usize)> {
156    let resolved = layout.resolve_gpa(gpa, &[])?.with_memories(snap, scratch);
157    Some((resolved.base.as_ref(), resolved.offset))
158}
159
160pub(crate) struct SharedMemoryPageTableBuffer<'a> {
161    snap: &'a [u8],
162    scratch: &'a [u8],
163    layout: SandboxMemoryLayout,
164    root: u64,
165}
166impl<'a> SharedMemoryPageTableBuffer<'a> {
167    pub(crate) fn new(
168        snap: &'a [u8],
169        scratch: &'a [u8],
170        layout: SandboxMemoryLayout,
171        root: u64,
172    ) -> Self {
173        Self {
174            snap,
175            scratch,
176            layout,
177            root,
178        }
179    }
180}
181impl<'a> hyperlight_common::vmem::TableReadOps for SharedMemoryPageTableBuffer<'a> {
182    type TableAddr = u64;
183    fn entry_addr(addr: u64, offset: u64) -> u64 {
184        addr + offset
185    }
186    unsafe fn read_entry(&self, addr: u64) -> vmem::PageTableEntry {
187        let memoff = access_gpa(self.snap, self.scratch, self.layout, addr);
188        let Some(pte_bytes) = memoff.and_then(|(mem, off)| mem.get(off..off + PTE_SIZE)) else {
189            // Attacker-controlled data pointed out-of-bounds. We'll
190            // default to returning 0 in this case, which, for most
191            // architectures (including x86-64 and arm64, the ones we
192            // care about presently) will be a not-present entry.
193            return 0;
194        };
195        // The `get()` above ensures exactly PTE_SIZE bytes.
196        #[allow(clippy::unwrap_used)]
197        vmem::PageTableEntry::from_le_bytes(pte_bytes.try_into().unwrap())
198    }
199    #[allow(clippy::unnecessary_cast)]
200    fn to_phys(addr: u64) -> vmem::PhysAddr {
201        addr as vmem::PhysAddr
202    }
203    #[allow(clippy::unnecessary_cast)]
204    fn from_phys(addr: vmem::PhysAddr) -> u64 {
205        addr as u64
206    }
207    fn root_table(&self) -> u64 {
208        self.root
209    }
210}
211impl<'a> core::convert::AsRef<SharedMemoryPageTableBuffer<'a>> for SharedMemoryPageTableBuffer<'a> {
212    fn as_ref(&self) -> &Self {
213        self
214    }
215}
216/// Return true if `virt_base` is a VA we must not preserve into the
217/// rebuilt snapshot page tables: it is either part of the scratch
218/// region (re-mapped freshly by `map_specials`) or, on amd64, part of
219/// the self-map of the snapshot's own page tables.
220fn skip_virt(virt_base: u64, scratch_gva: u64) -> bool {
221    if virt_base >= scratch_gva {
222        return true;
223    }
224    if virt_base >= hyperlight_common::layout::SNAPSHOT_PT_GVA_MIN as u64
225        && virt_base <= hyperlight_common::layout::SNAPSHOT_PT_GVA_MAX as u64
226    {
227        return true;
228    }
229    false
230}
231
232/// Find the contents of the page which starts at gpa in guest physical
233/// memory, taking into account excess host->guest regions
234///
235/// # Safety
236/// The host side of the regions identified by MemoryRegion must be
237/// alive and must not be mutated by any other thread: references to
238/// these regions may be created and live for `'a`.
239unsafe fn guest_page<'a>(
240    snap: &'a [u8],
241    scratch: &'a [u8],
242    regions: &[MemoryRegion],
243    layout: SandboxMemoryLayout,
244    gpa: u64,
245) -> Option<&'a [u8]> {
246    let resolved = layout
247        .resolve_gpa(gpa, regions)?
248        .with_memories(snap, scratch);
249    if resolved.as_ref().len() < PAGE_SIZE {
250        return None;
251    }
252    Some(&resolved.as_ref()[..PAGE_SIZE])
253}
254
255fn map_specials(pt_buf: &GuestPageTableBuffer, scratch_size: usize) {
256    if let Some((phys_base, virt_base)) = io_page() {
257        // Map the IO page
258        let mapping = Mapping {
259            phys_base,
260            virt_base,
261            len: PAGE_SIZE as u64,
262            kind: MappingKind::Basic(BasicMapping {
263                readable: true,
264                writable: true,
265                executable: false,
266            }),
267        };
268        unsafe { vmem::map(pt_buf, mapping) };
269    }
270    // Map the scratch region
271    let mapping = Mapping {
272        phys_base: scratch_base_gpa(scratch_size),
273        virt_base: scratch_base_gva(scratch_size),
274        len: scratch_size as u64,
275        kind: MappingKind::Basic(BasicMapping {
276            readable: true,
277            writable: true,
278            // assume that the guest will map these pages elsewhere if
279            // it actually needs to execute from them
280            executable: false,
281        }),
282    };
283    unsafe { vmem::map(pt_buf, mapping) };
284}
285
286impl Snapshot {
287    /// Create a new snapshot from the guest binary identified by `env`. With the configuration
288    /// specified in `cfg`.
289    pub(crate) fn from_env<'b>(
290        env: impl Into<GuestEnvironment<'b>>,
291        cfg: SandboxConfiguration,
292    ) -> Result<Self> {
293        let env = env.into();
294        let mut bin = env.guest_binary;
295        bin.canonicalize()?;
296        let blob = env.init_data;
297
298        let exe_info = match bin {
299            GuestBinary::FilePath(bin_path) => ExeInfo::from_file(&bin_path)?,
300            GuestBinary::Buffer(buffer) => ExeInfo::from_buf(buffer)?,
301        };
302
303        // Check guest/host version compatibility.
304        let host_version = env!("CARGO_PKG_VERSION");
305        if let Some(v) = exe_info.guest_bin_version()
306            && v != host_version
307        {
308            return Err(crate::HyperlightError::GuestBinVersionMismatch {
309                guest_bin_version: v.to_string(),
310                host_version: host_version.to_string(),
311            });
312        }
313
314        let guest_blob_size = blob.as_ref().map(|b| b.data.len()).unwrap_or(0);
315        let guest_blob_mem_flags = blob.as_ref().map(|b| b.permissions);
316
317        let mut layout = crate::mem::layout::SandboxMemoryLayout::new(
318            cfg,
319            exe_info.loaded_size(),
320            guest_blob_size,
321            guest_blob_mem_flags,
322        )?;
323
324        let load_addr = layout.get_guest_code_address() as u64;
325        let base_va = exe_info.base_va();
326        let entrypoint_va: u64 = exe_info.entrypoint().into();
327
328        let mut memory = vec![0; layout.get_memory_size()?];
329
330        let load_info = exe_info.load(
331            load_addr.try_into()?,
332            &mut memory[layout.guest_code_offset()..],
333        )?;
334
335        layout.write_peb(&mut memory)?;
336
337        blob.map(|x| layout.write_init_data(&mut memory, x.data))
338            .transpose()?;
339
340        // Set up page table entries for the snapshot
341        let pt_buf = GuestPageTableBuffer::new(layout.get_pt_base_gpa() as usize);
342
343        // 1. Map the (ideally readonly) pages of snapshot data
344        for rgn in layout.get_memory_regions_::<GuestMemoryRegion>(())?.iter() {
345            let readable = rgn.flags.contains(MemoryRegionFlags::READ);
346            let executable = rgn.flags.contains(MemoryRegionFlags::EXECUTE);
347            let writable = rgn.flags.contains(MemoryRegionFlags::WRITE);
348            let kind = if writable {
349                MappingKind::Cow(CowMapping {
350                    readable,
351                    executable,
352                })
353            } else {
354                MappingKind::Basic(BasicMapping {
355                    readable,
356                    writable: false,
357                    executable,
358                })
359            };
360            let mapping = Mapping {
361                phys_base: rgn.guest_region.start as u64,
362                virt_base: rgn.guest_region.start as u64,
363                len: rgn.guest_region.len() as u64,
364                kind,
365            };
366            unsafe { vmem::map(&pt_buf, mapping) };
367        }
368
369        // 2. Map the special mappings
370        map_specials(&pt_buf, layout.get_scratch_size());
371
372        let pt_bytes = pt_buf.into_bytes();
373        layout.set_pt_size(pt_bytes.len())?;
374        memory.extend(&pt_bytes);
375
376        let exn_stack_top_gva = hyperlight_common::layout::SCRATCH_TOP_GVA as u64
377            - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET
378            + 1;
379
380        let entrypoint_gva = load_addr + entrypoint_va - base_va;
381
382        Ok(Self {
383            memory: ReadonlySharedMemory::from_bytes(&memory, layout.snapshot_size())?,
384            layout,
385            load_info,
386            stack_top_gva: exn_stack_top_gva,
387            sregs: None,
388            #[cfg(target_arch = "x86_64")]
389            msrs: None,
390            next_action: NextAction::Initialise(entrypoint_gva),
391            original_entrypoint: entrypoint_gva,
392            snapshot_generation: 0,
393            host_functions: HostFunctionDetails {
394                host_functions: None,
395            },
396        })
397    }
398
399    // It might be nice to consider moving at least stack_top_gva into
400    // layout, and sharing (via RwLock or similar) the layout between
401    // the (host-side) mem mgr (where it can be passed in here) and
402    // the sandbox vm itself (which modifies it as it receives
403    // requests from the sandbox).
404    #[allow(clippy::too_many_arguments)]
405    /// Take a snapshot of the memory in `shared_mem`, then create a new
406    /// instance of `Self` with the snapshot stored therein.
407    #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
408    pub(crate) fn new<S: SharedMemory>(
409        shared_mem: &mut SnapshotSharedMemory<S>,
410        scratch_mem: &mut S,
411        mut layout: SandboxMemoryLayout,
412        load_info: LoadInfo,
413        regions: Vec<MemoryRegion>,
414        root_pt_gpas: &[u64],
415        stack_top_gva: u64,
416        sregs: CommonSpecialRegisters,
417        #[cfg(target_arch = "x86_64")] msrs: Vec<MsrEntry>,
418        next_action: NextAction,
419        original_entrypoint: u64,
420        snapshot_generation: u64,
421        host_functions: HostFunctionDetails,
422    ) -> Result<Self> {
423        let mut phys_seen = HashMap::<u64, usize>::new();
424        let scratch_gva = scratch_base_gva(layout.get_scratch_size());
425        let memory = shared_mem.with_contents(|snap_c| {
426            scratch_mem.with_contents(|scratch_c| {
427                // Phase 1: walk every PT root together. This detects
428                // aliased intermediate tables (e.g. Nanvix's kernel-
429                // half PTs, which multiple process PDs share by
430                // pointing at the same PT page). The walker emits
431                // `ThisSpace(leaf)` for private leaves and
432                // `AnotherSpace(ref)` for sub-trees that were already
433                // seen via an earlier root. Results are returned in
434                // `root_pt_gpas` order — which is also the topological
435                // order of the `AnotherSpace` references — so
436                // processing in iteration order is safe.
437                let op = SharedMemoryPageTableBuffer::new(
438                    snap_c,
439                    scratch_c,
440                    layout,
441                    root_pt_gpas.first().copied().unwrap_or(0),
442                );
443                let walk = unsafe {
444                    vmem::walk_va_spaces(
445                        &op,
446                        root_pt_gpas,
447                        0,
448                        hyperlight_common::layout::SCRATCH_TOP_GVA as u64,
449                    )
450                };
451
452                // Phase 2: rebuild each space's page tables, compacting
453                // `ThisSpace` leaves into a dense snapshot blob and
454                // linking `AnotherSpace` entries to already-built
455                // spaces' tables.
456                // TODO: Look for opportunities to hugepage map
457                let mut snapshot_memory: Vec<u8> = Vec::new();
458                let pt_buf = GuestPageTableBuffer::new(layout.get_pt_base_gpa() as usize);
459                // Allocate one root table per space and remember the
460                // addresses returned by `alloc_table` instead of
461                // assuming the buffer's physical layout.
462                let mut root_addrs: Vec<u64> = Vec::with_capacity(root_pt_gpas.len());
463                root_addrs.push(pt_buf.initial_root());
464                for _ in 1..root_pt_gpas.len() {
465                    root_addrs.push(unsafe { pt_buf.alloc_table() });
466                }
467
468                let mut built_roots: BTreeMap<SpaceId, u64> = BTreeMap::new();
469                for (root_idx, (space_id, mappings)) in walk.into_iter().enumerate() {
470                    pt_buf.set_root(root_addrs[root_idx]);
471                    built_roots.insert(space_id, root_addrs[root_idx]);
472
473                    for sam in mappings {
474                        match sam {
475                            SpaceAwareMapping::ThisSpace(mapping) => {
476                                // Drop the scratch region and (on
477                                // amd64) the snapshot's own PT
478                                // self-map; both are re-mapped
479                                // freshly by `map_specials`.
480                                if skip_virt(mapping.virt_base, scratch_gva) {
481                                    continue;
482                                }
483                                let Some(contents) = (unsafe {
484                                    guest_page(
485                                        snap_c,
486                                        scratch_c,
487                                        &regions,
488                                        layout,
489                                        mapping.phys_base,
490                                    )
491                                }) else {
492                                    continue;
493                                };
494
495                                // Writable pages become CoW in the
496                                // rebuilt snapshot; read-only pages
497                                // stay read-only.
498                                let kind = match mapping.kind {
499                                    MappingKind::Cow(cm) => MappingKind::Cow(cm),
500                                    MappingKind::Basic(bm) if bm.writable => {
501                                        MappingKind::Cow(CowMapping {
502                                            readable: bm.readable,
503                                            executable: bm.executable,
504                                        })
505                                    }
506                                    MappingKind::Basic(bm) => MappingKind::Basic(BasicMapping {
507                                        readable: bm.readable,
508                                        writable: false,
509                                        executable: bm.executable,
510                                    }),
511                                    MappingKind::Unmapped => continue,
512                                };
513                                let new_gpa =
514                                    phys_seen.entry(mapping.phys_base).or_insert_with(|| {
515                                        let new_offset = snapshot_memory.len();
516                                        snapshot_memory.extend(contents);
517                                        new_offset + SandboxMemoryLayout::BASE_ADDRESS
518                                    });
519
520                                let compacted = Mapping {
521                                    phys_base: *new_gpa as u64,
522                                    virt_base: mapping.virt_base,
523                                    len: PAGE_SIZE as u64,
524                                    kind,
525                                };
526                                unsafe { vmem::map(&pt_buf, compacted) };
527                            }
528                            SpaceAwareMapping::AnotherSpace(ref_map) => {
529                                // Link to the owning space's already-
530                                // rebuilt intermediate table — this
531                                // is what preserves Nanvix's
532                                // kernel-half-shared invariant across
533                                // process PDs after relocation.
534                                unsafe {
535                                    vmem::space_aware_map(&pt_buf, ref_map, &built_roots);
536                                }
537                            }
538                        }
539                    }
540                }
541
542                // Phase 3: Map the scratch region into each root.
543                for &root_addr in &root_addrs {
544                    pt_buf.set_root(root_addr);
545                    map_specials(&pt_buf, layout.get_scratch_size());
546                }
547                pt_buf.set_root(pt_buf.initial_root());
548
549                snapshot_memory.resize(
550                    snapshot_memory.len().next_multiple_of(page_size::get()),
551                    0u8,
552                );
553
554                // Phase 4: finalize PT bytes.
555                let pt_data = pt_buf.into_bytes();
556                layout.set_pt_size(pt_data.len())?;
557                snapshot_memory.extend(&pt_data);
558                Ok::<_, crate::HyperlightError>(snapshot_memory)
559            })
560        })???;
561        // Only the data prefix is exposed to the guest. The PT tail
562        // sits past it in the host mapping and is copied into the
563        // scratch region on restore. Keeping it out of the guest
564        // mapping of the snapshot region avoids overlap with
565        // `map_file_cow` regions installed immediately after the
566        // snapshot in guest PA space.
567        let guest_visible_size = memory.len() - layout.get_pt_size();
568        debug_assert!(guest_visible_size.is_multiple_of(page_size::get()));
569        layout.set_snapshot_size(guest_visible_size);
570
571        Ok(Self {
572            layout,
573            memory: ReadonlySharedMemory::from_bytes(&memory, guest_visible_size)?,
574            load_info,
575            stack_top_gva,
576            sregs: Some(sregs),
577            #[cfg(target_arch = "x86_64")]
578            msrs: Some(msrs),
579            next_action,
580            original_entrypoint,
581            snapshot_generation,
582            host_functions,
583        })
584    }
585
586    /// Generation number assigned to this snapshot when it was taken.
587    pub(crate) fn snapshot_generation(&self) -> u64 {
588        self.snapshot_generation
589    }
590
591    /// Return the main memory contents of the snapshot
592    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
593    pub(crate) fn memory(&self) -> &ReadonlySharedMemory {
594        &self.memory
595    }
596
597    /// Return a copy of the load info for the exe in the snapshot
598    pub(crate) fn load_info(&self) -> LoadInfo {
599        self.load_info.clone()
600    }
601
602    pub(crate) fn layout(&self) -> &crate::mem::layout::SandboxMemoryLayout {
603        &self.layout
604    }
605
606    pub(crate) fn root_pt_gpa(&self) -> u64 {
607        self.layout.get_pt_base_gpa()
608    }
609
610    pub(crate) fn stack_top_gva(&self) -> u64 {
611        self.stack_top_gva
612    }
613
614    /// Returns the special registers stored in this snapshot.
615    /// Returns None for snapshots created directly from a binary (before preinitialisation).
616    /// Returns Some for snapshots taken from a running sandbox.
617    /// Note: The CR3 value in the returned struct should NOT be used for restore;
618    /// use `root_pt_gpa()` instead since page tables are relocated during snapshot.
619    pub(crate) fn sregs(&self) -> Option<&CommonSpecialRegisters> {
620        self.sregs.as_ref()
621    }
622
623    /// The MSRs saved in this snapshot.
624    #[cfg(target_arch = "x86_64")]
625    pub(crate) fn msrs(&self) -> Option<&Vec<MsrEntry>> {
626        self.msrs.as_ref()
627    }
628
629    pub(crate) fn next_action(&self) -> NextAction {
630        self.next_action
631    }
632
633    /// Guest virtual address of the guest binary's ELF entry point,
634    /// preserved across the `Initialise` -> `Call` transition. Used
635    /// to fill `AT_ENTRY` in guest core dumps. 0 if unknown.
636    pub(crate) fn original_entrypoint(&self) -> u64 {
637        self.original_entrypoint
638    }
639
640    /// Validate that `provided` is a superset of the host functions
641    /// recorded in this snapshot: every function that was registered
642    /// at snapshot time must also be present in `provided` with a
643    /// matching signature. Extras in `provided` are allowed.
644    ///
645    /// A snapshot with no recorded host functions (e.g. one
646    /// produced by a test-only constructor) accepts any `provided`
647    /// set.
648    pub(crate) fn validate_host_functions(
649        &self,
650        provided: &crate::sandbox::host_funcs::FunctionRegistry,
651    ) -> Result<()> {
652        let required = match &self.host_functions.host_functions {
653            Some(v) => v,
654            None => return Ok(()),
655        };
656        if required.is_empty() {
657            return Ok(());
658        }
659
660        let mut missing: Vec<String> = Vec::new();
661        let mut signature_mismatches: Vec<String> = Vec::new();
662
663        for req in required {
664            match provided.function_signature(&req.function_name) {
665                // Function name is absent from the provided registry.
666                None => missing.push(req.function_name.clone()),
667                // Function exists, but signature does not match.
668                Some((found_parameter_types, found_return_type))
669                    if {
670                        let params_match = match req.parameter_types.as_deref() {
671                            Some(params) => params == found_parameter_types,
672                            None => found_parameter_types.is_empty(),
673                        };
674                        !params_match || req.return_type != found_return_type
675                    } =>
676                {
677                    signature_mismatches.push(format!(
678                        "{}: snapshot has {:?} -> {:?}, registered {:?} -> {:?}",
679                        req.function_name,
680                        req.parameter_types,
681                        req.return_type,
682                        Some(found_parameter_types.to_vec()),
683                        found_return_type,
684                    ));
685                }
686                // Function exists and signature matches.
687                Some(_) => {}
688            }
689        }
690
691        if missing.is_empty() && signature_mismatches.is_empty() {
692            return Ok(());
693        }
694
695        Err(crate::HyperlightError::SnapshotHostFunctionMismatch {
696            missing,
697            signature_mismatches,
698        })
699    }
700}
701
702#[cfg(test)]
703mod tests {
704    use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails;
705    use hyperlight_common::vmem::{self, BasicMapping, Mapping, MappingKind, PAGE_SIZE};
706
707    use crate::hypervisor::regs::CommonSpecialRegisters;
708    use crate::mem::exe::LoadInfo;
709    use crate::mem::layout::SandboxMemoryLayout;
710    use crate::mem::mgr::{GuestPageTableBuffer, SandboxMemoryManager, SnapshotSharedMemory};
711    use crate::mem::shared_mem::{
712        ExclusiveSharedMemory, HostSharedMemory, ReadonlySharedMemory, SharedMemory,
713    };
714
715    fn default_sregs() -> CommonSpecialRegisters {
716        CommonSpecialRegisters::default()
717    }
718
719    fn simple_pt_base() -> usize {
720        page_size::get() + SandboxMemoryLayout::BASE_ADDRESS
721    }
722
723    fn make_simple_pt_mem(contents: &[u8]) -> SnapshotSharedMemory<ExclusiveSharedMemory> {
724        let pt_buf = GuestPageTableBuffer::new(simple_pt_base());
725        let mapping = Mapping {
726            phys_base: SandboxMemoryLayout::BASE_ADDRESS as u64,
727            virt_base: SandboxMemoryLayout::BASE_ADDRESS as u64,
728            len: page_size::get() as u64,
729            kind: MappingKind::Basic(BasicMapping {
730                readable: true,
731                writable: true,
732                executable: true,
733            }),
734        };
735        unsafe { vmem::map(&pt_buf, mapping) };
736        super::map_specials(&pt_buf, PAGE_SIZE);
737        let pt_bytes = pt_buf.into_bytes();
738
739        let mut snapshot_mem = vec![0u8; page_size::get() + pt_bytes.len()];
740        snapshot_mem[0..page_size::get()].copy_from_slice(contents);
741        snapshot_mem[page_size::get()..].copy_from_slice(&pt_bytes);
742        ReadonlySharedMemory::from_bytes(&snapshot_mem, page_size::get())
743            .unwrap()
744            .to_mgr_snapshot_mem()
745            .unwrap()
746    }
747
748    fn make_simple_pt_mgr() -> (SandboxMemoryManager<HostSharedMemory>, u64) {
749        let cfg = crate::sandbox::SandboxConfiguration::default();
750        let scratch_mem = ExclusiveSharedMemory::new(cfg.get_scratch_size()).unwrap();
751        let mgr = SandboxMemoryManager::new(
752            SandboxMemoryLayout::new(cfg, 4096, 0x3000, None).unwrap(),
753            make_simple_pt_mem(&vec![0u8; page_size::get()]),
754            scratch_mem,
755            super::NextAction::None,
756        );
757        let (mgr, _) = mgr.build().unwrap();
758        (mgr, simple_pt_base() as u64)
759    }
760
761    #[test]
762    fn multiple_snapshots_independent() {
763        let (mut mgr, pt_base) = make_simple_pt_mgr();
764
765        // Create first snapshot with pattern A
766        let pattern_a = vec![0xAA; page_size::get()];
767        let snapshot_a = super::Snapshot::new(
768            &mut make_simple_pt_mem(&pattern_a).build().0,
769            &mut mgr.scratch_mem,
770            mgr.layout,
771            LoadInfo::dummy(),
772            Vec::new(),
773            &[pt_base],
774            0,
775            default_sregs(),
776            #[cfg(target_arch = "x86_64")]
777            Vec::new(),
778            super::NextAction::None,
779            0,
780            1,
781            HostFunctionDetails::default(),
782        )
783        .unwrap();
784
785        // Create second snapshot with pattern B
786        let pattern_b = vec![0xBB; page_size::get()];
787        let snapshot_b = super::Snapshot::new(
788            &mut make_simple_pt_mem(&pattern_b).build().0,
789            &mut mgr.scratch_mem,
790            mgr.layout,
791            LoadInfo::dummy(),
792            Vec::new(),
793            &[pt_base],
794            0,
795            default_sregs(),
796            #[cfg(target_arch = "x86_64")]
797            Vec::new(),
798            super::NextAction::None,
799            0,
800            2,
801            HostFunctionDetails::default(),
802        )
803        .unwrap();
804
805        // Restore snapshot A
806        mgr.restore_snapshot(&snapshot_a).unwrap();
807        mgr.shared_mem
808            .with_contents(|contents| assert_eq!(&contents[0..pattern_a.len()], &pattern_a[..]))
809            .unwrap();
810
811        // Restore snapshot B
812        mgr.restore_snapshot(&snapshot_b).unwrap();
813        mgr.shared_mem
814            .with_contents(|contents| assert_eq!(&contents[0..pattern_b.len()], &pattern_b[..]))
815            .unwrap();
816    }
817}