diff --git a/src/runtime-rs/crates/hypervisor/src/qemu/machine/platform.rs b/src/runtime-rs/crates/hypervisor/src/qemu/machine/platform.rs index 61943fb61e..679f44ac2a 100644 --- a/src/runtime-rs/crates/hypervisor/src/qemu/machine/platform.rs +++ b/src/runtime-rs/crates/hypervisor/src/qemu/machine/platform.rs @@ -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=`. /// `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, 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, pub memory_backends: Vec, pub numa_nodes: Vec, + /// `-numa dist` entries; emitted after all NUMA nodes. + pub numa_distances: Vec<(u32, u32, u32)>, pub thread_contexts: Vec, pub acpi_links: Vec, pub rng: Option, + /// CoCo protection object (`sev-snp-guest` or `tdx-guest`); emitted before `-machine`. + pub protection: Option, } 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, + /// `policy=bind` — always paired with `host_nodes`. + policy: Option, + }, + /// 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, + policy: Option, + 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::build("virt", memory_size) + pub(crate) fn from_config_defaults(machine_type: &str, memory_size: u64) -> Result { + Self::build(machine_type, memory_size) } fn build(machine_type: &str, memory_size: u64) -> Result { - 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> { + 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> { + let mut args: Vec = 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> { let mut args: Vec = 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()