Skip to content

Commit 427a949

Browse files
committed
feat(whp): support no-surrogate mode via HYPERLIGHT_MAX_SURROGATES=0
When HYPERLIGHT_MAX_SURROGATES=0, bypass the surrogate process manager and use VirtualAlloc + WHvMapGpaRange directly (single-VM-per-process). - Add DirectAllocation RAII guard with VirtualAlloc/VirtualFree and tracing on drop errors matching existing patterns - Add surrogates_disabled() helper (cached via OnceLock) - Guard WhpVm::new() with AtomicBool to reject a second concurrent VM with a clear error instead of a cryptic HRESULT; flag resets on Drop so sequential single-VM usage works - Refactor shared_mem to share validation and guard-page setup across allocation paths - Add targeted CI step for Windows that runs the no-surrogate test Signed-off-by: danbugs <danilochiarlone@gmail.com>
1 parent f5806e7 commit 427a949

4 files changed

Lines changed: 316 additions & 128 deletions

File tree

.github/workflows/dep_build_test.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ jobs:
118118
# with only one driver enabled (kvm/mshv3 features are unix-only, no-op on Windows)
119119
just test ${{ inputs.config }} ${{ inputs.hypervisor == 'mshv3' && 'mshv3' || 'kvm' }}
120120
121+
- name: Run no-surrogate VM tests (WHP only)
122+
if: runner.os == 'Windows'
123+
env:
124+
HYPERLIGHT_MAX_SURROGATES: "0"
125+
HYPERLIGHT_INITIAL_SURROGATES: "0"
126+
run: |
127+
cargo test -p hyperlight-host --${{ inputs.config }} --lib no_surrogate_tests -- --test-threads=1
128+
121129
- name: Run Rust tests with hw-interrupts
122130
run: |
123131
# with hw-interrupts feature enabled (+ explicit driver on Linux)

src/hyperlight_host/src/hypervisor/surrogate_process_manager.rs

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use std::fs::File;
1919
use std::io::Write;
2020
use std::mem::size_of;
2121
use std::path::{Path, PathBuf};
22+
use std::sync::OnceLock;
2223
use std::sync::atomic::{AtomicUsize, Ordering};
2324

2425
use crossbeam_channel::{Receiver, Sender, TryRecvError, unbounded};
@@ -86,26 +87,29 @@ fn surrogate_binary_name() -> Result<String> {
8687
/// (or `None` when the variable is unset or unparsable).
8788
///
8889
/// Resolution order:
89-
/// 1. `max` is clamped to `1..=HARD_MAX_SURROGATE_PROCESSES`, defaulting
90+
/// 1. `max` is clamped to `0..=HARD_MAX_SURROGATE_PROCESSES`, defaulting
9091
/// to `HARD_MAX_SURROGATE_PROCESSES` when `None`.
91-
/// 2. `initial` is clamped to `1..=max`, defaulting to `max` when `None`.
92+
/// 2. `initial` is clamped to `0..=max`, defaulting to `max` when `None`.
9293
/// This guarantees `initial <= max` without an extra conditional.
94+
///
95+
/// When `max == 0`, surrogates are disabled entirely and the system
96+
/// falls back to `WHvMapGpaRange` (single-VM-per-process mode).
9397
fn compute_surrogate_counts(raw_initial: Option<usize>, raw_max: Option<usize>) -> (usize, usize) {
9498
let max = raw_max
95-
.map(|n| n.clamp(1, HARD_MAX_SURROGATE_PROCESSES))
99+
.map(|n| n.clamp(0, HARD_MAX_SURROGATE_PROCESSES))
96100
.unwrap_or(HARD_MAX_SURROGATE_PROCESSES);
97101

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

101105
(initial, max)
102106
}
103107

104108
/// Returns the (initial, max) surrogate process counts from environment
105109
/// variables, applying validation and clamping.
106110
///
107-
/// - `HYPERLIGHT_INITIAL_SURROGATES`: clamped to `1..=max`, default `max`.
108-
/// - `HYPERLIGHT_MAX_SURROGATES`: clamped to `1..=512`, default 512.
111+
/// - `HYPERLIGHT_INITIAL_SURROGATES`: clamped to `0..=max`, default `max`.
112+
/// - `HYPERLIGHT_MAX_SURROGATES`: clamped to `0..=512`, default 512.
109113
fn surrogate_process_counts() -> (usize, usize) {
110114
let raw_initial = std::env::var(INITIAL_SURROGATES_ENV_VAR)
111115
.ok()
@@ -353,6 +357,21 @@ pub(crate) fn get_surrogate_process_manager() -> Result<&'static SurrogateProces
353357
}
354358
}
355359

