@@ -19,6 +19,7 @@ use std::fs::File;
1919use std:: io:: Write ;
2020use std:: mem:: size_of;
2121use std:: path:: { Path , PathBuf } ;
22+ use std:: sync:: OnceLock ;
2223use std:: sync:: atomic:: { AtomicUsize , Ordering } ;
2324
2425use 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).
9397fn 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.
109113fn 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" ) ]
358377fn 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 minimum → clamped to 1 ---
907+ // --- initial at zero → allowed (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 minimum → clamped to 1 , initial follows ---
931+ // --- max at zero → allowed (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