Skip to content

Commit baf9912

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 baf9912

File tree

7 files changed

+360
-117
lines changed

7 files changed

+360
-117
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/hyperv_windows.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ impl HypervWindowsDriver {
109109
mem_regions,
110110
interrupt_handle: Arc::new(WindowsInterruptHandle {
111111
running: AtomicBool::new(false),
112+
cancel_requested: AtomicBool::new(false),
112113
partition_handle,
113114
dropped: AtomicBool::new(false),
114115
}),
@@ -409,7 +410,25 @@ impl Hypervisor for HypervWindowsDriver {
409410
#[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
410411
fn run(&mut self) -> Result<super::HyperlightExit> {
411412
self.interrupt_handle.running.store(true, Ordering::Relaxed);
412-
let exit_context: WHV_RUN_VP_EXIT_CONTEXT = self.processor.run()?;
413+
414+
// Don't run the vcpu is `cancel_requested` is true
415+
let exit_context = if self
416+
.interrupt_handle
417+
.cancel_requested
418+
.load(Ordering::Relaxed)
419+
{
420+
WHV_RUN_VP_EXIT_CONTEXT {
421+
ExitReason: WHV_RUN_VP_EXIT_REASON(8193i32), // WHvRunVpExitReasonCanceled
422+
VpContext: Default::default(),
423+
Anonymous: Default::default(),
424+
Reserved: Default::default(),
425+
}
426+
} else {
427+
self.processor.run()?
428+
};
429+
self.interrupt_handle
430+
.cancel_requested
431+
.store(false, Ordering::Relaxed);
413432
self.interrupt_handle
414433
.running
415434
.store(false, Ordering::Relaxed);
@@ -510,12 +529,14 @@ impl Drop for HypervWindowsDriver {
510529
pub struct WindowsInterruptHandle {
511530
// `WHvCancelRunVirtualProcessor()` will return Ok even if the vcpu is not running, which is the reason we need this flag.
512531
running: AtomicBool,
532+
cancel_requested: AtomicBool,
513533
partition_handle: WHV_PARTITION_HANDLE,
514534
dropped: AtomicBool,
515535
}
516536

517537
impl InterruptHandle for WindowsInterruptHandle {
518538
fn kill(&self) -> bool {
539+
self.cancel_requested.store(true, Ordering::Relaxed);
519540
self.running.load(Ordering::Relaxed)
520541
&& unsafe { WHvCancelRunVirtualProcessor(self.partition_handle, 0, 0).is_ok() }
521542
}

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
}

0 commit comments

Comments
 (0)