qemu/machine: implement Q35 emission and apply_q35_defaults

platform.rs changes for Phase 3 Q35 support:

apply_q35_defaults:
  Build per-socket memory backends (Ram or File based on mem_path), populate
  NUMA nodes, propagate numa_distances from HostTopology, and generate
  cold-plug pcie-root-ports on pcie.0 (slot=0..N-1, multifunction=off).
  GPU SMMU groups on Q35 use vfio-pci + GpuPci kind (not nohotplug) and no
  arm-smmuv3 (x86 uses the global IOMMU).

apply_host_defaults:
  Wire ProtectionDevice into Q35::kernel_irqchip="split" and
  confidential_guest_support, and copy it into Objects::protection.  Dispatch
  to apply_q35_defaults or apply_virt_defaults by machine type.

to_qemu_args / emit_q35_args:
  Q35 and virt/Grace require different emission ordering: virt needs
  memory-backend= on the machine line so backends must precede the machine
  flag, while Q35 does not.  Split into emit_q35_args and emit_virt_args.

  Q35 order: protection object → -machine → (backend + NUMA node) per socket
  → -numa dist entries → pxb GPU roots (pxb + root ports + per-device iommufd
  + vfio-pci) → cold-plug root ports on pcie.0.

emit_vfio_q35 / emit_vfio_grace:
  Two distinct VFIO device formats.  Q35: host→id→[vendor/device-id]→bus→
  [iommufd].  Grace: device→host→bus→[rombar]→id→[iommufd].

emit_root_port:
  Handles optional slot, multifunction, and io_reserve fields, covering both
  Q35 cold-plug (slot+multifunction) and Grace (io_reserve) formats.

emit_machine:
  Q35 path now emits kernel_irqchip and confidential-guest-support when set.

from_config_defaults:
  Takes machine_type as first arg (was 0-arg in tests).  Q35 variant sets
  memory_backend: None since Q35 memory is expressed per NUMA node.

Signed-off-by: Zvonko Kaiser <zkaiser@nvidia.com>
Assisted-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Zvonko Kaiser <zkaiser@nvidia.com>
This commit is contained in:
Zvonko Kaiser
2026-07-14 14:58:52 +00:00
parent fb9d778065
commit e83210c041

View File

