Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 29 additions & 16 deletions src/hyperlight_host/src/hypervisor/surrogate_process_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,26 +86,29 @@ fn surrogate_binary_name() -> Result<String> {
/// (or `None` when the variable is unset or unparsable).
///
/// Resolution order:
/// 1. `max` is clamped to `1..=HARD_MAX_SURROGATE_PROCESSES`, defaulting
/// 1. `max` is clamped to `0..=HARD_MAX_SURROGATE_PROCESSES`, defaulting
/// to `HARD_MAX_SURROGATE_PROCESSES` when `None`.
/// 2. `initial` is clamped to `1..=max`, defaulting to `max` when `None`.
/// 2. `initial` is clamped to `0..=max`, defaulting to `max` when `None`.
/// This guarantees `initial <= max` without an extra conditional.
///
/// When `max == 0`, surrogates are disabled entirely and the system
/// falls back to `WHvMapGpaRange` (single-VM-per-process mode).
fn compute_surrogate_counts(raw_initial: Option<usize>, raw_max: Option<usize>) -> (usize, usize) {
let max = raw_max
.map(|n| n.clamp(1, HARD_MAX_SURROGATE_PROCESSES))
.map(|n| n.clamp(0, HARD_MAX_SURROGATE_PROCESSES))
.unwrap_or(HARD_MAX_SURROGATE_PROCESSES);

// Clamp initial to 1..=max so it can never exceed the authoritative limit.
let initial = raw_initial.map(|n| n.clamp(1, max)).unwrap_or(max);
// Clamp initial to 0..=max so it can never exceed the authoritative limit.
let initial = raw_initial.map(|n| n.clamp(0, max)).unwrap_or(max);

(initial, max)
}

/// Returns the (initial, max) surrogate process counts from environment
/// variables, applying validation and clamping.
///
/// - `HYPERLIGHT_INITIAL_SURROGATES`: clamped to `1..=max`, default `max`.
/// - `HYPERLIGHT_MAX_SURROGATES`: clamped to `1..=512`, default 512.
/// - `HYPERLIGHT_INITIAL_SURROGATES`: clamped to `0..=max`, default `max`.
/// - `HYPERLIGHT_MAX_SURROGATES`: clamped to `0..=512`, default 512.
fn surrogate_process_counts() -> (usize, usize) {
let raw_initial = std::env::var(INITIAL_SURROGATES_ENV_VAR)
.ok()
Expand Down Expand Up @@ -353,6 +356,16 @@ pub(crate) fn get_surrogate_process_manager() -> Result<&'static SurrogateProces
}
}

/// Returns `true` when `HYPERLIGHT_MAX_SURROGATES=0`, meaning surrogate
/// processes are disabled and the system should use `WHvMapGpaRange`
/// (single-VM-per-process mode) instead of `WHvMapGpaRange2`.
pub(crate) fn surrogates_disabled() -> bool {
std::env::var(MAX_SURROGATES_ENV_VAR)
.ok()
.and_then(|v| v.parse::<usize>().ok())
.is_some_and(|n| n == 0)
}

