hyperlight_host/sandbox/
initialized_multi_use.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::sync::{Arc, Mutex};
18
19use hyperlight_common::flatbuffer_wrappers::function_types::{
20    ParameterValue, ReturnType, ReturnValue,
21};
22use tracing::{instrument, Span};
23
24use super::host_funcs::FunctionRegistry;
25use super::{MemMgrWrapper, WrapperGetter};
26use crate::func::call_ctx::MultiUseGuestCallContext;
27use crate::func::guest_dispatch::call_function_on_guest;
28use crate::hypervisor::hypervisor_handler::HypervisorHandler;
29use crate::mem::shared_mem::HostSharedMemory;
30use crate::sandbox_state::sandbox::{DevolvableSandbox, EvolvableSandbox, Sandbox};
31use crate::sandbox_state::transition::{MultiUseContextCallback, Noop};
32use crate::Result;
33
34/// A sandbox that supports being used Multiple times.
35/// The implication of being used multiple times is two-fold:
36///
37/// 1. The sandbox can be used to call guest functions multiple times, each time a
38///    guest function is called the state of the sandbox is reset to the state it was in before the call was made.
39///
40/// 2. A MultiUseGuestCallContext can be created from the sandbox and used to make multiple guest function calls to the Sandbox.
41///    in this case the state of the sandbox is not reset until the context is finished and the `MultiUseSandbox` is returned.
42pub struct MultiUseSandbox {
43    // We need to keep a reference to the host functions, even if the compiler marks it as unused. The compiler cannot detect our dynamic usages of the host function in `HyperlightFunction::call`.
44    pub(super) _host_funcs: Arc<Mutex<FunctionRegistry>>,
45    pub(crate) mem_mgr: MemMgrWrapper<HostSharedMemory>,
46    hv_handler: HypervisorHandler,
47}
48
49// We need to implement drop to join the
50// threads, because, otherwise, we will
51// be leaking a thread with every
52// sandbox that is dropped. This was initially
53// caught by our benchmarks that created a ton of
54// sandboxes and caused the system to run out of
55// resources. Now, this is covered by the test:
56// `create_1000_sandboxes`.
57impl Drop for MultiUseSandbox {
58    fn drop(&mut self) {
59        match self.hv_handler.kill_hypervisor_handler_thread() {
60            Ok(_) => {}
61            Err(e) => {
62                log::error!("[POTENTIAL THREAD LEAK] Potentially failed to kill hypervisor handler thread when dropping MultiUseSandbox: {:?}", e);
63            }
64        }
65    }
66}
67
68impl MultiUseSandbox {
69    /// Move an `UninitializedSandbox` into a new `MultiUseSandbox` instance.
70    ///
71    /// This function is not equivalent to doing an `evolve` from uninitialized
72    /// to initialized, and is purposely not exposed publicly outside the crate
73    /// (as a `From` implementation would be)
74    #[instrument(skip_all, parent = Span::current(), level = "Trace")]
75    pub(super) fn from_uninit(
76        host_funcs: Arc<Mutex<FunctionRegistry>>,
77        mgr: MemMgrWrapper<HostSharedMemory>,
78        hv_handler: HypervisorHandler,
79    ) -> MultiUseSandbox {
80        Self {
81            _host_funcs: host_funcs,
82            mem_mgr: mgr,
83            hv_handler,
84        }
85    }
86
87    /// Create a new `MultiUseCallContext` suitable for making 0 or more
88    /// calls to guest functions within the same context.
89    ///
90    /// Since this function consumes `self`, the returned
91    /// `MultiUseGuestCallContext` is guaranteed mutual exclusion for calling
92    /// functions within the sandbox. This guarantee is enforced at compile
93    /// time, and no locks, atomics, or any other mutual exclusion mechanisms
94    /// are used at runtime.
95    ///
96    /// If you have called this function, have a `MultiUseGuestCallContext`,
97    /// and wish to "return" it to a `MultiUseSandbox`, call the `finish`
98    /// method on the context.
99    ///
100    /// Example usage (compiled as a "no_run" doctest since the test binary
101    /// will not be found):
102    ///
103    /// ```no_run
104    /// use hyperlight_host::sandbox::{UninitializedSandbox, MultiUseSandbox};
105    /// use hyperlight_common::flatbuffer_wrappers::function_types::{ReturnType, ParameterValue, ReturnValue};
106    /// use hyperlight_host::sandbox_state::sandbox::EvolvableSandbox;
107    /// use hyperlight_host::sandbox_state::transition::Noop;
108    /// use hyperlight_host::GuestBinary;
109    ///
110    /// // First, create a new uninitialized sandbox, then evolve it to become
111    /// // an initialized, single-use one.
112    /// let u_sbox = UninitializedSandbox::new(
113    ///     GuestBinary::FilePath("some_guest_binary".to_string()),
114    ///     None,
115    /// ).unwrap();
116    /// let sbox: MultiUseSandbox = u_sbox.evolve(Noop::default()).unwrap();
117    /// // Next, create a new call context from the single-use sandbox.
118    /// // After this line, your code will not compile if you try to use the
119    /// // original `sbox` variable.
120    /// let mut ctx = sbox.new_call_context();
121    ///
122    /// // Do a guest call with the context. Assumes that the loaded binary
123    /// // ("some_guest_binary") has a function therein called "SomeGuestFunc"
124    /// // that takes a single integer argument and returns an integer.
125    /// match ctx.call(
126    ///     "SomeGuestFunc",
127    ///     ReturnType::Int,
128    ///     Some(vec![ParameterValue::Int(1)])
129    /// ) {
130    ///     Ok(ReturnValue::Int(i)) => println!(
131    ///         "got successful return value {}",
132    ///         i,
133    ///     ),
134    ///     other => panic!(
135    ///         "failed to get return value as expected ({:?})",
136    ///         other,
137    ///     ),
138    /// };
139    /// // You can make further calls with the same context if you want.
140    /// // Otherwise, `ctx` will be dropped and all resources, including the
141    /// // underlying `MultiUseSandbox`, will be released and no further
142    /// // contexts can be created from that sandbox.
143    /// //
144    /// // If you want to avoid
145    /// // that behavior, call `finish` to convert the context back to
146    /// // the original `MultiUseSandbox`, as follows:
147    /// let _orig_sbox = ctx.finish();
148    /// // Now, you can operate on the original sandbox again (i.e. add more
149    /// // host functions etc...), create new contexts, and so on.
150    /// ```
151    #[instrument(skip_all, parent = Span::current())]
152    pub fn new_call_context(self) -> MultiUseGuestCallContext {
153        MultiUseGuestCallContext::start(self)
154    }
155
156    /// Call a guest function by name, with the given return type and arguments.
157    #[instrument(err(Debug), skip(self, args), parent = Span::current())]
158    pub fn call_guest_function_by_name(
159        &mut self,
160        func_name: &str,
161        func_ret_type: ReturnType,
162        args: Option<Vec<ParameterValue>>,
163    ) -> Result<ReturnValue> {
164        let res = call_function_on_guest(self, func_name, func_ret_type, args);
165        self.restore_state()?;
166        res
167    }
168
169    /// Restore the Sandbox's state
170    #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
171    pub(crate) fn restore_state(&mut self) -> Result<()> {
172        let mem_mgr = self.mem_mgr.unwrap_mgr_mut();
173        mem_mgr.restore_state_from_last_snapshot()
174    }
175}
176
177impl WrapperGetter for MultiUseSandbox {
178    fn get_mgr_wrapper(&self) -> &MemMgrWrapper<HostSharedMemory> {
179        &self.mem_mgr
180    }
181    fn get_mgr_wrapper_mut(&mut self) -> &mut MemMgrWrapper<HostSharedMemory> {
182        &mut self.mem_mgr
183    }
184    fn get_hv_handler(&self) -> &HypervisorHandler {
185        &self.hv_handler
186    }
187    fn get_hv_handler_mut(&mut self) -> &mut HypervisorHandler {
188        &mut self.hv_handler
189    }
190}
191
192impl Sandbox for MultiUseSandbox {
193    fn check_stack_guard(&self) -> Result<bool> {
194        self.mem_mgr.check_stack_guard()
195    }
196}
197
198impl std::fmt::Debug for MultiUseSandbox {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        f.debug_struct("MultiUseSandbox")
201            .field("stack_guard", &self.mem_mgr.get_stack_cookie())
202            .finish()
203    }
204}
205
206impl DevolvableSandbox<MultiUseSandbox, MultiUseSandbox, Noop<MultiUseSandbox, MultiUseSandbox>>
207    for MultiUseSandbox
208{
209    /// Consume `self` and move it back to a `MultiUseSandbox` with previous state.
210    ///
211    /// The purpose of this function is to allow multiple states to be associated with a single MultiUseSandbox.
212    ///
213    /// An implementation such as HyperlightJs or HyperlightWasm can use this to call guest functions to load JS or WASM code and then evolve the sandbox causing state to be captured.
214    /// The new MultiUseSandbox can then be used to call guest functions to execute the loaded code.
215    /// The devolve can be used to return the MultiUseSandbox to the state before the code was loaded. Thus avoiding initialisation overhead
216    #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
217    fn devolve(mut self, _tsn: Noop<MultiUseSandbox, MultiUseSandbox>) -> Result<MultiUseSandbox> {
218        self.mem_mgr
219            .unwrap_mgr_mut()
220            .pop_and_restore_state_from_snapshot()?;
221        Ok(self)
222    }
223}
224
225impl<'a, F>
226    EvolvableSandbox<
227        MultiUseSandbox,
228        MultiUseSandbox,
229        MultiUseContextCallback<'a, MultiUseSandbox, F>,
230    > for MultiUseSandbox
231where
232    F: FnOnce(&mut MultiUseGuestCallContext) -> Result<()> + 'a,
233{
234    /// The purpose of this function is to allow multiple states to be associated with a single MultiUseSandbox.
235    ///
236    /// An implementation such as HyperlightJs or HyperlightWasm can use this to call guest functions to load JS or WASM code and then evolve the sandbox causing state to be captured.
237    /// The new MultiUseSandbox can then be used to call guest functions to execute the loaded code.
238    ///
239    /// The evolve function creates a new MultiUseCallContext which is then passed to a callback function  allowing the
240    /// callback function to call guest functions as part of the evolve process, once the callback function  is complete
241    /// the context is finished using a crate internal method that does not restore the prior state of the Sandbox.
242    /// It then creates a mew  memory snapshot on the snapshot stack and returns the MultiUseSandbox
243    #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
244    fn evolve(
245        self,
246        transition_func: MultiUseContextCallback<'a, MultiUseSandbox, F>,
247    ) -> Result<MultiUseSandbox> {
248        let mut ctx = self.new_call_context();
249        transition_func.call(&mut ctx)?;
250        let mut sbox = ctx.finish_no_reset();
251        sbox.mem_mgr.unwrap_mgr_mut().push_state()?;
252        Ok(sbox)
253    }
254}
255
256#[cfg(test)]
257mod tests {
258    use hyperlight_common::flatbuffer_wrappers::function_types::{
259        ParameterValue, ReturnType, ReturnValue,
260    };
261    use hyperlight_testing::simple_guest_as_string;
262
263    use crate::func::call_ctx::MultiUseGuestCallContext;
264    use crate::sandbox::SandboxConfiguration;
265    use crate::sandbox_state::sandbox::{DevolvableSandbox, EvolvableSandbox};
266    use crate::sandbox_state::transition::{MultiUseContextCallback, Noop};
267    use crate::{GuestBinary, MultiUseSandbox, UninitializedSandbox};
268
269    // Tests to ensure that many (1000) function calls can be made in a call context with a small stack (1K) and heap(14K).
270    // This test effectively ensures that the stack is being properly reset after each call and we are not leaking memory in the Guest.
271    #[test]
272    fn test_with_small_stack_and_heap() {
273        let mut cfg = SandboxConfiguration::default();
274        cfg.set_heap_size(20 * 1024);
275        cfg.set_stack_size(16 * 1024);
276
277        let sbox1: MultiUseSandbox = {
278            let path = simple_guest_as_string().unwrap();
279            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
280            u_sbox.evolve(Noop::default())
281        }
282        .unwrap();
283
284        let mut ctx = sbox1.new_call_context();
285
286        for _ in 0..1000 {
287            ctx.call(
288                "Echo",
289                ReturnType::String,
290                Some(vec![ParameterValue::String("hello".to_string())]),
291            )
292            .unwrap();
293        }
294
295        let sbox2: MultiUseSandbox = {
296            let path = simple_guest_as_string().unwrap();
297            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
298            u_sbox.evolve(Noop::default())
299        }
300        .unwrap();
301
302        let mut ctx = sbox2.new_call_context();
303
304        for i in 0..1000 {
305            ctx.call(
306                "PrintUsingPrintf",
307                ReturnType::Int,
308                Some(vec![ParameterValue::String(
309                    format!("Hello World {}\n", i).to_string(),
310                )]),
311            )
312            .unwrap();
313        }
314    }
315
316    /// Tests that evolving from MultiUseSandbox to MultiUseSandbox creates a new state
317    /// and devolving from MultiUseSandbox to MultiUseSandbox restores the previous state
318    #[test]
319    fn evolve_devolve_handles_state_correctly() {
320        let sbox1: MultiUseSandbox = {
321            let path = simple_guest_as_string().unwrap();
322            let u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), None).unwrap();
323            u_sbox.evolve(Noop::default())
324        }
325        .unwrap();
326
327        let func = Box::new(|call_ctx: &mut MultiUseGuestCallContext| {
328            call_ctx.call(
329                "AddToStatic",
330                ReturnType::Int,
331                Some(vec![ParameterValue::Int(5)]),
332            )?;
333            Ok(())
334        });
335        let transition_func = MultiUseContextCallback::from(func);
336        let mut sbox2 = sbox1.evolve(transition_func).unwrap();
337        let res = sbox2
338            .call_guest_function_by_name("GetStatic", ReturnType::Int, None)
339            .unwrap();
340        assert_eq!(res, ReturnValue::Int(5));
341        let mut sbox3: MultiUseSandbox = sbox2.devolve(Noop::default()).unwrap();
342        let res = sbox3
343            .call_guest_function_by_name("GetStatic", ReturnType::Int, None)
344            .unwrap();
345        assert_eq!(res, ReturnValue::Int(0));
346    }
347}