Skip to content

Commit 4d093eb

Browse files
committed
Only clear io buffer after unsuccesfull guest call.
This commit also clears poisoned Mutex after a host function panics and introduces a new HyperlightError variant for use when a host function panics. Signed-off-by: Ludvig Liljenberg <[email protected]>
1 parent e760628 commit 4d093eb

File tree

9 files changed

+140
-49
lines changed

9 files changed

+140
-49
lines changed

src/hyperlight_host/src/error.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ pub enum HyperlightError {
122122
#[error("HostFunction {0} was not found")]
123123
HostFunctionNotFound(String),
124124

125+
/// Host function panicked
126+
#[error("Host function '{0}' panicked: {1}")]
127+
HostFunctionPanic(String, String),
128+
125129
/// Reading Writing or Seeking data failed.
126130
#[error("Reading Writing or Seeking data failed {0:?}")]
127131
IOError(#[from] std::io::Error),

src/hyperlight_host/src/func/host_functions.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,28 @@ macro_rules! impl_host_function {
179179
let func = Mutex::new(func);
180180
HostFunction {
181181
func: Arc::new(move |args: ($($P,)*)| {
182-
func.try_lock()
183-
.map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?
184-
(args)
182+
match func.try_lock() {
183+
Ok(mut guard) => {
184+
// Note: we can not just do `guard(args)` because seccomp violations require this explicit drop-order
185+
// in order to properly catch panics with `catch_unwind`. It's unclear why this is necessary, but without it,
186+
// the test `test_violate_seccomp_filters` will fail in release profile.
187+
let result = guard(args);
188+
drop(guard);
189+
result
190+
},
191+
Err(poison_err) => {
192+
match poison_err {
193+
// The previous call to this host function panicked, poisoning the lock.
194+
// We can clear the poison safely.
195+
std::sync::TryLockError::Poisoned(guard) => {
196+
guard.into_inner()(args)
197+
}
198+
std::sync::TryLockError::WouldBlock => {
199+
Err(new_error!("Error locking at {}:{}: mutex would block", file!(), line!()))
200+
}
201+
}
202+
}
203+
}
185204
})
186205
}
187206
}

src/hyperlight_host/src/sandbox/host_funcs.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,18 @@ fn maybe_with_seccomp<T: Send>(
190190
return Err(crate::HyperlightError::DisallowedSyscall);
191191
}
192192

193-
crate::log_then_return!("Host function {} panicked", name);
193+
let panic_msg = if let Some(s) = err.downcast_ref::<String>() {
194+
s.clone()
195+
} else if let Some(s) = err.downcast_ref::<&str>() {
196+
s.to_string()
197+
} else {
198+
"Unknown panic".to_string()
199+
};
200+
201+
Err(crate::HyperlightError::HostFunctionPanic(
202+
name.to_string(),
203+
panic_msg,
204+
))
194205
}
195206
}
196207
})?

src/hyperlight_host/src/sandbox/initialized_multi_use.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -421,9 +421,14 @@ impl MultiUseSandbox {
421421
.get_guest_function_call_result()
422422
})();
423423

424-
// TODO: Do we want to allow re-entrant guest function calls?
425-
self.get_mgr_wrapper_mut().as_mut().clear_io_buffers();
426-
424+
// In the happy path we do not need to clear io-buffers from the host because:
425+
// - the serialized guest function call is zeroed out by the guest during deserialization, see call to `try_pop_shared_input_data_into::<FunctionCall>()`
426+
// - the serialized guest function result is zeroed out by us (the host) during deserialization, see `get_guest_function_call_result`
427+
// - any serialized host function call are zeroed out by us (the host) during deserialization, see `get_host_function_call`
428+
// - any serialized host function result is zeroed out by the guest during deserialization, see `get_host_return_value`
429+
if res.is_err() {
430+
self.get_mgr_wrapper_mut().as_mut().clear_io_buffers();
431+
}
427432
res
428433
}
429434

@@ -502,6 +507,40 @@ mod tests {
502507
use crate::sandbox::SandboxConfiguration;
503508
use crate::{GuestBinary, HyperlightError, MultiUseSandbox, Result, UninitializedSandbox};
504509

510+
// make sure memory is cleared even if host function panics
511+
#[test]
512+
#[cfg_attr(
513+
not(feature = "seccomp"),
514+
should_panic(expected = "Host function panic msg") // if seccomp is not enabled, we do not spawn a new thread for host funcs,
515+
// and we do not use catch_unwind to catch any panics.
516+
)]
517+
fn test_host_func_panic_clear_buffer() {
518+
let path = simple_guest_as_string().unwrap();
519+
let mut cfg = SandboxConfiguration::default();
520+
cfg.set_input_data_size(20 * 1024);
521+
cfg.set_output_data_size(20 * 1024);
522+
let mut u_sbox = UninitializedSandbox::new(GuestBinary::FilePath(path), Some(cfg)).unwrap();
523+
u_sbox
524+
.register("host_panic", |_bytes: Vec<u8>| {
525+
panic!("Host function panic msg");
526+
#[expect(unreachable_code, reason = "Needed for type inference")]
527+
Ok(())
528+
})
529+
.unwrap();
530+
let mut sbox = u_sbox.evolve().unwrap();
531+
532+
for _ in 0..30 {
533+
// if io-buffers are not cleared on host func panic, this would eventually fail due to filling up input/output buffer
534+
let res = sbox
535+
.call::<Vec<u8>>("CallHostPanic", (vec![1; 1024],))
536+
.unwrap_err();
537+
assert!(matches!(
538+
res,
539+
crate::HyperlightError::HostFunctionPanic(func_name, payload) if func_name == "host_panic" && payload == "Host function panic msg"
540+
));
541+
}
542+
}
543+
505544
/// Tests that call_guest_function_by_name restores the state correctly
506545
#[test]
507546
fn test_call_guest_function_by_name() {

src/tests/rust_guests/callbackguest/Cargo.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tests/rust_guests/dummyguest/Cargo.lock

Lines changed: 11 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tests/rust_guests/simpleguest/Cargo.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tests/rust_guests/simpleguest/src/main.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,9 +898,27 @@ fn exec_mapped_buffer(function_call: &FunctionCall) -> Result<Vec<u8>> {
898898
}
899899
}
900900

901+
fn call_host_panic(_: &FunctionCall) -> Result<Vec<u8>> {
902+
call_host_function::<()>(
903+
"host_panic",
904+
Some(vec![ParameterValue::VecBytes(vec![1; 1024])]),
905+
ReturnType::VecBytes,
906+
)
907+
.unwrap();
908+
Ok(get_flatbuffer_result::<&[u8]>(vec![1; 1024].as_ref()))
909+
}
910+
901911
#[no_mangle]
902912
#[hyperlight_guest_tracing::trace_function]
903913
pub extern "C" fn hyperlight_main() {
914+
let call_host_panic_def = GuestFunctionDefinition::new(
915+
"CallHostPanic".to_string(),
916+
Vec::from(&[ParameterType::VecBytes]),
917+
ReturnType::VecBytes,
918+
call_host_panic as usize,
919+
);
920+
register_function(call_host_panic_def);
921+
904922
let read_from_user_memory_def = GuestFunctionDefinition::new(
905923
"ReadFromUserMemory".to_string(),
906924
Vec::from(&[ParameterType::ULong, ParameterType::VecBytes]),

0 commit comments

Comments
 (0)