Skip to content
Merged
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
32 changes: 32 additions & 0 deletions .github/workflows/dep_build_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,38 @@ jobs:
# with only one driver enabled (kvm/mshv3 features are unix-only, no-op on Windows)
just test ${{ inputs.config }} ${{ inputs.hypervisor == 'mshv3' && 'mshv3' || 'kvm' }}

- name: Run no-surrogate VM tests (WHP only)
Comment thread
danbugs marked this conversation as resolved.
if: runner.os == 'Windows'
env:
HYPERLIGHT_MAX_SURROGATES: "0"
HYPERLIGHT_INITIAL_SURROGATES: "0"
run: |
PROFILE=${{ inputs.config == 'debug' && 'dev' || inputs.config }}
# Verify each expected test actually ran (guards against silent
# renames where cargo test exits 0 with 0 matches).
# NOTE: keep in sync with `just test-no-surrogate`.
assert_ran() {
local output="$1"; shift
for name in "$@"; do
if ! echo "$output" | grep -q "$name \.\.\. ok"; then
echo "::error::Expected test '$name' did not run — may have been renamed"
return 1
fi
done
}
OUT=$(cargo test -p hyperlight-host --profile=$PROFILE --lib -- no_surrogate_tests --test-threads=1 2>&1) || { echo "$OUT"; exit 1; }
echo "$OUT"
assert_ran "$OUT" single_vm_lifecycle
OUT=$(cargo test -p hyperlight-host --profile=$PROFILE --test integration_test -- guest_malloc guest_panic corrupt_output_size_prefix_rejected --test-threads=1 2>&1) || { echo "$OUT"; exit 1; }
echo "$OUT"
assert_ran "$OUT" guest_malloc guest_panic corrupt_output_size_prefix_rejected
OUT=$(cargo test -p hyperlight-host --profile=$PROFILE --test sandbox_host_tests -- --exact callback_test float_roundtrip --test-threads=1 2>&1) || { echo "$OUT"; exit 1; }
echo "$OUT"
assert_ran "$OUT" callback_test float_roundtrip
OUT=$(cargo test -p hyperlight-host --profile=$PROFILE --lib -- snapshot_evolve_restore_handles_state_correctly restore_from_loaded_snapshot --test-threads=1 2>&1) || { echo "$OUT"; exit 1; }
echo "$OUT"
assert_ran "$OUT" snapshot_evolve_restore_handles_state_correctly restore_from_loaded_snapshot

- name: Run Rust tests with hw-interrupts
run: |
# with hw-interrupts feature enabled (+ explicit driver on Linux)
Expand Down
14 changes: 14 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ test-like-ci config=default-target hypervisor="kvm":
@# with hw-interrupts enabled (+ explicit driver on Linux)
{{ if os() == "linux" { if hypervisor == "mshv3" { "just test " + config + " mshv3,hw-interrupts" } else { "just test " + config + " kvm,hw-interrupts" } } else { "just test " + config + " hw-interrupts" } }}

@# no-surrogate mode smoke tests (Windows/WHP only)
{{ if os() == "windows" { "just test-no-surrogate " + config } else { "" } }}

@# make sure certain cargo features compile
just check

Expand Down Expand Up @@ -262,6 +265,17 @@ test-compilation-no-default-features target=default-target:
{{ if os() == "linux" { cargo-cmd + " check -p hyperlight-host --no-default-features --features kvm" } else { "" } }} {{ target-triple-flag }}
{{ if os() == "linux" { cargo-cmd + " check -p hyperlight-host --no-default-features --features mshv3" } else { "" } }} {{ target-triple-flag }}

# runs a subset of existing tests with HYPERLIGHT_MAX_SURROGATES=0 (Windows only).
# Covers: guest calls, host callbacks, in-memory snapshot/restore, and
# save/load snapshot from disk.
# NOTE: if any of the test names below are renamed, update both this
# recipe AND the matching CI step in .github/workflows/dep_build_test.yml.
test-no-surrogate target=default-target:
{{ set-env-command }}HYPERLIGHT_MAX_SURROGATES=0; {{ set-env-command }}HYPERLIGHT_INITIAL_SURROGATES=0; {{ cargo-cmd }} test -p hyperlight-host --profile={{ if target == "debug" { "dev" } else { target } }} --lib -- no_surrogate_tests --test-threads=1
{{ set-env-command }}HYPERLIGHT_MAX_SURROGATES=0; {{ set-env-command }}HYPERLIGHT_INITIAL_SURROGATES=0; {{ cargo-cmd }} test -p hyperlight-host --profile={{ if target == "debug" { "dev" } else { target } }} --test integration_test -- guest_malloc guest_panic corrupt_output_size_prefix_rejected --test-threads=1
{{ set-env-command }}HYPERLIGHT_MAX_SURROGATES=0; {{ set-env-command }}HYPERLIGHT_INITIAL_SURROGATES=0; {{ cargo-cmd }} test -p hyperlight-host --profile={{ if target == "debug" { "dev" } else { target } }} --test sandbox_host_tests -- --exact callback_test float_roundtrip --test-threads=1
{{ set-env-command }}HYPERLIGHT_MAX_SURROGATES=0; {{ set-env-command }}HYPERLIGHT_INITIAL_SURROGATES=0; {{ cargo-cmd }} test -p hyperlight-host --profile={{ if target == "debug" { "dev" } else { target } }} --lib -- snapshot_evolve_restore_handles_state_correctly restore_from_loaded_snapshot --test-threads=1

# runs tests that exercise gdb debugging
test-rust-gdb-debugging target=default-target features="":
{{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example guest-debugging {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }}
Expand Down
51 changes: 35 additions & 16 deletions src/hyperlight_host/src/hypervisor/surrogate_process_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ use std::fs::File;
use std::io::Write;
use std::mem::size_of;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::sync::atomic::{AtomicUsize, Ordering};

use crossbeam_channel::{Receiver, Sender, TryRecvError, unbounded};
Expand Down Expand Up @@ -86,26 +87,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 +357,21 @@ 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`.
///
/// The result is cached on first call — the env var is read only once.
pub(crate) fn surrogates_disabled() -> bool {
Comment thread
danbugs marked this conversation as resolved.
static DISABLED: OnceLock<bool> = OnceLock::new();
*DISABLED.get_or_init(|| {
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 +904,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 +928,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 +966,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
Loading
Loading