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}