Skip to content

Commit b359722

Browse files
acatangiualxiord
authored andcommitted
vcpu: add support for kicking out of KVM_RUN
This is done by making use of the kvm `immediate_exit` flag. The vcpu is signaled and the `immediate_exit` flag is set in the signal handler. This leads to race-free interruption of KVM_RUN. In order to access the `immediate_exit` flag for a particular vcpu from a global context such as the signal handler, we use the TLS of that vcpu thread to gain access to the vcpu's data. This commit tries to keep the flow as safe as possible, and in that spirit, the following vcpu methods are defined: - `init_thread_local_data(&mut self)` - associates the `self` vcpu to the current thread. - `reset_thread_local_data(&mut self)` - deassociates the `self` vcpu from the current thread. - `Vcpu::run_on_thread_local<F>(fn: F)` - runs `fn` passing it a reference to the vcpu in TLS of local thread. This model enforces a limited lifetime (lifetime of `fn`) for the unsafe vcpu reference. - `Vcpu::register_kick_signal_handler()` - Registers a signal handler that sets kvm `immediate_exit` using `run_on_thread_local()` in order to kick the vcpu running on the current thread. Signed-off-by: Adrian Catangiu <[email protected]>
1 parent 7250788 commit b359722

File tree

2 files changed

+214
-7
lines changed

2 files changed

+214
-7
lines changed

src/vmm/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,8 @@ impl Vmm {
914914
"The number of vCPU fds is corrupted!"
915915
);
916916

917+
Vcpu::register_kick_signal_handler();
918+
917919
self.vcpus_handles.reserve(vcpu_count as usize);
918920

919921
let vcpus_thread_barrier = Arc::new(Barrier::new((vcpu_count + 1) as usize));

src/vmm/src/vstate.rs

Lines changed: 212 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
// Use of this source code is governed by a BSD-style license that can be
66
// found in the THIRD-PARTY file.
77

8+
use libc::{c_int, c_void, siginfo_t};
9+
use std::cell::Cell;
810
use std::io;
911
use std::result;
12+
use std::sync::atomic::{fence, Ordering};
1013
use std::sync::{Arc, Barrier};
1114

1215
use super::TimestampUs;
@@ -23,6 +26,7 @@ use kvm_ioctls::*;
2326
use logger::{LogOption, Metric, LOGGER, METRICS};
2427
use memory_model::{Address, GuestAddress, GuestMemory, GuestMemoryError};
2528
use utils::eventfd::EventFd;
29+
use utils::signal::{register_signal_handler, SignalHandler};
2630
#[cfg(target_arch = "x86_64")]
2731
use vmm_config::machine_config::{CpuFeaturesTemplate, VmConfig};
2832

@@ -34,6 +38,8 @@ const MAGIC_IOPORT_SIGNAL_GUEST_BOOT_COMPLETE: u64 = 0x03f0;
3438
const MAGIC_IOPORT_SIGNAL_GUEST_BOOT_COMPLETE: u64 = 0x40000000;
3539
const MAGIC_VALUE_SIGNAL_GUEST_BOOT_COMPLETE: u8 = 123;
3640