// Creates a job object that will terminate all the surrogate processes when the struct instance is dropped.
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
fn create_job_object() -> Result<HandleWrapper> {
Expand Down Expand Up @@ -885,9 +898,9 @@ mod tests {
"initial should be clamped down to max when it exceeds it"
);

// --- initial below minimumclamped to 1 ---
// --- initial at zeroallowed (surrogates disabled when max is also 0) ---
let (initial, max) = compute_surrogate_counts(Some(0), None);
assert_eq!(initial, 1, "initial should be clamped to minimum of 1");
assert_eq!(initial, 0, "initial of 0 should be allowed");
assert_eq!(
max, HARD_MAX_SURROGATE_PROCESSES,
"max should default when unset"
Expand All @@ -909,10 +922,10 @@ mod tests {
"initial should be clamped down to max when it defaults above it"
);

// --- max below minimumclamped to 1, initial follows ---
// --- max at zeroallowed (surrogates disabled), initial follows ---
let (initial, max) = compute_surrogate_counts(None, Some(0));
assert_eq!(max, 1, "max should be clamped to minimum of 1");
assert_eq!(initial, 1, "initial should be clamped down to max");
assert_eq!(max, 0, "max of 0 should be allowed");
assert_eq!(initial, 0, "initial should be clamped down to max");

// --- max above hard limit → clamped to 512 ---
let (initial, max) = compute_surrogate_counts(None, Some(9999));
Expand Down Expand Up @@ -947,12 +960,12 @@ mod tests {
// gracefully adapts: it only asserts the invariant initial <= max <= 512.
let (initial, max) = surrogate_process_counts();
assert!(
(1..=HARD_MAX_SURROGATE_PROCESSES).contains(&initial),
"initial {initial} should be in 1..={HARD_MAX_SURROGATE_PROCESSES}"
(0..=HARD_MAX_SURROGATE_PROCESSES).contains(&initial),
"initial {initial} should be in 0..={HARD_MAX_SURROGATE_PROCESSES}"
);
assert!(
(1..=HARD_MAX_SURROGATE_PROCESSES).contains(&max),
"max {max} should be in 1..={HARD_MAX_SURROGATE_PROCESSES}"
(0..=HARD_MAX_SURROGATE_PROCESSES).contains(&max),
"max {max} should be in 0..={HARD_MAX_SURROGATE_PROCESSES}"
);
assert!(initial <= max, "initial ({initial}) must be <= max ({max})");
}
Expand Down
141 changes: 90 additions & 51 deletions src/hyperlight_host/src/hypervisor/virtual_machine/whp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ use crate::hypervisor::regs::{
WHP_SREGS_NAMES_LEN,
};
use crate::hypervisor::surrogate_process::SurrogateProcess;
use crate::hypervisor::surrogate_process_manager::get_surrogate_process_manager;
use crate::hypervisor::surrogate_process_manager::{
get_surrogate_process_manager, surrogates_disabled,
};
#[cfg(feature = "hw-interrupts")]
use crate::hypervisor::virtual_machine::x86_64::hw_interrupts::TimerThread;
use crate::hypervisor::virtual_machine::{
Expand Down Expand Up @@ -91,8 +93,10 @@ fn release_file_mapping(view_base: *mut c_void, mapping_handle: HandleWrapper) {
#[derive(Debug)]
pub(crate) struct WhpVm {
partition: WHV_PARTITION_HANDLE,
// Surrogate process for memory mapping
surrogate_process: SurrogateProcess,
// Surrogate process for memory mapping. `None` when surrogates are
// disabled (`HYPERLIGHT_MAX_SURROGATES=0`), in which case
// `WHvMapGpaRange` is used instead of `WHvMapGpaRange2`.
surrogate_process: Option<SurrogateProcess>,
/// Tracks host-side file mappings (view_base, mapping_handle) for
/// cleanup on unmap or drop. Only populated for MappedFile regions.
file_mappings: Vec<(HandleWrapper, *mut c_void)>,
Expand All @@ -101,11 +105,13 @@ pub(crate) struct WhpVm {
timer: Option<TimerThread>,
}

// Safety: `WhpVm` is !Send because it holds `SurrogateProcess` which contains a raw pointer
// `allocated_address` (*mut c_void). This pointer represents a memory mapped view address
// in the surrogate process. It is never dereferenced, only used for address arithmetic and
// resource management (unmapping). This is a system resource that is not bound to the creating
// thread and can be safely transferred between threads.
// Safety: `WhpVm` is !Send because it holds `Option<SurrogateProcess>` which
// contains a raw pointer `allocated_address` (*mut c_void). This pointer
// represents a memory mapped view address in the surrogate process. It is
// never dereferenced, only used for address arithmetic and resource management
// (unmapping). This is a system resource that is not bound to the creating
// thread and can be safely transferred between threads. When the `Option` is
// `None` (surrogates disabled), no such pointer exists.
// `file_mappings` contains raw pointers that are also kernel resource handles,
// safe to use from any thread.
unsafe impl Send for WhpVm {}
Expand Down Expand Up @@ -143,11 +149,16 @@ impl WhpVm {
p
};

let mgr = get_surrogate_process_manager()
.map_err(|e| CreateVmError::SurrogateProcess(e.to_string()))?;
let surrogate_process = mgr
.get_surrogate_process()
.map_err(|e| CreateVmError::SurrogateProcess(e.to_string()))?;
let surrogate_process = if surrogates_disabled() {
None
} else {
let mgr = get_surrogate_process_manager()
.map_err(|e| CreateVmError::SurrogateProcess(e.to_string()))?;
Some(
mgr.get_surrogate_process()
.map_err(|e| CreateVmError::SurrogateProcess(e.to_string()))?,
)
};

Ok(WhpVm {
partition,
Expand Down Expand Up @@ -183,18 +194,6 @@ impl VirtualMachine for WhpVm {
&mut self,
(_slot, region): (u32, &MemoryRegion),
) -> Result<(), MapMemoryError> {
// Calculate the surrogate process address for this region
let surrogate_base = self
.surrogate_process
.map(
region.host_region.start.from_handle,
region.host_region.start.handle_base,
region.host_region.start.handle_size,
&region.region_type.surrogate_mapping(),
)
.map_err(|e| MapMemoryError::SurrogateProcess(e.to_string()))?;
let surrogate_addr = surrogate_base.wrapping_add(region.host_region.start.offset);

let flags = region
.flags
.iter()
Expand All @@ -212,32 +211,71 @@ impl VirtualMachine for WhpVm {
.iter()
.fold(WHvMapGpaRangeFlagNone, |acc, flag| acc | *flag);

let whvmapgparange2_func = unsafe {
match try_load_whv_map_gpa_range2() {
Ok(func) => func,
Err(e) => {
return Err(MapMemoryError::LoadApi {
api_name: "WHvMapGpaRange2",
source: e,
});
match &mut self.surrogate_process {
None => {
let host_addr = (region.host_region.start.handle_base
+ region.host_region.start.offset)
as *const c_void;
let res = unsafe {
WHvMapGpaRange(
self.partition,
host_addr,
region.guest_region.start as u64,
region.guest_region.len() as u64,
flags,
)
};
if let Err(e) = res {
return Err(MapMemoryError::Hypervisor(HypervisorError::WindowsError(e)));
}
}
Some(surrogate) => {
// Calculate the surrogate process address for this region
let surrogate_base = surrogate
.map(
region.host_region.start.from_handle,
region.host_region.start.handle_base,
region.host_region.start.handle_size,
&region.region_type.surrogate_mapping(),
)
.map_err(|e| MapMemoryError::SurrogateProcess(e.to_string()))?;
let surrogate_addr = surrogate_base.wrapping_add(region.host_region.start.offset);

// This function dynamically loads the WHvMapGpaRange2 function from the winhvplatform.dll
// WHvMapGpaRange2 only available on Windows 11 or Windows Server 2022 and later
// we do things this way to allow a user trying to load hyperlight on an older version of windows to
// get an error message saying that hyperlight requires a newer version of windows, rather than just failing
// with an error about a missing entrypoint
// This function should always succeed since before we get here we have already checked that the hypervisor is present and
// that we are on a supported version of windows.
let whvmapgparange2_func = unsafe {
match try_load_whv_map_gpa_range2() {
Ok(func) => func,
Err(e) => {
return Err(MapMemoryError::LoadApi {
api_name: "WHvMapGpaRange2",
source: e,
});
}
}
};

let res = unsafe {
whvmapgparange2_func(
self.partition,
surrogate.process_handle.into(),
surrogate_addr,
region.guest_region.start as u64,
region.guest_region.len() as u64,
flags,
)
};
if res.is_err() {
return Err(MapMemoryError::Hypervisor(HypervisorError::WindowsError(
windows_result::Error::from_hresult(res),
)));
}
}
};

let res = unsafe {
whvmapgparange2_func(
self.partition,
self.surrogate_process.process_handle.into(),
surrogate_addr,
region.guest_region.start as u64,
region.guest_region.len() as u64,
flags,
)
};
if res.is_err() {
return Err(MapMemoryError::Hypervisor(HypervisorError::WindowsError(
windows_result::Error::from_hresult(res),
)));
}

// Track host-side file mappings for cleanup on unmap or drop.
Expand All @@ -263,8 +301,9 @@ impl VirtualMachine for WhpVm {
)
.map_err(|e| UnmapMemoryError::Hypervisor(HypervisorError::WindowsError(e)))?;
}
self.surrogate_process
.unmap(region.host_region.start.handle_base);
if let Some(surrogate) = &mut self.surrogate_process {
surrogate.unmap(region.host_region.start.handle_base);
}

// Clean up host-side file mapping resources for MappedFile regions.
if region.region_type == MemoryRegionType::MappedFile {
Expand Down
Loading
Loading