diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 000000000..b1c8f87b3
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,2 @@
+[target.'cfg(target_os = "macos")']
+runner = "dev/macos-sign-and-run.sh"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e634063b..d2e3c314b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Changed
+Certain fixed guest addresses were changed on AArch64 to more easily
+accommodate 16k pages without wasting memory. Snapshots taken from
+sandboxes using the old addresses will not be loadable by new
+hyperlight versions.
+
### Removed
### Fixed
diff --git a/Cargo.lock b/Cargo.lock
index 7db5b5dd0..56fc157d1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1663,10 +1663,12 @@ name = "hyperlight-host"
version = "0.16.0"
dependencies = [
"anyhow",
+ "bindgen",
"bitflags 2.13.1",
"blake3",
"built",
"bytemuck",
+ "cc",
"cfg-if",
"cfg_aliases",
"chrono",
@@ -1704,6 +1706,7 @@ dependencies = [
"opentelemetry-semantic-conventions",
"opentelemetry_sdk",
"page_size",
+ "parking_lot",
"proc-maps",
"proptest",
"rand 0.10.2",
diff --git a/dev/macos-entitlements.plist b/dev/macos-entitlements.plist
new file mode 100644
index 000000000..c2ef1a38b
--- /dev/null
+++ b/dev/macos-entitlements.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.hypervisor
+
+
+
diff --git a/dev/macos-sign-and-run.sh b/dev/macos-sign-and-run.sh
new file mode 100755
index 000000000..cc9bcef93
--- /dev/null
+++ b/dev/macos-sign-and-run.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+
+codesign -f -s - --entitlements "$(dirname "$0")/macos-entitlements.plist" "$1"
+exec "$@"
diff --git a/flake.lock b/flake.lock
index 487dd3b5c..faf4c21f8 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
- "lastModified": 1770115704,
- "narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=",
+ "lastModified": 1782723713,
+ "narHash": "sha256-oPXCU/SSUokcGaJREHibG1CBX3+s/W7orDWQOZDsEeQ=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "e6eae2ee2110f3d31110d5c222cd395303343b08",
+ "rev": "b5aa0fbd538984f6e3d201be0005b4463d8b09f8",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index ae01e57b9..f8a0f5ce8 100644
--- a/flake.nix
+++ b/flake.nix
@@ -11,8 +11,8 @@
orig = super.rustChannelOf args;
patchRustPkg = pkg: (pkg.overrideAttrs (oA: {
buildCommand = (builtins.replaceStrings
- [ "rustc,rustdoc" ]
- [ "rustc,rustdoc,clippy-driver,cargo-clippy,miri,cargo-miri" ]
+ [ "rustc,rustdoc" "librustc_driver-*.so" ]
+ [ "rustc,rustdoc,clippy-driver,cargo-clippy,miri,cargo-miri" "librustc_driver-*.{so,dylib}" ]
oA.buildCommand) + (let
wrapperPath = self.path + "/pkgs/build-support/bintools-wrapper/ld-wrapper.sh";
baseOut = self.clangStdenv.cc.bintools.out;
@@ -51,7 +51,7 @@
toolchainVersionAttrs = args;
};
})) // {
- targetPlatforms = [ "aarch64-linux" "x86_64-linux" ];
+ targetPlatforms = [ "aarch64-linux" "x86_64-linux" "aarch64-darwin" ];
badTargetPlatforms = [ ];
};
overrideRustPkg = pkg: self.lib.makeOverridable (origArgs:
@@ -82,7 +82,7 @@
"x86_64-unknown-linux-gnu"
"x86_64-pc-windows-msvc" "x86_64-unknown-none"
"wasm32-wasip1" "wasm32-wasip2" "wasm32-unknown-unknown"
- "aarch64-unknown-none"
+ "aarch64-unknown-none" "aarch64-apple-darwin"
];
extensions = [ "rust-src" ] ++ (if args.channel == "nightly" then [ "miri-preview" ] else []);
});
@@ -166,13 +166,14 @@
esac
if [ -f ''${root}/flake.nix ]; then
- mkdir -p $root/$.cargo
- cat >$root/.cargo/config.toml <>$root/.cargo/config.toml < Result<()> {
// re-run the build if this script is changed (or deleted!),
// even if the rust code is completely unchanged.
println!("cargo:rerun-if-changed=build.rs");
+ let out_dir = std::env::var("OUT_DIR")?;
+ let out_path = std::path::PathBuf::from(&out_dir);
+ let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?;
// Windows requires the hyperlight_surrogate.exe binary to be next to the executable running
// hyperlight. We are using rust-embed to include the binary in the hyperlight-host library
@@ -41,9 +44,7 @@ fn main() -> Result<()> {
// We need to copy/rename the source for hyperlight surrogate into a
// temp directory because we cannot include a file name `Cargo.toml`
// inside this package.
- let out_dir = std::env::var("OUT_DIR")?;
std::fs::create_dir_all(format!("{out_dir}/hyperlight_surrogate/src"))?;
- let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")?;
std::fs::copy(
format!("{manifest_dir}/src/hyperlight_surrogate/src/main.rs"),
format!("{out_dir}/hyperlight_surrogate/src/main.rs"),
@@ -62,7 +63,7 @@ fn main() -> Result<()> {
// be the same as the CARGO_TARGET_DIR for the hyperlight-host otherwise
// the build script will hang. Using a sub directory works tho!
// xref - https://github.com/rust-lang/cargo/issues/6412
- let target_dir = std::path::PathBuf::from(&out_dir).join("../../hls");
+ let target_dir = out_path.join("../../hls");
let profile = std::env::var("PROFILE")?;
let build_profile = if profile.to_lowercase() == "debug" {
@@ -101,6 +102,56 @@ fn main() -> Result<()> {
);
}
+ if std::env::var("CARGO_CFG_TARGET_OS")? == "macos"
+ && std::env::var("CARGO_FEATURE_HVF").is_ok()
+ {
+ println!("cargo:rerun-if-env-changed=BINDGEN_EXTRA_CLANG_ARGS");
+ println!(
+ "cargo:rerun-if-changed=src/hyperlight_host/src/hypervisor/virtual_machine/hvf/bindings.h"
+ );
+ println!(
+ "cargo:rerun-if-changed=src/hyperlight_host/src/hypervisor/virtual_machine/hvf/fp_abi.c"
+ );
+ println!("cargo:rustc-link-lib=framework=Hypervisor");
+ bindgen::Builder::default()
+ .clang_args(&["-framework", "Hypervisor"])
+ .clang_args(&[
+ "-I",
+ &format!("{manifest_dir}/src/hypervisor/virtual_machine/hvf"),
+ ])
+ // The hvf simd register functions use C simd vector
+ // parameters/returns for the register value, which Rust
+ // does not presently stably support for `extern "C"`
+ // code. So, we don't generate native bindings for these
+ // functions, and instead generate C stubs (see `fp_abi.c`,
+ // which is built below)
+ .blocklist_type("hv_simd_fp_uchar16_t")
+ .blocklist_function("hv_vcpu_get_simd_fp_reg")
+ .blocklist_function("hv_vcpu_set_simd_fp_reg")
+ .default_alias_style(bindgen::AliasVariation::NewType)
+ .type_alias("hv_memory_flags_t")
+ .newtype_enum("hv_reg_t")
+ .newtype_enum("hv_simd_fp_reg_t")
+ .newtype_enum("hv_sys_reg_t")
+ .newtype_enum("hv_exit_reason_t")
+ .no_debug("hv_return_t")
+ .formatter(bindgen::Formatter::Prettyplease)
+ .header(format!(
+ "{manifest_dir}/src/hypervisor/virtual_machine/hvf/bindings.h"
+ ))
+ .generate()
+ .context("Unable to generate hvf bindings")?
+ .write_to_file(out_path.join("hvf_bindings.rs"))
+ .context("Couldn't write hvf bindings")?;
+
+ cc::Build::new()
+ .opt_level(3)
+ .file(format!(
+ "{manifest_dir}/src/hypervisor/virtual_machine/hvf/fp_abi.c"
+ ))
+ .compile("hyperlight_host_hvf_abi_wrapper");
+ }
+
// Makes #[cfg(kvm)] == #[cfg(all(feature = "kvm", target_os = "linux"))]
// Essentially the kvm and mshv3 features are ignored on windows as long as you use #[cfg(kvm)] and not #[cfg(feature = "kvm")].
// You should never use #[cfg(feature = "kvm")] or #[cfg(feature = "mshv3")] in the codebase.
@@ -108,6 +159,7 @@ fn main() -> Result<()> {
gdb: { all(feature = "gdb", debug_assertions, target_arch = "x86_64") },
kvm: { all(feature = "kvm", target_os = "linux") },
mshv3: { all(feature = "mshv3", target_os = "linux") },
+ hvf: { all(feature = "hvf", target_os = "macos") },
crashdump: { all(feature = "crashdump", target_arch = "x86_64") },
// print_debug feature is aliased with debug_assertions to make it only available in debug-builds.
print_debug: { all(feature = "print_debug", debug_assertions) },
diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs
index 770959c14..15b196173 100644
--- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs
+++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs
@@ -17,24 +17,28 @@ limitations under the License.
// TODO(aarch64): implement arch-specific HyperlightVm methods
use std::sync::Arc;
-use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64};
use super::{
AccessPageTableError, CreateHyperlightVmError, DispatchGuestCallError, HyperlightVm,
InitializeError,
};
+#[cfg(hvf)]
+use crate::hypervisor::HvfInterruptHandle;
+use crate::hypervisor::InterruptHandleImpl;
+#[cfg(any(kvm, mshv3))]
+use crate::hypervisor::LinuxInterruptHandle;
#[cfg(gdb)]
use crate::hypervisor::gdb::{DebugCommChannel, DebugMsg, DebugResponse};
use crate::hypervisor::hyperlight_vm::get_guest_log_filter;
use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters};
+#[cfg(hvf)]
+use crate::hypervisor::virtual_machine::hvf::HvfVm;
#[cfg(kvm)]
use crate::hypervisor::virtual_machine::kvm::KvmVm;
-#[cfg(kvm)]
-use crate::hypervisor::virtual_machine::{HypervisorType, VmError};
use crate::hypervisor::virtual_machine::{
- RegisterError, ResetVcpuError, VirtualMachine, get_available_hypervisor,
+ HypervisorType, RegisterError, ResetVcpuError, VirtualMachine, VmError,
+ get_available_hypervisor,
};
-use crate::hypervisor::{InterruptHandleImpl, LinuxInterruptHandle};
use crate::mem::mgr::{SandboxMemoryManager, SnapshotSharedMemory};
use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory};
use crate::sandbox::SandboxConfiguration;
@@ -47,6 +51,7 @@ use crate::sandbox::uninitialized::SandboxRuntimeConfig;
impl HyperlightVm {
#[allow(clippy::too_many_arguments)]
+ #[cfg_attr(target_os = "macos", allow(unused))]
pub(crate) fn new(
snapshot_mem: SnapshotSharedMemory,
scratch_mem: GuestSharedMemory,
@@ -61,23 +66,26 @@ impl HyperlightVm {
) -> std::result::Result {
// TODO: support gdb on aarch64
type VmType = Box;
- let vm: VmType = match get_available_hypervisor() {
+ #[cfg(hvf)]
+ let interrupt_handle: Arc =
+ Arc::new(HvfInterruptHandle::new(config.get_interrupt_retry_delay()));
+ let mut vm: VmType = match get_available_hypervisor() {
#[cfg(kvm)]
Some(HypervisorType::Kvm) => Box::new(KvmVm::new().map_err(VmError::CreateVm)?),
// TODO: mshv support
#[cfg(mshv3)]
Some(HypervisorType::Mshv) => return Err(CreateHyperlightVmError::NoHypervisorFound),
+ #[cfg(hvf)]
+ Some(HypervisorType::Hvf) => {
+ Box::new(HvfVm::new(interrupt_handle.clone()).map_err(VmError::CreateVm)?)
+ }
None => return Err(CreateHyperlightVmError::NoHypervisorFound),
};
vm.set_sregs(&CommonSpecialRegisters::defaults(root_pt_addr))
.map_err(VmError::Register)?;
- let interrupt_handle: Arc = Arc::new(LinuxInterruptHandle {
- state: AtomicU8::new(0),
- tid: AtomicU64::new(unsafe { libc::pthread_self() as u64 }),
- retry_delay: config.get_interrupt_retry_delay(),
- sig_rt_min_offset: config.get_interrupt_vcpu_sigrtmin_offset(),
- dropped: AtomicBool::new(false),
- });
+ #[cfg(any(kvm, mshv3))]
+ let interrupt_handle: Arc =
+ Arc::new(LinuxInterruptHandle::new(config));
let snapshot_slot = 0u32;
let scratch_slot = 1u32;
diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs
index 2e545aba7..b8a9087ef 100644
--- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs
+++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs
@@ -587,7 +587,7 @@ impl HyperlightVm {
}
pub(crate) fn clear_cancel(&self) {
- self.interrupt_handle.clear_cancel();
+ self.interrupt_handle.state().clear_cancel();
}
pub(super) fn run(
@@ -607,12 +607,12 @@ impl HyperlightVm {
// without sending any signals/WHV api calls
#[cfg(any(kvm, mshv3))]
self.interrupt_handle.set_tid();
- self.interrupt_handle.set_running();
+ self.interrupt_handle.state().set_running();
// NOTE: `set_running()`` must be called before checking `is_cancelled()`
// otherwise we risk missing a call to `kill()` because the vcpu would not be marked as running yet so signals won't be sent
- let exit_reason = if self.interrupt_handle.is_cancelled()
- || self.interrupt_handle.is_debug_interrupted()
+ let exit_reason = if self.interrupt_handle.state().is_cancelled()
+ || self.interrupt_handle.state().is_debug_interrupted()
{
Ok(VmExit::Cancelled())
} else {
@@ -652,14 +652,14 @@ impl HyperlightVm {
// If kill() is called and ran to completion BEFORE this line executes:
// - CANCEL_BIT will be set. Cancellation is deferred to the next iteration.
// - Signals will be sent until `clear_running()` is called, which is ok
- self.interrupt_handle.clear_running();
+ self.interrupt_handle.state().clear_running();
// ===== KILL() TIMING POINT 5: Before capturing cancel_requested =====
// If kill() is called and ran to completion BEFORE this line executes:
// - CANCEL_BIT will be set. Cancellation is deferred to the next iteration.
// - Signals will not be sent
- let cancel_requested = self.interrupt_handle.is_cancelled();
- let debug_interrupted = self.interrupt_handle.is_debug_interrupted();
+ let cancel_requested = self.interrupt_handle.state().is_cancelled();
+ let debug_interrupted = self.interrupt_handle.state().is_debug_interrupted();
// ===== KILL() TIMING POINT 6: Before checking exit_reason =====
// If kill() is called and ran to completion BEFORE this line executes:
@@ -754,7 +754,7 @@ impl HyperlightVm {
// If the vcpu was interrupted by a debugger, we need to handle it
#[cfg(gdb)]
{
- self.interrupt_handle.clear_debug_interrupt();
+ self.interrupt_handle.state().clear_debug_interrupt();
if let Err(e) =
self.handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Interrupt)
{
diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs
index ae17e100b..efefff921 100644
--- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs
+++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs
@@ -18,11 +18,6 @@ limitations under the License.
use std::collections::HashMap;
#[cfg(crashdump)]
use std::path::Path;
-#[cfg(any(kvm, mshv3))]
-use std::sync::atomic::AtomicBool;
-use std::sync::atomic::AtomicU8;
-#[cfg(any(kvm, mshv3))]
-use std::sync::atomic::AtomicU64;
use std::sync::{Arc, Mutex};
use tracing::{Span, instrument};
@@ -32,6 +27,8 @@ use super::*;
use crate::hypervisor::InterruptHandleImpl;
#[cfg(any(kvm, mshv3))]
use crate::hypervisor::LinuxInterruptHandle;
+#[cfg(target_os = "windows")]
+use crate::hypervisor::WindowsInterruptHandle;
#[cfg(crashdump)]
use crate::hypervisor::crashdump;
#[cfg(gdb)]
@@ -54,8 +51,6 @@ use crate::hypervisor::virtual_machine::whp::WhpVm;
use crate::hypervisor::virtual_machine::{
HypervisorType, RegisterError, VmError, get_available_hypervisor,
};
-#[cfg(target_os = "windows")]
-use crate::hypervisor::{PartitionState, WindowsInterruptHandle};
#[cfg(crashdump)]
use crate::mem::memory_region::MemoryRegion;
use crate::mem::mgr::SandboxMemoryManager;
@@ -90,7 +85,7 @@ impl HyperlightVm {
#[cfg(not(gdb))]
type VmType = Box;
- let vm: VmType = match get_available_hypervisor() {
+ let mut vm: VmType = match get_available_hypervisor() {
#[cfg(kvm)]
Some(HypervisorType::Kvm) => Box::new(KvmVm::new().map_err(VmError::CreateVm)?),
#[cfg(mshv3)]
@@ -106,35 +101,12 @@ impl HyperlightVm {
.map_err(VmError::Register)?;
#[cfg(any(kvm, mshv3))]
- let interrupt_handle: Arc = Arc::new(LinuxInterruptHandle {
- state: AtomicU8::new(0),
- #[cfg(all(
- target_arch = "x86_64",
- target_vendor = "unknown",
- target_os = "linux",
- target_env = "musl"
- ))]
- tid: AtomicU64::new(unsafe { libc::pthread_self() as u64 }),
- #[cfg(not(all(
- target_arch = "x86_64",
- target_vendor = "unknown",
- target_os = "linux",
- target_env = "musl"
- )))]
- tid: AtomicU64::new(unsafe { libc::pthread_self() }),
- retry_delay: config.get_interrupt_retry_delay(),
- sig_rt_min_offset: config.get_interrupt_vcpu_sigrtmin_offset(),
- dropped: AtomicBool::new(false),
- });
+ let interrupt_handle: Arc =
+ Arc::new(LinuxInterruptHandle::new(config));
#[cfg(target_os = "windows")]
- let interrupt_handle: Arc = Arc::new(WindowsInterruptHandle {
- state: AtomicU8::new(0),
- partition_state: std::sync::RwLock::new(PartitionState {
- handle: vm.partition_handle(),
- dropped: false,
- }),
- });
+ let interrupt_handle: Arc =
+ Arc::new(WindowsInterruptHandle::new(vm.partition_handle()));
let snapshot_slot = 0u32;
let scratch_slot = 1u32;
@@ -598,8 +570,6 @@ impl HyperlightVm {
#[cfg(gdb)]
pub(super) mod debug {
- use hyperlight_common::mem::PAGE_SIZE;
-
use super::HyperlightVm;
use crate::hypervisor::gdb::arch::{SW_BP, SW_BP_SIZE};
use crate::hypervisor::gdb::{
@@ -789,7 +759,7 @@ pub(super) mod debug {
let read_len = std::cmp::min(
data.len(),
- (PAGE_SIZE - (gpa & (PAGE_SIZE - 1))).try_into().unwrap(),
+ page_size::get() - (gpa as usize & (page_size::get() - 1)),
);
mem_access.read(&mut data[..read_len], gpa)?;
@@ -817,7 +787,7 @@ pub(super) mod debug {
let write_len = std::cmp::min(
data.len(),
- (PAGE_SIZE - (gpa & (PAGE_SIZE - 1))).try_into().unwrap(),
+ page_size::get() - (gpa as usize & (page_size::get() - 1)),
);
// Use the memory access to write to guest memory
diff --git a/src/hyperlight_host/src/hypervisor/mod.rs b/src/hyperlight_host/src/hypervisor/mod.rs
index 732f08563..54ec16e8f 100644
--- a/src/hyperlight_host/src/hypervisor/mod.rs
+++ b/src/hyperlight_host/src/hypervisor/mod.rs
@@ -40,43 +40,122 @@ pub(crate) mod hyperlight_vm;
use std::fmt::Debug;
#[cfg(any(kvm, mshv3))]
-use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering};
-#[cfg(target_os = "windows")]
+use std::sync::atomic::{AtomicBool, AtomicU64};
use std::sync::atomic::{AtomicU8, Ordering};
-#[cfg(any(kvm, mshv3))]
+#[cfg(any(kvm, mshv3, hvf))]
use std::time::Duration;
-/// A trait for platform-specific interrupt handle implementation details
-pub(crate) trait InterruptHandleImpl: InterruptHandle {
- /// Set the thread ID for the vcpu thread
- #[cfg(any(kvm, mshv3))]
- fn set_tid(&self);
+#[derive(Debug)]
+pub(crate) struct InterruptHandleStateMachine(AtomicU8);
+impl InterruptHandleStateMachine {
+ const RUNNING_BIT: u8 = 1 << 1;
+ const CANCEL_BIT: u8 = 1 << 0;
+ #[cfg(gdb)]
+ const DEBUG_INTERRUPT_BIT: u8 = 1 << 2;
+
+ fn new() -> Self {
+ Self(AtomicU8::new(0))
+ }
/// Set the running state
- fn set_running(&self);
+ pub(crate) fn set_running(&self) {
+ // Release ordering to ensure that the tid store (which uses Release)
+ // is visible to any thread that observes running=true via Acquire ordering.
+ // This prevents the interrupt thread from reading a stale tid value.
+ self.0.fetch_or(Self::RUNNING_BIT, Ordering::Release);
+ }
/// Clear the running state
- fn clear_running(&self);
-
- /// Mark the handle as dropped
- fn set_dropped(&self);
+ pub(crate) fn clear_running(&self) {
+ // Release ordering to ensure all vcpu operations are visible before clearing running
+ self.0.fetch_and(!Self::RUNNING_BIT, Ordering::Release);
+ }
/// Check if cancellation was requested
- fn is_cancelled(&self) -> bool;
+ pub(crate) fn is_cancelled(&self) -> bool {
+ self.get_running_cancel_debug().1
+ }
+
+ /// Set the cancellation request flag
+ fn set_cancel(&self) {
+ // Release ordering ensures that any writes before kill() are visible to the vcpu thread
+ // when it checks is_cancelled() with Acquire ordering
+ self.0.fetch_or(Self::CANCEL_BIT, Ordering::Release);
+ }
/// Clear the cancellation request flag
- fn clear_cancel(&self);
+ fn clear_cancel(&self) {
+ // Release ordering to ensure that any operations from the previous run()
+ // are visible to other threads. While this is typically called by the vcpu thread
+ // at the start of run(), the VM itself can move between threads across guest calls.
+ self.0.fetch_and(!Self::CANCEL_BIT, Ordering::Release);
+ }
/// Check if debug interrupt was requested (always returns false when gdb feature is disabled)
- fn is_debug_interrupted(&self) -> bool;
+ pub(crate) fn is_debug_interrupted(&self) -> bool {
+ #[cfg(gdb)]
+ {
+ self.get_running_cancel_debug().2
+ }
+ #[cfg(not(gdb))]
+ {
+ false
+ }
+ }
+
+ /// Clear the debug interrupt request flag
+ #[cfg(gdb)]
+ fn set_debug_interrupt(&self) {
+ self.0
+ .fetch_or(Self::DEBUG_INTERRUPT_BIT, Ordering::Release);
+ }
- // Clear the debug interrupt request flag
+ /// Clear the debug interrupt request flag
#[cfg(gdb)]
- fn clear_debug_interrupt(&self);
+ fn clear_debug_interrupt(&self) {
+ self.0
+ .fetch_and(!Self::DEBUG_INTERRUPT_BIT, Ordering::Release);
+ }
+
+ /// Get the running, cancel and debug flags atomically.
+ fn get_running_cancel_debug(&self) -> (bool, bool, bool) {
+ let state = self.0.load(Ordering::Acquire);
+ let running = state & Self::RUNNING_BIT != 0;
+ let cancel = state & Self::CANCEL_BIT != 0;
+ #[cfg(gdb)]
+ let debug = state & Self::DEBUG_INTERRUPT_BIT != 0;
+ #[cfg(not(gdb))]
+ let debug = false;
+ (running, cancel, debug)
+ }
+}
+
+/// A trait for platform-specific interrupt handle implementation details
+pub(crate) trait InterruptHandleImpl: InterruptHandle {
+ /// Set the thread ID for the vcpu thread
+ #[cfg(any(kvm, mshv3))]
+ fn set_tid(&self);
+
+ /// Set the currently-executing vcpu id
+ #[cfg(hvf)]
+ fn set_vcpu(&self, vcpu: hv_vcpu_t);
+
+ /// Mark the handle as dropped
+ fn set_dropped(&self);
+}
+
+pub(crate) trait InterruptHandleInternal {
+ /// Local access the shared state, which does not perform any
+ /// operations other than updating the state machine
+ fn state(&self) -> &InterruptHandleStateMachine;
+ /// Trigger the actual kill-like operation without
+ /// modifying the state
+ fn common_kill(&self) -> bool;
}
/// A trait for handling interrupts to a sandbox's vcpu
-pub trait InterruptHandle: Send + Sync + Debug {
+#[allow(private_bounds)]
+pub trait InterruptHandle: Send + Sync + Debug + InterruptHandleInternal {
/// Interrupt the corresponding sandbox from running.
///
/// - If this is called while the the sandbox currently executing a guest function call, it will interrupt the sandbox and return `true`.
@@ -84,7 +163,10 @@ pub trait InterruptHandle: Send + Sync + Debug {
///
/// # Note
/// This function will block for the duration of the time it takes for the vcpu thread to be interrupted.
- fn kill(&self) -> bool;
+ fn kill(&self) -> bool {
+ self.state().set_cancel();
+ self.common_kill()
+ }
/// Used by a debugger to interrupt the corresponding sandbox from running.
///
@@ -96,25 +178,71 @@ pub trait InterruptHandle: Send + Sync + Debug {
/// # Note
/// This function will block for the duration of the time it takes for the vcpu thread to be interrupted.
#[cfg(gdb)]
- fn kill_from_debugger(&self) -> bool;
+ fn kill_from_debugger(&self) -> bool {
+ self.state().set_debug_interrupt();
+ self.common_kill()
+ }
/// Returns true if the corresponding sandbox has been dropped
fn dropped(&self) -> bool;
}
+#[cfg(any(kvm, mshv3, hvf))]
+#[derive(Debug)]
+pub(super) struct RetryingInterruptHandle {
+ retry_delay: Duration,
+ inner: T,
+}
+
+#[cfg(any(kvm, mshv3, hvf))]
+impl InterruptHandleImpl for RetryingInterruptHandle {
+ #[cfg(any(kvm, mshv3))]
+ fn set_tid(&self) {
+ self.inner.set_tid();
+ }
+
+ #[cfg(hvf)]
+ fn set_vcpu(&self, vcpu: hv_vcpu_t) {
+ self.inner.set_vcpu(vcpu);
+ }
+
+ fn set_dropped(&self) {
+ self.inner.set_dropped();
+ }
+}
+#[cfg(any(kvm, mshv3, hvf))]
+impl InterruptHandle for RetryingInterruptHandle {
+ fn dropped(&self) -> bool {
+ self.inner.dropped()
+ }
+}
+#[cfg(any(kvm, mshv3, hvf))]
+impl InterruptHandleInternal for RetryingInterruptHandle {
+ fn state(&self) -> &InterruptHandleStateMachine {
+ self.inner.state()
+ }
+ fn common_kill(&self) -> bool {
+ let mut succeeded = false;
+ loop {
+ let (running, cancel, debug) = self.state().get_running_cancel_debug();
+ // Check if we should continue sending signals
+ // Exit if not running OR if neither cancel nor debug_interrupt is set
+ let should_continue = running && (cancel || debug);
+ if !should_continue {
+ break;
+ }
+ tracing::info!("Trying to kill vcpu thread...");
+ succeeded |= self.inner.common_kill();
+ std::thread::sleep(self.retry_delay);
+ }
+ succeeded
+ }
+}
+
#[cfg(any(kvm, mshv3))]
#[derive(Debug)]
-pub(super) struct LinuxInterruptHandle {
- /// Atomic value packing vcpu execution state.
- ///
- /// Bit layout:
- /// - Bit 2: DEBUG_INTERRUPT_BIT - set when debugger interrupt is requested
- /// - Bit 1: RUNNING_BIT - set when vcpu is actively running
- /// - Bit 0: CANCEL_BIT - set when cancellation has been requested
- ///
- /// CANCEL_BIT persists across vcpu exits/re-entries within a single `VirtualCPU::run()` call
- /// (e.g., during host function calls), but is cleared at the start of each new `VirtualCPU::run()` call.
- state: AtomicU8,
+pub(super) struct LinuxInterruptHandleState {
+ state: InterruptHandleStateMachine,
/// Thread ID where the vcpu is running.
///
@@ -125,67 +253,42 @@ pub(super) struct LinuxInterruptHandle {
/// Whether the corresponding VM has been dropped.
dropped: AtomicBool,
- /// Delay between retry attempts when sending signals to interrupt the vcpu.
- retry_delay: Duration,
-
/// Offset from SIGRTMIN for the signal used to interrupt the vcpu thread.
sig_rt_min_offset: u8,
}
+#[cfg(any(kvm, mshv3))]
+pub(super) type LinuxInterruptHandle = RetryingInterruptHandle;
#[cfg(any(kvm, mshv3))]
impl LinuxInterruptHandle {
- const RUNNING_BIT: u8 = 1 << 1;
- const CANCEL_BIT: u8 = 1 << 0;
- #[cfg(gdb)]
- const DEBUG_INTERRUPT_BIT: u8 = 1 << 2;
-
- /// Get the running, cancel and debug flags atomically.
- ///
- /// # Memory Ordering
- /// Uses `Acquire` ordering to synchronize with the `Release` in `set_running()` and `kill()`.
- /// This ensures that when we observe running=true, we also see the correct `tid` value.
- fn get_running_cancel_debug(&self) -> (bool, bool, bool) {
- let state = self.state.load(Ordering::Acquire);
- let running = state & Self::RUNNING_BIT != 0;
- let cancel = state & Self::CANCEL_BIT != 0;
- #[cfg(gdb)]
- let debug = state & Self::DEBUG_INTERRUPT_BIT != 0;
- #[cfg(not(gdb))]
- let debug = false;
- (running, cancel, debug)
- }
-
- fn send_signal(&self) -> bool {
- let signal_number = libc::SIGRTMIN() + self.sig_rt_min_offset as libc::c_int;
- let mut sent_signal = false;
-
- loop {
- let (running, cancel, debug) = self.get_running_cancel_debug();
-
- // Check if we should continue sending signals
- // Exit if not running OR if neither cancel nor debug_interrupt is set
- let should_continue = running && (cancel || debug);
-
- if !should_continue {
- break;
- }
-
- tracing::info!("Sending signal to kill vcpu thread...");
- sent_signal = true;
- // Acquire ordering to synchronize with the Release store in set_tid()
- // This ensures we see the correct tid value for the currently running vcpu
- unsafe {
- libc::pthread_kill(self.tid.load(Ordering::Acquire) as _, signal_number);
- }
- std::thread::sleep(self.retry_delay);
+ fn new(config: &crate::sandbox::SandboxConfiguration) -> Self {
+ RetryingInterruptHandle {
+ retry_delay: config.get_interrupt_retry_delay(),
+ inner: LinuxInterruptHandleState {
+ state: InterruptHandleStateMachine::new(),
+ #[cfg(all(
+ target_arch = "x86_64",
+ target_vendor = "unknown",
+ target_os = "linux",
+ target_env = "musl"
+ ))]
+ tid: AtomicU64::new(unsafe { libc::pthread_self() as u64 }),
+ #[cfg(not(all(
+ target_arch = "x86_64",
+ target_vendor = "unknown",
+ target_os = "linux",
+ target_env = "musl"
+ )))]
+ tid: AtomicU64::new(unsafe { libc::pthread_self() as u64 }),
+ sig_rt_min_offset: config.get_interrupt_vcpu_sigrtmin_offset(),
+ dropped: AtomicBool::new(false),
+ },
}
-
- sent_signal
}
}
#[cfg(any(kvm, mshv3))]
-impl InterruptHandleImpl for LinuxInterruptHandle {
+impl InterruptHandleImpl for LinuxInterruptHandleState {
fn set_tid(&self) {
// Release ordering to synchronize with the Acquire load of `running` in send_signal()
// This ensures that when send_signal() observes RUNNING_BIT=true (via Acquire),
@@ -194,48 +297,6 @@ impl InterruptHandleImpl for LinuxInterruptHandle {
.store(unsafe { libc::pthread_self() as u64 }, Ordering::Release);
}
- fn set_running(&self) {
- // Release ordering to ensure that the tid store (which uses Release)
- // is visible to any thread that observes running=true via Acquire ordering.
- // This prevents the interrupt thread from reading a stale tid value.
- self.state.fetch_or(Self::RUNNING_BIT, Ordering::Release);
- }
-
- fn is_cancelled(&self) -> bool {
- // Acquire ordering to synchronize with the Release in kill()
- // This ensures we see the cancel flag set by the interrupt thread
- self.state.load(Ordering::Acquire) & Self::CANCEL_BIT != 0
- }
-
- fn clear_cancel(&self) {
- // Release ordering to ensure that any operations from the previous run()
- // are visible to other threads. While this is typically called by the vcpu thread
- // at the start of run(), the VM itself can move between threads across guest calls.
- self.state.fetch_and(!Self::CANCEL_BIT, Ordering::Release);
- }
-
- fn clear_running(&self) {
- // Release ordering to ensure all vcpu operations are visible before clearing running
- self.state.fetch_and(!Self::RUNNING_BIT, Ordering::Release);
- }
-
- fn is_debug_interrupted(&self) -> bool {
- #[cfg(gdb)]
- {
- self.state.load(Ordering::Acquire) & Self::DEBUG_INTERRUPT_BIT != 0
- }
- #[cfg(not(gdb))]
- {
- false
- }
- }
-
- #[cfg(gdb)]
- fn clear_debug_interrupt(&self) {
- self.state
- .fetch_and(!Self::DEBUG_INTERRUPT_BIT, Ordering::Release);
- }
-
fn set_dropped(&self) {
// Release ordering to ensure all VM cleanup operations are visible
// to any thread that checks dropped() via Acquire
@@ -244,22 +305,7 @@ impl InterruptHandleImpl for LinuxInterruptHandle {
}
#[cfg(any(kvm, mshv3))]
-impl InterruptHandle for LinuxInterruptHandle {
- fn kill(&self) -> bool {
- // Release ordering ensures that any writes before kill() are visible to the vcpu thread
- // when it checks is_cancelled() with Acquire ordering
- self.state.fetch_or(Self::CANCEL_BIT, Ordering::Release);
-
- // Send signals to interrupt the vcpu if it's currently running
- self.send_signal()
- }
-
- #[cfg(gdb)]
- fn kill_from_debugger(&self) -> bool {
- self.state
- .fetch_or(Self::DEBUG_INTERRUPT_BIT, Ordering::Release);
- self.send_signal()
- }
+impl InterruptHandle for LinuxInterruptHandleState {
fn dropped(&self) -> bool {
// Acquire ordering to synchronize with the Release in set_dropped()
// This ensures we see all VM cleanup operations that happened before drop
@@ -267,101 +313,63 @@ impl InterruptHandle for LinuxInterruptHandle {
}
}
-#[cfg(target_os = "windows")]
-#[derive(Debug)]
-pub(super) struct WindowsInterruptHandle {
- /// Atomic value packing vcpu execution state.
- ///
- /// Bit layout:
- /// - Bit 2: DEBUG_INTERRUPT_BIT - set when debugger interrupt is requested
- /// - Bit 1: RUNNING_BIT - set when vcpu is actively running
- /// - Bit 0: CANCEL_BIT - set when cancellation has been requested
- ///
- /// `WHvCancelRunVirtualProcessor()` will return Ok even if the vcpu is not running,
- /// which is why we need the RUNNING_BIT.
- ///
- /// CANCEL_BIT persists across vcpu exits/re-entries within a single `VirtualCPU::run()` call
- /// (e.g., during host function calls), but is cleared at the start of each new `VirtualCPU::run()` call.
- state: AtomicU8,
+#[cfg(any(kvm, mshv3))]
+impl InterruptHandleInternal for LinuxInterruptHandleState {
+ fn state(&self) -> &InterruptHandleStateMachine {
+ &self.state
+ }
+ fn common_kill(&self) -> bool {
+ let signal_number = libc::SIGRTMIN() + self.sig_rt_min_offset as libc::c_int;
+ unsafe {
+ libc::pthread_kill(self.tid.load(Ordering::Acquire) as _, signal_number);
+ }
+ true
+ }
+}
+#[cfg(any(target_os = "windows", hvf))]
+#[derive(Debug)]
+/// An interrupt handle that captures the pattern that requests to
+/// cancel need to be mutually exclusive with partition destruction
+#[allow(private_bounds)]
+pub(super) struct SynchronousInterruptHandle {
+ state: InterruptHandleStateMachine,
/// RwLock protecting the partition handle and dropped state.
///
- /// This lock prevents a race condition between `kill()` calling `WHvCancelRunVirtualProcessor`
- /// and `WhpVm::drop()` calling `WHvDeletePartition`. These two Windows Hypervisor Platform APIs
- /// must not execute concurrently - if `WHvDeletePartition` frees the partition while
- /// `WHvCancelRunVirtualProcessor` is still accessing it, the result is a use-after-free
- /// causing STATUS_ACCESS_VIOLATION or STATUS_HEAP_CORRUPTION.
+ /// Fox example, on Windows, this lock prevents a race condition
+ /// between `kill()` calling `WHvCancelRunVirtualProcessor` and
+ /// `WhpVm::drop()` calling `WHvDeletePartition`. These two
+ /// Windows Hypervisor Platform APIs must not execute
+ /// concurrently---if `WHvDeletePartition` frees the partition
+ /// while `WHvCancelRunVirtualProcessor` is still accessing it,
+ /// the result is a use-after-free causing STATUS_ACCESS_VIOLATION
+ /// or STATUS_HEAP_CORRUPTION.
///
/// The synchronization works as follows:
/// - `kill()` takes a read lock before calling `WHvCancelRunVirtualProcessor`
/// - `set_dropped()` takes a write lock, which blocks until all in-flight `kill()` calls complete,
/// then sets `dropped = true`. This is called from `HyperlightVm::drop()` before `WhpVm::drop()`
/// runs, ensuring no `kill()` is accessing the partition when `WHvDeletePartition` is called.
- partition_state: std::sync::RwLock,
+ dropped_state: std::sync::RwLock<(bool, T)>,
}
-
-/// State protected by the RwLock in `WindowsInterruptHandle`.
-///
-/// Contains a copy of the partition handle from `WhpVm` (not an owning reference).
-/// The RwLock and `dropped` flag ensure this handle is never used after `WhpVm`
-/// deletes the partition.
-#[cfg(target_os = "windows")]
-#[derive(Debug)]
-pub(super) struct PartitionState {
- /// Copy of partition handle from `WhpVm`. Only valid while `dropped` is false.
- pub(super) handle: windows::Win32::System::Hypervisor::WHV_PARTITION_HANDLE,
- /// Set true before partition deletion; prevents further use of `handle`.
- pub(super) dropped: bool,
-}
-
-#[cfg(target_os = "windows")]
-impl WindowsInterruptHandle {
- const RUNNING_BIT: u8 = 1 << 1;
- const CANCEL_BIT: u8 = 1 << 0;
- #[cfg(gdb)]
- const DEBUG_INTERRUPT_BIT: u8 = 1 << 2;
+#[cfg(any(target_os = "windows", hvf))]
+trait SynchronousInterruptState: Debug + Send + Sync {
+ /// The inside-the-lock part of the common part of both kill()
+ /// and kill_from_debugger()
+ fn actually_cancel(&self) -> bool;
+
+ #[cfg(hvf)]
+ fn set_vcpu(&mut self, vcpu: hv_vcpu_t);
}
-#[cfg(target_os = "windows")]
-impl InterruptHandleImpl for WindowsInterruptHandle {
- fn set_running(&self) {
- // Release ordering to ensure prior memory operations are visible when another thread observes running=true
- self.state.fetch_or(Self::RUNNING_BIT, Ordering::Release);
- }
-
- fn is_cancelled(&self) -> bool {
- // Acquire ordering to synchronize with the Release in kill()
- // This ensures we see the CANCEL_BIT set by the interrupt thread
- self.state.load(Ordering::Acquire) & Self::CANCEL_BIT != 0
- }
-
- fn clear_cancel(&self) {
- // Release ordering to ensure that any operations from the previous run()
- // are visible to other threads. While this is typically called by the vcpu thread
- // at the start of run(), the VM itself can move between threads across guest calls.
- self.state.fetch_and(!Self::CANCEL_BIT, Ordering::Release);
- }
-
- fn clear_running(&self) {
- // Release ordering to ensure all vcpu operations are visible before clearing running
- self.state.fetch_and(!Self::RUNNING_BIT, Ordering::Release);
- }
-
- fn is_debug_interrupted(&self) -> bool {
- #[cfg(gdb)]
- {
- self.state.load(Ordering::Acquire) & Self::DEBUG_INTERRUPT_BIT != 0
- }
- #[cfg(not(gdb))]
- {
- false
- }
- }
-
- #[cfg(gdb)]
- fn clear_debug_interrupt(&self) {
- self.state
- .fetch_and(!Self::DEBUG_INTERRUPT_BIT, Ordering::Release);
+#[cfg(any(target_os = "windows", hvf))]
+impl InterruptHandleImpl for SynchronousInterruptHandle {
+ #[cfg(hvf)]
+ fn set_vcpu(&self, vcpu: hv_vcpu_t) {
+ let Ok(mut guard) = self.dropped_state.write() else {
+ return;
+ };
+ guard.1.set_vcpu(vcpu);
}
fn set_dropped(&self) {
@@ -371,9 +379,9 @@ impl InterruptHandleImpl for WindowsInterruptHandle {
// 3. Set dropped=true so no future kill() calls will use the handle
// After this returns, no WHvCancelRunVirtualProcessor calls are in progress
// or will ever be made, so WHvDeletePartition can safely be called.
- match self.partition_state.write() {
+ match self.dropped_state.write() {
Ok(mut guard) => {
- guard.dropped = true;
+ guard.0 = true;
}
Err(e) => {
tracing::error!("Failed to acquire partition_state write lock: {}", e);
@@ -382,26 +390,33 @@ impl InterruptHandleImpl for WindowsInterruptHandle {
}
}
-#[cfg(target_os = "windows")]
-impl InterruptHandle for WindowsInterruptHandle {
- fn kill(&self) -> bool {
- use windows::Win32::System::Hypervisor::WHvCancelRunVirtualProcessor;
-
- // Release ordering ensures that any writes before kill() are visible to the vcpu thread
- // when it checks is_cancelled() with Acquire ordering
- self.state.fetch_or(Self::CANCEL_BIT, Ordering::Release);
-
- // Acquire ordering to synchronize with the Release in set_running()
- // This ensures we see the running state set by the vcpu thread
- let state = self.state.load(Ordering::Acquire);
- if state & Self::RUNNING_BIT == 0 {
+#[cfg(any(target_os = "windows", hvf))]
+impl InterruptHandle for SynchronousInterruptHandle {
+ fn dropped(&self) -> bool {
+ // Take read lock to check dropped state consistently
+ match self.dropped_state.read() {
+ Ok(guard) => guard.0,
+ Err(e) => {
+ tracing::error!("Failed to acquire partition_state read lock: {}", e);
+ true // Assume dropped if we can't acquire lock
+ }
+ }
+ }
+}
+#[cfg(any(target_os = "windows", hvf))]
+impl InterruptHandleInternal for SynchronousInterruptHandle {
+ fn state(&self) -> &InterruptHandleStateMachine {
+ &self.state
+ }
+ fn common_kill(&self) -> bool {
+ if !self.state.get_running_cancel_debug().0 {
return false;
}
// Take read lock to prevent race with WHvDeletePartition in set_dropped().
// Multiple kill() calls can proceed concurrently (read locks don't block each other),
// but set_dropped() will wait for all kill() calls to complete before proceeding.
- let guard = match self.partition_state.read() {
+ let guard = match self.dropped_state.read() {
Ok(guard) => guard,
Err(e) => {
tracing::error!("Failed to acquire partition_state read lock: {}", e);
@@ -409,49 +424,67 @@ impl InterruptHandle for WindowsInterruptHandle {
}
};
- if guard.dropped {
+ if guard.0 {
return false;
}
- unsafe { WHvCancelRunVirtualProcessor(guard.handle, 0, 0).is_ok() }
+ guard.1.actually_cancel()
}
- #[cfg(gdb)]
- fn kill_from_debugger(&self) -> bool {
- use windows::Win32::System::Hypervisor::WHvCancelRunVirtualProcessor;
-
- self.state
- .fetch_or(Self::DEBUG_INTERRUPT_BIT, Ordering::Release);
+}
- // Acquire ordering to synchronize with the Release in set_running()
- let state = self.state.load(Ordering::Acquire);
- if state & Self::RUNNING_BIT == 0 {
- return false;
+#[cfg(target_os = "windows")]
+use windows::Win32::System::Hypervisor::WHV_PARTITION_HANDLE;
+#[cfg(target_os = "windows")]
+pub(super) type WindowsInterruptHandle = SynchronousInterruptHandle;
+#[cfg(target_os = "windows")]
+impl WindowsInterruptHandle {
+ fn new(hdl: WHV_PARTITION_HANDLE) -> Self {
+ SynchronousInterruptHandle {
+ state: InterruptHandleStateMachine::new(),
+ dropped_state: std::sync::RwLock::new((false, hdl)),
}
+ }
+}
+#[cfg(target_os = "windows")]
+impl SynchronousInterruptState for WHV_PARTITION_HANDLE {
+ fn actually_cancel(&self) -> bool {
+ use windows::Win32::System::Hypervisor::WHvCancelRunVirtualProcessor;
+ unsafe { WHvCancelRunVirtualProcessor(*self, 0, 0).is_ok() }
+ }
+}
- // Take read lock to prevent race with WHvDeletePartition in set_dropped()
- let guard = match self.partition_state.read() {
- Ok(guard) => guard,
- Err(e) => {
- tracing::error!("Failed to acquire partition_state read lock: {}", e);
- return false;
- }
- };
-
- if guard.dropped {
+#[cfg(hvf)]
+use crate::hypervisor::virtual_machine::hvf::bindings::hv_vcpu_t;
+#[cfg(hvf)]
+pub(super) type HvfInterruptHandle =
+ RetryingInterruptHandle>>;
+#[cfg(hvf)]
+impl SynchronousInterruptState for Option {
+ fn actually_cancel(&self) -> bool {
+ use crate::hypervisor::virtual_machine::hvf::bindings::{HV_SUCCESS, hv_vcpus_exit};
+ let Some(vcpu) = self else {
return false;
+ };
+ unsafe {
+ // bindgen automatically uses *mut, but actually this will
+ // not be written to.
+ hv_vcpus_exit(&raw const *vcpu as *mut hv_vcpu_t, 1).0.0.0 == HV_SUCCESS
}
-
- unsafe { WHvCancelRunVirtualProcessor(guard.handle, 0, 0).is_ok() }
}
- fn dropped(&self) -> bool {
- // Take read lock to check dropped state consistently
- match self.partition_state.read() {
- Ok(guard) => guard.dropped,
- Err(e) => {
- tracing::error!("Failed to acquire partition_state read lock: {}", e);
- true // Assume dropped if we can't acquire lock
- }
+ fn set_vcpu(&mut self, vcpu: hv_vcpu_t) {
+ *self = Some(vcpu);
+ }
+}
+#[cfg(hvf)]
+impl HvfInterruptHandle {
+ pub(super) fn new(retry_delay: Duration) -> Self {
+ RetryingInterruptHandle {
+ retry_delay,
+ inner: SynchronousInterruptHandle {
+ state: InterruptHandleStateMachine::new(),
+ dropped_state: std::sync::RwLock::new((false, None)),
+ },
}
}
}
diff --git a/src/hyperlight_host/src/hypervisor/surrogate_process.rs b/src/hyperlight_host/src/hypervisor/surrogate_process.rs
index 481aa44e8..0189c6530 100644
--- a/src/hyperlight_host/src/hypervisor/surrogate_process.rs
+++ b/src/hyperlight_host/src/hypervisor/surrogate_process.rs
@@ -18,7 +18,6 @@ use core::ffi::c_void;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
-use hyperlight_common::mem::PAGE_SIZE_USIZE;
use tracing::{Span, instrument};
use windows::Win32::Foundation::HANDLE;
use windows::Win32::System::Memory::{
@@ -150,7 +149,7 @@ impl SurrogateProcess {
VirtualProtectEx(
self.process_handle.into(),
first_guard_page_start,
- PAGE_SIZE_USIZE,
+ page_size::get(),
PAGE_NOACCESS,
&mut unused_out_old_prot_flags,
)
@@ -161,12 +160,12 @@ impl SurrogateProcess {
// the last page of the raw_size is the guard page
let last_guard_page_start =
- unsafe { first_guard_page_start.add(host_size - PAGE_SIZE_USIZE) };
+ unsafe { first_guard_page_start.add(host_size - page_size::get()) };
if let Err(e) = unsafe {
VirtualProtectEx(
self.process_handle.into(),
last_guard_page_start,
- PAGE_SIZE_USIZE,
+ page_size::get(),
PAGE_NOACCESS,
&mut unused_out_old_prot_flags,
)
diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/hvf/bindings.h b/src/hyperlight_host/src/hypervisor/virtual_machine/hvf/bindings.h
new file mode 100644
index 000000000..7d0dd17d8
--- /dev/null
+++ b/src/hyperlight_host/src/hypervisor/virtual_machine/hvf/bindings.h
@@ -0,0 +1,22 @@
+/*
+Copyright 2025 The Hyperlight Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+#include
+
+hv_return_t
+hv_vcpu_get_simd_fp_reg_rsabi(hv_vcpu_t vcpu, hv_simd_fp_reg_t reg, char *val);
+hv_return_t
+hv_vcpu_set_simd_fp_reg_rsabi(hv_vcpu_t vcpu, hv_simd_fp_reg_t reg, const char *val);
diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/hvf/fp_abi.c b/src/hyperlight_host/src/hypervisor/virtual_machine/hvf/fp_abi.c
new file mode 100644
index 000000000..a6fa3edac
--- /dev/null
+++ b/src/hyperlight_host/src/hypervisor/virtual_machine/hvf/fp_abi.c
@@ -0,0 +1,36 @@
+/*
+Copyright 2025 The Hyperlight Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+#include "bindings.h"
+
+hv_return_t
+hv_vcpu_get_simd_fp_reg_rsabi(hv_vcpu_t vcpu, hv_simd_fp_reg_t reg, char *val) {
+ hv_simd_fp_uchar16_t simd = {0};
+ hv_return_t ret = hv_vcpu_get_simd_fp_reg(vcpu, reg, &simd);
+ for (int i = 0; i < 16; ++i) {
+ val[i] = simd[i];
+ }
+ return ret;
+}
+
+hv_return_t
+hv_vcpu_set_simd_fp_reg_rsabi(hv_vcpu_t vcpu, hv_simd_fp_reg_t reg, const char *val) {
+ hv_simd_fp_uchar16_t simd;
+ for (int i = 0; i < 16; ++i) {
+ simd[i] = val[i];
+ }
+ return hv_vcpu_set_simd_fp_reg(vcpu, reg, simd);
+}
diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/hvf/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/hvf/mod.rs
new file mode 100644
index 000000000..b83cdeaef
--- /dev/null
+++ b/src/hyperlight_host/src/hypervisor/virtual_machine/hvf/mod.rs
@@ -0,0 +1,938 @@
+/*
+Copyright 2025 The Hyperlight Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ */
+
+//! # Bridging Hyperlight's assumptions with Hypervisor.framework
+//!
+//! Hypervisor.framework has some constraints that run counter to the
+//! flexibility provided by the usual Hyperlight API. In particular,
+//! hvf assumes that there is only <= 1 VM per process, and <= 1 VCPU
+//! per thread.
+//!
+//! ## Supporting more than one Sandbox per process
+//!
+//! This is not yet implemented, but we plan to support >1 sandbox per
+//! process by using nested virtualisation on platforms where it is
+//! available, making each sandbox a nested guest VM. This does,
+//! unfortunately, of course have some performance implications.
+//!
+//! ## Supporting [`core::marker::Send`] on Sandboxes
+//!
+//! The Hyperlight public API constraints sandboxes (and, by
+//! extension, hypervisor API implementations) to implement
+//! [`core::marker::Send`]. Other hypervisors have one vCPU but allow
+//! the vCPU handle to be mgirated across threads, although this comes
+//! with severe performance impact in some cases (e.g. KVM).
+//!
+//! Unfortunately, this does not work on hvf, since cross-thread
+//! access is prohibited (most `hv_vcpu_` functions note that "This
+//! function must be called by the owning thread") rather than merely
+//! unperformant.
+//!
+//! There are, largely, two approaches that we could use to work
+//! around this. We could either:
+//!
+//! 1. Create a dedicated vcpu thread, and implement sandbox
+//! operations as RPCs to that thread
+//! 2. Create one vcpu per thread-from-which-a-Sandbox-is-used, and
+//! reset that vcpu's state to match the correct sandbox whenever a
+//! sandbox operation is called.
+//!
+//! A long time ago, Hyperlight briefly unconditionally used approach
+//! (1) on all hypervisors, but it had unacceptable performance impact
+//! on most of them.
+//!
+//! Although an implementation of (1) scoped just to hvf could make
+//! sense, this module presently implements (2), on the rationale that
+//! it ought to be /possible/ for a host application to get decent
+//! performance out of (2) (if the host application exercises some
+//! discipline and uses a 1:1 mapping between sandboxes and threads;
+//! although there is some unavoidable overhead due to needing to sync
+//! registers on every VM exit, unfortunately), but an implementation
+//! based on (1) would have to create the thread up-front (not knowing
+//! if the sandbox would in fact be Sent to another thread) and so
+//! would impose unavoidable overhead on all consumers.
+//!
+//! Applications which wish to use multiple sandboxes per thread, or
+//! multiple threads per sandbox, and to have decent performance on
+//! Hypervisor.framework, should consider (and benchmark!) the
+//! alternative architecture of keeping the sandbox itself on a single
+//! thread and pass data/make RPCs to/from that thread.
+
+use core::cell::RefCell;
+use core::ffi;
+use core::sync::atomic::{AtomicBool, AtomicU64, Ordering};
+use std::sync::Arc;
+use std::time::Duration;
+
+use hyperlight_common::outb::VmAction;
+use parking_lot::{Mutex, MutexGuard};
+
+use super::{
+ CreateVmError, HvfSyncError, HypervisorError, MapMemoryError, RegisterError, ResetVcpuError,
+ RunVcpuError, UnmapMemoryError, VirtualMachine, VmExit,
+};
+use crate::hypervisor::InterruptHandleImpl;
+use crate::hypervisor::regs::{
+ CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters,
+};
+use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags};
+
+#[allow(
+ dead_code,
+ non_snake_case,
+ non_upper_case_globals,
+ non_camel_case_types
+)] // bindgen
+pub(crate) mod bindings {
+ include!(concat!(env!("OUT_DIR"), "/hvf_bindings.rs"));
+ impl hv_return_t {
+ pub(super) fn is_success(&self) -> Result<(), super::HypervisorError> {
+ if self.0.0.0 == HV_SUCCESS {
+ Ok(())
+ } else {
+ Err(super::HypervisorError::HvfError(*self))
+ }
+ }
+ pub(super) fn unless_success(
+ &self,
+ f: impl Fn(super::HypervisorError) -> T,
+ ) -> Result<(), T> {
+ self.is_success().map_err(f)
+ }
+ }
+ impl core::fmt::Display for hv_return_t {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
+ write!(f, "0x{:x}", self.0.0.0)
+ }
+ }
+ // the default base-10 signed printout is totally useless
+ impl core::fmt::Debug for hv_return_t {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
+ write!(f, "0x{:x}", self.0.0.0)
+ }
+ }
+}
+
+pub(super) fn is_hypervisor_present() -> bool {
+ let mut val: ffi::c_int = 0;
+ let mut len: usize = core::mem::size_of::();
+ let ret = unsafe {
+ libc::sysctlbyname(
+ c"kern.hv_support".as_ptr(),
+ &raw mut val as *mut ffi::c_void,
+ &raw mut len,
+ std::ptr::null_mut(),
+ 0,
+ )
+ };
+ ret == 0 && val == 1
+}
+
+// Used to figure out when registers have been updated since the last
+// time that the sandbox ran on this vcpu & need to be sync'd back.
+#[derive(Clone, Copy, Default, Debug)]
+struct EpochStamped {
+ value: T,
+ epoch: u64,
+}
+
+#[derive(Clone, Copy, PartialEq, Debug)]
+struct SandboxId(u64);
+
+struct HvfCpu {
+ id: bindings::hv_vcpu_t,
+ exit: *mut bindings::hv_vcpu_exit_t,
+ current_loaded: Option>,
+
+ // reset_vcpu() needs to destroy the current vcpu before it can
+ // create the new one, but when it creates the new one and assigns
+ // it to the thread-local, the HvfCpu for the old one will be
+ // dropped. This lets the Drop implementation know that that has
+ // happened and avoid destroying the cpu a second time.
+ destroyed_in_reset_vcpu: bool,
+}
+
+impl HvfCpu {
+ /// Must be called only once per thread.
+ fn new() -> Result {
+ use core::mem::MaybeUninit;
+ let mut vcpu: MaybeUninit = MaybeUninit::zeroed();
+ let mut exit: *mut bindings::hv_vcpu_exit_t = core::ptr::null_mut();
+ unsafe {
+ let config = bindings::hv_vcpu_config_create();
+ bindings::hv_vcpu_create(vcpu.as_mut_ptr(), &raw mut exit, config)
+ }
+ .is_success()?;
+ Ok(Self {
+ id: unsafe { vcpu.assume_init() },
+ exit,
+ current_loaded: None,
+ destroyed_in_reset_vcpu: false,
+ })
+ }
+
+ fn reset_vcpu(&mut self) -> Result<(), HypervisorError> {
+ // TODO: figure out if there is a more efficient way to clear
+ // all the state
+ unsafe { bindings::hv_vcpu_destroy(self.id) }.is_success()?;
+ self.destroyed_in_reset_vcpu = true;
+ *self = HvfCpu::new()?;
+ Ok(())
+ }
+
+ fn hv_vcpu_get_reg(&self, reg: bindings::hv_reg_t) -> Result {
+ let mut value: u64 = 0;
+ unsafe { bindings::hv_vcpu_get_reg(self.id, reg, &raw mut value) }.is_success()?;
+ Ok(value)
+ }
+
+ fn hv_vcpu_set_reg(
+ &mut self,
+ reg: bindings::hv_reg_t,
+ value: u64,
+ ) -> Result<(), HypervisorError> {
+ unsafe { bindings::hv_vcpu_set_reg(self.id, reg, value) }.is_success()
+ }
+
+ fn hv_vcpu_get_simd_fp_reg(
+ &self,
+ reg: bindings::hv_simd_fp_reg_t,
+ ) -> Result {
+ let mut value: [u8; 16] = [0; 16];
+ unsafe {
+ bindings::hv_vcpu_get_simd_fp_reg_rsabi(self.id, reg, value.as_mut_ptr() as *mut i8)
+ }
+ .is_success()?;
+ Ok(u128::from_ne_bytes(value))
+ }
+
+ fn hv_vcpu_set_simd_fp_reg(
+ &mut self,
+ reg: bindings::hv_simd_fp_reg_t,
+ value: u128,
+ ) -> Result<(), HypervisorError> {
+ let bytes: [u8; 16] = value.to_ne_bytes();
+ unsafe {
+ bindings::hv_vcpu_set_simd_fp_reg_rsabi(self.id, reg, bytes.as_ptr() as *const i8)
+ }
+ .is_success()
+ }
+
+ fn hv_vcpu_get_sys_reg(&self, reg: bindings::hv_sys_reg_t) -> Result {
+ let mut value: u64 = 0;
+ unsafe { bindings::hv_vcpu_get_sys_reg(self.id, reg, &raw mut value) }.is_success()?;
+ Ok(value)
+ }
+
+ fn hv_vcpu_set_sys_reg(
+ &mut self,
+ reg: bindings::hv_sys_reg_t,
+ value: u64,
+ ) -> Result<(), HypervisorError> {
+ unsafe { bindings::hv_vcpu_set_sys_reg(self.id, reg, value) }.is_success()
+ }
+
+ fn set_vcpu_regs(&mut self, regs: &CommonRegisters) -> Result<(), HypervisorError> {
+ use bindings::{hv_reg_t, hv_sys_reg_t};
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X0, regs.x[0])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X1, regs.x[1])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X2, regs.x[2])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X3, regs.x[3])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X4, regs.x[4])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X5, regs.x[5])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X6, regs.x[6])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X7, regs.x[7])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X8, regs.x[8])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X9, regs.x[9])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X10, regs.x[10])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X11, regs.x[11])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X12, regs.x[12])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X13, regs.x[13])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X14, regs.x[14])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X15, regs.x[15])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X16, regs.x[16])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X17, regs.x[17])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X18, regs.x[18])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X19, regs.x[19])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X20, regs.x[20])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X21, regs.x[21])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X22, regs.x[22])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X23, regs.x[23])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X24, regs.x[24])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X25, regs.x[25])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X26, regs.x[26])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X27, regs.x[27])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X28, regs.x[28])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X29, regs.x[29])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_X30, regs.x[30])?;
+ // set SP_EL0 or SP_EL1 depending on SPSel
+ if regs.pstate & 0x1 == 0x1 {
+ self.hv_vcpu_set_sys_reg(hv_sys_reg_t::HV_SYS_REG_SP_EL1, regs.sp)?;
+ } else {
+ self.hv_vcpu_set_sys_reg(hv_sys_reg_t::HV_SYS_REG_SP_EL0, regs.sp)?;
+ }
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_PC, regs.pc)?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_CPSR, regs.pstate)?;
+ Ok(())
+ }
+
+ fn get_vcpu_regs(&self) -> Result {
+ use bindings::{hv_reg_t, hv_sys_reg_t};
+ let pstate = self.hv_vcpu_get_reg(hv_reg_t::HV_REG_CPSR)?;
+ Ok(CommonRegisters {
+ x: [
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X0)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X1)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X2)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X3)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X4)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X5)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X6)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X7)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X8)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X9)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X10)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X11)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X12)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X13)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X14)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X15)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X16)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X17)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X18)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X19)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X20)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X21)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X22)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X23)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X24)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X25)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X26)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X27)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X28)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X29)?,
+ self.hv_vcpu_get_reg(hv_reg_t::HV_REG_X30)?,
+ ],
+ sp: if pstate & 0x1 == 0x1 {
+ self.hv_vcpu_get_sys_reg(hv_sys_reg_t::HV_SYS_REG_SP_EL1)?
+ } else {
+ self.hv_vcpu_get_sys_reg(hv_sys_reg_t::HV_SYS_REG_SP_EL0)?
+ },
+ pc: self.hv_vcpu_get_reg(hv_reg_t::HV_REG_PC)?,
+ pstate,
+ })
+ }
+
+ fn set_vcpu_fpregs(&mut self, regs: &CommonFpu) -> Result<(), HypervisorError> {
+ use bindings::{hv_reg_t, hv_simd_fp_reg_t};
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q0, regs.v[0])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q1, regs.v[1])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q2, regs.v[2])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q3, regs.v[3])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q4, regs.v[4])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q5, regs.v[5])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q6, regs.v[6])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q7, regs.v[7])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q8, regs.v[8])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q9, regs.v[9])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q10, regs.v[10])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q11, regs.v[11])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q12, regs.v[12])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q13, regs.v[13])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q14, regs.v[14])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q15, regs.v[15])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q16, regs.v[16])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q17, regs.v[17])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q18, regs.v[18])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q19, regs.v[19])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q20, regs.v[20])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q21, regs.v[21])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q22, regs.v[22])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q23, regs.v[23])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q24, regs.v[24])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q25, regs.v[25])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q26, regs.v[26])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q27, regs.v[27])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q28, regs.v[28])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q29, regs.v[29])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q30, regs.v[30])?;
+ self.hv_vcpu_set_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q31, regs.v[31])?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_FPSR, regs.fpsr as u64)?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_FPCR, regs.fpcr as u64)?;
+ Ok(())
+ }
+
+ fn get_vcpu_fpregs(&mut self) -> Result {
+ use bindings::{hv_reg_t, hv_simd_fp_reg_t};
+ Ok(CommonFpu {
+ v: [
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q0)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q1)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q2)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q3)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q4)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q5)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q6)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q7)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q8)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q9)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q10)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q11)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q12)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q13)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q14)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q15)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q16)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q17)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q18)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q19)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q20)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q21)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q22)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q23)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q24)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q25)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q26)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q27)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q28)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q29)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q30)?,
+ self.hv_vcpu_get_simd_fp_reg(hv_simd_fp_reg_t::HV_SIMD_FP_REG_Q31)?,
+ ],
+ fpsr: self.hv_vcpu_get_reg(hv_reg_t::HV_REG_FPSR)? as u32,
+ fpcr: self.hv_vcpu_get_reg(hv_reg_t::HV_REG_FPCR)? as u32,
+ })
+ }
+
+ fn set_vcpu_sregs(&mut self, regs: &CommonSpecialRegisters) -> Result<(), HypervisorError> {
+ use bindings::hv_sys_reg_t;
+ self.hv_vcpu_set_sys_reg(hv_sys_reg_t::HV_SYS_REG_TTBR0_EL1, regs.ttbr0_el1)?;
+ self.hv_vcpu_set_sys_reg(hv_sys_reg_t::HV_SYS_REG_TCR_EL1, regs.tcr_el1)?;
+ self.hv_vcpu_set_sys_reg(hv_sys_reg_t::HV_SYS_REG_MAIR_EL1, regs.mair_el1)?;
+ self.hv_vcpu_set_sys_reg(hv_sys_reg_t::HV_SYS_REG_SCTLR_EL1, regs.sctlr_el1)?;
+ self.hv_vcpu_set_sys_reg(hv_sys_reg_t::HV_SYS_REG_CPACR_EL1, regs.cpacr_el1)?;
+ self.hv_vcpu_set_sys_reg(hv_sys_reg_t::HV_SYS_REG_VBAR_EL1, regs.vbar_el1)?;
+ self.hv_vcpu_set_sys_reg(hv_sys_reg_t::HV_SYS_REG_SP_EL1, regs.sp_el1)?;
+ Ok(())
+ }
+
+ fn get_vcpu_sregs(&self) -> Result {
+ use bindings::hv_sys_reg_t;
+ Ok(CommonSpecialRegisters {
+ ttbr0_el1: self.hv_vcpu_get_sys_reg(hv_sys_reg_t::HV_SYS_REG_TTBR0_EL1)?,
+ tcr_el1: self.hv_vcpu_get_sys_reg(hv_sys_reg_t::HV_SYS_REG_TCR_EL1)?,
+ mair_el1: self.hv_vcpu_get_sys_reg(hv_sys_reg_t::HV_SYS_REG_MAIR_EL1)?,
+ sctlr_el1: self.hv_vcpu_get_sys_reg(hv_sys_reg_t::HV_SYS_REG_SCTLR_EL1)?,
+ cpacr_el1: self.hv_vcpu_get_sys_reg(hv_sys_reg_t::HV_SYS_REG_CPACR_EL1)?,
+ vbar_el1: self.hv_vcpu_get_sys_reg(hv_sys_reg_t::HV_SYS_REG_VBAR_EL1)?,
+ sp_el1: self.hv_vcpu_get_sys_reg(hv_sys_reg_t::HV_SYS_REG_SP_EL1)?,
+ })
+ }
+
+ fn sync_state_from(
+ &mut self,
+ sandbox_id: EpochStamped,
+ regs: &EpochStamped,
+ sregs: &EpochStamped,
+ fpregs: &EpochStamped,
+ ) -> Result {
+ let (regs_dirty, fpregs_dirty, sregs_dirty) = match self.current_loaded {
+ None => (true, true, true),
+ Some(s) => {
+ if sandbox_id.value != s.value || sandbox_id.epoch > s.epoch {
+ self.reset_vcpu().map_err(HvfSyncError::ResetVcpu)?;
+ (true, true, true)
+ } else {
+ (
+ regs.epoch > s.epoch,
+ fpregs.epoch > s.epoch,
+ sregs.epoch > s.epoch,
+ )
+ }
+ }
+ };
+ if regs_dirty {
+ self.set_vcpu_regs(®s.value)
+ .map_err(RegisterError::SetRegs)?;
+ }
+ if fpregs_dirty {
+ self.set_vcpu_fpregs(&fpregs.value)
+ .map_err(RegisterError::SetFpu)?;
+ }
+ if sregs_dirty {
+ self.set_vcpu_sregs(&sregs.value)
+ .map_err(RegisterError::SetSregs)?;
+ }
+ let sync_epoch = core::cmp::max(sandbox_id.epoch, core::cmp::max(regs.epoch, sregs.epoch));
+ self.current_loaded = Some(EpochStamped {
+ value: sandbox_id.value,
+ epoch: sync_epoch,
+ });
+ Ok(sync_epoch)
+ }
+
+ fn sync_state_from_vm(&mut self, vm: &mut HvfVm) -> Result<(), HvfSyncError> {
+ let epoch = self.sync_state_from(vm.id, &vm.regs, &vm.sregs, &vm.fpu)?;
+ // update epoch eagerly, since at this point the vcpu
+ // epoch has been updated to the max, so if we returned early (e.g. due
+ // to an error in the next call(s)) it would be possible
+ // to miss updates
+ vm.id.epoch = epoch;
+ vm.regs.epoch = epoch;
+ vm.fpu.epoch = epoch;
+ vm.sregs.epoch = epoch;
+ Ok(())
+ }
+
+ fn sync_state_to_vm(&mut self, vm: &mut HvfVm) -> Result<(), HvfSyncError> {
+ let Some(EpochStamped {
+ value: sandbox_id,
+ epoch,
+ }) = self.current_loaded
+ else {
+ // sync_state_to_vm is always used just after
+ // sync_state_from_vm, so this should be impossible
+ debug_assert!(false);
+ return Err(HvfSyncError::SyncInvariant(
+ "Missing loaded sandbox".to_string(),
+ ));
+ };
+ if sandbox_id != vm.id.value {
+ // sync_state_to_vm is always used just after
+ // sync_state_from_vm, so this should be impossible
+ debug_assert!(false);
+ return Err(HvfSyncError::SyncInvariant(
+ "Wrong loaded sandbox".to_string(),
+ ));
+ }
+ vm.id.epoch = epoch;
+ vm.regs = EpochStamped {
+ value: self.get_vcpu_regs().map_err(RegisterError::GetRegs)?,
+ epoch,
+ };
+ vm.fpu = EpochStamped {
+ value: self.get_vcpu_fpregs().map_err(RegisterError::GetFpu)?,
+ epoch,
+ };
+ vm.sregs = EpochStamped {
+ value: self.get_vcpu_sregs().map_err(RegisterError::GetSregs)?,
+ epoch,
+ };
+ Ok(())
+ }
+
+ fn advance_pc(&mut self) -> Result<(), HypervisorError> {
+ use bindings::hv_reg_t;
+ let old_pc = self.hv_vcpu_get_reg(hv_reg_t::HV_REG_PC)?;
+ self.hv_vcpu_set_reg(hv_reg_t::HV_REG_PC, old_pc + 4)?;
+ Ok(())
+ }
+
+ fn run(&mut self) -> Result {
+ let ret = unsafe { bindings::hv_vcpu_run(self.id) };
+ if let Some(EpochStamped { ref mut epoch, .. }) = self.current_loaded {
+ *epoch += 1;
+ }
+ ret.is_success()?;
+ let exit = unsafe { self.exit.read() };
+
+ use bindings::hv_exit_reason_t;
+ let hl_exit = match exit.reason {
+ hv_exit_reason_t::HV_EXIT_REASON_CANCELED => VmExit::Cancelled(),
+ hv_exit_reason_t::HV_EXIT_REASON_EXCEPTION => {
+ #[inline(always)]
+ fn bits(x: u64) -> u64 {
+ (x & ((1 << (HIGH_BIT + 1)) - 1)) >> LOW_BIT
+ }
+
+ let esr = exit.exception.syndrome.0;
+ let ipa = exit.exception.physical_address.0;
+
+ let unknown_exit = VmExit::Unknown(format!(
+ "Unknown HVF vcpu exit ESR_EL2: {:16x} IPA {:16x}",
+ esr, ipa,
+ ));
+
+ const ESR_EL2_EC_DATA_ABORT_LOWER_EL: u64 = 0b100100;
+ let ec = bits::<31, 26>(esr);
+ let isv = bits::<24, 24>(esr);
+ if ec == ESR_EL2_EC_DATA_ABORT_LOWER_EL && isv == 0b1 {
+ let is_translation_fault =
+ bits::<5, 2>(esr) == 0b0001 || bits::<5, 0>(esr) == 0b101011;
+ let is_permission_fault = bits::<5, 2>(esr) == 0b0011;
+ if is_translation_fault || is_permission_fault {
+ // For MMIO exits, always resume after the
+ // faulting instruction to match kvm behaviour
+ self.advance_pc()?;
+
+ let wnr = bits::<6, 6>(esr);
+ if wnr == 1 {
+ let io_page_gpa =
+ const { hyperlight_common::layout::io_page().unwrap().0 };
+ if ipa >= io_page_gpa
+ && let off = (ipa - io_page_gpa) as usize
+ && off < hyperlight_common::vmem::PAGE_SIZE
+ {
+ let port = off / core::mem::size_of::();
+ if port == VmAction::Halt as usize {
+ VmExit::Halt()
+ } else {
+ let srt = bits::<20, 16>(esr);
+ let data = self.get_vcpu_regs()?.x[srt as usize];
+ VmExit::IoOut(port as u16, data.to_ne_bytes().to_vec())
+ }
+ } else {
+ VmExit::MmioWrite(ipa)
+ }
+ } else {
+ VmExit::MmioRead(ipa)
+ }
+ } else {
+ unknown_exit
+ }
+ } else {
+ unknown_exit
+ }
+ }
+ reason => VmExit::Unknown(format!("Unknown HVF vcpu exit reason: {}", reason.0)),
+ };
+ Ok(hl_exit)
+ }
+}
+
+impl Drop for HvfCpu {
+ fn drop(&mut self) {
+ unsafe {
+ if !self.destroyed_in_reset_vcpu {
+ bindings::hv_vcpu_destroy(self.id);
+ };
+ }
+ }
+}
+
+std::thread_local! {
+ static HVF_VCPU: RefCell