mod file;
mod file_tests;
use std::collections::{BTreeMap, HashMap};
pub use file::reference::{OciDigest, OciReference, OciTag};
use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails;
use hyperlight_common::layout::{io_page, scratch_base_gpa, scratch_base_gva};
use hyperlight_common::vmem;
use hyperlight_common::vmem::{
BasicMapping, CowMapping, Mapping, MappingKind, PAGE_SIZE, SpaceAwareMapping, SpaceId, TableOps,
};
use tracing::{Span, instrument};
use crate::Result;
use crate::hypervisor::regs::CommonSpecialRegisters;
use crate::mem::exe::{ExeInfo, LoadInfo};
use crate::mem::layout::SandboxMemoryLayout;
use crate::mem::memory_region::{GuestMemoryRegion, MemoryRegion, MemoryRegionFlags};
use crate::mem::mgr::{GuestPageTableBuffer, SnapshotSharedMemory};
use crate::mem::shared_mem::{ReadonlySharedMemory, SharedMemory};
use crate::sandbox::SandboxConfiguration;
use crate::sandbox::uninitialized::{GuestBinary, GuestEnvironment};
const PTE_SIZE: usize = size_of::<vmem::PageTableEntry>();
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum NextAction {
Initialise(u64),
Call(u64),
#[cfg(test)]
None,
}
pub struct Snapshot {
layout: crate::mem::layout::SandboxMemoryLayout,
memory: ReadonlySharedMemory,
load_info: LoadInfo,
stack_top_gva: u64,
sregs: Option<CommonSpecialRegisters>,
entrypoint: NextAction,
snapshot_generation: u64,
host_functions: HostFunctionDetails,
}
impl core::convert::AsRef<Snapshot> for Snapshot {
fn as_ref(&self) -> &Self {
self
}
}
impl hyperlight_common::vmem::TableReadOps for Snapshot {
type TableAddr = u64;
fn entry_addr(addr: u64, offset: u64) -> u64 {
addr + offset
}
unsafe fn read_entry(&self, addr: u64) -> vmem::PageTableEntry {
let addr = addr as usize;
let Some(pte_bytes) = self.memory.as_slice().get(addr..addr + PTE_SIZE) else {
return 0;
};
#[allow(clippy::unwrap_used)]
vmem::PageTableEntry::from_le_bytes(pte_bytes.try_into().unwrap())
}
#[allow(clippy::unnecessary_cast)]
fn to_phys(addr: u64) -> vmem::PhysAddr {
addr as vmem::PhysAddr
}
#[allow(clippy::unnecessary_cast)]
fn from_phys(addr: vmem::PhysAddr) -> u64 {
addr as u64
}
fn root_table(&self) -> u64 {
self.root_pt_gpa()
}
}
pub(crate) fn access_gpa<'a>(
snap: &'a [u8],
scratch: &'a [u8],
layout: SandboxMemoryLayout,
gpa: u64,
) -> Option<(&'a [u8], usize)> {
let resolved = layout.resolve_gpa(gpa, &[])?.with_memories(snap, scratch);
Some((resolved.base.as_ref(), resolved.offset))
}
pub(crate) struct SharedMemoryPageTableBuffer<'a> {
snap: &'a [u8],
scratch: &'a [u8],
layout: SandboxMemoryLayout,
root: u64,
}
impl<'a> SharedMemoryPageTableBuffer<'a> {
pub(crate) fn new(
snap: &'a [u8],
scratch: &'a [u8],
layout: SandboxMemoryLayout,
root: u64,
) -> Self {
Self {
snap,
scratch,
layout,
root,
}
}
}
impl<'a> hyperlight_common::vmem::TableReadOps for SharedMemoryPageTableBuffer<'a> {
type TableAddr = u64;
fn entry_addr(addr: u64, offset: u64) -> u64 {
addr + offset
}
unsafe fn read_entry(&self, addr: u64) -> vmem::PageTableEntry {
let memoff = access_gpa(self.snap, self.scratch, self.layout, addr);
let Some(pte_bytes) = memoff.and_then(|(mem, off)| mem.get(off..off + PTE_SIZE)) else {
return 0;
};
#[allow(clippy::unwrap_used)]
vmem::PageTableEntry::from_le_bytes(pte_bytes.try_into().unwrap())
}
#[allow(clippy::unnecessary_cast)]
fn to_phys(addr: u64) -> vmem::PhysAddr {
addr as vmem::PhysAddr
}
#[allow(clippy::unnecessary_cast)]
fn from_phys(addr: vmem::PhysAddr) -> u64 {
addr as u64
}
fn root_table(&self) -> u64 {
self.root
}
}
impl<'a> core::convert::AsRef<SharedMemoryPageTableBuffer<'a>> for SharedMemoryPageTableBuffer<'a> {
fn as_ref(&self) -> &Self {
self
}
}
fn skip_virt(virt_base: u64, scratch_gva: u64) -> bool {
if virt_base >= scratch_gva {
return true;
}
if virt_base >= hyperlight_common::layout::SNAPSHOT_PT_GVA_MIN as u64
&& virt_base <= hyperlight_common::layout::SNAPSHOT_PT_GVA_MAX as u64
{
return true;
}
false
}
unsafe fn guest_page<'a>(
snap: &'a [u8],
scratch: &'a [u8],
regions: &[MemoryRegion],
layout: SandboxMemoryLayout,
gpa: u64,
) -> Option<&'a [u8]> {
let resolved = layout
.resolve_gpa(gpa, regions)?
.with_memories(snap, scratch);
if resolved.as_ref().len() < PAGE_SIZE {
return None;
}
Some(&resolved.as_ref()[..PAGE_SIZE])
}
fn map_specials(pt_buf: &GuestPageTableBuffer, scratch_size: usize) {
if let Some((phys_base, virt_base)) = io_page() {
let mapping = Mapping {
phys_base,
virt_base,
len: PAGE_SIZE as u64,
kind: MappingKind::Basic(BasicMapping {
readable: true,
writable: true,
executable: false,
}),
};
unsafe { vmem::map(pt_buf, mapping) };
}
let mapping = Mapping {
phys_base: scratch_base_gpa(scratch_size),
virt_base: scratch_base_gva(scratch_size),
len: scratch_size as u64,
kind: MappingKind::Basic(BasicMapping {
readable: true,
writable: true,
executable: false,
}),
};
unsafe { vmem::map(pt_buf, mapping) };
}
impl Snapshot {
pub(crate) fn from_env<'a, 'b>(
env: impl Into<GuestEnvironment<'a, 'b>>,
cfg: SandboxConfiguration,
) -> Result<Self> {
let env = env.into();
let mut bin = env.guest_binary;
bin.canonicalize()?;
let blob = env.init_data;
let exe_info = match bin {
GuestBinary::FilePath(bin_path_str) => ExeInfo::from_file(&bin_path_str)?,
GuestBinary::Buffer(buffer) => ExeInfo::from_buf(buffer)?,
};
let host_version = env!("CARGO_PKG_VERSION");
if let Some(v) = exe_info.guest_bin_version()
&& v != host_version
{
return Err(crate::HyperlightError::GuestBinVersionMismatch {
guest_bin_version: v.to_string(),
host_version: host_version.to_string(),
});
}
let guest_blob_size = blob.as_ref().map(|b| b.data.len()).unwrap_or(0);
let guest_blob_mem_flags = blob.as_ref().map(|b| b.permissions);
let mut layout = crate::mem::layout::SandboxMemoryLayout::new(
cfg,
exe_info.loaded_size(),
guest_blob_size,
guest_blob_mem_flags,
)?;
let load_addr = layout.get_guest_code_address() as u64;
let base_va = exe_info.base_va();
let entrypoint_va: u64 = exe_info.entrypoint().into();
let mut memory = vec![0; layout.get_memory_size()?];
let load_info = exe_info.load(
load_addr.try_into()?,
&mut memory[layout.get_guest_code_offset()..],
)?;
layout.write_peb(&mut memory)?;
blob.map(|x| layout.write_init_data(&mut memory, x.data))
.transpose()?;
let pt_buf = GuestPageTableBuffer::new(layout.get_pt_base_gpa() as usize);
for rgn in layout.get_memory_regions_::<GuestMemoryRegion>(())?.iter() {
let readable = rgn.flags.contains(MemoryRegionFlags::READ);
let executable = rgn.flags.contains(MemoryRegionFlags::EXECUTE);
let writable = rgn.flags.contains(MemoryRegionFlags::WRITE);
let kind = if writable {
MappingKind::Cow(CowMapping {
readable,
executable,
})
} else {
MappingKind::Basic(BasicMapping {
readable,
writable: false,
executable,
})
};
let mapping = Mapping {
phys_base: rgn.guest_region.start as u64,
virt_base: rgn.guest_region.start as u64,
len: rgn.guest_region.len() as u64,
kind,
};
unsafe { vmem::map(&pt_buf, mapping) };
}
map_specials(&pt_buf, layout.get_scratch_size());
let pt_bytes = pt_buf.into_bytes();
layout.set_pt_size(pt_bytes.len())?;
memory.extend(&pt_bytes);
let exn_stack_top_gva = hyperlight_common::layout::SCRATCH_TOP_GVA as u64
- hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET
+ 1;
Ok(Self {
memory: ReadonlySharedMemory::from_bytes(&memory, layout.snapshot_size)?,
layout,
load_info,
stack_top_gva: exn_stack_top_gva,
sregs: None,
entrypoint: NextAction::Initialise(load_addr + entrypoint_va - base_va),
snapshot_generation: 0,
host_functions: HostFunctionDetails {
host_functions: None,
},
})
}
#[allow(clippy::too_many_arguments)]
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
pub(crate) fn new<S: SharedMemory>(
shared_mem: &mut SnapshotSharedMemory<S>,
scratch_mem: &mut S,
mut layout: SandboxMemoryLayout,
load_info: LoadInfo,
regions: Vec<MemoryRegion>,
root_pt_gpas: &[u64],
stack_top_gva: u64,
sregs: CommonSpecialRegisters,
entrypoint: NextAction,
snapshot_generation: u64,
host_functions: HostFunctionDetails,
) -> Result<Self> {
let mut phys_seen = HashMap::<u64, usize>::new();
let scratch_gva = scratch_base_gva(layout.get_scratch_size());
let memory = shared_mem.with_contents(|snap_c| {
scratch_mem.with_contents(|scratch_c| {
let op = SharedMemoryPageTableBuffer::new(
snap_c,
scratch_c,
layout,
root_pt_gpas.first().copied().unwrap_or(0),
);
let walk = unsafe {
vmem::walk_va_spaces(
&op,
root_pt_gpas,
0,
hyperlight_common::layout::SCRATCH_TOP_GVA as u64,
)
};
let mut snapshot_memory: Vec<u8> = Vec::new();
let pt_buf = GuestPageTableBuffer::new(layout.get_pt_base_gpa() as usize);
let mut root_addrs: Vec<u64> = Vec::with_capacity(root_pt_gpas.len());
root_addrs.push(pt_buf.initial_root());
for _ in 1..root_pt_gpas.len() {
root_addrs.push(unsafe { pt_buf.alloc_table() });
}
let mut built_roots: BTreeMap<SpaceId, u64> = BTreeMap::new();
for (root_idx, (space_id, mappings)) in walk.into_iter().enumerate() {
pt_buf.set_root(root_addrs[root_idx]);
built_roots.insert(space_id, root_addrs[root_idx]);
for sam in mappings {
match sam {
SpaceAwareMapping::ThisSpace(mapping) => {
if skip_virt(mapping.virt_base, scratch_gva) {
continue;
}
let Some(contents) = (unsafe {
guest_page(
snap_c,
scratch_c,
®ions,
layout,
mapping.phys_base,
)
}) else {
continue;
};
let kind = match mapping.kind {
MappingKind::Cow(cm) => MappingKind::Cow(cm),
MappingKind::Basic(bm) if bm.writable => {
MappingKind::Cow(CowMapping {
readable: bm.readable,
executable: bm.executable,
})
}
MappingKind::Basic(bm) => MappingKind::Basic(BasicMapping {
readable: bm.readable,
writable: false,
executable: bm.executable,
}),
MappingKind::Unmapped => continue,
};
let new_gpa =
phys_seen.entry(mapping.phys_base).or_insert_with(|| {
let new_offset = snapshot_memory.len();
snapshot_memory.extend(contents);
new_offset + SandboxMemoryLayout::BASE_ADDRESS
});
let compacted = Mapping {
phys_base: *new_gpa as u64,
virt_base: mapping.virt_base,
len: PAGE_SIZE as u64,
kind,
};
unsafe { vmem::map(&pt_buf, compacted) };
}
SpaceAwareMapping::AnotherSpace(ref_map) => {
unsafe {
vmem::space_aware_map(&pt_buf, ref_map, &built_roots);
}
}
}
}
}
for &root_addr in &root_addrs {
pt_buf.set_root(root_addr);
map_specials(&pt_buf, layout.get_scratch_size());
}
pt_buf.set_root(pt_buf.initial_root());
let pt_data = pt_buf.into_bytes();
layout.set_pt_size(pt_data.len())?;
snapshot_memory.extend(&pt_data);
Ok::<_, crate::HyperlightError>(snapshot_memory)
})
})???;
let guest_visible_size = memory.len() - layout.get_pt_size();
debug_assert!(guest_visible_size.is_multiple_of(PAGE_SIZE));
layout.set_snapshot_size(guest_visible_size);
Ok(Self {
layout,
memory: ReadonlySharedMemory::from_bytes(&memory, guest_visible_size)?,
load_info,
stack_top_gva,
sregs: Some(sregs),
entrypoint,
snapshot_generation,
host_functions,
})
}
pub(crate) fn snapshot_generation(&self) -> u64 {
self.snapshot_generation
}
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
pub(crate) fn memory(&self) -> &ReadonlySharedMemory {
&self.memory
}
pub(crate) fn load_info(&self) -> LoadInfo {
self.load_info.clone()
}
pub(crate) fn layout(&self) -> &crate::mem::layout::SandboxMemoryLayout {
&self.layout
}
pub(crate) fn root_pt_gpa(&self) -> u64 {
self.layout.get_pt_base_gpa()
}
pub(crate) fn stack_top_gva(&self) -> u64 {
self.stack_top_gva
}
pub(crate) fn sregs(&self) -> Option<&CommonSpecialRegisters> {
self.sregs.as_ref()
}
pub(crate) fn entrypoint(&self) -> NextAction {
self.entrypoint
}
pub(crate) fn validate_host_functions(
&self,
provided: &crate::sandbox::host_funcs::FunctionRegistry,
) -> Result<()> {
let required = match &self.host_functions.host_functions {
Some(v) => v,
None => return Ok(()),
};
if required.is_empty() {
return Ok(());
}
let mut missing: Vec<String> = Vec::new();
let mut signature_mismatches: Vec<String> = Vec::new();
for req in required {
match provided.function_signature(&req.function_name) {
None => missing.push(req.function_name.clone()),
Some((found_parameter_types, found_return_type))
if {
let params_match = match req.parameter_types.as_deref() {
Some(params) => params == found_parameter_types,
None => found_parameter_types.is_empty(),
};
!params_match || req.return_type != found_return_type
} =>
{
signature_mismatches.push(format!(
"{}: snapshot has {:?} -> {:?}, registered {:?} -> {:?}",
req.function_name,
req.parameter_types,
req.return_type,
Some(found_parameter_types.to_vec()),
found_return_type,
));
}
Some(_) => {}
}
}
if missing.is_empty() && signature_mismatches.is_empty() {
return Ok(());
}
Err(crate::HyperlightError::SnapshotHostFunctionMismatch {
missing,
signature_mismatches,
})
}
pub(crate) fn validate_compatibility(
&self,
layout: &crate::mem::layout::SandboxMemoryLayout,
host_funcs: &crate::sandbox::host_funcs::FunctionRegistry,
) -> Result<()> {
if !self.layout().is_compatible_with(layout) {
return Err(crate::HyperlightError::SnapshotLayoutMismatch);
}
self.validate_host_functions(host_funcs)
}
}
#[cfg(test)]
mod tests {
use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails;
use hyperlight_common::vmem::{self, BasicMapping, Mapping, MappingKind, PAGE_SIZE};
use crate::hypervisor::regs::CommonSpecialRegisters;
use crate::mem::exe::LoadInfo;
use crate::mem::layout::SandboxMemoryLayout;
use crate::mem::mgr::{GuestPageTableBuffer, SandboxMemoryManager, SnapshotSharedMemory};
use crate::mem::shared_mem::{
ExclusiveSharedMemory, HostSharedMemory, ReadonlySharedMemory, SharedMemory,
};
fn default_sregs() -> CommonSpecialRegisters {
CommonSpecialRegisters::default()
}
const SIMPLE_PT_BASE: usize = PAGE_SIZE + SandboxMemoryLayout::BASE_ADDRESS;
fn make_simple_pt_mem(contents: &[u8]) -> SnapshotSharedMemory<ExclusiveSharedMemory> {
let pt_buf = GuestPageTableBuffer::new(SIMPLE_PT_BASE);
let mapping = Mapping {
phys_base: SandboxMemoryLayout::BASE_ADDRESS as u64,
virt_base: SandboxMemoryLayout::BASE_ADDRESS as u64,
len: PAGE_SIZE as u64,
kind: MappingKind::Basic(BasicMapping {
readable: true,
writable: true,
executable: true,
}),
};
unsafe { vmem::map(&pt_buf, mapping) };
super::map_specials(&pt_buf, PAGE_SIZE);
let pt_bytes = pt_buf.into_bytes();
let mut snapshot_mem = vec![0u8; PAGE_SIZE + pt_bytes.len()];
snapshot_mem[0..PAGE_SIZE].copy_from_slice(contents);
snapshot_mem[PAGE_SIZE..].copy_from_slice(&pt_bytes);
ReadonlySharedMemory::from_bytes(&snapshot_mem, PAGE_SIZE)
.unwrap()
.to_mgr_snapshot_mem()
.unwrap()
}
fn make_simple_pt_mgr() -> (SandboxMemoryManager<HostSharedMemory>, u64) {
let cfg = crate::sandbox::SandboxConfiguration::default();
let scratch_mem = ExclusiveSharedMemory::new(cfg.get_scratch_size()).unwrap();
let mgr = SandboxMemoryManager::new(
SandboxMemoryLayout::new(cfg, 4096, 0x3000, None).unwrap(),
make_simple_pt_mem(&[0u8; PAGE_SIZE]),
scratch_mem,
super::NextAction::None,
);
let (mgr, _) = mgr.build().unwrap();
(mgr, SIMPLE_PT_BASE as u64)
}
#[test]
fn multiple_snapshots_independent() {
let (mut mgr, pt_base) = make_simple_pt_mgr();
let pattern_a = vec![0xAA; PAGE_SIZE];
let snapshot_a = super::Snapshot::new(
&mut make_simple_pt_mem(&pattern_a).build().0,
&mut mgr.scratch_mem,
mgr.layout,
LoadInfo::dummy(),
Vec::new(),
&[pt_base],
0,
default_sregs(),
super::NextAction::None,
1,
HostFunctionDetails::default(),
)
.unwrap();
let pattern_b = vec![0xBB; PAGE_SIZE];
let snapshot_b = super::Snapshot::new(
&mut make_simple_pt_mem(&pattern_b).build().0,
&mut mgr.scratch_mem,
mgr.layout,
LoadInfo::dummy(),
Vec::new(),
&[pt_base],
0,
default_sregs(),
super::NextAction::None,
2,
HostFunctionDetails::default(),
)
.unwrap();
mgr.restore_snapshot(&snapshot_a).unwrap();
mgr.shared_mem
.with_contents(|contents| assert_eq!(&contents[0..pattern_a.len()], &pattern_a[..]))
.unwrap();
mgr.restore_snapshot(&snapshot_b).unwrap();
mgr.shared_mem
.with_contents(|contents| assert_eq!(&contents[0..pattern_b.len()], &pattern_b[..]))
.unwrap();
}
}