Skip to main content

hyperlight_js/sandbox/
loaded_js_sandbox.rs

1use std::fmt::Debug;
2use std::sync::Arc;
3
4use hyperlight_host::hypervisor::InterruptHandle;
5use hyperlight_host::sandbox::snapshot::Snapshot;
6use hyperlight_host::HyperlightError::{self, JsonConversionFailure};
7use hyperlight_host::{MultiUseSandbox, Result};
8use tokio::task::JoinHandle;
9use tracing::{instrument, Level};
10
11use super::js_sandbox::JSSandbox;
12use super::metrics::{METRIC_SANDBOX_LOADS, METRIC_SANDBOX_UNLOADS};
13use super::monitor::runtime::get_monitor_runtime;
14use super::monitor::MonitorSet;
15#[cfg(feature = "function_call_metrics")]
16use crate::sandbox::metrics::EventHandlerMetricGuard;
17use crate::sandbox::metrics::SandboxMetricsGuard;
18
19/// A Hyperlight Sandbox with a JavaScript run time loaded and guest JavaScript handlers loaded.
20pub struct LoadedJSSandbox {
21    inner: MultiUseSandbox,
22    // Snapshot of state before the sandbox was loaded and before any handlers were added.
23    // This is used to restore state back to a JSSandbox.
24    snapshot: Snapshot,
25    // metric drop guard to manage sandbox metric
26    _metric_guard: SandboxMetricsGuard<LoadedJSSandbox>,
27}
28
29/// RAII guard that aborts a spawned monitor task on drop.
30///
31/// Wraps a tokio `JoinHandle` to ensure the monitor task is cancelled when
32/// the guard goes out of scope — whether that's after normal completion or
33/// on early return. Keeps the spawn-abort lifecycle in one place rather than
34/// requiring manual `abort()` calls at each exit point.
35struct MonitorTask(JoinHandle<()>);
36
37impl Drop for MonitorTask {
38    fn drop(&mut self) {
39        self.0.abort();
40    }
41}
42
43impl LoadedJSSandbox {
44    #[instrument(err(Debug), skip_all, level=Level::INFO)]
45    pub(super) fn new(inner: MultiUseSandbox, snapshot: Snapshot) -> Result<LoadedJSSandbox> {
46        metrics::counter!(METRIC_SANDBOX_LOADS).increment(1);
47        Ok(LoadedJSSandbox {
48            inner,
49            snapshot,
50            _metric_guard: SandboxMetricsGuard::new(),
51        })
52    }
53
54    /// Handles an event by calling the specified function with the event data.
55    #[instrument(err(Debug), skip(self, event, gc), level=Level::INFO)]
56    pub fn handle_event<F>(
57        &mut self,
58        func_name: F,
59        event: String,
60        gc: Option<bool>,
61    ) -> Result<String>
62    where
63        F: Into<String> + std::fmt::Debug,
64    {
65        // check that this string is a valid JSON
66
67        let _json_val: serde_json::Value =
68            serde_json::from_str(&event).map_err(JsonConversionFailure)?;
69
70        let should_gc = gc.unwrap_or(true);
71        let func_name = func_name.into();
72        if func_name.is_empty() {
73            return Err(HyperlightError::Error(
74                "Handler name must not be empty".to_string(),
75            ));
76        }
77
78        #[cfg(feature = "function_call_metrics")]
79        let _metric_guard = EventHandlerMetricGuard::new(&func_name, should_gc);
80
81        self.inner.call(&func_name, (event, should_gc))
82    }
83
84    /// Unloads the Handlers from the sandbox and returns a `JSSandbox` with the JavaScript runtime loaded.
85    #[instrument(err(Debug), skip_all, level=Level::DEBUG)]
86    pub fn unload(self) -> Result<JSSandbox> {
87        JSSandbox::from_loaded(self.inner, self.snapshot).inspect(|_| {
88            metrics::counter!(METRIC_SANDBOX_UNLOADS).increment(1);
89        })
90    }
91
92    /// Take a snapshot of the the current state of the sandbox.
93    /// This can be used to restore the state of the sandbox later.
94    #[instrument(err(Debug), skip_all, level=Level::DEBUG)]
95    pub fn snapshot(&mut self) -> Result<Snapshot> {
96        self.inner.snapshot()
97    }
98
99    /// Restore the state of the sandbox to a previous snapshot.
100    #[instrument(err(Debug), skip_all, level=Level::DEBUG)]
101    pub fn restore(&mut self, snapshot: &Snapshot) -> Result<()> {
102        self.inner.restore(snapshot)?;
103        Ok(())
104    }
105
106    /// Get a handle to the interrupt handler for this sandbox,
107    /// capable of interrupting guest execution.
108    pub fn interrupt_handle(&self) -> Arc<dyn InterruptHandle> {
109        self.inner.interrupt_handle()
110    }
111
112    /// Returns whether the sandbox is currently poisoned.
113    ///
114    /// A poisoned sandbox is in an inconsistent state due to the guest not running to completion.
115    /// This can happen when guest execution is interrupted (e.g., via `InterruptHandle::kill()`),
116    /// when the guest panics, or when memory violations occur.
117    ///
118    /// When poisoned, most operations will fail with `PoisonedSandbox` error.
119    /// Use `restore()` with a snapshot or `unload()` to recover from a poisoned state.
120    pub fn poisoned(&self) -> bool {
121        self.inner.poisoned()
122    }
123
124    /// Handles an event with execution monitoring.
125    ///
126    /// The monitor enforces execution limits (time, CPU usage, etc.) and will
127    /// terminate execution if limits are exceeded. If terminated, the sandbox
128    /// will be poisoned and an error is returned.
129    ///
130    /// # Fail-Closed Semantics
131    ///
132    /// If the monitor fails to initialize, the handler is **never executed**.
133    /// Execution cannot proceed unmonitored.
134    ///
135    /// # Tuple Monitors (OR semantics)
136    ///
137    /// Pass a tuple of monitors to enforce multiple limits. The first monitor
138    /// to fire terminates execution, and the winning monitor's name is logged:
139    ///
140    /// ```text
141    /// let monitor = (
142    ///     WallClockMonitor::new(Duration::from_secs(5))?,
143    ///     CpuTimeMonitor::new(Duration::from_millis(500))?,
144    /// );
145    /// loaded.handle_event_with_monitor("handler", "{}".into(), &monitor, None)?;
146    /// ```
147    ///
148    /// # Arguments
149    ///
150    /// * `func_name` - The name of the handler function to call.
151    /// * `event` - JSON string payload to pass to the handler.
152    /// * `monitor` - The execution monitor (or tuple of monitors) to enforce limits.
153    ///   Tuples race all sub-monitors; the first to fire wins and its name is logged.
154    /// * `gc` - Whether to run garbage collection after the call (defaults to `true` if `None`).
155    ///
156    /// # Returns
157    ///
158    /// The handler result string on success, or an error if execution failed
159    /// or was terminated by the monitor. If terminated, the sandbox will be
160    /// poisoned and subsequent calls will fail until restored or unloaded.
161    ///
162    /// # Example
163    ///
164    /// ```text
165    /// use hyperlight_js::WallClockMonitor;
166    /// use std::time::Duration;
167    ///
168    /// let monitor = WallClockMonitor::new(Duration::from_secs(5))?;
169    /// let result = loaded.handle_event_with_monitor(
170    ///     "handler",
171    ///     "{}".to_string(),
172    ///     &monitor,
173    ///     None,
174    /// )?;
175    /// println!("Handler returned: {}", result);
176    /// ```
177    #[instrument(err(Debug), skip(self, event, monitor, gc), level=Level::INFO)]
178    pub fn handle_event_with_monitor<F, M>(
179        &mut self,
180        func_name: F,
181        event: String,
182        monitor: &M,
183        gc: Option<bool>,
184    ) -> Result<String>
185    where
186        F: Into<String> + std::fmt::Debug,
187        M: MonitorSet,
188    {
189        let func_name = func_name.into();
190        if func_name.is_empty() {
191            return Err(HyperlightError::Error(
192                "Handler name must not be empty".to_string(),
193            ));
194        }
195        let interrupt_handle = self.interrupt_handle();
196
197        // Phase 1: Build the racing future on the calling thread.
198        // to_race() calls each sub-monitor's get_monitor() here, where
199        // monitors can capture thread-local state (e.g., CPU clock handles).
200        // If any monitor fails to initialize, we fail closed — handler never runs.
201        let racing_future = monitor.to_race().map_err(|e| {
202            tracing::error!("Failed to initialize execution monitor: {}", e);
203            HyperlightError::Error(format!("Execution monitor failed to start: {}", e))
204        })?;
205
206        // Phase 2: Spawn the racing future on the shared runtime.
207        // When the first monitor fires, to_race() emits the metric and log,
208        // then we call kill() to terminate the guest.
209        // kill() is safe to call even if the guest already finished — hyperlight's
210        // InterruptHandle checks RUNNING_BIT and clear_cancel() at the start of
211        // the next guest call clears any stale CANCEL_BIT.
212        let runtime = get_monitor_runtime().ok_or_else(|| {
213            tracing::error!("Monitor runtime is unavailable");
214            HyperlightError::Error("Monitor runtime is unavailable".to_string())
215        })?;
216
217        let _monitor_task = MonitorTask(runtime.spawn(async move {
218            racing_future.await;
219            interrupt_handle.kill();
220        }));
221
222        // Phase 3: Execute the handler (blocking). When this returns (success
223        // or error), _monitor_task drops and aborts the spawned monitor task.
224        self.handle_event(&func_name, event, gc)
225    }
226
227    /// Generate a crash dump of the current state of the VM underlying this sandbox.
228    ///
229    /// Creates an ELF core dump file that can be used for debugging. The dump
230    /// captures the current state of the sandbox including registers, memory regions,
231    /// and other execution context.
232    ///
233    /// The location of the core dump file is determined by the `HYPERLIGHT_CORE_DUMP_DIR`
234    /// environment variable. If not set, it defaults to the system's temporary directory.
235    ///
236    /// This is only available when the `crashdump` feature is enabled and then only if the sandbox
237    /// is also configured to allow core dumps (which is the default behavior).
238    ///
239    /// This can be useful for generating a crash dump from gdb when trying to debug issues in the
240    /// guest that dont cause crashes (e.g. a guest function that does not return)
241    ///
242    /// # Examples
243    ///
244    /// Attach to your running process with gdb and call this function:
245    ///
246    /// ```shell
247    /// sudo gdb -p <pid_of_your_process>
248    /// (gdb) info threads
249    /// # find the thread that is running the guest function you want to debug
250    /// (gdb) thread <thread_number>
251    /// # switch to the frame where you have access to your MultiUseSandbox instance
252    /// (gdb) backtrace
253    /// (gdb) frame <frame_number>
254    /// # get the pointer to your MultiUseSandbox instance
255    /// # Get the sandbox pointer
256    /// (gdb) print sandbox
257    /// # Call the crashdump function
258    /// call sandbox.generate_crashdump()
259    /// ```
260    /// The crashdump should be available in crash dump directory (see `HYPERLIGHT_CORE_DUMP_DIR` env var).
261    ///
262    #[cfg(feature = "crashdump")]
263    pub fn generate_crashdump(&self) -> Result<()> {
264        self.inner.generate_crashdump()
265    }
266}
267
268impl Debug for LoadedJSSandbox {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        f.debug_struct("LoadedJSSandbox").finish()
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277    use crate::{SandboxBuilder, Script};
278
279    fn get_valid_handler() -> Script {
280        Script::from_content(
281            r#"
282        function handler(event) {
283            event.request.uri = "/redirected.html";
284            return event
285        }
286        "#,
287        )
288    }
289
290    fn get_valid_event() -> String {
291        r#"
292        {
293            "request": {
294                "uri": "/index.html"
295            }
296        }
297        "#
298        .to_string()
299    }
300
301    fn get_static_counter_handler() -> Script {
302        Script::from_content(
303            r#"
304        let count = 0;
305        function handler(event) {
306            event.count = ++count;
307            return event
308        }
309        "#,
310        )
311    }
312
313    fn get_static_counter_event() -> String {
314        r#"
315        {
316            "count": 0
317        }
318        "#
319        .to_string()
320    }
321
322    fn get_loaded_sandbox() -> Result<LoadedJSSandbox> {
323        let proto_js_sandbox = SandboxBuilder::new().build().unwrap();
324        let mut sandbox = proto_js_sandbox.load_runtime().unwrap();
325
326        sandbox.add_handler("handler", get_valid_handler()).unwrap();
327
328        sandbox.get_loaded_sandbox()
329    }
330
331    #[test]
332    fn test_handle_event() {
333        let proto_js_sandbox = SandboxBuilder::new().build().unwrap();
334        let mut sandbox = proto_js_sandbox.load_runtime().unwrap();
335
336        sandbox.add_handler("handler", get_valid_handler()).unwrap();
337
338        let mut loaded_js_sandbox = sandbox.get_loaded_sandbox().unwrap();
339        let gc = Some(true);
340        let result = loaded_js_sandbox.handle_event("handler".to_string(), get_valid_event(), gc);
341
342        assert!(result.is_ok());
343    }
344
345    #[test]
346    fn test_handle_event_accumulates_state() {
347        let proto_js_sandbox = SandboxBuilder::new().build().unwrap();
348        let mut sandbox = proto_js_sandbox.load_runtime().unwrap();
349        sandbox
350            .add_handler("handler", get_static_counter_handler())
351            .unwrap();
352
353        let mut loaded_js_sandbox = sandbox.get_loaded_sandbox().unwrap();
354        let gc = Some(true);
355        let result = loaded_js_sandbox.handle_event("handler", get_static_counter_event(), gc);
356
357        assert!(result.is_ok());
358        let response = result.unwrap();
359        let response_json: serde_json::Value = serde_json::from_str(&response).unwrap();
360        assert_eq!(response_json["count"], 1);
361
362        let result = loaded_js_sandbox.handle_event("handler", get_static_counter_event(), gc);
363        assert!(result.is_ok());
364        let response = result.unwrap();
365        let response_json: serde_json::Value = serde_json::from_str(&response).unwrap();
366        assert_eq!(response_json["count"], 2);
367    }
368
369    #[test]
370    fn test_snapshot_and_restore() {
371        let proto_js_sandbox = SandboxBuilder::new().build().unwrap();
372        let mut sandbox = proto_js_sandbox.load_runtime().unwrap();
373
374        sandbox
375            .add_handler("handler", get_static_counter_handler())
376            .unwrap();
377
378        let mut loaded_js_sandbox = sandbox.get_loaded_sandbox().unwrap();
379        let gc = Some(true);
380
381        let result = loaded_js_sandbox
382            .handle_event("handler", get_static_counter_event(), gc)
383            .unwrap();
384
385        let response_json: serde_json::Value = serde_json::from_str(&result).unwrap();
386        assert_eq!(response_json["count"], 1);
387
388        // Take a snapshot after handling 1 event
389        let snapshot = loaded_js_sandbox.snapshot().unwrap();
390
391        // Handle 2 more events
392        let result = loaded_js_sandbox
393            .handle_event("handler", get_static_counter_event(), gc)
394            .unwrap();
395        let response_json: serde_json::Value = serde_json::from_str(&result).unwrap();
396        assert_eq!(response_json["count"], 2);
397
398        let result = loaded_js_sandbox
399            .handle_event("handler", get_static_counter_event(), gc)
400            .unwrap();
401        let response_json: serde_json::Value = serde_json::from_str(&result).unwrap();
402        assert_eq!(response_json["count"], 3);
403
404        // Restore the snapshot
405        loaded_js_sandbox.restore(&snapshot).unwrap();
406
407        // Handle the event again, should reset to initial state
408        let result = loaded_js_sandbox
409            .handle_event("handler", get_static_counter_event(), gc)
410            .unwrap();
411        let response_json: serde_json::Value = serde_json::from_str(&result).unwrap();
412        assert_eq!(response_json["count"], 2);
413
414        // unload and reload, and restore
415        let mut js_sandbox = loaded_js_sandbox.unload().unwrap();
416
417        js_sandbox
418            .add_handler("handler2", get_static_counter_handler())
419            .unwrap();
420
421        let mut reloaded_js_sandbox = js_sandbox.get_loaded_sandbox().unwrap();
422
423        // handler2 should be available, not handler
424        let result = reloaded_js_sandbox
425            .handle_event("handler2", get_static_counter_event(), gc)
426            .unwrap();
427        let response_json: serde_json::Value = serde_json::from_str(&result).unwrap();
428        assert_eq!(response_json["count"], 1);
429
430        reloaded_js_sandbox
431            .handle_event("handler", get_static_counter_event(), gc)
432            .unwrap_err();
433
434        // restore to snapshot before unload/reload
435        reloaded_js_sandbox.restore(&snapshot).unwrap();
436        // handler should be available again
437        let result = reloaded_js_sandbox
438            .handle_event("handler", get_static_counter_event(), gc)
439            .unwrap();
440        let response_json: serde_json::Value = serde_json::from_str(&result).unwrap();
441        assert_eq!(response_json["count"], 2);
442
443        // but handler2 should not be available
444        reloaded_js_sandbox
445            .handle_event("handler2", get_static_counter_event(), gc)
446            .unwrap_err();
447    }
448
449    #[test]
450    fn test_add_handler_unload_and_reuse_resets_state() {
451        let proto_js_sandbox = SandboxBuilder::new().build().unwrap();
452        let mut sandbox = proto_js_sandbox.load_runtime().unwrap();
453        sandbox
454            .add_handler("handler", get_static_counter_handler())
455            .unwrap();
456        let mut loaded_js_sandbox = sandbox.get_loaded_sandbox().unwrap();
457        let gc = Some(true);
458
459        let result = loaded_js_sandbox.handle_event("handler", get_static_counter_event(), gc);
460        assert!(result.is_ok());
461        let response = result.unwrap();
462        let response_json: serde_json::Value = serde_json::from_str(&response).unwrap();
463        assert_eq!(response_json["count"], 1);
464
465        let result = loaded_js_sandbox.handle_event("handler", get_static_counter_event(), gc);
466        assert!(result.is_ok());
467        let response = result.unwrap();
468        let response_json: serde_json::Value = serde_json::from_str(&response).unwrap();
469        assert_eq!(response_json["count"], 2);
470
471        // Unload the sandbox
472        let mut sandbox = loaded_js_sandbox.unload().unwrap();
473        sandbox
474            .add_handler("handler", get_static_counter_handler())
475            .unwrap();
476        // Add the handler again
477        let mut loaded_js_sandbox = sandbox.get_loaded_sandbox().unwrap();
478        let gc = Some(true);
479
480        let result = loaded_js_sandbox.handle_event("handler", get_static_counter_event(), gc);
481        assert!(result.is_ok());
482        let response = result.unwrap();
483        let response_json: serde_json::Value = serde_json::from_str(&response).unwrap();
484        assert_eq!(response_json["count"], 1);
485
486        let result = loaded_js_sandbox.handle_event("handler", get_static_counter_event(), gc);
487        assert!(result.is_ok());
488        let response = result.unwrap();
489        let response_json: serde_json::Value = serde_json::from_str(&response).unwrap();
490        assert_eq!(response_json["count"], 2);
491    }
492
493    #[test]
494    fn test_unload() {
495        let sandbox = get_loaded_sandbox().unwrap();
496
497        let result = sandbox.unload();
498
499        assert!(result.is_ok());
500    }
501
502    use crate::sandbox::monitor::ExecutionMonitor;
503
504    /// A mock monitor that always fails to initialize (returns Err).
505    /// Used to test fail-closed behavior.
506    struct FailingMonitor;
507
508    impl ExecutionMonitor for FailingMonitor {
509        fn get_monitor(
510            &self,
511        ) -> hyperlight_host::Result<impl std::future::Future<Output = ()> + Send + 'static>
512        {
513            Err::<std::future::Ready<()>, _>(hyperlight_host::HyperlightError::Error(
514                "Simulated initialization failure".to_string(),
515            ))
516        }
517
518        fn name(&self) -> &'static str {
519            "failing-monitor"
520        }
521    }
522
523    #[test]
524    fn test_handle_event_with_monitor_fails_if_monitor_cannot_start() {
525        let mut loaded = get_loaded_sandbox().unwrap();
526        let monitor = FailingMonitor;
527
528        // Should fail because monitor returns Err (fail closed, not open)
529        let result = loaded.handle_event_with_monitor("handler", get_valid_event(), &monitor, None);
530
531        assert!(result.is_err(), "Should fail when monitor can't start");
532        let err = result.unwrap_err();
533        assert!(
534            err.to_string().contains("failed to start"),
535            "Error should mention monitor failure: {}",
536            err
537        );
538
539        // Sandbox should NOT be poisoned - we never ran the handler
540        assert!(
541            !loaded.poisoned(),
542            "Sandbox should not be poisoned when monitor fails to start"
543        );
544    }
545}