From 32a533748e17df2f1910496ca404a8e68bd8845f Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 21 Aug 2026 11:31:00 -0700 Subject: [PATCH 01/18] vmm integration tests for vf reset --- Cargo.lock | 17 ++ Cargo.toml | 1 + openvmm/openvmm_resources/Cargo.toml | 1 + openvmm/openvmm_resources/src/lib.rs | 1 + petri/src/vm/openvmm/mod.rs | 1 + petri/src/vm/openvmm/modify.rs | 106 ++++++++++++ vm/devices/net/gdma/src/lib.rs | 26 +++ vm/devices/net/gdma/src/queues.rs | 2 +- vm/devices/net/gdma/src/resolver.rs | 49 +++--- vm/devices/net/gdma_resources/src/lib.rs | 32 ++++ vm/devices/net/gdma_test/Cargo.toml | 24 +++ vm/devices/net/gdma_test/src/lib.rs | 13 ++ vm/devices/net/gdma_test/src/resolver.rs | 161 ++++++++++++++++++ .../tests/x86_64/openhcl_linux_direct.rs | 58 +------ .../x86_64/openhcl_linux_direct/mana_nic.rs | 146 ++++++++++++++++ 15 files changed, 562 insertions(+), 76 deletions(-) create mode 100644 vm/devices/net/gdma_test/Cargo.toml create mode 100644 vm/devices/net/gdma_test/src/lib.rs create mode 100644 vm/devices/net/gdma_test/src/resolver.rs create mode 100644 vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs diff --git a/Cargo.lock b/Cargo.lock index 9758628b57..a7540e5a5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2876,6 +2876,22 @@ dependencies = [ "vm_resource", ] +[[package]] +name = "gdma_test" +version = "0.0.0" +dependencies = [ + "async-trait", + "futures", + "gdma", + "gdma_defs", + "gdma_resources", + "pal_async", + "pci_resources", + "test_with_tracing", + "vm_resource", + "zerocopy", +] + [[package]] name = "generation_id" version = "0.0.0" @@ -6037,6 +6053,7 @@ dependencies = [ "disklayer_vhdx", "firmware_uefi", "gdma", + "gdma_test", "guest_crash_device", "guest_emulation_device", "guest_emulation_log", diff --git a/Cargo.toml b/Cargo.toml index e0e1029e30..798841fd2e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -280,6 +280,7 @@ missing_dev_resources = { path = "vm/devices/missing_dev_resources" } gdma = { path = "vm/devices/net/gdma" } gdma_defs = { path = "vm/devices/net/gdma_defs" } gdma_resources = { path = "vm/devices/net/gdma_resources" } +gdma_test = { path = "vm/devices/net/gdma_test" } linux_net_bindings = { path = "vm/devices/net/linux_net_bindings" } mana_driver = { path = "vm/devices/net/mana_driver" } vfio_sys = { path = "vm/devices/user_driver/vfio_sys" } diff --git a/openvmm/openvmm_resources/Cargo.toml b/openvmm/openvmm_resources/Cargo.toml index ee1e92e762..27804fcd68 100644 --- a/openvmm/openvmm_resources/Cargo.toml +++ b/openvmm/openvmm_resources/Cargo.toml @@ -62,6 +62,7 @@ vmgs_broker.workspace = true # PCI devices cxl_spec.workspace = true gdma.workspace = true +gdma_test.workspace = true nvme.workspace = true nvme_test.workspace = true diff --git a/openvmm/openvmm_resources/src/lib.rs b/openvmm/openvmm_resources/src/lib.rs index 91054d9fcf..aa413203ec 100644 --- a/openvmm/openvmm_resources/src/lib.rs +++ b/openvmm/openvmm_resources/src/lib.rs @@ -88,6 +88,7 @@ vm_resource::register_static_resolvers! { // PCI devices cxl_spec::test::resolver::CxlTestDeviceResolver, gdma::resolver::GdmaDeviceResolver, + gdma_test::resolver::GdmaTestDeviceResolver, nvme::resolver::NvmeControllerResolver, nvme_test::resolver::NvmeFaultControllerResolver, virtio::resolver::VirtioPciResolver, diff --git a/petri/src/vm/openvmm/mod.rs b/petri/src/vm/openvmm/mod.rs index 2b00d38147..fc6a060d9a 100644 --- a/petri/src/vm/openvmm/mod.rs +++ b/petri/src/vm/openvmm/mod.rs @@ -19,6 +19,7 @@ mod start; pub use hugetlb::HUGETLB_2MB_PAGE_SIZE; #[cfg(target_os = "linux")] pub use hugetlb::ensure_2mb_hugetlb_pages; +pub use modify::ManaTestControl; pub use runtime::OpenVmmFramebufferAccess; pub use runtime::OpenVmmInspector; pub use runtime::PetriVmOpenVmm; diff --git a/petri/src/vm/openvmm/modify.rs b/petri/src/vm/openvmm/modify.rs index 1994c45608..e68785dbed 100644 --- a/petri/src/vm/openvmm/modify.rs +++ b/petri/src/vm/openvmm/modify.rs @@ -15,9 +15,12 @@ use chipset_resources::battery::HostBatteryUpdate; use disk_backend_resources::LayeredDiskHandle; use disk_backend_resources::layer::RamDiskLayerHandle; use gdma_resources::GdmaDeviceHandle; +use gdma_resources::GdmaTestDeviceHandle; +use gdma_resources::GdmaTestRequest; use gdma_resources::VportDefinition; use get_resources::ged::IgvmAttestTestConfig; use guid::Guid; +use mesh::rpc::RpcSend; use net_backend_resources::mac_address::MacAddress; use nvme_resources::NamespaceDefinition; use nvme_resources::NvmeControllerHandle; @@ -35,6 +38,8 @@ use openvmm_defs::config::Vtl2BaseAddressType; use vm_resource::IntoResource; use vmotherboard::ChipsetDeviceHandle; +const MANA_VTL0_INSTANCE: Guid = guid::guid!("f8615163-d915-4743-a7d8-efa75db7b85a"); + impl PetriVmConfigOpenVmm { /// Enable the VTL0 alias map. // TODO: Remove once #912 is fixed. @@ -154,6 +159,62 @@ impl PetriVmConfigOpenVmm { self } + /// Add a test-controllable VTL2 PCIe NIC and its paired VTL0 VF. + pub fn with_nic_test_control(mut self, test_config: ManaTestConfig) -> Self { + let vtl2_endpoint = net_backend_resources::consomme::ConsommeHandle { + cidr: None, + ports: Vec::new(), + recv: None, + } + .into_resource(); + let vtl0_endpoint = net_backend_resources::consomme::ConsommeHandle { + cidr: None, + ports: Vec::new(), + recv: None, + } + .into_resource(); + + if let Some(vtl2_settings) = self.runtime_config.vtl2_settings.as_mut() { + self.config.vpci_devices.extend([ + VpciDeviceConfig { + vtl: DeviceVtl::Vtl2, + instance_id: MANA_INSTANCE, + resource: GdmaTestDeviceHandle { + vports: vec![VportDefinition { + mac_address: NIC_MAC_ADDRESS, + endpoint: vtl2_endpoint, + }], + request_recv: test_config.request_recv, + } + .into_resource(), + vnode: None, + }, + VpciDeviceConfig { + vtl: DeviceVtl::Vtl0, + instance_id: MANA_VTL0_INSTANCE, + resource: GdmaDeviceHandle { + vports: vec![VportDefinition { + mac_address: NIC_MAC_ADDRESS, + endpoint: vtl0_endpoint, + }], + } + .into_resource(), + vnode: None, + }, + ]); + + vtl2_settings.dynamic.as_mut().unwrap().nic_devices.push( + vtl2_settings_proto::NicDeviceLegacy { + instance_id: MANA_INSTANCE.to_string(), + subordinate_instance_id: Some(MANA_VTL0_INSTANCE.to_string()), + max_sub_channels: None, + }, + ); + } + + self + } + /// Add a PCIe NVMe device to the VM using the NVMe emulator. pub fn with_pcie_nvme(mut self, port_name: &str, subsystem_id: Guid) -> Self { self.config.pcie_devices.push(PcieDeviceConfig { @@ -589,3 +650,48 @@ impl PetriVmConfigOpenVmm { self } } + +/// Control interface for issuing test requests to the MANA emulator. +/// +/// Created with [`ManaTestControl::new`] and connected with +/// [`PetriVmConfigOpenVmm::with_nic_test_control`]. +pub struct ManaTestControl { + request_send: mesh::Sender>, +} + +/// Configuration token for a test-controllable MANA emulator. +pub struct ManaTestConfig { + request_recv: mesh::Receiver>, +} + +impl ManaTestControl { + /// Creates a test control and its corresponding device configuration. + pub fn new() -> (Self, ManaTestConfig) { + let (request_send, request_recv) = mesh::channel(); + (Self { request_send }, ManaTestConfig { request_recv }) + } + + /// Requests VF reconfiguration through the emulated hardware channel. + /// + /// `revoke_vtl0_vf`: when `true` the guest VTL0 VF is revoked as part of + /// the reconfiguration. + pub async fn inject_vf_reset(&self, revoke_vtl0_vf: bool) -> Result<(), mesh::rpc::RpcError> { + self.request_send + .call(|rpc| rpc, GdmaTestRequest::VfReset { revoke_vtl0_vf }) + .await + } + + /// Changes the link state of `vport`. + pub async fn set_vport_link_state( + &self, + vport: u32, + connected: bool, + ) -> Result<(), mesh::rpc::RpcError> { + self.request_send + .call( + |rpc| rpc, + GdmaTestRequest::VportLinkState { vport, connected }, + ) + .await + } +} diff --git a/vm/devices/net/gdma/src/lib.rs b/vm/devices/net/gdma/src/lib.rs index 00f01dd5b1..85186220d8 100644 --- a/vm/devices/net/gdma/src/lib.rs +++ b/vm/devices/net/gdma/src/lib.rs @@ -121,12 +121,38 @@ enum SmcError { pub use bnic::BnicConfig; +/// A cloneable handle for injecting EQEs into the hardware channel EQ. +/// +/// Obtained via [`GdmaDevice::hwc_eq_injector`] before handing the device to +/// the VM machinery. +#[derive(Clone)] +pub struct HwcEqInjector { + queues: Arc, +} + +impl HwcEqInjector { + /// Posts an EQE directly to the hardware channel EQ, bypassing the guest + /// driver command path. Intended for out-of-band test EQE injection in + /// VMM tests (`gdma_test`). + pub fn post(&self, ty: u8, data: &[u8]) { + self.queues.post_eq(queues::ID_OFFSET as u32, ty, data) + } +} + pub struct VportConfig { pub mac_address: MacAddress, pub endpoint: Box, } impl GdmaDevice { + /// Returns a cloneable handle for injecting EQEs into the hardware channel + /// EQ after this device has been handed to the VM machinery. + pub fn hwc_eq_injector(&self) -> HwcEqInjector { + HwcEqInjector { + queues: self.queues.clone(), + } + } + pub fn new( driver_source: &VmTaskDriverSource, gm: GuestMemory, diff --git a/vm/devices/net/gdma/src/queues.rs b/vm/devices/net/gdma/src/queues.rs index 8aa186dfb4..3746ddffa8 100644 --- a/vm/devices/net/gdma/src/queues.rs +++ b/vm/devices/net/gdma/src/queues.rs @@ -37,7 +37,7 @@ use zerocopy::IntoBytes; use zerocopy::KnownLayout; // Offset the queue IDs seen by the guest. -const ID_OFFSET: usize = 24; +pub(crate) const ID_OFFSET: usize = 24; struct CqEq { region: DmaRegion, diff --git a/vm/devices/net/gdma/src/resolver.rs b/vm/devices/net/gdma/src/resolver.rs index ec2e9c4cdb..825919e872 100644 --- a/vm/devices/net/gdma/src/resolver.rs +++ b/vm/devices/net/gdma/src/resolver.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -//! Resource resolver for the nvme controller. +//! Resource resolver for the GDMA device. use crate::GdmaDevice; use crate::VportConfig; @@ -26,7 +26,7 @@ declare_static_async_resolver! { (PciDeviceHandleKind, GdmaDeviceHandle), } -/// Error returned by [`GdmaDeviceResolver`]. +/// Error returned by [`GdmaDeviceResolver`] and shared resolvers. #[derive(Debug, Error)] #[expect(missing_docs)] pub enum Error { @@ -34,6 +34,33 @@ pub enum Error { VportResolve(#[source] ResolveError), } +/// Resolves a list of [`gdma_resources::VportDefinition`]s into +/// [`VportConfig`]s. +/// +/// Shared by [`GdmaDeviceResolver`] and `gdma_test`'s test resolver. +pub async fn resolve_vports( + resolver: &ResourceResolver, + vports: Vec, +) -> Result, Error> { + try_join_all(vports.into_iter().map(async |vport| { + let endpoint = resolver + .resolve( + vport.endpoint, + ResolveEndpointParams { + mac_address: vport.mac_address, + }, + ) + .await + .map_err(Error::VportResolve)?; + + Ok(VportConfig { + mac_address: vport.mac_address, + endpoint: endpoint.0, + }) + })) + .await +} + #[async_trait] impl AsyncResolveResource for GdmaDeviceResolver { type Output = ResolvedPciDevice; @@ -45,23 +72,7 @@ impl AsyncResolveResource for GdmaDeviceR resource: GdmaDeviceHandle, input: ResolvePciDeviceHandleParams<'_>, ) -> Result { - let vports = try_join_all(resource.vports.into_iter().map(async |vport| { - let endpoint = resolver - .resolve( - vport.endpoint, - ResolveEndpointParams { - mac_address: vport.mac_address, - }, - ) - .await - .map_err(Error::VportResolve)?; - - Ok(VportConfig { - mac_address: vport.mac_address, - endpoint: endpoint.0, - }) - })) - .await?; + let vports = resolve_vports(resolver, resource.vports).await?; let device = GdmaDevice::new( input.driver_source, diff --git a/vm/devices/net/gdma_resources/src/lib.rs b/vm/devices/net/gdma_resources/src/lib.rs index 9bbc7d0e3f..c963c52e40 100644 --- a/vm/devices/net/gdma_resources/src/lib.rs +++ b/vm/devices/net/gdma_resources/src/lib.rs @@ -23,6 +23,38 @@ impl ResourceId for GdmaDeviceHandle { const ID: &'static str = "gdma"; } +/// A resource handle to a test-controllable GDMA device. +/// +/// Used in VMM tests to issue typed hardware requests. +#[derive(MeshPayload)] +pub struct GdmaTestDeviceHandle { + /// The vports to instantiate on the NIC. + pub vports: Vec, + /// Channel for delivering requests from the test harness. + pub request_recv: mesh::Receiver>, +} + +impl ResourceId for GdmaTestDeviceHandle { + const ID: &'static str = "gdma-test"; +} + +/// A test request for an emulated GDMA device. +#[derive(MeshPayload)] +pub enum GdmaTestRequest { + /// Request that the VF be reconfigured. + VfReset { + /// Whether OpenHCL should revoke the VTL0 VF during reset. + revoke_vtl0_vf: bool, + }, + /// Change a vport's link state. + VportLinkState { + /// The zero-based vport index. + vport: u32, + /// Whether the link should be connected. + connected: bool, + }, +} + /// A basic NIC vport definition. #[derive(MeshPayload)] pub struct VportDefinition { diff --git a/vm/devices/net/gdma_test/Cargo.toml b/vm/devices/net/gdma_test/Cargo.toml new file mode 100644 index 0000000000..6323d2a0ae --- /dev/null +++ b/vm/devices/net/gdma_test/Cargo.toml @@ -0,0 +1,24 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[package] +name = "gdma_test" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +gdma.workspace = true +gdma_defs.workspace = true +gdma_resources.workspace = true +pci_resources.workspace = true +vm_resource.workspace = true +pal_async.workspace = true +async-trait.workspace = true +futures.workspace = true +zerocopy.workspace = true + +[dev-dependencies] +test_with_tracing.workspace = true + +[lints] +workspace = true diff --git a/vm/devices/net/gdma_test/src/lib.rs b/vm/devices/net/gdma_test/src/lib.rs new file mode 100644 index 0000000000..e4c542c624 --- /dev/null +++ b/vm/devices/net/gdma_test/src/lib.rs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! A GDMA device emulator with out-of-band EQE injection support. +//! +//! The production [`gdma`] crate has no test-specific state; +//! this crate provides the test-controllable variant via +//! [`gdma_resources::GdmaTestDeviceHandle`] and +//! [`resolver::GdmaTestDeviceResolver`]. + +#![forbid(unsafe_code)] + +pub mod resolver; diff --git a/vm/devices/net/gdma_test/src/resolver.rs b/vm/devices/net/gdma_test/src/resolver.rs new file mode 100644 index 0000000000..6d1d69fc56 --- /dev/null +++ b/vm/devices/net/gdma_test/src/resolver.rs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Resource resolver for [`GdmaTestDeviceHandle`]. + +use async_trait::async_trait; +use futures::StreamExt; +use gdma::GdmaDevice; +use gdma::HwcEqInjector; +use gdma::resolver::Error; +use gdma::resolver::resolve_vports; +use gdma_defs::EqeDataReconfig; +use gdma_defs::EqeVfReset; +use gdma_defs::GDMA_EQE_HWC_RECONFIG_DATA; +use gdma_defs::GDMA_EQE_HWC_RESET_REQUEST; +use gdma_defs::HWC_DATA_TYPE_HW_VPORT_LINK_CONNECT; +use gdma_defs::HWC_DATA_TYPE_HW_VPORT_LINK_DISCONNECT; +use gdma_resources::GdmaTestDeviceHandle; +use gdma_resources::GdmaTestRequest; +use pal_async::task::Spawn; +use pci_resources::ResolvePciDeviceHandleParams; +use pci_resources::ResolvedPciDevice; +use vm_resource::AsyncResolveResource; +use vm_resource::ResourceResolver; +use vm_resource::declare_static_async_resolver; +use vm_resource::kind::PciDeviceHandleKind; +use zerocopy::IntoBytes; + +/// Resource resolver for [`GdmaTestDeviceHandle`]. +/// +/// Creates a standard GDMA device and spawns a detached background task that +/// translates test requests into EQEs injected directly into the HWC EQ via +/// [`gdma::HwcEqInjector`], which wraps the device's shared queue state. +pub struct GdmaTestDeviceResolver; + +declare_static_async_resolver! { + GdmaTestDeviceResolver, + (PciDeviceHandleKind, GdmaTestDeviceHandle), +} + +enum EncodedTestRequest { + VfReset(EqeVfReset), + VportLinkState(EqeDataReconfig), +} + +impl EncodedTestRequest { + fn eqe_type(&self) -> u8 { + match self { + Self::VfReset(_) => GDMA_EQE_HWC_RESET_REQUEST, + Self::VportLinkState(_) => GDMA_EQE_HWC_RECONFIG_DATA, + } + } + + fn data(&self) -> &[u8] { + match self { + Self::VfReset(data) => data.as_bytes(), + Self::VportLinkState(data) => data.as_bytes(), + } + } +} + +fn encode_request(request: GdmaTestRequest) -> EncodedTestRequest { + match request { + GdmaTestRequest::VfReset { revoke_vtl0_vf } => { + EncodedTestRequest::VfReset(EqeVfReset::new().with_revoke_vtl0_vf(revoke_vtl0_vf)) + } + GdmaTestRequest::VportLinkState { vport, connected } => { + let data_type = if connected { + HWC_DATA_TYPE_HW_VPORT_LINK_CONNECT + } else { + HWC_DATA_TYPE_HW_VPORT_LINK_DISCONNECT + }; + let vport = vport.to_le_bytes(); + EncodedTestRequest::VportLinkState(EqeDataReconfig { + data: [vport[0], vport[1], vport[2]], + data_type, + reserved1: [0; 8], + }) + } + } +} + +#[async_trait] +impl AsyncResolveResource for GdmaTestDeviceResolver { + type Output = ResolvedPciDevice; + type Error = Error; + + async fn resolve( + &self, + resolver: &ResourceResolver, + resource: GdmaTestDeviceHandle, + input: ResolvePciDeviceHandleParams<'_>, + ) -> Result { + let vports = resolve_vports(resolver, resource.vports).await?; + + let device = GdmaDevice::new( + input.driver_source, + input.dma_target.guest_memory().clone(), + input.dma_target.msi_target(), + vports, + input.register_mmio, + ); + + let injector: HwcEqInjector = device.hwc_eq_injector(); + let mut request_recv = resource.request_recv; + + input + .driver_source + .simple() + .spawn("gdma-test-control", async move { + while let Some(rpc) = request_recv.next().await { + rpc.handle(async |request| { + let request = encode_request(request); + injector.post(request.eqe_type(), request.data()) + }) + .await; + } + }) + .detach(); + + Ok(device.into()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use test_with_tracing::test; + + #[test] + fn encode_vf_reset() { + for revoke_vtl0_vf in [false, true] { + let request = encode_request(GdmaTestRequest::VfReset { revoke_vtl0_vf }); + assert_eq!(request.eqe_type(), GDMA_EQE_HWC_RESET_REQUEST); + let EncodedTestRequest::VfReset(data) = request else { + panic!("VF reset encoded as the wrong event type"); + }; + assert_eq!(data.revoke_vtl0_vf(), revoke_vtl0_vf); + } + } + + #[test] + fn encode_vport_link_state() { + for (connected, expected_data_type) in [ + (false, HWC_DATA_TYPE_HW_VPORT_LINK_DISCONNECT), + (true, HWC_DATA_TYPE_HW_VPORT_LINK_CONNECT), + ] { + let request = encode_request(GdmaTestRequest::VportLinkState { + vport: 0x00ab_cdef, + connected, + }); + assert_eq!(request.eqe_type(), GDMA_EQE_HWC_RECONFIG_DATA); + let EncodedTestRequest::VportLinkState(data) = request else { + panic!("vport link state encoded as the wrong event type"); + }; + assert_eq!(data.data, [0xef, 0xcd, 0xab]); + assert_eq!(data.data_type, expected_data_type); + assert_eq!(data.reserved1, [0; 8]); + } + } +} diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct.rs index c5593ee428..608f3511c8 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct.rs @@ -3,6 +3,8 @@ //! Integration tests for x86_64 Linux direct boot with OpenHCL. +mod mana_nic; + use crate::x86_64::storage::new_test_vtl2_nvme_device; use guid::Guid; use memory_range::MemoryRange; @@ -23,62 +25,6 @@ use petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_LINUX_DIRECT_TEST_ use vmm_test_macros::openvmm_test; use zerocopy::FromBytes; -/// Today this only tests that the nic can get an IP address via consomme's DHCP -/// implementation. -/// -/// FUTURE: Test traffic on the nic. -async fn validate_mana_nic(agent: &PipetteClient) -> Result<(), anyhow::Error> { - let sh = agent.unix_shell(); - cmd!(sh, "ifconfig eth0 up").run().await?; - cmd!(sh, "udhcpc eth0").run().await?; - let output = cmd!(sh, "ifconfig eth0").read().await?; - // Validate that we see a mana nic with the expected MAC address and IPs. - assert!(output.contains("HWaddr 00:15:5D:12:12:12")); - assert!(output.contains("inet addr:10.0.0.2")); - assert!(output.contains("inet6 addr: fe80::215:5dff:fe12:1212/64")); - - Ok(()) -} - -/// Test an OpenHCL Linux direct VM with a MANA nic assigned to VTL2 (backed by -/// the MANA emulator), and vmbus relay. -#[openvmm_test(openhcl_linux_direct_x64)] -async fn mana_nic(config: PetriVmBuilder) -> Result<(), anyhow::Error> { - let (vm, agent) = config - .with_vmbus_redirect(true) - .modify_backend(|b| b.with_nic()) - .run() - .await?; - - validate_mana_nic(&agent).await?; - - agent.power_off().await?; - vm.wait_for_clean_teardown().await?; - - Ok(()) -} - -/// Test an OpenHCL Linux direct VM with a MANA nic assigned to VTL2 (backed by -/// the MANA emulator), and vmbus relay. Use the shared pool override to test -/// the shared pool dma path. -#[openvmm_test(openhcl_linux_direct_x64)] -async fn mana_nic_shared_pool( - config: PetriVmBuilder, -) -> Result<(), anyhow::Error> { - let (vm, agent) = config - .with_vmbus_redirect(true) - .modify_backend(|b| b.with_nic()) - .run() - .await?; - - validate_mana_nic(&agent).await?; - - agent.power_off().await?; - vm.wait_for_clean_teardown().await?; - - Ok(()) -} - /// Test an OpenHCL Linux direct VM with many NVMe devices assigned to VTL2 and vmbus relay. #[openvmm_test(openhcl_linux_direct_x64 [LATEST_LINUX_DIRECT_TEST_X64])] async fn many_nvme_devices_servicing_very_heavy( diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs new file mode 100644 index 0000000000..a817efcc21 --- /dev/null +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! MANA integration tests for x86_64 Linux direct boot with OpenHCL. + +use petri::PetriVmBuilder; +use petri::openvmm::ManaTestControl; +use petri::openvmm::OpenVmmPetriBackend; +use petri::pipette::PipetteClient; +use petri::pipette::cmd; +use vmm_test_macros::openvmm_test; + +/// Validates that the nic can get an IP address via consomme's DHCP implementation. +/// Validates ICMP by testing that the nic can ping consomme's IP address. +/// +/// FUTURE: TCP / UDP traffic? +async fn validate_mana_nic(agent: &PipetteClient) -> Result<(), anyhow::Error> { + let sh = agent.unix_shell(); + cmd!(sh, "ifconfig eth0 up").run().await?; + cmd!(sh, "udhcpc eth0").run().await?; + let output = cmd!(sh, "ifconfig eth0").read().await?; + // Validate that we see a mana nic with the expected MAC address and IPs. + assert!(output.contains("HWaddr 00:15:5D:12:12:12")); + assert!(output.contains("inet addr:10.0.0.2")); + assert!(output.contains("inet6 addr: fe80::215:5dff:fe12:1212/64")); + cmd!(sh, "ping -c 1 -W 5 -I eth0 10.0.0.1").run().await?; + + Ok(()) +} + +/// Test an OpenHCL Linux direct VM with a MANA nic assigned to VTL2 (backed by +/// the MANA emulator), and vmbus relay. +#[openvmm_test(openhcl_linux_direct_x64)] +async fn mana_nic(config: PetriVmBuilder) -> Result<(), anyhow::Error> { + let (vm, agent) = config + .with_vmbus_redirect(true) + .modify_backend(|b| b.with_nic()) + .run() + .await?; + + validate_mana_nic(&agent).await?; + + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + +/// Test an OpenHCL Linux direct VM with a MANA nic assigned to VTL2 (backed by +/// the MANA emulator), and vmbus relay. Use the shared pool override to test +/// the shared pool dma path. +#[openvmm_test(openhcl_linux_direct_x64)] +async fn mana_nic_shared_pool( + config: PetriVmBuilder, +) -> Result<(), anyhow::Error> { + let (vm, agent) = config + .with_vmbus_redirect(true) + .modify_backend(|b| b.with_nic()) + .run() + .await?; + + validate_mana_nic(&agent).await?; + + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + +async fn mana_nic_vf_reconfig( + config: PetriVmBuilder, + revoke_vtl0_vf: bool, +) -> Result<(), anyhow::Error> { + let (mana, mana_config) = ManaTestControl::new(); + let config = config + .with_vmbus_redirect(true) + .modify_backend(move |b| b.with_nic_test_control(mana_config)); + + let (vm, agent) = config.run().await?; + + validate_mana_nic(&agent).await?; + + let sh = agent.unix_shell(); + + mana.inject_vf_reset(revoke_vtl0_vf).await?; + cmd!(sh, "sleep 5").run().await?; + validate_mana_nic(&agent).await?; + + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + +/// Test VF reconfiguration while retaining the VTL0 VF. +#[openvmm_test(openhcl_linux_direct_x64)] +async fn mana_nic_vf_reconfig_keep_vtl0_vf( + config: PetriVmBuilder, +) -> Result<(), anyhow::Error> { + mana_nic_vf_reconfig(config, false).await +} + +/// Test VF reconfiguration while revoking the VTL0 VF. +#[openvmm_test(openhcl_linux_direct_x64)] +async fn mana_nic_vf_reconfig_revoke_vtl0_vf( + config: PetriVmBuilder, +) -> Result<(), anyhow::Error> { + mana_nic_vf_reconfig(config, true).await +} + +/// Test guest-visible vport link disconnect and reconnect events. +#[openvmm_test(openhcl_linux_direct_x64)] +async fn mana_nic_vport_link_state( + config: PetriVmBuilder, +) -> Result<(), anyhow::Error> { + let (mana, mana_config) = ManaTestControl::new(); + let config = config + .with_vmbus_redirect(true) + .modify_backend(move |b| b.with_nic_test_control(mana_config)); + + let (vm, agent) = config.run().await?; + validate_mana_nic(&agent).await?; + + let sh = agent.unix_shell(); + mana.set_vport_link_state(0, false).await?; + cmd!( + sh, + "timeout 30 sh -c 'until [ \"$(cat /sys/class/net/eth0/carrier)\" = 0 ]; do sleep 1; done'" + ) + .run() + .await?; + + mana.set_vport_link_state(0, true).await?; + cmd!( + sh, + "timeout 30 sh -c 'until [ \"$(cat /sys/class/net/eth0/carrier)\" = 1 ]; do sleep 1; done'" + ) + .run() + .await?; + validate_mana_nic(&agent).await?; + + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} From 711bdd7b84548779faf225d04208fba1b3c786d3 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 21 Aug 2026 11:49:47 -0700 Subject: [PATCH 02/18] move eqe injector behind test_helpers --- vm/devices/net/gdma/Cargo.toml | 3 +++ vm/devices/net/gdma/src/lib.rs | 33 +++++++----------------- vm/devices/net/gdma_test/Cargo.toml | 2 +- vm/devices/net/gdma_test/src/resolver.rs | 8 +++--- 4 files changed, 18 insertions(+), 28 deletions(-) diff --git a/vm/devices/net/gdma/Cargo.toml b/vm/devices/net/gdma/Cargo.toml index a2ca8dc3bc..f9786892a8 100644 --- a/vm/devices/net/gdma/Cargo.toml +++ b/vm/devices/net/gdma/Cargo.toml @@ -6,6 +6,9 @@ name = "gdma" edition.workspace = true rust-version.workspace = true +[features] +test_helpers = [] + [dependencies] gdma_defs.workspace = true gdma_resources.workspace = true diff --git a/vm/devices/net/gdma/src/lib.rs b/vm/devices/net/gdma/src/lib.rs index 85186220d8..739ed6085d 100644 --- a/vm/devices/net/gdma/src/lib.rs +++ b/vm/devices/net/gdma/src/lib.rs @@ -121,21 +121,16 @@ enum SmcError { pub use bnic::BnicConfig; -/// A cloneable handle for injecting EQEs into the hardware channel EQ. -/// -/// Obtained via [`GdmaDevice::hwc_eq_injector`] before handing the device to -/// the VM machinery. -#[derive(Clone)] -pub struct HwcEqInjector { - queues: Arc, -} - -impl HwcEqInjector { - /// Posts an EQE directly to the hardware channel EQ, bypassing the guest - /// driver command path. Intended for out-of-band test EQE injection in - /// VMM tests (`gdma_test`). - pub fn post(&self, ty: u8, data: &[u8]) { - self.queues.post_eq(queues::ID_OFFSET as u32, ty, data) +/// Helpers for cross-crate testing. +#[cfg(feature = "test_helpers")] +pub mod test_helpers { + use super::GdmaDevice; + use super::queues; + + /// Returns a function that injects EQEs into the hardware channel EQ. + pub fn hwc_eq_injector(device: &GdmaDevice) -> impl Fn(u8, &[u8]) + Send + Sync + 'static { + let queues = device.queues.clone(); + move |ty, data| queues.post_eq(queues::ID_OFFSET as u32, ty, data) } } @@ -145,14 +140,6 @@ pub struct VportConfig { } impl GdmaDevice { - /// Returns a cloneable handle for injecting EQEs into the hardware channel - /// EQ after this device has been handed to the VM machinery. - pub fn hwc_eq_injector(&self) -> HwcEqInjector { - HwcEqInjector { - queues: self.queues.clone(), - } - } - pub fn new( driver_source: &VmTaskDriverSource, gm: GuestMemory, diff --git a/vm/devices/net/gdma_test/Cargo.toml b/vm/devices/net/gdma_test/Cargo.toml index 6323d2a0ae..2e6839eda5 100644 --- a/vm/devices/net/gdma_test/Cargo.toml +++ b/vm/devices/net/gdma_test/Cargo.toml @@ -7,7 +7,7 @@ edition.workspace = true rust-version.workspace = true [dependencies] -gdma.workspace = true +gdma = { workspace = true, features = ["test_helpers"] } gdma_defs.workspace = true gdma_resources.workspace = true pci_resources.workspace = true diff --git a/vm/devices/net/gdma_test/src/resolver.rs b/vm/devices/net/gdma_test/src/resolver.rs index 6d1d69fc56..e369c3b896 100644 --- a/vm/devices/net/gdma_test/src/resolver.rs +++ b/vm/devices/net/gdma_test/src/resolver.rs @@ -6,9 +6,9 @@ use async_trait::async_trait; use futures::StreamExt; use gdma::GdmaDevice; -use gdma::HwcEqInjector; use gdma::resolver::Error; use gdma::resolver::resolve_vports; +use gdma::test_helpers::hwc_eq_injector; use gdma_defs::EqeDataReconfig; use gdma_defs::EqeVfReset; use gdma_defs::GDMA_EQE_HWC_RECONFIG_DATA; @@ -30,7 +30,7 @@ use zerocopy::IntoBytes; /// /// Creates a standard GDMA device and spawns a detached background task that /// translates test requests into EQEs injected directly into the HWC EQ via -/// [`gdma::HwcEqInjector`], which wraps the device's shared queue state. +/// [`gdma::test_helpers::hwc_eq_injector`]. pub struct GdmaTestDeviceResolver; declare_static_async_resolver! { @@ -101,7 +101,7 @@ impl AsyncResolveResource for GdmaTes input.register_mmio, ); - let injector: HwcEqInjector = device.hwc_eq_injector(); + let inject_eqe = hwc_eq_injector(&device); let mut request_recv = resource.request_recv; input @@ -111,7 +111,7 @@ impl AsyncResolveResource for GdmaTes while let Some(rpc) = request_recv.next().await { rpc.handle(async |request| { let request = encode_request(request); - injector.post(request.eqe_type(), request.data()) + inject_eqe(request.eqe_type(), request.data()) }) .await; } From 6efce8d0363579ae517f493bbc98e7477c6cd94e Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 21 Aug 2026 11:50:31 -0700 Subject: [PATCH 03/18] move resolve_vprots behind test_helpers --- vm/devices/net/gdma/src/lib.rs | 11 +++++++++++ vm/devices/net/gdma/src/resolver.rs | 6 +----- vm/devices/net/gdma_test/src/resolver.rs | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/vm/devices/net/gdma/src/lib.rs b/vm/devices/net/gdma/src/lib.rs index 739ed6085d..883f7f3b9e 100644 --- a/vm/devices/net/gdma/src/lib.rs +++ b/vm/devices/net/gdma/src/lib.rs @@ -125,7 +125,18 @@ pub use bnic::BnicConfig; #[cfg(feature = "test_helpers")] pub mod test_helpers { use super::GdmaDevice; + use super::VportConfig; use super::queues; + use gdma_resources::VportDefinition; + use vm_resource::ResourceResolver; + + /// Resolves vport definitions for a test GDMA device. + pub async fn resolve_vports( + resolver: &ResourceResolver, + vports: Vec, + ) -> Result, super::resolver::Error> { + super::resolver::resolve_vports(resolver, vports).await + } /// Returns a function that injects EQEs into the hardware channel EQ. pub fn hwc_eq_injector(device: &GdmaDevice) -> impl Fn(u8, &[u8]) + Send + Sync + 'static { diff --git a/vm/devices/net/gdma/src/resolver.rs b/vm/devices/net/gdma/src/resolver.rs index 825919e872..113b508cc7 100644 --- a/vm/devices/net/gdma/src/resolver.rs +++ b/vm/devices/net/gdma/src/resolver.rs @@ -34,11 +34,7 @@ pub enum Error { VportResolve(#[source] ResolveError), } -/// Resolves a list of [`gdma_resources::VportDefinition`]s into -/// [`VportConfig`]s. -/// -/// Shared by [`GdmaDeviceResolver`] and `gdma_test`'s test resolver. -pub async fn resolve_vports( +pub(crate) async fn resolve_vports( resolver: &ResourceResolver, vports: Vec, ) -> Result, Error> { diff --git a/vm/devices/net/gdma_test/src/resolver.rs b/vm/devices/net/gdma_test/src/resolver.rs index e369c3b896..7e5b44ab93 100644 --- a/vm/devices/net/gdma_test/src/resolver.rs +++ b/vm/devices/net/gdma_test/src/resolver.rs @@ -7,8 +7,8 @@ use async_trait::async_trait; use futures::StreamExt; use gdma::GdmaDevice; use gdma::resolver::Error; -use gdma::resolver::resolve_vports; use gdma::test_helpers::hwc_eq_injector; +use gdma::test_helpers::resolve_vports; use gdma_defs::EqeDataReconfig; use gdma_defs::EqeVfReset; use gdma_defs::GDMA_EQE_HWC_RECONFIG_DATA; From 2594370c85f46fc896e64c959a25cc6cd2f2535a Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 21 Aug 2026 13:54:37 -0700 Subject: [PATCH 04/18] make ipv6 check depend on VF --- .../x86_64/openhcl_linux_direct/mana_nic.rs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs index a817efcc21..2be5bdb431 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs @@ -14,7 +14,10 @@ use vmm_test_macros::openvmm_test; /// Validates ICMP by testing that the nic can ping consomme's IP address. /// /// FUTURE: TCP / UDP traffic? -async fn validate_mana_nic(agent: &PipetteClient) -> Result<(), anyhow::Error> { +async fn validate_mana_nic( + agent: &PipetteClient, + eth0_is_mana_vf: bool, +) -> Result<(), anyhow::Error> { let sh = agent.unix_shell(); cmd!(sh, "ifconfig eth0 up").run().await?; cmd!(sh, "udhcpc eth0").run().await?; @@ -22,7 +25,11 @@ async fn validate_mana_nic(agent: &PipetteClient) -> Result<(), anyhow::Error> { // Validate that we see a mana nic with the expected MAC address and IPs. assert!(output.contains("HWaddr 00:15:5D:12:12:12")); assert!(output.contains("inet addr:10.0.0.2")); - assert!(output.contains("inet6 addr: fe80::215:5dff:fe12:1212/64")); + if eth0_is_mana_vf { + cmd!(sh, "ifconfig eth1").ignore_status().run().await?; + } else { + assert!(output.contains("inet6 addr: fe80::215:5dff:fe12:1212/64")); + } cmd!(sh, "ping -c 1 -W 5 -I eth0 10.0.0.1").run().await?; Ok(()) @@ -38,7 +45,7 @@ async fn mana_nic(config: PetriVmBuilder) -> Result<(), any .run() .await?; - validate_mana_nic(&agent).await?; + validate_mana_nic(&agent, false).await?; agent.power_off().await?; vm.wait_for_clean_teardown().await?; @@ -59,7 +66,7 @@ async fn mana_nic_shared_pool( .run() .await?; - validate_mana_nic(&agent).await?; + validate_mana_nic(&agent, false).await?; agent.power_off().await?; vm.wait_for_clean_teardown().await?; @@ -78,13 +85,13 @@ async fn mana_nic_vf_reconfig( let (vm, agent) = config.run().await?; - validate_mana_nic(&agent).await?; + validate_mana_nic(&agent, true).await?; let sh = agent.unix_shell(); mana.inject_vf_reset(revoke_vtl0_vf).await?; cmd!(sh, "sleep 5").run().await?; - validate_mana_nic(&agent).await?; + validate_mana_nic(&agent, true).await?; agent.power_off().await?; vm.wait_for_clean_teardown().await?; @@ -119,7 +126,7 @@ async fn mana_nic_vport_link_state( .modify_backend(move |b| b.with_nic_test_control(mana_config)); let (vm, agent) = config.run().await?; - validate_mana_nic(&agent).await?; + validate_mana_nic(&agent, true).await?; let sh = agent.unix_shell(); mana.set_vport_link_state(0, false).await?; @@ -137,7 +144,7 @@ async fn mana_nic_vport_link_state( ) .run() .await?; - validate_mana_nic(&agent).await?; + validate_mana_nic(&agent, true).await?; agent.power_off().await?; vm.wait_for_clean_teardown().await?; From 82ff4e48e41b99139a5e3c89baa5764a0487deb5 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 21 Aug 2026 13:55:26 -0700 Subject: [PATCH 05/18] diagnostics --- petri/src/vm/mod.rs | 15 ++++++++ petri/src/vm/openvmm/construct.rs | 9 ++++- .../x86_64/openhcl_linux_direct/mana_nic.rs | 35 +++++++++++++++++-- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/petri/src/vm/mod.rs b/petri/src/vm/mod.rs index 8c09ccfa8d..8fb4c6405b 100644 --- a/petri/src/vm/mod.rs +++ b/petri/src/vm/mod.rs @@ -1405,6 +1405,18 @@ impl PetriVmBuilder { self } + /// Append additional command line arguments to the VTL0 Linux kernel. + pub fn with_vtl0_kernel_command_line(mut self, additional_command_line: &str) -> Self { + let Firmware::OpenhclLinuxDirect { openhcl_config, .. } = &mut self.config.firmware else { + panic!("VTL0 kernel command line is only supported for OpenHCL Linux direct firmware.") + }; + append_cmdline( + &mut openhcl_config.vtl0_kernel_command_line, + additional_command_line, + ); + self + } + /// Enable confidential filtering, even if the VM is not confidential. pub fn with_confidential_filtering(self) -> Self { if !self.config.firmware.is_openhcl() { @@ -2507,6 +2519,8 @@ pub struct OpenHclConfig { /// command line and pass to OpenHCL. VM backends should use /// [`OpenHclConfig::command_line()`] rather than reading this directly. pub custom_command_line: Option, + /// Test-specified command-line parameters to append to the VTL0 Linux kernel command line. + pub vtl0_kernel_command_line: Option, /// Command line parameters that control OpenHCL logging behavior. Separate /// from `command_line` so that petri can decide to use default log /// levels. @@ -2566,6 +2580,7 @@ impl Default for OpenHclConfig { Self { vmbus_redirect: false, custom_command_line: None, + vtl0_kernel_command_line: None, log_levels: OpenvmmLogConfig::TestDefault, vtl2_base_address_type: None, vtl2_settings: None, diff --git a/petri/src/vm/openvmm/construct.rs b/petri/src/vm/openvmm/construct.rs index e6c456d815..4bb7beb806 100644 --- a/petri/src/vm/openvmm/construct.rs +++ b/petri/src/vm/openvmm/construct.rs @@ -991,6 +991,7 @@ impl PetriVmConfigSetupCore<'_> { let OpenHclConfig { vmbus_redirect: _, // config_openhcl_vmbus_devices custom_command_line: _, + vtl0_kernel_command_line, log_levels: _, vtl2_base_address_type, vtl2_settings: _, // run_core @@ -1005,10 +1006,16 @@ impl PetriVmConfigSetupCore<'_> { // Set UNDERHILL_SERIAL_WAIT_FOR_RTS=1 so that we don't pull serial data // until the guest is ready. Otherwise, Linux will drop the input serial // data on the floor during boot. + let mut vtl0_cmdline = format!("rdinit=/bin/sh {vsock_blacklist}"); + if let Some(additional_command_line) = vtl0_kernel_command_line { + vtl0_cmdline.push(' '); + vtl0_cmdline.push_str(additional_command_line); + } append_cmdline( &mut cmdline, format!( - "UNDERHILL_SERIAL_WAIT_FOR_RTS=1 UNDERHILL_CMDLINE_APPEND=\"rdinit=/bin/sh {vsock_blacklist}\"" + "UNDERHILL_SERIAL_WAIT_FOR_RTS=1 UNDERHILL_CMDLINE_APPEND=\"{}\"", + vtl0_cmdline ), ); false diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs index 2be5bdb431..c524fe9285 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs @@ -3,6 +3,7 @@ //! MANA integration tests for x86_64 Linux direct boot with OpenHCL. +use petri::OpenvmmLogConfig; use petri::PetriVmBuilder; use petri::openvmm::ManaTestControl; use petri::openvmm::OpenVmmPetriBackend; @@ -10,6 +11,36 @@ use petri::pipette::PipetteClient; use petri::pipette::cmd; use vmm_test_macros::openvmm_test; +fn configure_mana_vf_diagnostics( + config: PetriVmBuilder, +) -> PetriVmBuilder { + config + .with_vtl0_kernel_command_line( + "rcupdate.rcu_cpu_stall_timeout=10 rcupdate.rcu_cpu_stall_cputime=1 mana.dyndbg=+p hv_netvsc.dyndbg=+p pci_hyperv.dyndbg=+p", + ) + .with_host_log_levels(OpenvmmLogConfig::Custom( + [ + ( + "OPENVMM_LOG".to_owned(), + "debug,gdma=trace,vpci=trace,hv1_emulator::message_queues=trace".to_owned(), + ), + ("OPENVMM_SHOW_SPANS".to_owned(), "true".to_owned()), + ] + .into(), + )) + .with_openhcl_log_levels(OpenvmmLogConfig::Custom( + [ + ( + "OPENVMM_LOG".to_owned(), + "debug,underhill_core::emuplat::netvsp=trace,netvsp=trace,mana_driver=trace" + .to_owned(), + ), + ("OPENVMM_SHOW_SPANS".to_owned(), "true".to_owned()), + ] + .into(), + )) +} + /// Validates that the nic can get an IP address via consomme's DHCP implementation. /// Validates ICMP by testing that the nic can ping consomme's IP address. /// @@ -79,7 +110,7 @@ async fn mana_nic_vf_reconfig( revoke_vtl0_vf: bool, ) -> Result<(), anyhow::Error> { let (mana, mana_config) = ManaTestControl::new(); - let config = config + let config = configure_mana_vf_diagnostics(config) .with_vmbus_redirect(true) .modify_backend(move |b| b.with_nic_test_control(mana_config)); @@ -121,7 +152,7 @@ async fn mana_nic_vport_link_state( config: PetriVmBuilder, ) -> Result<(), anyhow::Error> { let (mana, mana_config) = ManaTestControl::new(); - let config = config + let config = configure_mana_vf_diagnostics(config) .with_vmbus_redirect(true) .modify_backend(move |b| b.with_nic_test_control(mana_config)); From 7bf243f352d26c594324eeadd41806be92f8f490 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 21 Aug 2026 14:04:05 -0700 Subject: [PATCH 06/18] pr feedback --- petri/src/vm/openvmm/modify.rs | 2 ++ vm/devices/net/gdma_test/src/resolver.rs | 4 ++++ .../tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs | 3 +-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/petri/src/vm/openvmm/modify.rs b/petri/src/vm/openvmm/modify.rs index e68785dbed..3e58084079 100644 --- a/petri/src/vm/openvmm/modify.rs +++ b/petri/src/vm/openvmm/modify.rs @@ -210,6 +210,8 @@ impl PetriVmConfigOpenVmm { max_sub_channels: None, }, ); + } else { + panic!("with_nic_test_control requires VTL2 settings"); } self diff --git a/vm/devices/net/gdma_test/src/resolver.rs b/vm/devices/net/gdma_test/src/resolver.rs index 7e5b44ab93..307c058869 100644 --- a/vm/devices/net/gdma_test/src/resolver.rs +++ b/vm/devices/net/gdma_test/src/resolver.rs @@ -70,6 +70,10 @@ fn encode_request(request: GdmaTestRequest) -> EncodedTestRequest { } else { HWC_DATA_TYPE_HW_VPORT_LINK_DISCONNECT }; + assert!( + vport <= 0x00ff_ffff, + "vport index does not fit in the 24-bit EQE field" + ); let vport = vport.to_le_bytes(); EncodedTestRequest::VportLinkState(EqeDataReconfig { data: [vport[0], vport[1], vport[2]], diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs index c524fe9285..cbb2c6b2a8 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs @@ -85,8 +85,7 @@ async fn mana_nic(config: PetriVmBuilder) -> Result<(), any } /// Test an OpenHCL Linux direct VM with a MANA nic assigned to VTL2 (backed by -/// the MANA emulator), and vmbus relay. Use the shared pool override to test -/// the shared pool dma path. +/// the MANA emulator), and vmbus relay. #[openvmm_test(openhcl_linux_direct_x64)] async fn mana_nic_shared_pool( config: PetriVmBuilder, From 997ef9547a55ca6514bab5aa0c1fd7370ff26358 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 21 Aug 2026 16:02:52 -0700 Subject: [PATCH 07/18] less test logging --- .../tests/x86_64/openhcl_linux_direct/mana_nic.rs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs index cbb2c6b2a8..8501b057b4 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs @@ -16,24 +16,13 @@ fn configure_mana_vf_diagnostics( ) -> PetriVmBuilder { config .with_vtl0_kernel_command_line( - "rcupdate.rcu_cpu_stall_timeout=10 rcupdate.rcu_cpu_stall_cputime=1 mana.dyndbg=+p hv_netvsc.dyndbg=+p pci_hyperv.dyndbg=+p", + "rcupdate.rcu_cpu_stall_timeout=10 rcupdate.rcu_cpu_stall_cputime=1", ) - .with_host_log_levels(OpenvmmLogConfig::Custom( - [ - ( - "OPENVMM_LOG".to_owned(), - "debug,gdma=trace,vpci=trace,hv1_emulator::message_queues=trace".to_owned(), - ), - ("OPENVMM_SHOW_SPANS".to_owned(), "true".to_owned()), - ] - .into(), - )) .with_openhcl_log_levels(OpenvmmLogConfig::Custom( [ ( "OPENVMM_LOG".to_owned(), - "debug,underhill_core::emuplat::netvsp=trace,netvsp=trace,mana_driver=trace" - .to_owned(), + "debug,underhill_core::emuplat::netvsp=trace".to_owned(), ), ("OPENVMM_SHOW_SPANS".to_owned(), "true".to_owned()), ] From 79f3497666e137c906c825c456dba1b8fd3b8cc1 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Wed, 26 Aug 2026 12:52:34 -0700 Subject: [PATCH 08/18] test device handles VPCI OFFER, bnic handles FILTER, test device offers shutdown --- .../src/pipelines/checkin_gates.rs | 19 +++- .../local_build_and_run_nextest_vmm_tests.rs | 2 +- flowey/flowey_lib_hvlite/src/build_openvmm.rs | 2 + openvmm/openvmm/Cargo.toml | 1 + openvmm/openvmm_resources/Cargo.toml | 3 + petri/src/vm/openvmm/construct.rs | 6 ++ petri/src/vm/openvmm/modify.rs | 7 ++ .../get/guest_emulation_device/src/lib.rs | 2 + .../src/test_utilities.rs | 18 ++++ vm/devices/net/gdma/src/bnic.rs | 23 +++- vm/devices/net/gdma/src/lib.rs | 9 +- vm/devices/net/gdma_resources/src/lib.rs | 2 + vm/devices/net/gdma_test/src/resolver.rs | 20 +++- .../x86_64/openhcl_linux_direct/mana_nic.rs | 100 ++++++++++++------ 14 files changed, 166 insertions(+), 48 deletions(-) diff --git a/flowey/flowey_hvlite/src/pipelines/checkin_gates.rs b/flowey/flowey_hvlite/src/pipelines/checkin_gates.rs index e7ce2e1167..712b7c608d 100644 --- a/flowey/flowey_hvlite/src/pipelines/checkin_gates.rs +++ b/flowey/flowey_hvlite/src/pipelines/checkin_gates.rs @@ -657,7 +657,10 @@ impl IntoPipeline for CheckinGatesCli { }, profile: CommonProfile::from_release(release), // FIXME: this relies on openvmm default features - features: [].into(), + features: [ + flowey_lib_hvlite::build_openvmm::OpenvmmFeature::TestUtilities, + ] + .into(), }, openvmm, } @@ -827,8 +830,11 @@ impl IntoPipeline for CheckinGatesCli { }, profile: CommonProfile::from_release(release), // FIXME: this relies on openvmm default features - features: [flowey_lib_hvlite::build_openvmm::OpenvmmFeature::Tpm] - .into(), + features: [ + flowey_lib_hvlite::build_openvmm::OpenvmmFeature::TestUtilities, + flowey_lib_hvlite::build_openvmm::OpenvmmFeature::Tpm, + ] + .into(), }, openvmm, } @@ -904,8 +910,11 @@ impl IntoPipeline for CheckinGatesCli { platform: CommonPlatform::LinuxMusl, }, profile: CommonProfile::from_release(release), - features: [flowey_lib_hvlite::build_openvmm::OpenvmmFeature::Tpm] - .into(), + features: [ + flowey_lib_hvlite::build_openvmm::OpenvmmFeature::TestUtilities, + flowey_lib_hvlite::build_openvmm::OpenvmmFeature::Tpm, + ] + .into(), }, openvmm, } 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 ec4ed5dce2..7b141e1fbb 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 @@ -283,7 +283,7 @@ impl SimpleFlowNode for Node { target: target.clone(), profile: CommonProfile::from_release(release), // FIXME: this relies on openvmm default features - features: [].into(), + features: [crate::build_openvmm::OpenvmmFeature::TestUtilities].into(), }, openvmm: v, }); diff --git a/flowey/flowey_lib_hvlite/src/build_openvmm.rs b/flowey/flowey_lib_hvlite/src/build_openvmm.rs index d14adc23c9..045d7ce630 100644 --- a/flowey/flowey_lib_hvlite/src/build_openvmm.rs +++ b/flowey/flowey_lib_hvlite/src/build_openvmm.rs @@ -12,6 +12,7 @@ use std::collections::BTreeSet; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum OpenvmmFeature { Gdb, + TestUtilities, Tpm, } @@ -96,6 +97,7 @@ impl FlowNode for Node { .map(|f| { match f { OpenvmmFeature::Gdb => "gdb", + OpenvmmFeature::TestUtilities => "test_utilities", OpenvmmFeature::Tpm => "tpm", } .into() diff --git a/openvmm/openvmm/Cargo.toml b/openvmm/openvmm/Cargo.toml index 7eb546ee81..94a1ffd002 100644 --- a/openvmm/openvmm/Cargo.toml +++ b/openvmm/openvmm/Cargo.toml @@ -25,6 +25,7 @@ default = [ gdb = ["openvmm_resources/gdb"] vendored_crypto = ["crypto/vendored"] tpm = ["openvmm_resources/tpm"] +test_utilities = ["openvmm_resources/test_utilities"] virt_hvf = ["openvmm_resources/virt_hvf", "openvmm_hypervisors/virt_hvf"] virt_kvm = ["openvmm_resources/virt_kvm", "openvmm_hypervisors/virt_kvm"] virt_mshv = ["openvmm_resources/virt_mshv", "openvmm_hypervisors/virt_mshv"] diff --git a/openvmm/openvmm_resources/Cargo.toml b/openvmm/openvmm_resources/Cargo.toml index 27804fcd68..1a469a552d 100644 --- a/openvmm/openvmm_resources/Cargo.toml +++ b/openvmm/openvmm_resources/Cargo.toml @@ -25,6 +25,9 @@ virt_whp = ["openvmm_core/virt_whp", "dep:virt_whp"] # Enable building with macOS hypervisor framework support. virt_hvf = ["dep:virt_hvf"] +# Enable utilities used by VMM tests. +test_utilities = ["guest_emulation_device/test_utilities"] + [dependencies] hypervisor_resources.workspace = true mesh_worker.workspace = true diff --git a/petri/src/vm/openvmm/construct.rs b/petri/src/vm/openvmm/construct.rs index 4bb7beb806..8753904dfe 100644 --- a/petri/src/vm/openvmm/construct.rs +++ b/petri/src/vm/openvmm/construct.rs @@ -1008,6 +1008,12 @@ impl PetriVmConfigSetupCore<'_> { // data on the floor during boot. let mut vtl0_cmdline = format!("rdinit=/bin/sh {vsock_blacklist}"); if let Some(additional_command_line) = vtl0_kernel_command_line { + anyhow::ensure!( + !additional_command_line + .chars() + .any(|c| matches!(c, '\'' | '"')), + "VTL0 kernel command line cannot contain quotes" + ); vtl0_cmdline.push(' '); vtl0_cmdline.push_str(additional_command_line); } diff --git a/petri/src/vm/openvmm/modify.rs b/petri/src/vm/openvmm/modify.rs index 3e58084079..0c5a9f60a8 100644 --- a/petri/src/vm/openvmm/modify.rs +++ b/petri/src/vm/openvmm/modify.rs @@ -696,4 +696,11 @@ impl ManaTestControl { ) .await } + + /// Shuts down test control after all preceding requests have completed. + pub async fn shutdown(self) -> Result<(), mesh::rpc::RpcError> { + self.request_send + .call(|rpc| rpc, GdmaTestRequest::Shutdown) + .await + } } diff --git a/vm/devices/get/guest_emulation_device/src/lib.rs b/vm/devices/get/guest_emulation_device/src/lib.rs index 339e4d27c7..ad1d8a1bc2 100644 --- a/vm/devices/get/guest_emulation_device/src/lib.rs +++ b/vm/devices/get/guest_emulation_device/src/lib.rs @@ -645,6 +645,8 @@ impl GedChannel { HostRequests::CREATE_RAM_GPA_RANGE => self.handle_create_ram_gpa_range(message_buf)?, HostRequests::RESET_RAM_GPA_RANGE => self.handle_reset_ram_gpa_range(message_buf)?, HostRequests::LOAD_FIRMWARE => self.handle_load_firmware(message_buf)?, + #[cfg(feature = "test_utilities")] + HostRequests::VPCI_DEVICE_CONTROL => self.handle_vpci_device_control(message_buf)?, _ => { tracing::error!(message_id = ?header.message_id(), "unexpected message"); return Err(Error::InvalidSequence); diff --git a/vm/devices/get/guest_emulation_device/src/test_utilities.rs b/vm/devices/get/guest_emulation_device/src/test_utilities.rs index 60d98025c3..6a026e1dc2 100644 --- a/vm/devices/get/guest_emulation_device/src/test_utilities.rs +++ b/vm/devices/get/guest_emulation_device/src/test_utilities.rs @@ -227,6 +227,24 @@ impl TestGedChannel { } } +impl GedChannel { + pub(super) fn handle_vpci_device_control(&mut self, message_buf: &[u8]) -> Result<(), Error> { + let request: get_protocol::VpciDeviceControlRequest = + get_protocol::VpciDeviceControlRequest::read_from_prefix(message_buf) + .map_err(|_| Error::MessageTooSmall)? + .0; + let status = if request.code == get_protocol::VpciDeviceControlCode::OFFER { + get_protocol::VpciDeviceControlStatus::SUCCESS + } else { + get_protocol::VpciDeviceControlStatus::INVALID_REQUEST + }; + let response = get_protocol::VpciDeviceControlResponse::new(status); + self.channel + .try_send(response.as_bytes()) + .map_err(Error::Vmbus) + } +} + /// Create the host Guest Emulation Device and corresponding I/O. /// /// If `ged_responses` is Some(), then TestGedChannel will be used to diff --git a/vm/devices/net/gdma/src/bnic.rs b/vm/devices/net/gdma/src/bnic.rs index 9d6c66fd11..6224cdbfee 100644 --- a/vm/devices/net/gdma/src/bnic.rs +++ b/vm/devices/net/gdma/src/bnic.rs @@ -35,6 +35,7 @@ use gdma_defs::Wqe; use gdma_defs::access::WqeAccess; use gdma_defs::bnic as bnic_defs; use gdma_defs::bnic::ManaDestroyWqobjReq; +use gdma_defs::bnic::ManaMoveFilterVTL2PrivilegedReq; use gdma_defs::bnic::ManaTxShortOob; use gdma_defs::bnic::Tristate; use guestmem::GuestMemory; @@ -190,6 +191,7 @@ struct Vport { task: TaskControl, queue_cfg: QueueCfg, serial_no: u32, + direction_to_vtl0: bool, } impl InspectMut for Vport { @@ -201,6 +203,7 @@ impl InspectMut for Vport { .field("tx_cq", self.queue_cfg.tx.map(|(_wq, cq)| cq)) .field("rx_wq", self.queue_cfg.rx.map(|(wq, _cq)| wq)) .field("rx_cq", self.queue_cfg.rx.map(|(_wq, cq)| cq)) + .field("direction_to_vtl0", self.direction_to_vtl0) .merge(&mut self.task); } } @@ -228,6 +231,7 @@ impl BasicNic { task: TaskControl::new(TxRxState), queue_cfg: QueueCfg { tx: None, rx: None }, serial_no: 0, + direction_to_vtl0: false, } }, ) @@ -424,19 +428,30 @@ impl BasicNic { } } ManaCommandCode::MANA_VTL2_MOVE_FILTER => { - anyhow::bail!("unsupported command MANA_VTL2_MOVE_FILTER"); + let req: ManaMoveFilterVTL2PrivilegedReq = read + .read_plain() + .context("reading move vport filter request")?; + let vport = self + .vports + .get_mut(req.vport as usize) + .context("invalid vport")?; + vport.direction_to_vtl0 = match req.direction_to_vtl0 { + 0 => false, + 1 => true, + value => anyhow::bail!("invalid filter direction {value}"), + }; } ManaCommandCode::MANA_VTL2_QUERY_FILTER_STATE => { let req: gdma_defs::bnic::ManaQueryFilterStateReq = read .read_plain() .context("reading query vport filter state request")?; - let _ = self + let vport = self .vports - .get_mut(req.vport as usize) + .get(req.vport as usize) .context("invalid vport")?; let resp = gdma_defs::bnic::ManaQueryFilterStateResponse { - direction_to_vtl0: 0, + direction_to_vtl0: vport.direction_to_vtl0.into(), reserved: [0; 7], }; diff --git a/vm/devices/net/gdma/src/lib.rs b/vm/devices/net/gdma/src/lib.rs index 883f7f3b9e..7b3611ad3d 100644 --- a/vm/devices/net/gdma/src/lib.rs +++ b/vm/devices/net/gdma/src/lib.rs @@ -128,6 +128,7 @@ pub mod test_helpers { use super::VportConfig; use super::queues; use gdma_resources::VportDefinition; + use std::sync::Arc; use vm_resource::ResourceResolver; /// Resolves vport definitions for a test GDMA device. @@ -140,8 +141,12 @@ pub mod test_helpers { /// Returns a function that injects EQEs into the hardware channel EQ. pub fn hwc_eq_injector(device: &GdmaDevice) -> impl Fn(u8, &[u8]) + Send + Sync + 'static { - let queues = device.queues.clone(); - move |ty, data| queues.post_eq(queues::ID_OFFSET as u32, ty, data) + let queues = Arc::downgrade(&device.queues); + move |ty, data| { + if let Some(queues) = queues.upgrade() { + queues.post_eq(queues::ID_OFFSET as u32, ty, data) + } + } } } diff --git a/vm/devices/net/gdma_resources/src/lib.rs b/vm/devices/net/gdma_resources/src/lib.rs index c963c52e40..df4ec35907 100644 --- a/vm/devices/net/gdma_resources/src/lib.rs +++ b/vm/devices/net/gdma_resources/src/lib.rs @@ -41,6 +41,8 @@ impl ResourceId for GdmaTestDeviceHandle { /// A test request for an emulated GDMA device. #[derive(MeshPayload)] pub enum GdmaTestRequest { + /// Shut down the test-control channel. + Shutdown, /// Request that the VF be reconfigured. VfReset { /// Whether OpenHCL should revoke the VTL0 VF during reset. diff --git a/vm/devices/net/gdma_test/src/resolver.rs b/vm/devices/net/gdma_test/src/resolver.rs index 307c058869..5c0d37c437 100644 --- a/vm/devices/net/gdma_test/src/resolver.rs +++ b/vm/devices/net/gdma_test/src/resolver.rs @@ -28,9 +28,10 @@ use zerocopy::IntoBytes; /// Resource resolver for [`GdmaTestDeviceHandle`]. /// -/// Creates a standard GDMA device and spawns a detached background task that -/// translates test requests into EQEs injected directly into the HWC EQ via -/// [`gdma::test_helpers::hwc_eq_injector`]. +/// Creates a standard GDMA device and spawns a background task that translates +/// test requests into EQEs injected directly into the HWC EQ via +/// [`gdma::test_helpers::hwc_eq_injector`]. The task exits when test control is +/// shut down. pub struct GdmaTestDeviceResolver; declare_static_async_resolver! { @@ -61,6 +62,7 @@ impl EncodedTestRequest { fn encode_request(request: GdmaTestRequest) -> EncodedTestRequest { match request { + GdmaTestRequest::Shutdown => unreachable!("shutdown requests are handled by the loop"), GdmaTestRequest::VfReset { revoke_vtl0_vf } => { EncodedTestRequest::VfReset(EqeVfReset::new().with_revoke_vtl0_vf(revoke_vtl0_vf)) } @@ -113,11 +115,19 @@ impl AsyncResolveResource for GdmaTes .simple() .spawn("gdma-test-control", async move { while let Some(rpc) = request_recv.next().await { + let mut shutdown = false; rpc.handle(async |request| { - let request = encode_request(request); - inject_eqe(request.eqe_type(), request.data()) + if matches!(request, GdmaTestRequest::Shutdown) { + shutdown = true; + } else { + let request = encode_request(request); + inject_eqe(request.eqe_type(), request.data()) + } }) .await; + if shutdown { + break; + } } }) .detach(); diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs index 8501b057b4..f59b4d789d 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs @@ -3,7 +3,6 @@ //! MANA integration tests for x86_64 Linux direct boot with OpenHCL. -use petri::OpenvmmLogConfig; use petri::PetriVmBuilder; use petri::openvmm::ManaTestControl; use petri::openvmm::OpenVmmPetriBackend; @@ -11,46 +10,35 @@ use petri::pipette::PipetteClient; use petri::pipette::cmd; use vmm_test_macros::openvmm_test; -fn configure_mana_vf_diagnostics( - config: PetriVmBuilder, -) -> PetriVmBuilder { - config - .with_vtl0_kernel_command_line( - "rcupdate.rcu_cpu_stall_timeout=10 rcupdate.rcu_cpu_stall_cputime=1", - ) - .with_openhcl_log_levels(OpenvmmLogConfig::Custom( - [ - ( - "OPENVMM_LOG".to_owned(), - "debug,underhill_core::emuplat::netvsp=trace".to_owned(), - ), - ("OPENVMM_SHOW_SPANS".to_owned(), "true".to_owned()), - ] - .into(), - )) -} - /// Validates that the nic can get an IP address via consomme's DHCP implementation. /// Validates ICMP by testing that the nic can ping consomme's IP address. /// /// FUTURE: TCP / UDP traffic? async fn validate_mana_nic( agent: &PipetteClient, - eth0_is_mana_vf: bool, + has_vtl0_mana_vf: bool, ) -> Result<(), anyhow::Error> { let sh = agent.unix_shell(); - cmd!(sh, "ifconfig eth0 up").run().await?; - cmd!(sh, "udhcpc eth0").run().await?; - let output = cmd!(sh, "ifconfig eth0").read().await?; + let interface = if has_vtl0_mana_vf { "eth1" } else { "eth0" }; + cmd!(sh, "ifconfig {interface} up").run().await?; + cmd!(sh, "udhcpc -i {interface}").run().await?; + let output = cmd!(sh, "ifconfig {interface}").read().await?; // Validate that we see a mana nic with the expected MAC address and IPs. assert!(output.contains("HWaddr 00:15:5D:12:12:12")); assert!(output.contains("inet addr:10.0.0.2")); - if eth0_is_mana_vf { - cmd!(sh, "ifconfig eth1").ignore_status().run().await?; + if has_vtl0_mana_vf { + let vf_output = cmd!(sh, "ifconfig eth0").read().await?; + assert!(vf_output.contains("HWaddr 00:15:5D:12:12:12")); + let vf_master = cmd!(sh, "readlink /sys/class/net/eth0/master") + .read() + .await?; + assert_eq!(vf_master.rsplit('/').next(), Some("eth1")); } else { assert!(output.contains("inet6 addr: fe80::215:5dff:fe12:1212/64")); } - cmd!(sh, "ping -c 1 -W 5 -I eth0 10.0.0.1").run().await?; + cmd!(sh, "ping -c 1 -W 5 -I {interface} 10.0.0.1") + .run() + .await?; Ok(()) } @@ -93,12 +81,60 @@ async fn mana_nic_shared_pool( Ok(()) } +#[openvmm_test(openhcl_linux_direct_x64)] +async fn mana_nic_with_vtl0_vf( + config: PetriVmBuilder, +) -> Result<(), anyhow::Error> { + let (mana, mana_config) = ManaTestControl::new(); + let config = config + .with_vmbus_redirect(true) + .modify_backend(move |b| b.with_nic_test_control(mana_config)); + + let (vm, agent) = config.run().await?; + validate_mana_nic(&agent, true).await?; + + mana.shutdown().await?; + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + +#[openvmm_test(openhcl_linux_direct_x64)] +async fn mana_nic_unbind_mana_driver( + config: PetriVmBuilder, +) -> Result<(), anyhow::Error> { + let (mana, mana_config) = ManaTestControl::new(); + let config = config + .with_vmbus_redirect(true) + .modify_backend(move |b| b.with_nic_test_control(mana_config)); + + let (vm, agent) = config.run().await?; + validate_mana_nic(&agent, true).await?; + + let sh = agent.unix_shell(); + cmd!(sh, "sh") + .args([ + "-c", + "bdf=$(basename $(readlink -f /sys/class/net/eth0/device)); echo $bdf > /sys/bus/pci/drivers/mana/unbind", + ]) + .run() + .await?; + cmd!(sh, "test ! -e /sys/class/net/eth0").run().await?; + + mana.shutdown().await?; + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + + Ok(()) +} + async fn mana_nic_vf_reconfig( config: PetriVmBuilder, revoke_vtl0_vf: bool, ) -> Result<(), anyhow::Error> { let (mana, mana_config) = ManaTestControl::new(); - let config = configure_mana_vf_diagnostics(config) + let config = config .with_vmbus_redirect(true) .modify_backend(move |b| b.with_nic_test_control(mana_config)); @@ -112,6 +148,7 @@ async fn mana_nic_vf_reconfig( cmd!(sh, "sleep 5").run().await?; validate_mana_nic(&agent, true).await?; + mana.shutdown().await?; agent.power_off().await?; vm.wait_for_clean_teardown().await?; @@ -140,7 +177,7 @@ async fn mana_nic_vport_link_state( config: PetriVmBuilder, ) -> Result<(), anyhow::Error> { let (mana, mana_config) = ManaTestControl::new(); - let config = configure_mana_vf_diagnostics(config) + let config = config .with_vmbus_redirect(true) .modify_backend(move |b| b.with_nic_test_control(mana_config)); @@ -151,7 +188,7 @@ async fn mana_nic_vport_link_state( mana.set_vport_link_state(0, false).await?; cmd!( sh, - "timeout 30 sh -c 'until [ \"$(cat /sys/class/net/eth0/carrier)\" = 0 ]; do sleep 1; done'" + "timeout 30 sh -c 'until [ \"$(cat /sys/class/net/eth1/carrier)\" = 0 ]; do sleep 1; done'" ) .run() .await?; @@ -159,12 +196,13 @@ async fn mana_nic_vport_link_state( mana.set_vport_link_state(0, true).await?; cmd!( sh, - "timeout 30 sh -c 'until [ \"$(cat /sys/class/net/eth0/carrier)\" = 1 ]; do sleep 1; done'" + "timeout 30 sh -c 'until [ \"$(cat /sys/class/net/eth1/carrier)\" = 1 ]; do sleep 1; done'" ) .run() .await?; validate_mana_nic(&agent, true).await?; + mana.shutdown().await?; agent.power_off().await?; vm.wait_for_clean_teardown().await?; From b306d76f2c1acf4d076a24ccf941bd17989f9428 Mon Sep 17 00:00:00 2001 From: erfrimod <31358361+erfrimod@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:59:57 -0700 Subject: [PATCH 09/18] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- petri/src/vm/openvmm/construct.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/petri/src/vm/openvmm/construct.rs b/petri/src/vm/openvmm/construct.rs index 8753904dfe..2ee8dacb96 100644 --- a/petri/src/vm/openvmm/construct.rs +++ b/petri/src/vm/openvmm/construct.rs @@ -1009,10 +1009,10 @@ impl PetriVmConfigSetupCore<'_> { let mut vtl0_cmdline = format!("rdinit=/bin/sh {vsock_blacklist}"); if let Some(additional_command_line) = vtl0_kernel_command_line { anyhow::ensure!( - !additional_command_line + additional_command_line .chars() - .any(|c| matches!(c, '\'' | '"')), - "VTL0 kernel command line cannot contain quotes" + .all(|c| !c.is_control() && !matches!(c, '\'' | '"')), + "VTL0 kernel command line cannot contain quotes or control characters" ); vtl0_cmdline.push(' '); vtl0_cmdline.push_str(additional_command_line); From 4cf9d9e73a66110956ca0d90463de0fe77dfe713 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Wed, 26 Aug 2026 16:03:42 -0700 Subject: [PATCH 10/18] pr feedback --- openvmm/openvmm_resources/Cargo.toml | 4 ++-- openvmm/openvmm_resources/src/lib.rs | 1 + .../tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs | 7 ++++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/openvmm/openvmm_resources/Cargo.toml b/openvmm/openvmm_resources/Cargo.toml index 1a469a552d..8151524e63 100644 --- a/openvmm/openvmm_resources/Cargo.toml +++ b/openvmm/openvmm_resources/Cargo.toml @@ -26,7 +26,7 @@ virt_whp = ["openvmm_core/virt_whp", "dep:virt_whp"] virt_hvf = ["dep:virt_hvf"] # Enable utilities used by VMM tests. -test_utilities = ["guest_emulation_device/test_utilities"] +test_utilities = ["dep:gdma_test", "guest_emulation_device/test_utilities"] [dependencies] hypervisor_resources.workspace = true @@ -65,7 +65,7 @@ vmgs_broker.workspace = true # PCI devices cxl_spec.workspace = true gdma.workspace = true -gdma_test.workspace = true +gdma_test = { workspace = true, optional = true } nvme.workspace = true nvme_test.workspace = true diff --git a/openvmm/openvmm_resources/src/lib.rs b/openvmm/openvmm_resources/src/lib.rs index aa413203ec..4f0fa62d36 100644 --- a/openvmm/openvmm_resources/src/lib.rs +++ b/openvmm/openvmm_resources/src/lib.rs @@ -88,6 +88,7 @@ vm_resource::register_static_resolvers! { // PCI devices cxl_spec::test::resolver::CxlTestDeviceResolver, gdma::resolver::GdmaDeviceResolver, + #[cfg(feature = "test_utilities")] gdma_test::resolver::GdmaTestDeviceResolver, nvme::resolver::NvmeControllerResolver, nvme_test::resolver::NvmeFaultControllerResolver, diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs index f59b4d789d..c31a368481 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs @@ -145,7 +145,12 @@ async fn mana_nic_vf_reconfig( let sh = agent.unix_shell(); mana.inject_vf_reset(revoke_vtl0_vf).await?; - cmd!(sh, "sleep 5").run().await?; + cmd!( + sh, + "timeout 30 sh -c 'until [ \"$(basename \"$(readlink /sys/class/net/eth0/master)\")\" = eth1 ] && [ \"$(cat /sys/class/net/eth1/carrier)\" = 1 ] && ping -c 1 -W 1 -I eth1 10.0.0.1 >/dev/null 2>&1; do sleep 1; done'" + ) + .run() + .await?; validate_mana_nic(&agent, true).await?; mana.shutdown().await?; From f8b0de1e2a5f359139f139c708221236d9a1103b Mon Sep 17 00:00:00 2001 From: erfrimod Date: Thu, 27 Aug 2026 10:54:16 -0700 Subject: [PATCH 11/18] revert testing changes to vtl0 kernel command line --- petri/src/vm/mod.rs | 15 --------------- petri/src/vm/openvmm/construct.rs | 15 +-------------- 2 files changed, 1 insertion(+), 29 deletions(-) diff --git a/petri/src/vm/mod.rs b/petri/src/vm/mod.rs index 8fb4c6405b..8c09ccfa8d 100644 --- a/petri/src/vm/mod.rs +++ b/petri/src/vm/mod.rs @@ -1405,18 +1405,6 @@ impl PetriVmBuilder { self } - /// Append additional command line arguments to the VTL0 Linux kernel. - pub fn with_vtl0_kernel_command_line(mut self, additional_command_line: &str) -> Self { - let Firmware::OpenhclLinuxDirect { openhcl_config, .. } = &mut self.config.firmware else { - panic!("VTL0 kernel command line is only supported for OpenHCL Linux direct firmware.") - }; - append_cmdline( - &mut openhcl_config.vtl0_kernel_command_line, - additional_command_line, - ); - self - } - /// Enable confidential filtering, even if the VM is not confidential. pub fn with_confidential_filtering(self) -> Self { if !self.config.firmware.is_openhcl() { @@ -2519,8 +2507,6 @@ pub struct OpenHclConfig { /// command line and pass to OpenHCL. VM backends should use /// [`OpenHclConfig::command_line()`] rather than reading this directly. pub custom_command_line: Option, - /// Test-specified command-line parameters to append to the VTL0 Linux kernel command line. - pub vtl0_kernel_command_line: Option, /// Command line parameters that control OpenHCL logging behavior. Separate /// from `command_line` so that petri can decide to use default log /// levels. @@ -2580,7 +2566,6 @@ impl Default for OpenHclConfig { Self { vmbus_redirect: false, custom_command_line: None, - vtl0_kernel_command_line: None, log_levels: OpenvmmLogConfig::TestDefault, vtl2_base_address_type: None, vtl2_settings: None, diff --git a/petri/src/vm/openvmm/construct.rs b/petri/src/vm/openvmm/construct.rs index 2ee8dacb96..e6c456d815 100644 --- a/petri/src/vm/openvmm/construct.rs +++ b/petri/src/vm/openvmm/construct.rs @@ -991,7 +991,6 @@ impl PetriVmConfigSetupCore<'_> { let OpenHclConfig { vmbus_redirect: _, // config_openhcl_vmbus_devices custom_command_line: _, - vtl0_kernel_command_line, log_levels: _, vtl2_base_address_type, vtl2_settings: _, // run_core @@ -1006,22 +1005,10 @@ impl PetriVmConfigSetupCore<'_> { // Set UNDERHILL_SERIAL_WAIT_FOR_RTS=1 so that we don't pull serial data // until the guest is ready. Otherwise, Linux will drop the input serial // data on the floor during boot. - let mut vtl0_cmdline = format!("rdinit=/bin/sh {vsock_blacklist}"); - if let Some(additional_command_line) = vtl0_kernel_command_line { - anyhow::ensure!( - additional_command_line - .chars() - .all(|c| !c.is_control() && !matches!(c, '\'' | '"')), - "VTL0 kernel command line cannot contain quotes or control characters" - ); - vtl0_cmdline.push(' '); - vtl0_cmdline.push_str(additional_command_line); - } append_cmdline( &mut cmdline, format!( - "UNDERHILL_SERIAL_WAIT_FOR_RTS=1 UNDERHILL_CMDLINE_APPEND=\"{}\"", - vtl0_cmdline + "UNDERHILL_SERIAL_WAIT_FOR_RTS=1 UNDERHILL_CMDLINE_APPEND=\"rdinit=/bin/sh {vsock_blacklist}\"" ), ); false From 18d429b1cb68b0d6a8b8cab24f5fdbd2147e9c38 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Wed, 2 Sep 2026 15:01:01 -0700 Subject: [PATCH 12/18] removing feature gate for gdma test resolver --- .../src/pipelines/checkin_gates.rs | 19 +++++-------------- .../local_build_and_run_nextest_vmm_tests.rs | 2 +- flowey/flowey_lib_hvlite/src/build_openvmm.rs | 2 -- openvmm/openvmm/Cargo.toml | 1 - openvmm/openvmm_resources/Cargo.toml | 5 +---- openvmm/openvmm_resources/src/lib.rs | 1 - .../get/guest_emulation_device/src/lib.rs | 16 +++++++++++++++- .../src/test_utilities.rs | 18 ------------------ 8 files changed, 22 insertions(+), 42 deletions(-) diff --git a/flowey/flowey_hvlite/src/pipelines/checkin_gates.rs b/flowey/flowey_hvlite/src/pipelines/checkin_gates.rs index 712b7c608d..e7ce2e1167 100644 --- a/flowey/flowey_hvlite/src/pipelines/checkin_gates.rs +++ b/flowey/flowey_hvlite/src/pipelines/checkin_gates.rs @@ -657,10 +657,7 @@ impl IntoPipeline for CheckinGatesCli { }, profile: CommonProfile::from_release(release), // FIXME: this relies on openvmm default features - features: [ - flowey_lib_hvlite::build_openvmm::OpenvmmFeature::TestUtilities, - ] - .into(), + features: [].into(), }, openvmm, } @@ -830,11 +827,8 @@ impl IntoPipeline for CheckinGatesCli { }, profile: CommonProfile::from_release(release), // FIXME: this relies on openvmm default features - features: [ - flowey_lib_hvlite::build_openvmm::OpenvmmFeature::TestUtilities, - flowey_lib_hvlite::build_openvmm::OpenvmmFeature::Tpm, - ] - .into(), + features: [flowey_lib_hvlite::build_openvmm::OpenvmmFeature::Tpm] + .into(), }, openvmm, } @@ -910,11 +904,8 @@ impl IntoPipeline for CheckinGatesCli { platform: CommonPlatform::LinuxMusl, }, profile: CommonProfile::from_release(release), - features: [ - flowey_lib_hvlite::build_openvmm::OpenvmmFeature::TestUtilities, - flowey_lib_hvlite::build_openvmm::OpenvmmFeature::Tpm, - ] - .into(), + features: [flowey_lib_hvlite::build_openvmm::OpenvmmFeature::Tpm] + .into(), }, openvmm, } 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 7b141e1fbb..ec4ed5dce2 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 @@ -283,7 +283,7 @@ impl SimpleFlowNode for Node { target: target.clone(), profile: CommonProfile::from_release(release), // FIXME: this relies on openvmm default features - features: [crate::build_openvmm::OpenvmmFeature::TestUtilities].into(), + features: [].into(), }, openvmm: v, }); diff --git a/flowey/flowey_lib_hvlite/src/build_openvmm.rs b/flowey/flowey_lib_hvlite/src/build_openvmm.rs index 045d7ce630..d14adc23c9 100644 --- a/flowey/flowey_lib_hvlite/src/build_openvmm.rs +++ b/flowey/flowey_lib_hvlite/src/build_openvmm.rs @@ -12,7 +12,6 @@ use std::collections::BTreeSet; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum OpenvmmFeature { Gdb, - TestUtilities, Tpm, } @@ -97,7 +96,6 @@ impl FlowNode for Node { .map(|f| { match f { OpenvmmFeature::Gdb => "gdb", - OpenvmmFeature::TestUtilities => "test_utilities", OpenvmmFeature::Tpm => "tpm", } .into() diff --git a/openvmm/openvmm/Cargo.toml b/openvmm/openvmm/Cargo.toml index 94a1ffd002..7eb546ee81 100644 --- a/openvmm/openvmm/Cargo.toml +++ b/openvmm/openvmm/Cargo.toml @@ -25,7 +25,6 @@ default = [ gdb = ["openvmm_resources/gdb"] vendored_crypto = ["crypto/vendored"] tpm = ["openvmm_resources/tpm"] -test_utilities = ["openvmm_resources/test_utilities"] virt_hvf = ["openvmm_resources/virt_hvf", "openvmm_hypervisors/virt_hvf"] virt_kvm = ["openvmm_resources/virt_kvm", "openvmm_hypervisors/virt_kvm"] virt_mshv = ["openvmm_resources/virt_mshv", "openvmm_hypervisors/virt_mshv"] diff --git a/openvmm/openvmm_resources/Cargo.toml b/openvmm/openvmm_resources/Cargo.toml index 8151524e63..27804fcd68 100644 --- a/openvmm/openvmm_resources/Cargo.toml +++ b/openvmm/openvmm_resources/Cargo.toml @@ -25,9 +25,6 @@ virt_whp = ["openvmm_core/virt_whp", "dep:virt_whp"] # Enable building with macOS hypervisor framework support. virt_hvf = ["dep:virt_hvf"] -# Enable utilities used by VMM tests. -test_utilities = ["dep:gdma_test", "guest_emulation_device/test_utilities"] - [dependencies] hypervisor_resources.workspace = true mesh_worker.workspace = true @@ -65,7 +62,7 @@ vmgs_broker.workspace = true # PCI devices cxl_spec.workspace = true gdma.workspace = true -gdma_test = { workspace = true, optional = true } +gdma_test.workspace = true nvme.workspace = true nvme_test.workspace = true diff --git a/openvmm/openvmm_resources/src/lib.rs b/openvmm/openvmm_resources/src/lib.rs index 4f0fa62d36..aa413203ec 100644 --- a/openvmm/openvmm_resources/src/lib.rs +++ b/openvmm/openvmm_resources/src/lib.rs @@ -88,7 +88,6 @@ vm_resource::register_static_resolvers! { // PCI devices cxl_spec::test::resolver::CxlTestDeviceResolver, gdma::resolver::GdmaDeviceResolver, - #[cfg(feature = "test_utilities")] gdma_test::resolver::GdmaTestDeviceResolver, nvme::resolver::NvmeControllerResolver, nvme_test::resolver::NvmeFaultControllerResolver, diff --git a/vm/devices/get/guest_emulation_device/src/lib.rs b/vm/devices/get/guest_emulation_device/src/lib.rs index ad1d8a1bc2..4375f23d27 100644 --- a/vm/devices/get/guest_emulation_device/src/lib.rs +++ b/vm/devices/get/guest_emulation_device/src/lib.rs @@ -645,7 +645,6 @@ impl GedChannel { HostRequests::CREATE_RAM_GPA_RANGE => self.handle_create_ram_gpa_range(message_buf)?, HostRequests::RESET_RAM_GPA_RANGE => self.handle_reset_ram_gpa_range(message_buf)?, HostRequests::LOAD_FIRMWARE => self.handle_load_firmware(message_buf)?, - #[cfg(feature = "test_utilities")] HostRequests::VPCI_DEVICE_CONTROL => self.handle_vpci_device_control(message_buf)?, _ => { tracing::error!(message_id = ?header.message_id(), "unexpected message"); @@ -1086,6 +1085,21 @@ impl GedChannel { Ok(()) } + fn handle_vpci_device_control(&mut self, message_buf: &[u8]) -> Result<(), Error> { + let request = get_protocol::VpciDeviceControlRequest::read_from_prefix(message_buf) + .map_err(|_| Error::MessageTooSmall)? + .0; + let status = if request.code == get_protocol::VpciDeviceControlCode::OFFER { + get_protocol::VpciDeviceControlStatus::SUCCESS + } else { + get_protocol::VpciDeviceControlStatus::INVALID_REQUEST + }; + let response = get_protocol::VpciDeviceControlResponse::new(status); + self.channel + .try_send(response.as_bytes()) + .map_err(Error::Vmbus) + } + fn handle_host_notification( &mut self, header: get_protocol::HeaderHostNotification, diff --git a/vm/devices/get/guest_emulation_device/src/test_utilities.rs b/vm/devices/get/guest_emulation_device/src/test_utilities.rs index 6a026e1dc2..60d98025c3 100644 --- a/vm/devices/get/guest_emulation_device/src/test_utilities.rs +++ b/vm/devices/get/guest_emulation_device/src/test_utilities.rs @@ -227,24 +227,6 @@ impl TestGedChannel { } } -impl GedChannel { - pub(super) fn handle_vpci_device_control(&mut self, message_buf: &[u8]) -> Result<(), Error> { - let request: get_protocol::VpciDeviceControlRequest = - get_protocol::VpciDeviceControlRequest::read_from_prefix(message_buf) - .map_err(|_| Error::MessageTooSmall)? - .0; - let status = if request.code == get_protocol::VpciDeviceControlCode::OFFER { - get_protocol::VpciDeviceControlStatus::SUCCESS - } else { - get_protocol::VpciDeviceControlStatus::INVALID_REQUEST - }; - let response = get_protocol::VpciDeviceControlResponse::new(status); - self.channel - .try_send(response.as_bytes()) - .map_err(Error::Vmbus) - } -} - /// Create the host Guest Emulation Device and corresponding I/O. /// /// If `ged_responses` is Some(), then TestGedChannel will be used to From 0d12e47446cec0ba65bc14f4bd61b5a231aaff09 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 4 Sep 2026 14:28:25 -0700 Subject: [PATCH 13/18] cleaner logic in validate_mana_nic --- .../x86_64/openhcl_linux_direct/mana_nic.rs | 68 ++++++++++++------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs index c31a368481..be1797f996 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs @@ -10,32 +10,24 @@ use petri::pipette::PipetteClient; use petri::pipette::cmd; use vmm_test_macros::openvmm_test; -/// Validates that the nic can get an IP address via consomme's DHCP implementation. +/// Get an IP address via consomme's DHCP implementation. +async fn configure_mana_nic(agent: &PipetteClient, interface: &str) -> Result<(), anyhow::Error> { + let sh = agent.unix_shell(); + cmd!(sh, "ifconfig {interface} up").run().await?; + cmd!(sh, "udhcpc -i {interface}").run().await?; + + Ok(()) +} + /// Validates ICMP by testing that the nic can ping consomme's IP address. /// /// FUTURE: TCP / UDP traffic? -async fn validate_mana_nic( - agent: &PipetteClient, - has_vtl0_mana_vf: bool, -) -> Result<(), anyhow::Error> { +async fn validate_mana_nic(agent: &PipetteClient, interface: &str) -> Result<(), anyhow::Error> { let sh = agent.unix_shell(); - let interface = if has_vtl0_mana_vf { "eth1" } else { "eth0" }; - cmd!(sh, "ifconfig {interface} up").run().await?; - cmd!(sh, "udhcpc -i {interface}").run().await?; let output = cmd!(sh, "ifconfig {interface}").read().await?; // Validate that we see a mana nic with the expected MAC address and IPs. assert!(output.contains("HWaddr 00:15:5D:12:12:12")); assert!(output.contains("inet addr:10.0.0.2")); - if has_vtl0_mana_vf { - let vf_output = cmd!(sh, "ifconfig eth0").read().await?; - assert!(vf_output.contains("HWaddr 00:15:5D:12:12:12")); - let vf_master = cmd!(sh, "readlink /sys/class/net/eth0/master") - .read() - .await?; - assert_eq!(vf_master.rsplit('/').next(), Some("eth1")); - } else { - assert!(output.contains("inet6 addr: fe80::215:5dff:fe12:1212/64")); - } cmd!(sh, "ping -c 1 -W 5 -I {interface} 10.0.0.1") .run() .await?; @@ -43,6 +35,18 @@ async fn validate_mana_nic( Ok(()) } +async fn validate_vtl0_mana_vf(agent: &PipetteClient) -> Result<(), anyhow::Error> { + let sh = agent.unix_shell(); + let vf_output = cmd!(sh, "ifconfig eth0").read().await?; + assert!(vf_output.contains("HWaddr 00:15:5D:12:12:12")); + let vf_master = cmd!(sh, "readlink /sys/class/net/eth0/master") + .read() + .await?; + assert_eq!(vf_master.rsplit('/').next(), Some("eth1")); + + Ok(()) +} + /// Test an OpenHCL Linux direct VM with a MANA nic assigned to VTL2 (backed by /// the MANA emulator), and vmbus relay. #[openvmm_test(openhcl_linux_direct_x64)] @@ -53,7 +57,8 @@ async fn mana_nic(config: PetriVmBuilder) -> Result<(), any .run() .await?; - validate_mana_nic(&agent, false).await?; + configure_mana_nic(&agent, "eth0").await?; + validate_mana_nic(&agent, "eth0").await?; agent.power_off().await?; vm.wait_for_clean_teardown().await?; @@ -73,7 +78,8 @@ async fn mana_nic_shared_pool( .run() .await?; - validate_mana_nic(&agent, false).await?; + configure_mana_nic(&agent, "eth0").await?; + validate_mana_nic(&agent, "eth0").await?; agent.power_off().await?; vm.wait_for_clean_teardown().await?; @@ -91,7 +97,9 @@ async fn mana_nic_with_vtl0_vf( .modify_backend(move |b| b.with_nic_test_control(mana_config)); let (vm, agent) = config.run().await?; - validate_mana_nic(&agent, true).await?; + configure_mana_nic(&agent, "eth1").await?; + validate_mana_nic(&agent, "eth1").await?; + validate_vtl0_mana_vf(&agent).await?; mana.shutdown().await?; agent.power_off().await?; @@ -110,7 +118,9 @@ async fn mana_nic_unbind_mana_driver( .modify_backend(move |b| b.with_nic_test_control(mana_config)); let (vm, agent) = config.run().await?; - validate_mana_nic(&agent, true).await?; + configure_mana_nic(&agent, "eth1").await?; + validate_mana_nic(&agent, "eth1").await?; + validate_vtl0_mana_vf(&agent).await?; let sh = agent.unix_shell(); cmd!(sh, "sh") @@ -140,7 +150,9 @@ async fn mana_nic_vf_reconfig( let (vm, agent) = config.run().await?; - validate_mana_nic(&agent, true).await?; + configure_mana_nic(&agent, "eth1").await?; + validate_mana_nic(&agent, "eth1").await?; + validate_vtl0_mana_vf(&agent).await?; let sh = agent.unix_shell(); @@ -151,7 +163,8 @@ async fn mana_nic_vf_reconfig( ) .run() .await?; - validate_mana_nic(&agent, true).await?; + validate_mana_nic(&agent, "eth1").await?; + validate_vtl0_mana_vf(&agent).await?; mana.shutdown().await?; agent.power_off().await?; @@ -187,7 +200,9 @@ async fn mana_nic_vport_link_state( .modify_backend(move |b| b.with_nic_test_control(mana_config)); let (vm, agent) = config.run().await?; - validate_mana_nic(&agent, true).await?; + configure_mana_nic(&agent, "eth1").await?; + validate_mana_nic(&agent, "eth1").await?; + validate_vtl0_mana_vf(&agent).await?; let sh = agent.unix_shell(); mana.set_vport_link_state(0, false).await?; @@ -205,7 +220,8 @@ async fn mana_nic_vport_link_state( ) .run() .await?; - validate_mana_nic(&agent, true).await?; + validate_mana_nic(&agent, "eth1").await?; + validate_vtl0_mana_vf(&agent).await?; mana.shutdown().await?; agent.power_off().await?; From 9f032c16c39d77d3164e4c1a7c2f865e354b6e23 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 4 Sep 2026 15:07:40 -0700 Subject: [PATCH 14/18] trim out the vport linkstate tests --- petri/src/vm/openvmm/modify.rs | 14 ------ vm/devices/net/gdma_resources/src/lib.rs | 7 --- vm/devices/net/gdma_test/src/resolver.rs | 48 +------------------ .../x86_64/openhcl_linux_direct/mana_nic.rs | 41 ---------------- 4 files changed, 1 insertion(+), 109 deletions(-) diff --git a/petri/src/vm/openvmm/modify.rs b/petri/src/vm/openvmm/modify.rs index 0c5a9f60a8..d971b127df 100644 --- a/petri/src/vm/openvmm/modify.rs +++ b/petri/src/vm/openvmm/modify.rs @@ -683,20 +683,6 @@ impl ManaTestControl { .await } - /// Changes the link state of `vport`. - pub async fn set_vport_link_state( - &self, - vport: u32, - connected: bool, - ) -> Result<(), mesh::rpc::RpcError> { - self.request_send - .call( - |rpc| rpc, - GdmaTestRequest::VportLinkState { vport, connected }, - ) - .await - } - /// Shuts down test control after all preceding requests have completed. pub async fn shutdown(self) -> Result<(), mesh::rpc::RpcError> { self.request_send diff --git a/vm/devices/net/gdma_resources/src/lib.rs b/vm/devices/net/gdma_resources/src/lib.rs index df4ec35907..d5096f42f1 100644 --- a/vm/devices/net/gdma_resources/src/lib.rs +++ b/vm/devices/net/gdma_resources/src/lib.rs @@ -48,13 +48,6 @@ pub enum GdmaTestRequest { /// Whether OpenHCL should revoke the VTL0 VF during reset. revoke_vtl0_vf: bool, }, - /// Change a vport's link state. - VportLinkState { - /// The zero-based vport index. - vport: u32, - /// Whether the link should be connected. - connected: bool, - }, } /// A basic NIC vport definition. diff --git a/vm/devices/net/gdma_test/src/resolver.rs b/vm/devices/net/gdma_test/src/resolver.rs index 5c0d37c437..0e0634f802 100644 --- a/vm/devices/net/gdma_test/src/resolver.rs +++ b/vm/devices/net/gdma_test/src/resolver.rs @@ -9,12 +9,8 @@ use gdma::GdmaDevice; use gdma::resolver::Error; use gdma::test_helpers::hwc_eq_injector; use gdma::test_helpers::resolve_vports; -use gdma_defs::EqeDataReconfig; use gdma_defs::EqeVfReset; -use gdma_defs::GDMA_EQE_HWC_RECONFIG_DATA; use gdma_defs::GDMA_EQE_HWC_RESET_REQUEST; -use gdma_defs::HWC_DATA_TYPE_HW_VPORT_LINK_CONNECT; -use gdma_defs::HWC_DATA_TYPE_HW_VPORT_LINK_DISCONNECT; use gdma_resources::GdmaTestDeviceHandle; use gdma_resources::GdmaTestRequest; use pal_async::task::Spawn; @@ -41,21 +37,18 @@ declare_static_async_resolver! { enum EncodedTestRequest { VfReset(EqeVfReset), - VportLinkState(EqeDataReconfig), } impl EncodedTestRequest { fn eqe_type(&self) -> u8 { match self { Self::VfReset(_) => GDMA_EQE_HWC_RESET_REQUEST, - Self::VportLinkState(_) => GDMA_EQE_HWC_RECONFIG_DATA, } } fn data(&self) -> &[u8] { match self { Self::VfReset(data) => data.as_bytes(), - Self::VportLinkState(data) => data.as_bytes(), } } } @@ -66,23 +59,6 @@ fn encode_request(request: GdmaTestRequest) -> EncodedTestRequest { GdmaTestRequest::VfReset { revoke_vtl0_vf } => { EncodedTestRequest::VfReset(EqeVfReset::new().with_revoke_vtl0_vf(revoke_vtl0_vf)) } - GdmaTestRequest::VportLinkState { vport, connected } => { - let data_type = if connected { - HWC_DATA_TYPE_HW_VPORT_LINK_CONNECT - } else { - HWC_DATA_TYPE_HW_VPORT_LINK_DISCONNECT - }; - assert!( - vport <= 0x00ff_ffff, - "vport index does not fit in the 24-bit EQE field" - ); - let vport = vport.to_le_bytes(); - EncodedTestRequest::VportLinkState(EqeDataReconfig { - data: [vport[0], vport[1], vport[2]], - data_type, - reserved1: [0; 8], - }) - } } } @@ -146,30 +122,8 @@ mod tests { for revoke_vtl0_vf in [false, true] { let request = encode_request(GdmaTestRequest::VfReset { revoke_vtl0_vf }); assert_eq!(request.eqe_type(), GDMA_EQE_HWC_RESET_REQUEST); - let EncodedTestRequest::VfReset(data) = request else { - panic!("VF reset encoded as the wrong event type"); - }; + let EncodedTestRequest::VfReset(data) = request; assert_eq!(data.revoke_vtl0_vf(), revoke_vtl0_vf); } } - - #[test] - fn encode_vport_link_state() { - for (connected, expected_data_type) in [ - (false, HWC_DATA_TYPE_HW_VPORT_LINK_DISCONNECT), - (true, HWC_DATA_TYPE_HW_VPORT_LINK_CONNECT), - ] { - let request = encode_request(GdmaTestRequest::VportLinkState { - vport: 0x00ab_cdef, - connected, - }); - assert_eq!(request.eqe_type(), GDMA_EQE_HWC_RECONFIG_DATA); - let EncodedTestRequest::VportLinkState(data) = request else { - panic!("vport link state encoded as the wrong event type"); - }; - assert_eq!(data.data, [0xef, 0xcd, 0xab]); - assert_eq!(data.data_type, expected_data_type); - assert_eq!(data.reserved1, [0; 8]); - } - } } diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs index be1797f996..fc01c93113 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs @@ -188,44 +188,3 @@ async fn mana_nic_vf_reconfig_revoke_vtl0_vf( ) -> Result<(), anyhow::Error> { mana_nic_vf_reconfig(config, true).await } - -/// Test guest-visible vport link disconnect and reconnect events. -#[openvmm_test(openhcl_linux_direct_x64)] -async fn mana_nic_vport_link_state( - config: PetriVmBuilder, -) -> Result<(), anyhow::Error> { - let (mana, mana_config) = ManaTestControl::new(); - let config = config - .with_vmbus_redirect(true) - .modify_backend(move |b| b.with_nic_test_control(mana_config)); - - let (vm, agent) = config.run().await?; - configure_mana_nic(&agent, "eth1").await?; - validate_mana_nic(&agent, "eth1").await?; - validate_vtl0_mana_vf(&agent).await?; - - let sh = agent.unix_shell(); - mana.set_vport_link_state(0, false).await?; - cmd!( - sh, - "timeout 30 sh -c 'until [ \"$(cat /sys/class/net/eth1/carrier)\" = 0 ]; do sleep 1; done'" - ) - .run() - .await?; - - mana.set_vport_link_state(0, true).await?; - cmd!( - sh, - "timeout 30 sh -c 'until [ \"$(cat /sys/class/net/eth1/carrier)\" = 1 ]; do sleep 1; done'" - ) - .run() - .await?; - validate_mana_nic(&agent, "eth1").await?; - validate_vtl0_mana_vf(&agent).await?; - - mana.shutdown().await?; - agent.power_off().await?; - vm.wait_for_clean_teardown().await?; - - Ok(()) -} From 09e8ccf8184ff9b63a22a3a78f0d4631e3db409f Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 4 Sep 2026 15:24:49 -0700 Subject: [PATCH 15/18] handle vpci device control succeds for revoke and reset --- vm/devices/get/guest_emulation_device/src/lib.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/vm/devices/get/guest_emulation_device/src/lib.rs b/vm/devices/get/guest_emulation_device/src/lib.rs index 4375f23d27..715d0997f9 100644 --- a/vm/devices/get/guest_emulation_device/src/lib.rs +++ b/vm/devices/get/guest_emulation_device/src/lib.rs @@ -1089,10 +1089,13 @@ impl GedChannel { let request = get_protocol::VpciDeviceControlRequest::read_from_prefix(message_buf) .map_err(|_| Error::MessageTooSmall)? .0; - let status = if request.code == get_protocol::VpciDeviceControlCode::OFFER { - get_protocol::VpciDeviceControlStatus::SUCCESS - } else { - get_protocol::VpciDeviceControlStatus::INVALID_REQUEST + let status = match request.code { + get_protocol::VpciDeviceControlCode::OFFER + | get_protocol::VpciDeviceControlCode::REVOKE + | get_protocol::VpciDeviceControlCode::RESET => { + get_protocol::VpciDeviceControlStatus::SUCCESS + } + _ => get_protocol::VpciDeviceControlStatus::INVALID_REQUEST, }; let response = get_protocol::VpciDeviceControlResponse::new(status); self.channel From 3983fa0c3005e10dfc83315a74de0349612e2472 Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 4 Sep 2026 15:58:13 -0700 Subject: [PATCH 16/18] trim useless polling check --- .../tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs index fc01c93113..7808e406c2 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs @@ -157,12 +157,6 @@ async fn mana_nic_vf_reconfig( let sh = agent.unix_shell(); mana.inject_vf_reset(revoke_vtl0_vf).await?; - cmd!( - sh, - "timeout 30 sh -c 'until [ \"$(basename \"$(readlink /sys/class/net/eth0/master)\")\" = eth1 ] && [ \"$(cat /sys/class/net/eth1/carrier)\" = 1 ] && ping -c 1 -W 1 -I eth1 10.0.0.1 >/dev/null 2>&1; do sleep 1; done'" - ) - .run() - .await?; validate_mana_nic(&agent, "eth1").await?; validate_vtl0_mana_vf(&agent).await?; From e6644706004c676695a3528ff97344a230124e6b Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 4 Sep 2026 16:05:22 -0700 Subject: [PATCH 17/18] clippy --- .../tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs index 7808e406c2..4cb5335ed8 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs @@ -154,8 +154,6 @@ async fn mana_nic_vf_reconfig( validate_mana_nic(&agent, "eth1").await?; validate_vtl0_mana_vf(&agent).await?; - let sh = agent.unix_shell(); - mana.inject_vf_reset(revoke_vtl0_vf).await?; validate_mana_nic(&agent, "eth1").await?; validate_vtl0_mana_vf(&agent).await?; From 5851227586fd7513afdddd9fe4f8221bacf075da Mon Sep 17 00:00:00 2001 From: erfrimod Date: Fri, 4 Sep 2026 16:11:09 -0700 Subject: [PATCH 18/18] comments, remove unbind test --- petri/src/vm/openvmm/modify.rs | 4 +++ .../x86_64/openhcl_linux_direct/mana_nic.rs | 32 +------------------ 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/petri/src/vm/openvmm/modify.rs b/petri/src/vm/openvmm/modify.rs index d971b127df..d680fd9249 100644 --- a/petri/src/vm/openvmm/modify.rs +++ b/petri/src/vm/openvmm/modify.rs @@ -675,6 +675,10 @@ impl ManaTestControl { /// Requests VF reconfiguration through the emulated hardware channel. /// + /// Completion means the reset EQE has been posted to the HWC EQ. + /// It may take time for the EQE to be processed. + /// Then, VF Reconfiguration will be completed asynchronously. + /// /// `revoke_vtl0_vf`: when `true` the guest VTL0 VF is revoked as part of /// the reconfiguration. pub async fn inject_vf_reset(&self, revoke_vtl0_vf: bool) -> Result<(), mesh::rpc::RpcError> { diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs index 4cb5335ed8..f54ecedff2 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/openhcl_linux_direct/mana_nic.rs @@ -108,37 +108,6 @@ async fn mana_nic_with_vtl0_vf( Ok(()) } -#[openvmm_test(openhcl_linux_direct_x64)] -async fn mana_nic_unbind_mana_driver( - config: PetriVmBuilder, -) -> Result<(), anyhow::Error> { - let (mana, mana_config) = ManaTestControl::new(); - let config = config - .with_vmbus_redirect(true) - .modify_backend(move |b| b.with_nic_test_control(mana_config)); - - let (vm, agent) = config.run().await?; - configure_mana_nic(&agent, "eth1").await?; - validate_mana_nic(&agent, "eth1").await?; - validate_vtl0_mana_vf(&agent).await?; - - let sh = agent.unix_shell(); - cmd!(sh, "sh") - .args([ - "-c", - "bdf=$(basename $(readlink -f /sys/class/net/eth0/device)); echo $bdf > /sys/bus/pci/drivers/mana/unbind", - ]) - .run() - .await?; - cmd!(sh, "test ! -e /sys/class/net/eth0").run().await?; - - mana.shutdown().await?; - agent.power_off().await?; - vm.wait_for_clean_teardown().await?; - - Ok(()) -} - async fn mana_nic_vf_reconfig( config: PetriVmBuilder, revoke_vtl0_vf: bool, @@ -154,6 +123,7 @@ async fn mana_nic_vf_reconfig( validate_mana_nic(&agent, "eth1").await?; validate_vtl0_mana_vf(&agent).await?; + // Injection only waits for the EQE to be posted. mana.inject_vf_reset(revoke_vtl0_vf).await?; validate_mana_nic(&agent, "eth1").await?; validate_vtl0_mana_vf(&agent).await?;