Skip to main content

hyperlight_wasm/sandbox/
loaded_wasm_sandbox.rs

1/*
2Copyright 2024 The Hyperlight Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17use std::fmt::Debug;
18use std::sync::Arc;
19
20use hyperlight_host::func::{ParameterTuple, SupportedReturnType};
21use hyperlight_host::hypervisor::InterruptHandle;
22use hyperlight_host::sandbox::snapshot::Snapshot;
23use hyperlight_host::sandbox::{Callable, SandboxStatus};
24use hyperlight_host::{MultiUseSandbox, Result, log_then_return, new_error};
25
26use super::metrics::METRIC_TOTAL_LOADED_WASM_SANDBOXES;
27use super::wasm_sandbox::WasmSandbox;
28use crate::sandbox::metrics::{METRIC_ACTIVE_LOADED_WASM_SANDBOXES, METRIC_SANDBOX_UNLOADS};
29
30/// A sandbox that has both a Wasm engine and an arbitrary Wasm module
31/// loaded into memory.
32///
33/// `LoadedWasmSandbox`es are ready to execute
34/// guest code and can execute a guest call, with `call_guest_function`,
35/// multiple times. Each call to `call_guest_function` executes in the same
36/// memory context. If you want to "reset" the memory context, create
37/// a new `LoadedWasmSandbox` -- either from another `WasmSandbox` or by
38/// calling `my_loaded_wasm_sandbox.devolve()?.evolve()?`
39pub struct LoadedWasmSandbox {
40    // inner is an Option<MultiUseSandbox> as we need to take ownership of it
41    // We implement drop on the LoadedWasmSandbox to decrement the count of Sandboxes when it is dropped
42    // because of this we cannot implement drop without making inner an Option (alternatively we could make MultiUseSandbox Copy but that would introduce other issues)
43    inner: Option<MultiUseSandbox>,
44    // The state the sandbox was in before loading a wasm module. Used for transitioning back to a `WasmSandbox` (unloading the wasm module).
45    runtime_snapshot: Option<Arc<Snapshot>>,
46}
47
48impl LoadedWasmSandbox {
49    /// Call the function in the guest with the name `fn_name`, passing
50    /// parameters `params`.
51    ///
52    /// On success, return an `Ok` with the return
53    /// value and a new copy of `Self` suitable for further use. On failure,
54    /// return an appropriate `Err`.
55    ///
56    /// # Errors
57    ///
58    /// Returns `Err(HyperlightError::PoisonedSandbox)` if the sandbox is in a
59    /// poisoned state. Use [`restore()`](Self::restore) to recover a poisoned
60    /// sandbox before calling this method again.
61    ///
62    /// Note: A sandbox becomes poisoned when a *previous* call fails due to
63    /// abnormal guest execution. That call returns the original error (e.g.,
64    /// `ExecutionCanceledByHost` from `interrupt_handle().kill()`, or errors
65    /// from guest panics, memory violations, etc.), and the sandbox is marked
66    /// as poisoned. This method then returns `PoisonedSandbox` on subsequent
67    /// calls until the sandbox is recovered.
68    pub fn call_guest_function<Output: SupportedReturnType>(
69        &mut self,
70        fn_name: &str,
71        params: impl ParameterTuple,
72    ) -> Result<Output> {
73        match &mut self.inner {
74            Some(inner) => inner.call(fn_name, params),
75            None => log_then_return!("No inner MultiUseSandbox to call"),
76        }
77    }
78
79    /// Take a snapshot of the current state of the sandbox.
80    ///
81    /// The snapshot can later be used with [`restore()`](Self::restore) to
82    /// return the sandbox to this state.
83    ///
84    /// # Errors
85    ///
86    /// Returns `Err(HyperlightError::PoisonedSandbox)` if the sandbox is in a
87    /// poisoned state. Use [`restore()`](Self::restore) with a previously
88    /// taken snapshot to recover before taking a new snapshot.
89    pub fn snapshot(&mut self) -> Result<Arc<Snapshot>> {
90        match &mut self.inner {
91            Some(inner) => inner.snapshot(),
92            None => log_then_return!("No inner MultiUseSandbox to snapshot"),
93        }
94    }
95
96    /// Restore the state of the sandbox to the state captured in the given snapshot.
97    ///
98    /// This method clears the poisoned state if the sandbox was poisoned, making
99    /// it usable again for guest function calls.
100    ///
101    /// # Recovery from poisoned state
102    ///
103    /// If a sandbox becomes poisoned (e.g., after `interrupt_handle().kill()`),
104    /// calling `restore()` with a valid snapshot will:
105    /// 1. Clear the poisoned state
106    /// 2. Reset memory to the snapshot state
107    /// 3. Allow subsequent [`call_guest_function()`](Self::call_guest_function) calls to succeed
108    pub fn restore(&mut self, snapshot: Arc<Snapshot>) -> Result<()> {
109        match &mut self.inner {
110            Some(inner) => inner.restore(snapshot),
111            None => log_then_return!("No inner MultiUseSandbox to restore"),
112        }
113    }
114
115    /// Unload the wasm module and return a `WasmSandbox` that can be
116    /// used to load another module.
117    ///
118    /// This method defers calling [`restore()`](Self::restore) to
119    /// reset the sandbox to its pre-module state until a new module
120    /// is loaded. However, the sandbox will always be restored when a
121    /// new module is loaded, so a poisoned sandbox can be recovered
122    /// by unloading and reloading a module.
123    pub fn unload_module(mut self) -> Result<WasmSandbox> {
124        let sandbox = self
125            .inner
126            .take()
127            .ok_or_else(|| new_error!("No inner MultiUseSandbox to unload"))?;
128
129        let snapshot = self
130            .runtime_snapshot
131            .take()
132            .ok_or_else(|| new_error!("No snapshot of the WasmSandbox to unload"))?;
133
134        WasmSandbox::new_from_loaded(sandbox, snapshot).inspect(|_| {
135            metrics::counter!(METRIC_SANDBOX_UNLOADS).increment(1);
136        })
137    }
138
139    pub(super) fn new(
140        inner: MultiUseSandbox,
141        runtime_snapshot: Arc<Snapshot>,
142    ) -> Result<LoadedWasmSandbox> {
143        metrics::gauge!(METRIC_ACTIVE_LOADED_WASM_SANDBOXES).increment(1);
144        metrics::counter!(METRIC_TOTAL_LOADED_WASM_SANDBOXES).increment(1);
145        Ok(LoadedWasmSandbox {
146            inner: Some(inner),
147            runtime_snapshot: Some(runtime_snapshot),
148        })
149    }
150
151    /// Get a handle to the interrupt handler for this sandbox,
152    /// capable of interrupting guest execution.
153    pub fn interrupt_handle(&self) -> Result<Arc<dyn InterruptHandle>> {
154        if let Some(inner) = &self.inner {
155            Ok(inner.interrupt_handle())
156        } else {
157            Err(new_error!(
158                "WasmSandbox is None, cannot get interrupt handle"
159            ))
160        }
161    }
162
163    /// Get the current lifecycle state of the sandbox.
164    ///
165    /// # Errors
166    ///
167    /// Returns an error if the sandbox is in an invalid state.
168    pub fn status(&self) -> Result<SandboxStatus> {
169        match &self.inner {
170            Some(inner) => Ok(inner.status()),
171            None => log_then_return!("No inner MultiUseSandbox to check status"),
172        }
173    }
174
175    /// Check if the sandbox is in a poisoned state.
176    ///
177    /// A sandbox becomes poisoned when guest execution does not complete normally,
178    /// such as after:
179    /// - Forced termination via `interrupt_handle().kill()`
180    /// - Guest panic or abort
181    /// - Memory violation
182    /// - Stack or heap exhaustion
183    ///
184    /// Note: The call that causes poisoning returns the original error (e.g.,
185    /// `ExecutionCanceledByHost`), not `PoisonedSandbox`. The sandbox is marked
186    /// as poisoned after that error, and subsequent calls to
187    /// [`call_guest_function()`](Self::call_guest_function) will return
188    /// `Err(HyperlightError::PoisonedSandbox)`.
189    ///
190    /// A poisoned sandbox cannot execute guest functions until recovered via
191    /// [`restore()`](Self::restore). Calling [`unload_module()`](Self::unload_module)
192    /// will also recover a poisoned sandbox since it performs a restore internally.
193    ///
194    /// # Returns
195    /// - `Ok(true)` if the sandbox is poisoned and needs recovery
196    /// - `Ok(false)` if the sandbox is healthy and can execute guest functions
197    /// - `Err` if the sandbox is in an invalid state
198    #[deprecated(since = "0.15.0", note = "use status().is_poisoned() instead")]
199    pub fn is_poisoned(&self) -> Result<bool> {
200        Ok(self.status()?.is_poisoned())
201    }
202}
203
204impl Callable for LoadedWasmSandbox {
205    fn call<Output: SupportedReturnType>(
206        &mut self,
207        func_name: &str,
208        args: impl ParameterTuple,
209    ) -> Result<Output> {
210        self.call_guest_function(func_name, args)
211    }
212}
213
214impl Drop for LoadedWasmSandbox {
215    fn drop(&mut self) {
216        metrics::gauge!(METRIC_ACTIVE_LOADED_WASM_SANDBOXES).decrement(1);
217    }
218}
219
220impl Debug for LoadedWasmSandbox {
221    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222        f.debug_struct("LoadedWasmSandbox")
223            .field("inner", &self.inner)
224            .finish()
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use std::sync::Arc;
231    use std::thread;
232
233    use crossbeam_queue::ArrayQueue;
234    use examples_common::get_wasm_module_path;
235    use hyperlight_host::{HyperlightError, new_error};
236
237    use super::{LoadedWasmSandbox, WasmSandbox};
238    use crate::Result;
239    use crate::sandbox::proto_wasm_sandbox::ProtoWasmSandbox;
240    use crate::sandbox::sandbox_builder::SandboxBuilder;
241
242    fn get_time_since_boot_microsecond() -> Result<i64> {
243        let res = std::time::SystemTime::now()
244            .duration_since(std::time::SystemTime::UNIX_EPOCH)?
245            .as_micros();
246        i64::try_from(res).map_err(HyperlightError::IntConversionFailure)
247    }
248
249    // Ensure that we can use a sandbox multiple times to call guest functions and that we dont run out of memory or have any other issues
250
251    #[test]
252    fn test_call_guest_functions_with_default_config_multiple_times() {
253        let mut sandbox = ProtoWasmSandbox::default();
254
255        sandbox
256            .register(
257                "GetTimeSinceBootMicrosecond",
258                get_time_since_boot_microsecond,
259            )
260            .unwrap();
261
262        let wasm_sandbox = sandbox.load_runtime().unwrap();
263        let loaded_wasm_sandbox: LoadedWasmSandbox = {
264            let mod_path = get_wasm_module_path("RunWasm.aot").unwrap();
265            wasm_sandbox.load_module(mod_path)
266        }
267        .unwrap();
268
269        call_funcs(loaded_wasm_sandbox, 500);
270    }
271
272    #[test]
273    fn test_sandbox_use_on_different_threads() {
274        let wasm_sandbox_queue = Arc::new(ArrayQueue::<WasmSandbox>::new(10));
275        let loaded_wasm_sandbox_queue = Arc::new(ArrayQueue::<LoadedWasmSandbox>::new(10));
276
277        // Create a queue of WasmSandbox instances
278        for i in 0..10 {
279            println!("Creating WasmSandbox instance {}", i);
280            let mut sandbox = ProtoWasmSandbox::default();
281
282            sandbox
283                .register(
284                    "GetTimeSinceBootMicrosecond",
285                    get_time_since_boot_microsecond,
286                )
287                .unwrap();
288
289            let wasm_sandbox = sandbox.load_runtime().unwrap();
290            wasm_sandbox_queue.push(wasm_sandbox).unwrap();
291            println!("Pushed WasmSandbox instance {}", i);
292        }
293
294        // Get the WasmSandbox instances from the queue and load the module on a new thread
295        // then call a function and push the LoadedWasmSandbox instance to the loaded_wasm_sandbox_queue
296        let thread_handles: Vec<_> = (0..10)
297            .map(|i| {
298                let wq = wasm_sandbox_queue.clone();
299                let lwq = loaded_wasm_sandbox_queue.clone();
300
301                thread::spawn(move || {
302                    println!("Loading module on thread {}", i);
303                    let wasm_sandbox = wq.pop().unwrap();
304                    let loaded_wasm_sandbox: LoadedWasmSandbox = {
305                        let mod_path = get_wasm_module_path("RunWasm.aot").unwrap();
306                        wasm_sandbox.load_module(mod_path)
307                    }
308                    .unwrap();
309                    println!("Calling function on thread {}", i);
310                    let lws = call_funcs(loaded_wasm_sandbox, 1);
311                    lwq.push(lws).unwrap();
312                    println!("Pushed LoadedWasmSandbox instance to queue on thread {}", i)
313                })
314            })
315            .collect::<Vec<_>>();
316
317        for handle in thread_handles {
318            handle.join().unwrap();
319        }
320
321        // Get the LoadedWasmSandbox instances from the queue and call a function on a new thread, then unload the module and
322        // push the WasmSandbox instance back to the wasm_sandbox_queue
323
324        let thread_handles: Vec<_> = (0..10)
325            .map(|i| {
326                let wq = wasm_sandbox_queue.clone();
327                let lwq = loaded_wasm_sandbox_queue.clone();
328
329                thread::spawn(move || {
330                    println!("Popping sandbox on thread {}", i);
331                    let loaded_wasm_sandbox = lwq.pop().unwrap();
332                    println!("Calling funcs on thread {}", i);
333                    let lws = call_funcs(loaded_wasm_sandbox, 1);
334                    println!("Unloading module on thread {}", i);
335                    let ws = lws.unload_module().unwrap();
336                    println!("Pusing WasmSandbox on thread {}", i);
337                    wq.push(ws).unwrap();
338                })
339            })
340            .collect::<Vec<_>>();
341
342        for handle in thread_handles {
343            handle.join().unwrap();
344        }
345
346        // Now get the sandbox back from the queue and load the module and call a function
347        // this time we will load the .wasm version of the module rather than the .aot version
348
349        let thread_handles: Vec<_> = (0..10)
350            .map(|i| {
351                let wq = wasm_sandbox_queue.clone();
352
353                thread::spawn(move || {
354                    println!("Popping WasmSandbox on thread {}", i);
355                    let wasm_sandbox = wq.pop().unwrap();
356                    println!("Loading module on thread {}", i);
357                    let loaded_wasm_sandbox: LoadedWasmSandbox = {
358                        let mod_path = get_wasm_module_path("RunWasm.aot").unwrap();
359                        wasm_sandbox.load_module(mod_path)
360                    }
361                    .unwrap();
362                    println!("Calling function on thread {}", i);
363                    call_funcs(loaded_wasm_sandbox, 1);
364                })
365            })
366            .collect::<Vec<_>>();
367
368        for handle in thread_handles {
369            handle.join().unwrap();
370        }
371    }
372
373    #[test]
374    fn test_call_guest_functions_with_custom_config_multiple_times() {
375        let mut sandbox = SandboxBuilder::new()
376            .with_guest_scratch_size(32 * 1024)
377            .with_guest_heap_size(128 * 1024)
378            .build()
379            .unwrap();
380
381        sandbox
382            .register(
383                "GetTimeSinceBootMicrosecond",
384                get_time_since_boot_microsecond,
385            )
386            .unwrap();
387
388        let wasm_sandbox = sandbox.load_runtime().unwrap();
389
390        let loaded_wasm_sandbox: LoadedWasmSandbox = {
391            let mod_path = get_wasm_module_path("RunWasm.aot").unwrap();
392            wasm_sandbox.load_module(mod_path)
393        }
394        .unwrap();
395
396        call_funcs(loaded_wasm_sandbox, 1000);
397    }
398
399    #[test]
400    fn test_call_host_func_with_vecbytes() {
401        let host_func = |b: Vec<u8>, l: i32| {
402            // get the C String from the vec of bytes
403
404            let s = std::str::from_utf8(&b).unwrap();
405            println!("Host function received buffer: {}", s);
406
407            // check that s is the expected value if not return an error
408            if s != "Hello World!" {
409                return Err(new_error!("Unexpected value in buffer {}", s));
410            }
411
412            if l != 12 {
413                return Err(new_error!("Unexpected length of buffer {}", l));
414            }
415            Ok(0i32)
416        };
417
418        let mut proto_wasm_sandbox = SandboxBuilder::new().build().unwrap();
419
420        proto_wasm_sandbox
421            .register("HostFuncWithBufferAndLength", host_func)
422            .unwrap();
423
424        let wasm_sandbox = proto_wasm_sandbox.load_runtime().unwrap();
425
426        let mut loaded_wasm_sandbox: LoadedWasmSandbox = {
427            let mod_path = get_wasm_module_path("HostFunction.aot").unwrap();
428            wasm_sandbox.load_module(mod_path)
429        }
430        .unwrap();
431
432        // Call a guest function that calls a host function that takes a buffer and a length
433
434        let r: i32 = loaded_wasm_sandbox
435            .call_guest_function("PassBufferAndLengthToHost", ())
436            .unwrap();
437
438        assert_eq!(r, 0);
439    }
440
441    #[test]
442    fn test_load_module_fails_with_missing_host_function() {
443        // HostFunction.aot imports "HostFuncWithBufferAndLength" from "env".
444        // Loading it without registering that host function should fail
445        // at instantiation time (linker.instantiate) because the import
446        // cannot be satisfied.
447        let proto_wasm_sandbox = SandboxBuilder::new().build().unwrap();
448
449        let wasm_sandbox = proto_wasm_sandbox.load_runtime().unwrap();
450
451        let result: std::result::Result<LoadedWasmSandbox, _> = {
452            let mod_path = get_wasm_module_path("HostFunction.aot").unwrap();
453            wasm_sandbox.load_module(mod_path)
454        };
455
456        let err = result.unwrap_err();
457        let err_msg = format!("{:?}", err);
458        assert!(
459            err_msg.contains("HostFuncWithBufferAndLength"),
460            "Error should mention the missing host function, got: {err_msg}"
461        );
462    }
463
464    fn call_funcs(
465        mut loaded_wasm_sandbox: LoadedWasmSandbox,
466        iterations: i32,
467    ) -> LoadedWasmSandbox {
468        // Call a guest function that returns an int
469
470        for i in 0..iterations {
471            let result: i32 = loaded_wasm_sandbox
472                .call_guest_function("CalcFib", 4i32)
473                .unwrap();
474
475            println!(
476                "Got result: {:?} from the host function! iteration {}",
477                result, i,
478            );
479        }
480
481        // Call a guest function that returns a string
482
483        for i in 0..iterations {
484            let result: String = loaded_wasm_sandbox
485                .call_guest_function(
486                    "Echo",
487                    "Message from Rust Example to Wasm Function".to_string(),
488                )
489                .unwrap();
490
491            println!(
492                "Got result: {:?} from the host function! iteration {}",
493                result, i,
494            );
495        }
496
497        for i in 0..iterations {
498            let result: String = loaded_wasm_sandbox
499                .call_guest_function(
500                    "ToUpper",
501                    "Message from Rust Example to WASM Function".to_string(),
502                )
503                .unwrap();
504
505            println!(
506                "Got result: {:?} from the host function! iteration {}",
507                result, i,
508            );
509
510            assert_eq!(
511                result,
512                "MESSAGE FROM RUST EXAMPLE TO WASM FUNCTION".to_string()
513            );
514        }
515
516        // Call a guest function that returns a size prefixed buffer
517
518        for i in 0..iterations {
519            let result: Vec<u8> = loaded_wasm_sandbox
520                .call_guest_function("ReceiveByteArray", (vec![0x01, 0x02, 0x03], 3i32))
521                .unwrap();
522
523            println!(
524                "Got result: {:?} from the host function! iteration {}",
525                result, i,
526            );
527        }
528
529        // Call a guest function that Prints a string using HostPrint Host function
530
531        for i in 0..iterations {
532            loaded_wasm_sandbox
533                .call_guest_function::<()>(
534                    "Print",
535                    "Message from Rust Example to Wasm Function\n".to_string(),
536                )
537                .unwrap();
538
539            println!("Called the host function! iteration {}", i,);
540        }
541
542        // Call a guest function that calls prints a string constant using printf
543
544        for i in 0..iterations {
545            loaded_wasm_sandbox
546                .call_guest_function::<()>("PrintHelloWorld", ())
547                .unwrap();
548
549            println!("Called the host function! iteration {}", i,);
550        }
551
552        loaded_wasm_sandbox
553    }
554}