Skip to content

Commit d8524a9

Browse files
authored
Add timeout support to pyhl::Runtime with cancellable host sleep (#73)
* feat: add run_code_with_timeout to pyhl::Runtime Signed-off-by: danbugs <danilochiarlone@gmail.com> * fix: make __hl_sleep cancellable via condvar std::thread::sleep() restarts on EINTR, so interrupt_handle.kill() could never wake a sleeping guest — the host function blocked for the full duration (up to 60s cap). Replace with a Condvar-based sleep that can be woken immediately by SleepCancel::cancel(). Signed-off-by: danbugs <danilochiarlone@gmail.com> * test: assert time.sleep kill latency under 10s Signed-off-by: danbugs <danilochiarlone@gmail.com> * chore: bump version to 0.6.0 Signed-off-by: danbugs <danilochiarlone@gmail.com> * style: cargo fmt Signed-off-by: danbugs <danilochiarlone@gmail.com> * fix: address copilot review feedback - Fix doc comment on SleepCancel (no is_cancelled method exists) - Use wait_timeout_while to handle spurious condvar wakeups - Reset sleep_cancel unconditionally after timer joins to handle race where timer fires as guest call completes Signed-off-by: danbugs <danilochiarlone@gmail.com> * chore: update Cargo.lock for 0.6.0 Signed-off-by: danbugs <danilochiarlone@gmail.com> --------- Signed-off-by: danbugs <danilochiarlone@gmail.com>
1 parent ca05c5a commit d8524a9

5 files changed

Lines changed: 277 additions & 17 deletions

File tree

host/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

host/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "hyperlight-unikraft-host"
3-
version = "0.5.0"
3+
version = "0.6.0"
44
edition = "2021"
55
description = "Embedded Hyperlight host for running Unikraft unikernels"
66
license = "MIT OR Apache-2.0"

host/src/lib.rs

Lines changed: 133 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ use std::collections::{HashMap, HashSet};
6868
use std::net::IpAddr;
6969
use std::path::Path;
7070
use std::sync::atomic::{AtomicI32, Ordering};
71-
use std::sync::Arc;
71+
use std::sync::{Arc, Condvar, Mutex};
7272
use 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).
109109
const 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.
112148
const MAX_DIR_ENTRIES: usize = 100_000;
113149

@@ -957,6 +993,7 @@ fn build_tools(
957993
fn 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

9831021
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
9841022
use std::net::SocketAddr;
985-
use std::sync::Mutex;
9861023

9871024
struct 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

Comments
 (0)