Files
cloud-hypervisor/vmm/src/vm_config.rs
Philipp Schuster 04bc6b3ccc vmm, main: split ConsoleConfig and SerialConfig into separate structs
This commit introduces a new struct `CommonConsoleConfig` which is the
base for the split into `ConsoleConfig` and `SerialConfig`. This is a
pre-requisite for allowing more configurable PCI options for the
virtio-console device.

The commit doesn't change or add any functionality.

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

1196 lines
34 KiB
Rust

// Copyright © 2022 Intel Corporation
//
// SPDX-License-Identifier: Apache-2.0
//
use std::net::IpAddr;
use std::path::{Path, PathBuf};
#[cfg(feature = "fw_cfg")]
use std::str::FromStr;
use std::{fs, result};
use block::ImageType;
pub use block::fcntl::LockGranularityChoice;
use log::{debug, warn};
use net_util::MacAddr;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use virtio_devices::RateLimiterConfig;
use crate::Landlock;
use crate::landlock::LandlockError;
pub type LandlockResult<T> = result::Result<T, LandlockError>;
/// Trait to apply Landlock on VmConfig elements
pub(crate) trait ApplyLandlock {
/// Apply Landlock rules to file paths
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()>;
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct CpuAffinity {
pub vcpu: u32,
pub host_cpus: Vec<usize>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct CpuFeatures {
#[cfg(target_arch = "x86_64")]
#[serde(default)]
pub amx: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub enum CoreScheduling {
#[default]
Vm, // All vCPUs have the same cookie so can share a core
Vcpu, // Each vCPU has a unique cookie so can't share a core
Off,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct CpuTopology {
pub threads_per_core: u16,
pub cores_per_die: u16,
pub dies_per_package: u16,
pub packages: u16,
}
// When booting with PVH boot the maximum physical addressable size
// is a 46 bit address space even when the host supports with 5-level
// paging.
pub const DEFAULT_MAX_PHYS_BITS: u8 = 46;
pub fn default_cpuconfig_max_phys_bits() -> u8 {
DEFAULT_MAX_PHYS_BITS
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct CpusConfig {
pub boot_vcpus: u32,
pub max_vcpus: u32,
#[serde(default)]
pub topology: Option<CpuTopology>,
#[serde(default)]
pub kvm_hyperv: bool,
#[serde(default = "default_cpuconfig_max_phys_bits")]
pub max_phys_bits: u8,
#[serde(default)]
pub affinity: Option<Vec<CpuAffinity>>,
#[serde(default)]
pub features: CpuFeatures,
#[serde(default = "default_cpusconfig_nested")]
pub nested: bool,
#[serde(default)]
pub core_scheduling: CoreScheduling,
}
pub const DEFAULT_VCPUS: u32 = 1;
impl Default for CpusConfig {
fn default() -> Self {
CpusConfig {
boot_vcpus: DEFAULT_VCPUS,
max_vcpus: DEFAULT_VCPUS,
topology: None,
kvm_hyperv: false,
max_phys_bits: DEFAULT_MAX_PHYS_BITS,
affinity: None,
features: CpuFeatures::default(),
nested: true,
core_scheduling: CoreScheduling::default(),
}
}
}
pub const DEFAULT_NUM_PCI_SEGMENTS: u16 = 1;
pub fn default_platformconfig_num_pci_segments() -> u16 {
DEFAULT_NUM_PCI_SEGMENTS
}
pub const DEFAULT_IOMMU_ADDRESS_WIDTH_BITS: u8 = 64;
pub fn default_platformconfig_iommu_address_width_bits() -> u8 {
DEFAULT_IOMMU_ADDRESS_WIDTH_BITS
}
pub fn default_platformconfig_vfio_p2p_dma() -> bool {
true
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct PlatformConfig {
#[serde(default = "default_platformconfig_num_pci_segments")]
pub num_pci_segments: u16,
#[serde(default)]
pub iommu_segments: Option<Vec<u16>>,
#[serde(default = "default_platformconfig_iommu_address_width_bits")]
pub iommu_address_width_bits: u8,
#[serde(default)]
pub serial_number: Option<String>,
#[serde(default)]
pub uuid: Option<String>,
#[serde(default)]
pub oem_strings: Option<Vec<String>>,
#[cfg(feature = "tdx")]
#[serde(default)]
pub tdx: bool,
#[cfg(feature = "sev_snp")]
#[serde(default)]
pub sev_snp: bool,
#[serde(default)]
pub iommufd: bool,
#[serde(default = "default_platformconfig_vfio_p2p_dma")]
pub vfio_p2p_dma: bool,
}
pub const DEFAULT_PCI_SEGMENT_APERTURE_WEIGHT: u32 = 1;
fn default_pci_segment_aperture_weight() -> u32 {
DEFAULT_PCI_SEGMENT_APERTURE_WEIGHT
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct PciSegmentConfig {
#[serde(default)]
pub pci_segment: u16,
#[serde(default = "default_pci_segment_aperture_weight")]
pub mmio32_aperture_weight: u32,
#[serde(default = "default_pci_segment_aperture_weight")]
pub mmio64_aperture_weight: u32,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
pub struct MemoryZoneConfig {
pub id: String,
pub size: u64,
#[serde(default)]
pub file: Option<PathBuf>,
#[serde(default)]
pub shared: bool,
#[serde(default)]
pub hugepages: bool,
#[serde(default)]
pub hugepage_size: Option<u64>,
#[serde(default)]
pub host_numa_node: Option<u32>,
#[serde(default)]
pub hotplug_size: Option<u64>,
#[serde(default)]
pub hotplugged_size: Option<u64>,
#[serde(default)]
pub prefault: bool,
#[serde(default)]
pub mergeable: bool,
}
impl ApplyLandlock for MemoryZoneConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
if let Some(file) = &self.file {
landlock.add_rule_with_access(file, "rw")?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub enum HotplugMethod {
#[default]
Acpi,
VirtioMem,
}
fn default_memoryconfig_thp() -> bool {
true
}
fn default_cpusconfig_nested() -> bool {
true
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct MemoryConfig {
pub size: u64,
#[serde(default)]
pub mergeable: bool,
#[serde(default)]
pub hotplug_method: HotplugMethod,
#[serde(default)]
pub hotplug_size: Option<u64>,
#[serde(default)]
pub hotplugged_size: Option<u64>,
#[serde(default)]
pub shared: bool,
#[serde(default)]
pub hugepages: bool,
#[serde(default)]
pub hugepage_size: Option<u64>,
#[serde(default)]
pub prefault: bool,
#[serde(default)]
pub zones: Option<Vec<MemoryZoneConfig>>,
#[serde(default = "default_memoryconfig_thp")]
pub thp: bool,
}
pub const DEFAULT_MEMORY_MB: u64 = 512;
impl Default for MemoryConfig {
fn default() -> Self {
MemoryConfig {
size: DEFAULT_MEMORY_MB << 20,
mergeable: false,
hotplug_method: HotplugMethod::Acpi,
hotplug_size: None,
hotplugged_size: None,
shared: false,
hugepages: false,
hugepage_size: None,
prefault: false,
zones: None,
thp: true,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub enum VhostMode {
#[default]
Client,
Server,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct RateLimiterGroupConfig {
#[serde(default)]
pub id: String,
#[serde(default)]
pub rate_limiter_config: RateLimiterConfig,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct VirtQueueAffinity {
pub queue_index: u16,
pub host_cpus: Vec<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct PciDeviceCommonConfig {
#[serde(default)]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "<&bool as std::ops::Not>::not")]
pub iommu: bool,
#[serde(default)]
pub pci_segment: u16,
#[serde(default)]
pub pci_device_id: Option<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct DiskConfig {
#[serde(flatten)]
pub pci_common: PciDeviceCommonConfig,
pub path: Option<PathBuf>,
#[serde(default)]
pub readonly: bool,
#[serde(default)]
pub direct: bool,
#[serde(default = "default_diskconfig_num_queues")]
pub num_queues: usize,
#[serde(default = "default_diskconfig_queue_size")]
pub queue_size: u16,
#[serde(default)]
pub vhost_user: bool,
pub vhost_socket: Option<String>,
#[serde(default)]
pub rate_limit_group: Option<String>,
#[serde(default)]
pub rate_limiter_config: Option<RateLimiterConfig>,
// For testing use only. Not exposed in API.
#[serde(default)]
pub disable_io_uring: bool,
// For testing use only. Not exposed in API.
#[serde(default)]
pub disable_aio: bool,
#[serde(default)]
pub serial: Option<String>,
#[serde(default)]
pub queue_affinity: Option<Vec<VirtQueueAffinity>>,
#[serde(default)]
pub backing_files: bool,
#[serde(default = "default_diskconfig_sparse")]
pub sparse: bool,
#[serde(default)]
pub image_type: ImageType,
#[serde(default)]
pub lock_granularity: LockGranularityChoice,
}
impl ApplyLandlock for DiskConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
if let Some(path) = &self.path {
landlock.add_rule_with_access(path, "rw")?;
}
Ok(())
}
}
pub const DEFAULT_DISK_NUM_QUEUES: usize = 1;
pub fn default_diskconfig_num_queues() -> usize {
DEFAULT_DISK_NUM_QUEUES
}
pub const DEFAULT_DISK_QUEUE_SIZE: u16 = 128;
pub fn default_diskconfig_queue_size() -> u16 {
DEFAULT_DISK_QUEUE_SIZE
}
pub fn default_diskconfig_sparse() -> bool {
true
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct NetConfig {
#[serde(flatten)]
pub pci_common: PciDeviceCommonConfig,
#[serde(default = "default_netconfig_tap")]
pub tap: Option<String>,
pub ip: Option<IpAddr>,
pub mask: Option<IpAddr>,
#[serde(default = "default_netconfig_mac")]
pub mac: MacAddr,
#[serde(default)]
pub host_mac: Option<MacAddr>,
#[serde(default)]
pub mtu: Option<u16>,
#[serde(default = "default_netconfig_num_queues")]
pub num_queues: usize,
#[serde(default = "default_netconfig_queue_size")]
pub queue_size: u16,
#[serde(default)]
pub vhost_user: bool,
pub vhost_socket: Option<String>,
#[serde(default)]
pub vhost_mode: VhostMode,
// Special deserialize handling:
// Therefore, we don't serialize FDs, and whatever value is here after
// deserialization is invalid.
//
// Valid FDs are transmitted via a different channel (SCM_RIGHTS message)
// and will be populated into this struct on the destination VMM eventually.
#[serde(default, deserialize_with = "deserialize_netconfig_fds")]
pub fds: Option<Vec<i32>>,
#[serde(default)]
pub rate_limiter_config: Option<RateLimiterConfig>,
#[serde(default = "default_netconfig_true")]
pub offload_tso: bool,
#[serde(default = "default_netconfig_true")]
pub offload_ufo: bool,
#[serde(default = "default_netconfig_true")]
pub offload_csum: bool,
}
pub fn default_netconfig_true() -> bool {
true
}
pub fn default_netconfig_tap() -> Option<String> {
None
}
pub fn default_netconfig_mac() -> MacAddr {
MacAddr::local_random()
}
pub const DEFAULT_NET_NUM_QUEUES: usize = 2;
pub fn default_netconfig_num_queues() -> usize {
DEFAULT_NET_NUM_QUEUES
}
pub const DEFAULT_NET_QUEUE_SIZE: u16 = 256;
pub fn default_netconfig_queue_size() -> u16 {
DEFAULT_NET_QUEUE_SIZE
}
fn deserialize_netconfig_fds<'de, D>(d: D) -> Result<Option<Vec<i32>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let invalid_fds: Option<Vec<i32>> = Option::deserialize(d)?;
if let Some(invalid_fds) = invalid_fds {
debug!(
"FDs in 'NetConfig' won't be deserialized as they are most likely invalid now. Deserializing them as -1."
);
Ok(Some(vec![-1; invalid_fds.len()]))
} else {
Ok(None)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct RngConfig {
#[serde(flatten)]
pub pci_common: PciDeviceCommonConfig,
pub src: PathBuf,
}
impl RngConfig {
pub const DEFAULT_RNG_SOURCE: &str = "/dev/urandom";
}
impl Default for RngConfig {
fn default() -> Self {
RngConfig {
src: PathBuf::from(Self::DEFAULT_RNG_SOURCE),
pci_common: PciDeviceCommonConfig::default(),
}
}
}
impl ApplyLandlock for RngConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
// Rng Path only need read access
landlock.add_rule_with_access(&self.src, "r")?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct BalloonConfig {
pub size: u64,
/// Option to deflate the balloon in case the guest is out of memory.
#[serde(default)]
pub deflate_on_oom: bool,
/// Option to enable free page reporting from the guest.
#[serde(default)]
pub free_page_reporting: bool,
}
#[cfg(feature = "pvmemcontrol")]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)]
pub struct PvmemcontrolConfig {}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct FsConfig {
#[serde(flatten)]
pub pci_common: PciDeviceCommonConfig,
pub tag: String,
pub socket: PathBuf,
#[serde(default = "default_fsconfig_num_queues")]
pub num_queues: usize,
#[serde(default = "default_fsconfig_queue_size")]
pub queue_size: u16,
}
pub fn default_fsconfig_num_queues() -> usize {
1
}
pub fn default_fsconfig_queue_size() -> u16 {
1024
}
impl ApplyLandlock for FsConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
landlock.add_rule_with_access(&self.socket, "rw")?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct GenericVhostUserConfig {
#[serde(flatten)]
pub pci_common: PciDeviceCommonConfig,
pub socket: PathBuf,
pub queue_sizes: Vec<u16>,
pub device_type: u32,
}
impl ApplyLandlock for GenericVhostUserConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
landlock.add_rule_with_access(&self.socket, "rw")?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct PmemConfig {
#[serde(flatten)]
pub pci_common: PciDeviceCommonConfig,
pub file: PathBuf,
#[serde(default)]
pub size: Option<u64>,
#[serde(default)]
pub discard_writes: bool,
}
impl ApplyLandlock for PmemConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
let access = if self.discard_writes { "r" } else { "rw" };
landlock.add_rule_with_access(&self.file, access)?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum ConsoleOutputMode {
Off,
Pty,
Tty,
File,
Socket,
Null,
}
/// Common configuration for plain console configs.
///
/// Independent of PCI or legacy devices.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct CommonConsoleConfig {
#[serde(default)]
pub file: Option<PathBuf>,
pub mode: ConsoleOutputMode,
#[serde(default)]
pub socket: Option<PathBuf>,
}
impl ApplyLandlock for CommonConsoleConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
if self.mode == ConsoleOutputMode::Pty {
landlock.add_rule_with_access(Path::new("/dev/pts"), "rw")?;
landlock.add_rule_with_access(Path::new("/dev/ptmx"), "rw")?;
}
if let Some(file) = &self.file {
landlock.add_rule_with_access(file, "rw")?;
}
if let Some(socket) = &self.socket {
landlock.add_rule_with_access(socket, "rw")?;
}
Ok(())
}
}
/// Configuration for a legacy serial console device.
///
/// - On x86_64, this is a port I/O-mapped UART16550-compatible device
/// - On aarch64, this is a MMIO-mapped PL011 device
/// - On RISCV, this is a MMIO-mapped UART16550-compatible device
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct SerialConfig {
#[serde(flatten)]
pub common: CommonConsoleConfig,
#[serde(default, skip_serializing_if = "<&bool as std::ops::Not>::not")]
pub iommu: bool,
}
impl SerialConfig {
pub const SYNTAX: &str =
"Control serial port: \"off|null|pty|tty|file=<path>|socket=<path>,iommu=on|off\"";
}
impl Default for SerialConfig {
fn default() -> Self {
Self {
common: CommonConsoleConfig {
file: None,
mode: ConsoleOutputMode::Null,
socket: None,
},
iommu: false,
}
}
}
impl ApplyLandlock for SerialConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
self.common.apply_landlock(landlock)
}
}
/// Configuration for a virtio-console device.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct ConsoleConfig {
#[serde(flatten)]
pub common: CommonConsoleConfig,
#[serde(default, skip_serializing_if = "<&bool as std::ops::Not>::not")]
pub iommu: bool,
}
impl ConsoleConfig {
pub const SYNTAX: &str =
"Control (virtio) console: \"off|null|pty|tty|file=<path>,iommu=on|off\"";
}
impl Default for ConsoleConfig {
fn default() -> Self {
Self {
common: CommonConsoleConfig {
file: None,
mode: ConsoleOutputMode::Tty,
socket: None,
},
iommu: false,
}
}
}
impl ApplyLandlock for ConsoleConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
self.common.apply_landlock(landlock)
}
}
#[cfg(target_arch = "x86_64")]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct DebugConsoleConfig {
#[serde(default)]
pub file: Option<PathBuf>,
pub mode: ConsoleOutputMode,
/// Optionally dedicated I/O-port, if the default port should not be used.
pub iobase: Option<u16>,
}
#[cfg(target_arch = "x86_64")]
impl Default for DebugConsoleConfig {
fn default() -> Self {
Self {
file: None,
mode: ConsoleOutputMode::Off,
iobase: Some(devices::debug_console::DEFAULT_PORT as u16),
}
}
}
#[cfg(target_arch = "x86_64")]
impl ApplyLandlock for DebugConsoleConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
if self.mode == ConsoleOutputMode::Pty {
landlock.add_rule_with_access(Path::new("/dev/pts"), "rw")?;
landlock.add_rule_with_access(Path::new("/dev/ptmx"), "rw")?;
}
if let Some(file) = &self.file {
landlock.add_rule_with_access(file, "rw")?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct DeviceConfig {
#[serde(flatten)]
pub pci_common: PciDeviceCommonConfig,
pub path: PathBuf,
#[serde(default)]
pub x_nv_gpudirect_clique: Option<u8>,
#[serde(default)]
pub x_exclude_mmap_bars: Vec<u64>,
}
impl ApplyLandlock for DeviceConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
let device_path = fs::read_link(self.path.as_path()).map_err(LandlockError::OpenPath)?;
let iommu_group = device_path.file_name();
let iommu_group_str = iommu_group
.ok_or(LandlockError::InvalidPath)?
.to_str()
.ok_or(LandlockError::InvalidPath)?;
let mut vfio_group_path = PathBuf::from("/dev/vfio");
vfio_group_path.push(iommu_group_str);
landlock.add_rule_with_access(&vfio_group_path, "rw")?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct UserDeviceConfig {
#[serde(flatten)]
pub pci_common: PciDeviceCommonConfig,
pub socket: PathBuf,
}
impl ApplyLandlock for UserDeviceConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
landlock.add_rule_with_access(&self.socket, "rw")?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct VdpaConfig {
#[serde(flatten)]
pub pci_common: PciDeviceCommonConfig,
pub path: PathBuf,
#[serde(default = "default_vdpaconfig_num_queues")]
pub num_queues: usize,
}
pub fn default_vdpaconfig_num_queues() -> usize {
1
}
impl ApplyLandlock for VdpaConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
landlock.add_rule_with_access(&self.path, "rw")?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct VsockConfig {
#[serde(flatten)]
pub pci_common: PciDeviceCommonConfig,
pub cid: u32,
pub socket: PathBuf,
}
impl ApplyLandlock for VsockConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
if let Some(parent) = self.socket.parent() {
landlock.add_rule_with_access(parent, "w")?;
}
landlock.add_rule_with_access(&self.socket, "rw")?;
Ok(())
}
}
#[cfg(feature = "ivshmem")]
pub const DEFAULT_IVSHMEM_SIZE: usize = 128;
#[cfg(feature = "ivshmem")]
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct IvshmemConfig {
pub path: PathBuf,
pub size: usize,
}
#[cfg(feature = "ivshmem")]
impl Default for IvshmemConfig {
fn default() -> Self {
Self {
path: PathBuf::new(),
size: DEFAULT_IVSHMEM_SIZE << 20,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct NumaDistance {
#[serde(default)]
pub destination: u32,
#[serde(default)]
pub distance: u8,
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct NumaConfig {
pub guest_numa_id: u32,
#[serde(default)]
pub cpus: Option<Vec<u32>>,
#[serde(default)]
pub distances: Option<Vec<NumaDistance>>,
#[serde(default)]
pub memory_zones: Option<Vec<String>>,
#[serde(default)]
pub pci_segments: Option<Vec<u16>>,
#[serde(default)]
pub device_id: Option<String>,
}
/// Errors describing a misconfigured payload, i.e., a configuration that
/// can't be booted by Cloud Hypervisor.
///
/// This typically is the case for invalid combinations of cmdline, kernel,
/// firmware, and initrd.
#[derive(Debug, Error, PartialEq, Eq)]
pub enum PayloadConfigError {
/// Specifying a kernel is not supported when a firmware is provided.
#[error("Specifying a kernel is not supported when a firmware is provided")]
FirmwarePlusOtherPayloads,
/// No bootitem provided: neither firmware nor kernel.
#[error("No bootitem provided: neither firmware nor kernel")]
MissingBootitem,
#[cfg(feature = "igvm")]
/// Specifying a kernel or firmware is not supported when an igvm is provided.
#[error("Specifying a kernel or firmware is not supported when an igvm is provided")]
IgvmPlusOtherPayloads,
#[cfg(feature = "fw_cfg")]
/// FwCfg missing kernel
#[error("Error --fw-cfg-config: missing --kernel")]
FwCfgMissingKernel,
#[cfg(feature = "fw_cfg")]
/// FwCfg missing cmdline
#[error("Error --fw-cfg-config: missing --cmdline")]
FwCfgMissingCmdline,
#[cfg(feature = "fw_cfg")]
/// FwCfg missing initramfs
#[error("Error --fw-cfg-config: missing --initramfs")]
FwCfgMissingInitramfs,
#[cfg(feature = "fw_cfg")]
/// Invalid fw_cfg item content
#[error(
"Error --fw-cfg-config: invalid item '{0}' (exactly one of 'file' or 'string' is required)"
)]
FwCfgInvalidItem(String),
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct PayloadConfig {
#[serde(default)]
pub firmware: Option<PathBuf>,
#[serde(default)]
pub kernel: Option<PathBuf>,
#[serde(default)]
pub cmdline: Option<String>,
#[serde(default)]
pub initramfs: Option<PathBuf>,
#[cfg(feature = "igvm")]
#[serde(default)]
pub igvm: Option<PathBuf>,
#[cfg(feature = "sev_snp")]
#[serde(default)]
pub host_data: Option<String>,
#[cfg(feature = "fw_cfg")]
pub fw_cfg_config: Option<FwCfgConfig>,
}
#[cfg(feature = "fw_cfg")]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct FwCfgConfig {
pub e820: bool,
pub kernel: bool,
pub cmdline: bool,
pub initramfs: bool,
pub acpi_tables: bool,
pub items: Option<FwCfgItemList>,
}
#[cfg(feature = "fw_cfg")]
impl Default for FwCfgConfig {
fn default() -> Self {
FwCfgConfig {
e820: true,
kernel: true,
cmdline: true,
initramfs: true,
acpi_tables: true,
items: None,
}
}
}
#[cfg(feature = "fw_cfg")]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct FwCfgItemList {
#[serde(default)]
pub item_list: Vec<FwCfgItem>,
}
#[cfg(feature = "fw_cfg")]
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct FwCfgItem {
#[serde(default)]
pub name: String,
#[serde(default)]
pub file: Option<PathBuf>,
#[serde(default)]
pub string: Option<String>,
}
#[cfg(feature = "fw_cfg")]
pub enum FwCfgItemError {
InvalidValue(String),
}
#[cfg(feature = "fw_cfg")]
impl FromStr for FwCfgItemList {
type Err = FwCfgItemError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let body = s
.trim()
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.ok_or_else(|| FwCfgItemError::InvalidValue(s.to_string()))?;
let mut fw_cfg_items: Vec<FwCfgItem> = vec![];
let items: Vec<&str> = body.split(':').collect();
for item in items {
fw_cfg_items.push(
FwCfgItem::parse(item)
.map_err(|_| FwCfgItemError::InvalidValue(item.to_string()))?,
);
}
Ok(FwCfgItemList {
item_list: fw_cfg_items,
})
}
}
impl PayloadConfig {
/// Validates the payload config.
///
/// Succeeds if Cloud Hypervisor will be able to boot the configuration.
/// Further, warns for some odd configurations.
pub fn validate(&mut self) -> Result<(), PayloadConfigError> {
#[cfg(feature = "igvm")]
{
if self.igvm.is_some() {
if self.firmware.is_some() {
return Err(PayloadConfigError::IgvmPlusOtherPayloads);
}
return Ok(());
}
}
match (&self.firmware, &self.kernel) {
(Some(_firmware), Some(_kernel)) => Err(PayloadConfigError::FirmwarePlusOtherPayloads),
(Some(_firmware), None) => {
if self.cmdline.is_some() {
warn!("Ignoring cmdline parameter as firmware is provided as the payload");
self.cmdline = None;
}
if self.initramfs.is_some() {
warn!("Ignoring initramfs parameter as firmware is provided as the payload");
self.initramfs = None;
}
Ok(())
}
(None, Some(_kernel)) => Ok(()),
(None, None) => Err(PayloadConfigError::MissingBootitem),
}?;
#[cfg(feature = "fw_cfg")]
if let Some(fw_cfg_config) = &self.fw_cfg_config {
fw_cfg_config.validate(self)?;
}
Ok(())
}
}
impl ApplyLandlock for PayloadConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
// Payload only needs read access
if let Some(firmware) = &self.firmware {
landlock.add_rule_with_access(firmware, "r")?;
}
if let Some(kernel) = &self.kernel {
landlock.add_rule_with_access(kernel, "r")?;
}
if let Some(initramfs) = &self.initramfs {
landlock.add_rule_with_access(initramfs, "r")?;
}
#[cfg(feature = "igvm")]
if let Some(igvm) = &self.igvm {
landlock.add_rule_with_access(igvm, "r")?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct TpmConfig {
pub socket: PathBuf,
}
impl ApplyLandlock for TpmConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
landlock.add_rule_with_access(&self.socket, "rw")?;
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct LandlockConfig {
pub path: PathBuf,
pub access: String,
}
impl ApplyLandlock for LandlockConfig {
fn apply_landlock(&self, landlock: &mut Landlock) -> LandlockResult<()> {
landlock.add_rule_with_access(&self.path, self.access.clone().as_str())?;
Ok(())
}
}
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct VmConfig {
#[serde(default)]
pub cpus: CpusConfig,
#[serde(default)]
pub memory: MemoryConfig,
pub payload: Option<PayloadConfig>,
pub rate_limit_groups: Option<Vec<RateLimiterGroupConfig>>,
pub disks: Option<Vec<DiskConfig>>,
pub net: Option<Vec<NetConfig>>,
#[serde(default)]
pub rng: RngConfig,
pub balloon: Option<BalloonConfig>,
pub generic_vhost_user: Option<Vec<GenericVhostUserConfig>>,
pub fs: Option<Vec<FsConfig>>,
pub pmem: Option<Vec<PmemConfig>>,
#[serde(default)]
pub serial: SerialConfig,
#[serde(default)]
pub console: ConsoleConfig,
#[cfg(target_arch = "x86_64")]
#[serde(default)]
pub debug_console: DebugConsoleConfig,
pub devices: Option<Vec<DeviceConfig>>,
pub user_devices: Option<Vec<UserDeviceConfig>>,
pub vdpa: Option<Vec<VdpaConfig>>,
pub vsock: Option<VsockConfig>,
#[cfg(feature = "pvmemcontrol")]
#[serde(default)]
pub pvmemcontrol: Option<PvmemcontrolConfig>,
#[serde(default)]
pub pvpanic: bool,
#[serde(default)]
pub iommu: bool,
pub numa: Option<Vec<NumaConfig>>,
#[serde(default)]
pub watchdog: bool,
#[cfg(feature = "guest_debug")]
#[serde(default)]
pub gdb: bool,
pub pci_segments: Option<Vec<PciSegmentConfig>>,
pub platform: Option<PlatformConfig>,
pub tpm: Option<TpmConfig>,
// Preserved FDs are the ones that share the same life-time as its holding
// VmConfig instance, such as FDs for creating TAP devices.
// Preserved FDs will stay open as long as the holding VmConfig instance is
// valid, and will be closed when the holding VmConfig instance is destroyed.
//
// This is populated as devices are added at runtime. Removing them again
// causes the FDs to be closed early. This allows management software to
// gracefully clean up resources (e.g., libvirt closes tap devices).
#[serde(skip)]
pub preserved_fds: Option<Vec<i32>>,
#[serde(default)]
pub landlock_enable: bool,
pub landlock_rules: Option<Vec<LandlockConfig>>,
#[cfg(feature = "ivshmem")]
pub ivshmem: Option<IvshmemConfig>,
}
impl VmConfig {
pub(crate) fn apply_landlock(&self) -> LandlockResult<()> {
let mut landlock = Landlock::new()?;
#[cfg(target_arch = "aarch64")]
{
landlock.add_rule_with_access(Path::new("/sys/devices/system/cpu/cpu0/cache"), "r")?;
}
if let Some(mem_zones) = &self.memory.zones {
for zone in mem_zones.iter() {
zone.apply_landlock(&mut landlock)?;
}
}
let disks = &self.disks;
if let Some(disks) = disks {
for disk in disks.iter() {
disk.apply_landlock(&mut landlock)?;
}
}
self.rng.apply_landlock(&mut landlock)?;
if let Some(fs_configs) = &self.fs {
for fs_config in fs_configs.iter() {
fs_config.apply_landlock(&mut landlock)?;
}
}
if let Some(generic_vhost_user_configs) = &self.generic_vhost_user {
for generic_vhost_user_config in generic_vhost_user_configs.iter() {
generic_vhost_user_config.apply_landlock(&mut landlock)?;
}
}
if let Some(pmem_configs) = &self.pmem {
for pmem_config in pmem_configs.iter() {
pmem_config.apply_landlock(&mut landlock)?;
}
}
self.console.apply_landlock(&mut landlock)?;
self.serial.apply_landlock(&mut landlock)?;
#[cfg(target_arch = "x86_64")]
{
self.debug_console.apply_landlock(&mut landlock)?;
}
if let Some(devices) = &self.devices {
landlock.add_rule_with_access(Path::new("/dev/vfio/vfio"), "rw")?;
for device in devices.iter() {
device.apply_landlock(&mut landlock)?;
}
}
if let Some(user_devices) = &self.user_devices {
for user_devices in user_devices.iter() {
user_devices.apply_landlock(&mut landlock)?;
}
}
if let Some(vdpa_configs) = &self.vdpa {
for vdpa_config in vdpa_configs.iter() {
vdpa_config.apply_landlock(&mut landlock)?;
}
}
if let Some(vsock_config) = &self.vsock {
vsock_config.apply_landlock(&mut landlock)?;
}
if let Some(payload) = &self.payload {
payload.apply_landlock(&mut landlock)?;
}
if let Some(tpm_config) = &self.tpm {
tpm_config.apply_landlock(&mut landlock)?;
}
if self.net.is_some() {
landlock.add_rule_with_access(Path::new("/dev/net/tun"), "rw")?;
}
if let Some(landlock_rules) = &self.landlock_rules {
for landlock_rule in landlock_rules.iter() {
landlock_rule.apply_landlock(&mut landlock)?;
}
}
landlock.restrict_self()?;
Ok(())
}
#[cfg(all(feature = "kvm", target_arch = "x86_64"))]
pub(crate) fn max_apic_id(&self) -> u32 {
if let Some(topology) = &self.cpus.topology {
arch::x86_64::get_max_x2apic_id((
topology.threads_per_core,
topology.cores_per_die,
topology.dies_per_package,
topology.packages,
))
} else {
self.cpus.max_vcpus
}
}
}