Skip to content

Commit 47aa821

Browse files
authored
Add unrecoverable sandbox state
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent e65c966 commit 47aa821

10 files changed

Lines changed: 762 additions & 106 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55
## [Prerelease] - Unreleased
66

77
### Added
8+
* Add `MultiUseSandbox::status()`, which returns `SandboxStatus` for inspecting sandbox lifecycle state.
89

910
### Changed
1011
* **Breaking:** Guest MSR state is now saved and restored across snapshots.
@@ -13,10 +14,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
1314
resets to a clean default. On KVM the guest may only read or write declared
1415
MSRs, on MSHV and WHP this is not enforced. by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/991
1516
* **Breaking:** Filesystem paths are now represented using `PathBuf`. `GuestBinary::FilePath` now stores a `PathBuf` instead of a `String`, and `MultiUseSandbox::generate_crashdump_to_dir` accepts `Into<PathBuf>` instead of `Into<String>`. Callers passing a `String` to `GuestBinary::FilePath` must convert it using `.into()`.
17+
* Deprecate `MultiUseSandbox::poisoned` in favor of `MultiUseSandbox::status().is_poisoned()`.
1618

1719
### Removed
1820

1921
### Fixed
22+
* Mark a sandbox unrecoverable when snapshot restore fails while updating its VM mappings.
2023
* Fix symbol resolution in guest core dumps for sandboxes created from snapshots by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/1618
2124
* Reject malformed OCI snapshot metadata and non-regular artifact files during load.
2225
* Reset XCR0 during x86 snapshot restore.

