Skip to main content

hyperlight_host/hypervisor/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4/// GDB debugging support
5#[cfg(gdb)]
6pub(crate) mod gdb;
7
8/// Abstracts over different hypervisor register representations
9pub(crate) mod regs;
10
11pub(crate) mod virtual_machine;
12
13#[cfg(target_os = "windows")]
14/// Hyperlight Surrogate Process
15pub(crate) mod surrogate_process;
16#[cfg(target_os = "windows")]
17/// Hyperlight Surrogate Process
18pub(crate) mod surrogate_process_manager;
19/// Safe wrappers around windows types like `PSTR`
20#[cfg(target_os = "windows")]
21pub mod wrappers;
22
23#[cfg(crashdump)]
24pub(crate) mod crashdump;
25
26pub(crate) mod hyperlight_vm;
27
28use std::fmt::Debug;
29#[cfg(any(kvm, mshv3))]
30use std::sync::atomic::{AtomicBool, AtomicU64};
31use std::sync::atomic::{AtomicU8, Ordering};
32#[cfg(any(kvm, mshv3, hvf))]
33use std::time::Duration;
34
35#[derive(Debug)]
36pub(crate) struct InterruptHandleStateMachine(AtomicU8);
37impl InterruptHandleStateMachine {
38    const RUNNING_BIT: u8 = 1 << 1;
39    const CANCEL_BIT: u8 = 1 << 0;
40    #[cfg(gdb)]
41    const DEBUG_INTERRUPT_BIT: u8 = 1 << 2;
42
43    fn new() -> Self {
44        Self(AtomicU8::new(0))
45    }
46
47    /// Set the running state
48    pub(crate) fn set_running(&self) {
49        // Release ordering to ensure that the tid store (which uses Release)
50        // is visible to any thread that observes running=true via Acquire ordering.
51        // This prevents the interrupt thread from reading a stale tid value.
52        self.0.fetch_or(Self::RUNNING_BIT, Ordering::Release);
53    }
54
55    /// Clear the running state
56    pub(crate) fn clear_running(&self) {
57        // Release ordering to ensure all vcpu operations are visible before clearing running
58        self.0.fetch_and(!Self::RUNNING_BIT, Ordering::Release);
59    }
60
61    /// Check if cancellation was requested
62    pub(crate) fn is_cancelled(&self) -> bool {
63        self.get_running_cancel_debug().1
64    }
65
66    /// Set the cancellation request flag
67    fn set_cancel(&self) {
68        // Release ordering ensures that any writes before kill() are visible to the vcpu thread
69        // when it checks is_cancelled() with Acquire ordering
70        self.0.fetch_or(Self::CANCEL_BIT, Ordering::Release);
71    }
72
73    /// Clear the cancellation request flag
74    fn clear_cancel(&self) {
75        // Release ordering to ensure that any operations from the previous run()
76        // are visible to other threads. While this is typically called by the vcpu thread
77        // at the start of run(), the VM itself can move between threads across guest calls.
78        self.0.fetch_and(!Self::CANCEL_BIT, Ordering::Release);
79    }
80
81    /// Check if debug interrupt was requested (always returns false when gdb feature is disabled)
82    pub(crate) fn is_debug_interrupted(&self) -> bool {
83        #[cfg(gdb)]
84        {
85            self.get_running_cancel_debug().2
86        }
87        #[cfg(not(gdb))]
88        {
89            false
90        }
91    }
92
93    /// Clear the debug interrupt request flag
94    #[cfg(gdb)]
95    fn set_debug_interrupt(&self) {
96        self.0
97            .fetch_or(Self::DEBUG_INTERRUPT_BIT, Ordering::Release);
98    }
99
100    /// Clear the debug interrupt request flag
101    #[cfg(gdb)]
102    fn clear_debug_interrupt(&self) {
103        self.0
104            .fetch_and(!Self::DEBUG_INTERRUPT_BIT, Ordering::Release);
105    }
106
107    /// Get the running, cancel and debug flags atomically.
108    fn get_running_cancel_debug(&self) -> (bool, bool, bool) {
109        let state = self.0.load(Ordering::Acquire);
110        let running = state & Self::RUNNING_BIT != 0;
111        let cancel = state & Self::CANCEL_BIT != 0;
112        #[cfg(gdb)]
113        let debug = state & Self::DEBUG_INTERRUPT_BIT != 0;
114        #[cfg(not(gdb))]
115        let debug = false;
116        (running, cancel, debug)
117    }
118}
119
120/// A trait for platform-specific interrupt handle implementation details
121pub(crate) trait InterruptHandleImpl: InterruptHandle {
122    /// Set the thread ID for the vcpu thread
123    #[cfg(any(kvm, mshv3))]
124    fn set_tid(&self);
125
126    /// Set the currently-executing vcpu id
127    #[cfg(hvf)]
128    fn set_vcpu(&self, vcpu: Option<hv_vcpu_t>);
129
130    /// Mark the handle as dropped
131    fn set_dropped(&self);
132}
133
134pub(crate) trait InterruptHandleInternal {
135    /// Local access the shared state, which does not perform any
136    /// operations other than updating the state machine
137    fn state(&self) -> &InterruptHandleStateMachine;
138    /// Trigger the actual kill-like operation without
139    /// modifying the state
140    fn common_kill(&self) -> bool;
141}
142
143/// A trait for handling interrupts to a sandbox's vcpu
144#[allow(private_bounds)]
145pub trait InterruptHandle: Send + Sync + Debug + InterruptHandleInternal {
146    /// Interrupt the corresponding sandbox from running.
147    ///
148    /// - If this is called while the the sandbox currently executing a guest function call, it will interrupt the sandbox and return `true`.
149    /// - If this is called while the sandbox is not running (for example before or after calling a guest function), it will do nothing and return `false`.
150    ///
151    /// # Note
152    /// This function will block for the duration of the time it takes for the vcpu thread to be interrupted.
153    fn kill(&self) -> bool {
154        self.state().set_cancel();
155        self.common_kill()
156    }
157
158    /// Used by a debugger to interrupt the corresponding sandbox from running.
159    ///
160    /// - If this is called while the vcpu is running, then it will interrupt the vcpu and return `true`.
161    /// - If this is called while the vcpu is not running, (for example during a host call), the
162    ///   vcpu will not immediately be interrupted, but will prevent the vcpu from running **the next time**
163    ///   it's scheduled, and returns `false`.
164    ///
165    /// # Note
166    /// This function will block for the duration of the time it takes for the vcpu thread to be interrupted.
167    #[cfg(gdb)]
168    fn kill_from_debugger(&self) -> bool {
169        self.state().set_debug_interrupt();
170        self.common_kill()
171    }
172
173    /// Returns true if the corresponding sandbox has been dropped
174    fn dropped(&self) -> bool;
175}
176
177#[cfg(any(kvm, mshv3, hvf))]
178#[derive(Debug)]
179pub(super) struct RetryingInterruptHandle<T: InterruptHandleImpl> {
180    retry_delay: Duration,
181    inner: T,
182}
183
184#[cfg(any(kvm, mshv3, hvf))]
185impl<T: InterruptHandleImpl> InterruptHandleImpl for RetryingInterruptHandle<T> {
186    #[cfg(any(kvm, mshv3))]
187    fn set_tid(&self) {
188        self.inner.set_tid();
189    }
190
191    #[cfg(hvf)]
192    fn set_vcpu(&self, vcpu: Option<hv_vcpu_t>) {
193        self.inner.set_vcpu(vcpu);
194    }
195
196    fn set_dropped(&self) {
197        self.inner.set_dropped();
198    }
199}
200#[cfg(any(kvm, mshv3, hvf))]
201impl<T: InterruptHandleImpl> InterruptHandle for RetryingInterruptHandle<T> {
202    fn dropped(&self) -> bool {
203        self.inner.dropped()
204    }
205}
206#[cfg(any(kvm, mshv3, hvf))]
207impl<T: InterruptHandleImpl> InterruptHandleInternal for RetryingInterruptHandle<T> {
208    fn state(&self) -> &InterruptHandleStateMachine {
209        self.inner.state()
210    }
211    fn common_kill(&self) -> bool {
212        let mut succeeded = false;
213        loop {
214            let (running, cancel, debug) = self.state().get_running_cancel_debug();
215            // Check if we should continue sending signals
216            // Exit if not running OR if neither cancel nor debug_interrupt is set
217            let should_continue = running && (cancel || debug);
218            if !should_continue {
219                break;
220            }
221            tracing::info!("Trying to kill vcpu thread...");
222            succeeded |= self.inner.common_kill();
223            std::thread::sleep(self.retry_delay);
224        }
225        succeeded
226    }
227}
228
229#[cfg(any(kvm, mshv3))]
230#[derive(Debug)]
231pub(super) struct LinuxInterruptHandleState {
232    state: InterruptHandleStateMachine,
233
234    /// Thread ID where the vcpu is running.
235    ///
236    /// Note: Multiple VMs may have the same `tid` (same thread runs multiple sandboxes sequentially),
237    /// but at most one VM will have RUNNING_BIT set at any given time.
238    tid: AtomicU64,
239
240    /// Whether the corresponding VM has been dropped.
241    dropped: AtomicBool,
242
243    /// Offset from SIGRTMIN for the signal used to interrupt the vcpu thread.
244    sig_rt_min_offset: u8,
245}
246#[cfg(any(kvm, mshv3))]
247pub(super) type LinuxInterruptHandle = RetryingInterruptHandle<LinuxInterruptHandleState>;
248
249#[cfg(any(kvm, mshv3))]
250impl LinuxInterruptHandle {
251    fn new(config: &crate::sandbox::SandboxConfiguration) -> Self {
252        RetryingInterruptHandle {
253            retry_delay: config.get_interrupt_retry_delay(),
254            inner: LinuxInterruptHandleState {
255                state: InterruptHandleStateMachine::new(),
256                tid: AtomicU64::new(unsafe { libc::pthread_self() as u64 }),
257                sig_rt_min_offset: config.get_interrupt_vcpu_sigrtmin_offset(),
258                dropped: AtomicBool::new(false),
259            },
260        }
261    }
262}
263
264#[cfg(any(kvm, mshv3))]
265impl InterruptHandleImpl for LinuxInterruptHandleState {
266    fn set_tid(&self) {
267        // Release ordering to synchronize with the Acquire load of `running` in send_signal()
268        // This ensures that when send_signal() observes RUNNING_BIT=true (via Acquire),
269        // it also sees the correct tid value stored here
270        self.tid
271            .store(unsafe { libc::pthread_self() as u64 }, Ordering::Release);
272    }
273
274    fn set_dropped(&self) {
275        // Release ordering to ensure all VM cleanup operations are visible
276        // to any thread that checks dropped() via Acquire
277        self.dropped.store(true, Ordering::Release);
278    }
279}
280
281#[cfg(any(kvm, mshv3))]
282impl InterruptHandle for LinuxInterruptHandleState {
283    fn dropped(&self) -> bool {
284        // Acquire ordering to synchronize with the Release in set_dropped()
285        // This ensures we see all VM cleanup operations that happened before drop
286        self.dropped.load(Ordering::Acquire)
287    }
288}
289
290#[cfg(any(kvm, mshv3))]
291impl InterruptHandleInternal for LinuxInterruptHandleState {
292    fn state(&self) -> &InterruptHandleStateMachine {
293        &self.state
294    }
295    fn common_kill(&self) -> bool {
296        let signal_number = libc::SIGRTMIN() + self.sig_rt_min_offset as libc::c_int;
297        unsafe {
298            libc::pthread_kill(self.tid.load(Ordering::Acquire) as _, signal_number);
299        }
300        true
301    }
302}
303
304#[cfg(any(target_os = "windows", hvf))]
305#[derive(Debug)]
306/// An interrupt handle that captures the pattern that requests to
307/// cancel need to be mutually exclusive with partition destruction
308#[allow(private_bounds)]
309pub(super) struct SynchronousInterruptHandle<T: SynchronousInterruptState> {
310    state: InterruptHandleStateMachine,
311    /// RwLock protecting the partition handle and dropped state.
312    ///
313    /// Fox example, on Windows, this lock prevents a race condition
314    /// between `kill()` calling `WHvCancelRunVirtualProcessor` and
315    /// `WhpVm::drop()` calling `WHvDeletePartition`. These two
316    /// Windows Hypervisor Platform APIs must not execute
317    /// concurrently---if `WHvDeletePartition` frees the partition
318    /// while `WHvCancelRunVirtualProcessor` is still accessing it,
319    /// the result is a use-after-free causing STATUS_ACCESS_VIOLATION
320    /// or STATUS_HEAP_CORRUPTION.
321    ///
322    /// The synchronization works as follows:
323    /// - `kill()` takes a read lock before calling `WHvCancelRunVirtualProcessor`
324    /// - `set_dropped()` takes a write lock, which blocks until all in-flight `kill()` calls complete,
325    ///   then sets `dropped = true`. This is called from `HyperlightVm::drop()` before `WhpVm::drop()`
326    ///   runs, ensuring no `kill()` is accessing the partition when `WHvDeletePartition` is called.
327    dropped_state: std::sync::RwLock<(bool, T)>,
328}
329#[cfg(any(target_os = "windows", hvf))]
330trait SynchronousInterruptState: Debug + Send + Sync {
331    ///  The inside-the-lock part of the common part of both kill()
332    ///  and kill_from_debugger()
333    fn actually_cancel(&self) -> bool;
334
335    #[cfg(hvf)]
336    fn set_vcpu(&mut self, vcpu: Option<hv_vcpu_t>);
337}
338
339#[cfg(any(target_os = "windows", hvf))]
340impl<T: SynchronousInterruptState> InterruptHandleImpl for SynchronousInterruptHandle<T> {
341    #[cfg(hvf)]
342    fn set_vcpu(&self, vcpu: Option<hv_vcpu_t>) {
343        let Ok(mut guard) = self.dropped_state.write() else {
344            return;
345        };
346        guard.1.set_vcpu(vcpu);
347    }
348
349    fn set_dropped(&self) {
350        // Take write lock to:
351        // 1. Wait for any in-flight kill() calls (holding read locks) to complete
352        // 2. Block new kill() calls from starting while we hold the write lock
353        // 3. Set dropped=true so no future kill() calls will use the handle
354        // After this returns, no WHvCancelRunVirtualProcessor calls are in progress
355        // or will ever be made, so WHvDeletePartition can safely be called.
356        match self.dropped_state.write() {
357            Ok(mut guard) => {
358                guard.0 = true;
359            }
360            Err(e) => {
361                tracing::error!("Failed to acquire partition_state write lock: {}", e);
362            }
363        }
364    }
365}
366
367#[cfg(any(target_os = "windows", hvf))]
368impl<T: SynchronousInterruptState> InterruptHandle for SynchronousInterruptHandle<T> {
369    fn dropped(&self) -> bool {
370        // Take read lock to check dropped state consistently
371        match self.dropped_state.read() {
372            Ok(guard) => guard.0,
373            Err(e) => {
374                tracing::error!("Failed to acquire partition_state read lock: {}", e);
375                true // Assume dropped if we can't acquire lock
376            }
377        }
378    }
379}
380#[cfg(any(target_os = "windows", hvf))]
381impl<T: SynchronousInterruptState> InterruptHandleInternal for SynchronousInterruptHandle<T> {
382    fn state(&self) -> &InterruptHandleStateMachine {
383        &self.state
384    }
385    fn common_kill(&self) -> bool {
386        if !self.state.get_running_cancel_debug().0 {
387            return false;
388        }
389
390        // Take read lock to prevent race with WHvDeletePartition in set_dropped().
391        // Multiple kill() calls can proceed concurrently (read locks don't block each other),
392        // but set_dropped() will wait for all kill() calls to complete before proceeding.
393        let guard = match self.dropped_state.read() {
394            Ok(guard) => guard,
395            Err(e) => {
396                tracing::error!("Failed to acquire partition_state read lock: {}", e);
397                return false;
398            }
399        };
400
401        if guard.0 {
402            return false;
403        }
404
405        guard.1.actually_cancel()
406    }
407}
408
409#[cfg(target_os = "windows")]
410use windows::Win32::System::Hypervisor::WHV_PARTITION_HANDLE;
411#[cfg(target_os = "windows")]
412pub(super) type WindowsInterruptHandle = SynchronousInterruptHandle<WHV_PARTITION_HANDLE>;
413#[cfg(target_os = "windows")]
414impl WindowsInterruptHandle {
415    fn new(hdl: WHV_PARTITION_HANDLE) -> Self {
416        SynchronousInterruptHandle {
417            state: InterruptHandleStateMachine::new(),
418            dropped_state: std::sync::RwLock::new((false, hdl)),
419        }
420    }
421}
422#[cfg(target_os = "windows")]
423impl SynchronousInterruptState for WHV_PARTITION_HANDLE {
424    fn actually_cancel(&self) -> bool {
425        use windows::Win32::System::Hypervisor::WHvCancelRunVirtualProcessor;
426        unsafe { WHvCancelRunVirtualProcessor(*self, 0, 0).is_ok() }
427    }
428}
429
430#[cfg(hvf)]
431use crate::hypervisor::virtual_machine::hvf::bindings::hv_vcpu_t;
432#[cfg(hvf)]
433pub(super) type HvfInterruptHandle =
434    RetryingInterruptHandle<SynchronousInterruptHandle<Option<hv_vcpu_t>>>;
435#[cfg(hvf)]
436impl SynchronousInterruptState for Option<hv_vcpu_t> {
437    fn actually_cancel(&self) -> bool {
438        use crate::hypervisor::virtual_machine::hvf::bindings::{HV_SUCCESS, hv_vcpus_exit};
439        let Some(vcpu) = self else {
440            return false;
441        };
442        unsafe {
443            // bindgen automatically uses *mut, but actually this will
444            // not be written to.
445            hv_vcpus_exit(&raw const *vcpu as *mut hv_vcpu_t, 1).0.0.0 == HV_SUCCESS
446        }
447    }
448
449    fn set_vcpu(&mut self, vcpu: Option<hv_vcpu_t>) {
450        *self = vcpu;
451    }
452}
453#[cfg(hvf)]
454impl HvfInterruptHandle {
455    pub(super) fn new(retry_delay: Duration) -> Self {
456        RetryingInterruptHandle {
457            retry_delay,
458            inner: SynchronousInterruptHandle {
459                state: InterruptHandleStateMachine::new(),
460                dropped_state: std::sync::RwLock::new((false, None)),
461            },
462        }
463    }
464}
465
466#[cfg(all(test, any(target_os = "windows", kvm)))]
467pub(crate) mod tests {
468    use std::sync::{Arc, Mutex};
469
470    use hyperlight_testing::dummy_guest_as_pathbuf;
471
472    use crate::sandbox::uninitialized::GuestBinary;
473    #[cfg(any(crashdump, gdb))]
474    use crate::sandbox::uninitialized::SandboxRuntimeConfig;
475    use crate::sandbox::uninitialized_evolve::set_up_hypervisor_partition;
476    use crate::sandbox::{SandboxConfiguration, UninitializedSandbox};
477    use crate::{Result, is_hypervisor_present};
478
479    #[cfg_attr(feature = "hw-interrupts", ignore)]
480    #[test]
481    fn test_initialise() -> Result<()> {
482        if !is_hypervisor_present() {
483            return Ok(());
484        }
485
486        use crate::mem::ptr::RawPtr;
487        use crate::sandbox::host_funcs::FunctionRegistry;
488
489        let filename = dummy_guest_as_pathbuf();
490
491        let config: SandboxConfiguration = Default::default();
492        #[cfg(any(crashdump, gdb))]
493        let rt_cfg: SandboxRuntimeConfig = Default::default();
494        let sandbox =
495            UninitializedSandbox::new(GuestBinary::FilePath(filename.clone()), Some(config))?;
496        let (mut mem_mgr, gshm) = sandbox.mgr.build().unwrap();
497        let exn_stack_top_gva = hyperlight_common::layout::SCRATCH_TOP_GVA as u64
498            - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET
499            + 1;
500        let mut vm = set_up_hypervisor_partition(
501            gshm,
502            &config,
503            exn_stack_top_gva,
504            page_size::get(),
505            #[cfg(any(crashdump, gdb))]
506            rt_cfg,
507            sandbox.load_info,
508        )?;
509
510        // Set up required parameters for initialise
511        let peb_addr = RawPtr::from(0x1000u64); // Dummy PEB address
512        let seed = 12345u64; // Random seed
513        let host_funcs = Arc::new(Mutex::new(FunctionRegistry::default()));
514        let guest_max_log_level = Some(tracing_core::LevelFilter::ERROR);
515
516        // Test the initialise method
517        vm.initialise(
518            peb_addr,
519            seed,
520            &mut mem_mgr,
521            &host_funcs,
522            guest_max_log_level,
523        )
524        .unwrap();
525
526        Ok(())
527    }
528}