From fa74cb3b4fd1dafa73d12ec86b338b664589c34a Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Wed, 7 Oct 2026 20:08:59 +0200 Subject: [PATCH] feat: restrict guest capabilities during snapshot checkpoint This patch introduces the execution mode on the host and allows call guest function and prohibit the guest to call host functions. The only user of this mode is snapshotting for now. Signed-off-by: Tomasz Andrzejak --- CHANGELOG.md | 2 + src/hyperlight_host/src/mem/mgr.rs | 18 +++++++ .../src/sandbox/initialized.rs | 54 ++++++++++++++++++- src/hyperlight_host/src/sandbox/outb.rs | 9 +++- 4 files changed, 80 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61bab3109..21ecfcb42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). rejects snapshots without transport state. * Running snapshots checkpoint dirty virtqueues before capture. Ordinary calls keep their deferred result path. +* Snapshot checkpoints reject host function calls. Violations poison the + sandbox. * Use the reclaimed stack pages to raise the default G2H and H2G pools to 12 and 8 pages. * `hyperlight_guest_bin::exception::arch`, previously available on diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index 6f7de60c4..0417e6258 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -133,6 +133,15 @@ impl ReadonlySharedMemory { } pub(crate) use unused_hack::SnapshotSharedMemory; +/// Guest operations the host accepts during a VM entry. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ExecutionMode { + /// Guest initialization and guest function calls. + Unrestricted, + /// Snapshot checkpoint preparation. Host function calls are rejected. + Restricted, +} + /// A struct that is responsible for laying out and managing the memory /// for a given `Sandbox`. pub(crate) struct SandboxMemoryManager { @@ -151,6 +160,8 @@ pub(crate) struct SandboxMemoryManager { pub(crate) original_entrypoint: u64, /// Buffer for accumulating guest abort messages pub(crate) abort_buffer: Vec, + /// Guest operations the host accepts during the current VM entry. + pub(crate) execution_mode: ExecutionMode, /// Generation counter: how many snapshots have been taken from /// this sandbox's execution path from init to here. Incremented /// on each `snapshot` call; on `restore_snapshot` we inherit the @@ -174,6 +185,7 @@ impl Clone for SandboxMemoryManager { next_action: self.next_action, original_entrypoint: self.original_entrypoint, abort_buffer: self.abort_buffer.clone(), + execution_mode: self.execution_mode, snapshot_count: self.snapshot_count, g2h_consumer: None, h2g_consumer: None, @@ -314,6 +326,7 @@ where next_action, original_entrypoint: 0, abort_buffer: Vec::new(), + execution_mode: ExecutionMode::Unrestricted, snapshot_count: 0, g2h_consumer: None, h2g_consumer: None, @@ -368,6 +381,7 @@ impl SandboxMemoryManager { next_action: self.next_action, original_entrypoint: self.original_entrypoint, abort_buffer: self.abort_buffer, + execution_mode: self.execution_mode, snapshot_count: self.snapshot_count, g2h_consumer: None, h2g_consumer: None, @@ -380,6 +394,7 @@ impl SandboxMemoryManager { next_action: self.next_action, original_entrypoint: self.original_entrypoint, abort_buffer: Vec::new(), // Guest doesn't need abort buffer + execution_mode: self.execution_mode, snapshot_count: self.snapshot_count, g2h_consumer: None, h2g_consumer: None, @@ -692,6 +707,9 @@ impl SandboxMemoryManager { self.g2h_consumer = None; self.h2g_consumer = None; + // A panic during restricted dispatch can skip the mode reset. + self.execution_mode = ExecutionMode::Unrestricted; + let gsnapshot = if *snapshot.memory() == self.shared_mem { // If the snapshot memory is already the correct memory, // which is readonly, don't bother with restoring it, diff --git a/src/hyperlight_host/src/sandbox/initialized.rs b/src/hyperlight_host/src/sandbox/initialized.rs index e31b42251..619e436c6 100644 --- a/src/hyperlight_host/src/sandbox/initialized.rs +++ b/src/hyperlight_host/src/sandbox/initialized.rs @@ -21,7 +21,7 @@ use crate::func::{ParameterTuple, SupportedReturnType}; use crate::hypervisor::InterruptHandle; use crate::hypervisor::hyperlight_vm::{HyperlightVm, HyperlightVmError}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; -use crate::mem::mgr::SandboxMemoryManager; +use crate::mem::mgr::{ExecutionMode, SandboxMemoryManager}; use crate::mem::shared_mem::{HostSharedMemory, SharedMemory as _}; use crate::metrics::{ METRIC_GUEST_ERROR, METRIC_GUEST_ERROR_LABEL_CODE, maybe_time_and_emit_guest_call, @@ -477,7 +477,7 @@ impl Sandbox { fn checkpoint_transport_for_snapshot(&mut self) -> Result<()> { self.with_guest_execution(|sbox| { sbox.mem_mgr.begin_snapshot_checkpoint()?; - sbox.dispatch_guest_call()?; + sbox.dispatch_guest_call_restricted()?; sbox.mem_mgr.finish_snapshot_checkpoint()?; @@ -1028,6 +1028,15 @@ impl Sandbox { }) } + /// Dispatch in restricted mode, then restore the previous mode. + fn dispatch_guest_call_restricted(&mut self) -> Result<()> { + let mode = std::mem::replace(&mut self.mem_mgr.execution_mode, ExecutionMode::Restricted); + let res = self.dispatch_guest_call(); + self.mem_mgr.execution_mode = mode; + + res + } + /// Returns a handle for interrupting guest execution. /// /// # Examples @@ -1258,6 +1267,7 @@ mod tests { #[cfg(not(gdb))] use crate::hypervisor::hyperlight_vm::test_support::VmOperation; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; + use crate::mem::mgr::ExecutionMode; use crate::mem::shared_mem::{ExclusiveSharedMemory, GuestSharedMemory, SharedMemory as _}; use crate::sandbox::SandboxConfiguration; use crate::sandbox::uninitialized::{GuestBlob, GuestEnvironment}; @@ -1606,6 +1616,46 @@ mod tests { assert_eq!(sandbox.status(), SandboxStatus::Ready); } + #[test] + fn restricted_dispatch_rejects_host_calls() { + let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf()) + .build() + .unwrap(); + + let call = FunctionCall::new( + "PrintOutput".into(), + Some(vec![ParameterValue::String("checkpoint".into())]), + FunctionCallType::Guest, + i32::TYPE, + ); + sandbox.mem_mgr.write_guest_function_call(&call).unwrap(); + + let error = sandbox.dispatch_guest_call_restricted().unwrap_err(); + + assert!( + error.to_string().contains("during a snapshot checkpoint"), + "{error}" + ); + assert_eq!(sandbox.status(), SandboxStatus::Poisoned); + assert_eq!(sandbox.mem_mgr.execution_mode, ExecutionMode::Unrestricted); + } + + #[test] + fn restore_resets_execution_mode() { + let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf()) + .build() + .unwrap(); + let snapshot = sandbox.snapshot().unwrap(); + + // A panic during restricted dispatch skips the mode reset. + sandbox.mem_mgr.execution_mode = ExecutionMode::Restricted; + sandbox.restore(snapshot).unwrap(); + + sandbox + .call::("PrintOutput", "restored".to_string()) + .unwrap(); + } + #[test] fn guest_execution_preserves_pre_entry_cancellation() { let mut sandbox = SandboxBuilder::from_file(simple_guest_as_pathbuf()) diff --git a/src/hyperlight_host/src/sandbox/outb.rs b/src/hyperlight_host/src/sandbox/outb.rs index 92f70a04e..b0f371f48 100644 --- a/src/hyperlight_host/src/sandbox/outb.rs +++ b/src/hyperlight_host/src/sandbox/outb.rs @@ -16,7 +16,7 @@ use tracing::{Span, instrument}; use super::host_funcs::FunctionRegistry; #[cfg(feature = "mem_profile")] use crate::hypervisor::regs::CommonRegisters; -use crate::mem::mgr::SandboxMemoryManager; +use crate::mem::mgr::{ExecutionMode, SandboxMemoryManager}; use crate::mem::shared_mem::HostSharedMemory; use crate::mem::virtq; #[cfg(feature = "mem_profile")] @@ -36,6 +36,8 @@ pub enum HandleOutbError { InvalidPort(String), #[error("Failed to read host function call: {0}")] ReadHostFunctionCall(String), + #[error("Guest called a host function during a snapshot checkpoint")] + HostCallDuringCheckpoint, #[error("Failed to acquire lock at {0}:{1} - {2}")] LockFailed(&'static str, u32, String), #[error("Failed to write host function response: {0}")] @@ -212,6 +214,7 @@ fn outb_virtq_call( host_funcs: &Arc>, ) -> Result<(), HandleOutbError> { let max_recv_len = mem_mgr.layout.get_g2h_queue_dims().pool_len(); + let mode = mem_mgr.execution_mode; let Some(consumer) = mem_mgr.g2h_consumer.as_mut() else { return Err(HandleOutbError::ReadHostFunctionCall( @@ -233,6 +236,10 @@ fn outb_virtq_call( let header = virtq::read_message_header(&mut request) .map_err(|error| HandleOutbError::ReadHostFunctionCall(error.to_string()))?; + if mode == ExecutionMode::Restricted && header.kind == MsgKind::Request { + return Err(HandleOutbError::HostCallDuringCheckpoint); + } + match header.kind { MsgKind::Request => break (request, reply, header), MsgKind::Log => {