src/hyperlight_host/src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ pub enum HyperlightError {
224224
#[error("The sandbox was poisoned")]
225225
PoisonedSandbox,
226226

227+
/// The sandbox cannot safely perform further operations.
228+
#[error("The sandbox is unrecoverable and must be discarded")]
229+
UnrecoverableSandbox,
230+
227231
/// Raw pointer is less than base address
228232
#[error("Raw pointer ({0:?}) was less than the base address ({1})")]
229233
RawPointerLessThanBaseAddress(RawPtr, u64),
@@ -408,6 +412,7 @@ impl HyperlightError {
408412
| HyperlightError::UnexpectedNoOfArguments(_, _)
409413
| HyperlightError::UnexpectedParameterValueType(_, _)
410414
| HyperlightError::UnexpectedReturnValueType(_, _)
415+
| HyperlightError::UnrecoverableSandbox
411416
| HyperlightError::UTF8StringConversionFailure(_)
412417
| HyperlightError::VectorCapacityIncorrect(_, _, _) => false,
413418

src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ mod x86_64;
1919

2020
#[cfg(target_arch = "aarch64")]
2121
mod aarch64;
22+
#[cfg(all(test, not(gdb), any(kvm, mshv3, target_os = "windows")))]
23+
pub(crate) mod test_support;
2224
#[cfg(gdb)]
2325
use std::collections::HashMap;
2426
use std::str::FromStr;
@@ -532,11 +534,12 @@ impl HyperlightVm {
532534
let guest_base = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64;
533535
let rgn = snapshot.mapping_at(guest_base, MemoryRegionType::Snapshot);
534536

535-
if let Some(old_snapshot) = self.snapshot_memory.replace(snapshot) {
537+
if let Some(old_snapshot) = self.snapshot_memory.as_ref() {
536538
let old_rgn = old_snapshot.mapping_at(guest_base, MemoryRegionType::Snapshot);
537539
self.vm.unmap_memory((self.snapshot_slot, &old_rgn))?;
538540
}
539541
unsafe { self.vm.map_memory((self.snapshot_slot, &rgn))? };
542+
self.snapshot_memory = Some(snapshot);
540543

541544
Ok(())
542545
}
@@ -549,12 +552,13 @@ impl HyperlightVm {
549552
let guest_base = hyperlight_common::layout::scratch_base_gpa(scratch.mem_size());
550553
let rgn = scratch.mapping_at(guest_base, MemoryRegionType::Scratch);
551554

552-
if let Some(old_scratch) = self.scratch_memory.replace(scratch) {
555+
if let Some(old_scratch) = self.scratch_memory.as_ref() {
553556
let old_base = hyperlight_common::layout::scratch_base_gpa(old_scratch.mem_size());
554557
let old_rgn = old_scratch.mapping_at(old_base, MemoryRegionType::Scratch);
555558
self.vm.unmap_memory((self.scratch_slot, &old_rgn))?;
556559
}
557560
unsafe { self.vm.map_memory((self.scratch_slot, &rgn))? };
561+
self.scratch_memory = Some(scratch);
558562

559563
Ok(())
560564
}
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
/*
2+
Copyright 2025 The Hyperlight Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
use std::collections::VecDeque;
18+
19+
use super::*;
20+
#[cfg(target_arch = "x86_64")]
21+
use crate::hypervisor::regs::MsrEntry;
22+
use crate::hypervisor::regs::{
23+
CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters,
24+
};
25+
use crate::hypervisor::virtual_machine::{CreateVmError, HypervisorError};
26+
27+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28+
pub(crate) enum VmOperation {
29+
Map(MemoryRegionType),
30+
Unmap(MemoryRegionType),
31+
#[cfg(target_arch = "x86_64")]
32+
SetRegs,
33+
#[cfg(target_arch = "x86_64")]
34+
SetDebugRegs,
35+
#[cfg(target_arch = "x86_64")]
36+
ResetXsave,
37+
#[cfg(target_arch = "x86_64")]
38+
SetSregs,
39+
#[cfg(target_arch = "x86_64")]
40+
SetMsrs,
41+
#[cfg(target_arch = "aarch64")]
42+
ResetVcpu,
43+
}
44+
45+
#[derive(Clone, Debug)]
46+
pub(crate) struct VmFaultPlan {
47+
operations: Arc<Mutex<VecDeque<VmOperation>>>,
48+
}
49+
50+
impl VmFaultPlan {
51+
fn new(operations: impl IntoIterator<Item = VmOperation>) -> Self {
52+
Self {
53+
operations: Arc::new(Mutex::new(operations.into_iter().collect())),
54+
}
55+
}
56+
57+
pub(crate) fn is_consumed(&self) -> bool {
58+
self.operations.lock().unwrap().is_empty()
59+
}
60+
61+
fn should_fail(&self, operation: VmOperation) -> bool {
62+
let mut operations = self.operations.lock().unwrap();
63+
if operations.front() == Some(&operation) {
64+
operations.pop_front();
65+
true
66+
} else {
67+
false
68+
}
69+
}
70+
}
71+
72+
#[derive(Debug)]
73+
struct FaultInjectingVirtualMachine {
74+
inner: Option<Box<dyn VirtualMachine>>,
75+
fault_plan: VmFaultPlan,
76+
}
77+
78+
impl FaultInjectingVirtualMachine {
79+
fn new(
80+
inner: Box<dyn VirtualMachine>,
81+
operations: impl IntoIterator<Item = VmOperation>,
82+
) -> (Self, VmFaultPlan) {
83+
let fault_plan = VmFaultPlan::new(operations);
84+
(
85+
Self {
86+
inner: Some(inner),
87+
fault_plan: fault_plan.clone(),
88+
},
89+
fault_plan,
90+
)
91+
}
92+
93+
fn placeholder() -> Self {
94+
Self {
95+
inner: None,
96+
fault_plan: VmFaultPlan::new([]),
97+
}
98+
}
99+
100+
fn inner(&self) -> &dyn VirtualMachine {
101+
self.inner.as_deref().expect("placeholder VM was used")
102+
}
103+
104+
fn inner_mut(&mut self) -> &mut dyn VirtualMachine {
105+
self.inner.as_deref_mut().expect("placeholder VM was used")
106+
}
107+
108+
fn should_fail(&self, operation: VmOperation) -> bool {
109+
self.fault_plan.should_fail(operation)
110+
}
111+
112+
fn injected_error() -> HypervisorError {
113+
#[cfg(kvm)]
114+
let error = kvm_ioctls::Error::new(libc::EIO);
115+
#[cfg(all(not(kvm), mshv3))]
116+
let error = mshv_ioctls::MshvError::from(libc::EIO);
117+
#[cfg(target_os = "windows")]
118+
let error = windows_result::Error::from_hresult(windows_result::HRESULT::from_win32(5));
119+
error.into()
120+
}
121+
}
122+
123+
impl VirtualMachine for FaultInjectingVirtualMachine {
124+
unsafe fn map_memory(
125+
&mut self,
126+
region: (u32, &MemoryRegion),
127+
) -> std::result::Result<(), MapMemoryError> {
128+
if self.should_fail(VmOperation::Map(region.1.region_type)) {
129+
return Err(MapMemoryError::Hypervisor(Self::injected_error()));
130+
}
131+
// SAFETY: The decorator forwards the caller's preconditions unchanged.
132+
unsafe { self.inner_mut().map_memory(region) }
133+
}
134+
135+
fn unmap_memory(
136+
&mut self,
137+
region: (u32, &MemoryRegion),
138+
) -> std::result::Result<(), UnmapMemoryError> {
139+
if self.should_fail(VmOperation::Unmap(region.1.region_type)) {
140+
return Err(UnmapMemoryError::Hypervisor(Self::injected_error()));
141+
}
142+
self.inner_mut().unmap_memory(region)
143+
}
144+
145+
fn run_vcpu(
146+
&mut self,
147+
#[cfg(feature = "trace_guest")] tc: &mut crate::sandbox::trace::TraceContext,
148+
) -> std::result::Result<VmExit, RunVcpuError> {
149+
self.inner_mut().run_vcpu(
150+
#[cfg(feature = "trace_guest")]
151+
tc,
152+
)
153+
}
154+
155+
fn regs(&self) -> std::result::Result<CommonRegisters, RegisterError> {
156+
self.inner().regs()
157+
}
158+
159+
fn set_regs(&self, regs: &CommonRegisters) -> std::result::Result<(), RegisterError> {
160+
#[cfg(target_arch = "x86_64")]
161+
if self.should_fail(VmOperation::SetRegs) {
162+
return Err(RegisterError::SetRegs(Self::injected_error()));
163+
}
164+
self.inner().set_regs(regs)
165+
}
166+
167+
fn fpu(&self) -> std::result::Result<CommonFpu, RegisterError> {
168+
self.inner().fpu()
169+
}
170+
171+
fn set_fpu(&self, fpu: &CommonFpu) -> std::result::Result<(), RegisterError> {
172+
self.inner().set_fpu(fpu)
173+
}
174+
175+
fn sregs(&self) -> std::result::Result<CommonSpecialRegisters, RegisterError> {
176+
self.inner().sregs()
177+
}
178+
179+
fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> std::result::Result<(), RegisterError> {
180+
#[cfg(target_arch = "x86_64")]
181+
if self.should_fail(VmOperation::SetSregs) {
182+
return Err(RegisterError::SetSregs(Self::injected_error()));
183+
}
184+
self.inner().set_sregs(sregs)
185+
}
186+
187+
fn debug_regs(&self) -> std::result::Result<CommonDebugRegs, RegisterError> {
188+
self.inner().debug_regs()
189+
}
190+
191+
fn set_debug_regs(&self, drs: &CommonDebugRegs) -> std::result::Result<(), RegisterError> {
192+
#[cfg(target_arch = "x86_64")]
193+
if self.should_fail(VmOperation::SetDebugRegs) {
194+
return Err(RegisterError::SetDebugRegs(Self::injected_error()));
195+
}
196+
self.inner().set_debug_regs(drs)
197+
}
198+
199+
#[cfg(target_arch = "x86_64")]
200+
fn msrs(&self, indices: &[u32]) -> std::result::Result<Vec<MsrEntry>, RegisterError> {
201+
self.inner().msrs(indices)
202+
}
203+
204+
#[cfg(target_arch = "x86_64")]
205+
fn set_msrs(&self, msrs: &[MsrEntry]) -> std::result::Result<(), RegisterError> {
206+
if self.should_fail(VmOperation::SetMsrs) {
207+
return Err(RegisterError::SetMsrs(Self::injected_error()));
208+
}
209+
self.inner().set_msrs(msrs)
210+
}
211+
212+
#[cfg(target_arch = "x86_64")]
213+
fn msr_reset_indices(
214+
&self,
215+
guest_msrs: &[u32],
216+
) -> std::result::Result<Vec<u32>, CreateVmError> {
217+
self.inner().msr_reset_indices(guest_msrs)
218+
}
219+
220+
#[cfg(not(target_arch = "aarch64"))]
221+
fn xsave(&self) -> std::result::Result<Vec<u8>, RegisterError> {
222+
self.inner().xsave()
223+
}
224+
225+
#[cfg(not(target_arch = "aarch64"))]
226+
fn reset_xsave(&self) -> std::result::Result<(), RegisterError> {
227+
#[cfg(target_arch = "x86_64")]
228+
if self.should_fail(VmOperation::ResetXsave) {
229+
return Err(RegisterError::SetXsave(Self::injected_error()));
230+
}
231+
self.inner().reset_xsave()
232+
}
233+
234+
#[cfg(not(target_arch = "aarch64"))]
235+
fn set_xsave(&self, xsave: &[u32]) -> std::result::Result<(), RegisterError> {
236+
self.inner().set_xsave(xsave)
237+
}
238+
239+
#[cfg(all(test, target_arch = "x86_64"))]
240+
fn xcr0(&self) -> std::result::Result<u64, RegisterError> {
241+
self.inner().xcr0()
242+
}
243+
244+
#[cfg(target_arch = "x86_64")]
245+
fn set_xcr0(&self, value: u64) -> std::result::Result<(), RegisterError> {
246+
self.inner().set_xcr0(value)
247+
}
248+
249+
#[cfg(target_arch = "aarch64")]
250+
fn can_reset_vcpu(&self) -> bool {
251+
self.inner().can_reset_vcpu()
252+
}
253+
254+
#[cfg(target_arch = "aarch64")]
255+
fn reset_vcpu(&mut self) -> std::result::Result<(), ResetVcpuError> {
256+
if self.should_fail(VmOperation::ResetVcpu) {
257+
return Err(ResetVcpuError::Hypervisor(Self::injected_error()));
258+
}
259+
self.inner_mut().reset_vcpu()
260+
}
261+
262+
#[cfg(target_os = "windows")]
263+
fn partition_handle(&self) -> windows::Win32::System::Hypervisor::WHV_PARTITION_HANDLE {
264+
self.inner().partition_handle()
265+
}
266+
}
267+
268+
impl HyperlightVm {
269+
pub(crate) fn inject_vm_faults(
270+
&mut self,
271+
operations: impl IntoIterator<Item = VmOperation>,
272+
) -> VmFaultPlan {
273+
let placeholder = Box::new(FaultInjectingVirtualMachine::placeholder());
274+
let inner = std::mem::replace(&mut self.vm, placeholder);
275+
let (vm, fault_plan) = FaultInjectingVirtualMachine::new(inner, operations);
276+
self.vm = Box::new(vm);
277+
fault_plan
278+
}
279+
280+
#[allow(clippy::type_complexity, reason = "test-only mapping state")]
281+
pub(crate) fn base_mapping_state(&self) -> (Option<(usize, usize)>, Option<(usize, usize)>) {
282+
let snapshot = self
283+
.snapshot_memory
284+
.as_ref()
285+
.map(|memory| (memory.base_addr(), memory.mem_size()));
286+
let scratch = self
287+
.scratch_memory
288+
.as_ref()
289+
.map(|memory| (memory.base_addr(), memory.mem_size()));
290+
(snapshot, scratch)
291+
}
292+
}

src/hyperlight_host/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ pub use hypervisor::virtual_machine::is_hypervisor_present;
8989
/// A sandbox that can call be used to make multiple calls to guest functions,
9090
/// and otherwise reused multiple times
9191
pub use sandbox::MultiUseSandbox;
92+
/// The lifecycle state of a [`MultiUseSandbox`].
93+
pub use sandbox::SandboxStatus;
9294
/// The re-export for the `UninitializedSandbox` type
9395
pub use sandbox::UninitializedSandbox;
9496
/// A collection of host functions that can be supplied to a sandbox

0 commit comments

Comments
 (0)