@@ -8,7 +8,7 @@ use anyhow::{bail, Result};
use kata_types::config::hypervisor::Hypervisor as HypervisorConfig;
use super::{
probe::{HostTopology, SocketInfo},
probe::{HostTopology, ProtectionDevice, SocketInfo},
pseries::Pseries,
q35::Q35,
s390x::S390xCcwVirtio,
@@ -57,6 +57,7 @@ pub(crate) struct BaseMachine {
/// ID of the primary memory backend, written as `-machine memory-backend=<id>`.
/// `None` for topologies that supply memory per NUMA node via `-numa node,memdev=`
/// rather than a single machine-wide backend (e.g. multi-socket vEGM).
/// Q35 never uses this field; virt uses it for the single-backend case.
pub memory_backend: Option<String>,
pub cpu: CpuConfig,
}
@@ -113,12 +114,18 @@ pub(crate) const SNP_CRYPTO_FEATURES: &[&str] = &[
];
pub(crate) struct Objects {
/// Shared iommufd object used by all Grace GPU devices.
/// For CoCo x86, per-device iommufd is stored on `VfioDevice::iommufd_id` instead.
pub iommufd: Option<IommufdBackend>,
pub memory_backends: Vec<MemoryBackend>,
pub numa_nodes: Vec<NumaNode>,
/// `-numa dist` entries; emitted after all NUMA nodes.
pub numa_distances: Vec<(u32, u32, u32)>,
pub thread_contexts: Vec<ThreadContext>,
pub acpi_links: Vec<AcpiPciNodeLink>,
pub rng: Option<ObjectRngRandom>,
/// CoCo protection object (`sev-snp-guest` or `tdx-guest`); emitted before `-machine`.
pub protection: Option<ProtectionDevice>,
}
pub(crate) struct IommufdBackend {
@@ -126,8 +133,29 @@ pub(crate) struct IommufdBackend {
}
pub(crate) enum MemoryBackend {
Ram { id: String, size: u64 },
File { id: String, size: u64, path: String, prealloc: bool, share: bool, is_egm: bool },
Ram {
id: String,
size: u64,
/// `host-nodes=N` — NUMA pinning for Q35 CoCo RAM-backed memory.
host_nodes: Option<u32>,
/// `policy=bind` — always paired with `host_nodes`.
policy: Option<String>,
},
/// File-backed memory.
/// path = "/dev/shm" -- NUMA-pinned SHM for vanilla Q35 (host_nodes set)
/// path = "/dev/hugepages/" -- hugepages-backed guest RAM (vCMDQ)
/// path = "/dev/egmN" -- per-socket EGM region (vEGM)
File {
id: String,
size: u64,
path: String,
prealloc: bool,
share: bool,
/// `host-nodes=N,policy=bind` — set for Q35 SHM; absent for hugepages/EGM.
host_nodes: Option<u32>,
policy: Option<String>,
is_egm: bool,
},
}
pub(crate) struct NumaNode {
@@ -170,54 +198,184 @@ impl Platform {
}
#[cfg(test)]
pub(crate) fn from_config_defaults(memory_size: u64) -> Result<Self> {
Self::build("virt", memory_size)
pub(crate) fn from_config_defaults(machine_type: &str, memory_size: u64) -> Result<Self> {
Self::build(machine_type, memory_size)
}
fn build(machine_type: &str, memory_size: u64) -> Result<Self> {
let base = BaseMachine {
let virt_base = BaseMachine {
accel: "kvm".to_owned(),
memory_backend: Some(PRIMARY_RAM_ID.to_owned()),
cpu: CpuConfig {
model: CpuModel::Host { extra_features: vec![] },
},
cpu: CpuConfig { model: CpuModel::Host { extra_features: vec![] } },
};
let machine = match machine_type {
"q35" => Machine::Q35(Q35 {
base,
kernel_irqchip: Some("on".to_owned()),
intel_iommu: None,
}),
"q35" => {
// Q35 machine line does not carry memory-backend=; memory is
// expressed via Objects::memory_backends and -numa node,memdev=.
let base = BaseMachine {
accel: "kvm".to_owned(),
memory_backend: None,
cpu: CpuConfig { model: CpuModel::Host { extra_features: vec![] } },
};
Machine::Q35(Q35 {
base,
kernel_irqchip: None,
confidential_guest_support: None,
intel_iommu: None,
})
}
"virt" => Machine::Virt(Virt {
base,
base: virt_base,
gic_version: Some(3),
ras: true,
highmem_mmio_size: Some(4 << 40),
}),
"pseries" => Machine::Pseries(Pseries { base }),
"s390-ccw-virtio" => Machine::S390xCcwVirtio(S390xCcwVirtio { base }),
"pseries" => Machine::Pseries(Pseries { base: virt_base }),
"s390-ccw-virtio" => Machine::S390xCcwVirtio(S390xCcwVirtio { base: virt_base }),
other => bail!("unknown machine type: {other}"),
};
Ok(Self {
machine,
pci: PciTopology { default_bus: Some("pcie.0".to_owned()), roots: vec![] },
pci: PciTopology {
default_bus: Some("pcie.0".to_owned()),
roots: vec![],
cold_plug_ports: vec![],
},
objects: Objects {
iommufd: None,
memory_backends: vec![MemoryBackend::Ram {
id: PRIMARY_RAM_ID.to_owned(),
size: memory_size,
host_nodes: None,
policy: None,
}],
numa_nodes: vec![],
numa_distances: vec![],
thread_contexts: vec![],
acpi_links: vec![],
rng: None,
protection: None,
},
})
}
pub fn apply_host_defaults(&mut self, topo: &HostTopology) {
// Protection device drives CoCo machine flags and the preamble object.
if let Some(ref prot) = topo.protection {
if let Machine::Q35(ref mut q) = self.machine {
q.kernel_irqchip = Some("split".to_owned());
q.confidential_guest_support = Some(prot.id().to_owned());
}
self.objects.protection = Some(prot.clone());
}
match &self.machine {
Machine::Q35(_) => self.apply_q35_defaults(topo),
Machine::Virt(_) => self.apply_virt_defaults(topo),
_ => {}
}
}
fn apply_q35_defaults(&mut self, topo: &HostTopology) {
self.objects.memory_backends.clear();
self.objects.numa_nodes.clear();
for (i, socket) in topo.sockets.iter().enumerate() {
let id = format!("numa-mem{i}");
let size = socket.mem_size.unwrap_or(0);
let backend = if let Some(ref path) = socket.mem_path {
MemoryBackend::File {
id: id.clone(),
size,
path: path.clone(),
prealloc: false,
share: true,
host_nodes: socket.host_node,
policy: socket.host_node.map(|_| "bind".to_owned()),
is_egm: false,
}
} else {
MemoryBackend::Ram {
id: id.clone(),
size,
host_nodes: socket.host_node,
policy: socket.host_node.map(|_| "bind".to_owned()),
}
};
self.objects.memory_backends.push(backend);
self.objects.numa_nodes.push(NumaNode {
nodeid: i as u32,
memdev: Some(id),
cpus: Some(socket.cpu_range.clone()),
});
}
self.objects.numa_distances = topo.numa_distances.clone();
// Pre-provisioned cold-plug root ports on pcie.0.
self.pci.cold_plug_ports = (0..topo.cold_plug_ports)
.map(|i| PciRootPort {
id: format!("rp{i}"),
chassis: 0,
slot: Some(i as u8),
multifunction: Some(false),
io_reserve: None,
device: None,
})
.collect();
let has_gpus = topo.gpu_smmu_groups.iter().any(|g| !g.pci_bus_addrs.is_empty());
if !has_gpus {
return;
}
let mut gpu_idx = 0usize;
let mut bus_nr_running: u8 = 0;
for (group_idx, group) in topo.gpu_smmu_groups.iter().enumerate() {
let bus_nr = 32u8 + bus_nr_running;
bus_nr_running += 1;
let cpu_mem_node = socket_numa_node(&topo.sockets, group.socket);
let pxb_id = format!("pxb-numa{group_idx}");
let mut root_ports = Vec::new();
for pci_addr in &group.pci_bus_addrs {
let rp_id = format!("rp-numa{group_idx}-{gpu_idx}");
root_ports.push(PciRootPort {
id: rp_id,
chassis: 10,
slot: Some(0),
multifunction: Some(false),
io_reserve: None,
device: Some(VfioDevice {
id: format!("dev{gpu_idx}"),
host: pci_addr.clone(),
rombar: None,
kind: VfioDeviceKind::GpuPci,
iommufd_id: None,
pci_vendor_id: None,
pci_device_id: None,
}),
});
gpu_idx += 1;
}
self.pci.roots.push(PciRootComplex {
id: pxb_id,
bus_nr,
numa_node: Some(cpu_mem_node),
iommu: None,
root_ports,
});
}
}
fn apply_virt_defaults(&mut self, topo: &HostTopology) {
let has_gpus = topo.gpu_smmu_groups.iter().any(|g| !g.pci_bus_addrs.is_empty());
if !has_gpus {
return;
@@ -236,6 +394,8 @@ impl Platform {
path: egm.path.clone(),
prealloc: true,
share: true,
host_nodes: None,
policy: None,
is_egm: true,
});
}
@@ -307,11 +467,17 @@ impl Platform {
root_ports.push(PciRootPort {
id: rp_id,
chassis: (gpu_idx + 1) as u8,
slot: None,
multifunction: None,
io_reserve: Some(0),
device: Some(VfioDevice {
id: dev_id,
host: pci_addr.clone(),
rombar: false,
rombar: Some(false),
kind: VfioDeviceKind::Gpu,
iommufd_id: None,
pci_vendor_id: None,
pci_device_id: None,
}),
});
@@ -342,19 +508,20 @@ impl Platform {
.memory_backends
.into_iter()
.map(|b| match b {
MemoryBackend::Ram { id, size } => MemoryBackend::File {
MemoryBackend::Ram { id, size, .. } => MemoryBackend::File {
id,
size,
path: path.to_owned(),
prealloc: true,
share: true,
host_nodes: None,
policy: None,
is_egm: false,
},
other => other,
})
.collect();
// Hugepages provides physically contiguous memory; enable cmdqv on all SMMUs.
let roots = self
.pci
.roots
@@ -374,16 +541,95 @@ impl Platform {
}
}
/// Emit the complete QEMU command-line argument list.
///
/// Dispatch by machine type: Q35 and virt/Grace have different emission
/// ordering because virt requires `memory-backend=` on the machine line
/// (backends must precede machine), whereas Q35 does not.
pub fn to_qemu_args(&self) -> Result<Vec<String>> {
match &self.machine {
Machine::Q35(_) => self.emit_q35_args(),
Machine::Virt(_) => self.emit_virt_args(),
Machine::Pseries(_) => todo!("pSeries args"),
Machine::S390xCcwVirtio(_) => todo!("s390x args"),
}
}
/// Q35 emission order:
/// 1. protection object (sev-snp-guest / tdx-guest), if any
/// 2. -machine q35,...
/// 3. memory backends + NUMA nodes, interleaved per socket
/// 4. -numa dist entries
/// 5. pxb-pcie GPU roots (pxb + root ports + per-device iommufd + vfio)
/// 6. cold-plug root ports on pcie.0
fn emit_q35_args(&self) -> Result<Vec<String>> {
let mut args: Vec<String> = Vec::new();
if let Some(ref prot) = self.objects.protection {
args.push("-object".to_owned());
args.push(emit_protection(prot));
}
args.push("-machine".to_owned());
args.push(emit_machine(&self.machine));
for (backend, node) in
self.objects.memory_backends.iter().zip(self.objects.numa_nodes.iter())
{
args.push("-object".to_owned());
args.push(emit_backend(backend, backend_id(backend)));
args.push("-numa".to_owned());
args.push(emit_numa_node(node));
}
for &(src, dst, val) in &self.objects.numa_distances {
args.push("-numa".to_owned());
args.push(format!("dist,src={src},dst={dst},val={val}"));
}
let default_bus = self.pci.default_bus.as_deref().unwrap_or("pcie.0");
for root in &self.pci.roots {
args.push("-device".to_owned());
args.push(emit_pxb(root, default_bus));
for port in &root.root_ports {
args.push("-device".to_owned());
args.push(emit_root_port(port, &root.id));
if let Some(vfio) = &port.device {
if let Some(ref ifd_id) = vfio.iommufd_id {
args.push("-object".to_owned());
args.push(format!("iommufd,id={ifd_id}"));
}
args.push("-device".to_owned());
args.push(emit_vfio_q35(vfio, &port.id));
}
}
}
for port in &self.pci.cold_plug_ports {
args.push("-device".to_owned());
args.push(emit_root_port(port, default_bus));
}
Ok(args)
}
/// virt / Grace emission order (unchanged from Phase 2):
/// 1. iommufd object
/// 2. memory backends
/// 3. -machine (references memory-backend=)
/// 4. NUMA nodes
/// 5. pxb-pcie + arm-smmuv3 + root ports + vfio
/// 6. acpi_links (GenericInitiators then EgmMemory)
fn emit_virt_args(&self) -> Result<Vec<String>> {
let mut args: Vec<String> = Vec::new();
// iommufd object
if let Some(ifd) = &self.objects.iommufd {
args.push("-object".to_owned());
args.push(format!("iommufd,id={}", ifd.id));
}
// Memory backends: EGM configs skip memory_backends[0] and re-index the rest as m0,m1,...
let has_egm = self.machine.base().memory_backend.is_none()
&& self.objects.memory_backends.len() > 1;
@@ -399,17 +645,14 @@ impl Platform {
}
}
// Machine
args.push("-machine".to_owned());
args.push(emit_machine(&self.machine));
// NUMA nodes
for node in &self.objects.numa_nodes {
args.push("-numa".to_owned());
args.push(emit_numa_node(node));
}
// PCI topology
let default_bus = self.pci.default_bus.as_deref().unwrap_or("pcie.0");
for root in &self.pci.roots {
args.push("-device".to_owned());
@@ -426,12 +669,11 @@ impl Platform {
if let Some(vfio) = &port.device {
args.push("-device".to_owned());
args.push(emit_vfio(vfio, &port.id, self.objects.iommufd.as_ref()));
args.push(emit_vfio_grace(vfio, &port.id, self.objects.iommufd.as_ref()));
}
}
}
// ACPI links: GenericInitiators before EgmMemory (ACPI SRAT ordering)
for link in self.objects.acpi_links.iter() {
if let AcpiPciNodeLink::GenericInitiator { id, pci_dev, node } = link {
args.push("-object".to_owned());
@@ -468,8 +710,15 @@ fn backend_id(backend: &MemoryBackend) -> &str {
fn emit_backend(backend: &MemoryBackend, id: &str) -> String {
match backend {
MemoryBackend::Ram { size, .. } => {
format!("memory-backend-ram,size={},id={id}", format_memory(*size))
MemoryBackend::Ram { size, host_nodes, policy, .. } => {
let mut s = format!("memory-backend-ram,id={id},size={}", format_memory(*size));
if let Some(hn) = host_nodes {
s.push_str(&format!(",host-nodes={hn}"));
}
if let Some(pol) = policy {
s.push_str(&format!(",policy={pol}"));
}
s
}
MemoryBackend::File { size, path, is_egm: true, .. } => {
format!(
@@ -477,17 +726,57 @@ fn emit_backend(backend: &MemoryBackend, id: &str) -> String {
format_memory(*size)
)
}
MemoryBackend::File { size, path, is_egm: false, .. } => {
format!(
"memory-backend-file,id={id},size={},mem-path={path},prealloc=on,share=on",
format_memory(*size)
)
MemoryBackend::File { size, path, is_egm: false, host_nodes, policy, .. } => {
let mut s =
format!("memory-backend-file,id={id},size={},mem-path={path}", format_memory(*size));
if let Some(hn) = host_nodes {
s.push_str(&format!(",host-nodes={hn}"));
}
if let Some(pol) = policy {
s.push_str(&format!(",policy={pol}"));
}
s.push_str(",share=on");
s
}
}
}
fn emit_protection(prot: &ProtectionDevice) -> String {
match prot {
ProtectionDevice::SevSnp {
id,
cbitpos,
reduced_phys_bits,
kernel_hashes,
policy,
host_data,
} => {
let mut s = format!(
"sev-snp-guest,id={id},cbitpos={cbitpos},reduced-phys-bits={reduced_phys_bits},\
kernel-hashes={},policy={policy}",
if *kernel_hashes { "on" } else { "off" },
);
if let Some(hd) = host_data {
s.push_str(&format!(",host-data={hd}"));
}
s
}
ProtectionDevice::Tdx { id } => format!("tdx-guest,id={id}"),
}
}
fn emit_machine(machine: &Machine) -> String {
match machine {
Machine::Q35(q) => {
let mut s = format!("q35,accel={}", q.base.accel);
if let Some(ki) = &q.kernel_irqchip {
s.push_str(&format!(",kernel_irqchip={ki}"));
}
if let Some(cgs) = &q.confidential_guest_support {
s.push_str(&format!(",confidential-guest-support={cgs}"));
}
s
}
Machine::Virt(v) => {
let mut s = format!("virt,accel={}", v.base.accel);
if let Some(gv) = v.gic_version {
@@ -502,7 +791,6 @@ fn emit_machine(machine: &Machine) -> String {
}
s
}
Machine::Q35(_) => todo!("Q35 machine emission"),
Machine::Pseries(_) => todo!("pSeries machine emission"),
Machine::S390xCcwVirtio(_) => todo!("s390x machine emission"),
}
@@ -525,7 +813,7 @@ fn emit_numa_node(node: &NumaNode) -> String {
}
fn emit_pxb(root: &PciRootComplex, default_bus: &str) -> String {
let mut s = format!("pxb-pcie,id={},bus_nr={},bus={}", root.id, root.bus_nr, default_bus);
let mut s = format!("pxb-pcie,id={},bus={},bus_nr={}", root.id, default_bus, root.bus_nr);
if let Some(nn) = root.numa_node {
s.push_str(&format!(",numa_node={nn}"));
}
@@ -548,27 +836,61 @@ fn emit_smmu(smmu: &SmmuV3Config, pxb_id: &str) -> String {
s
}
fn emit_root_port(port: &PciRootPort, pxb_id: &str) -> String {
format!(
"pcie-root-port,id={},bus={},chassis={},io-reserve=0",
port.id, pxb_id, port.chassis
)
fn emit_root_port(port: &PciRootPort, bus: &str) -> String {
let mut s = format!("pcie-root-port,id={},bus={},chassis={}", port.id, bus, port.chassis);
if let Some(slot) = port.slot {
s.push_str(&format!(",slot={slot}"));
}
if let Some(mf) = port.multifunction {
s.push_str(if mf { ",multifunction=on" } else { ",multifunction=off" });
}
if let Some(io) = port.io_reserve {
s.push_str(&format!(",io-reserve={io}"));
}
s
}
fn emit_vfio(vfio: &VfioDevice, port_id: &str, iommufd: Option<&IommufdBackend>) -> String {
/// Q35 vfio emission: host → id → [x-pci-vendor-id] → [x-pci-device-id] → bus → [iommufd]
fn emit_vfio_q35(vfio: &VfioDevice, port_id: &str) -> String {
let device = match vfio.kind {
VfioDeviceKind::Gpu => "vfio-pci-nohotplug",
VfioDeviceKind::Nic => "vfio-pci",
VfioDeviceKind::GpuPci | VfioDeviceKind::Nic => "vfio-pci",
};
let rombar = if vfio.rombar { "1" } else { "0" };
let mut s = format!("{device},host={},bus={port_id},rombar={rombar},id={}", vfio.host, vfio.id);
let mut s = format!("{device},host={},id={}", vfio.host, vfio.id);
if let Some(vid) = vfio.pci_vendor_id {
s.push_str(&format!(",x-pci-vendor-id={vid:#06x}"));
}
if let Some(did) = vfio.pci_device_id {
s.push_str(&format!(",x-pci-device-id={did:#06x}"));
}
s.push_str(&format!(",bus={port_id}"));
if let Some(ref ifd_id) = vfio.iommufd_id {
s.push_str(&format!(",iommufd={ifd_id}"));
}
s
}
/// Grace/virt vfio emission: device → host → bus → [rombar] → id → [iommufd]
fn emit_vfio_grace(
vfio: &VfioDevice,
port_id: &str,
iommufd: Option<&IommufdBackend>,
) -> String {
let device = match vfio.kind {
VfioDeviceKind::Gpu => "vfio-pci-nohotplug",
VfioDeviceKind::GpuPci | VfioDeviceKind::Nic => "vfio-pci",
};
let mut s = format!("{device},host={},bus={port_id}", vfio.host);
if let Some(rombar) = vfio.rombar {
s.push_str(if rombar { ",rombar=1" } else { ",rombar=0" });
}
s.push_str(&format!(",id={}", vfio.id));
if let Some(ifd) = iommufd {
s.push_str(&format!(",iommufd={}", ifd.id));
}
s
}
// Socket IDs are not guaranteed contiguous; use position to get a dense NUMA node number.
fn socket_numa_node(sockets: &[SocketInfo], socket_id: u32) -> u32 {
sockets
.iter()