Skip to main content

hyperlight_js/sandbox/
js_sandbox.rs

1use std::collections::HashMap;
2use std::fmt::Debug;
3
4use hyperlight_host::sandbox::snapshot::Snapshot;
5use hyperlight_host::{new_error, MultiUseSandbox, Result};
6use tracing::{instrument, Level};
7
8use super::loaded_js_sandbox::LoadedJSSandbox;
9use crate::sandbox::metrics::SandboxMetricsGuard;
10use crate::Script;
11
12/// A Hyperlight Sandbox with a JavaScript run time loaded but no guest code.
13pub struct JSSandbox {
14    pub(super) inner: MultiUseSandbox,
15    handlers: HashMap<String, Script>,
16    // Snapshot of state before any handlers are added.
17    // This is used to restore state back to a neutral JSSandbox.
18    snapshot: Snapshot,
19    // metric drop guard to manage sandbox metric
20    _metric_guard: SandboxMetricsGuard<JSSandbox>,
21}
22
23impl JSSandbox {
24    #[instrument(err(Debug), skip(inner), level=Level::INFO)]
25    pub(super) fn new(mut inner: MultiUseSandbox) -> Result<Self> {
26        let snapshot = inner.snapshot()?;
27        Ok(Self {
28            inner,
29            handlers: HashMap::new(),
30            snapshot,
31            _metric_guard: SandboxMetricsGuard::new(),
32        })
33    }
34
35    /// Creates a new `JSSandbox` from a `MultiUseSandbox` and a `Snapshot` of state before any handlers were added.
36    pub(crate) fn from_loaded(mut loaded: MultiUseSandbox, snapshot: Snapshot) -> Result<Self> {
37        loaded.restore(&snapshot)?;
38        Ok(Self {
39            inner: loaded,
40            handlers: HashMap::new(),
41            snapshot,
42            _metric_guard: SandboxMetricsGuard::new(),
43        })
44    }
45
46    /// Adds a new handler function to the sandboxes collection of handlers. This Handler will be
47    /// available to the host to call once `get_loaded_sandbox` is called.
48    #[instrument(err(Debug), skip(self, script), level=Level::DEBUG)]
49    pub fn add_handler<F>(&mut self, function_name: F, script: Script) -> Result<()>
50    where
51        F: Into<String> + std::fmt::Debug,
52    {
53        let function_name = function_name.into();
54        if function_name.is_empty() {
55            return Err(new_error!("Handler name must not be empty"));
56        }
57        if self.handlers.contains_key(&function_name) {
58            return Err(new_error!(
59                "Handler already exists for function name: {}",
60                function_name
61            ));
62        }
63
64        self.handlers.insert(function_name, script);
65        Ok(())
66    }
67
68    /// Removes a handler function from the sandboxes collection of handlers.
69    #[instrument(err(Debug), skip(self), level=Level::DEBUG)]
70    pub fn remove_handler(&mut self, function_name: &str) -> Result<()> {
71        if function_name.is_empty() {
72            return Err(new_error!("Handler name must not be empty"));
73        }
74        match self.handlers.remove(function_name) {
75            Some(_) => Ok(()),
76            None => Err(new_error!(
77                "Handler does not exist for function name: {}",
78                function_name
79            )),
80        }
81    }
82
83    /// Clears all handlers from the sandbox.
84    #[instrument(skip_all, level=Level::TRACE)]
85    pub fn clear_handlers(&mut self) {
86        self.handlers.clear();
87    }
88
89    /// Returns whether the sandbox is currently poisoned.
90    ///
91    /// A poisoned sandbox is in an inconsistent state due to the guest not running to completion.
92    /// This can happen when guest execution is interrupted (e.g., via `InterruptHandle::kill()`),
93    /// when the guest panics, or when memory violations occur.
94    ///
95    pub fn poisoned(&self) -> bool {
96        self.inner.poisoned()
97    }
98
99    #[cfg(test)]
100    fn get_number_of_handlers(&self) -> usize {
101        self.handlers.len()
102    }
103
104    /// Creates a new `LoadedJSSandbox` with the handlers that have been added to this `JSSandbox`.
105    #[instrument(err(Debug), skip_all, level=Level::TRACE)]
106    pub fn get_loaded_sandbox(mut self) -> Result<LoadedJSSandbox> {
107        if self.handlers.is_empty() {
108            return Err(new_error!("No handlers have been added to the sandbox"));
109        }
110
111        let handlers = self.handlers.clone();
112        for (function_name, script) in handlers {
113            let content = script.content().to_owned();
114
115            let path = script
116                .base_path()
117                .map(|p| p.to_string_lossy().to_string())
118                .unwrap_or_default();
119            self.inner
120                .call::<()>("register_handler", (function_name, content, path))?;
121        }
122
123        LoadedJSSandbox::new(self.inner, self.snapshot)
124    }
125    /// Generate a crash dump of the current state of the VM underlying this sandbox.
126    ///
127    /// Creates an ELF core dump file that can be used for debugging. The dump
128    /// captures the current state of the sandbox including registers, memory regions,
129    /// and other execution context.
130    ///
131    /// The location of the core dump file is determined by the `HYPERLIGHT_CORE_DUMP_DIR`
132    /// environment variable. If not set, it defaults to the system's temporary directory.
133    ///
134    /// This is only available when the `crashdump` feature is enabled and then only if the sandbox
135    /// is also configured to allow core dumps (which is the default behavior).
136    ///
137    /// This can be useful for generating a crash dump from gdb when trying to debug issues in the
138    /// guest that dont cause crashes (e.g. a guest function that does not return)
139    ///
140    /// # Examples
141    ///
142    /// Attach to your running process with gdb and call this function:
143    ///
144    /// ```shell
145    /// sudo gdb -p <pid_of_your_process>
146    /// (gdb) info threads
147    /// # find the thread that is running the guest function you want to debug
148    /// (gdb) thread <thread_number>
149    /// # switch to the frame where you have access to your MultiUseSandbox instance
150    /// (gdb) backtrace
151    /// (gdb) frame <frame_number>
152    /// # get the pointer to your MultiUseSandbox instance
153    /// # Get the sandbox pointer
154    /// (gdb) print sandbox
155    /// # Call the crashdump function
156    /// call sandbox.generate_crashdump()
157    /// ```
158    /// The crashdump should be available in crash dump directory (see `HYPERLIGHT_CORE_DUMP_DIR` env var).
159    ///
160    #[cfg(feature = "crashdump")]
161    pub fn generate_crashdump(&self) -> Result<()> {
162        self.inner.generate_crashdump()
163    }
164}
165
166impl Debug for JSSandbox {
167    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168        f.debug_struct("JSSandbox")
169            .field("handlers", &self.handlers)
170            .finish()
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::SandboxBuilder;
178
179    #[test]
180    fn test_add_handler() {
181        let proto_js_sandbox = SandboxBuilder::new().build().unwrap();
182        let mut sandbox = proto_js_sandbox.load_runtime().unwrap();
183        sandbox.add_handler("handler1", "script1".into()).unwrap();
184        sandbox.add_handler("handler2", "script2".into()).unwrap();
185
186        assert_eq!(sandbox.get_number_of_handlers(), 2);
187    }
188
189    #[test]
190    fn test_remove_handler() {
191        let proto_js_sandbox = SandboxBuilder::new().build().unwrap();
192        let mut sandbox = proto_js_sandbox.load_runtime().unwrap();
193        sandbox.add_handler("handler1", "script1".into()).unwrap();
194        sandbox.add_handler("handler2", "script2".into()).unwrap();
195
196        sandbox.remove_handler("handler1").unwrap();
197
198        assert_eq!(sandbox.get_number_of_handlers(), 1);
199    }
200
201    #[test]
202    fn test_clear_handlers() {
203        let proto_js_sandbox = SandboxBuilder::new().build().unwrap();
204        let mut sandbox = proto_js_sandbox.load_runtime().unwrap();
205        sandbox.add_handler("handler1", "script1".into()).unwrap();
206        sandbox.add_handler("handler2", "script2".into()).unwrap();
207
208        sandbox.clear_handlers();
209
210        assert_eq!(sandbox.get_number_of_handlers(), 0);
211    }
212
213    #[test]
214    fn test_get_loaded_sandbox() {
215        let proto_js_sandbox = SandboxBuilder::new().build().unwrap();
216        let mut sandbox = proto_js_sandbox.load_runtime().unwrap();
217        sandbox
218            .add_handler(
219                "handler1",
220                Script::from_content(
221                    r#"function handler(event) {
222                    event.request.uri = "/redirected.html";
223                    return event
224                }"#,
225                ),
226            )
227            .unwrap();
228
229        let res = sandbox.get_loaded_sandbox();
230        assert!(res.is_ok());
231    }
232}