Skip to main content

nub_host_kvm/
error.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
17use std::array::TryFromSliceError;
18use std::cell::{BorrowError, BorrowMutError};
19use std::convert::Infallible;
20use std::error::Error;
21use std::num::TryFromIntError;
22use std::string::FromUtf8Error;
23use std::sync::{MutexGuard, PoisonError};
24use std::time::SystemTimeError;
25
26#[cfg(target_os = "windows")]
27use crossbeam_channel::{RecvError, SendError};
28use hyperlight_common::flatbuffer_wrappers::function_types::{ParameterValue, ReturnValue};
29use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
30use thiserror::Error;
31
32use crate::hypervisor::hyperlight_vm::HyperlightVmError;
33#[cfg(target_os = "windows")]
34use crate::hypervisor::wrappers::HandleWrapper;
35use crate::mem::memory_region::MemoryRegionFlags;
36use crate::mem::ptr::RawPtr;
37
38/// The error type for Hyperlight operations
39#[derive(Error, Debug)]
40pub enum HyperlightError {
41    /// Anyhow error
42    #[error("Anyhow Error was returned: {0}")]
43    AnyhowError(#[from] anyhow::Error),
44    /// Memory access out of bounds
45    #[error("Offset: {0} out of bounds, Max is: {1}")]
46    BoundsCheckFailed(u64, usize),
47
48    /// Checked Add Overflow
49    #[error("Couldn't add offset to base address. Offset: {0}, Base Address: {1}")]
50    CheckedAddOverflow(u64, u64),
51
52    /// Cross beam channel receive error
53    #[error("{0:?}")]
54    #[cfg(target_os = "windows")]
55    CrossBeamReceiveError(#[from] RecvError),
56
57    /// Cross beam channel send error
58    #[error("{0:?}")]
59    #[cfg(target_os = "windows")]
60    CrossBeamSendError(#[from] SendError<HandleWrapper>),
61
62    /// CString conversion error
63    #[error("Error converting CString {0:?}")]
64    CStringConversionError(#[from] std::ffi::NulError),
65
66    /// A generic error with a message
67    #[error("{0}")]
68    Error(String),
69
70    /// Execution violation
71    #[error("Non-executable address {0:#x} tried to be executed")]
72    ExecutionAccessViolation(u64),
73
74    /// Guest execution was cancelled by the host
75    #[error("Execution was cancelled by the host.")]
76    ExecutionCanceledByHost(),
77
78    /// Accessing the value of a flatbuffer parameter failed
79    #[error("Failed to get a value from flat buffer parameter")]
80    FailedToGetValueFromParameter(),
81
82    ///Field Name not found in decoded GuestLogData
83    #[error("Field Name {0} not found in decoded GuestLogData")]
84    FieldIsMissingInGuestLogData(String),
85
86    /// Guest aborted during outb
87    #[error("Guest aborted: {0} {1}")]
88    GuestAborted(u8, String),
89
90    /// Guest call resulted in error in guest
91    #[error("Guest error occurred {0:?}: {1}")]
92    GuestError(ErrorCode, String),
93
94    /// An attempt to cancel guest execution failed because it is hanging on a host function call
95    #[error("Guest execution hung on the execution of a host function call")]
96    GuestExecutionHungOnHostFunctionCall(),
97
98    /// Guest call already in progress
99    #[error("Guest call is already in progress")]
100    GuestFunctionCallAlreadyInProgress(),
101
102    /// The given type is not supported by the guest interface.
103    #[error("Unsupported type: {0}")]
104    GuestInterfaceUnsupportedType(String),
105
106    /// The guest binary was built with a different hyperlight-guest-bin version than the host expects.
107    /// Hyperlight currently provides no backwards compatibility guarantees for guest binaries,
108    /// so the guest and host versions must match exactly. This might change in the future.
109    #[error(
110        "Guest binary was built with hyperlight-guest-bin {guest_bin_version}, \
111         but the host is running hyperlight {host_version}"
112    )]
113    GuestBinVersionMismatch {
114        /// Version of hyperlight-guest-bin the guest was compiled against.
115        guest_bin_version: String,
116        /// Version of hyperlight-host.
117        host_version: String,
118    },
119
120    /// A Host function was called by the guest but it was not registered.
121    #[error("HostFunction {0} was not found")]
122    HostFunctionNotFound(String),
123
124    /// Hyperlight VM error.
125    ///
126    /// **Note:** This error variant is considered internal and its structure is not stable.
127    /// It may change between versions without notice. Users should not rely on this.
128    #[doc(hidden)]
129    #[error("Internal Hyperlight VM error: {0}")]
130    HyperlightVmError(#[from] HyperlightVmError),
131
132    /// Reading Writing or Seeking data failed.
133    #[error("Reading Writing or Seeking data failed {0:?}")]
134    IOError(#[from] std::io::Error),
135
136    /// Failed to convert to Integer
137    #[error("Failed To Convert Size to usize")]
138    IntConversionFailure(#[from] TryFromIntError),
139
140    /// Conversion of str to Json failed
141    #[error("Conversion of str data to json failed")]
142    JsonConversionFailure(#[from] serde_json::Error),
143
144    /// An attempt to get a lock from a Mutex failed.
145    #[error("Unable to lock resource")]
146    LockAttemptFailed(String),
147
148    /// Memory Access Violation at the given address. The access type and memory region flags are provided.
149    #[error("Memory Access Violation at address {0:#x} of type {1}, but memory is marked as {2}")]
150    MemoryAccessViolation(u64, MemoryRegionFlags, MemoryRegionFlags),
151
152    /// Memory Allocation Failed.
153    #[error("Memory Allocation Failed with OS Error {0:?}.")]
154    MemoryAllocationFailed(Option<i32>),
155
156    /// Memory Protection Failed
157    #[error("Memory Protection Failed with OS Error {0:?}.")]
158    MemoryProtectionFailed(Option<i32>),
159
160    /// Memory region size mismatch
161    #[error("Memory region size mismatch: host size {0:?}, guest size {1:?} region {2:?}")]
162    MemoryRegionSizeMismatch(usize, usize, String),
163
164    /// The memory request exceeds the maximum size allowed
165    #[error("Memory requested {0} exceeds maximum size allowed {1}")]
166    MemoryRequestTooBig(usize, usize),
167
168    /// The memory request is too small to contain everything that is
169    /// required
170    #[error("Memory requested {0} is less than the minimum size allowed {1}")]
171    MemoryRequestTooSmall(usize, usize),
172
173    /// Metric Not Found.
174    #[error("Metric Not Found {0:?}.")]
175    MetricNotFound(&'static str),
176
177    /// mmap Failed.
178    #[error("mmap failed with os error {0:?}")]
179    MmapFailed(Option<i32>),
180
181    /// mprotect Failed.
182    #[error("mprotect failed with os error {0:?}")]
183    MprotectFailed(Option<i32>),
184
185    /// No Hypervisor was found for Sandbox.
186    #[error("No Hypervisor was found for Sandbox")]
187    NoHypervisorFound(),
188
189    /// Restore_state called with no valid snapshot
190    #[error("Restore_state called with no valid snapshot")]
191    NoMemorySnapshot,
192
193    /// Failed to get value from parameter value
194    #[error("Failed To Convert Parameter Value {0:?} to {1:?}")]
195    ParameterValueConversionFailure(ParameterValue, &'static str),
196
197    /// a failure occurred processing a PE file
198    #[error("Failure processing PE File {0:?}")]
199    PEFileProcessingFailure(#[from] goblin::error::Error),
200
201    /// The sandbox becomes **poisoned** when the guest is not run to completion, leaving it in
202    /// an inconsistent state that could compromise memory safety, data integrity, or security.
203    ///
204    /// ### When Does Poisoning Occur?
205    ///
206    /// Poisoning happens when guest execution is interrupted before normal completion:
207    ///
208    /// - **Guest panics or aborts** - When a guest function panics, crashes, or calls `abort()`,
209    ///   the normal cleanup and unwinding process is interrupted
210    /// - **Invalid memory access** - Attempts to read/write/execute memory outside allowed regions
211    /// - **Stack overflow** - Guest exhausts its stack space during execution
212    /// - **Heap exhaustion** - Guest runs out of heap memory
213    /// - **Host-initiated cancellation** - Calling `InterruptHandle::kill()` to forcefully
214    ///   terminate an in-progress guest function
215    ///
216    /// ## Recovery
217    ///
218    /// Use `MultiUseSandbox::restore()` to recover from a poisoned sandbox.
219    #[error("The sandbox was poisoned")]
220    PoisonedSandbox,
221
222    /// Raw pointer is less than base address
223    #[error("Raw pointer ({0:?}) was less than the base address ({1})")]
224    RawPointerLessThanBaseAddress(RawPtr, u64),
225
226    /// RefCell borrow failed
227    #[error("RefCell borrow failed")]
228    RefCellBorrowFailed(#[from] BorrowError),
229
230    /// RefCell mut borrow failed
231    #[error("RefCell mut borrow failed")]
232    RefCellMutBorrowFailed(#[from] BorrowMutError),
233
234    /// Failed to get value from return value
235    #[error("Failed To Convert Return Value {0:?} to {1:?}")]
236    ReturnValueConversionFailure(ReturnValue, &'static str),
237
238    /// Tried to restore snapshot to a sandbox that is not the same as the one the snapshot was taken from
239    #[error("Snapshot was taken from a different sandbox")]
240    SnapshotSandboxMismatch,
241
242    /// SystemTimeError
243    #[error("SystemTimeError {0:?}")]
244    SystemTimeError(#[from] SystemTimeError),
245
246    /// Error occurred converting a slice to an array
247    #[error("TryFromSliceError {0:?}")]
248    TryFromSliceError(#[from] TryFromSliceError),
249
250    /// A function was called with an incorrect number of arguments
251    #[error("The number of arguments to the function is wrong: got {0:?} expected {1:?}")]
252    UnexpectedNoOfArguments(usize, usize),
253
254    /// The parameter value type is unexpected
255    #[error("The parameter value type is unexpected got {0:?} expected {1:?}")]
256    UnexpectedParameterValueType(ParameterValue, String),
257
258    /// The return value type is unexpected
259    #[error("The return value type is unexpected got {0:?} expected {1:?}")]
260    UnexpectedReturnValueType(ReturnValue, String),
261
262    /// Slice conversion to UTF8 failed
263    #[error("String Conversion of UTF8 data to str failed")]
264    UTF8StringConversionFailure(#[from] FromUtf8Error),
265
266    /// The capacity of the vector is incorrect
267    #[error(
268        "The capacity of the vector is incorrect. Capacity: {0}, Length: {1}, FlatBuffer Size: {2}"
269    )]
270    VectorCapacityIncorrect(usize, usize, i32),
271
272    /// vmm sys Error Occurred
273    #[error("vmm sys Error {0:?}")]
274    #[cfg(target_os = "linux")]
275    VmmSysError(vmm_sys_util::errno::Error),
276
277    /// Windows Error
278    #[cfg(target_os = "windows")]
279    #[error("Windows API Error Result {0:?}")]
280    WindowsAPIError(#[from] windows_result::Error),
281}
282
283impl From<Infallible> for HyperlightError {
284    fn from(_: Infallible) -> Self {
285        "Impossible as this is an infallible error".into()
286    }
287}
288
289impl From<&str> for HyperlightError {
290    fn from(s: &str) -> Self {
291        HyperlightError::Error(s.to_string())
292    }
293}
294
295impl<T> From<PoisonError<MutexGuard<'_, T>>> for HyperlightError {
296    // Implemented this way rather than passing the error as a source to LockAttemptFailed as that would require
297    // Box<dyn Error + Send + Sync> which is not easy to implement for PoisonError<MutexGuard<'_, T>>
298    // This is a good enough solution and allows use to use the ? operator on lock() calls
299    fn from(e: PoisonError<MutexGuard<'_, T>>) -> Self {
300        let source = match e.source() {
301            Some(s) => s.to_string(),
302            None => String::from(""),
303        };
304        HyperlightError::LockAttemptFailed(source)
305    }
306}
307
308impl HyperlightError {
309    /// Internal helper to determines if the given error has potential to poison the sandbox.
310    ///
311    /// Errors that poison the sandbox are those that can leave the sandbox in an inconsistent
312    /// state where memory, resources, or data structures may be corrupted or leaked. Usually
313    /// due to the guest not running to completion.
314    ///
315    /// Post-Stage-F: poison-tracking on `MultiUseSandbox` was removed along
316    /// with `snapshot()` / `restore()`. This method is unused by the host
317    /// driver — callers drop the sandbox and rebuild on error. It is kept
318    /// (and `dead_code`-allowed) only so the inner
319    /// `DispatchGuestCallError::is_poison_error` chain still type-checks.
320    #[allow(dead_code)]
321    pub(crate) fn is_poison_error(&self) -> bool {
322        // wildcard _ or matches! not used here purposefully to ensure that new error variants
323        // are explicitly considered for poisoning behavior.
324        match self {
325            // These errors poison the sandbox because they can leave it in an inconsistent state due
326            // to the guest not running to completion.
327            HyperlightError::GuestAborted(_, _)
328            | HyperlightError::ExecutionCanceledByHost()
329            | HyperlightError::PoisonedSandbox
330            | HyperlightError::ExecutionAccessViolation(_)
331            | HyperlightError::MemoryAccessViolation(_, _, _)
332            | HyperlightError::MemoryRegionSizeMismatch(_, _, _)
333            // HyperlightVmError::Restore is already handled manually in restore(), but we mark it
334            // as poisoning here too for defense in depth.
335            | HyperlightError::HyperlightVmError(HyperlightVmError::Restore(_)) => true,
336
337            // These errors poison the sandbox because they can leave
338            // it in an inconsistent state due to snapshot restore
339            // failing partway through
340            HyperlightError::HyperlightVmError(HyperlightVmError::UpdateRegion(_))
341            | HyperlightError::HyperlightVmError(HyperlightVmError::AccessPageTable(_)) => true,
342
343            // HyperlightVmError::DispatchGuestCall may poison the sandbox
344            HyperlightError::HyperlightVmError(HyperlightVmError::DispatchGuestCall(e)) => {
345                e.is_poison_error()
346            }
347
348            // All other errors do not poison the sandbox.
349            HyperlightError::AnyhowError(_)
350            | HyperlightError::BoundsCheckFailed(_, _)
351            | HyperlightError::CheckedAddOverflow(_, _)
352            | HyperlightError::CStringConversionError(_)
353            | HyperlightError::Error(_)
354            | HyperlightError::FailedToGetValueFromParameter()
355            | HyperlightError::FieldIsMissingInGuestLogData(_)
356            | HyperlightError::GuestBinVersionMismatch { .. }
357            | HyperlightError::GuestError(_, _)
358            | HyperlightError::GuestExecutionHungOnHostFunctionCall()
359            | HyperlightError::GuestFunctionCallAlreadyInProgress()
360            | HyperlightError::GuestInterfaceUnsupportedType(_)
361            | HyperlightError::HostFunctionNotFound(_)
362            | HyperlightError::HyperlightVmError(HyperlightVmError::Create(_))
363            | HyperlightError::HyperlightVmError(HyperlightVmError::Initialize(_))
364            | HyperlightError::HyperlightVmError(HyperlightVmError::MapRegion(_))
365            | HyperlightError::HyperlightVmError(HyperlightVmError::UnmapRegion(_))
366            | HyperlightError::IOError(_)
367            | HyperlightError::IntConversionFailure(_)
368            | HyperlightError::JsonConversionFailure(_)
369            | HyperlightError::LockAttemptFailed(_)
370            | HyperlightError::MemoryAllocationFailed(_)
371            | HyperlightError::MemoryProtectionFailed(_)
372            | HyperlightError::MemoryRequestTooBig(_, _)
373            | HyperlightError::MemoryRequestTooSmall(_, _)
374            | HyperlightError::MetricNotFound(_)
375            | HyperlightError::MmapFailed(_)
376            | HyperlightError::MprotectFailed(_)
377            | HyperlightError::NoHypervisorFound()
378            | HyperlightError::NoMemorySnapshot
379            | HyperlightError::ParameterValueConversionFailure(_, _)
380            | HyperlightError::PEFileProcessingFailure(_)
381            | HyperlightError::RawPointerLessThanBaseAddress(_, _)
382            | HyperlightError::RefCellBorrowFailed(_)
383            | HyperlightError::RefCellMutBorrowFailed(_)
384            | HyperlightError::ReturnValueConversionFailure(_, _)
385            | HyperlightError::SnapshotSandboxMismatch
386            | HyperlightError::SystemTimeError(_)
387            | HyperlightError::TryFromSliceError(_)
388            | HyperlightError::UnexpectedNoOfArguments(_, _)
389            | HyperlightError::UnexpectedParameterValueType(_, _)
390            | HyperlightError::UnexpectedReturnValueType(_, _)
391            | HyperlightError::UTF8StringConversionFailure(_)
392            | HyperlightError::VectorCapacityIncorrect(_, _, _) => false,
393
394            #[cfg(target_os = "windows")]
395            HyperlightError::CrossBeamReceiveError(_) => false,
396            #[cfg(target_os = "windows")]
397            HyperlightError::CrossBeamSendError(_) => false,
398            #[cfg(target_os = "windows")]
399            HyperlightError::WindowsAPIError(_) => false,
400            #[cfg(target_os = "linux")]
401            HyperlightError::VmmSysError(_) => false,
402        }
403    }
404}
405
406/// Creates a `HyperlightError::Error` from a string literal or format string
407#[macro_export]
408macro_rules! new_error {
409    ($msg:literal $(,)?) => {{
410        let __args = std::format_args!($msg);
411        let __err_msg = match __args.as_str() {
412            Some(msg) => String::from(msg),
413            None => std::format!($msg),
414        };
415        $crate::HyperlightError::Error(__err_msg)
416    }};
417    ($fmtstr:expr, $($arg:tt)*) => {{
418           let __err_msg = std::format!($fmtstr, $($arg)*);
419           $crate::error::HyperlightError::Error(__err_msg)
420    }};
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426    use crate::hypervisor::hyperlight_vm::{
427        DispatchGuestCallError, HandleIoError, HyperlightVmError, RunVmError,
428    };
429    use crate::sandbox::outb::HandleOutbError;
430
431    /// Test that ExecutionCancelledByHost promotes to HyperlightError::ExecutionCanceledByHost
432    #[test]
433    fn test_promote_execution_cancelled_by_host() {
434        let err = DispatchGuestCallError::Run(RunVmError::ExecutionCancelledByHost);
435        let (promoted, should_poison) = err.promote();
436
437        assert!(
438            should_poison,
439            "ExecutionCancelledByHost should poison the sandbox"
440        );
441        assert!(
442            matches!(promoted, HyperlightError::ExecutionCanceledByHost()),
443            "Expected HyperlightError::ExecutionCanceledByHost, got {:?}",
444            promoted
445        );
446    }
447
448    /// Test that GuestAborted promotes to HyperlightError::GuestAborted with correct values
449    #[test]
450    fn test_promote_guest_aborted() {
451        let err = DispatchGuestCallError::Run(RunVmError::HandleIo(HandleIoError::Outb(
452            HandleOutbError::GuestAborted {
453                code: 42,
454                message: "test abort".to_string(),
455            },
456        )));
457        let (promoted, should_poison) = err.promote();
458
459        assert!(should_poison, "GuestAborted should poison the sandbox");
460        match promoted {
461            HyperlightError::GuestAborted(code, msg) => {
462                assert_eq!(code, 42);
463                assert_eq!(msg, "test abort");
464            }
465            _ => panic!("Expected HyperlightError::GuestAborted, got {:?}", promoted),
466        }
467    }
468
469    /// Test that MemoryAccessViolation promotes to HyperlightError::MemoryAccessViolation
470    #[test]
471    fn test_promote_memory_access_violation() {
472        let err = DispatchGuestCallError::Run(RunVmError::MemoryAccessViolation {
473            addr: 0xDEADBEEF,
474            access_type: MemoryRegionFlags::WRITE,
475            region_flags: MemoryRegionFlags::READ,
476        });
477        let (promoted, should_poison) = err.promote();
478
479        assert!(
480            should_poison,
481            "MemoryAccessViolation should poison the sandbox"
482        );
483        match promoted {
484            HyperlightError::MemoryAccessViolation(addr, access_type, region_flags) => {
485                assert_eq!(addr, 0xDEADBEEF);
486                assert_eq!(access_type, MemoryRegionFlags::WRITE);
487                assert_eq!(region_flags, MemoryRegionFlags::READ);
488            }
489            _ => panic!(
490                "Expected HyperlightError::MemoryAccessViolation, got {:?}",
491                promoted
492            ),
493        }
494    }
495
496    /// Test that non-promoted Run errors are wrapped in HyperlightVmError
497    #[test]
498    fn test_promote_other_run_errors_wrapped() {
499        let err = DispatchGuestCallError::Run(RunVmError::MmioReadUnmapped(0x1000));
500        let (promoted, should_poison) = err.promote();
501
502        assert!(should_poison, "Run errors should poison the sandbox");
503        assert!(
504            matches!(
505                promoted,
506                HyperlightError::HyperlightVmError(HyperlightVmError::DispatchGuestCall(_))
507            ),
508            "Expected HyperlightError::HyperlightVmError, got {:?}",
509            promoted
510        );
511    }
512}