41+
pub(crate) const VCPU_RTSIG_OFFSET: i32 = 0;
42+
3743
/// Errors associated with the wrappers over KVM ioctls.
3844
#[derive(Debug)]
3945
pub enum Error {
@@ -86,6 +92,10 @@ pub enum Error {
8692
Irq(kvm_ioctls::Error),
8793
/// Cannot spawn a new vCPU thread.
8894
VcpuSpawn(io::Error),
95+
/// Cannot clean init vcpu TLS.
96+
VcpuTlsInit,
97+
/// Vcpu not present in TLS.
98+
VcpuTlsNotPresent,
8999
/// Unexpected KVM_RUN exit reason
90100
VcpuUnhandledKvmExit,
91101
#[cfg(target_arch = "aarch64")]
@@ -267,6 +277,9 @@ impl Vm {
267277
}
268278
}
269279

280+
// Using this for easier explicit type-casting to help IDEs interpret the code.
281+
type VcpuCell = Cell<Option<*const Vcpu>>;
282+
270283
/// A wrapper around creating and using a kvm-based VCPU.
271284
pub struct Vcpu {
272285
#[cfg(target_arch = "x86_64")]
@@ -282,6 +295,95 @@ pub struct Vcpu {
282295
}
283296

284297
impl Vcpu {
298+
thread_local!(static TLS_VCPU_PTR: VcpuCell = Cell::new(None));
299+
300+
/// Associates `self` with the current thread.
301+
///
302+
/// It is a prerequisite to successfully run `init_thread_local_data()` before using
303+
/// `run_on_thread_local()` on the current thread.
304+
/// This function will return an error if there already is a `Vcpu` present in the TLS.
305+
fn init_thread_local_data(&mut self) -> Result<()> {
306+
Self::TLS_VCPU_PTR.with(|cell: &VcpuCell| {
307+
if cell.get().is_some() {
308+
return Err(Error::VcpuTlsInit);
309+
}
310+
cell.set(Some(self as *const Vcpu));
311+
Ok(())
312+
})
313+
}
314+
315+
/// Deassociates `self` from the current thread.
316+
///
317+
/// Should be called if the current `self` had called `init_thread_local_data()` and
318+
/// now needs to move to a different thread.
319+
///
320+
/// Fails if `self` was not previously associated with the current thread.
321+
fn reset_thread_local_data(&mut self) -> Result<()> {
322+
// Best-effort to clean up TLS. If the `Vcpu` was moved to another thread
323+
// _before_ running this, then there is nothing we can do.
324+
Self::TLS_VCPU_PTR.with(|cell: &VcpuCell| {
325+
if let Some(vcpu_ptr) = cell.get() {
326+
if vcpu_ptr == self as *const Vcpu {
327+
Self::TLS_VCPU_PTR.with(|cell: &VcpuCell| cell.take());
328+
return Ok(());
329+
}
330+
}
331+
Err(Error::VcpuTlsNotPresent)
332+
})
333+
}
334+
335+
/// Runs `func` for the `Vcpu` associated with the current thread.
336+
///
337+
/// It requires that `init_thread_local_data()` was run on this thread.
338+
///
339+
/// Fails if there is no `Vcpu` associated with the current thread.
340+
///
341+
/// # Safety
342+
///
343+
/// This is marked unsafe as it allows temporary aliasing through
344+
/// dereferencing from pointer an already borrowed `Vcpu`.
345+
unsafe fn run_on_thread_local<F>(func: F) -> Result<()>
346+
where
347+
F: FnOnce(&Vcpu),
348+
{
349+
Self::TLS_VCPU_PTR.with(|cell: &VcpuCell| {
350+
if let Some(vcpu_ptr) = cell.get() {
351+
// Dereferencing here is safe since `TLS_VCPU_PTR` is populated/non-empty,
352+
// and it is being cleared on `Vcpu::drop` so there is no dangling pointer.
353+
let vcpu_ref: &Vcpu = &*vcpu_ptr;
354+
func(vcpu_ref);
355+
Ok(())
356+
} else {
357+
Err(Error::VcpuTlsNotPresent)
358+
}
359+
})
360+
}
361+
362+
/// Registers a signal handler which makes use of TLS and kvm immediate exit to
363+
/// kick the vcpu running on the current thread, if there is one.
364+
pub fn register_kick_signal_handler() {
365+
extern "C" fn handle_signal(_: c_int, _: *mut siginfo_t, _: *mut c_void) {
366+
// This is safe because it's temporarily aliasing the `Vcpu` object, but we are
367+
// only reading `vcpu.fd` which does not change for the lifetime of the `Vcpu`.
368+
unsafe {
369+
let _ = Vcpu::run_on_thread_local(|vcpu| {
370+
vcpu.fd.set_kvm_immediate_exit(1);
371+
fence(Ordering::Release);
372+
});
373+
}
374+
}
375+
unsafe {
376+
// This uses an async signal safe handler to kill the vcpu handles.
377+
register_signal_handler(
378+
VCPU_RTSIG_OFFSET,
379+
SignalHandler::Siginfo(handle_signal),
380+
true,
381+
libc::SA_SIGINFO,
382+
)
383+
.expect("Failed to register vcpu signal handler");
384+
}
385+
}
386+
285387
/// Constructs a new VCPU for `vm`.
286388
///
287389
/// # Arguments
@@ -439,29 +541,32 @@ impl Vcpu {
439541
}
440542
}
441543

442-
fn run_emulation(&mut self) -> Result<()> {
544+
/// Runs the vCPU in KVM context and handles the kvm exit reason.
545+
///
546+
/// Returns error or enum specifying whether emulation was handled or interrupted.
547+
fn run_emulation(&mut self) -> Result<VcpuEmulation> {
443548
match self.fd.run() {
444549
Ok(run) => match run {
445550
#[cfg(target_arch = "x86_64")]
446551
VcpuExit::IoIn(addr, data) => {
447552
self.io_bus.read(u64::from(addr), data);
448553
METRICS.vcpu.exit_io_in.inc();
449-
Ok(())
554+
Ok(VcpuEmulation::Handled)
450555
}
451556
#[cfg(target_arch = "x86_64")]
452557
VcpuExit::IoOut(addr, data) => {
453558
self.check_boot_complete_signal(u64::from(addr), data);
454559

455560
self.io_bus.write(u64::from(addr), data);
456561
METRICS.vcpu.exit_io_out.inc();
457-
Ok(())
562+
Ok(VcpuEmulation::Handled)
458563
}
459564
VcpuExit::MmioRead(addr, data) => {
460565
if let Some(ref mmio_bus) = self.mmio_bus {
461566
mmio_bus.read(addr, data);
462567
METRICS.vcpu.exit_mmio_read.inc();
463568
}
464-
Ok(())
569+
Ok(VcpuEmulation::Handled)
465570
}
466571
VcpuExit::MmioWrite(addr, data) => {
467572
if let Some(ref mmio_bus) = self.mmio_bus {
@@ -471,7 +576,7 @@ impl Vcpu {
471576
mmio_bus.write(addr, data);
472577
METRICS.vcpu.exit_mmio_write.inc();
473578
}
474-
Ok(())
579+
Ok(VcpuEmulation::Handled)
475580
}
476581
VcpuExit::Hlt => {
477582
info!("Received KVM_EXIT_HLT signal");
@@ -505,8 +610,12 @@ impl Vcpu {
505610
// error in our code in which case it is better to panic.
506611
Err(ref e) => {
507612
match e.errno() {
508-
// Why do we check for these if we only return EINVAL?
509-
libc::EAGAIN | libc::EINTR => Ok(()),
613+
libc::EAGAIN => Ok(VcpuEmulation::Handled),
614+
libc::EINTR => {
615+
self.fd.set_kvm_immediate_exit(0);
616+
// Notify that this KVM_RUN was interrupted.
617+
Ok(VcpuEmulation::Interrupted)
618+
}
510619
_ => {
511620
METRICS.vcpu.failures.inc();
512621
error!("Failure during vcpu run: {}", e);
@@ -528,6 +637,9 @@ impl Vcpu {
528637
seccomp_level: u32,
529638
vcpu_exit_evt: EventFd,
530639
) {
640+
self.init_thread_local_data()
641+
.expect("Cannot cleanly initialize vcpu TLS.");
642+
531643
// Load seccomp filters for this vCPU thread.
532644
// Execution panics if filters cannot be loaded, use --seccomp-level=0 if skipping filters
533645
// altogether is the desired behaviour.
@@ -550,12 +662,24 @@ impl Vcpu {
550662
}
551663
}
552664

665+
impl Drop for Vcpu {
666+
fn drop(&mut self) {
667+
let _ = self.reset_thread_local_data();
668+
}
669+
}
670+
671+
enum VcpuEmulation {
672+
Handled,
673+
Interrupted,
674+
}
675+
553676
#[cfg(test)]
554677
mod tests {
555678
use std::fs::File;
556679

557680
use super::super::devices;
558681
use super::*;
682+
use utils::signal::Killable;
559683

560684
// Auxiliary function being used throughout the tests.
561685
fn setup_vcpu() -> (Vm, Vcpu) {
@@ -740,4 +864,85 @@ mod tests {
740864
assert_eq!(m1.dev(), m2.dev());
741865
assert_eq!(m1.ino(), m2.ino());
742866
}
867+
868+
#[test]
869+
fn test_vcpu_tls() {
870+
let (_, mut vcpu) = setup_vcpu();
871+
872+
// Running on the TLS vcpu should fail before we actually initialize it.
873+
unsafe {
874+
assert!(Vcpu::run_on_thread_local(|_| ()).is_err());
875+
}
876+
877+
// Initialize vcpu TLS.
878+
vcpu.init_thread_local_data().unwrap();
879+
880+
// Validate TLS vcpu is the local vcpu by changing the `id` then validating against
881+
// the one in TLS.
882+
vcpu.id = 12;
883+
unsafe {
884+
assert!(Vcpu::run_on_thread_local(|v| assert_eq!(v.id, 12)).is_ok());
885+
}
886+
887+
// Reset vcpu TLS.
888+
assert!(vcpu.reset_thread_local_data().is_ok());
889+
890+
// Running on the TLS vcpu after TLS reset should fail.
891+
unsafe {
892+
assert!(Vcpu::run_on_thread_local(|_| ()).is_err());
893+
}
894+
895+
// Second reset should return error.
896+
assert!(vcpu.reset_thread_local_data().is_err());
897+
}
898+
899+
#[test]
900+
fn test_invalid_tls() {
901+
let (_, mut vcpu) = setup_vcpu();
902+
// Initialize vcpu TLS.
903+
vcpu.init_thread_local_data().unwrap();
904+
// Trying to initialize non-empty TLS should error.
905+
vcpu.init_thread_local_data().unwrap_err();
906+
}
907+
908+
#[test]
909+
fn test_vcpu_kick() {
910+
Vcpu::register_kick_signal_handler();
911+
let (vm, mut vcpu) = setup_vcpu();
912+
913+
let kvm_run =
914+
KvmRunWrapper::mmap_from_fd(&vcpu.fd, vm.fd.run_size()).expect("cannot mmap kvm-run");
915+
let success = Arc::new(std::sync::atomic::AtomicBool::new(false));
916+
let vcpu_success = success.clone();
917+
let barrier = Arc::new(Barrier::new(2));
918+
let vcpu_barrier = barrier.clone();
919+
// Start Vcpu thread which will be kicked with a signal.
920+
let handle = std::thread::Builder::new()
921+
.name("test_vcpu_kick".to_string())
922+
.spawn(move || {
923+
vcpu.init_thread_local_data().unwrap();
924+
// Notify TLS was populated.
925+
vcpu_barrier.wait();
926+
// Loop for max 1 second to check if the signal handler has run.
927+
for _ in 0..10 {
928+
if kvm_run.as_mut_ref().immediate_exit == 1 {
929+
// Signal handler has run and set immediate_exit to 1.
930+
vcpu_success.store(true, Ordering::Release);
931+
break;
932+
}
933+
std::thread::sleep(std::time::Duration::from_millis(100));
934+
}
935+
})
936+
.expect("cannot start thread");
937+
938+
// Wait for the vcpu to initialize its TLS.
939+
barrier.wait();
940+
// Kick the Vcpu using the custom signal.
941+
handle
942+
.kill(VCPU_RTSIG_OFFSET)
943+
.expect("failed to signal thread");
944+
handle.join().expect("failed to join thread");
945+
// Verify that the Vcpu saw its kvm immediate-exit as set.
946+
assert!(success.load(Ordering::Acquire));
947+
}
743948
}

0 commit comments

Comments
 (0)