From c2178a9cc79d39fcbe0c5f6be0a58bfade85786a Mon Sep 17 00:00:00 2001 From: Will Wright Date: Wed, 22 Jul 2026 18:24:13 +0000 Subject: [PATCH 1/2] virtio_vsock: add kernel vhost_vsock backend --- Cargo.lock | 2 + Guide/src/reference/openvmm/management/cli.md | 11 + openvmm/openvmm_entry/src/cli_args.rs | 41 + openvmm/openvmm_entry/src/lib.rs | 14 + openvmm/openvmm_resources/src/lib.rs | 2 + vm/devices/virtio/virtio_resources/src/lib.rs | 22 + vm/devices/virtio/virtio_vsock/Cargo.toml | 5 + vm/devices/virtio/virtio_vsock/src/lib.rs | 5 +- .../virtio/virtio_vsock/src/resolver.rs | 31 + vm/devices/virtio/virtio_vsock/src/vhost.rs | 886 ++++++++++++++++++ 10 files changed, 1018 insertions(+), 1 deletion(-) create mode 100644 vm/devices/virtio/virtio_vsock/src/vhost.rs diff --git a/Cargo.lock b/Cargo.lock index 73fa47397b..11ba3b793c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9831,11 +9831,13 @@ dependencies = [ "hybrid_vsock", "inspect", "mesh", + "nix 0.30.1", "open_enum", "pal_async", "pal_event", "parking_lot", "smallvec", + "sparse_mmap", "task_control", "tempfile", "test_with_tracing", diff --git a/Guide/src/reference/openvmm/management/cli.md b/Guide/src/reference/openvmm/management/cli.md index dde6a32581..2d8baaffd0 100644 --- a/Guide/src/reference/openvmm/management/cli.md +++ b/Guide/src/reference/openvmm/management/cli.md @@ -159,6 +159,17 @@ as well as the generated CLI help (via `cargo run -- --help`). The guest kernel must have `CONFIG_HW_RANDOM_VIRTIO` enabled. * `--virtio-rng-bus `: Select the bus for the virtio-rng device (`auto`, `mmio`, `pci`, `vpci`). Defaults to `auto`. +* `--virtio-vsock-path `: Add a virtio-vsock device using OpenVMM's + hybrid Unix-socket relay. +* `--virtio-vsock-vhost-cid `: Add a virtio-vsock device backed by the + Linux kernel's `vhost_vsock` implementation. This makes the guest reachable + from host applications through `AF_VSOCK` at `CID`, which must be between 3 + and 4294967294 (CIDs 0-2 are reserved for the hypervisor, loopback, and host, + respectively, and u32::MAX is the ANY wildcard). This option requires + `/dev/vhost-vsock`, the `vhost_vsock` kernel module, and shared file-backed + guest RAM (the default memory backing). It uses identity-mapped DMA and + does not support a non-identity virtual IOMMU. It conflicts with + `--virtio-vsock-path`. * `--vhost-user ,type=[,tag=][,num_queues=][,queue_size=][,pcie_port=]`: Attach a vhost-user device backed by an external process over a Unix socket (Linux only). The backend process must already be listening on `SOCKET_PATH`. diff --git a/openvmm/openvmm_entry/src/cli_args.rs b/openvmm/openvmm_entry/src/cli_args.rs index 38ac1baae3..ed6420026b 100644 --- a/openvmm/openvmm_entry/src/cli_args.rs +++ b/openvmm/openvmm_entry/src/cli_args.rs @@ -765,6 +765,17 @@ options: #[clap(long, value_name = "PATH")] pub virtio_vsock_path: Option, + /// expose the guest in the host AF_VSOCK namespace using the Linux + /// vhost_vsock kernel backend + #[cfg(target_os = "linux")] + #[clap( + long, + value_name = "CID", + conflicts_with = "virtio_vsock_path", + value_parser = parse_vhost_vsock_cid + )] + pub virtio_vsock_vhost_cid: Option, + /// expose a virtio network with the given backend (dio | vmnic | tap | /// none) /// @@ -1430,6 +1441,17 @@ pub enum VirtioBusCli { Vpci, } +#[cfg(target_os = "linux")] +fn parse_vhost_vsock_cid(value: &str) -> Result { + let cid = value + .parse::() + .map_err(|error| format!("invalid CID '{value}': {error}"))?; + if !(3..u32::MAX).contains(&cid) { + return Err(format!("CID must be between 3 and {}", u32::MAX - 1)); + } + Ok(cid) +} + /// Parse an optional `pcie_port=:` prefix from a CLI argument string. /// /// Returns `(Some(port_name), rest)` if the prefix is present, or @@ -4913,6 +4935,25 @@ mod tests { assert!(VhostUserCli::from_str("/run/x.sock,device_id=1").is_err()); // device_id without queue_sizes } + #[cfg(target_os = "linux")] + #[test] + fn test_vhost_vsock_cli() { + let opt = Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "3"]).unwrap(); + assert_eq!(opt.virtio_vsock_vhost_cid, Some(3)); + + assert!(Options::try_parse_from(["openvmm", "--virtio-vsock-vhost-cid", "2"]).is_err()); + assert!( + Options::try_parse_from([ + "openvmm", + "--virtio-vsock-vhost-cid", + "3", + "--virtio-vsock-path", + "/tmp/vsock", + ]) + .is_err() + ); + } + #[test] fn test_nvme_controller_cli_pcie() { let c = NvmeControllerCli::from_str("id=nvme0,pcie_port=p0").unwrap(); diff --git a/openvmm/openvmm_entry/src/lib.rs b/openvmm/openvmm_entry/src/lib.rs index 340eaab6ba..448f84e73b 100644 --- a/openvmm/openvmm_entry/src/lib.rs +++ b/openvmm/openvmm_entry/src/lib.rs @@ -1889,6 +1889,20 @@ async fn vm_config_from_command_line( ); } + #[cfg(target_os = "linux")] + if let Some(guest_cid) = opt.virtio_vsock_vhost_cid { + let vhost = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/vhost-vsock") + .context("failed to open /dev/vhost-vsock")? + .into(); + add_virtio_device( + VirtioBusCli::Auto, + virtio_resources::vsock::VirtioVsockVhostHandle { vhost, guest_cid }.into_resource(), + ); + } + let mut cfg = Config { chipset, load_mode, diff --git a/openvmm/openvmm_resources/src/lib.rs b/openvmm/openvmm_resources/src/lib.rs index aee7c50fd3..83a7f6ba73 100644 --- a/openvmm/openvmm_resources/src/lib.rs +++ b/openvmm/openvmm_resources/src/lib.rs @@ -107,6 +107,8 @@ vm_resource::register_static_resolvers! { #[cfg(target_os = "linux")] vhost_user_frontend::resolver::VhostUserFrontendResolver, virtio_vsock::resolver::VirtioVsockResolver, + #[cfg(target_os = "linux")] + virtio_vsock::resolver::VirtioVsockVhostResolver, // Vmbus devices guest_crash_device::resolver::GuestCrashDeviceResolver, diff --git a/vm/devices/virtio/virtio_resources/src/lib.rs b/vm/devices/virtio/virtio_resources/src/lib.rs index 29609a74e0..689c1b57f1 100644 --- a/vm/devices/virtio/virtio_resources/src/lib.rs +++ b/vm/devices/virtio/virtio_resources/src/lib.rs @@ -237,6 +237,8 @@ pub mod vhost_user { pub mod vsock { use mesh::MeshPayload; + #[cfg(target_os = "linux")] + use std::os::fd::OwnedFd; use unix_socket::UnixListener; use vm_resource::ResourceId; use vm_resource::kind::VirtioDeviceHandle; @@ -251,4 +253,24 @@ pub mod vsock { impl ResourceId for VirtioVsockHandle { const ID: &'static str = "virtio-vsock"; } + + /// A virtio-vsock device backed by the Linux kernel's `vhost_vsock` + /// implementation. + #[cfg(target_os = "linux")] + #[derive(MeshPayload)] + pub struct VirtioVsockVhostHandle { + /// A pre-opened `/dev/vhost-vsock` file descriptor. + /// + /// The device resolver takes ownership and configures the vhost owner + /// and guest CID. + pub vhost: OwnedFd, + /// The CID used to address the guest from the host's `AF_VSOCK` + /// namespace. + pub guest_cid: u32, + } + + #[cfg(target_os = "linux")] + impl ResourceId for VirtioVsockVhostHandle { + const ID: &'static str = "virtio-vsock-vhost"; + } } diff --git a/vm/devices/virtio/virtio_vsock/Cargo.toml b/vm/devices/virtio/virtio_vsock/Cargo.toml index 4e9fbc22f1..44170e2062 100644 --- a/vm/devices/virtio/virtio_vsock/Cargo.toml +++ b/vm/devices/virtio/virtio_vsock/Cargo.toml @@ -30,6 +30,11 @@ tracelimit.workspace = true unicycle.workspace = true zerocopy.workspace = true +[target.'cfg(target_os = "linux")'.dependencies] +nix = { workspace = true, features = ["ioctl"] } +pal_event.workspace = true +sparse_mmap.workspace = true + [dev-dependencies] getrandom.workspace = true guid.workspace = true diff --git a/vm/devices/virtio/virtio_vsock/src/lib.rs b/vm/devices/virtio/virtio_vsock/src/lib.rs index 98546f88cd..cbbb52b43b 100644 --- a/vm/devices/virtio/virtio_vsock/src/lib.rs +++ b/vm/devices/virtio/virtio_vsock/src/lib.rs @@ -3,7 +3,8 @@ //! Virtio vsock device implementation, per section 5.10 of the virtio specification. -// UNSAFETY: Pointer casts between AtomicU8 and u8 to allow direct read/write into guest memory. +// UNSAFETY: Pointer casts between AtomicU8 and u8 to allow direct read/write +// into guest memory and Linux vhost ioctls. #![expect(unsafe_code)] mod connections; @@ -11,6 +12,8 @@ pub mod resolver; mod ring; mod spec; mod unix_relay; +#[cfg(target_os = "linux")] +mod vhost; #[cfg(test)] mod integration_tests; diff --git a/vm/devices/virtio/virtio_vsock/src/resolver.rs b/vm/devices/virtio/virtio_vsock/src/resolver.rs index 3b5fc223ce..d7af0af5ed 100644 --- a/vm/devices/virtio/virtio_vsock/src/resolver.rs +++ b/vm/devices/virtio/virtio_vsock/src/resolver.rs @@ -37,3 +37,34 @@ impl ResolveResource for VirtioVsockResol Ok(device.into()) } } + +/// Resolver for Linux kernel `vhost_vsock` devices. +#[cfg(target_os = "linux")] +pub struct VirtioVsockVhostResolver; + +#[cfg(target_os = "linux")] +declare_static_resolver! { + VirtioVsockVhostResolver, + (VirtioDeviceHandle, virtio_resources::vsock::VirtioVsockVhostHandle), +} + +#[cfg(target_os = "linux")] +impl ResolveResource + for VirtioVsockVhostResolver +{ + type Output = ResolvedVirtioDevice; + type Error = anyhow::Error; + + fn resolve( + &self, + resource: virtio_resources::vsock::VirtioVsockVhostHandle, + input: VirtioResolveInput<'_>, + ) -> Result { + Ok(crate::vhost::VhostVsockDevice::new( + input.driver_source, + resource.vhost, + resource.guest_cid, + )? + .into()) + } +} diff --git a/vm/devices/virtio/virtio_vsock/src/vhost.rs b/vm/devices/virtio/virtio_vsock/src/vhost.rs new file mode 100644 index 0000000000..8f40434cbe --- /dev/null +++ b/vm/devices/virtio/virtio_vsock/src/vhost.rs @@ -0,0 +1,886 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Linux kernel `vhost_vsock` backend. +//! +//! The virtio transport remains in OpenVMM, while `/dev/vhost-vsock` processes +//! the receive and transmit virtqueues. This exposes the guest directly in the +//! host's `AF_VSOCK` namespace. +//! +//! This backend requires file-backed guest RAM and identity-mapped DMA +//! addresses. It does not configure a vhost IOTLB for a virtual IOMMU. + +use crate::spec::VsockConfig; +use anyhow::Context as _; +use guestmem::GuestMemory; +use inspect::InspectMut; +use pal_event::Event; +use sparse_mmap::SparseMapping; +use std::os::fd::AsFd as _; +use std::os::fd::AsRawFd as _; +use std::os::fd::OwnedFd; +use std::os::fd::RawFd; +use virtio::DeviceTraits; +use virtio::QueueResources; +use virtio::VirtioDevice; +use virtio::queue::QueueParams; +use virtio::queue::QueueState; +use virtio::spec::VirtioDeviceFeatures; +use virtio::spec::VirtioDeviceType; +use vmcore::interrupt::EventProxy; +use vmcore::vm_task::VmTaskDriver; +use vmcore::vm_task::VmTaskDriverSource; +use zerocopy::Immutable; +use zerocopy::IntoBytes; +use zerocopy::KnownLayout; + +const QUEUE_COUNT: usize = 3; +const RX_QUEUE: u16 = 0; +const TX_QUEUE: u16 = 1; +const EVENT_QUEUE: u16 = 2; + +// Dirty logging is a vhost control feature, not a guest virtio feature. +const VHOST_F_LOG_ALL: u64 = 1 << 26; +// OpenVMM only exposes modern virtio devices. +const VIRTIO_F_ANY_LAYOUT: u64 = 1 << 27; +// OpenVMM's transport exposes ACCESS_PLATFORM, but this backend uses the +// direct GPA-to-HVA memory table and does not configure a vhost IOTLB. +const VIRTIO_F_ACCESS_PLATFORM: u64 = 1 << 33; +const MASKED_FEATURES: u64 = VHOST_F_LOG_ALL | VIRTIO_F_ANY_LAYOUT | VIRTIO_F_ACCESS_PLATFORM; + +#[repr(C)] +#[derive(Copy, Clone)] +struct VhostVringState { + index: u32, + num: u32, +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct VhostVringAddr { + index: u32, + flags: u32, + desc_user_addr: u64, + used_user_addr: u64, + avail_user_addr: u64, + log_guest_addr: u64, +} + +#[repr(C)] +#[derive(Copy, Clone)] +struct VhostVringFile { + index: u32, + fd: i32, +} + +#[repr(C)] +#[derive(Copy, Clone, IntoBytes, Immutable, KnownLayout)] +struct VhostMemoryHeader { + nregions: u32, + padding: u32, +} + +#[repr(C)] +#[derive(Copy, Clone, IntoBytes, Immutable, KnownLayout)] +struct VhostMemoryRegion { + guest_phys_addr: u64, + memory_size: u64, + userspace_addr: u64, + flags_padding: u64, +} + +mod ioctl { + use super::VhostMemoryHeader; + use super::VhostVringAddr; + use super::VhostVringFile; + use super::VhostVringState; + + const VHOST_VIRTIO: u8 = 0xaf; + + nix::ioctl_read!(get_features, VHOST_VIRTIO, 0x00, u64); + nix::ioctl_write_ptr!(set_features, VHOST_VIRTIO, 0x00, u64); + nix::ioctl_none!(set_owner, VHOST_VIRTIO, 0x01); + nix::ioctl_write_ptr!(set_mem_table, VHOST_VIRTIO, 0x03, VhostMemoryHeader); + nix::ioctl_write_ptr!(set_vring_num, VHOST_VIRTIO, 0x10, VhostVringState); + nix::ioctl_write_ptr!(set_vring_addr, VHOST_VIRTIO, 0x11, VhostVringAddr); + nix::ioctl_write_ptr!(set_vring_base, VHOST_VIRTIO, 0x12, VhostVringState); + nix::ioctl_readwrite!(get_vring_base, VHOST_VIRTIO, 0x12, VhostVringState); + nix::ioctl_write_ptr!(set_vring_kick, VHOST_VIRTIO, 0x20, VhostVringFile); + nix::ioctl_write_ptr!(set_vring_call, VHOST_VIRTIO, 0x21, VhostVringFile); + nix::ioctl_write_ptr!(set_guest_cid, VHOST_VIRTIO, 0x60, u64); + nix::ioctl_write_ptr!(set_running, VHOST_VIRTIO, 0x61, i32); +} + +struct VhostBackend { + fd: OwnedFd, +} + +impl VhostBackend { + fn new(fd: OwnedFd, guest_cid: u32) -> anyhow::Result<(Self, u64)> { + validate_guest_cid(guest_cid)?; + + let backend = Self { fd }; + + // SAFETY: The fd refers to a vhost-vsock device and this ioctl has no argument. + unsafe { ioctl::set_owner(backend.raw_fd()) }.context("VHOST_SET_OWNER failed")?; + + let cid = u64::from(guest_cid); + // SAFETY: The ioctl copies a u64 from the supplied pointer. + unsafe { ioctl::set_guest_cid(backend.raw_fd(), &cid) } + .context("VHOST_VSOCK_SET_GUEST_CID failed")?; + + let mut features = 0; + // SAFETY: The ioctl writes a u64 to the supplied pointer. + unsafe { ioctl::get_features(backend.raw_fd(), &mut features) } + .context("VHOST_GET_FEATURES failed")?; + + Ok((backend, features & !MASKED_FEATURES)) + } + + fn raw_fd(&self) -> RawFd { + self.fd.as_raw_fd() + } + + fn set_features(&self, features: u64) -> anyhow::Result<()> { + // SAFETY: The ioctl copies a u64 from the supplied pointer. + unsafe { ioctl::set_features(self.raw_fd(), &features) } + .context("VHOST_SET_FEATURES failed")?; + Ok(()) + } + + fn set_memory(&self, memory: &VhostMemory) -> anyhow::Result<()> { + let nregions = + u32::try_from(memory.regions.len()).context("too many vhost memory regions")?; + let mut table = Vec::with_capacity( + size_of::() + memory.regions.len() * size_of::(), + ); + table.extend_from_slice( + VhostMemoryHeader { + nregions, + padding: 0, + } + .as_bytes(), + ); + for region in &memory.regions { + table.extend_from_slice( + VhostMemoryRegion { + guest_phys_addr: region.guest_address, + memory_size: region.len(), + userspace_addr: region.host_address(), + flags_padding: 0, + } + .as_bytes(), + ); + } + + // SAFETY: `table` contains a vhost_memory header followed by exactly + // `nregions` vhost_memory_region entries, and the kernel copies it + // during the ioctl. + unsafe { ioctl::set_mem_table(self.raw_fd(), table.as_ptr().cast::()) } + .context("VHOST_SET_MEM_TABLE failed")?; + Ok(()) + } + + fn set_vring_num(&self, index: u16, size: u16) -> anyhow::Result<()> { + let state = VhostVringState { + index: u32::from(index), + num: u32::from(size), + }; + // SAFETY: The ioctl copies a VhostVringState from the supplied pointer. + unsafe { ioctl::set_vring_num(self.raw_fd(), &state) } + .with_context(|| format!("VHOST_SET_VRING_NUM failed for queue {index}"))?; + Ok(()) + } + + fn set_vring_base(&self, index: u16, base: u32) -> anyhow::Result<()> { + let state = VhostVringState { + index: u32::from(index), + num: base, + }; + // SAFETY: The ioctl copies a VhostVringState from the supplied pointer. + unsafe { ioctl::set_vring_base(self.raw_fd(), &state) } + .with_context(|| format!("VHOST_SET_VRING_BASE failed for queue {index}"))?; + Ok(()) + } + + fn get_vring_base(&self, index: u16) -> anyhow::Result { + let mut state = VhostVringState { + index: u32::from(index), + num: 0, + }; + // SAFETY: The ioctl reads and writes a VhostVringState through the + // supplied pointer. + unsafe { ioctl::get_vring_base(self.raw_fd(), &mut state) } + .with_context(|| format!("VHOST_GET_VRING_BASE failed for queue {index}"))?; + Ok(state.num) + } + + fn set_vring_addr(&self, index: u16, desc: u64, used: u64, avail: u64) -> anyhow::Result<()> { + let addr = VhostVringAddr { + index: u32::from(index), + flags: 0, + desc_user_addr: desc, + used_user_addr: used, + avail_user_addr: avail, + log_guest_addr: 0, + }; + // SAFETY: The ioctl copies a VhostVringAddr from the supplied pointer. + unsafe { ioctl::set_vring_addr(self.raw_fd(), &addr) } + .with_context(|| format!("VHOST_SET_VRING_ADDR failed for queue {index}"))?; + Ok(()) + } + + fn set_vring_kick(&self, index: u16, fd: RawFd) -> anyhow::Result<()> { + let file = VhostVringFile { + index: u32::from(index), + fd, + }; + // SAFETY: The ioctl copies a VhostVringFile from the supplied pointer. + unsafe { ioctl::set_vring_kick(self.raw_fd(), &file) } + .with_context(|| format!("VHOST_SET_VRING_KICK failed for queue {index}"))?; + Ok(()) + } + + fn set_vring_call(&self, index: u16, fd: RawFd) -> anyhow::Result<()> { + let file = VhostVringFile { + index: u32::from(index), + fd, + }; + // SAFETY: The ioctl copies a VhostVringFile from the supplied pointer. + unsafe { ioctl::set_vring_call(self.raw_fd(), &file) } + .with_context(|| format!("VHOST_SET_VRING_CALL failed for queue {index}"))?; + Ok(()) + } + + fn unbind_vring_events(&self, index: u16) { + if let Err(error) = self.set_vring_kick(index, -1) { + tracelimit::warn_ratelimited!( + error = &*error as &dyn std::error::Error, + index, + "failed to unbind vhost-vsock kick event" + ); + } + if let Err(error) = self.set_vring_call(index, -1) { + tracelimit::warn_ratelimited!( + error = &*error as &dyn std::error::Error, + index, + "failed to unbind vhost-vsock call event" + ); + } + } + + fn set_running(&self, running: bool) -> anyhow::Result<()> { + let running = i32::from(running); + // SAFETY: The ioctl copies an i32 from the supplied pointer. + unsafe { ioctl::set_running(self.raw_fd(), &running) } + .with_context(|| format!("VHOST_VSOCK_SET_RUNNING({running}) failed"))?; + Ok(()) + } +} + +struct MappedRegion { + guest_address: u64, + mapping: SparseMapping, +} + +impl MappedRegion { + fn len(&self) -> u64 { + self.mapping.len() as u64 + } + + fn host_address(&self) -> u64 { + self.mapping.as_ptr() as usize as u64 + } + + fn contains(&self, gpa: u64, len: u64) -> bool { + gpa.checked_sub(self.guest_address) + .and_then(|offset| offset.checked_add(len)) + .is_some_and(|end| end <= self.len()) + } + + fn translate(&self, gpa: u64) -> anyhow::Result { + let offset = gpa + .checked_sub(self.guest_address) + .context("guest physical address precedes mapped region")?; + anyhow::ensure!( + offset < self.len(), + "guest physical address is outside mapped region" + ); + self.host_address() + .checked_add(offset) + .context("host virtual address overflow") + } + + fn read_u16(&self, gpa: u64) -> anyhow::Result { + let offset = gpa + .checked_sub(self.guest_address) + .context("guest physical address precedes mapped region")?; + anyhow::ensure!( + offset + .checked_add(size_of::() as u64) + .is_some_and(|end| end <= self.len()), + "u16 read crosses mapped region boundary" + ); + let mut bytes = [0; 2]; + self.mapping + .read_at(offset as usize, &mut bytes) + .context("failed to read mapped guest memory")?; + Ok(u16::from_le_bytes(bytes)) + } +} + +struct VhostMemory { + // Separate mappings make every RAM region eagerly accessible to the kernel; + // the GuestMemory mapping itself may be populated lazily. + regions: Vec, +} + +impl VhostMemory { + async fn new(guest_memory: &GuestMemory) -> anyhow::Result { + let sharing = guest_memory + .sharing() + .context("kernel vhost-vsock requires shared file-backed guest memory")?; + let mut shared_regions = sharing + .get_regions() + .await + .map_err(anyhow::Error::from_boxed) + .context("failed to query shareable guest-memory regions")?; + anyhow::ensure!( + !shared_regions.is_empty(), + "guest memory has no shareable RAM regions" + ); + shared_regions.sort_by_key(|region| region.guest_address); + + let page_size = SparseMapping::page_size() as u64; + let mut previous_end = 0; + let mut regions = Vec::with_capacity(shared_regions.len()); + for region in shared_regions { + anyhow::ensure!(region.size != 0, "guest-memory region is empty"); + let end = region + .guest_address + .checked_add(region.size) + .context("guest-memory region overflows the GPA address space")?; + anyhow::ensure!( + region.guest_address >= previous_end, + "guest-memory regions overlap" + ); + anyhow::ensure!( + region.guest_address.is_multiple_of(page_size) + && region.size.is_multiple_of(page_size) + && region.file_offset.is_multiple_of(page_size), + "guest-memory region is not page aligned" + ); + + let len = usize::try_from(region.size) + .context("guest-memory region does not fit the host address space")?; + let mapping = SparseMapping::new(len).with_context(|| { + format!( + "failed to reserve mapping for GPA {:#x}", + region.guest_address + ) + })?; + mapping + .map_file(0, len, region.file.as_ref(), region.file_offset, true) + .with_context(|| { + format!( + "failed to map guest-memory region {:#x}..{end:#x}", + region.guest_address + ) + })?; + regions.push(MappedRegion { + guest_address: region.guest_address, + mapping, + }); + previous_end = end; + } + + Ok(Self { regions }) + } + + fn translate(&self, gpa: u64) -> anyhow::Result { + let region = self + .regions + .iter() + .find(|region| region.contains(gpa, 1)) + .with_context(|| format!("guest physical address {gpa:#x} is not in RAM"))?; + region.translate(gpa) + } + + fn read_used_index(&self, params: &QueueParams) -> anyhow::Result { + let gpa = params + .used_addr + .checked_add(2) + .context("used ring index address overflow")?; + let region = self + .regions + .iter() + .find(|region| region.contains(gpa, size_of::() as u64)) + .with_context(|| format!("used ring index address {gpa:#x} is not in RAM"))?; + region.read_u16(gpa) + } +} + +struct QueueRuntime { + params: QueueParams, + _kick: Option, + _call: Option, + _proxy: Option, +} + +impl QueueRuntime { + fn event_queue(params: QueueParams) -> Self { + Self { + params, + _kick: None, + _call: None, + _proxy: None, + } + } +} + +/// A virtio-vsock device whose data path is handled by Linux `vhost_vsock`. +#[derive(InspectMut)] +pub struct VhostVsockDevice { + guest_cid: u32, + #[inspect(skip)] + driver: VmTaskDriver, + #[inspect(skip)] + backend: VhostBackend, + #[inspect(hex)] + kernel_features: u64, + #[inspect(skip)] + memory: Option, + #[inspect(skip)] + queues: [Option; QUEUE_COUNT], + negotiated_features: VirtioDeviceFeatures, + features_set: bool, + running: bool, +} + +impl VhostVsockDevice { + /// Takes ownership of an open `/dev/vhost-vsock` fd and assigns `guest_cid`. + pub fn new( + driver_source: &VmTaskDriverSource, + vhost: OwnedFd, + guest_cid: u32, + ) -> anyhow::Result { + let (backend, kernel_features) = VhostBackend::new(vhost, guest_cid)?; + Ok(Self { + guest_cid, + driver: driver_source.simple(), + backend, + kernel_features, + memory: None, + queues: [const { None }; QUEUE_COUNT], + negotiated_features: VirtioDeviceFeatures::new(), + features_set: false, + running: false, + }) + } + + async fn prepare_backend( + &mut self, + guest_memory: &GuestMemory, + features: &VirtioDeviceFeatures, + ) -> anyhow::Result<()> { + if self.memory.is_none() { + let memory = VhostMemory::new(guest_memory).await?; + self.backend.set_memory(&memory)?; + self.memory = Some(memory); + } + + let negotiated = + VirtioDeviceFeatures::from_bits(features.into_bits() & self.kernel_features); + anyhow::ensure!( + negotiated.version_1(), + "the host vhost-vsock backend does not support modern virtio" + ); + anyhow::ensure!( + !features.ring_packed() || negotiated.ring_packed(), + "the host vhost-vsock backend does not support packed queues" + ); + + if self.features_set { + anyhow::ensure!( + negotiated.into_bits() == self.negotiated_features.into_bits(), + "virtio features changed while vhost-vsock queues were active" + ); + } else { + self.backend.set_features(negotiated.into_bits())?; + self.negotiated_features = negotiated; + self.features_set = true; + } + Ok(()) + } + + fn memory(&self) -> &VhostMemory { + self.memory + .as_ref() + .expect("memory is configured before data queues") + } + + fn configure_data_queue( + &self, + index: u16, + resources: QueueResources, + initial_state: Option, + ) -> anyhow::Result { + let params = resources.params; + self.backend.set_vring_num(index, params.size)?; + self.backend.set_vring_base( + index, + encode_vring_base(self.negotiated_features, initial_state), + )?; + self.backend.set_vring_addr( + index, + self.memory().translate(params.desc_addr)?, + self.memory().translate(params.used_addr)?, + self.memory().translate(params.avail_addr)?, + )?; + + let kick = resources.event; + self.backend + .set_vring_kick(index, kick.as_fd().as_raw_fd())?; + + let (call, proxy) = match resources.notify.event_or_proxy(&self.driver) { + Ok(value) => value, + Err(error) => { + self.backend.unbind_vring_events(index); + return Err(error).context("failed to obtain a virtio interrupt event"); + } + }; + if let Err(error) = self.backend.set_vring_call(index, call.as_fd().as_raw_fd()) { + self.backend.unbind_vring_events(index); + return Err(error); + } + + Ok(QueueRuntime { + params, + _kick: Some(kick), + _call: Some(call), + _proxy: proxy, + }) + } + + fn stop_running(&mut self) { + if !self.running { + return; + } + if let Err(error) = self.backend.set_running(false) { + tracelimit::warn_ratelimited!( + error = &*error as &dyn std::error::Error, + "failed to stop vhost-vsock" + ); + } + self.running = false; + } +} + +impl VirtioDevice for VhostVsockDevice { + fn traits(&self) -> DeviceTraits { + DeviceTraits { + device_id: VirtioDeviceType::VSOCK, + device_features: VirtioDeviceFeatures::from_bits(self.kernel_features), + max_queues: QUEUE_COUNT as u16, + device_register_length: size_of::() as u32, + ..Default::default() + } + } + + async fn read_registers_u32(&mut self, offset: u16) -> u32 { + let config = VsockConfig { + guest_cid: u64::from(self.guest_cid).to_le(), + }; + let bytes = config.as_bytes(); + let offset = usize::from(offset); + if offset + 4 > bytes.len() { + return 0; + } + u32::from_le_bytes([ + bytes[offset], + bytes[offset + 1], + bytes[offset + 2], + bytes[offset + 3], + ]) + } + + async fn write_registers_u32(&mut self, offset: u16, val: u32) { + tracelimit::warn_ratelimited!(offset, val, "vhost-vsock: unexpected config write"); + } + + async fn start_queue( + &mut self, + index: u16, + resources: QueueResources, + features: &VirtioDeviceFeatures, + initial_state: Option, + ) -> anyhow::Result<()> { + let queue_index = usize::from(index); + anyhow::ensure!(queue_index < QUEUE_COUNT, "invalid queue index {index}"); + anyhow::ensure!( + self.queues[queue_index].is_none(), + "virtio queue {index} is already started" + ); + + if index == EVENT_QUEUE { + self.queues[queue_index] = Some(QueueRuntime::event_queue(resources.params)); + return Ok(()); + } + + self.prepare_backend(&resources.guest_memory, features) + .await?; + let runtime = self.configure_data_queue(index, resources, initial_state)?; + self.queues[queue_index] = Some(runtime); + + if self.queues[RX_QUEUE as usize].is_some() + && self.queues[TX_QUEUE as usize].is_some() + && !self.running + { + if let Err(error) = self.backend.set_running(true) { + for data_queue in [RX_QUEUE, TX_QUEUE] { + if self.queues[data_queue as usize].take().is_some() { + let _ = self.backend.get_vring_base(data_queue); + self.backend.unbind_vring_events(data_queue); + } + } + return Err(error); + } + self.running = true; + } + + Ok(()) + } + + async fn stop_queue(&mut self, index: u16) -> Option { + let queue_index = usize::from(index); + let runtime = self.queues.get_mut(queue_index)?.take()?; + if index == EVENT_QUEUE { + return Some(QueueState::default()); + } + + self.stop_running(); + let base = self.backend.get_vring_base(index); + self.backend.unbind_vring_events(index); + + // TODO: Remove once the trait shape supports it + drop(runtime._kick); + drop(runtime._call); + drop(runtime._proxy); + + match base { + Ok(base) => { + let split_used_index = if self.negotiated_features.ring_packed() { + 0 + } else { + match self.memory().read_used_index(&runtime.params) { + Ok(index) => index, + Err(error) => { + tracelimit::warn_ratelimited!( + error = &*error as &dyn std::error::Error, + index, + "failed to read vhost-vsock used index" + ); + return None; + } + } + }; + Some(decode_vring_base( + self.negotiated_features, + base, + split_used_index, + )) + } + Err(error) => { + tracelimit::warn_ratelimited!( + error = &*error as &dyn std::error::Error, + index, + "failed to stop vhost-vsock queue" + ); + None + } + } + } + + async fn reset(&mut self) { + self.stop_running(); + for index in [RX_QUEUE, TX_QUEUE] { + if self.queues[index as usize].is_some() { + let _ = self.backend.get_vring_base(index); + self.backend.unbind_vring_events(index); + } + } + self.queues = [const { None }; QUEUE_COUNT]; + self.negotiated_features = VirtioDeviceFeatures::new(); + self.features_set = false; + } +} + +fn validate_guest_cid(guest_cid: u32) -> anyhow::Result<()> { + anyhow::ensure!( + guest_cid > 2 && guest_cid != u32::MAX, + "vhost-vsock guest CID must be between 3 and {}", + u32::MAX - 1 + ); + Ok(()) +} + +fn encode_vring_base(features: VirtioDeviceFeatures, state: Option) -> u32 { + if features.ring_packed() { + let avail = state.map_or(0x8000, |state| state.avail_index); + let used = state.map_or(0x8000, |state| state.used_index); + u32::from(avail) | (u32::from(used) << 16) + } else { + u32::from(state.map_or(0, |state| state.avail_index)) + } +} + +fn decode_vring_base( + features: VirtioDeviceFeatures, + base: u32, + split_used_index: u16, +) -> QueueState { + if features.ring_packed() { + QueueState { + avail_index: base as u16, + used_index: (base >> 16) as u16, + } + } else { + QueueState { + avail_index: base as u16, + used_index: split_used_index, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use guestmem::GuestMemorySharing; + use guestmem::ProvideShareableRegions; + use guestmem::ShareableRegion; + use guestmem::ShareableRegionError; + use pal_async::async_test; + use std::ptr::NonNull; + use std::sync::Arc; + use test_with_tracing::test; + + struct ShareableGuestMemory { + mapping: SparseMapping, + file: Arc, + size: u64, + } + + impl ShareableGuestMemory { + fn new(size: usize) -> Self { + let file = sparse_mmap::alloc_shared_memory(size, "vhost-vsock-test").unwrap(); + let mapping = SparseMapping::new(size).unwrap(); + mapping.map_file(0, size, &file, 0, true).unwrap(); + Self { + mapping, + file: Arc::new(file), + size: size as u64, + } + } + } + + // SAFETY: `mapping` is stable for the lifetime of this object and `file` + // refers to the same fully committed shared backing. + unsafe impl guestmem::GuestMemoryAccess for ShareableGuestMemory { + fn mapping(&self) -> Option> { + NonNull::new(self.mapping.as_ptr().cast()) + } + + fn max_address(&self) -> u64 { + self.size + } + + fn sharing(&self) -> Option { + Some(GuestMemorySharing::new(TestRegionProvider { + file: self.file.clone(), + size: self.size, + })) + } + } + + struct TestRegionProvider { + file: Arc, + size: u64, + } + + impl ProvideShareableRegions for TestRegionProvider { + async fn get_regions(&self) -> Result, ShareableRegionError> { + Ok(vec![ShareableRegion { + guest_address: 0, + size: self.size, + file: self.file.clone(), + file_offset: 0, + }]) + } + } + + #[test] + fn validates_guest_cids() { + assert!(validate_guest_cid(2).is_err()); + assert!(validate_guest_cid(3).is_ok()); + assert!(validate_guest_cid(u32::MAX - 1).is_ok()); + assert!(validate_guest_cid(u32::MAX).is_err()); + } + + #[test] + fn encodes_queue_state() { + let split = VirtioDeviceFeatures::new(); + let state = QueueState { + avail_index: 7, + used_index: 5, + }; + assert_eq!(encode_vring_base(split, Some(state)), 7); + assert_eq!( + decode_vring_base(split, 7, 5), + QueueState { + avail_index: 7, + used_index: 5, + } + ); + + let packed = VirtioDeviceFeatures::new().with_ring_packed(true); + assert_eq!(encode_vring_base(packed, None), 0x8000_8000); + assert_eq!( + decode_vring_base(packed, 0x8005_8007, 0), + QueueState { + avail_index: 0x8007, + used_index: 0x8005, + } + ); + } + + #[async_test] + async fn maps_shareable_guest_memory() { + let guest_memory = GuestMemory::new("vhost-vsock-test", ShareableGuestMemory::new(0x2000)); + guest_memory.write_at(0x102, &[0x34, 0x12]).unwrap(); + + let memory = VhostMemory::new(&guest_memory).await.unwrap(); + assert_eq!(memory.regions.len(), 1); + assert_eq!( + memory + .read_used_index(&QueueParams { + used_addr: 0x100, + ..Default::default() + }) + .unwrap(), + 0x1234 + ); + assert!( + memory + .read_used_index(&QueueParams { + used_addr: 0x1ffe, + ..Default::default() + }) + .is_err() + ); + assert_eq!( + memory.translate(0x123).unwrap(), + memory.regions[0].host_address() + 0x123 + ); + assert!(memory.translate(0x2000).is_err()); + } +} From 8201d63feab5070ca4de885df050b5327a77a869 Mon Sep 17 00:00:00 2001 From: Will Wright Date: Wed, 22 Jul 2026 18:55:35 +0000 Subject: [PATCH 2/2] virtio_vsock: add kernel backend VMM test --- .github/workflows/openvmm-ci.yaml | 10 ++- .github/workflows/openvmm-pr-release.yaml | 10 ++- .github/workflows/openvmm-pr.yaml | 10 ++- ci-flowey/openvmm-pr.yaml | 4 +- ...sume_and_test_nextest_vmm_tests_archive.rs | 7 ++ .../local_build_and_run_nextest_vmm_tests.rs | 1 + .../src/test_nextest_vmm_tests_archive.rs | 26 ++++++++ petri/Cargo.toml | 3 + petri/src/vm/mod.rs | 33 ++++++++++ petri/src/vm/openvmm/construct.rs | 47 ++++++++++---- petri/src/vm/openvmm/runtime.rs | 64 +++++++++++++++++++ vmm_tests/vmm_tests/tests/tests/multiarch.rs | 24 +++++++ 12 files changed, 220 insertions(+), 19 deletions(-) diff --git a/.github/workflows/openvmm-ci.yaml b/.github/workflows/openvmm-ci.yaml index d75e869df0..2f42b34899 100644 --- a/.github/workflows/openvmm-ci.yaml +++ b/.github/workflows/openvmm-ci.yaml @@ -5243,6 +5243,9 @@ jobs: - name: ensure 2 MiB hugetlb pages are available run: flowey e 24 flowey_lib_hvlite::test_nextest_vmm_tests_archive 2 shell: bash + - name: prepare vhost-vsock + run: flowey e 24 flowey_lib_hvlite::test_nextest_vmm_tests_archive 3 + shell: bash - name: create cargo-nextest cache dir run: |- flowey e 24 flowey_lib_common::download_cargo_nextest 0 @@ -5402,7 +5405,7 @@ jobs: flowey e 24 flowey_lib_hvlite::test_nextest_vmm_tests_archive 0 flowey e 24 flowey_lib_hvlite::run_cargo_nextest_run 1 flowey e 24 flowey_core::pipeline::artifact::resolve 6 - flowey e 24 flowey_lib_hvlite::test_nextest_vmm_tests_archive 3 + flowey e 24 flowey_lib_hvlite::test_nextest_vmm_tests_archive 4 shell: bash - name: generate nextest command run: flowey e 24 flowey_lib_common::gen_cargo_nextest_run_cmd 0 @@ -5558,6 +5561,9 @@ jobs: - name: ensure hypervisor device is accessible run: flowey e 25 flowey_lib_hvlite::test_nextest_vmm_tests_archive 0 shell: bash + - name: prepare vhost-vsock + run: flowey e 25 flowey_lib_hvlite::test_nextest_vmm_tests_archive 1 + shell: bash - name: create cargo-nextest cache dir run: |- flowey e 25 flowey_lib_common::download_cargo_nextest 0 @@ -5716,7 +5722,7 @@ jobs: flowey e 25 flowey_lib_hvlite::init_vmm_tests_env 0 flowey e 25 flowey_lib_hvlite::run_cargo_nextest_run 1 flowey e 25 flowey_core::pipeline::artifact::resolve 5 - flowey e 25 flowey_lib_hvlite::test_nextest_vmm_tests_archive 1 + flowey e 25 flowey_lib_hvlite::test_nextest_vmm_tests_archive 2 shell: bash - name: generate nextest command run: flowey e 25 flowey_lib_common::gen_cargo_nextest_run_cmd 0 diff --git a/.github/workflows/openvmm-pr-release.yaml b/.github/workflows/openvmm-pr-release.yaml index 486af937bc..942d4e6fa4 100644 --- a/.github/workflows/openvmm-pr-release.yaml +++ b/.github/workflows/openvmm-pr-release.yaml @@ -5020,6 +5020,9 @@ jobs: - name: ensure 2 MiB hugetlb pages are available run: flowey e 24 flowey_lib_hvlite::test_nextest_vmm_tests_archive 2 shell: bash + - name: prepare vhost-vsock + run: flowey e 24 flowey_lib_hvlite::test_nextest_vmm_tests_archive 3 + shell: bash - name: create cargo-nextest cache dir run: |- flowey e 24 flowey_lib_common::download_cargo_nextest 0 @@ -5179,7 +5182,7 @@ jobs: flowey e 24 flowey_lib_hvlite::test_nextest_vmm_tests_archive 0 flowey e 24 flowey_lib_hvlite::run_cargo_nextest_run 1 flowey e 24 flowey_core::pipeline::artifact::resolve 6 - flowey e 24 flowey_lib_hvlite::test_nextest_vmm_tests_archive 3 + flowey e 24 flowey_lib_hvlite::test_nextest_vmm_tests_archive 4 shell: bash - name: generate nextest command run: flowey e 24 flowey_lib_common::gen_cargo_nextest_run_cmd 0 @@ -5336,6 +5339,9 @@ jobs: - name: ensure hypervisor device is accessible run: flowey e 25 flowey_lib_hvlite::test_nextest_vmm_tests_archive 0 shell: bash + - name: prepare vhost-vsock + run: flowey e 25 flowey_lib_hvlite::test_nextest_vmm_tests_archive 1 + shell: bash - name: create cargo-nextest cache dir run: |- flowey e 25 flowey_lib_common::download_cargo_nextest 0 @@ -5494,7 +5500,7 @@ jobs: flowey e 25 flowey_lib_hvlite::init_vmm_tests_env 0 flowey e 25 flowey_lib_hvlite::run_cargo_nextest_run 1 flowey e 25 flowey_core::pipeline::artifact::resolve 5 - flowey e 25 flowey_lib_hvlite::test_nextest_vmm_tests_archive 1 + flowey e 25 flowey_lib_hvlite::test_nextest_vmm_tests_archive 2 shell: bash - name: generate nextest command run: flowey e 25 flowey_lib_common::gen_cargo_nextest_run_cmd 0 diff --git a/.github/workflows/openvmm-pr.yaml b/.github/workflows/openvmm-pr.yaml index 3f971a7553..96af00a215 100644 --- a/.github/workflows/openvmm-pr.yaml +++ b/.github/workflows/openvmm-pr.yaml @@ -5424,6 +5424,9 @@ jobs: - name: ensure 2 MiB hugetlb pages are available run: flowey e 26 flowey_lib_hvlite::test_nextest_vmm_tests_archive 2 shell: bash + - name: prepare vhost-vsock + run: flowey e 26 flowey_lib_hvlite::test_nextest_vmm_tests_archive 3 + shell: bash - name: create cargo-nextest cache dir run: |- flowey e 26 flowey_lib_common::download_cargo_nextest 0 @@ -5583,7 +5586,7 @@ jobs: flowey e 26 flowey_lib_hvlite::test_nextest_vmm_tests_archive 0 flowey e 26 flowey_lib_hvlite::run_cargo_nextest_run 1 flowey e 26 flowey_core::pipeline::artifact::resolve 6 - flowey e 26 flowey_lib_hvlite::test_nextest_vmm_tests_archive 3 + flowey e 26 flowey_lib_hvlite::test_nextest_vmm_tests_archive 4 shell: bash - name: generate nextest command run: flowey e 26 flowey_lib_common::gen_cargo_nextest_run_cmd 0 @@ -5740,6 +5743,9 @@ jobs: - name: ensure hypervisor device is accessible run: flowey e 27 flowey_lib_hvlite::test_nextest_vmm_tests_archive 0 shell: bash + - name: prepare vhost-vsock + run: flowey e 27 flowey_lib_hvlite::test_nextest_vmm_tests_archive 1 + shell: bash - name: create cargo-nextest cache dir run: |- flowey e 27 flowey_lib_common::download_cargo_nextest 0 @@ -5898,7 +5904,7 @@ jobs: flowey e 27 flowey_lib_hvlite::init_vmm_tests_env 0 flowey e 27 flowey_lib_hvlite::run_cargo_nextest_run 1 flowey e 27 flowey_core::pipeline::artifact::resolve 5 - flowey e 27 flowey_lib_hvlite::test_nextest_vmm_tests_archive 1 + flowey e 27 flowey_lib_hvlite::test_nextest_vmm_tests_archive 2 shell: bash - name: generate nextest command run: flowey e 27 flowey_lib_common::gen_cargo_nextest_run_cmd 0 diff --git a/ci-flowey/openvmm-pr.yaml b/ci-flowey/openvmm-pr.yaml index 5b5e88e4c9..4c17efc2c1 100644 --- a/ci-flowey/openvmm-pr.yaml +++ b/ci-flowey/openvmm-pr.yaml @@ -4536,6 +4536,8 @@ jobs: displayName: ensure hypervisor device is accessible - bash: $(FLOWEY_BIN) e 19 flowey_lib_hvlite::test_nextest_vmm_tests_archive 2 displayName: ensure 2 MiB hugetlb pages are available + - bash: $(FLOWEY_BIN) e 19 flowey_lib_hvlite::test_nextest_vmm_tests_archive 3 + displayName: prepare vhost-vsock - bash: |- set -e $(FLOWEY_BIN) e 19 flowey_lib_common::download_cargo_nextest 0 @@ -4650,7 +4652,7 @@ jobs: $(FLOWEY_BIN) e 19 flowey_lib_hvlite::test_nextest_vmm_tests_archive 0 $(FLOWEY_BIN) e 19 flowey_lib_hvlite::run_cargo_nextest_run 1 $(FLOWEY_BIN) e 19 flowey_core::pipeline::artifact::resolve 6 - $(FLOWEY_BIN) e 19 flowey_lib_hvlite::test_nextest_vmm_tests_archive 3 + $(FLOWEY_BIN) e 19 flowey_lib_hvlite::test_nextest_vmm_tests_archive 4 displayName: setting up vmm_tests env - bash: $(FLOWEY_BIN) e 19 flowey_lib_common::gen_cargo_nextest_run_cmd 0 displayName: generate nextest command diff --git a/flowey/flowey_lib_hvlite/src/_jobs/consume_and_test_nextest_vmm_tests_archive.rs b/flowey/flowey_lib_hvlite/src/_jobs/consume_and_test_nextest_vmm_tests_archive.rs index 30f1dbe64f..d89ac3efc2 100644 --- a/flowey/flowey_lib_hvlite/src/_jobs/consume_and_test_nextest_vmm_tests_archive.rs +++ b/flowey/flowey_lib_hvlite/src/_jobs/consume_and_test_nextest_vmm_tests_archive.rs @@ -288,6 +288,12 @@ impl SimpleFlowNode for Node { register_prep_steps.claim_unused(ctx); } + let prepare_vhost_vsock = incubator_profile.is_none() + && matches!( + target.operating_system, + target_lexicon::OperatingSystem::Linux + ) + && matches!(target.architecture, target_lexicon::Architecture::X86_64); let (extra_env, nextest_working_dir, nextest_config_file) = if let Some(profile_name) = incubator_profile { @@ -368,6 +374,7 @@ impl SimpleFlowNode for Node { extra_env, pre_run_deps, hugetlb_2mb_overcommit_pages, + prepare_vhost_vsock, results: v, }); diff --git a/flowey/flowey_lib_hvlite/src/_jobs/local_build_and_run_nextest_vmm_tests.rs b/flowey/flowey_lib_hvlite/src/_jobs/local_build_and_run_nextest_vmm_tests.rs index 827505c608..8e9fe4cb24 100644 --- a/flowey/flowey_lib_hvlite/src/_jobs/local_build_and_run_nextest_vmm_tests.rs +++ b/flowey/flowey_lib_hvlite/src/_jobs/local_build_and_run_nextest_vmm_tests.rs @@ -952,6 +952,7 @@ impl SimpleFlowNode for Node { extra_env, pre_run_deps: side_effects, hugetlb_2mb_overcommit_pages: None, + prepare_vhost_vsock: false, results: v, }); diff --git a/flowey/flowey_lib_hvlite/src/test_nextest_vmm_tests_archive.rs b/flowey/flowey_lib_hvlite/src/test_nextest_vmm_tests_archive.rs index a472b40e75..093784e6d9 100644 --- a/flowey/flowey_lib_hvlite/src/test_nextest_vmm_tests_archive.rs +++ b/flowey/flowey_lib_hvlite/src/test_nextest_vmm_tests_archive.rs @@ -37,6 +37,8 @@ flowey_request! { pub pre_run_deps: Vec>, /// If set, configure this 2 MiB hugetlb surplus page overcommit limit before running tests. pub hugetlb_2mb_overcommit_pages: Option, + /// Load vhost-vsock and make `/dev/vhost-vsock` accessible before tests. + pub prepare_vhost_vsock: bool, /// Results of running the tests pub results: WriteVar, } @@ -63,6 +65,7 @@ impl SimpleFlowNode for Node { mut extra_env, mut pre_run_deps, hugetlb_2mb_overcommit_pages, + prepare_vhost_vsock, results, } = request; @@ -136,6 +139,29 @@ impl SimpleFlowNode for Node { }) }); } + + if prepare_vhost_vsock { + pre_run_deps.push({ + ctx.emit_rust_step("prepare vhost-vsock", |_| { + |rt| { + flowey::shell_cmd!(rt, "sudo modprobe vhost_vsock").run()?; + for _ in 0..50 { + if Path::new("/dev/vhost-vsock").exists() { + break; + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + if !Path::new("/dev/vhost-vsock").exists() { + anyhow::bail!( + "/dev/vhost-vsock did not appear after loading vhost_vsock" + ); + } + flowey::shell_cmd!(rt, "sudo chmod a+rw /dev/vhost-vsock").run()?; + Ok(()) + } + }) + }); + } } let nextest_archive = nextest_archive_file.map(ctx, |x| x.archive_file); diff --git a/petri/Cargo.toml b/petri/Cargo.toml index 3459be75c4..f727ec74f0 100644 --- a/petri/Cargo.toml +++ b/petri/Cargo.toml @@ -96,6 +96,9 @@ vmsocket.workspace = true vmswitch.workspace = true windows-version.workspace = true +[target.'cfg(target_os = "linux")'.dependencies] +vmsocket.workspace = true + [target.'cfg(target_arch = "x86_64")'.dependencies] hvdef.workspace = true safe_intrinsics.workspace = true diff --git a/petri/src/vm/mod.rs b/petri/src/vm/mod.rs index 67345fb0a1..c7dfa9e3aa 100644 --- a/petri/src/vm/mod.rs +++ b/petri/src/vm/mod.rs @@ -185,6 +185,9 @@ pub struct PetriVmBuilder { prebuilt_initrd: Option, // Use virtio vsock instead of VMBus-based hvsocket for guest communication. use_virtio_vsock: bool, + // Use the Linux kernel vhost-vsock backend with this guest CID. + #[cfg(target_os = "linux")] + vhost_vsock_guest_cid: Option, // Disable VMBus entirely (no vmbus server, no vmbus storage controllers). no_vmbus: bool, } @@ -293,6 +296,9 @@ pub struct PetriVmProperties { pub has_agent_disk: bool, /// Use virtio vsock instead of VMBus-based hvsocket pub use_virtio_vsock: bool, + /// Linux kernel vhost-vsock guest CID, when that backend is enabled. + #[cfg(target_os = "linux")] + pub vhost_vsock_guest_cid: Option, /// VMBus is entirely disabled pub no_vmbus: bool, } @@ -467,6 +473,8 @@ impl PetriVmBuilder { enable_screenshots: true, prebuilt_initrd: None, use_virtio_vsock: false, + #[cfg(target_os = "linux")] + vhost_vsock_guest_cid: None, no_vmbus: false, } .add_petri_scsi_controllers() @@ -544,6 +552,8 @@ impl PetriVmBuilder { enable_screenshots: true, prebuilt_initrd: None, use_virtio_vsock: false, + #[cfg(target_os = "linux")] + vhost_vsock_guest_cid: None, no_vmbus: false, }) } @@ -652,6 +662,27 @@ impl PetriVmBuilder { /// blacklist hv_sock instead of virtio_vsock. pub fn with_virtio_vsock(mut self) -> Self { self.use_virtio_vsock = true; + #[cfg(target_os = "linux")] + { + self.vhost_vsock_guest_cid = None; + } + self + } + + /// Use the Linux kernel vhost-vsock backend for guest communication. + /// + /// The host connects directly to the guest's `AF_VSOCK` listener at + /// `guest_cid`. The OpenVMM backend automatically uses shared guest memory, + /// which is required by kernel vhost. + #[cfg(target_os = "linux")] + pub fn with_vhost_vsock(mut self, guest_cid: u32) -> Self { + assert!( + (3..u32::MAX).contains(&guest_cid), + "vhost-vsock guest CID must be between 3 and {}", + u32::MAX - 1 + ); + self.use_virtio_vsock = true; + self.vhost_vsock_guest_cid = Some(guest_cid); self } @@ -973,6 +1004,8 @@ impl PetriVmBuilder { prebuilt_initrd: self.prebuilt_initrd.clone(), has_agent_disk: self.has_agent_disk(), use_virtio_vsock: self.use_virtio_vsock, + #[cfg(target_os = "linux")] + vhost_vsock_guest_cid: self.vhost_vsock_guest_cid, no_vmbus: self.no_vmbus, } } diff --git a/petri/src/vm/openvmm/construct.rs b/petri/src/vm/openvmm/construct.rs index b1e08d4204..a80780dfcf 100644 --- a/petri/src/vm/openvmm/construct.rs +++ b/petri/src/vm/openvmm/construct.rs @@ -89,11 +89,14 @@ use video_core::SharedFramebufferHandle; use virtio_resources::VirtioPciDeviceHandle; use virtio_resources::blk::VirtioBlkHandle; use virtio_resources::vsock::VirtioVsockHandle; +#[cfg(target_os = "linux")] +use virtio_resources::vsock::VirtioVsockVhostHandle; use vm_manifest_builder::VmChipsetResult; use vm_manifest_builder::VmManifestBuilder; use vm_resource::IntoResource; use vm_resource::Resource; use vm_resource::kind::SerialBackendHandle; +use vm_resource::kind::VirtioDeviceHandle; use vm_resource::kind::VmbusDeviceHandleKind; use vmbus_serial_resources::VmbusSerialDeviceHandle; use vmbus_serial_resources::VmbusSerialPort; @@ -131,6 +134,10 @@ impl PetriVmConfigOpenVmm { tracing::debug!(?firmware, ?arch, "Petri VM firmware configuration"); let PetriVmResources { driver, log_source } = resources; + #[cfg(target_os = "linux")] + let vhost_vsock_guest_cid = properties.vhost_vsock_guest_cid; + #[cfg(not(target_os = "linux"))] + let vhost_vsock_guest_cid: Option = None; let mesh = Mesh::new("petri_mesh".to_string())?; @@ -447,15 +454,16 @@ impl PetriVmConfigOpenVmm { // - PCAT (Gen1) relies on x86 legacy support (the VGA hole and // PAM registers), which toggles low RAM visibility in a way // that requires shared, file-backed memory. - let private_incompatible = firmware.is_openhcl() || firmware.is_pcat(); + let private_incompatible = + firmware.is_openhcl() || firmware.is_pcat() || vhost_vsock_guest_cid.is_some(); let private_memory = match private_memory { // An explicit request for private memory that the firmware // cannot honor is an error, rather than a silent downgrade. Some(true) if private_incompatible => { anyhow::bail!( "private guest memory was explicitly requested but is \ - not supported with this firmware (OpenHCL and \ - PCAT/Gen1 require shared memory)" + not supported with this configuration (OpenHCL, \ + PCAT/Gen1, and kernel vhost-vsock require shared memory)" ); } Some(explicit) => explicit, @@ -593,17 +601,32 @@ impl PetriVmConfigOpenVmm { .map(|i| format!("s0rc0rp{i}")) .find(|name| !pcie_devices.iter().any(|d| d.port_name == *name)) .unwrap(); + let resource: Resource = match vhost_vsock_guest_cid { + #[cfg(target_os = "linux")] + Some(guest_cid) => { + let vhost = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open("/dev/vhost-vsock") + .context("failed to open /dev/vhost-vsock")? + .into(); + // The kernel backend does not use the Unix relay. Clear it + // so VmbusConfig below does not receive the listener. + vsock_listener = None; + VirtioVsockVhostHandle { vhost, guest_cid }.into_resource() + } + #[cfg(not(target_os = "linux"))] + Some(_) => unreachable!("kernel vhost-vsock is Linux-only"), + None => VirtioVsockHandle { + guest_cid: 0x3, + base_path: vsock_path_string.to_string(), + listener: vsock_listener.take().unwrap(), + } + .into_resource(), + }; pcie_devices.push(PcieDeviceConfig { port_name: vsock_port, - resource: VirtioPciDeviceHandle( - VirtioVsockHandle { - guest_cid: 0x3, - base_path: vsock_path_string.to_string(), - listener: vsock_listener.take().unwrap(), - } - .into_resource(), - ) - .into_resource(), + resource: VirtioPciDeviceHandle(resource).into_resource(), }); } diff --git a/petri/src/vm/openvmm/runtime.rs b/petri/src/vm/openvmm/runtime.rs index 57ea3082ea..b2304dd12a 100644 --- a/petri/src/vm/openvmm/runtime.rs +++ b/petri/src/vm/openvmm/runtime.rs @@ -30,12 +30,18 @@ use mesh_process::Mesh; use openvmm_defs::rpc::PulseSaveRestoreError; use pal_async::socket::PolledSocket; use petri_artifacts_core::ResolvedArtifact; +#[cfg(target_os = "linux")] +use pipette_client::PIPETTE_PORT; use pipette_client::PipetteClient; use std::future::Future; use std::path::Path; use std::sync::Arc; use std::time::Duration; use vmm_core_defs::HaltReason; +#[cfg(target_os = "linux")] +use vmsocket::VmAddress; +#[cfg(target_os = "linux")] +use vmsocket::VmSocket; use vtl2_settings_proto::Vtl2Settings; /// A running VM that tests can interact with. @@ -564,6 +570,15 @@ impl PetriVmInner { } async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result { + #[cfg(target_os = "linux")] + if let Some(guest_cid) = self.resources.properties.vhost_vsock_guest_cid { + assert!( + !set_high_vtl, + "kernel vhost-vsock pipette transport does not support VTL2" + ); + return self.wait_for_agent_vhost_vsock(guest_cid).await; + } + // Use TCP transport if configured (Windows no-vmbus guests). if let Some(port) = self.tcp_pipette_port { assert!(!set_high_vtl, "TCP pipette transport does not support VTL2"); @@ -638,6 +653,55 @@ impl PetriVmInner { Ok(client) } + /// Connect to pipette directly through the host's AF_VSOCK namespace. + #[cfg(target_os = "linux")] + async fn wait_for_agent_vhost_vsock( + &mut self, + guest_cid: u32, + ) -> anyhow::Result { + tracing::info!( + guest_cid, + port = PIPETTE_PORT, + "connecting to pipette via kernel vhost-vsock" + ); + let socket = loop { + let connect = async { + let socket = VmSocket::new().context("failed to create AF_VSOCK socket")?; + socket + .set_connect_timeout(Duration::from_secs(5)) + .context("failed to set AF_VSOCK connect timeout")?; + let mut socket = PolledSocket::new(&self.resources.driver, socket) + .context("failed to create polled AF_VSOCK socket")? + .convert(); + socket + .connect(&VmAddress::vsock(guest_cid, PIPETTE_PORT).into()) + .await + .context("failed to connect to guest AF_VSOCK listener")?; + Ok::<_, anyhow::Error>(socket) + }; + + match connect.await { + Ok(socket) => break socket, + Err(error) => { + tracing::trace!( + error = error.as_ref() as &dyn std::error::Error, + "AF_VSOCK connect failed, guest not ready yet" + ); + } + } + + pal_async::timer::PolledTimer::new(&self.resources.driver) + .sleep(Duration::from_secs(1)) + .await; + }; + tracing::info!("AF_VSOCK connected, handshaking with pipette"); + let client = PipetteClient::new(&self.resources.driver, socket, &self.resources.output_dir) + .await + .context("pipette AF_VSOCK handshake failed")?; + tracing::info!("completed pipette AF_VSOCK handshake"); + Ok(client) + } + /// Connect to pipette via TCP through consomme port forwarding. /// /// The guest pipette agent listens on `0.0.0.0:{port}` and consomme diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch.rs b/vmm_tests/vmm_tests/tests/tests/multiarch.rs index df52becdae..1527ef74c7 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch.rs @@ -120,6 +120,30 @@ async fn boot_virtio_vsock(config: PetriVmBuilder) -> anyho Ok(()) } +/// Basic boot test using the Linux kernel vhost-vsock backend. +/// +/// Petri connects to the guest directly through the host AF_VSOCK namespace, +/// so successfully establishing the pipette session validates the full +/// host-kernel-to-guest virtio-vsock path. +#[cfg(target_os = "linux")] +#[openvmm_test(linux_direct_x64)] +async fn boot_vhost_vsock(config: PetriVmBuilder) -> anyhow::Result<()> { + // Avoid collisions with other vhost-vsock devices on a shared test host. + let guest_cid = 0x4000_0000 | (std::process::id() & 0x3fff_ffff); + let (vm, agent) = config + .with_memory(MemoryConfig { + private_memory: Some(false), + ..Default::default() + }) + .with_vhost_vsock(guest_cid) + .modify_backend(|b| b.with_pcie_root_topology(1, 1, 1)) + .run() + .await?; + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + Ok(()) +} + /// Boot Linux direct with VMBus entirely disabled. /// /// Virtio-vsock provides the pipette transport. No VMBus server, no VMBus