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}