Skip to content

Commit 34a5772

Browse files
committed
Fix bug where stale signals can interrupt wrong sandbox, and allow preventing host functions to return control to guest after being interrupted
Signed-off-by: Ludvig Liljenberg <[email protected]>
1 parent 2543f60 commit 34a5772

File tree

6 files changed

+335
-108
lines changed

6 files changed

+335
-108
lines changed

src/hyperlight_host/src/hypervisor/hyperv_linux.rs

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,7 @@ impl HypervLinuxDriver {
395395
orig_rsp: rsp_ptr,
396396
interrupt_handle: Arc::new(LinuxInterruptHandle {
397397
running: AtomicBool::new(false),
398+
cancel_requested: AtomicBool::new(false),
398399
tid: AtomicU64::new(unsafe { libc::pthread_self() }),
399400
dropped: AtomicBool::new(false),
400401
}),
@@ -584,37 +585,52 @@ impl Hypervisor for HypervLinuxDriver {
584585
self.interrupt_handle
585586
.tid
586587
.store(unsafe { libc::pthread_self() as u64 }, Ordering::Relaxed);
587-
// Note: if a `InterruptHandle::kill()` signal is delivered to this thread **here**
588-
// - before we've set the running to true,
589-
// Then the signal does not have any effect, because the signal handler is a no-op.
588+
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
589+
// Then this is fine since `cancel_requested` is set to true, so we will skip the `VcpuFd::run()` call
590590
self.interrupt_handle.running.store(true, Ordering::Relaxed);
591-
// Note: if a `InterruptHandle::kill()` signal is delivered to this thread **here**
592-
// - after we've set the running to true,
593-
// - before we've called `VcpuFd::run()`
594-
// Then the individual signal is lost, because the signal is only processed after we've left userspace.
595-
// However, for this reason, we keep sending the signal again and again until we see that the atomic `running` is set to false.
596-
#[cfg(mshv2)]
597-
let run_result = {
598-
let hv_message: hv_message = Default::default();
599-
self.vcpu_fd.run(hv_message)
591+
// Don't run the vcpu is `cancel_requested` is true
592+
//
593+
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
594+
// Then this is fine since `cancel_requested` is set to true, so we will skip the `VcpuFd::run()` call
595+
let exit_reason = if self
596+
.interrupt_handle
597+
.cancel_requested
598+
.swap(false, Ordering::Relaxed)
599+
{
600+
return Ok(HyperlightExit::Cancelled());
601+
} else {
602+
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
603+
// Then the vcpu will run, but we will keep sending signals to this thread
604+
// to interrupt it until `running` is set to false. The `vcpu_fd::run()` call will
605+
// return either normally with an exit reason, or from being "kicked" by out signal handler, with an EINTR error,
606+
// both of which are fine.
607+
#[cfg(mshv2)]
608+
{
609+
let hv_message: hv_message = Default::default();
610+
self.vcpu_fd.run(hv_message)
611+
}
612+
#[cfg(mshv3)]
613+
self.vcpu_fd.run()
600614
};
601-
#[cfg(mshv3)]
602-
let run_result = self.vcpu_fd.run();
603-
// Note: if a `InterruptHandle::kill()` signal is delivered to this thread **here**
604-
// - after we've called `VcpuFd::run()`
605-
// - before we've set the running to false
606-
// Then this is fine because the call to `VcpuFd::run()` is already finished,
607-
// the signal handler itself is a no-op, and the signals will stop being sent
608-
// once we've set the `running` to false.
615+
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
616+
// Then signals will be sent to this thread until `running` is set to false.
617+
// This is fine since the signal handler is a no-op.
618+
let cancel_requested = self
619+
.interrupt_handle
620+
.cancel_requested
621+
.swap(false, Ordering::Relaxed);
622+
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
623+
// Then `cancel_requested` will be set to true again, which will cancel the **next vcpu run**.
624+
// Additionally signals will be sent to this thread until `running` is set to false.
625+
// This is fine since the signal handler is a no-op.
609626
self.interrupt_handle
610627
.running
611628
.store(false, Ordering::Relaxed);
612-
// Note: if a `InterruptHandle::kill()` signal is delivered to this thread **here**
613-
// - after we've set the running to false,
614-
// Then the signal does not have any effect, because the signal handler is a no-op.
615-
// This is fine since we are already done with the `VcpuFd::run()` call.
616-
617-
let result = match run_result {
629+
// At this point, `running` is false so no more signals will be sent to this thread,
630+
// but we may still receive async signals that were sent before this point.
631+
// To prevent those signals from interrupting subsequent calls to `run()`,
632+
// we make sure to check `cancel_requested` before cancelling (see `libc::EINTR` match-arm below).
633+
let result = match exit_reason {
618634
Ok(m) => match m.header.message_type {
619635
HALT_MESSAGE => {
620636
crate::debug!("mshv - Halt Details : {:#?}", &self);
@@ -691,7 +707,15 @@ impl Hypervisor for HypervLinuxDriver {
691707
},
692708
Err(e) => match e.errno() {
693709
// we send a signal to the thread to cancel execution this results in EINTR being returned by KVM so we return Cancelled
694-
libc::EINTR => HyperlightExit::Cancelled(),
710+
libc::EINTR => {
711+
// If cancellation was not requested for this specific vm, the vcpu was interrupted because of stale signal
712+
// that was meant to be delivered to a previous/other vcpu on this same thread, so let's ignore it
713+
if !cancel_requested {
714+
HyperlightExit::Retry()
715+
} else {
716+
HyperlightExit::Cancelled()
717+
}
718+
}
695719
libc::EAGAIN => HyperlightExit::Retry(),
696720
_ => {
697721
crate::debug!("mshv Error - Details: Error: {} \n {:#?}", e, &self);

src/hyperlight_host/src/hypervisor/kvm.rs

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ impl KVMDriver {
350350
mem_regions,
351351
interrupt_handle: Arc::new(LinuxInterruptHandle {
352352
running: AtomicBool::new(false),
353+
cancel_requested: AtomicBool::new(false),
353354
tid: AtomicU64::new(unsafe { libc::pthread_self() }),
354355
dropped: AtomicBool::new(false),
355356
}),
@@ -519,29 +520,47 @@ impl Hypervisor for KVMDriver {
519520
self.interrupt_handle
520521
.tid
521522
.store(unsafe { libc::pthread_self() as u64 }, Ordering::Relaxed);
522-
// Note: if a `InterruptHandle::kill()` signal is delivered to this thread **here**
523-
// - before we've set the running to true,
524-
// Then the signal does not have any effect, because the signal handler is a no-op.
523+
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
524+
// Then this is fine since `cancel_requested` is set to true, so we will skip the `VcpuFd::run()` call
525525
self.interrupt_handle.running.store(true, Ordering::Relaxed);
526-
// Note: if a `InterruptHandle::kill()` signal is delivered to this thread **here**
527-
// - after we've set the running to true,
528-
// - before we've called `VcpuFd::run()`
529-
// Then the individual signal is lost, because the signal is only processed after we've left userspace.
530-
// However, for this reason, we keep sending the signal again and again until we see that the atomic `running` is set to false.
531-
let exit_reason = self.vcpu_fd.run();
532-
// Note: if a `InterruptHandle::kill()` signal is delivered to this thread **here**
533-
// - after we've called `VcpuFd::run()`
534-
// - before we've set the running to false
535-
// Then this is fine because the call to `VcpuFd::run()` is already finished,
536-
// the signal handler itself is a no-op, and the signals will stop being sent
537-
// once we've set the `running` to false.
526+
// Don't run the vcpu is `cancel_requested` is true
527+
//
528+
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
529+
// Then this is fine since `cancel_requested` is set to true, so we will skip the `VcpuFd::run()` call
530+
let exit_reason = if self
531+
.interrupt_handle
532+
.cancel_requested
533+
.load(Ordering::Relaxed)
534+
{
535+
Err(kvm_ioctls::Error::new(libc::EINTR))
536+
} else {
537+
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
538+
// Then the vcpu will run, but we will keep sending signals to this thread
539+
// to interrupt it until `running` is set to false. The `vcpu_fd::run()` call will
540+
// return either normally with an exit reason, or from being "kicked" by out signal handler, with an EINTR error,
541+
// both of which are fine.
542+
self.vcpu_fd.run()
543+
};
544+
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
545+
// Then signals will be sent to this thread until `running` is set to false.
546+
// This is fine since the signal handler is a no-op.
547+
#[allow(unused_variables)]
548+
// The variable is only used when `cfg(not(gdb))`, but the flag needs to be reset always anyway
549+
let cancel_requested = self
550+
.interrupt_handle
551+
.cancel_requested
552+
.swap(false, Ordering::Relaxed);
553+
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
554+
// Then `cancel_requested` will be set to true again, which will cancel the **next vcpu run**.
555+
// Additionally signals will be sent to this thread until `running` is set to false.
556+
// This is fine since the signal handler is a no-op.
538557
self.interrupt_handle
539558
.running
540559
.store(false, Ordering::Relaxed);
541-
// Note: if a `InterruptHandle::kill()` signal is delivered to this thread **here**
542-
// - after we've set the running to false,
543-
// Then the signal does not have any effect, because the signal handler is a no-op.
544-
// This is fine since we are already done with the `VcpuFd::run()` call.
560+
// At this point, `running` is false so no more signals will be sent to this thread,
561+
// but we may still receive async signals that were sent before this point.
562+
// To prevent those signals from interrupting subsequent calls to `run()` (on other vms!),
563+
// we make sure to check `cancel_requested` before cancelling (see `libc::EINTR` match-arm below).
545564
let result = match exit_reason {
546565
Ok(VcpuExit::Hlt) => {
547566
crate::debug!("KVM - Halt Details : {:#?}", &self);
@@ -593,7 +612,15 @@ impl Hypervisor for KVMDriver {
593612
libc::EINTR => HyperlightExit::Debug(VcpuStopReason::Interrupt),
594613
// we send a signal to the thread to cancel execution this results in EINTR being returned by KVM so we return Cancelled
595614
#[cfg(not(gdb))]
596-
libc::EINTR => HyperlightExit::Cancelled(),
615+
libc::EINTR => {
616+
// If cancellation was not requested for this specific vm, the vcpu was interrupted because of stale signal
617+
// that was meant to be delivered to a previous/other vcpu on this same thread, so let's ignore it
618+
if !cancel_requested {
619+
HyperlightExit::Retry()
620+
} else {
621+
HyperlightExit::Cancelled()
622+
}
623+
}
597624
libc::EAGAIN => HyperlightExit::Retry(),
598625
_ => {
599626
crate::debug!("KVM Error -Details: Address: {} \n {:#?}", e, &self);

src/hyperlight_host/src/hypervisor/mod.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use tracing::{instrument, Span};
2020
use crate::error::HyperlightError::ExecutionCanceledByHost;
2121
use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags};
2222
use crate::metrics::METRIC_GUEST_CANCELLATION;
23+
#[cfg(any(kvm, mshv))]
24+
use crate::signal_handlers::INTERRUPT_VCPU_SIGRTMIN_OFFSET;
2325
use crate::{log_then_return, new_error, HyperlightError, Result};
2426

2527
/// Util for handling x87 fpu state
@@ -322,10 +324,12 @@ impl VirtualCPU {
322324

323325
/// A trait for handling interrupts to a sandbox's vcpu
324326
pub trait InterruptHandle: Send + Sync {
325-
/// Interrupt the corresponding sandbox's vcpu if it's running.
327+
/// Interrupt the corresponding sandbox from running.
326328
///
327329
/// - If this is called while the vcpu is running, then it will interrupt the vcpu and return `true`.
328-
/// - If this is called while the vcpu is not running, then it will do nothing and return `false`.
330+
/// - If this is called while the vcpu is not running, (for example during a host call), the
331+
/// vcpu will not immediately be interrupted, but will prevent the vcpu from running **the next time**
332+
/// it's scheduled, and returns `false`.
329333
///
330334
/// # Note
331335
/// This function will block for the duration of the time it takes for the vcpu thread to be interrupted.
@@ -338,25 +342,35 @@ pub trait InterruptHandle: Send + Sync {
338342
#[cfg(any(kvm, mshv))]
339343
#[derive(Debug)]
340344
pub(super) struct LinuxInterruptHandle {
341-
/// True when the vcpu is currently running and blocking the thread
345+
/// Invariant: vcpu is running => `running` is true. (Neither converse nor inverse is true)
342346
running: AtomicBool,
343-
/// The thread id on which the vcpu was most recently run on or is currently running on
347+
/// Invariant: vcpu is running => `tid` is the thread on which it is running.
344348
tid: AtomicU64,
349+
/// True when an "interruptor" has requested the VM to be cancelled. Set immediately when
350+
/// `kill()` is called, and cleared when the vcpu is no longer running.
351+
/// This is used to
352+
/// 1. make sure stale signals do not interrupt the
353+
/// the wrong vcpu (a vcpu may only be interrupted iff `cancel_requested` is true),
354+
/// 2. ensure that if a vm is killed while a host call is running,
355+
/// the vm will not re-enter the guest after the host call returns.
356+
cancel_requested: AtomicBool,
345357
/// Whether the corresponding vm is dropped
346358
dropped: AtomicBool,
347359
}
348360

349361
#[cfg(any(kvm, mshv))]
350362
impl InterruptHandle for LinuxInterruptHandle {
351363
fn kill(&self) -> bool {
352-
let sigrtmin = libc::SIGRTMIN();
364+
self.cancel_requested.store(true, Ordering::Relaxed);
365+
366+
let signal_number = libc::SIGRTMIN() + INTERRUPT_VCPU_SIGRTMIN_OFFSET;
353367
let mut sent_signal = false;
354368

355369
while self.running.load(Ordering::Relaxed) {
356370
log::info!("Sending signal to kill vcpu thread...");
357371
sent_signal = true;
358372
unsafe {
359-
libc::pthread_kill(self.tid.load(Ordering::Relaxed) as _, sigrtmin);
373+
libc::pthread_kill(self.tid.load(Ordering::Relaxed) as _, signal_number);
360374
}
361375
std::thread::sleep(std::time::Duration::from_micros(50));
362376
}

src/hyperlight_host/src/sandbox/initialized_multi_use.rs

Lines changed: 1 addition & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -295,12 +295,10 @@ where
295295

296296
#[cfg(test)]
297297
mod tests {
298-
use std::thread;
299-
300298
use hyperlight_common::flatbuffer_wrappers::function_types::{
301299
ParameterValue, ReturnType, ReturnValue,
302300
};
303-
use hyperlight_testing::{callback_guest_as_string, simple_guest_as_string};
301+
use hyperlight_testing::simple_guest_as_string;
304302

305303
use crate::func::call_ctx::MultiUseGuestCallContext;
306304
use crate::sandbox::SandboxConfiguration;
@@ -465,54 +463,6 @@ mod tests {
465463
Ok(())
466464
}
467465

468-
// This test is to capture the case where the guest execution is running a host function when cancelled and that host function
469-
// is never going to return.
470-
// The host function that is called will end after 5 seconds, but by this time the cancellation will have given up
471-
// (using default timeout settings) , so this tests looks for the error "Failed to cancel guest execution".
472-
#[test]
473-
#[ignore = "We cannot cancel host functions. TODO reenable this test when it's enabled"]
474-
fn test_terminate_vcpu_calling_host_spinning_cpu() {
475-
// This test relies upon a Hypervisor being present so for now
476-
// we will skip it if there isn't one.
477-
if !is_hypervisor_present() {
478-
println!("Skipping test_call_guest_function_by_name because no hypervisor is present");
479-
return;
480-
}
481-
let mut usbox = UninitializedSandbox::new(
482-
GuestBinary::FilePath(callback_guest_as_string().expect("Guest Binary Missing")),
483-
None,
484-
)
485-
.unwrap();
486-
487-
// Make this host call run for 5 seconds
488-
489-
fn spin() -> Result<()> {
490-
thread::sleep(std::time::Duration::from_secs(5));
491-
Ok(())
492-
}
493-
494-
#[cfg(any(target_os = "windows", not(feature = "seccomp")))]
495-
usbox.register("Spin", spin).unwrap();
496-
497-
#[cfg(all(target_os = "linux", feature = "seccomp"))]
498-
usbox
499-
.register_with_extra_allowed_syscalls("Spin", spin, vec![libc::SYS_clock_nanosleep])
500-
.unwrap();
501-
502-
let sandbox: MultiUseSandbox = usbox.evolve(Noop::default()).unwrap();
503-
let mut ctx = sandbox.new_call_context();
504-
let result = ctx.call("CallHostSpin", ReturnType::Void, None);
505-
506-
assert!(result.is_err());
507-
match result.unwrap_err() {
508-
HyperlightError::GuestExecutionHungOnHostFunctionCall() => {}
509-
e => panic!(
510-
"Expected HyperlightError::GuestExecutionHungOnHostFunctionCall but got {:?}",
511-
e
512-
),
513-
}
514-
}
515-
516466
#[test]
517467
fn test_trigger_exception_on_guest() {
518468
let usbox = UninitializedSandbox::new(

src/hyperlight_host/src/signal_handlers/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
#[cfg(feature = "seccomp")]
1818
pub mod sigsys_signal_handler;
1919

20+
pub(crate) const INTERRUPT_VCPU_SIGRTMIN_OFFSET: i32 = 0;
21+
2022
pub(crate) fn setup_signal_handlers() -> crate::Result<()> {
2123
// This is unsafe because signal handlers only allow a very restrictive set of
2224
// functions (i.e., async-signal-safe functions) to be executed inside them.
@@ -45,7 +47,10 @@ pub(crate) fn setup_signal_handlers() -> crate::Result<()> {
4547
original_hook(panic_info);
4648
}));
4749
}
48-
vmm_sys_util::signal::register_signal_handler(libc::SIGRTMIN(), vm_kill_signal)?;
50+
vmm_sys_util::signal::register_signal_handler(
51+
libc::SIGRTMIN() + INTERRUPT_VCPU_SIGRTMIN_OFFSET,
52+
vm_kill_signal,
53+
)?;
4954

5055
// Note: For libraries registering signal handlers, it's important to keep in mind that
5156
// the user of the library could have their own signal handlers that we don't want to

0 commit comments

Comments
 (0)