Files
cloud-hypervisor/vmm/src/lib.rs
Philipp Schuster 36a3369802 vmm: drop misleading iommu option from serial config
`--serial` still accepted `iommu=on|off` from the shared serial/console
configuration shape, even though only virtio-console has a meaningful
IOMMU connection.

Cloud Hypervisor wires virtio-iommu support through DMA-capable
virtio/VFIO PCI endpoints. The serial devices are legacy UARTs accessed
through PIO or MMIO registers, so exposing an IOMMU option there is
misleading.

On-behalf-of: Philipp Schuster@sap.com
Signed-off-by: Philipp Schuster <philipp.schuster@cyberus-technology.de>
2026-05-11 20:18:03 +00:00

3196 lines
108 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
#[cfg(all(
feature = "kvm",
feature = "sev_snp",
feature = "fw_cfg",
target_arch = "x86_64"
))]
pub(crate) mod sev;
mod sigwinch_listener;
mod sync_utils;
mod uffd;
mod userfaultfd;
pub mod vm;
pub mod vm_config;
type GuestMemoryMmap = vm_memory::GuestMemoryMmap<AtomicBitmap>;
type GuestRegionMmap = vm_memory::GuestRegionMmap<AtomicBitmap>;
/// 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<ApiResponse>),
/// 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 shut the VM down
#[error("Error shutting down VM")]
VmShutdown(#[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<gdb::GdbResponse>),
#[error("Cannot spawn a signal handler thread")]
SignalHandlerSpawn(#[source] io::Error),
#[error("Failed to join on threads: {0:?}")]
ThreadCleanup(std::boxed::Box<dyn std::any::Any + std::marker::Send>),
/// 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(),
#[cfg(feature = "sev_snp")]
vmsa_features: 0,
nested: _value.cpus.nested,
smt_enabled: _value
.cpus
.topology
.as_ref()
.is_some_and(|t| t.threads_per_core > 1),
}
}
}
pub type Result<T> = result::Result<T, Error>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u64)]
pub enum EpollDispatch {
Exit = 0,
Reset = 1,
Api = 2,
ActivateVirtioDevices = 3,
Debug = 4,
GuestExit = 5,
Unknown,
}
impl From<u64> for EpollDispatch {
fn from(v: u64) -> Self {
use EpollDispatch::*;
match v {
0 => Exit,
1 => Reset,
2 => Api,
3 => ActivateVirtioDevices,
4 => Debug,
5 => GuestExit,
_ => Unknown,
}
}
}
pub struct EpollContext {
epoll_file: File,
}
impl EpollContext {
pub fn new() -> result::Result<EpollContext, io::Error> {
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<T>(&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<T>(
&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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
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<String> {
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<thread::JoinHandle<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<String>,
http_fd: Option<RawFd>,
#[cfg(feature = "dbus_api")] dbus_options: Option<DBusApiOptions>,
api_event: EventFd,
api_sender: Sender<ApiRequest>,
api_receiver: Receiver<ApiRequest>,
#[cfg(feature = "guest_debug")] debug_path: Option<PathBuf>,
#[cfg(feature = "guest_debug")] debug_event: EventFd,
#[cfg(feature = "guest_debug")] vm_debug_event: EventFd,
exit_event: EventFd,
seccomp_action: &SeccompAction,
hypervisor: Arc<dyn hypervisor::Hypervisor>,
no_shutdown: bool,
landlock_enable: bool,
) -> Result<VmmThreadHandle> {
#[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,
no_shutdown,
)?;
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<T, E, F>(f: F) -> result::Result<(T, Duration), E>
where
F: FnOnce() -> result::Result<T, E>,
{
let begin = Instant::now();
let value = f()?;
let duration = begin.elapsed();
Ok((value, duration))
}
#[derive(Clone, Deserialize, Serialize)]
struct VmMigrationConfig {
vm_config: Arc<Mutex<VmConfig>>,
#[cfg(all(feature = "kvm", target_arch = "x86_64"))]
common_cpuid: Vec<hypervisor::arch::x86::CpuIdEntry>,
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<Result<()>>,
#[cfg(feature = "dbus_api")]
pub dbus_shutdown_chs: Option<DBusApiShutdownChannels>,
pub http_api_handle: Option<HttpApiHandle>,
}
pub struct Vmm {
epoll: EpollContext,
exit_evt: EventFd,
reset_evt: EventFd,
guest_exit_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>,
vm_config: Option<Arc<Mutex<VmConfig>>>,
seccomp_action: SeccompAction,
hypervisor: Arc<dyn hypervisor::Hypervisor>,
activate_evt: EventFd,
signals: Option<Handle>,
threads: Vec<thread::JoinHandle<()>>,
original_termios_opt: Arc<Mutex<Option<termios>>>,
console_resize_pipe: Option<Arc<File>>,
console_info: Option<ConsoleInfo>,
no_shutdown: bool,
}
/// Just a wrapper for the data that goes into
/// [`ReceiveMigrationState::Configured`]
struct ReceiveMigrationConfiguredData {
memory_manager: Arc<Mutex<MemoryManager>>,
guest_memory: GuestMemoryAtomic<GuestMemoryMmap>,
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<Option<termios>>,
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<dyn hypervisor::Hypervisor>,
exit_evt: EventFd,
no_shutdown: bool,
) -> Result<Self> {
let mut epoll = EpollContext::new().map_err(Error::Epoll)?;
let reset_evt = EventFd::new(EFD_NONBLOCK).map_err(Error::EventFdCreate)?;
let guest_exit_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(&guest_exit_evt, EpollDispatch::GuestExit)
.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,
guest_exit_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,
no_shutdown,
})
}
/// 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<ReceiveMigrationState, MigratableError> {
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<u32, File>|
-> std::result::Result<ReceiveMigrationConfiguredData, MigratableError> {
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<Vec<(u32, File)>, 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::CompletePaused => {
debug!("Migration (incoming): Receiving final state of a paused VM");
Ok(Completed)
}
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<T>(
&mut self,
req: &Request,
socket: &mut T,
existing_memory_files: HashMap<u32, File>,
) -> std::result::Result<Arc<Mutex<MemoryManager>>, MigratableError>
where
T: Read,
{
// Read in config data along with memory manager data
let mut data: Vec<u8> = 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<T>(
&mut self,
req: &Request,
socket: &mut T,
mm: Arc<Mutex<MemoryManager>>,
) -> 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<u8> = 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}"))
})?;
let guest_exit_evt = self.guest_exit_evt.try_clone().map_err(|e| {
MigratableError::MigrateReceive(anyhow!("Error cloning guest exit 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,
guest_exit_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),
#[cfg(feature = "igvm")]
None,
)
.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<bool, MigratableError>,
mem_send: &mut SendAdditionalConnections,
) -> result::Result<MemoryRangeTable /* remaining */, MigratableError> {
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<bool, MigratableError> {
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();
if vm.get_state() != VmState::Paused {
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,
initial_vm_state: VmState,
) -> 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 (skip if already paused, e.g. migrating a paused VM)
let downtime_begin = Instant::now();
if vm.get_state() != VmState::Paused {
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_req = if initial_vm_state == VmState::Running {
Request::complete()
} else {
Request::complete_paused()
};
let (_, complete_duration) = measure_ok(|| {
migration_transport::send_request_expect_ok(
&mut socket,
complete_req,
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<Mutex<VmConfig>>,
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<Mutex<VmConfig>>,
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)?;
let guest_exit_evt = self
.guest_exit_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,
guest_exit_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<ApiRequest>,
#[cfg(feature = "guest_debug")] gdb_receiver: &Receiver<gdb::GdbRequest>,
) -> 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::GuestExit => {
info!("VM guest exit event");
self.guest_exit_evt.read().map_err(Error::EventFdRead)?;
if self.no_shutdown {
self.vm_shutdown().map_err(Error::VmShutdown)?;
} else {
self.vmm_shutdown().map_err(Error::VmmShutdown)?;
break 'outer;
}
}
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<VmConfig>) -> 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)?;
let guest_exit_evt = self
.guest_exit_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,
guest_exit_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)?;
let guest_exit_evt = self
.guest_exit_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,
guest_exit_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<VmInfoResponse, VmError> {
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<u32>,
desired_ram: Option<u64>,
desired_balloon: Option<u64>,
) -> 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<Option<Vec<u8>>, 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<Option<Vec<u8>>, 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<Option<Vec<u8>>, 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<Option<Vec<u8>>, 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<Option<Vec<u8>>, 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<Option<Vec<u8>>, 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<Option<Vec<u8>>, 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<Option<Vec<u8>>, 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<Option<Vec<u8>>, 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<Option<Vec<u8>>, 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")))?;
let initial_vm_state = vm.get_state();
if initial_vm_state != VmState::Running && initial_vm_state != VmState::Paused {
return Err(MigratableError::MigrateSend(anyhow!(
"VM is not running or paused: {initial_vm_state:?}"
)));
}
event!("vm", "migration-started");
Self::send_migration(
vm,
#[cfg(all(feature = "kvm", target_arch = "x86_64"))]
self.hypervisor.as_ref(),
&send_data_migration,
initial_vm_state,
)
.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;
}
// Only resume if the VM was originally running; a VM that was already
// paused before migration should remain paused after failure.
if initial_vm_state == VmState::Running
&& 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::{
CommonConsoleConfig, ConsoleConfig, ConsoleOutputMode, CoreScheduling, CpuFeatures,
CpusConfig, HotplugMethod, MemoryConfig, PayloadConfig, PciDeviceCommonConfig, RngConfig,
SerialConfig,
};
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(),
false,
)
.unwrap()
}
fn create_dummy_vm_config() -> Box<VmConfig> {
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"),
pci_common: PciDeviceCommonConfig::default(),
},
balloon: None,
fs: None,
generic_vhost_user: None,
pmem: None,
serial: SerialConfig {
common: CommonConsoleConfig {
file: None,
mode: ConsoleOutputMode::Null,
socket: None,
},
},
console: ConsoleConfig {
common: CommonConsoleConfig {
file: None,
// Caution: Don't use `Tty` to not mess with users terminal
mode: ConsoleOutputMode::Off,
socket: None,
},
iommu: false,
},
#[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
);
}
}