// Copyright © 2019 Intel Corporation // // SPDX-License-Identifier: Apache-2.0 // use std::collections::HashMap; use std::fs::File; use std::io::{Read, Write, stdout}; use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; use std::panic::AssertUnwindSafe; #[cfg(feature = "guest_debug")] use std::path::PathBuf; use std::sync::mpsc::{Receiver, RecvError, SendError, Sender}; use std::sync::{Arc, Mutex}; use std::time::Duration; #[cfg(not(target_arch = "riscv64"))] use std::time::Instant; use std::{io, result, thread}; use anyhow::{Context, anyhow}; #[cfg(feature = "dbus_api")] use api::dbus::{DBusApiOptions, DBusApiShutdownChannels}; use api::http::HttpApiHandle; #[cfg(all(feature = "kvm", target_arch = "x86_64"))] use arch::x86_64::MAX_SUPPORTED_CPUS_LEGACY; use console_devices::{ConsoleInfo, pre_create_console_devices}; use event_monitor::event; use landlock::LandlockError; use libc::{EFD_NONBLOCK, SIGINT, SIGTERM, TCSANOW, tcsetattr, termios}; use log::{debug, error, info, trace, warn}; use memory_manager::MemoryManagerSnapshotData; use pci::PciBdf; use seccompiler::{SeccompAction, apply_filter}; use serde::ser::{SerializeStruct, Serializer}; use serde::{Deserialize, Serialize}; use signal_hook::iterator::{Handle, Signals}; use thiserror::Error; use tracer::trace_scoped; use vm_memory::GuestMemoryAtomic; use vm_memory::bitmap::AtomicBitmap; use vm_migration::protocol::*; use vm_migration::{ MemoryMigrationContext, Migratable, MigratableError, OngoingMigrationContext, Pausable, Snapshot, Snapshottable, Transportable, }; use vmm_sys_util::eventfd::EventFd; use vmm_sys_util::signal::unblock_signal; use vmm_sys_util::sock_ctrl_msg::ScmSocket; use crate::api::{ ApiRequest, ApiResponse, RequestHandler, TimeoutStrategy, VmInfoResponse, VmReceiveMigrationData, VmSendMigrationData, VmmPingResponse, }; use crate::config::{MemoryRestoreMode, RestoreConfig, add_to_config}; #[cfg(all(target_arch = "x86_64", feature = "guest_debug"))] use crate::coredump::GuestDebuggable; use crate::landlock::Landlock; use crate::memory_manager::MemoryManager; #[cfg(all(feature = "kvm", target_arch = "x86_64"))] use crate::migration::get_vm_snapshot; use crate::migration::{recv_vm_config, recv_vm_state}; use crate::migration_transport::{ ReceiveAdditionalConnections, ReceiveListener, SendAdditionalConnections, SocketStream, }; use crate::seccomp_filters::{Thread, get_seccomp_filter}; use crate::vm::{Error as VmError, Vm, VmState}; use crate::vm_config::{ DeviceConfig, DiskConfig, FsConfig, GenericVhostUserConfig, NetConfig, PmemConfig, UserDeviceConfig, VdpaConfig, VmConfig, VsockConfig, }; mod acpi; pub mod api; mod clone3; pub mod config; pub mod console_devices; #[cfg(all(target_arch = "x86_64", feature = "guest_debug"))] mod coredump; pub mod cpu; pub mod device_manager; pub mod device_tree; #[cfg(feature = "guest_debug")] mod gdb; #[cfg(feature = "igvm")] mod igvm; pub mod interrupt; pub mod landlock; pub mod memory_manager; pub mod migration; pub mod migration_transport; mod pci_segment; pub mod seccomp_filters; mod serial_manager; mod sigwinch_listener; mod sync_utils; mod uffd; mod userfaultfd; pub mod vm; pub mod vm_config; type GuestMemoryMmap = vm_memory::GuestMemoryMmap; type GuestRegionMmap = vm_memory::GuestRegionMmap; /// Errors associated with VMM management #[derive(Debug, Error)] pub enum Error { /// API request receive error #[error("Error receiving API request")] ApiRequestRecv(#[source] RecvError), /// API response send error #[error("Error sending API request")] ApiResponseSend(#[source] SendError), /// Cannot bind to the UNIX domain socket path #[error("Error binding to UNIX domain socket")] Bind(#[source] io::Error), /// Cannot clone EventFd. #[error("Error cloning EventFd")] EventFdClone(#[source] io::Error), /// Cannot create EventFd. #[error("Error creating EventFd")] EventFdCreate(#[source] io::Error), /// Cannot read from EventFd. #[error("Error reading from EventFd")] EventFdRead(#[source] io::Error), /// Cannot create epoll context. #[error("Error creating epoll context")] Epoll(#[source] io::Error), /// Cannot create HTTP thread #[error("Error spawning HTTP thread")] HttpThreadSpawn(#[source] io::Error), /// Cannot create D-Bus thread #[cfg(feature = "dbus_api")] #[error("Error spawning D-Bus thread")] DBusThreadSpawn(#[source] io::Error), /// Cannot start D-Bus session #[cfg(feature = "dbus_api")] #[error("Error starting D-Bus session")] CreateDBusSession(#[source] zbus::Error), /// Cannot create `event-monitor` thread #[error("Error spawning `event-monitor` thread")] EventMonitorThreadSpawn(#[source] io::Error), /// Cannot handle the VM STDIN stream #[error("Error handling VM stdin")] Stdin(#[source] VmError), /// Cannot handle the VM pty stream #[error("Error handling VM pty")] Pty(#[source] VmError), /// Cannot reboot the VM #[error("Error rebooting VM")] VmReboot(#[source] VmError), /// Cannot create VMM thread #[error("Error spawning VMM thread")] VmmThreadSpawn(#[source] io::Error), /// Cannot shut the VMM down #[error("Error shutting down VMM")] VmmShutdown(#[source] VmError), /// Cannot create seccomp filter #[error("Error creating seccomp filter")] CreateSeccompFilter(#[source] seccompiler::Error), /// Cannot apply seccomp filter #[error("Error applying seccomp filter")] ApplySeccompFilter(#[source] seccompiler::Error), /// Error activating virtio devices #[error("Error activating virtio devices")] ActivateVirtioDevices(#[source] VmError), /// Error creating API server // TODO We should add #[source] here once the type implements Error. // Then we also can remove the `: {}` to align with the other errors. #[error("Error creating API server: {0}")] CreateApiServer(micro_http::ServerError), /// Error binding API server socket #[error("Error creation API server's socket")] CreateApiServerSocket(#[source] io::Error), #[cfg(feature = "guest_debug")] #[error("Failed to start the GDB thread")] GdbThreadSpawn(#[source] io::Error), /// GDB request receive error #[cfg(feature = "guest_debug")] #[error("Error receiving GDB request")] GdbRequestRecv(#[source] RecvError), /// GDB response send error #[cfg(feature = "guest_debug")] #[error("Error sending GDB request")] GdbResponseSend(#[source] SendError), #[error("Cannot spawn a signal handler thread")] SignalHandlerSpawn(#[source] io::Error), #[error("Failed to join on threads: {0:?}")] ThreadCleanup(std::boxed::Box), /// Cannot create Landlock object #[error("Error creating landlock object")] CreateLandlock(#[source] LandlockError), /// Cannot apply landlock based sandboxing #[error("Error applying landlock")] ApplyLandlock(#[source] LandlockError), } impl From<&VmConfig> for hypervisor::HypervisorVmConfig { fn from(_value: &VmConfig) -> Self { hypervisor::HypervisorVmConfig { #[cfg(feature = "tdx")] tdx_enabled: _value.platform.as_ref().is_some_and(|p| p.tdx), #[cfg(feature = "sev_snp")] sev_snp_enabled: _value.is_sev_snp_enabled(), #[cfg(feature = "sev_snp")] mem_size: _value.memory.total_size(), nested: _value.cpus.nested, smt_enabled: _value .cpus .topology .as_ref() .is_some_and(|t| t.threads_per_core > 1), } } } pub type Result = result::Result; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u64)] pub enum EpollDispatch { Exit = 0, Reset = 1, Api = 2, ActivateVirtioDevices = 3, Debug = 4, Unknown, } impl From for EpollDispatch { fn from(v: u64) -> Self { use EpollDispatch::*; match v { 0 => Exit, 1 => Reset, 2 => Api, 3 => ActivateVirtioDevices, 4 => Debug, _ => Unknown, } } } pub struct EpollContext { epoll_file: File, } impl EpollContext { pub fn new() -> result::Result { let epoll_fd = epoll::create(true)?; // Use 'File' to enforce closing on 'epoll_fd' // SAFETY: the epoll_fd returned by epoll::create is valid and owned by us. let epoll_file = unsafe { File::from_raw_fd(epoll_fd) }; Ok(EpollContext { epoll_file }) } pub fn add_event(&mut self, fd: &T, token: EpollDispatch) -> result::Result<(), io::Error> where T: AsRawFd, { let dispatch_index = token as u64; epoll::ctl( self.epoll_file.as_raw_fd(), epoll::ControlOptions::EPOLL_CTL_ADD, fd.as_raw_fd(), epoll::Event::new(epoll::Events::EPOLLIN, dispatch_index), )?; Ok(()) } #[cfg(fuzzing)] pub fn add_event_custom( &mut self, fd: &T, id: u64, evts: epoll::Events, ) -> result::Result<(), io::Error> where T: AsRawFd, { epoll::ctl( self.epoll_file.as_raw_fd(), epoll::ControlOptions::EPOLL_CTL_ADD, fd.as_raw_fd(), epoll::Event::new(evts, id), )?; Ok(()) } } impl AsRawFd for EpollContext { fn as_raw_fd(&self) -> RawFd { self.epoll_file.as_raw_fd() } } pub struct PciDeviceInfo { pub id: String, pub bdf: PciBdf, } impl Serialize for PciDeviceInfo { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { let bdf_str = self.bdf.to_string(); // Serialize the structure. let mut state = serializer.serialize_struct("PciDeviceInfo", 2)?; state.serialize_field("id", &self.id)?; state.serialize_field("bdf", &bdf_str)?; state.end() } } pub fn feature_list() -> Vec { vec![ #[cfg(feature = "dbus_api")] "dbus_api".to_string(), #[cfg(feature = "dhat-heap")] "dhat-heap".to_string(), #[cfg(feature = "fw_cfg")] "fw_cfg".to_string(), #[cfg(feature = "guest_debug")] "guest_debug".to_string(), #[cfg(feature = "igvm")] "igvm".to_string(), #[cfg(feature = "io_uring")] "io_uring".to_string(), #[cfg(feature = "kvm")] "kvm".to_string(), #[cfg(feature = "mshv")] "mshv".to_string(), #[cfg(feature = "sev_snp")] "sev_snp".to_string(), #[cfg(feature = "tdx")] "tdx".to_string(), #[cfg(feature = "tracing")] "tracing".to_string(), #[cfg(feature = "ivshmem")] "ivshmem".to_string(), ] } pub fn start_event_monitor_thread( mut monitor: event_monitor::Monitor, seccomp_action: &SeccompAction, landlock_enable: bool, hypervisor_type: hypervisor::HypervisorType, exit_event: EventFd, ) -> Result>> { // Retrieve seccomp filter let seccomp_filter = get_seccomp_filter(seccomp_action, Thread::EventMonitor, hypervisor_type) .map_err(Error::CreateSeccompFilter)?; thread::Builder::new() .name("event-monitor".to_owned()) .spawn(move || { // Apply seccomp filter if !seccomp_filter.is_empty() { apply_filter(&seccomp_filter) .map_err(Error::ApplySeccompFilter) .inspect_err(|e| { error!("Error applying seccomp filter: {e:?}"); exit_event.write(1).ok(); })?; } if landlock_enable { Landlock::new() .map_err(Error::CreateLandlock)? .restrict_self() .map_err(Error::ApplyLandlock) .inspect_err(|e| { error!("Error applying landlock to event monitor thread: {e:?}"); exit_event.write(1).ok(); })?; } std::panic::catch_unwind(AssertUnwindSafe(move || { while let Ok(event) = monitor.rx.recv() { let event = Arc::new(event); if let Some(ref mut file) = monitor.file { file.write_all(event.as_bytes().as_ref()).ok(); file.write_all(b"\n\n").ok(); } for tx in monitor.broadcast.iter() { tx.send(event.clone()).ok(); } } })) .map_err(|_| { error!("`event-monitor` thread panicked"); exit_event.write(1).ok(); }) .ok(); Ok(()) }) .map_err(Error::EventMonitorThreadSpawn) } #[allow(unused_variables)] #[allow(clippy::too_many_arguments)] pub fn start_vmm_thread( vmm_version: VmmVersionInfo, http_path: &Option, http_fd: Option, #[cfg(feature = "dbus_api")] dbus_options: Option, api_event: EventFd, api_sender: Sender, api_receiver: Receiver, #[cfg(feature = "guest_debug")] debug_path: Option, #[cfg(feature = "guest_debug")] debug_event: EventFd, #[cfg(feature = "guest_debug")] vm_debug_event: EventFd, exit_event: EventFd, seccomp_action: &SeccompAction, hypervisor: Arc, landlock_enable: bool, ) -> Result { #[cfg(feature = "guest_debug")] let gdb_hw_breakpoints = hypervisor.get_guest_debug_hw_bps(); #[cfg(feature = "guest_debug")] let (gdb_sender, gdb_receiver) = std::sync::mpsc::channel(); #[cfg(feature = "guest_debug")] let gdb_debug_event = debug_event.try_clone().map_err(Error::EventFdClone)?; #[cfg(feature = "guest_debug")] let gdb_vm_debug_event = vm_debug_event.try_clone().map_err(Error::EventFdClone)?; let api_event_clone = api_event.try_clone().map_err(Error::EventFdClone)?; let hypervisor_type = hypervisor.hypervisor_type(); // Retrieve seccomp filter let vmm_seccomp_filter = get_seccomp_filter(seccomp_action, Thread::Vmm, hypervisor_type) .map_err(Error::CreateSeccompFilter)?; let vmm_seccomp_action = seccomp_action.clone(); let thread = { let exit_event = exit_event.try_clone().map_err(Error::EventFdClone)?; thread::Builder::new() .name("vmm".to_string()) .spawn(move || { // Apply seccomp filter for VMM thread. if !vmm_seccomp_filter.is_empty() { apply_filter(&vmm_seccomp_filter).map_err(Error::ApplySeccompFilter)?; } let mut vmm = Vmm::new( vmm_version, api_event, #[cfg(feature = "guest_debug")] debug_event, #[cfg(feature = "guest_debug")] vm_debug_event, vmm_seccomp_action, hypervisor, exit_event, )?; vmm.setup_signal_handler(landlock_enable)?; vmm.control_loop( &api_receiver, #[cfg(feature = "guest_debug")] &gdb_receiver, ) }) .map_err(Error::VmmThreadSpawn)? }; // The VMM thread is started, we can start the dbus thread // and start serving HTTP requests #[cfg(feature = "dbus_api")] let dbus_shutdown_chs = match dbus_options { Some(opts) => { let (_, chs) = api::start_dbus_thread( opts, api_event_clone.try_clone().map_err(Error::EventFdClone)?, api_sender.clone(), seccomp_action, exit_event.try_clone().map_err(Error::EventFdClone)?, hypervisor_type, )?; Some(chs) } None => None, }; let http_api_handle = if let Some(http_path) = http_path { Some(api::start_http_path_thread( http_path, api_event_clone, api_sender, seccomp_action, exit_event, hypervisor_type, landlock_enable, )?) } else if let Some(http_fd) = http_fd { Some(api::start_http_fd_thread( http_fd, api_event_clone, api_sender, seccomp_action, exit_event, hypervisor_type, landlock_enable, )?) } else { None }; #[cfg(feature = "guest_debug")] if let Some(debug_path) = debug_path { let target = gdb::GdbStub::new( gdb_sender, gdb_debug_event, gdb_vm_debug_event, gdb_hw_breakpoints, ); thread::Builder::new() .name("gdb".to_owned()) .spawn(move || gdb::gdb_thread(target, &debug_path)) .map_err(Error::GdbThreadSpawn)?; } Ok(VmmThreadHandle { thread_handle: thread, #[cfg(feature = "dbus_api")] dbus_shutdown_chs, http_api_handle, }) } /// Measures the time of the callback, in case it returns `Ok`. fn measure_ok(f: F) -> result::Result<(T, Duration), E> where F: FnOnce() -> result::Result, { let begin = Instant::now(); let value = f()?; let duration = begin.elapsed(); Ok((value, duration)) } #[derive(Clone, Deserialize, Serialize)] struct VmMigrationConfig { vm_config: Arc>, #[cfg(all(feature = "kvm", target_arch = "x86_64"))] common_cpuid: Vec, memory_manager_data: MemoryManagerSnapshotData, } #[derive(Debug, Clone)] pub struct VmmVersionInfo { pub build_version: String, pub version: String, } impl VmmVersionInfo { pub fn new(build_version: &str, version: &str) -> Self { Self { build_version: build_version.to_owned(), version: version.to_owned(), } } } pub struct VmmThreadHandle { pub thread_handle: thread::JoinHandle>, #[cfg(feature = "dbus_api")] pub dbus_shutdown_chs: Option, pub http_api_handle: Option, } pub struct Vmm { epoll: EpollContext, exit_evt: EventFd, reset_evt: EventFd, api_evt: EventFd, #[cfg(feature = "guest_debug")] debug_evt: EventFd, #[cfg(feature = "guest_debug")] vm_debug_evt: EventFd, version: VmmVersionInfo, vm: Option, vm_config: Option>>, seccomp_action: SeccompAction, hypervisor: Arc, activate_evt: EventFd, signals: Option, threads: Vec>, original_termios_opt: Arc>>, console_resize_pipe: Option>, console_info: Option, } /// Just a wrapper for the data that goes into /// [`ReceiveMigrationState::Configured`] struct ReceiveMigrationConfiguredData { memory_manager: Arc>, guest_memory: GuestMemoryAtomic, connections: ReceiveAdditionalConnections, } /// The receiver's state machine behind the migration protocol. enum ReceiveMigrationState { /// The connection is established and we haven't received any commands yet. Established, /// We received the start command. Started, /// We received file descriptors for memory. This can only happen on UNIX domain sockets. MemoryFdsReceived(Vec<(u32, File)>), /// We received the VM configuration. We keep a direct reference to the guest memory /// around to populate it without having to acquire a lock (which we would have to do /// when accessing the memory through the memory manager). /// /// We keep the memory manager around to pass it into the next state. From this point /// on, the sender can start sending memory updates. Configured(ReceiveMigrationConfiguredData), /// Memory is populated and we received the state. The VM is ready to go. StateReceived { /// The timestamp where the VMM started to receive the final state. state_receive_begin: Instant, }, /// The migration is successful. Completed, /// The migration couldn't complete, either due to an error or because the sender abandoned the migration. Aborted, } impl ReceiveMigrationState { fn finished(&self) -> bool { matches!( self, ReceiveMigrationState::Completed | ReceiveMigrationState::Aborted ) } } impl Vmm { pub const HANDLED_SIGNALS: [i32; 2] = [SIGTERM, SIGINT]; fn signal_handler( mut signals: Signals, original_termios_opt: &Mutex>, exit_evt: &EventFd, ) { for sig in &Self::HANDLED_SIGNALS { unblock_signal(*sig).unwrap(); } for signal in signals.forever() { match signal { #[allow(clippy::collapsible_match)] SIGTERM | SIGINT => { if exit_evt.write(1).is_err() { // Resetting the terminal is usually done as the VMM exits if let Ok(lock) = original_termios_opt.lock() { if let Some(termios) = *lock { // SAFETY: FFI call let _ = unsafe { tcsetattr(stdout().lock().as_raw_fd(), TCSANOW, &termios) }; } } else { warn!("Failed to lock original termios"); } std::process::exit(1); } } _ => (), } } } fn setup_signal_handler(&mut self, landlock_enable: bool) -> Result<()> { let signals = Signals::new(Self::HANDLED_SIGNALS); match signals { Ok(signals) => { self.signals = Some(signals.handle()); let exit_evt = self.exit_evt.try_clone().map_err(Error::EventFdClone)?; let original_termios_opt = Arc::clone(&self.original_termios_opt); let signal_handler_seccomp_filter = get_seccomp_filter( &self.seccomp_action, Thread::SignalHandler, self.hypervisor.hypervisor_type(), ) .map_err(Error::CreateSeccompFilter)?; self.threads.push( thread::Builder::new() .name("vmm_signal_handler".to_string()) .spawn(move || { if !signal_handler_seccomp_filter.is_empty() && let Err(e) = apply_filter(&signal_handler_seccomp_filter) .map_err(Error::ApplySeccompFilter) { error!("Error applying seccomp filter: {e:?}"); exit_evt.write(1).ok(); return; } if landlock_enable{ match Landlock::new() { Ok(landlock) => { let _ = landlock.restrict_self().map_err(Error::ApplyLandlock).map_err(|e| { error!("Error applying Landlock to signal handler thread: {e:?}"); exit_evt.write(1).ok(); }); } Err(e) => { error!("Error creating Landlock object: {e:?}"); exit_evt.write(1).ok(); } } } std::panic::catch_unwind(AssertUnwindSafe(|| { Vmm::signal_handler(signals, original_termios_opt.as_ref(), &exit_evt); })) .map_err(|_| { error!("vmm signal_handler thread panicked"); exit_evt.write(1).ok() }) .ok(); }) .map_err(Error::SignalHandlerSpawn)?, ); } Err(e) => error!("Signal not found {e}"), } Ok(()) } #[allow(clippy::too_many_arguments)] fn new( vmm_version: VmmVersionInfo, api_evt: EventFd, #[cfg(feature = "guest_debug")] debug_evt: EventFd, #[cfg(feature = "guest_debug")] vm_debug_evt: EventFd, seccomp_action: SeccompAction, hypervisor: Arc, exit_evt: EventFd, ) -> Result { let mut epoll = EpollContext::new().map_err(Error::Epoll)?; let reset_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::EventFdCreate)?; let activate_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::EventFdCreate)?; epoll .add_event(&exit_evt, EpollDispatch::Exit) .map_err(Error::Epoll)?; epoll .add_event(&reset_evt, EpollDispatch::Reset) .map_err(Error::Epoll)?; epoll .add_event(&activate_evt, EpollDispatch::ActivateVirtioDevices) .map_err(Error::Epoll)?; epoll .add_event(&api_evt, EpollDispatch::Api) .map_err(Error::Epoll)?; #[cfg(feature = "guest_debug")] epoll .add_event(&debug_evt, EpollDispatch::Debug) .map_err(Error::Epoll)?; Ok(Vmm { epoll, exit_evt, reset_evt, api_evt, #[cfg(feature = "guest_debug")] debug_evt, #[cfg(feature = "guest_debug")] vm_debug_evt, version: vmm_version, vm: None, vm_config: None, seccomp_action, hypervisor, activate_evt, signals: None, threads: vec![], original_termios_opt: Arc::new(Mutex::new(None)), console_resize_pipe: None, console_info: None, }) } /// Try to receive a file descriptor from a socket. Returns the slot number and the file descriptor. fn vm_receive_memory_fd( socket: &mut SocketStream, ) -> std::result::Result<(u32, File), MigratableError> { if let SocketStream::Unix(unix_socket) = socket { let mut buf = [0u8; 4]; let (_, file) = unix_socket.recv_with_fd(&mut buf).map_err(|e| { MigratableError::MigrateReceive(anyhow!("Error receiving slot from socket: {e}")) })?; file.ok_or_else(|| MigratableError::MigrateReceive(anyhow!("Failed to receive socket"))) .map(|file| (u32::from_le_bytes(buf), file)) } else { Err(MigratableError::MigrateReceive(anyhow!( "Unsupported socket type" ))) } } /// Handle a migration command and advance the protocol state machine. /// /// **Note**: This function is responsible for consuming any payloads! It also must /// _not_ write any response to the socket. fn vm_receive_migration_step( &mut self, socket: &mut SocketStream, listener: &ReceiveListener, state: ReceiveMigrationState, req: &Request, _receive_data_migration: &VmReceiveMigrationData, ) -> std::result::Result { use ReceiveMigrationState::*; let invalid_command = || { Err(MigratableError::MigrateReceive(anyhow!( "Can't handle command in current state" ))) }; let mut configure_vm = |socket: &mut SocketStream, memory_files: HashMap| -> std::result::Result { let memory_manager = self.vm_receive_config(req, socket, memory_files)?; let guest_memory = memory_manager.lock().unwrap().guest_memory(); // Create the additional-connection receiver even in the single-connection case. // At this point the receiver does not know whether the sender will use extra TCP // connections. If it does not, no worker connections are accepted and memory // requests continue to arrive on the main connection. let connections = listener .try_clone() .and_then(|l| ReceiveAdditionalConnections::new(l, guest_memory.clone()))?; Ok(ReceiveMigrationConfiguredData { memory_manager, guest_memory, connections, }) }; let recv_memory_fd = |socket: &mut SocketStream, mut memory_files: Vec<(u32, File)>| -> std::result::Result, MigratableError> { let (slot, file) = Self::vm_receive_memory_fd(socket)?; memory_files.push((slot, file)); Ok(memory_files) }; if req.command() == Command::Abandon { info!("Abandon Command Received"); return Ok(Aborted); } match state { Established => match req.command() { Command::Start => Ok(Started), _ => invalid_command(), }, Started => match req.command() { Command::MemoryFd => recv_memory_fd(socket, Vec::new()).map(MemoryFdsReceived), Command::Config => configure_vm(socket, Default::default()).map(Configured), _ => invalid_command(), }, MemoryFdsReceived(memory_files) => match req.command() { Command::MemoryFd => recv_memory_fd(socket, memory_files).map(MemoryFdsReceived), Command::Config => { configure_vm(socket, HashMap::from_iter(memory_files)).map(Configured) } _ => invalid_command(), }, Configured(mut config_data) => match req.command() { // Memory commands use the main connection only in the single-connection case. // When multiple TCP connections are configured, the worker connections carry // all memory commands and the main connection is used only for control traffic. Command::Memory => { migration_transport::receive_memory_ranges( &config_data.guest_memory, req, socket, ) .inspect_err(|_| { // connections.cleanup() already logs all errors that occurred in one of the // threads. Furthermore, this path is only taken in the single-connection case, // thus we do not expect any errors during this cleanup. The warning should // reflect that. if let Err(e) = config_data.connections.cleanup() { warn!( "Unexpected error while cleaning up migration connections after a main-connection memory receive failure: {e}" ); } })?; Ok(Configured(config_data)) } Command::State => { let state_receive_begin = Instant::now(); config_data.connections.cleanup()?; let (recv_state_dur, restore_vm_dur) = self.vm_receive_state(req, socket, config_data.memory_manager)?; debug!( "Migration (incoming): recv_snapshot:{}ms restore:{}ms", recv_state_dur.as_millis(), restore_vm_dur.as_millis(), ); Ok(StateReceived { state_receive_begin, }) } _ => invalid_command(), }, StateReceived { state_receive_begin, } => match req.command() { Command::Complete => { // The unwrap is safe, because the state machine makes sure we called // vm_receive_state before, which creates the VM. let vm = self.vm.as_mut().unwrap(); let (_, resume_duration) = measure_ok(|| vm.resume())?; debug!( "Migration (incoming): resume:{}ms", resume_duration.as_millis() ); // This logs the downtime without the final memory delta, so // it does not reflect the actual downtime. While we could // pass along the timestamp from when the VM was paused, // that would rely on both VM hosts having synchronized // clocks, which we cannot guarantee. For that reason, this // is logged as debug! rather than info!. debug!( "Migration (incoming): Receiving final state and resuming the VM took {}ms", state_receive_begin.elapsed().as_millis() ); Ok(Completed) } _ => invalid_command(), }, Completed | Aborted => { unreachable!("Performed a step on the finished state machine") } } } fn vm_receive_config( &mut self, req: &Request, socket: &mut T, existing_memory_files: HashMap, ) -> std::result::Result>, MigratableError> where T: Read, { // Read in config data along with memory manager data let mut data: Vec = Vec::new(); data.resize_with(req.length() as usize, Default::default); socket .read_exact(&mut data) .map_err(MigratableError::MigrateSocket)?; let vm_migration_config: VmMigrationConfig = serde_json::from_slice(&data).map_err(|e| { MigratableError::MigrateReceive(anyhow!("Error deserialising config: {e}")) })?; #[cfg(all(feature = "kvm", target_arch = "x86_64"))] self.vm_check_cpuid_compatibility( &vm_migration_config.vm_config, &vm_migration_config.common_cpuid, )?; let config = vm_migration_config.vm_config.clone(); self.vm_config = Some(vm_migration_config.vm_config); self.console_info = Some(pre_create_console_devices(self).map_err(|e| { MigratableError::MigrateReceive(anyhow!("Error creating console devices: {e:?}")) })?); if self .vm_config .as_ref() .unwrap() .lock() .unwrap() .landlock_enable { let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); apply_landlock(&mut config).map_err(|e| { MigratableError::MigrateReceive(anyhow!("Error applying landlock: {e:?}")) })?; } let vm = Vm::create_hypervisor_vm( self.hypervisor.as_ref(), (&*self.vm_config.as_ref().unwrap().lock().unwrap()).into(), ) .map_err(|e| { MigratableError::MigrateReceive(anyhow!( "Error creating hypervisor VM from snapshot: {e:?}" )) })?; #[cfg(all(feature = "kvm", target_arch = "x86_64"))] if config.lock().unwrap().max_apic_id() > MAX_SUPPORTED_CPUS_LEGACY { vm.enable_x2apic_api().unwrap(); } let phys_bits = vm::physical_bits( self.hypervisor.as_ref(), config.lock().unwrap().cpus.max_phys_bits, ); let memory_manager = MemoryManager::new( vm, &config.lock().unwrap().memory.clone(), None, phys_bits, #[cfg(feature = "tdx")] false, Some(&vm_migration_config.memory_manager_data), existing_memory_files, ) .map_err(|e| { MigratableError::MigrateReceive(anyhow!( "Error creating MemoryManager from snapshot: {e:?}" )) })?; Ok(memory_manager) } /// Receives the final VM state (devices, vCPUs) and restores the VM. /// /// Measures the time for each step. fn vm_receive_state( &mut self, req: &Request, socket: &mut T, mm: Arc>, ) -> std::result::Result< ( Duration, /* state receive + deserialize */ Duration, /* restoring */ ), MigratableError, > where T: Read, { let (snapshot, receive_duration): (Snapshot, Duration) = measure_ok(|| { let mut data: Vec = Vec::new(); data.resize_with(req.length() as usize, Default::default); socket .read_exact(&mut data) .map_err(MigratableError::MigrateSocket)?; serde_json::from_slice(&data).map_err(|e| { MigratableError::MigrateReceive(anyhow!("Error deserialising snapshot: {e}")) }) })?; let exit_evt = self.exit_evt.try_clone().map_err(|e| { MigratableError::MigrateReceive(anyhow!("Error cloning exit EventFd: {e}")) })?; let reset_evt = self.reset_evt.try_clone().map_err(|e| { MigratableError::MigrateReceive(anyhow!("Error cloning reset EventFd: {e}")) })?; #[cfg(feature = "guest_debug")] let debug_evt = self.vm_debug_evt.try_clone().map_err(|e| { MigratableError::MigrateReceive(anyhow!("Error cloning debug EventFd: {e}")) })?; let activate_evt = self.activate_evt.try_clone().map_err(|e| { MigratableError::MigrateReceive(anyhow!("Error cloning activate EventFd: {e}")) })?; let (vm, restore_duration) = measure_ok(|| { #[cfg(not(target_arch = "riscv64"))] let timestamp = Instant::now(); let hypervisor_vm = mm.lock().unwrap().vm.clone(); let mut vm = Vm::new_from_memory_manager( self.vm_config.clone().unwrap(), mm, hypervisor_vm, exit_evt, reset_evt, #[cfg(feature = "guest_debug")] debug_evt, &self.seccomp_action, self.hypervisor.clone(), activate_evt, #[cfg(not(target_arch = "riscv64"))] timestamp, self.console_info.clone(), self.console_resize_pipe.clone(), Arc::clone(&self.original_termios_opt), Some(&snapshot), ) .map_err(|e| { MigratableError::MigrateReceive(anyhow!("Error creating VM from snapshot: {e:?}")) })?; // Create VM vm.restore().map_err(|e| { MigratableError::MigrateReceive(anyhow!("Failed restoring the Vm: {e}")) })?; Ok(vm) })?; self.vm = Some(vm); Ok((receive_duration, restore_duration)) } /// Performs the initial memory transmission (iteration zero) plus a /// variable number of memory iterations with the goal to eventually migrate /// the VM in a reasonably small downtime. /// /// This returns as soon as the precopy migration indicates it is converged /// (e.g., reasonably small downtime) is reached. fn do_memory_iterations( vm: &mut Vm, socket: &mut SocketStream, ctx: &mut MemoryMigrationContext, is_converged: impl Fn(&MemoryMigrationContext) -> result::Result, mem_send: &mut SendAdditionalConnections, ) -> result::Result { loop { let iteration_begin = Instant::now(); let iteration_table = if ctx.iteration == 0 { vm.memory_range_table()? } else { // TODO do this in a thread #7816 vm.dirty_log()? }; ctx.update_metrics_before_transfer(iteration_begin, &iteration_table); if is_converged(ctx)? { debug!("Precopy converged: {ctx}"); break Ok(iteration_table); } // Send the current dirty pages let transfer_begin = Instant::now(); mem_send.send_memory(iteration_table, socket)?; let transfer_duration = transfer_begin.elapsed(); ctx.update_metrics_after_transfer(transfer_begin, transfer_duration); // Log progress of the current iteration debug!("Precopy: {ctx}"); // Enables management software (e.g., libvirt) to easily track forward progress. event!( "vm", "migration-memory-iteration", "id", ctx.iteration.to_string() ); // Increment iteration last: This way we ensure that the logging // above matches the actual iteration. ctx.iteration += 1; } } /// Checks whether the precopy memory migration has converged and it is safe /// to proceed to the final (paused) memory iteration. /// /// Once this returns, the VM is expected to stop as soon as possible. /// /// Convergence is reached when any of the following criteria is met: /// /// 1. **No dirty pages remain** – the current iteration would transfer zero /// bytes. /// 2. **Downtime budget is met** – the estimated downtime for the final /// (paused) iteration is within the caller-specified /// [`VmSendMigrationData::downtime`] budget. /// 3. **Timeout** – the precopy phase has been running for at least /// [`VmSendMigrationData::timeout`]. The outcome depends on /// [`VmSendMigrationData::timeout_strategy`]: /// - [`TimeoutStrategy::Cancel`] – returns /// - [`TimeoutStrategy::Ignore`] – the migration completes despite not /// meeting the downtime budget. /// [`MigratableError::MigrateSend`] so the caller can abort the /// migration cleanly. /// /// # Returns /// /// * `Ok(true)` – convergence criterion met; the caller should stop precopy /// iterations. /// * `Ok(false)` – not yet converged; the caller should run another /// dirty-page iteration. /// * `Err(_)` – the timeout was reached and [`TimeoutStrategy::Cancel`] /// is in effect. fn is_precopy_converged( ctx: &MemoryMigrationContext, send_data_migration: &VmSendMigrationData, ) -> result::Result { if ctx.current_iteration_total_bytes == 0 { debug!("Precopy: No more memory to transfer"); return Ok(true); } // We currently ignore the time required to transfer the final // VM state (device state and vCPUs) and the time needed on the // receiver to create the VM and initialize its data structures // before execution can resume. // // Manual testing showed that migrating an idle VM on a modern // AMD CPU (CHV release build) adds ~5 ms of overhead when // scaling from 1 to 200 vCPUs. Given this small cost, we // deliberately avoid additional heuristics to estimate the // downtime more precisely - for now. Instead, we approximate // the downtime just by the transfer time of the final memory // delta. if let Some(memory_downtime) = ctx.estimated_downtime && memory_downtime <= send_data_migration.downtime() { debug!( "Precopy: Target downtime can be met: {}ms <= {}ms", memory_downtime.as_millis(), send_data_migration.downtime().as_millis() ); return Ok(true); } // We check the beginning of the precopy migration and not the overall migration, and // this is fine: precopy takes the longest and the earlier steps are negligible. if ctx.migration_begin.elapsed() >= send_data_migration.timeout() { return match send_data_migration.timeout_strategy { TimeoutStrategy::Cancel => { let msg = format!( "Precopy: Timeout reached: {}s: migration didn't converge in time", send_data_migration.timeout().as_secs() ); Err(MigratableError::MigrateSend(anyhow!("{msg}"))) } TimeoutStrategy::Ignore => { info!( "Precopy: Pausing VM, ignoring target downtime ({}ms) due to timeout ({}s): Estimated downtime: {}ms", send_data_migration.downtime().as_millis(), send_data_migration.timeout().as_secs(), ctx.estimated_downtime .unwrap_or(Duration::from_secs(0)) .as_millis() ); Ok(true) } }; } Ok(false) } /// Performs the memory migration including multiple iterations. /// /// This includes: /// - initial memory - VM is running /// - multiple memory delta transmissions - VM is running /// - final memory iteration - VM is paused /// /// Stores the [finalized] [`MemoryMigrationContext`] in the provided /// [`OngoingMigrationContext`]. /// /// [finalized]: MemoryMigrationContext::finalize fn do_memory_migration( vm: &mut Vm, socket: &mut SocketStream, send_data_migration: &VmSendMigrationData, mem_send: &mut SendAdditionalConnections, ctx: &mut OngoingMigrationContext, ) -> result::Result<(), MigratableError> { let mut mem_ctx = MemoryMigrationContext::new(); vm.start_dirty_log()?; let remaining = Self::do_memory_iterations( vm, socket, &mut mem_ctx, // We bind send_data_migration to the callback |ctx| Self::is_precopy_converged(ctx, send_data_migration), mem_send, )?; let downtime_begin = Instant::now(); vm.pause()?; // Send last batch of dirty pages: final iteration { let iteration_begin = Instant::now(); let mut final_table = vm.dirty_log()?; final_table.extend(remaining); mem_ctx.update_metrics_before_transfer(iteration_begin, &final_table); let transfer_begin = Instant::now(); mem_send.send_memory(final_table, socket)?; let transfer_duration = transfer_begin.elapsed(); mem_ctx.update_metrics_after_transfer(transfer_begin, transfer_duration); mem_ctx.iteration += 1; } mem_ctx.finalize(); info!("Precopy complete: {mem_ctx}"); ctx.set_vm_paused(downtime_begin, mem_ctx) .expect("migration context should transition to VmPaused after memory migration"); Ok(()) } /// Performs a migration including all its phases. fn send_migration( vm: &mut Vm, #[cfg(all(feature = "kvm", target_arch = "x86_64"))] hypervisor: &dyn hypervisor::Hypervisor, send_data_migration: &VmSendMigrationData, ) -> result::Result<(), MigratableError> { // State machine that is updated with more context as we progress. let mut ctx = OngoingMigrationContext::new(); // Set up the socket connection let mut socket = migration_transport::send_migration_socket(&send_data_migration.destination_url)?; // Start the migration migration_transport::send_request_expect_ok( &mut socket, Request::start(), MigratableError::MigrateSend(anyhow!("Error starting migration")), )?; // Send config let vm_config = vm.get_config(); #[cfg(all(feature = "kvm", target_arch = "x86_64"))] let common_cpuid = { #[cfg(feature = "tdx")] if vm_config.lock().unwrap().is_tdx_enabled() { return Err(MigratableError::MigrateSend(anyhow!( "Live Migration is not supported when TDX is enabled" ))); } let amx = vm_config.lock().unwrap().cpus.features.amx; let phys_bits = vm::physical_bits(hypervisor, vm_config.lock().unwrap().cpus.max_phys_bits); arch::generate_common_cpuid( hypervisor, &arch::CpuidConfig { phys_bits, kvm_hyperv: vm_config.lock().unwrap().cpus.kvm_hyperv, #[cfg(feature = "tdx")] tdx: false, amx, }, ) .map_err(|e| { MigratableError::MigrateSend(anyhow!("Error generating common cpuid': {e:?}")) })? }; if send_data_migration.local { match &mut socket { SocketStream::Unix(unix_socket) => { // Proceed with sending memory file descriptors over UNIX socket vm.send_memory_fds(unix_socket)?; } SocketStream::Tcp(_tcp_socket) => { return Err(MigratableError::MigrateSend(anyhow!( "--local option is not supported with TCP sockets", ))); } } } let vm_migration_config = VmMigrationConfig { vm_config, #[cfg(all(feature = "kvm", target_arch = "x86_64"))] common_cpuid, memory_manager_data: vm.memory_manager_data(), }; migration_transport::send_config(&mut socket, &vm_migration_config)?; // Let every Migratable object know about the migration being started. vm.start_migration()?; if send_data_migration.local { // Now pause VM let downtime_begin = Instant::now(); vm.pause()?; ctx.set_vm_paused( downtime_begin, // No memory was transferred MemoryMigrationContext::empty_finalized(), ) .expect("migration context should transition to VmPaused for local migration"); } else { let mut mem_send = migration_transport::SendAdditionalConnections::new( &send_data_migration.destination_url, send_data_migration.connections, &vm.guest_memory(), )?; Self::do_memory_migration( vm, &mut socket, send_data_migration, &mut mem_send, &mut ctx, ) .inspect_err(|_| { // Calling cleanup multiple times is fine, thus here we just make sure // that it is called. if let Err(e) = mem_send.cleanup() { warn!("Error cleaning up migration connections: {e}"); } })?; mem_send.cleanup()?; } // We release the locks early to enable locking them on the destination host. // The VM is already stopped. vm.release_disk_locks() .map_err(|e| MigratableError::UnlockError(anyhow!("{e}")))?; // Capture snapshot and send it let (vm_snapshot, snapshot_duration) = measure_ok(|| vm.snapshot())?; let (_, send_snapshot_duration) = measure_ok(|| migration_transport::send_state(&mut socket, &vm_snapshot))?; // Complete the migration. // When this returns, we know the VM was resumed (if it was running // before the migration) and that the receiving VMM acquired disk // locks again. let (_, complete_duration) = measure_ok(|| { migration_transport::send_request_expect_ok( &mut socket, Request::complete(), MigratableError::MigrateSend(anyhow!("Error completing migration")), ) })?; let ctx = ctx .finalize(snapshot_duration, send_snapshot_duration, complete_duration) .expect("migration context should finalize after memory migration completed"); info!( "Migration completed after {:.1}s with a downtime of {}ms (goal was {}ms)", ctx.migration_dur.as_secs_f32(), ctx.downtime_ctx.effective_downtime.as_millis(), send_data_migration.downtime().as_millis() ); debug!("Downtime breakdown: {}", ctx.downtime_ctx); // Stop logging dirty pages if !send_data_migration.local { vm.stop_dirty_log()?; } // Let every Migratable object know about the migration being complete vm.complete_migration() } #[cfg(all(feature = "kvm", target_arch = "x86_64"))] fn vm_check_cpuid_compatibility( &self, src_vm_config: &Arc>, src_vm_cpuid: &[hypervisor::arch::x86::CpuIdEntry], ) -> result::Result<(), MigratableError> { #[cfg(feature = "tdx")] if src_vm_config.lock().unwrap().is_tdx_enabled() { return Err(MigratableError::MigrateReceive(anyhow!( "Live Migration is not supported when TDX is enabled" ))); } // We check the `CPUID` compatibility of between the source vm and destination, which is // mostly about feature compatibility. let dest_cpuid = &{ let vm_config = &src_vm_config.lock().unwrap(); let phys_bits = vm::physical_bits(self.hypervisor.as_ref(), vm_config.cpus.max_phys_bits); arch::generate_common_cpuid( self.hypervisor.as_ref(), &arch::CpuidConfig { phys_bits, kvm_hyperv: vm_config.cpus.kvm_hyperv, #[cfg(feature = "tdx")] tdx: false, amx: vm_config.cpus.features.amx, }, ) .map_err(|e| { MigratableError::MigrateReceive(anyhow!("Error generating common cpuid: {e:?}")) })? }; arch::CpuidFeatureEntry::check_cpuid_compatibility(src_vm_cpuid, dest_cpuid).map_err(|e| { MigratableError::MigrateReceive(anyhow!( "Error checking cpu feature compatibility': {e:?}" )) }) } fn vm_restore( &mut self, source_url: &str, vm_config: Arc>, prefault: bool, memory_restore_mode: MemoryRestoreMode, ) -> std::result::Result<(), VmError> { let snapshot = recv_vm_state(source_url).map_err(VmError::Restore)?; #[cfg(all(feature = "kvm", target_arch = "x86_64"))] let vm_snapshot = get_vm_snapshot(&snapshot).map_err(VmError::Restore)?; #[cfg(all(feature = "kvm", target_arch = "x86_64"))] self.vm_check_cpuid_compatibility(&vm_config, &vm_snapshot.common_cpuid) .map_err(VmError::Restore)?; self.vm_config = Some(Arc::clone(&vm_config)); // Always re-populate the 'console_info' based on the new 'vm_config' self.console_info = Some(pre_create_console_devices(self).map_err(VmError::CreateConsoleDevices)?); let exit_evt = self.exit_evt.try_clone().map_err(VmError::EventFdClone)?; let reset_evt = self.reset_evt.try_clone().map_err(VmError::EventFdClone)?; #[cfg(feature = "guest_debug")] let debug_evt = self .vm_debug_evt .try_clone() .map_err(VmError::EventFdClone)?; let activate_evt = self .activate_evt .try_clone() .map_err(VmError::EventFdClone)?; let vm = Vm::new( vm_config, exit_evt, reset_evt, #[cfg(feature = "guest_debug")] debug_evt, &self.seccomp_action, self.hypervisor.clone(), activate_evt, self.console_info.clone(), self.console_resize_pipe.clone(), Arc::clone(&self.original_termios_opt), Some(&snapshot), Some(source_url), Some(prefault), Some(memory_restore_mode), )?; self.vm = Some(vm); if self .vm_config .as_ref() .unwrap() .lock() .unwrap() .landlock_enable { let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); apply_landlock(&mut config).map_err(VmError::ApplyLandlock)?; } // Now we can restore the rest of the VM. if let Some(ref mut vm) = self.vm { vm.restore() } else { Err(VmError::VmNotCreated) } } fn control_loop( &mut self, api_receiver: &Receiver, #[cfg(feature = "guest_debug")] gdb_receiver: &Receiver, ) -> Result<()> { const EPOLL_EVENTS_LEN: usize = 100; let mut events = vec![epoll::Event::new(epoll::Events::empty(), 0); EPOLL_EVENTS_LEN]; let epoll_fd = self.epoll.as_raw_fd(); 'outer: loop { let num_events = match epoll::wait(epoll_fd, -1, &mut events[..]) { Ok(res) => res, Err(e) => { if e.kind() == io::ErrorKind::Interrupted { // It's well defined from the epoll_wait() syscall // documentation that the epoll loop can be interrupted // before any of the requested events occurred or the // timeout expired. In both those cases, epoll_wait() // returns an error of type EINTR, but this should not // be considered as a regular error. Instead it is more // appropriate to retry, by calling into epoll_wait(). continue; } return Err(Error::Epoll(e)); } }; for event in events.iter().take(num_events) { let dispatch_event: EpollDispatch = event.data.into(); match dispatch_event { EpollDispatch::Unknown => { let event = event.data; warn!("Unknown VMM loop event: {event}"); } EpollDispatch::Exit => { info!("VM exit event"); // Consume the event. self.exit_evt.read().map_err(Error::EventFdRead)?; self.vmm_shutdown().map_err(Error::VmmShutdown)?; break 'outer; } EpollDispatch::Reset => { info!("VM reset event"); // Consume the event. self.reset_evt.read().map_err(Error::EventFdRead)?; self.vm_reboot().map_err(Error::VmReboot)?; } EpollDispatch::ActivateVirtioDevices => { if let Some(ref vm) = self.vm { let count = self.activate_evt.read().map_err(Error::EventFdRead)?; info!("Trying to activate pending virtio devices: count = {count}"); vm.activate_virtio_devices() .map_err(Error::ActivateVirtioDevices)?; } } EpollDispatch::Api => { // Consume the events. for _ in 0..self.api_evt.read().map_err(Error::EventFdRead)? { // Read from the API receiver channel let api_request = api_receiver.recv().map_err(Error::ApiRequestRecv)?; if api_request(self)? { break 'outer; } } } #[cfg(feature = "guest_debug")] EpollDispatch::Debug => { // Consume the events. for _ in 0..self.debug_evt.read().map_err(Error::EventFdRead)? { // Read from the API receiver channel let gdb_request = gdb_receiver.recv().map_err(Error::GdbRequestRecv)?; let response = if let Some(ref mut vm) = self.vm { vm.debug_request(&gdb_request.payload, gdb_request.cpu_id) } else { Err(VmError::VmNotRunning) } .map_err(gdb::Error::Vm); gdb_request .sender .send(response) .map_err(Error::GdbResponseSend)?; } } #[cfg(not(feature = "guest_debug"))] EpollDispatch::Debug => {} } } } // Trigger the termination of the signal_handler thread if let Some(signals) = self.signals.take() { signals.close(); } // Wait for all the threads to finish for thread in self.threads.drain(..) { thread.join().map_err(Error::ThreadCleanup)?; } Ok(()) } } fn apply_landlock(vm_config: &mut VmConfig) -> result::Result<(), LandlockError> { vm_config.apply_landlock()?; Ok(()) } impl RequestHandler for Vmm { fn vm_create(&mut self, config: Box) -> result::Result<(), VmError> { // We only store the passed VM config. // The VM will be created when being asked to boot it. if self.vm_config.is_some() { return Err(VmError::VmAlreadyCreated); } self.vm_config = Some(Arc::new(Mutex::new(*config))); self.console_info = Some(pre_create_console_devices(self).map_err(VmError::CreateConsoleDevices)?); if self .vm_config .as_ref() .is_some_and(|config| config.lock().unwrap().landlock_enable) { let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); apply_landlock(&mut config).map_err(VmError::ApplyLandlock)?; } Ok(()) } fn vm_boot(&mut self) -> result::Result<(), VmError> { tracer::start(); info!("Booting VM"); event!("vm", "booting"); let r = { trace_scoped!("vm_boot"); // If we don't have a config, we cannot boot a VM. if self.vm_config.is_none() { return Err(VmError::VmMissingConfig); } // console_info is set to None in vm_shutdown. re-populate here if empty if self.console_info.is_none() { self.console_info = Some(pre_create_console_devices(self).map_err(VmError::CreateConsoleDevices)?); } // Create a new VM if we don't have one yet. if self.vm.is_none() { let exit_evt = self.exit_evt.try_clone().map_err(VmError::EventFdClone)?; let reset_evt = self.reset_evt.try_clone().map_err(VmError::EventFdClone)?; #[cfg(feature = "guest_debug")] let vm_debug_evt = self .vm_debug_evt .try_clone() .map_err(VmError::EventFdClone)?; let activate_evt = self .activate_evt .try_clone() .map_err(VmError::EventFdClone)?; if let Some(ref vm_config) = self.vm_config { let vm = Vm::new( Arc::clone(vm_config), exit_evt, reset_evt, #[cfg(feature = "guest_debug")] vm_debug_evt, &self.seccomp_action, self.hypervisor.clone(), activate_evt, self.console_info.clone(), self.console_resize_pipe.clone(), Arc::clone(&self.original_termios_opt), None, None, None, None, )?; self.vm = Some(vm); } } // Now we can boot the VM. if let Some(ref mut vm) = self.vm { vm.boot() } else { Err(VmError::VmNotCreated) } }; tracer::end(); if r.is_ok() { event!("vm", "booted"); } r } fn vm_pause(&mut self) -> result::Result<(), VmError> { if let Some(ref mut vm) = self.vm { vm.pause().map_err(VmError::Pause) } else { Err(VmError::VmNotRunning) } } fn vm_resume(&mut self) -> result::Result<(), VmError> { if let Some(ref mut vm) = self.vm { vm.resume().map_err(VmError::Resume) } else { Err(VmError::VmNotRunning) } } fn vm_snapshot(&mut self, destination_url: &str) -> result::Result<(), VmError> { if let Some(ref mut vm) = self.vm { // Drain console_info so that FDs are not reused let _ = self.console_info.take(); vm.snapshot() .map_err(VmError::Snapshot) .and_then(|snapshot| { vm.send(&snapshot, destination_url) .map_err(VmError::SnapshotSend) }) } else { Err(VmError::VmNotRunning) } } fn vm_restore(&mut self, restore_cfg: RestoreConfig) -> result::Result<(), VmError> { if self.vm.is_some() || self.vm_config.is_some() { return Err(VmError::VmAlreadyCreated); } let source_url = restore_cfg.source_url.as_path().to_str(); if source_url.is_none() { return Err(VmError::InvalidRestoreSourceUrl); } // Safe to unwrap as we checked it was Some(&str). let source_url = source_url.unwrap(); let vm_config = Arc::new(Mutex::new( recv_vm_config(source_url).map_err(VmError::Restore)?, )); restore_cfg .validate(&vm_config.lock().unwrap().clone()) .map_err(VmError::ConfigValidation)?; // Update VM's net configurations with new fds received for restore operation if let (Some(restored_nets), Some(vm_net_configs)) = (restore_cfg.net_fds, &mut vm_config.lock().unwrap().net) { for net in restored_nets.iter() { for net_config in vm_net_configs.iter_mut() { // update only if the net dev is backed by FDs if net_config.pci_common.id.as_ref() == Some(&net.id) && net_config.fds.is_some() { net_config.fds.clone_from(&net.fds); } } } } self.vm_restore( source_url, vm_config, restore_cfg.prefault, restore_cfg.memory_restore_mode, ) .and_then(|()| { if restore_cfg.resume { self.vm_resume() } else { Ok(()) } }) .map_err(|e| { error!("VM Restore failed: {e:?}"); if let Err(e) = self.vm_delete() { return e; } e })?; Ok(()) } #[cfg(all(target_arch = "x86_64", feature = "guest_debug"))] fn vm_coredump(&mut self, destination_url: &str) -> result::Result<(), VmError> { if let Some(ref mut vm) = self.vm { vm.coredump(destination_url).map_err(VmError::Coredump) } else { Err(VmError::VmNotRunning) } } fn vm_shutdown(&mut self) -> result::Result<(), VmError> { let r = if let Some(ref mut vm) = self.vm.take() { // Drain console_info so that the FDs are not reused let _ = self.console_info.take(); vm.shutdown() } else { Err(VmError::VmNotRunning) }; if r.is_ok() { event!("vm", "shutdown"); } r } fn vm_reboot(&mut self) -> result::Result<(), VmError> { event!("vm", "rebooting"); // First we stop the current VM let config = if let Some(mut vm) = self.vm.take() { let config = vm.get_config(); vm.shutdown()?; config } else { return Err(VmError::VmNotCreated); }; // vm.shutdown() closes all the console devices, so set console_info to None // so that the closed FD #s are not reused. let _ = self.console_info.take(); let exit_evt = self.exit_evt.try_clone().map_err(VmError::EventFdClone)?; let reset_evt = self.reset_evt.try_clone().map_err(VmError::EventFdClone)?; #[cfg(feature = "guest_debug")] let debug_evt = self .vm_debug_evt .try_clone() .map_err(VmError::EventFdClone)?; let activate_evt = self .activate_evt .try_clone() .map_err(VmError::EventFdClone)?; // The Linux kernel fires off an i8042 reset after doing the ACPI reset so there may be // an event sitting in the shared reset_evt. Without doing this we get very early reboots // during the boot process. if self.reset_evt.read().is_ok() { warn!("Spurious second reset event received. Ignoring."); } self.console_info = Some(pre_create_console_devices(self).map_err(VmError::CreateConsoleDevices)?); // Then we create the new VM let mut vm = Vm::new( config, exit_evt, reset_evt, #[cfg(feature = "guest_debug")] debug_evt, &self.seccomp_action, self.hypervisor.clone(), activate_evt, self.console_info.clone(), self.console_resize_pipe.clone(), Arc::clone(&self.original_termios_opt), None, None, None, None, )?; // And we boot it vm.boot()?; self.vm = Some(vm); event!("vm", "rebooted"); Ok(()) } fn vm_info(&self) -> result::Result { match &self.vm_config { Some(vm_config) => { let state = match &self.vm { Some(vm) => vm.get_state(), None => VmState::Created, }; let config = vm_config.lock().unwrap().clone(); let mut memory_actual_size = config.memory.total_size() - config.memory.hotplugged_size(); if let Some(vm) = &self.vm { memory_actual_size = memory_actual_size.saturating_sub(vm.balloon_size()); memory_actual_size += vm.virtio_mem_plugged_size(); } let device_tree = self .vm .as_ref() .map(|vm| vm.device_tree().lock().unwrap().clone()); Ok(VmInfoResponse { config: Box::new(config), state, memory_actual_size, device_tree, }) } None => Err(VmError::VmNotCreated), } } fn vmm_ping(&self) -> VmmPingResponse { let VmmVersionInfo { build_version, version, } = self.version.clone(); VmmPingResponse { build_version, version, pid: std::process::id() as i64, features: feature_list(), } } fn vm_delete(&mut self) -> result::Result<(), VmError> { if self.vm_config.is_none() { return Ok(()); } // If a VM is booted, we first try to shut it down. if self.vm.is_some() { self.vm_shutdown()?; } self.vm_config = None; event!("vm", "deleted"); Ok(()) } fn vmm_shutdown(&mut self) -> result::Result<(), VmError> { self.vm_delete()?; event!("vmm", "shutdown"); Ok(()) } fn vm_resize( &mut self, desired_vcpus: Option, desired_ram: Option, desired_balloon: Option, ) -> result::Result<(), VmError> { self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; if let Some(ref mut vm) = self.vm { vm.resize(desired_vcpus, desired_ram, desired_balloon) .inspect_err(|e| error!("Error when resizing VM: {e:?}"))?; Ok(()) } else { let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); if let Some(desired_vcpus) = desired_vcpus { config.cpus.boot_vcpus = desired_vcpus; } if let Some(desired_ram) = desired_ram { config.memory.size = desired_ram; } if let Some(desired_balloon) = desired_balloon && let Some(balloon_config) = &mut config.balloon { balloon_config.size = desired_balloon; } Ok(()) } } fn vm_resize_disk(&mut self, id: String, desired_size: u64) -> result::Result<(), VmError> { self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; if let Some(ref mut vm) = self.vm { return vm.resize_disk(&id, desired_size); } Err(VmError::ResizeDisk) } fn vm_resize_zone(&mut self, id: String, desired_ram: u64) -> result::Result<(), VmError> { self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; if let Some(ref mut vm) = self.vm { vm.resize_zone(&id, desired_ram) .inspect_err(|e| error!("Error when resizing zone: {e:?}"))?; Ok(()) } else { // Update VmConfig by setting the new desired ram. let memory_config = &mut self.vm_config.as_ref().unwrap().lock().unwrap().memory; if let Some(zones) = &mut memory_config.zones { for zone in zones.iter_mut() { if zone.id == id { zone.size = desired_ram; return Ok(()); } } } error!("Could not find the memory zone {id} for the resize"); Err(VmError::ResizeZone) } } fn vm_add_device( &mut self, device_cfg: DeviceConfig, ) -> result::Result>, VmError> { self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; { // Validate the configuration change in a cloned configuration let mut config = self.vm_config.as_ref().unwrap().lock().unwrap().clone(); add_to_config(&mut config.devices, device_cfg.clone()); config.validate().map_err(VmError::ConfigValidation)?; } if let Some(ref mut vm) = self.vm { let info = vm.add_device(device_cfg).inspect_err(|e| { error!("Error when adding new device to the VM: {e:?}"); })?; serde_json::to_vec(&info) .map(Some) .map_err(VmError::SerializeJson) } else { // Update VmConfig by adding the new device. let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); add_to_config(&mut config.devices, device_cfg); Ok(None) } } fn vm_add_user_device( &mut self, device_cfg: UserDeviceConfig, ) -> result::Result>, VmError> { self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; { // Validate the configuration change in a cloned configuration let mut config = self.vm_config.as_ref().unwrap().lock().unwrap().clone(); add_to_config(&mut config.user_devices, device_cfg.clone()); config.validate().map_err(VmError::ConfigValidation)?; } if let Some(ref mut vm) = self.vm { let info = vm.add_user_device(device_cfg).inspect_err(|e| { error!("Error when adding new user device to the VM: {e:?}"); })?; serde_json::to_vec(&info) .map(Some) .map_err(VmError::SerializeJson) } else { // Update VmConfig by adding the new device. let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); add_to_config(&mut config.user_devices, device_cfg); Ok(None) } } fn vm_remove_device(&mut self, id: String) -> result::Result<(), VmError> { if let Some(ref mut vm) = self.vm { vm.remove_device(&id) .inspect_err(|e| error!("Error when removing device from the VM: {e:?}"))?; Ok(()) } else if let Some(ref config) = self.vm_config { let mut config = config.lock().unwrap(); if config.remove_device(&id) { Ok(()) } else { Err(VmError::NoDeviceToRemove(id)) } } else { Err(VmError::VmNotCreated) } } fn vm_add_disk(&mut self, disk_cfg: DiskConfig) -> result::Result>, VmError> { self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; { // Validate the configuration change in a cloned configuration let mut config = self.vm_config.as_ref().unwrap().lock().unwrap().clone(); add_to_config(&mut config.disks, disk_cfg.clone()); config.validate().map_err(VmError::ConfigValidation)?; } if let Some(ref mut vm) = self.vm { let info = vm.add_disk(disk_cfg).inspect_err(|e| { error!("Error when adding new disk to the VM: {e:?}"); })?; serde_json::to_vec(&info) .map(Some) .map_err(VmError::SerializeJson) } else { // Update VmConfig by adding the new device. let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); add_to_config(&mut config.disks, disk_cfg); Ok(None) } } fn vm_add_fs(&mut self, fs_cfg: FsConfig) -> result::Result>, VmError> { self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; { // Validate the configuration change in a cloned configuration let mut config = self.vm_config.as_ref().unwrap().lock().unwrap().clone(); add_to_config(&mut config.fs, fs_cfg.clone()); config.validate().map_err(VmError::ConfigValidation)?; } if let Some(ref mut vm) = self.vm { let info = vm.add_fs(fs_cfg).inspect_err(|e| { error!("Error when adding new fs to the VM: {e:?}"); })?; serde_json::to_vec(&info) .map(Some) .map_err(VmError::SerializeJson) } else { // Update VmConfig by adding the new device. let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); add_to_config(&mut config.fs, fs_cfg); Ok(None) } } fn vm_add_generic_vhost_user( &mut self, generic_vhost_user_cfg: GenericVhostUserConfig, ) -> result::Result>, VmError> { self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; { // Validate the configuration change in a cloned configuration let mut config = self.vm_config.as_ref().unwrap().lock().unwrap().clone(); add_to_config( &mut config.generic_vhost_user, generic_vhost_user_cfg.clone(), ); config.validate().map_err(VmError::ConfigValidation)?; } if let Some(ref mut vm) = self.vm { let info = vm .add_generic_vhost_user(generic_vhost_user_cfg) .inspect_err(|e| { error!("Error when adding new generic vhost-user device to the VM: {e:?}"); })?; serde_json::to_vec(&info) .map(Some) .map_err(VmError::SerializeJson) } else { // Update VmConfig by adding the new device. let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); add_to_config(&mut config.generic_vhost_user, generic_vhost_user_cfg); Ok(None) } } fn vm_add_pmem(&mut self, pmem_cfg: PmemConfig) -> result::Result>, VmError> { self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; { // Validate the configuration change in a cloned configuration let mut config = self.vm_config.as_ref().unwrap().lock().unwrap().clone(); add_to_config(&mut config.pmem, pmem_cfg.clone()); config.validate().map_err(VmError::ConfigValidation)?; } if let Some(ref mut vm) = self.vm { let info = vm.add_pmem(pmem_cfg).inspect_err(|e| { error!("Error when adding new pmem device to the VM: {e:?}"); })?; serde_json::to_vec(&info) .map(Some) .map_err(VmError::SerializeJson) } else { // Update VmConfig by adding the new device. let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); add_to_config(&mut config.pmem, pmem_cfg); Ok(None) } } fn vm_add_net(&mut self, net_cfg: NetConfig) -> result::Result>, VmError> { self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; { // Validate the configuration change in a cloned configuration let mut config = self.vm_config.as_ref().unwrap().lock().unwrap().clone(); add_to_config(&mut config.net, net_cfg.clone()); config.validate().map_err(VmError::ConfigValidation)?; } if let Some(ref mut vm) = self.vm { let info = vm.add_net(net_cfg).inspect_err(|e| { error!("Error when adding new network device to the VM: {e:?}"); })?; serde_json::to_vec(&info) .map(Some) .map_err(VmError::SerializeJson) } else { // Update VmConfig by adding the new device. let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); add_to_config(&mut config.net, net_cfg); Ok(None) } } fn vm_add_vdpa(&mut self, vdpa_cfg: VdpaConfig) -> result::Result>, VmError> { self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; { // Validate the configuration change in a cloned configuration let mut config = self.vm_config.as_ref().unwrap().lock().unwrap().clone(); add_to_config(&mut config.vdpa, vdpa_cfg.clone()); config.validate().map_err(VmError::ConfigValidation)?; } if let Some(ref mut vm) = self.vm { let info = vm.add_vdpa(vdpa_cfg).inspect_err(|e| { error!("Error when adding new vDPA device to the VM: {e:?}"); })?; serde_json::to_vec(&info) .map(Some) .map_err(VmError::SerializeJson) } else { // Update VmConfig by adding the new device. let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); add_to_config(&mut config.vdpa, vdpa_cfg); Ok(None) } } fn vm_add_vsock(&mut self, vsock_cfg: VsockConfig) -> result::Result>, VmError> { self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; { // Validate the configuration change in a cloned configuration let mut config = self.vm_config.as_ref().unwrap().lock().unwrap().clone(); if config.vsock.is_some() { return Err(VmError::TooManyVsockDevices); } config.vsock = Some(vsock_cfg.clone()); config.validate().map_err(VmError::ConfigValidation)?; } if let Some(ref mut vm) = self.vm { let info = vm.add_vsock(vsock_cfg).inspect_err(|e| { error!("Error when adding new vsock device to the VM: {e:?}"); })?; serde_json::to_vec(&info) .map(Some) .map_err(VmError::SerializeJson) } else { // Update VmConfig by adding the new device. let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); config.vsock = Some(vsock_cfg); Ok(None) } } fn vm_counters(&mut self) -> result::Result>, VmError> { if let Some(ref mut vm) = self.vm { let info = vm.counters().inspect_err(|e| { error!("Error when getting counters from the VM: {e:?}"); })?; serde_json::to_vec(&info) .map(Some) .map_err(VmError::SerializeJson) } else { Err(VmError::VmNotRunning) } } fn vm_power_button(&mut self) -> result::Result<(), VmError> { if let Some(ref mut vm) = self.vm { vm.power_button() } else { Err(VmError::VmNotRunning) } } fn vm_nmi(&mut self) -> result::Result<(), VmError> { if let Some(ref mut vm) = self.vm { vm.nmi() } else { Err(VmError::VmNotRunning) } } fn vm_receive_migration( &mut self, receive_data_migration: VmReceiveMigrationData, ) -> result::Result<(), MigratableError> { info!( "Receiving migration: receiver_url = {}", receive_data_migration.receiver_url ); let mut listener = migration_transport::receive_migration_listener(&receive_data_migration.receiver_url)?; // Accept the connection and get the socket let mut socket = listener.accept()?; event!("vm", "migration-receive-started"); let mut state = ReceiveMigrationState::Established; while !state.finished() { let req = Request::read_from(&mut socket)?; trace!("Command {:?} received", req.command()); let (response, new_state) = match self.vm_receive_migration_step( &mut socket, &listener, state, &req, &receive_data_migration, ) { Ok(next_state) => (Response::ok(), next_state), Err(err) => { warn!( "Migration aborted as migration command {:?} failed: {}", req.command(), err ); (Response::error(), ReceiveMigrationState::Aborted) } }; state = new_state; assert_eq!(response.length(), 0); response.write_to(&mut socket)?; } if let ReceiveMigrationState::Aborted = state { event!("vm", "migration-receive-failed"); self.vm = None; self.vm_config = None; } else { event!("vm", "migration-receive-finished"); } Ok(()) } fn vm_send_migration( &mut self, send_data_migration: VmSendMigrationData, ) -> result::Result<(), MigratableError> { send_data_migration .validate() .context("Invalid send migration configuration") .map_err(MigratableError::MigrateSend)?; info!( "Sending migration: destination_url={},local={},downtime={}ms,timeout={}s,timeout_strategy={:?}", send_data_migration.destination_url, send_data_migration.local, send_data_migration.downtime().as_millis(), send_data_migration.timeout().as_secs(), send_data_migration.timeout_strategy ); if !self .vm_config .as_ref() .unwrap() .lock() .unwrap() .backed_by_shared_memory() && send_data_migration.local { return Err(MigratableError::MigrateSend(anyhow!( "Local migration requires shared memory or hugepages enabled" ))); } let vm = self .vm .as_mut() .ok_or_else(|| MigratableError::MigrateSend(anyhow!("VM is not running")))?; // Only running VMs can be migrated: Future work can fix this to allow // also the migration of paused VMs while preserving the state in success // and error case. See #7815. if vm.get_state() != VmState::Running { return Err(MigratableError::MigrateSend(anyhow!( "VM is not in running state: {:?}", vm.get_state() ))); } event!("vm", "migration-started"); Self::send_migration( vm, #[cfg(all(feature = "kvm", target_arch = "x86_64"))] self.hypervisor.as_ref(), &send_data_migration, ) .map_err(|migration_err| { error!("Migration failed: {migration_err:?}"); event!("vm", "migration-failed"); // Stop logging dirty pages only for non-local migrations if !send_data_migration.local && let Err(e) = vm.stop_dirty_log() { return e; } if vm.get_state() == VmState::Paused && let Err(e) = vm.resume() { return e; } migration_err })?; event!("vm", "migration-finished"); // Shutdown the VM after the migration succeeded self.exit_evt.write(1).map_err(|e| { MigratableError::MigrateSend(anyhow!( "Failed shutting down the VM after migration: {e:?}" )) }) } } const CPU_MANAGER_SNAPSHOT_ID: &str = "cpu-manager"; const MEMORY_MANAGER_SNAPSHOT_ID: &str = "memory-manager"; const DEVICE_MANAGER_SNAPSHOT_ID: &str = "device-manager"; #[cfg(test)] mod unit_tests { use std::path::PathBuf; use super::*; #[cfg(target_arch = "x86_64")] use crate::vm_config::DebugConsoleConfig; use crate::vm_config::{ ConsoleConfig, ConsoleOutputMode, CoreScheduling, CpuFeatures, CpusConfig, HotplugMethod, MemoryConfig, PayloadConfig, RngConfig, }; fn create_dummy_vmm() -> Vmm { Vmm::new( VmmVersionInfo::new("dummy", "dummy"), EventFd::new(EFD_NONBLOCK).unwrap(), #[cfg(feature = "guest_debug")] EventFd::new(EFD_NONBLOCK).unwrap(), #[cfg(feature = "guest_debug")] EventFd::new(EFD_NONBLOCK).unwrap(), SeccompAction::Allow, hypervisor::new().unwrap(), EventFd::new(EFD_NONBLOCK).unwrap(), ) .unwrap() } fn create_dummy_vm_config() -> Box { Box::new(VmConfig { cpus: CpusConfig { boot_vcpus: 1, max_vcpus: 1, topology: None, kvm_hyperv: false, max_phys_bits: 46, affinity: None, features: CpuFeatures::default(), nested: true, core_scheduling: CoreScheduling::default(), }, memory: MemoryConfig { size: 536_870_912, mergeable: false, hotplug_method: HotplugMethod::Acpi, hotplug_size: None, hotplugged_size: None, shared: true, hugepages: false, hugepage_size: None, prefault: false, zones: None, thp: true, }, payload: Some(PayloadConfig { kernel: Some(PathBuf::from("/path/to/kernel")), firmware: None, cmdline: None, initramfs: None, #[cfg(feature = "igvm")] igvm: None, #[cfg(feature = "sev_snp")] host_data: None, #[cfg(feature = "fw_cfg")] fw_cfg_config: None, }), rate_limit_groups: None, disks: None, net: None, rng: RngConfig { src: PathBuf::from("/dev/urandom"), iommu: false, }, balloon: None, fs: None, generic_vhost_user: None, pmem: None, serial: ConsoleConfig { file: None, mode: ConsoleOutputMode::Null, iommu: false, socket: None, }, console: ConsoleConfig { file: None, // Caution: Don't use `Tty` to not mess with users terminal mode: ConsoleOutputMode::Off, iommu: false, socket: None, }, #[cfg(target_arch = "x86_64")] debug_console: DebugConsoleConfig::default(), devices: None, user_devices: None, vdpa: None, vsock: None, #[cfg(feature = "pvmemcontrol")] pvmemcontrol: None, pvpanic: false, iommu: false, numa: None, watchdog: false, #[cfg(feature = "guest_debug")] gdb: false, pci_segments: None, platform: None, tpm: None, preserved_fds: None, landlock_enable: false, landlock_rules: None, #[cfg(feature = "ivshmem")] ivshmem: None, }) } #[test] fn test_vmm_vm_create() { let mut vmm = create_dummy_vmm(); let config = create_dummy_vm_config(); assert!(matches!(vmm.vm_create(config.clone()), Ok(()))); assert!(matches!( vmm.vm_create(config), Err(VmError::VmAlreadyCreated) )); } #[test] fn test_vmm_vm_cold_add_device() { let mut vmm = create_dummy_vmm(); let device_config = DeviceConfig::parse("path=/path/to/device").unwrap(); assert!(matches!( vmm.vm_add_device(device_config.clone()), Err(VmError::VmNotCreated) )); let _ = vmm.vm_create(create_dummy_vm_config()); assert!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .devices .is_none() ); assert!(vmm.vm_add_device(device_config.clone()).unwrap().is_none()); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .devices .clone() .unwrap() .len(), 1 ); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .devices .clone() .unwrap()[0], device_config ); } #[test] fn test_vmm_vm_cold_add_user_device() { let mut vmm = create_dummy_vmm(); let user_device_config = UserDeviceConfig::parse("socket=/path/to/socket,id=8,pci_segment=2").unwrap(); assert!(matches!( vmm.vm_add_user_device(user_device_config.clone()), Err(VmError::VmNotCreated) )); let _ = vmm.vm_create(create_dummy_vm_config()); assert!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .user_devices .is_none() ); assert!( vmm.vm_add_user_device(user_device_config.clone()) .unwrap() .is_none() ); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .user_devices .clone() .unwrap() .len(), 1 ); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .user_devices .clone() .unwrap()[0], user_device_config ); } #[test] fn test_vmm_vm_cold_add_disk() { let mut vmm = create_dummy_vmm(); let disk_config = DiskConfig::parse("path=/path/to_file").unwrap(); assert!(matches!( vmm.vm_add_disk(disk_config.clone()), Err(VmError::VmNotCreated) )); let _ = vmm.vm_create(create_dummy_vm_config()); assert!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .disks .is_none() ); assert!(vmm.vm_add_disk(disk_config.clone()).unwrap().is_none()); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .disks .clone() .unwrap() .len(), 1 ); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .disks .clone() .unwrap()[0], disk_config ); } #[test] fn test_vmm_vm_cold_add_fs() { let mut vmm = create_dummy_vmm(); let fs_config = FsConfig::parse("tag=mytag,socket=/tmp/sock").unwrap(); assert!(matches!( vmm.vm_add_fs(fs_config.clone()), Err(VmError::VmNotCreated) )); let _ = vmm.vm_create(create_dummy_vm_config()); assert!(vmm.vm_config.as_ref().unwrap().lock().unwrap().fs.is_none()); assert!(vmm.vm_add_fs(fs_config.clone()).unwrap().is_none()); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .fs .clone() .unwrap() .len(), 1 ); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .fs .clone() .unwrap()[0], fs_config ); } #[test] fn test_vmm_vm_cold_add_generic_vhost_user() { let mut vmm = create_dummy_vmm(); let generic_vhost_user_config = GenericVhostUserConfig::parse("virtio_id=26,socket=/tmp/sock,queue_sizes=[1024]") .unwrap(); assert!(matches!( vmm.vm_add_generic_vhost_user(generic_vhost_user_config.clone()), Err(VmError::VmNotCreated) )); let _ = vmm.vm_create(create_dummy_vm_config()); assert!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .generic_vhost_user .is_none() ); assert!( vmm.vm_add_generic_vhost_user(generic_vhost_user_config.clone()) .unwrap() .is_none() ); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .generic_vhost_user .clone() .unwrap() .len(), 1 ); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .generic_vhost_user .clone() .unwrap()[0], generic_vhost_user_config ); } #[test] fn test_vmm_vm_cold_add_pmem() { let mut vmm = create_dummy_vmm(); let pmem_config = PmemConfig::parse("file=/tmp/pmem,size=128M").unwrap(); assert!(matches!( vmm.vm_add_pmem(pmem_config.clone()), Err(VmError::VmNotCreated) )); let _ = vmm.vm_create(create_dummy_vm_config()); assert!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .pmem .is_none() ); assert!(vmm.vm_add_pmem(pmem_config.clone()).unwrap().is_none()); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .pmem .clone() .unwrap() .len(), 1 ); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .pmem .clone() .unwrap()[0], pmem_config ); } #[test] fn test_vmm_vm_cold_add_net() { let mut vmm = create_dummy_vmm(); let net_config = NetConfig::parse( "mac=de:ad:be:ef:12:34,host_mac=12:34:de:ad:be:ef,vhost_user=true,socket=/tmp/sock", ) .unwrap(); assert!(matches!( vmm.vm_add_net(net_config.clone()), Err(VmError::VmNotCreated) )); let _ = vmm.vm_create(create_dummy_vm_config()); assert!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .net .is_none() ); assert!(vmm.vm_add_net(net_config.clone()).unwrap().is_none()); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .net .clone() .unwrap() .len(), 1 ); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .net .clone() .unwrap()[0], net_config ); } #[test] fn test_vmm_vm_cold_add_vdpa() { let mut vmm = create_dummy_vmm(); let vdpa_config = VdpaConfig::parse("path=/dev/vhost-vdpa,num_queues=2").unwrap(); assert!(matches!( vmm.vm_add_vdpa(vdpa_config.clone()), Err(VmError::VmNotCreated) )); let _ = vmm.vm_create(create_dummy_vm_config()); assert!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .vdpa .is_none() ); assert!(vmm.vm_add_vdpa(vdpa_config.clone()).unwrap().is_none()); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .vdpa .clone() .unwrap() .len(), 1 ); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .vdpa .clone() .unwrap()[0], vdpa_config ); } #[test] fn test_vmm_vm_cold_add_vsock() { let mut vmm = create_dummy_vmm(); let vsock_config = VsockConfig::parse("socket=/tmp/sock,cid=3,iommu=on").unwrap(); assert!(matches!( vmm.vm_add_vsock(vsock_config.clone()), Err(VmError::VmNotCreated) )); let _ = vmm.vm_create(create_dummy_vm_config()); assert!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .vsock .is_none() ); assert!(vmm.vm_add_vsock(vsock_config.clone()).unwrap().is_none()); assert_eq!( vmm.vm_config .as_ref() .unwrap() .lock() .unwrap() .vsock .clone() .unwrap(), vsock_config ); } }