Skip to main content

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