@@ -68,7 +68,7 @@ use std::collections::{HashMap, HashSet};
6868use std:: net:: IpAddr ;
6969use std:: path:: Path ;
7070use std:: sync:: atomic:: { AtomicI32 , Ordering } ;
71- use std:: sync:: Arc ;
71+ use std:: sync:: { Arc , Condvar , Mutex } ;
7272use std:: time:: Duration ;
7373
7474/// Magic header for cmdline embedded in initrd: "HLCMDLN\0"
@@ -108,6 +108,42 @@ const MAX_DISPATCH_PAYLOAD: usize = 64 * 1024 * 1024;
108108/// Cap for `__hl_sleep` duration to prevent unbounded host-thread blocking (60 s).
109109const MAX_SLEEP_NS : u64 = 60_000_000_000 ;
110110
111+ /// Shared cancellation primitive for `__hl_sleep`. Calling
112+ /// [`SleepCancel::cancel`] wakes up any in-progress sleep immediately so
113+ /// the host function returns and the hypervisor execution loop can detect
114+ /// the pending cancellation.
115+ #[ derive( Clone ) ]
116+ pub struct SleepCancel ( Arc < ( Mutex < bool > , Condvar ) > ) ;
117+
118+ impl SleepCancel {
119+ fn new ( ) -> Self {
120+ Self ( Arc :: new ( ( Mutex :: new ( false ) , Condvar :: new ( ) ) ) )
121+ }
122+
123+ /// Wake any in-progress `__hl_sleep` immediately.
124+ pub fn cancel ( & self ) {
125+ let ( lock, cvar) = & * self . 0 ;
126+ * lock. lock ( ) . unwrap ( ) = true ;
127+ cvar. notify_all ( ) ;
128+ }
129+
130+ /// Reset so the next guest call can sleep normally.
131+ pub fn reset ( & self ) {
132+ * self . 0 . 0 . lock ( ) . unwrap ( ) = false ;
133+ }
134+
135+ fn wait ( & self , dur : Duration ) {
136+ let ( lock, cvar) = & * self . 0 ;
137+ let guard = lock. lock ( ) . unwrap ( ) ;
138+ if * guard {
139+ return ;
140+ }
141+ // wait_timeout_while handles spurious wakeups by re-checking the
142+ // predicate; we only return early when actually cancelled.
143+ let _ = cvar. wait_timeout_while ( guard, dur, |cancelled| !* cancelled) ;
144+ }
145+ }
146+
111147/// Cap for `fs_list` directory entries to prevent host OOM on huge directories.
112148const MAX_DIR_ENTRIES : usize = 100_000 ;
113149
@@ -957,6 +993,7 @@ fn build_tools(
957993fn register_internal_tools (
958994 tools : & mut ToolRegistry ,
959995 exit_code : & Arc < AtomicI32 > ,
996+ sleep_cancel : & SleepCancel ,
960997 network : Option < & NetworkPolicy > ,
961998 listen_ports : Option < & ListenPorts > ,
962999) -> Option < Arc < Mutex < SocketTable > > > {
@@ -966,10 +1003,11 @@ fn register_internal_tools(
9661003 ec. store ( code, Ordering :: Relaxed ) ;
9671004 Ok ( serde_json:: json!( { } ) )
9681005 } ) ;
969- tools. register ( "__hl_sleep" , |args| {
1006+ let sc = sleep_cancel. clone ( ) ;
1007+ tools. register ( "__hl_sleep" , move |args| {
9701008 let ns = args[ "ns" ] . as_u64 ( ) . unwrap_or ( 0 ) . min ( MAX_SLEEP_NS ) ;
9711009 if ns > 0 {
972- std :: thread :: sleep ( std :: time :: Duration :: from_nanos ( ns) ) ;
1010+ sc . wait ( Duration :: from_nanos ( ns) ) ;
9731011 }
9741012 Ok ( serde_json:: json!( { } ) )
9751013 } ) ;
@@ -982,7 +1020,6 @@ fn register_internal_tools(
9821020
9831021use socket2:: { Domain , Protocol , SockAddr , Socket , Type } ;
9841022use std:: net:: SocketAddr ;
985- use std:: sync:: Mutex ;
9861023
9871024struct HostSocket {
9881025 socket : Socket ,
@@ -1940,6 +1977,8 @@ pub struct Sandbox {
19401977 /// Shared socket table — cleared on [`Sandbox::restore`] so that
19411978 /// host-side fds don't leak across guest restore cycles.
19421979 socket_table : Option < Arc < Mutex < SocketTable > > > ,
1980+ /// Cancellation token for in-progress `__hl_sleep` host calls.
1981+ sleep_cancel : SleepCancel ,
19431982}
19441983
19451984/// Where the initrd comes from — either a file (zero-copy `map_file_cow`)
@@ -2148,15 +2187,17 @@ impl Sandbox {
21482187 let mut usbox = UninitializedSandbox :: new ( env, Some ( config. sandbox_config ( ) ) ) ?;
21492188
21502189 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
2190+ let sleep_cancel = SleepCancel :: new ( ) ;
21512191 let mut tools = build_tools ( tools, preopens) ?. unwrap_or_default ( ) ;
2152- let socket_table = register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
2192+ let socket_table =
2193+ register_internal_tools ( & mut tools, & exit_code, & sleep_cancel, network, listen_ports) ;
21532194 let tools = Arc :: new ( tools) ;
21542195 let tools_ref = tools. clone ( ) ;
21552196 usbox. register_host_function ( "__dispatch" , move |payload : Vec < u8 > | -> Vec < u8 > {
21562197 tools_ref. dispatch ( & payload)
21572198 } ) ?;
21582199
2159- Self :: finish_evolve ( usbox, None , 0 , exit_code, socket_table)
2200+ Self :: finish_evolve ( usbox, None , 0 , exit_code, sleep_cancel , socket_table)
21602201 }
21612202
21622203 /// Low-level: boot with a zero-copy mapped initrd file. Prefer the builder.
@@ -2200,8 +2241,10 @@ impl Sandbox {
22002241 }
22012242
22022243 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
2244+ let sleep_cancel = SleepCancel :: new ( ) ;
22032245 let mut tools = build_tools ( tools, preopens) ?. unwrap_or_default ( ) ;
2204- let socket_table = register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
2246+ let socket_table =
2247+ register_internal_tools ( & mut tools, & exit_code, & sleep_cancel, network, listen_ports) ;
22052248 let tools = Arc :: new ( tools) ;
22062249 let tools_ref = tools. clone ( ) ;
22072250 usbox. register_host_function ( "__dispatch" , move |payload : Vec < u8 > | -> Vec < u8 > {
@@ -2213,6 +2256,7 @@ impl Sandbox {
22132256 initrd_path. map ( |p| p. to_path_buf ( ) ) ,
22142257 INITRD_MAP_BASE ,
22152258 exit_code,
2259+ sleep_cancel,
22162260 socket_table,
22172261 )
22182262 }
@@ -2222,6 +2266,7 @@ impl Sandbox {
22222266 file_mapping_path : Option < std:: path:: PathBuf > ,
22232267 file_mapping_base : u64 ,
22242268 exit_code : Arc < AtomicI32 > ,
2269+ sleep_cancel : SleepCancel ,
22252270 socket_table : Option < Arc < Mutex < SocketTable > > > ,
22262271 ) -> Result < Self > {
22272272 let mut inner = usbox. evolve ( ) ?;
@@ -2233,6 +2278,7 @@ impl Sandbox {
22332278 file_mapping_base,
22342279 exit_code,
22352280 socket_table,
2281+ sleep_cancel,
22362282 } )
22372283 }
22382284
@@ -2299,6 +2345,18 @@ impl Sandbox {
22992345 self . exit_code . store ( 0 , Ordering :: Relaxed ) ;
23002346 }
23012347
2348+ /// Obtain a handle that can interrupt a running guest call from
2349+ /// another thread. See [`hyperlight_host::hypervisor::InterruptHandle`].
2350+ pub fn interrupt_handle ( & self ) -> Arc < dyn hyperlight_host:: hypervisor:: InterruptHandle > {
2351+ self . inner . interrupt_handle ( )
2352+ }
2353+
2354+ /// Obtain the sleep-cancellation token. Call `.cancel()` on it to
2355+ /// wake any in-progress `__hl_sleep` immediately.
2356+ pub fn sleep_cancel ( & self ) -> SleepCancel {
2357+ self . sleep_cancel . clone ( )
2358+ }
2359+
23022360 /// Take a new snapshot of the current guest state.
23032361 ///
23042362 /// Useful for the "snapshot after one-time warm-up" pattern: call
@@ -2412,8 +2470,10 @@ impl Sandbox {
24122470 let arc = Arc :: new ( loaded) ;
24132471
24142472 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
2473+ let sleep_cancel = SleepCancel :: new ( ) ;
24152474 let mut tools = build_tools ( None , preopens) ?. unwrap_or_default ( ) ;
2416- let socket_table = register_internal_tools ( & mut tools, & exit_code, network, listen_ports) ;
2475+ let socket_table =
2476+ register_internal_tools ( & mut tools, & exit_code, & sleep_cancel, network, listen_ports) ;
24172477 let tools = Arc :: new ( tools) ;
24182478 let tools_ref = tools. clone ( ) ;
24192479
@@ -2436,6 +2496,7 @@ impl Sandbox {
24362496 file_mapping_base : INITRD_MAP_BASE ,
24372497 exit_code,
24382498 socket_table,
2499+ sleep_cancel,
24392500 } )
24402501 }
24412502}
@@ -3159,10 +3220,12 @@ mod tests {
31593220 fn net_tools_registered_with_blocklist ( ) {
31603221 let mut tools = ToolRegistry :: new ( ) ;
31613222 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
3223+ let sc = SleepCancel :: new ( ) ;
31623224 let bl = BlockList :: from_hosts ( & [ "1.2.3.4" ] ) . unwrap ( ) ;
31633225 register_internal_tools (
31643226 & mut tools,
31653227 & exit_code,
3228+ & sc,
31663229 Some ( & NetworkPolicy :: BlockList ( bl) ) ,
31673230 None ,
31683231 ) ;
@@ -3176,7 +3239,8 @@ mod tests {
31763239 fn net_tools_not_registered_without_policy ( ) {
31773240 let mut tools = ToolRegistry :: new ( ) ;
31783241 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
3179- register_internal_tools ( & mut tools, & exit_code, None , None ) ;
3242+ let sc = SleepCancel :: new ( ) ;
3243+ register_internal_tools ( & mut tools, & exit_code, & sc, None , None ) ;
31803244 let req = br#"{"name":"net_socket","args":{"family":2,"type":1}}"# ;
31813245 let resp = tools. dispatch ( req) ;
31823246 let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
@@ -3187,7 +3251,14 @@ mod tests {
31873251 fn net_tools_registered_with_allow_all ( ) {
31883252 let mut tools = ToolRegistry :: new ( ) ;
31893253 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
3190- register_internal_tools ( & mut tools, & exit_code, Some ( & NetworkPolicy :: AllowAll ) , None ) ;
3254+ let sc = SleepCancel :: new ( ) ;
3255+ register_internal_tools (
3256+ & mut tools,
3257+ & exit_code,
3258+ & sc,
3259+ Some ( & NetworkPolicy :: AllowAll ) ,
3260+ None ,
3261+ ) ;
31913262 let req = br#"{"name":"net_socket","args":{"family":2,"type":1}}"# ;
31923263 let resp = tools. dispatch ( req) ;
31933264 let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
@@ -3213,7 +3284,14 @@ mod tests {
32133284 fn net_bind_denied_without_listen_ports ( ) {
32143285 let mut tools = ToolRegistry :: new ( ) ;
32153286 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
3216- register_internal_tools ( & mut tools, & exit_code, Some ( & NetworkPolicy :: AllowAll ) , None ) ;
3287+ let sc = SleepCancel :: new ( ) ;
3288+ register_internal_tools (
3289+ & mut tools,
3290+ & exit_code,
3291+ & sc,
3292+ Some ( & NetworkPolicy :: AllowAll ) ,
3293+ None ,
3294+ ) ;
32173295 // Create a socket first
32183296 let req = br#"{"name":"net_socket","args":{"family":2,"type":1}}"# ;
32193297 let resp = tools. dispatch ( req) ;
@@ -3235,10 +3313,12 @@ mod tests {
32353313 fn net_bind_allowed_with_matching_port ( ) {
32363314 let mut tools = ToolRegistry :: new ( ) ;
32373315 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
3316+ let sc = SleepCancel :: new ( ) ;
32383317 let lp = ListenPorts :: from_ports ( [ 8080 ] ) ;
32393318 register_internal_tools (
32403319 & mut tools,
32413320 & exit_code,
3321+ & sc,
32423322 Some ( & NetworkPolicy :: AllowAll ) ,
32433323 Some ( & lp) ,
32443324 ) ;
@@ -3259,10 +3339,12 @@ mod tests {
32593339 fn net_bind_denied_with_wrong_port ( ) {
32603340 let mut tools = ToolRegistry :: new ( ) ;
32613341 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
3342+ let sc = SleepCancel :: new ( ) ;
32623343 let lp = ListenPorts :: from_ports ( [ 8080 ] ) ;
32633344 register_internal_tools (
32643345 & mut tools,
32653346 & exit_code,
3347+ & sc,
32663348 Some ( & NetworkPolicy :: AllowAll ) ,
32673349 Some ( & lp) ,
32683350 ) ;
@@ -3302,14 +3384,44 @@ mod tests {
33023384
33033385 let mut tools = ToolRegistry :: new ( ) ;
33043386 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
3305- register_internal_tools ( & mut tools, & exit_code, None , None ) ;
3387+ let sc = SleepCancel :: new ( ) ;
3388+ register_internal_tools ( & mut tools, & exit_code, & sc, None , None ) ;
33063389
33073390 let req = br#"{"name":"__hl_sleep","args":{"ns":0}}"# ;
33083391 let resp = tools. dispatch ( req) ;
33093392 let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
33103393 assert ! ( !s. contains( "\" error\" " ) , "sleep(0) should succeed: {s}" ) ;
33113394 }
33123395
3396+ #[ test]
3397+ fn test_sleep_cancel_wakes_immediately ( ) {
3398+ let mut tools = ToolRegistry :: new ( ) ;
3399+ let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
3400+ let sc = SleepCancel :: new ( ) ;
3401+ register_internal_tools ( & mut tools, & exit_code, & sc, None , None ) ;
3402+
3403+ let sc2 = sc. clone ( ) ;
3404+ let handle = std:: thread:: spawn ( move || {
3405+ std:: thread:: sleep ( Duration :: from_millis ( 200 ) ) ;
3406+ sc2. cancel ( ) ;
3407+ } ) ;
3408+
3409+ let start = std:: time:: Instant :: now ( ) ;
3410+ let req = br#"{"name":"__hl_sleep","args":{"ns":60000000000}}"# ;
3411+ let resp = tools. dispatch ( req) ;
3412+ let elapsed = start. elapsed ( ) ;
3413+ handle. join ( ) . unwrap ( ) ;
3414+ sc. reset ( ) ;
3415+
3416+ let s = std:: str:: from_utf8 ( & resp) . unwrap ( ) ;
3417+ assert ! ( !s. contains( "\" error\" " ) , "sleep should succeed: {s}" ) ;
3418+ assert ! (
3419+ elapsed. as_secs( ) < 5 ,
3420+ "cancelled sleep should wake promptly, took {:.1}s" ,
3421+ elapsed. as_secs_f64( )
3422+ ) ;
3423+ }
3424+
33133425 #[ test]
33143426 fn net_getsockopt_returns_correct_type_for_dgram ( ) {
33153427 let mut reg = ToolRegistry :: new ( ) ;
@@ -3467,9 +3579,15 @@ mod tests {
34673579 fn net_socket_has_default_timeout ( ) {
34683580 let mut tools = ToolRegistry :: new ( ) ;
34693581 let exit_code = Arc :: new ( AtomicI32 :: new ( 0 ) ) ;
3470- let table =
3471- register_internal_tools ( & mut tools, & exit_code, Some ( & NetworkPolicy :: AllowAll ) , None )
3472- . expect ( "network tools should be registered" ) ;
3582+ let sc = SleepCancel :: new ( ) ;
3583+ let table = register_internal_tools (
3584+ & mut tools,
3585+ & exit_code,
3586+ & sc,
3587+ Some ( & NetworkPolicy :: AllowAll ) ,
3588+ None ,
3589+ )
3590+ . expect ( "network tools should be registered" ) ;
34733591
34743592 let req = br#"{"name":"net_socket","args":{"family":2,"type":1}}"# ;
34753593 let resp = tools. dispatch ( req) ;
0 commit comments