360+
/// Returns `true` when `HYPERLIGHT_MAX_SURROGATES=0`, meaning surrogate
361+
/// processes are disabled and the system should use `WHvMapGpaRange`
362+
/// (single-VM-per-process mode) instead of `WHvMapGpaRange2`.
363+
///
364+
/// The result is cached on first call — the env var is read only once.
365+
pub(crate) fn surrogates_disabled() -> bool {
366+
static DISABLED: OnceLock<bool> = OnceLock::new();
367+
*DISABLED.get_or_init(|| {
368+
std::env::var(MAX_SURROGATES_ENV_VAR)
369+
.ok()
370+
.and_then(|v| v.parse::<usize>().ok())
371+
.is_some_and(|n| n == 0)
372+
})
373+
}
374+
356375
// Creates a job object that will terminate all the surrogate processes when the struct instance is dropped.
357376
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
358377
fn create_job_object() -> Result<HandleWrapper> {
@@ -885,9 +904,9 @@ mod tests {
885904
"initial should be clamped down to max when it exceeds it"
886905
);
887906

888-
// --- initial below minimumclamped to 1 ---
907+
// --- initial at zeroallowed (surrogates disabled when max is also 0) ---
889908
let (initial, max) = compute_surrogate_counts(Some(0), None);
890-
assert_eq!(initial, 1, "initial should be clamped to minimum of 1");
909+
assert_eq!(initial, 0, "initial of 0 should be allowed");
891910
assert_eq!(
892911
max, HARD_MAX_SURROGATE_PROCESSES,
893912
"max should default when unset"
@@ -909,10 +928,10 @@ mod tests {
909928
"initial should be clamped down to max when it defaults above it"
910929
);
911930

912-
// --- max below minimumclamped to 1, initial follows ---
931+
// --- max at zeroallowed (surrogates disabled), initial follows ---
913932
let (initial, max) = compute_surrogate_counts(None, Some(0));
914-
assert_eq!(max, 1, "max should be clamped to minimum of 1");
915-
assert_eq!(initial, 1, "initial should be clamped down to max");
933+
assert_eq!(max, 0, "max of 0 should be allowed");
934+
assert_eq!(initial, 0, "initial should be clamped down to max");
916935

917936
// --- max above hard limit → clamped to 512 ---
918937
let (initial, max) = compute_surrogate_counts(None, Some(9999));
@@ -947,12 +966,12 @@ mod tests {
947966
// gracefully adapts: it only asserts the invariant initial <= max <= 512.
948967
let (initial, max) = surrogate_process_counts();
949968
assert!(
950-
(1..=HARD_MAX_SURROGATE_PROCESSES).contains(&initial),
951-
"initial {initial} should be in 1..={HARD_MAX_SURROGATE_PROCESSES}"
969+
(0..=HARD_MAX_SURROGATE_PROCESSES).contains(&initial),
970+
"initial {initial} should be in 0..={HARD_MAX_SURROGATE_PROCESSES}"
952971
);
953972
assert!(
954-
(1..=HARD_MAX_SURROGATE_PROCESSES).contains(&max),
955-
"max {max} should be in 1..={HARD_MAX_SURROGATE_PROCESSES}"
973+
(0..=HARD_MAX_SURROGATE_PROCESSES).contains(&max),
974+
"max {max} should be in 0..={HARD_MAX_SURROGATE_PROCESSES}"
956975
);
957976
assert!(initial <= max, "initial ({initial}) must be <= max ({max})");
958977
}

0 commit comments

Comments
 (0)