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::HostFuncsWrapper;
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<HostFuncsWrapper>>,
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<HostFuncsWrapper>>,
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    ///     None,
116    ///     None,
117    /// ).unwrap();
118    /// let sbox: MultiUseSandbox = u_sbox.evolve(Noop::default()).unwrap();
119    /// // Next, create a new call context from the single-use sandbox.
120    /// // After this line, your code will not compile if you try to use the
121    /// // original `sbox` variable.
122    /// let mut ctx = sbox.new_call_context();
123    ///
124    /// // Do a guest call with the context. Assumes that the loaded binary
125    /// // ("some_guest_binary") has a function therein called "SomeGuestFunc"
126    /// // that takes a single integer argument and returns an integer.
127    /// match ctx.call(
128    ///     "SomeGuestFunc",
129    ///     ReturnType::Int,
130    ///     Some(vec![ParameterValue::Int(1)])
131    /// ) {
132    ///     Ok(ReturnValue::Int(i)) => println!(
133    ///         "got successful return value {}",
134    ///         i,
135    ///     ),
136    ///     other => panic!(
137    ///         "failed to get return value as expected ({:?})",
138    ///         other,
139    ///     ),
140    /// };
141    /// // You can make further calls with the same context if you want.
142    /// // Otherwise, `ctx` will be dropped and all resources, including the
143    /// // underlying `MultiUseSandbox`, will be released and no further
144    /// // contexts can be created from that sandbox.
145    /// //
146    /// // If you want to avoid
147    /// // that behavior, call `finish` to convert the context back to
148    /// // the original `MultiUseSandbox`, as follows:
149    /// let _orig_sbox = ctx.finish();
150    /// // Now, you can operate on the original sandbox again (i.e. add more
151    /// // host functions etc...), create new contexts, and so on.
152    /// ```
153    #[instrument(skip_all, parent = Span::current())]
154    pub fn new_call_context(self) -> MultiUseGuestCallContext {
155        MultiUseGuestCallContext::start(self)
156    }
157
158    /// Call a guest function by name, with the given return type and arguments.
159    #[instrument(err(Debug), skip(self, args), parent = Span::current())]
160    pub fn call_guest_function_by_name(
161        &mut self,
162        func_name: &str,
163        func_ret_type: ReturnType,
164        args: Option<Vec<ParameterValue>>,
165    ) -> Result<ReturnValue> {
166        let res = call_function_on_guest(self, func_name, func_ret_type, args);
167        self.restore_state()?;
168        res
169    }
170
171    /// Restore the Sandbox's state
172    #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
173    pub(crate) fn restore_state(&mut self) -> Result<()> {
174        let mem_mgr = self.mem_mgr.unwrap_mgr_mut();
175        mem_mgr.restore_state_from_last_snapshot()
176    }
177}
178
179impl WrapperGetter for MultiUseSandbox {
180    fn get_mgr_wrapper(&self) -> &MemMgrWrapper<HostSharedMemory> {
181        &self.mem_mgr
182    }
183    fn get_mgr_wrapper_mut(&mut self) -> &mut MemMgrWrapper<HostSharedMemory> {
184        &mut self.mem_mgr
185    }
186    fn get_hv_handler(&self) -> &HypervisorHandler {
187        &self.hv_handler
188    }
189    fn get_hv_handler_mut(&mut self) -> &mut HypervisorHandler {
190        &mut self.hv_handler
191    }
192}
193
194impl Sandbox for MultiUseSandbox {
195    fn check_stack_guard(&self) -> Result<bool> {
196        self.mem_mgr.check_stack_guard()
197    }
198}
199
200impl std::fmt::Debug for MultiUseSandbox {
201    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
202        f.debug_struct("MultiUseSandbox")
203            .field("stack_guard", &self.mem_mgr.get_stack_cookie())
204            .finish()
205    }
206}
207
208impl DevolvableSandbox<MultiUseSandbox, MultiUseSandbox, Noop<MultiUseSandbox, MultiUseSandbox>>
209    for MultiUseSandbox
210{
211    /// Consume `self` and move it back to a `MultiUseSandbox` with previous state.
212    ///
213    /// The purpose of this function is to allow multiple states to be associated with a single MultiUseSandbox.
214    ///
215    /// 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.
216    /// The new MultiUseSandbox can then be used to call guest functions to execute the loaded code.
217    /// The devolve can be used to return the MultiUseSandbox to the state before the code was loaded. Thus avoiding initialisation overhead
218    #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
219    fn devolve(mut self, _tsn: Noop<MultiUseSandbox, MultiUseSandbox>) -> Result<MultiUseSandbox> {
220        self.mem_mgr
221            .unwrap_mgr_mut()
222            .pop_and_restore_state_from_snapshot()?;
223        Ok(self)
224    }
225}
226
227impl<'a, F>
228    EvolvableSandbox<
229        MultiUseSandbox,
230        MultiUseSandbox,
231        MultiUseContextCallback<'a, MultiUseSandbox, F>,
232    > for MultiUseSandbox
233where
234    F: FnOnce(&mut MultiUseGuestCallContext) -> Result<()> + 'a,
235{
236    /// The purpose of this function is to allow multiple states to be associated with a single MultiUseSandbox.
237    ///
238    /// 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.
239    /// The new MultiUseSandbox can then be used to call guest functions to execute the loaded code.
240    ///
241    /// The evolve function creates a new MultiUseCallContext which is then passed to a callback function  allowing the
242    /// callback function to call guest functions as part of the evolve process, once the callback function  is complete
243    /// the context is finished using a crate internal method that does not restore the prior state of the Sanbbox.
244    /// It then creates a mew  memory snapshot on the snapshot stack and returns the MultiUseSandbox
245    #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
246    fn evolve(
247        self,
248        transition_func: MultiUseContextCallback<'a, MultiUseSandbox, F>,
249    ) -> Result<MultiUseSandbox> {
250        let mut ctx = self.new_call_context();
251        transition_func.call(&mut ctx)?;
252        let mut sbox = ctx.finish_no_reset();
253        sbox.mem_mgr.unwrap_mgr_mut().push_state()?;
254        Ok(sbox)
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use hyperlight_common::flatbuffer_wrappers::function_types::{
261        ParameterValue, ReturnType, ReturnValue,
262    };
263    use hyperlight_testing::simple_guest_as_string;
264
265    use crate::func::call_ctx::MultiUseGuestCallContext;
266    use crate::sandbox::SandboxConfiguration;
267    use crate::sandbox_state::sandbox::{DevolvableSandbox, EvolvableSandbox};
268    use crate::sandbox_state::transition::{MultiUseContextCallback, Noop};
269    use crate::{GuestBinary, MultiUseSandbox, UninitializedSandbox};
270
271    // Tests to ensure that many (1000) function calls can be made in a call context with a small stack (1K) and heap(14K).
272    // This test effectively ensures that the stack is being properly reset after each call and we are not leaking memory in the Guest.
273    #[test]
274    fn test_with_small_stack_and_heap() {
275        let mut cfg = SandboxConfiguration::default();
276        cfg.set_heap_size(20 * 1024);
277        cfg.set_stack_size(16 * 1024);
278
279        let sbox1: MultiUseSandbox = {
280            let path = simple_guest_as_string().unwrap();
281            let u_sbox =
282                UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg), None, None)
283                    .unwrap();
284            u_sbox.evolve(Noop::default())
285        }
286        .unwrap();
287
288        let mut ctx = sbox1.new_call_context();
289
290        for _ in 0..1000 {
291            ctx.call(
292                "Echo",
293                ReturnType::String,
294                Some(vec![ParameterValue::String("hello".to_string())]),
295            )
296            .unwrap();
297        }
298
299        let sbox2: MultiUseSandbox = {
300            let path = simple_guest_as_string().unwrap();
301            let u_sbox =
302                UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg), None, None)
303                    .unwrap();
304            u_sbox.evolve(Noop::default())
305        }
306        .unwrap();
307
308        let mut ctx = sbox2.new_call_context();
309
310        for i in 0..1000 {
311            ctx.call(
312                "PrintUsingPrintf",
313                ReturnType::Int,
314                Some(vec![ParameterValue::String(
315                    format!("Hello World {}\n", i).to_string(),
316                )]),
317            )
318            .unwrap();
319        }
320    }
321
322    /// Tests that evolving from MultiUseSandbox to MultiUseSandbox creates a new state
323    /// and devolving from MultiUseSandbox to MultiUseSandbox restores the previous state
324    #[test]
325    fn evolve_devolve_handles_state_correctly() {
326        let sbox1: MultiUseSandbox = {
327            let path = simple_guest_as_string().unwrap();
328            let u_sbox =
329                UninitializedSandbox::new(GuestBinary::FilePath(path), None, None, None).unwrap();
330            u_sbox.evolve(Noop::default())
331        }
332        .unwrap();
333
334        let func = Box::new(|call_ctx: &mut MultiUseGuestCallContext| {
335            call_ctx.call(
336                "AddToStatic",
337                ReturnType::Int,
338                Some(vec![ParameterValue::Int(5)]),
339            )?;
340            Ok(())
341        });
342        let transition_func = MultiUseContextCallback::from(func);
343        let mut sbox2 = sbox1.evolve(transition_func).unwrap();
344        let res = sbox2
345            .call_guest_function_by_name("GetStatic", ReturnType::Int, None)
346            .unwrap();
347        assert_eq!(res, ReturnValue::Int(5));
348        let mut sbox3: MultiUseSandbox = sbox2.devolve(Noop::default()).unwrap();
349        let res = sbox3
350            .call_guest_function_by_name("GetStatic", ReturnType::Int, None)
351            .unwrap();
352        assert_eq!(res, ReturnValue::Int(0));
353    }
354}