diff --git a/Cargo.lock b/Cargo.lock index 2e29b4f830..629c22d239 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -553,6 +553,7 @@ version = "0.0.0" dependencies = [ "inspect", "mesh", + "tdisp", ] [[package]] @@ -577,6 +578,7 @@ dependencies = [ "chipset_device", "guestmem", "inspect", + "tdisp", "vm_resource", "vmcore", ] @@ -4864,6 +4866,7 @@ dependencies = [ "pci_resources", "scsi_buffers", "task_control", + "tdisp", "thiserror 2.0.16", "tracelimit", "tracing", @@ -5028,6 +5031,15 @@ dependencies = [ "vmcore", ] +[[package]] +name = "openhcl_tdisp" +version = "0.0.0" +dependencies = [ + "anyhow", + "tdisp", + "tdisp_proto", +] + [[package]] name = "openssl" version = "0.10.73" @@ -7375,11 +7387,13 @@ name = "tdisp" version = "0.0.0" dependencies = [ "anyhow", + "bitfield-struct 0.11.0", "parking_lot", "prost", + "static_assertions", "tdisp_proto", - "thiserror 2.0.16", "tracing", + "zerocopy 0.8.27", ] [[package]] @@ -7389,6 +7403,7 @@ dependencies = [ "inspect", "prost", "prost-build", + "thiserror 2.0.16", ] [[package]] @@ -9880,10 +9895,12 @@ dependencies = [ "hvdef", "inspect", "mesh", + "openhcl_tdisp", "pal_async", "parking_lot", "pci_core", "task_control", + "tdisp", "test_with_tracing", "thiserror 2.0.16", "tracelimit", @@ -9909,11 +9926,13 @@ dependencies = [ "guid", "inspect", "mesh", + "openhcl_tdisp", "pal_async", "parking_lot", "pci_core", "slab", "task_control", + "tdisp", "test_with_tracing", "thiserror 2.0.16", "tracelimit", @@ -9950,6 +9969,7 @@ dependencies = [ "inspect", "memory_range", "mesh", + "openhcl_tdisp", "pci_core", "slab", "sparse_mmap", diff --git a/Cargo.toml b/Cargo.toml index 821ce5c58d..e6a7910812 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -192,6 +192,7 @@ underhill_init = { path = "openhcl/underhill_init" } underhill_mem = { path = "openhcl/underhill_mem" } openhcl_attestation_protocol = { path = "openhcl/openhcl_attestation_protocol" } openvmm_hcl_resources = { path = "openhcl/openvmm_hcl_resources" } +openhcl_tdisp = { path = "openhcl/openhcl_tdisp" } underhill_threadpool = { path = "openhcl/underhill_threadpool" } virt_mshv_vtl = { path = "openhcl/virt_mshv_vtl" } diff --git a/openhcl/openhcl_tdisp/Cargo.toml b/openhcl/openhcl_tdisp/Cargo.toml new file mode 100644 index 0000000000..73617f827c --- /dev/null +++ b/openhcl/openhcl_tdisp/Cargo.toml @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[package] +name = "openhcl_tdisp" +rust-version.workspace = true +edition.workspace = true + +[dependencies] +tdisp.workspace = true +tdisp_proto.workspace = true + +anyhow.workspace = true + +[lints] +workspace = true diff --git a/openhcl/openhcl_tdisp/src/lib.rs b/openhcl/openhcl_tdisp/src/lib.rs new file mode 100644 index 0000000000..0739b1d30c --- /dev/null +++ b/openhcl/openhcl_tdisp/src/lib.rs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! This module provides resources and traits for a TDISP client device +//! interface for OpenHCL devices. +//! +//! See: `vm/devices/tdisp` for more information. + +use std::future::Future; + +// Re-export the TDISP protocol types necessary for OpenHCL from top level tdisp crates +// to avoid a direct dependency on tdisp_proto and tdisp. +pub use tdisp::TdispGuestOperationError; +pub use tdisp::devicereport::TdiReportStruct; +pub use tdisp::serialize_proto::deserialize_command; +pub use tdisp::serialize_proto::deserialize_response; +pub use tdisp::serialize_proto::serialize_command; +pub use tdisp::serialize_proto::serialize_response; +pub use tdisp_proto::GuestToHostCommand; +pub use tdisp_proto::GuestToHostCommandExt; +pub use tdisp_proto::GuestToHostResponse; +pub use tdisp_proto::GuestToHostResponseExt; +pub use tdisp_proto::TdispCommandRequestGetDeviceInterfaceInfo; +pub use tdisp_proto::TdispCommandResponseBind; +pub use tdisp_proto::TdispCommandResponseGetDeviceInterfaceInfo; +pub use tdisp_proto::TdispCommandResponseGetTdiReport; +pub use tdisp_proto::TdispCommandResponseStartTdi; +pub use tdisp_proto::TdispCommandResponseUnbind; +pub use tdisp_proto::TdispDeviceInterfaceInfo; +pub use tdisp_proto::TdispGuestOperationErrorCode; +pub use tdisp_proto::TdispGuestProtocolType; +pub use tdisp_proto::TdispGuestUnbindReason; +pub use tdisp_proto::TdispReportType; + +use tdisp_proto::TdispCommandRequestBind; +use tdisp_proto::TdispCommandRequestGetTdiReport; +use tdisp_proto::TdispCommandRequestStartTdi; +use tdisp_proto::TdispCommandRequestUnbind; +use tdisp_proto::guest_to_host_command::Command; + +/// Represents a TDISP device assigned to a guest partition. This trait allows +/// implementations to send TDISP commands to the host through a backing interface +/// such as a VPCI channel. +/// +pub trait TdispVirtualDeviceInterface: Send + Sync { + /// Sends a TDISP command to the device through the VPCI channel. + fn send_tdisp_command( + &self, + payload: GuestToHostCommand, + ) -> impl Future> + Send; + + /// Get the TDISP interface info for the device. + fn tdisp_get_device_interface_info( + &self, + ) -> impl Future> + Send; + + /// Bind the device to the current partition and transition to Locked. + /// NOTE: While the device is in the Locked state, it can continue to + /// perform unencrypted operations until it is moved to the Running state. + /// The Locked state is a transitional state that is designed to keep + /// the device from modifying its resources prior to attestation. + fn tdisp_bind_interface(&self) -> impl Future> + Send; + + /// Start a bound device by transitioning it to the Run state from the Locked state. + /// This allows for attestation and for resources to be accepted into the guest context. + fn tdisp_start_device(&self) -> impl Future> + Send; + + /// Request a device report from the TDI or physical device depending on the report type. + fn tdisp_get_device_report( + &self, + report_type: &TdispReportType, + ) -> impl Future>> + Send; + + /// Request a TDI report from the TDI or physical device. + fn tdisp_get_tdi_report(&self) -> impl Future> + Send; + + /// Request the TDI device id from the vpci channel. + fn tdisp_get_tdi_device_id(&self) -> impl Future> + Send; + + /// Request to unbind the device and return to the Unlocked state. + fn tdisp_unbind( + &self, + reason: TdispGuestUnbindReason, + ) -> impl Future> + Send; +} + +/// Creates a [`GuestToHostCommand`] for the `GetDeviceInterfaceInfo` command. +pub fn make_get_device_interface_info_command( + device_id: u64, + guest_protocol_type: TdispGuestProtocolType, +) -> GuestToHostCommand { + GuestToHostCommand { + device_id, + command: Some(Command::GetDeviceInterfaceInfo( + TdispCommandRequestGetDeviceInterfaceInfo { + guest_protocol_type: guest_protocol_type as i32, + }, + )), + } +} + +/// Creates a [`GuestToHostCommand`] for the `Bind` command. +pub fn make_bind_command(device_id: u64) -> GuestToHostCommand { + GuestToHostCommand { + device_id, + command: Some(Command::Bind(TdispCommandRequestBind {})), + } +} + +/// Creates a [`GuestToHostCommand`] for the `StartTdi` command. +pub fn make_start_tdi_command(device_id: u64) -> GuestToHostCommand { + GuestToHostCommand { + device_id, + command: Some(Command::StartTdi(TdispCommandRequestStartTdi {})), + } +} + +/// Creates a [`GuestToHostCommand`] for the `GetTdiReport` command. +pub fn make_get_tdi_report_command( + device_id: u64, + report_type: TdispReportType, +) -> GuestToHostCommand { + GuestToHostCommand { + device_id, + command: Some(Command::GetTdiReport(TdispCommandRequestGetTdiReport { + report_type: report_type as i32, + })), + } +} + +/// Creates a [`GuestToHostCommand`] for the `Unbind` command. +pub fn make_unbind_command(device_id: u64, reason: TdispGuestUnbindReason) -> GuestToHostCommand { + GuestToHostCommand { + device_id, + command: Some(Command::Unbind(TdispCommandRequestUnbind { + unbind_reason: reason as i32, + })), + } +} diff --git a/openhcl/underhill_core/src/options.rs b/openhcl/underhill_core/src/options.rs index 2113901236..d6b44aae39 100644 --- a/openhcl/underhill_core/src/options.rs +++ b/openhcl/underhill_core/src/options.rs @@ -21,6 +21,9 @@ pub enum TestScenarioConfig { SaveFail, RestoreStuck, SaveStuck, + + /// Exercises a mocked TDISP flow for emulated TDISP devices produced by OpenVMM tests. + VpciTdispFlow, } impl FromStr for TestScenarioConfig { @@ -31,6 +34,7 @@ impl FromStr for TestScenarioConfig { "SERVICING_SAVE_FAIL" => Ok(TestScenarioConfig::SaveFail), "SERVICING_RESTORE_STUCK" => Ok(TestScenarioConfig::RestoreStuck), "SERVICING_SAVE_STUCK" => Ok(TestScenarioConfig::SaveStuck), + "TDISP_VPCI_FLOW_TEST" => Ok(TestScenarioConfig::VpciTdispFlow), _ => Err(anyhow::anyhow!("Invalid test config: {}", s)), } } diff --git a/openhcl/underhill_core/src/worker.rs b/openhcl/underhill_core/src/worker.rs index 7d61326e62..706ea85c02 100644 --- a/openhcl/underhill_core/src/worker.rs +++ b/openhcl/underhill_core/src/worker.rs @@ -3219,6 +3219,13 @@ async fn new_underhill_vm( ) }, vtom, + VpciRelayOptions { + // Exercises a mocked TDISP flow for emulated TDISP devices produced by OpenVMM tests. + test_tdisp_flow: matches!( + env_cfg.test_configuration, + Some(TestScenarioConfig::VpciTdispFlow) + ), + }, ); // Allow NVMe devices. diff --git a/vm/chipset_device/Cargo.toml b/vm/chipset_device/Cargo.toml index 298415d096..e948b36fbf 100644 --- a/vm/chipset_device/Cargo.toml +++ b/vm/chipset_device/Cargo.toml @@ -9,6 +9,7 @@ rust-version.workspace = true [dependencies] mesh.workspace = true inspect.workspace = true +tdisp.workspace = true [lints] workspace = true diff --git a/vm/chipset_device/src/lib.rs b/vm/chipset_device/src/lib.rs index cf4ae4f420..cba9380eca 100644 --- a/vm/chipset_device/src/lib.rs +++ b/vm/chipset_device/src/lib.rs @@ -61,6 +61,13 @@ pub trait ChipsetDevice: 'static + Send /* see DEVNOTE before adding bounds */ { ) -> Option<&mut dyn interrupt::AcknowledgePicInterrupt> { None } + + /// Optionally returns a trait object which implements TDISP host + /// communication. + #[inline(always)] + fn supports_tdisp(&mut self) -> Option<&mut dyn tdisp::TdispHostDeviceTarget> { + None + } } /// Shared by `mmio` and `pio` diff --git a/vm/chipset_device_resources/Cargo.toml b/vm/chipset_device_resources/Cargo.toml index 7ae989e819..4421fb5cc7 100644 --- a/vm/chipset_device_resources/Cargo.toml +++ b/vm/chipset_device_resources/Cargo.toml @@ -9,6 +9,7 @@ rust-version.workspace = true [dependencies] chipset_device.workspace = true guestmem.workspace = true +tdisp.workspace = true vmcore.workspace = true vm_resource.workspace = true diff --git a/vm/chipset_device_resources/src/lib.rs b/vm/chipset_device_resources/src/lib.rs index 519e6e93fe..f8ba3fa07a 100644 --- a/vm/chipset_device_resources/src/lib.rs +++ b/vm/chipset_device_resources/src/lib.rs @@ -173,6 +173,10 @@ impl ChipsetDevice for ErasedChipsetDevice { ) -> Option<&mut dyn chipset_device::interrupt::AcknowledgePicInterrupt> { self.0.supports_acknowledge_pic_interrupt() } + + fn supports_tdisp(&mut self) -> Option<&mut dyn tdisp::TdispHostDeviceTarget> { + self.0.supports_tdisp() + } } impl ProtobufSaveRestore for ErasedChipsetDevice { diff --git a/vm/devices/pci/vpci/Cargo.toml b/vm/devices/pci/vpci/Cargo.toml index dbd2a4b11e..2116542bf6 100644 --- a/vm/devices/pci/vpci/Cargo.toml +++ b/vm/devices/pci/vpci/Cargo.toml @@ -8,6 +8,8 @@ rust-version.workspace = true [dependencies] pci_core.workspace = true +tdisp.workspace = true +openhcl_tdisp.workspace = true vpci_protocol.workspace = true device_emulators.workspace = true @@ -36,5 +38,6 @@ thiserror.workspace = true tracelimit.workspace = true tracing.workspace = true zerocopy.workspace = true + [lints] workspace = true diff --git a/vm/devices/pci/vpci/src/device.rs b/vm/devices/pci/vpci/src/device.rs index 38a7ffb180..7ea9d26818 100644 --- a/vm/devices/pci/vpci/src/device.rs +++ b/vm/devices/pci/vpci/src/device.rs @@ -44,6 +44,7 @@ use vmcore::vpci_msi::RegisterInterruptError; use vmcore::vpci_msi::VpciInterruptMapper; use vmcore::vpci_msi::VpciInterruptParameters; use vpci_protocol as protocol; +use vpci_protocol::MAX_VPCI_TDISP_COMMAND_SIZE; use vpci_protocol::SlotNumber; use zerocopy::FromBytes; use zerocopy::FromZeros; @@ -229,6 +230,8 @@ enum PacketError { RegisterInterrupt(#[source] RegisterInterruptError), #[error("unknown interrupt address {:#x}/data {:#x}", .0.address, .0.data)] UnknownInterrupt(MsiAddressData), + #[error("invalid packet serialization")] + InvalidSerialization(#[source] anyhow::Error), } #[derive(Debug)] @@ -266,6 +269,9 @@ enum DeviceRequest { }, ReleaseResources, Reset, + TdispCommand { + data: Vec, + }, } #[derive(Debug)] @@ -482,6 +488,25 @@ fn parse_packet(packet: &queue::DataPacket<'_, T>) -> Result { + let (header, rest) = Ref::<_, protocol::VpciTdispCommandHeader>::from_prefix(buf) + .map_err(|_| PacketError::PacketTooSmall("tdisp_command_header"))?; // TODO: zerocopy: map_err (https://github.com/microsoft/openvmm/issues/759) + + let data_len = header.data_length as usize; + if data_len > MAX_VPCI_TDISP_COMMAND_SIZE { + return Err(PacketError::PacketTooLarge); + } + + let data = rest + .get(..data_len) + .ok_or(PacketError::PacketTooSmall("tdisp_command_data"))? + .to_vec(); + + PacketData::DeviceRequest { + slot: header.slot, + request: DeviceRequest::TdispCommand { data }, + } + } typ => return Err(PacketError::UnknownType(typ)), }; Ok(data) @@ -883,6 +908,70 @@ impl ReadyState { &[], )?; } + DeviceRequest::TdispCommand { data } => { + let command = match tdisp::serialize_proto::deserialize_command(&data) { + Ok(cmd) => cmd, + Err(err) => { + tracelimit::warn_ratelimited!( + error = err.as_ref() as &dyn std::error::Error, + "failed to deserialize TDISP command" + ); + conn.send_completion( + transaction_id, + &protocol::Status::BAD_DATA, + &[], + )?; + return Ok(()); + } + }; + + tracing::debug!(?command, "received TDISP command over vpci channel"); + + let mut locked_dev = dev.device.lock(); + if let Some(tdisp) = locked_dev.supports_tdisp() { + tracelimit::info_ratelimited!( + "chipset device supports TDISP, handing off command for processing" + ); + let response = tdisp + .tdisp_handle_guest_command(command) + .map_err(PacketError::InvalidSerialization)?; + + tracing::debug!("host interface responded successfully with payload"); + let response_serialized = + tdisp::serialize_proto::serialize_response(&response); + + let response_header = protocol::VpciTdispCommandHeaderReply { + status: protocol::Status::SUCCESS, + slot, + data_length: response_serialized.len() as u64, + }; + + tracing::debug!(?response_header, "guest response"); + tracing::debug!( + response_header_len = response_header.as_bytes().len(), + "guest response header size" + ); + tracing::debug!( + payload_size = response_serialized.len(), + "guest response payload size" + ); + + conn.send_completion( + transaction_id, + &response_header, + response_serialized.as_bytes(), + )?; + } else { + tracelimit::info_ratelimited!( + "chipset device reported that TDISP is not supported, returning NOT_SUPPORTED" + ); + conn.send_completion( + transaction_id, + &protocol::Status::NOT_SUPPORTED, + &[], + )?; + } + } } } } @@ -1328,6 +1417,7 @@ mod tests { use hvdef::HV_PAGE_SIZE; use inspect::Inspect; use inspect::InspectMut; + use openhcl_tdisp::make_get_device_interface_info_command; use pal_async::DefaultDriver; use pal_async::async_test; use pal_async::driver::SpawnDriver; @@ -1345,6 +1435,12 @@ mod tests { use std::sync::Arc; use std::sync::atomic::AtomicU64; use std::sync::atomic::Ordering; + use tdisp::GuestToHostResponseExt; + use tdisp::TdispCommandResponseGetDeviceInterfaceInfo; + use tdisp::TdispHostDeviceTargetEmulator; + use tdisp::test_helpers::TDISP_MOCK_DEVICE_ID; + use tdisp::test_helpers::TDISP_MOCK_GUEST_PROTOCOL; + use tdisp::test_helpers::TDISP_MOCK_SUPPORTED_FEATURES; use test_with_tracing::test; use thiserror::Error; use vmbus_async::queue::IncomingPacket; @@ -1480,6 +1576,28 @@ mod tests { .map_err(GuestError::Queue) } + async fn write_packet_with_header( + &mut self, + transaction_id: Option, + header: &T, + extra: &[u8], + ) -> Result<(), GuestError> { + self.host_queue + .split() + .1 + .write(OutgoingPacket { + transaction_id: transaction_id.unwrap_or(0), + packet_type: if transaction_id.is_some() { + OutgoingPacketType::InBandWithCompletion + } else { + OutgoingPacketType::InBandNoCompletion + }, + payload: &[header.as_bytes(), extra], + }) + .await + .map_err(GuestError::Queue) + } + async fn negotiate_version(&mut self) { if let Err(vsp_version) = self.try_negotiate_version().await { self.protocol_version = vsp_version; @@ -1607,6 +1725,62 @@ mod tests { assert_eq!(reply.interrupt.message_count, 1); (reply.interrupt.address, reply.interrupt.data_payload) } + + /// Serializes `command` to a `VPCI_TDISP_COMMAND` vmbus packet, sends it + /// to the server requesting a completion, then reads the completion and + /// deserializes the payload back to a [`tdisp::GuestToHostResponse`]. + async fn send_tdisp_command( + &mut self, + command: tdisp::GuestToHostCommand, + ) -> tdisp::GuestToHostResponse { + let serialized = tdisp::serialize_proto::serialize_command(&command); + + let header = protocol::VpciTdispCommandHeader { + message_type: protocol::MessageType::VPCI_TDISP_COMMAND, + slot: SlotNumber::new(), + data_length: serialized.len() as u64, + }; + let transaction_id = self.transaction_id.fetch_add(1, Ordering::Relaxed); + self.write_packet_with_header(Some(transaction_id), &header, serialized.as_bytes()) + .await + .unwrap(); + + let mut queue = self.host_queue.split().0; + let packet = queue.read().await.map_err(GuestError::Queue).unwrap(); + match &*packet { + IncomingPacket::Completion(completion) => { + assert_eq!(completion.transaction_id(), transaction_id); + + // Read the entire completion payload at once before splitting it. + let mut reader = completion.reader(); + let mut all_bytes = vec![0u8; reader.len()]; + reader.read(&mut all_bytes).unwrap(); + + let (reply_header, proto_bytes) = + protocol::VpciTdispCommandHeaderReply::read_from_prefix(&all_bytes) + .expect("completion payload too small to contain status"); + + assert_eq!( + reply_header.status, + protocol::Status::SUCCESS, + "tdisp command completion returned non-success status" + ); + + tracing::debug!( + reply_header_size = reply_header.as_bytes().len(), + "completion header size" + ); + tracing::debug!(payload_size = proto_bytes.len(), "completion payload size"); + + // Read only data_length bytes from the payload. + let proto_bytes_shaved = &proto_bytes[..reply_header.data_length as usize]; + + tdisp::serialize_proto::deserialize_response(proto_bytes_shaved) + .expect("failed to deserialize GuestToHostResponse") + } + _ => panic!("unexpected incoming packet type"), + } + } } struct NullDevice { @@ -1797,14 +1971,14 @@ mod tests { assert_eq!(value, 0x20); } - #[async_test] - async fn verify_simple_device_registers(driver: DefaultDriver) { - let msi_controller = TestVpciInterruptController::new(); - - struct TestDevice(ConfigSpaceType0Emulator); - impl TestDevice { - fn new(register_mmio: &mut dyn RegisterMmioIntercept) -> Self { - Self(ConfigSpaceType0Emulator::new( + struct TestDevice { + config_space: ConfigSpaceType0Emulator, + tdisp_interface: TdispHostDeviceTargetEmulator, + } + impl TestDevice { + fn new(register_mmio: &mut dyn RegisterMmioIntercept) -> Self { + Self { + config_space: ConfigSpaceType0Emulator::new( HardwareIds { vendor_id: 0x123, device_id: 0x789, @@ -1825,90 +1999,100 @@ mod tests { 0x2000, BarMemoryKind::Intercept(register_mmio.new_io_region("bar2", 0x2000)), ), - )) + ), + tdisp_interface: tdisp::test_helpers::make_null_tdisp_interface("vpci-unit-test"), } + } - fn read_bar_u32(&self, bar: u8, offset: u16) -> u32 { - if bar == 0 && offset == 0 { - 1 - } else if bar == 0 && offset == 4 { - 2 - } else if bar == 2 && offset == 0 { - 3 - } else if bar == 2 && offset == HV_PAGE_SIZE as u16 { - 4 - } else { - panic!("Unexpected address {}/{:#x}", bar, offset); - } + fn read_bar_u32(&self, bar: u8, offset: u16) -> u32 { + if bar == 0 && offset == 0 { + 1 + } else if bar == 0 && offset == 4 { + 2 + } else if bar == 2 && offset == 0 { + 3 + } else if bar == 2 && offset == HV_PAGE_SIZE as u16 { + 4 + } else { + panic!("Unexpected address {}/{:#x}", bar, offset); } + } - fn write_bar_u32(&mut self, bar: u8, offset: u16, val: u32) { - if bar == 0 && offset == 0 { - assert_eq!(val, 1); - } else if bar == 0 && offset == 4 { - assert_eq!(val, 2); - } else if bar == 2 && offset == 0 { - assert_eq!(val, 3); - } else if bar == 2 && offset == HV_PAGE_SIZE as u16 { - assert_eq!(val, 4); - } else { - panic!("Unexpected address {}/{:#x}", bar, offset); - } + fn write_bar_u32(&mut self, bar: u8, offset: u16, val: u32) { + if bar == 0 && offset == 0 { + assert_eq!(val, 1); + } else if bar == 0 && offset == 4 { + assert_eq!(val, 2); + } else if bar == 2 && offset == 0 { + assert_eq!(val, 3); + } else if bar == 2 && offset == HV_PAGE_SIZE as u16 { + assert_eq!(val, 4); + } else { + panic!("Unexpected address {}/{:#x}", bar, offset); } } + } - impl InspectMut for TestDevice { - fn inspect_mut(&mut self, req: inspect::Request<'_>) { - req.ignore(); - } + impl InspectMut for TestDevice { + fn inspect_mut(&mut self, req: inspect::Request<'_>) { + req.ignore(); } + } - impl Inspect for TestDevice { - fn inspect(&self, req: inspect::Request<'_>) { - req.ignore(); - } + impl Inspect for TestDevice { + fn inspect(&self, req: inspect::Request<'_>) { + req.ignore(); } + } - impl ChipsetDevice for TestDevice { - fn supports_mmio(&mut self) -> Option<&mut dyn MmioIntercept> { - Some(self) - } + impl ChipsetDevice for TestDevice { + fn supports_mmio(&mut self) -> Option<&mut dyn MmioIntercept> { + Some(self) + } - fn supports_pci(&mut self) -> Option<&mut dyn PciConfigSpace> { - Some(self) - } + fn supports_pci(&mut self) -> Option<&mut dyn PciConfigSpace> { + Some(self) } - impl MmioIntercept for TestDevice { - fn mmio_read(&mut self, address: u64, data: &mut [u8]) -> IoResult { - if let Some((bar, offset)) = self.0.find_bar(address) { - read_as_u32_chunks(offset, data, |offset| self.read_bar_u32(bar, offset)) - } - IoResult::Ok - } + fn supports_tdisp(&mut self) -> Option<&mut dyn tdisp::TdispHostDeviceTarget> { + Some(&mut self.tdisp_interface) + } + } - fn mmio_write(&mut self, address: u64, data: &[u8]) -> IoResult { - if let Some((bar, offset)) = self.0.find_bar(address) { - write_as_u32_chunks(offset, data, |offset, request_type| match request_type { - ReadWriteRequestType::Write(value) => { - self.write_bar_u32(bar, offset, value); - None - } - ReadWriteRequestType::Read => Some(self.read_bar_u32(bar, offset)), - }) - } - IoResult::Ok + impl MmioIntercept for TestDevice { + fn mmio_read(&mut self, address: u64, data: &mut [u8]) -> IoResult { + if let Some((bar, offset)) = self.config_space.find_bar(address) { + read_as_u32_chunks(offset, data, |offset| self.read_bar_u32(bar, offset)) } + IoResult::Ok } - impl PciConfigSpace for TestDevice { - fn pci_cfg_read(&mut self, offset: u16, value: &mut u32) -> IoResult { - self.0.read_u32(offset, value) - } - fn pci_cfg_write(&mut self, offset: u16, value: u32) -> IoResult { - self.0.write_u32(offset, value) + fn mmio_write(&mut self, address: u64, data: &[u8]) -> IoResult { + if let Some((bar, offset)) = self.config_space.find_bar(address) { + write_as_u32_chunks(offset, data, |offset, request_type| match request_type { + ReadWriteRequestType::Write(value) => { + self.write_bar_u32(bar, offset, value); + None + } + ReadWriteRequestType::Read => Some(self.read_bar_u32(bar, offset)), + }) } + IoResult::Ok + } + } + + impl PciConfigSpace for TestDevice { + fn pci_cfg_read(&mut self, offset: u16, value: &mut u32) -> IoResult { + self.config_space.read_u32(offset, value) + } + fn pci_cfg_write(&mut self, offset: u16, value: u32) -> IoResult { + self.config_space.write_u32(offset, value) } + } + + #[async_test] + async fn verify_simple_device_registers(driver: DefaultDriver) { + let msi_controller = TestVpciInterruptController::new(); let vm_chipset = TestChipset::default(); let pci = vm_chipset @@ -1968,6 +2152,53 @@ mod tests { write_u32(bar_address2 + HV_PAGE_SIZE, 4); } + /// Verifies that the TDISP guest protocol can be negotiated correctly over a hosted VMBUS channel. + /// This test covers: + /// - Some basic VMBUS VPCI packet serialization for VpciTdispCommand + /// - VPCI VMBUS server interface receiving and responding to TDISP commands + #[async_test] + async fn verify_tdisp_get_device_interface_info(driver: DefaultDriver) { + let msi_controller = TestVpciInterruptController::new(); + let vm_chipset = TestChipset::default(); + let pci = vm_chipset + .device_builder("test") + .with_external_pci() + .add(|services| TestDevice::new(&mut services.register_mmio())) + .unwrap(); + let mut guest_driver = connected_device(&driver, pci.clone(), msi_controller); + guest_driver.start_device(0x1000000).await; + + let guest_protocol_type: tdisp::TdispGuestProtocolType = TDISP_MOCK_GUEST_PROTOCOL; + let command = make_get_device_interface_info_command( + SlotNumber::new().into_bits() as u64, + TDISP_MOCK_GUEST_PROTOCOL, + ); + let response = guest_driver.send_tdisp_command(command).await; + + let response = response.get_response::(); + match response { + Ok(info_resp) => { + let interface_info = info_resp + .interface_info + .expect("interface_info must be set"); + + assert_eq!( + interface_info.guest_protocol_type, + guest_protocol_type as i32 + ); + assert_eq!( + interface_info.supported_features, + TDISP_MOCK_SUPPORTED_FEATURES + ); + assert_eq!(interface_info.tdisp_device_id, TDISP_MOCK_DEVICE_ID); + } + _ => panic!( + "expected GetDeviceInterfaceInfo response, got {:?}", + response + ), + } + } + #[async_test] async fn verify_simple_device_interrupt(driver: DefaultDriver) { let msi_controller = TestVpciInterruptController::new(); diff --git a/vm/devices/pci/vpci_client/Cargo.toml b/vm/devices/pci/vpci_client/Cargo.toml index 698d453bee..651e0eed8f 100644 --- a/vm/devices/pci/vpci_client/Cargo.toml +++ b/vm/devices/pci/vpci_client/Cargo.toml @@ -7,7 +7,9 @@ rust-version.workspace = true edition.workspace = true [dependencies] +openhcl_tdisp.workspace = true pci_core.workspace = true +tdisp.workspace = true vpci_protocol.workspace = true vmbus_async.workspace = true vmbus_channel.workspace = true diff --git a/vm/devices/pci/vpci_client/src/lib.rs b/vm/devices/pci/vpci_client/src/lib.rs index 21b5b14555..2a532385e3 100644 --- a/vm/devices/pci/vpci_client/src/lib.rs +++ b/vm/devices/pci/vpci_client/src/lib.rs @@ -19,6 +19,21 @@ use inspect::Inspect; use inspect::InspectMut; use mesh::rpc::FailableRpc; use mesh::rpc::RpcSend; +use openhcl_tdisp::GuestToHostCommand; +use openhcl_tdisp::GuestToHostCommandExt; +use openhcl_tdisp::GuestToHostResponse; +use openhcl_tdisp::GuestToHostResponseExt; +use openhcl_tdisp::TdispCommandResponseBind; +use openhcl_tdisp::TdispCommandResponseGetDeviceInterfaceInfo; +use openhcl_tdisp::TdispCommandResponseGetTdiReport; +use openhcl_tdisp::TdispCommandResponseStartTdi; +use openhcl_tdisp::TdispCommandResponseUnbind; +use openhcl_tdisp::TdispDeviceInterfaceInfo; +use openhcl_tdisp::TdispGuestOperationErrorCode; +use openhcl_tdisp::TdispGuestProtocolType; +use openhcl_tdisp::TdispGuestUnbindReason; +use openhcl_tdisp::TdispReportType; +use openhcl_tdisp::TdispVirtualDeviceInterface; use pal_async::task::Spawn; use pal_async::task::Task; use parking_lot::Mutex; @@ -28,6 +43,7 @@ use pci_core::spec::hwid::HardwareIds; use std::pin::Pin; use std::sync::Arc; use std::task::Poll; +use tdisp::devicereport::TdiReportStruct; use thiserror::Error; use vmbus_async::queue::IncomingPacket; use vmbus_async::queue::OutgoingPacket; @@ -38,6 +54,7 @@ use vmcore::vpci_msi::MapVpciInterrupt; use vmcore::vpci_msi::MsiAddressData; use vmcore::vpci_msi::RegisterInterruptError; use vpci_protocol as protocol; +use vpci_protocol::MAX_VPCI_TDISP_COMMAND_SIZE; use vpci_protocol::SlotNumber; use zerocopy::FromBytes; use zerocopy::FromZeros; @@ -70,6 +87,7 @@ enum WorkerRequest { QueryResourceRequirements(FailableRpc), Init(FailableRpc), Done(DeviceId), + TdispCommand(FailableRpc), } #[derive(Debug, Copy, Clone, Inspect)] @@ -576,6 +594,173 @@ impl MapVpciInterrupt for VpciDevice { } } +impl TdispVirtualDeviceInterface for VpciDevice { + async fn send_tdisp_command( + &self, + payload: GuestToHostCommand, + ) -> Result { + let serialized = openhcl_tdisp::serialize_command(&payload); + + // Ensure that the length does not exceed the VMBUS maximum packet size. + // This shouldn't be possible since the host should reject the command anyways, + // but fail earlier for safety. + if serialized.len() > MAX_VPCI_TDISP_COMMAND_SIZE { + return Err(anyhow::anyhow!( + "serialized TDISP command exceeds VMBUS maximum packet size ({} > {})", + serialized.len(), + MAX_VPCI_TDISP_COMMAND_SIZE + )); + } + + // Make a mesh call to send the VMBUS packet to the host and await a response + // packet from the host. + let res = self + .dev + .req + .call_failable( + WorkerRequest::TdispCommand, + protocol::VpciTdispCommand { + header: protocol::VpciTdispCommandHeader { + message_type: protocol::MessageType::VPCI_TDISP_COMMAND, + slot: self.dev.id.slot, + data_length: serialized.len() as u64, + }, + data: serialized, + }, + ) + .await + .map_err(|err: mesh::rpc::RpcError| { + tracing::error!( + error = &err as &dyn std::error::Error, + "failed to send tdisp command" + ); + anyhow::anyhow!("failed to send tdisp command") + })?; + + match res.get_error_code() { + Some(TdispGuestOperationErrorCode::Success) => Ok(res), + _ => { + let err_msg = format!( + "send_tdisp_command {:?} failed because host responded with an error: {:?}", + payload.get_type_name(), + res.result + ); + + tracing::error!(msg = err_msg); + Err(anyhow::anyhow!(err_msg)) + } + } + } + + async fn tdisp_get_device_interface_info(&self) -> anyhow::Result { + // TDISP TODO: Configure the correct guest protocol type when TDX support is added. + let target_protocol_type = TdispGuestProtocolType::AmdSevTioV1; + + let res = self + .send_tdisp_command(openhcl_tdisp::make_get_device_interface_info_command( + self.dev.id.slot.into_bits() as u64, + target_protocol_type, + )) + .await?; + + match res.get_response::() { + Ok(info) => info.interface_info.ok_or_else(|| { + anyhow::anyhow!("missing interface_info after validation, this should never happen") + }), + Err(err) => Err(anyhow::anyhow!( + "error response in get_device_interface_info: {err}" + )), + } + } + + async fn tdisp_bind_interface(&self) -> anyhow::Result<()> { + let res = self + .send_tdisp_command(openhcl_tdisp::make_bind_command( + self.dev.id.slot.into_bits() as u64, + )) + .await?; + + match res.get_response::() { + Ok(_) => Ok(()), + Err(err) => Err(anyhow::anyhow!( + "error response in tdisp_bind_interface: {err}" + )), + } + } + + async fn tdisp_start_device(&self) -> anyhow::Result<()> { + let res = self + .send_tdisp_command(openhcl_tdisp::make_start_tdi_command( + self.dev.id.slot.into_bits() as u64, + )) + .await?; + + match res.get_response::() { + Ok(_) => Ok(()), + Err(err) => Err(anyhow::anyhow!( + "error response in tdisp_start_device: {err}" + )), + } + } + + async fn tdisp_get_device_report( + &self, + report_type: &TdispReportType, + ) -> anyhow::Result> { + let res = self + .send_tdisp_command(openhcl_tdisp::make_get_tdi_report_command( + self.dev.id.slot.into_bits() as u64, + *report_type, + )) + .await?; + + match res.get_response::() { + Ok(r) => Ok(r.report_buffer), + Err(err) => Err(anyhow::anyhow!( + "error response in tdisp_get_device_report: {err}" + )), + } + } + + async fn tdisp_get_tdi_report(&self) -> anyhow::Result { + let buffer = self + .tdisp_get_device_report(&TdispReportType::InterfaceReport) + .await + .context("failed to get TDI report")?; + + tdisp::devicereport::deserialize_tdi_report(&buffer) + .context("failed to deserialize TDI report from host") + } + + async fn tdisp_get_tdi_device_id(&self) -> anyhow::Result { + let buffer = self + .tdisp_get_device_report(&TdispReportType::GuestDeviceId) + .await + .context("failed to get TDI device ID")?; + + // Ensure it's a u64 + if buffer.len() != size_of::() { + return Err(anyhow::anyhow!("unexpected buffer size for TDI device ID")); + } + + Ok(u64::from_le_bytes(buffer.try_into().unwrap())) + } + + async fn tdisp_unbind(&self, reason: TdispGuestUnbindReason) -> anyhow::Result<()> { + let res = self + .send_tdisp_command(openhcl_tdisp::make_unbind_command( + self.dev.id.slot.into_bits() as u64, + reason, + )) + .await?; + + match res.get_response::() { + Ok(_) => Ok(()), + Err(err) => Err(anyhow::anyhow!("error response in tdisp_unbind: {err}")), + } + } +} + #[derive(InspectMut)] struct VpciClientWorker { conn: VpciConnection, @@ -627,6 +812,7 @@ enum Tx { #[inspect(skip)] FailableRpc<(), protocol::QueryResourceRequirementsReply>, ), AssignedResources(#[inspect(skip)] FailableRpc<(), ()>), + TdispCommand(#[inspect(skip)] FailableRpc<(), GuestToHostResponse>), } impl VpciClient { @@ -995,6 +1181,53 @@ impl WorkerState { )); } } + Tx::TdispCommand(rpc) => { + if status == protocol::Status::SUCCESS { + let mut reader = p.reader(); + + if reader.len() == 0 { + rpc.fail(anyhow::anyhow!("Unexpected empty response from host")); + return Ok(()); + } + + let header = reader + .read_plain::() + .context("failed to read tdisp command header")?; + + let data_len = header.data_length as usize; + if data_len > MAX_VPCI_TDISP_COMMAND_SIZE { + rpc.fail(anyhow::anyhow!( + "Received TdispCommand data length exceeds maximum allowed: {} > {}", + data_len, + MAX_VPCI_TDISP_COMMAND_SIZE + )); + return Ok(()); + } + + // Allocate a mutable vector with the correct size + let mut data: Vec = vec![0; data_len]; + + // Read data_len bytes from start_of_data into Vec + reader + .read(data.as_mut_slice()) + .context("failed to read tdisp command data")?; + + let host_response = openhcl_tdisp::deserialize_response(data.as_slice()) + .context("failed to deserialize tdisp response"); + + rpc.complete(host_response.map_err(mesh::error::RemoteError::new)); + } else { + if status == protocol::Status::NOT_SUPPORTED { + rpc.fail(anyhow::anyhow!( + "TDISP interface is not supported by this device or host" + )); + } else { + rpc.fail(anyhow::anyhow!( + "vmbus server responded error status: {status:#x?}", + )); + } + } + } } Ok(()) } @@ -1093,6 +1326,17 @@ impl WorkerState { send_eject_complete(write, id.slot).await?; } } + WorkerRequest::TdispCommand(rpc) => { + let (req, reply) = rpc.split(); + self.send_tx( + write, + Tx::TdispCommand(reply), + req.header, + req.data.as_slice(), + ) + .await + .context("failed to send tdisp command message")?; + } } Ok(None) } diff --git a/vm/devices/pci/vpci_client/src/tests.rs b/vm/devices/pci/vpci_client/src/tests.rs index 44d72d0cbf..1756bd121d 100644 --- a/vm/devices/pci/vpci_client/src/tests.rs +++ b/vm/devices/pci/vpci_client/src/tests.rs @@ -12,11 +12,17 @@ use chipset_device::pci::PciConfigSpace; use closeable_mutex::CloseableMutex; use guestmem::GuestMemory; use guid::Guid; +use openhcl_tdisp::TdispVirtualDeviceInterface; use pal_async::DefaultDriver; use pal_async::async_test; use pal_async::task::Spawn; use std::sync::Arc; use task_control::StopTask; +use tdisp::TdispHostDeviceTargetEmulator; +use tdisp::test_helpers::TDISP_MOCK_DEVICE_ID; +use tdisp::test_helpers::TDISP_MOCK_GUEST_PROTOCOL; +use tdisp::test_helpers::TDISP_MOCK_SUPPORTED_FEATURES; +use tdisp::test_helpers::make_null_tdisp_interface; use test_with_tracing::test; use vmbus_channel::simple::SimpleVmbusDevice; use vmcore::vpci_msi::MapVpciInterrupt; @@ -26,12 +32,18 @@ use vmcore::vpci_msi::VpciInterruptParameters; use vpci::bus::VpciBusDevice; use vpci::test_helpers::TestVpciInterruptController; -struct NoopDevice; +struct NoopDevice { + tdisp_interface: TdispHostDeviceTargetEmulator, +} impl ChipsetDevice for NoopDevice { fn supports_pci(&mut self) -> Option<&mut dyn PciConfigSpace> { Some(self) } + + fn supports_tdisp(&mut self) -> Option<&mut dyn tdisp::TdispHostDeviceTarget> { + Some(&mut self.tdisp_interface) + } } impl PciConfigSpace for NoopDevice { @@ -71,9 +83,15 @@ impl super::MemoryAccess for BusWrapper { } } +fn make_noop_device() -> Arc> { + Arc::new(CloseableMutex::new(NoopDevice { + tdisp_interface: make_null_tdisp_interface("vpci-unit-test"), + })) +} + #[async_test] async fn test_negotiate_version(driver: DefaultDriver) { - let device = Arc::new(CloseableMutex::new(NoopDevice)); + let device = make_noop_device(); let msi_controller = TestVpciInterruptController::new(); let (bus, mut channel) = VpciBusDevice::new( Guid::new_random(), @@ -116,3 +134,54 @@ async fn test_negotiate_version(driver: DefaultDriver) { device.unregister_interrupt(address, data).await; } + +/// Tests that VPCI can negotiate basic TDISP commands with a device. +/// This test covers: +/// - VMBUS VPCI packet serialization for VpciTdispCommand +/// - TDISP command serialization +/// - VPCI VMBUS server interface receiving and responding to TDISP commands +/// - VPCI VMBUS client interface sending and receiving TDISP commands +/// - Basic TDISP state machine processing +#[async_test] +async fn test_tdisp_interface_get_device_interface_info(driver: DefaultDriver) { + let device = make_noop_device(); + let msi_controller = TestVpciInterruptController::new(); + let (bus, mut channel) = VpciBusDevice::new( + Guid::new_random(), + device, + &mut ExternallyManagedMmioIntercepts, + VpciInterruptMapper::new(msi_controller), + None, + ) + .unwrap(); + + let (host, guest) = vmbus_channel::connected_async_channels(32768); + + let mut runner = channel.open(host, GuestMemory::empty()).unwrap(); + let _task = driver.spawn("server", async move { + StopTask::run_with(std::future::pending(), async |stop| { + let _ = channel.run(stop, &mut runner).await; + }) + .await + }); + + let (_client, devices) = + super::VpciClient::connect(&driver, guest, Box::new(BusWrapper(bus)), mesh::channel().0) + .await + .unwrap(); + + let (device, _removed) = devices.into_iter().next().unwrap().init().await.unwrap(); + let interface = device.tdisp_get_device_interface_info().await; + + match interface { + Ok(interface) => { + assert_eq!( + interface.guest_protocol_type, + TDISP_MOCK_GUEST_PROTOCOL as i32 + ); + assert_eq!(interface.supported_features, TDISP_MOCK_SUPPORTED_FEATURES); + assert_eq!(interface.tdisp_device_id, TDISP_MOCK_DEVICE_ID); + } + Err(err) => panic!("unexpected error: {err}"), + } +} diff --git a/vm/devices/pci/vpci_protocol/src/lib.rs b/vm/devices/pci/vpci_protocol/src/lib.rs index 91d6a456cd..e10a292260 100644 --- a/vm/devices/pci/vpci_protocol/src/lib.rs +++ b/vm/devices/pci/vpci_protocol/src/lib.rs @@ -106,6 +106,8 @@ open_enum! { CREATE_INTERRUPT3 = 0x4249001b, /// Reset a device RESET_DEVICE = 0x4249001c, + /// TDISP command from guest to host + VPCI_TDISP_COMMAND = 0x4249001D, } } @@ -806,3 +808,45 @@ pub struct PdoMessage { /// PCI slot number of the target device pub slot: SlotNumber, } + +/// A TDISP packet being sent to the host. +#[repr(C)] +#[derive(Debug, Copy, Clone, IntoBytes, Immutable, KnownLayout, FromBytes)] +pub struct VpciTdispCommandHeader { + /// Type of message (must be VPCI_TDISP_COMMAND) + pub message_type: MessageType, + /// PCI slot number of the target device + pub slot: SlotNumber, + /// Length of the data payload to follow + pub data_length: u64, + // + // pub data: [u8; data_length...], +} + +/// A TDISP packet response from the host to the guest. +#[repr(C)] +#[derive(Debug, Copy, Clone, IntoBytes, Immutable, KnownLayout, FromBytes)] +pub struct VpciTdispCommandHeaderReply { + /// Status of the translation operation + pub status: Status, + /// PCI slot number of the target device + pub slot: SlotNumber, + /// Length of the data payload to follow + pub data_length: u64, + // + // pub data: [u8; data_length...], +} + +/// A serialized TDISP VPCI VMBUS command packet. +#[derive(Debug, Clone)] +pub struct VpciTdispCommand { + /// Header of the VMBUS packet + pub header: VpciTdispCommandHeader, + + /// The payload of the command (serialized to Vec) + pub data: Vec, +} + +/// Maximum size of a TDISP command in bytes. Property of the VMBUS implementation on the host. +pub const MAX_VPCI_TDISP_COMMAND_SIZE: usize = + MAXIMUM_PACKET_SIZE - size_of::(); diff --git a/vm/devices/pci/vpci_relay/Cargo.toml b/vm/devices/pci/vpci_relay/Cargo.toml index cfbc1a7b50..5232fd72e5 100644 --- a/vm/devices/pci/vpci_relay/Cargo.toml +++ b/vm/devices/pci/vpci_relay/Cargo.toml @@ -13,6 +13,7 @@ pci_core.workspace = true sparse_mmap.workspace = true state_unit.workspace = true tdisp.workspace = true +openhcl_tdisp.workspace = true user_driver.workspace = true vpci_client.workspace = true vpci.workspace = true diff --git a/vm/devices/pci/vpci_relay/src/lib.rs b/vm/devices/pci/vpci_relay/src/lib.rs index a8fe562d2e..083a61c1d1 100644 --- a/vm/devices/pci/vpci_relay/src/lib.rs +++ b/vm/devices/pci/vpci_relay/src/lib.rs @@ -24,6 +24,7 @@ use futures::StreamExt as _; use inspect::Inspect; use inspect::InspectMut; use memory_range::MemoryRange; +use openhcl_tdisp::TdispVirtualDeviceInterface; use pci_core::spec::hwid::HardwareIds; use state_unit::StateUnits; use std::future::poll_fn; @@ -49,6 +50,9 @@ use vpci_client::VpciDeviceEject; /// TODO TDISP: Required for the tdisp crate to be built in the meantime. #[expect(unused_imports)] use tdisp::TdispHostDeviceInterface; +use tdisp::test_helpers::TDISP_MOCK_DEVICE_ID; +use tdisp::test_helpers::TDISP_MOCK_GUEST_PROTOCOL; +use tdisp::test_helpers::TDISP_MOCK_SUPPORTED_FEATURES; /// Trait for creating memory access instances. pub trait CreateMemoryAccess: 'static + Send + Sync { @@ -59,6 +63,14 @@ pub trait CreateMemoryAccess: 'static + Send + Sync { /// The size of the MMIO region required for each VPCI device. pub const VPCI_RELAY_MMIO_PER_DEVICE: u64 = vpci_client::MMIO_SIZE; +/// Flags for controlling optional behavior of the VPCI relay. +#[derive(Inspect, Debug, Default, Copy, Clone)] +pub struct VpciRelayOptions { + /// When set, the relay will exercise a mock TDISP flow for emulated TDISP + /// devices produced by OpenVMM tests. + pub test_tdisp_flow: bool, +} + /// Virtual PCI relay. #[derive(Inspect)] pub struct VpciRelay { @@ -80,6 +92,7 @@ pub struct VpciRelay { allowed_devices: Vec, #[inspect(hex)] vtom: Option, + options: VpciRelayOptions, } #[derive(Inspect)] @@ -164,6 +177,7 @@ impl VpciRelay { mmio_range: MemoryRange, mmio_access: Box, vtom: Option, + options: VpciRelayOptions, ) -> Self { Self { driver_source, @@ -176,6 +190,7 @@ impl VpciRelay { mmio_access, allowed_devices: Vec::new(), vtom, + options, } } @@ -299,6 +314,12 @@ impl VpciRelay { .context("failed to initialize vpci device")?; let vpci_device = Arc::new(vpci_device); + if self.options.test_tdisp_flow { + Self::tdisp_test_mock_flow(vpci_device.clone()) + .await + .expect("failed to exercise TDISP flow test"); + } + let device_name = format!("assigned_device:vpci-{instance_id}"); let (device_unit, device) = chipset .add_dyn_device(&self.driver_source, state_units, device_name, async |_| { @@ -345,6 +366,39 @@ impl VpciRelay { state_units.start_stopped_units().await; Ok(()) } + + /// Exercises a mocked TDISP flow for emulated TDISP devices produced by OpenVMM tests. + /// Configured with the OPENHCL_TEST_CONFIG=TDISP_VPCI_FLOW_TEST environment variable. + async fn tdisp_test_mock_flow(device: Arc) -> anyhow::Result<()> { + // For now, exercise just the "get device interface" flow and ensure that the device responds as + // TDISP capable and with the right mocked device information. + + tracing::info!( + "tdisp_test_mock_flow: exercising TDISP flow because OPENHCL_TEST_CONFIG=TDISP_VPCI_FLOW_TEST was set" + ); + + let device_interface_info = device + .tdisp_get_device_interface_info() + .await + .context("tdisp_test_mock_flow: failed to get device interface info over vpci")?; + + tracing::info!( + "tdisp_test_mock_flow: device interface info: {:?}", + device_interface_info + ); + + assert_eq!( + device_interface_info.guest_protocol_type, + TDISP_MOCK_GUEST_PROTOCOL as i32 + ); + assert_eq!(device_interface_info.tdisp_device_id, TDISP_MOCK_DEVICE_ID); + assert_eq!( + device_interface_info.supported_features, + TDISP_MOCK_SUPPORTED_FEATURES + ); + + Ok(()) + } } #[derive(InspectMut)] diff --git a/vm/devices/storage/disk_nvme/nvme_driver/src/tests.rs b/vm/devices/storage/disk_nvme/nvme_driver/src/tests.rs index 94ff743bce..e5df5ec20b 100644 --- a/vm/devices/storage/disk_nvme/nvme_driver/src/tests.rs +++ b/vm/devices/storage/disk_nvme/nvme_driver/src/tests.rs @@ -470,6 +470,7 @@ async fn test_nvme_fault_injection(driver: DefaultDriver, fault_configuration: F subsystem_id: Guid::new_random(), }, fault_configuration, + None, ); nvme.client() // 2MB namespace diff --git a/vm/devices/storage/nvme_resources/src/fault.rs b/vm/devices/storage/nvme_resources/src/fault.rs index 319742395a..8fcad86766 100644 --- a/vm/devices/storage/nvme_resources/src/fault.rs +++ b/vm/devices/storage/nvme_resources/src/fault.rs @@ -141,6 +141,7 @@ pub struct PciFaultConfig { /// // Define `NamespaceDefinitions` here /// ], /// fault_config: fault_configuration, +/// enable_tdisp_tests: false, /// }; /// /// // Send the namespace change notification and await processing. @@ -368,6 +369,7 @@ pub struct CommandMatch { /// // Define NamespaceDefinitions here /// ], /// fault_config: fault_configuration, +/// enable_tdisp_tests: false, /// }; /// // Pass the controller handle in to the vm config to create and attach the fault controller. At this point the fault is inactive. /// fault_start_updater.set(true); // Activate the fault injection. diff --git a/vm/devices/storage/nvme_resources/src/lib.rs b/vm/devices/storage/nvme_resources/src/lib.rs index feace060fd..fc384ebb5b 100644 --- a/vm/devices/storage/nvme_resources/src/lib.rs +++ b/vm/devices/storage/nvme_resources/src/lib.rs @@ -57,6 +57,8 @@ pub struct NvmeFaultControllerHandle { pub namespaces: Vec, /// Configuration for the fault pub fault_config: FaultConfiguration, + /// Enable TDISP testing on this device when presented by a TDISP host. + pub enable_tdisp_tests: bool, } impl ResourceId for NvmeFaultControllerHandle { diff --git a/vm/devices/storage/nvme_test/Cargo.toml b/vm/devices/storage/nvme_test/Cargo.toml index 1e0f49d914..bce8004032 100644 --- a/vm/devices/storage/nvme_test/Cargo.toml +++ b/vm/devices/storage/nvme_test/Cargo.toml @@ -16,6 +16,7 @@ scsi_buffers.workspace = true device_emulators.workspace = true pci_core.workspace = true pci_resources.workspace = true +tdisp.workspace = true chipset_device.workspace = true guestmem.workspace = true diff --git a/vm/devices/storage/nvme_test/src/pci.rs b/vm/devices/storage/nvme_test/src/pci.rs index d66464812a..d8c8b22657 100644 --- a/vm/devices/storage/nvme_test/src/pci.rs +++ b/vm/devices/storage/nvme_test/src/pci.rs @@ -43,6 +43,7 @@ use pci_core::spec::hwid::HardwareIds; use pci_core::spec::hwid::ProgrammingInterface; use pci_core::spec::hwid::Subclass; use std::sync::Arc; +use tdisp::TdispHostDeviceTarget; use vmcore::device_state::ChangeDeviceState; use vmcore::save_restore::SaveError; use vmcore::save_restore::SaveRestore; @@ -64,6 +65,9 @@ pub struct NvmeFaultController { pci_fault_config: PciFaultConfig, #[inspect(skip)] fault_active: mesh::Cell, + /// The NVMe fault controller is repurposed for use in TDISP tests. + #[inspect(skip)] + tdisp_interface: Option>, } #[derive(Inspect)] @@ -120,6 +124,7 @@ impl NvmeFaultController { register_mmio: &mut dyn RegisterMmioIntercept, caps: NvmeFaultControllerCaps, mut fault_configuration: FaultConfiguration, + tdisp_interface: Option>, ) -> Self { let (msix, msix_cap) = MsixEmulator::new(4, caps.msix_count, msi_target); let bars = DeviceBars::new() @@ -178,6 +183,7 @@ impl NvmeFaultController { qe_sizes, pci_fault_config, fault_active, + tdisp_interface, } } @@ -467,6 +473,7 @@ impl ChangeDeviceState for NvmeFaultController { workers, pci_fault_config: _, fault_active: _, + tdisp_interface: _, } = self; workers.reset().await; cfg_space.reset(); @@ -483,6 +490,19 @@ impl ChipsetDevice for NvmeFaultController { fn supports_pci(&mut self) -> Option<&mut dyn PciConfigSpace> { Some(self) } + + /// The NVMe fault controller is repurposed for use in TDISP tests. + fn supports_tdisp(&mut self) -> Option<&mut dyn TdispHostDeviceTarget> { + tracing::debug!( + supported = self.tdisp_interface.is_some(), + "fault controller TDISP support in ChipsetDevice" + ); + + match &mut self.tdisp_interface { + Some(tdisp) => Some(tdisp.as_mut()), + None => None, + } + } } impl MmioIntercept for NvmeFaultController { diff --git a/vm/devices/storage/nvme_test/src/resolver.rs b/vm/devices/storage/nvme_test/src/resolver.rs index fc792d4be4..5e5984982b 100644 --- a/vm/devices/storage/nvme_test/src/resolver.rs +++ b/vm/devices/storage/nvme_test/src/resolver.rs @@ -12,6 +12,7 @@ use nvme_resources::NamespaceDefinition; use nvme_resources::NvmeFaultControllerHandle; use pci_resources::ResolvePciDeviceHandleParams; use pci_resources::ResolvedPciDevice; +use tdisp::test_helpers::make_null_tdisp_interface; use thiserror::Error; use vm_resource::AsyncResolveResource; use vm_resource::ResolveError; @@ -54,6 +55,15 @@ impl AsyncResolveResource resource: NvmeFaultControllerHandle, input: ResolvePciDeviceHandleParams<'_>, ) -> Result { + // If TDISP tests are enabled, create a mock TDISP interface to expose + // for the device from OpenVMM. + let tdisp_interface: Option> = + if resource.enable_tdisp_tests { + Some(Box::new(make_null_tdisp_interface("fault-controller-test"))) + } else { + None + }; + let controller = NvmeFaultController::new( input.driver_source, input.guest_memory.clone(), @@ -65,6 +75,7 @@ impl AsyncResolveResource subsystem_id: resource.subsystem_id, }, resource.fault_config, + tdisp_interface, ); for NamespaceDefinition { nsid, diff --git a/vm/devices/storage/nvme_test/src/tests/controller_tests.rs b/vm/devices/storage/nvme_test/src/tests/controller_tests.rs index 2bf57e6090..98e5295feb 100644 --- a/vm/devices/storage/nvme_test/src/tests/controller_tests.rs +++ b/vm/devices/storage/nvme_test/src/tests/controller_tests.rs @@ -52,6 +52,7 @@ fn instantiate_controller( subsystem_id: Guid::new_random(), }, fault_configuration, + None, ); if let Some(intc) = int_controller { diff --git a/vm/devices/tdisp/Cargo.toml b/vm/devices/tdisp/Cargo.toml index 4c3b1af7fc..f6040673b8 100644 --- a/vm/devices/tdisp/Cargo.toml +++ b/vm/devices/tdisp/Cargo.toml @@ -9,10 +9,12 @@ rust-version.workspace = true [dependencies] anyhow.workspace = true parking_lot.workspace = true -thiserror.workspace = true prost.workspace = true tracing.workspace = true tdisp_proto.workspace = true +bitfield-struct.workspace = true +zerocopy.workspace = true +static_assertions.workspace = true [lints] workspace = true diff --git a/vm/devices/tdisp/src/devicereport.rs b/vm/devices/tdisp/src/devicereport.rs new file mode 100644 index 0000000000..2f8926ace0 --- /dev/null +++ b/vm/devices/tdisp/src/devicereport.rs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use bitfield_struct::bitfield; +use zerocopy::FromBytes; +use zerocopy::Immutable; +use zerocopy::KnownLayout; + +/// PCI Express Base Specification Revision 6.3 Section 11.3.11 DEVICE_INTERFACE_REPORT +#[bitfield(u16)] +#[derive(KnownLayout, FromBytes, Immutable)] +pub struct TdispTdiReportInterfaceInfo { + /// When 1, indicates that device firmware updates are not permitted + /// while in CONFIG_LOCKED or RUN. When 0, indicates that firmware + /// updates are permitted while in these states + pub firmware_update_allowed: bool, + + /// TDI generates DMA requests without PASID + pub generate_dma_without_pasid: bool, + + /// TDI generates DMA requests with PASID + pub generate_dma_with_pasid: bool, + + /// ATS supported and enabled for the TDI + pub ats_support_enabled: bool, + + /// PRS supported and enabled for the TDI + pub prs_support_enabled: bool, + #[bits(11)] + _reserved0: u16, +} + +/// PCI Express Base Specification Revision 6.3 Section 11.3.11 DEVICE_INTERFACE_REPORT +#[bitfield(u16)] +#[derive(KnownLayout, FromBytes, Immutable)] +pub struct TdispTdiReportMmioFlags { + /// MSI-X Table – if the range maps MSI-X table. This must be reported only if locked by the LOCK_INTERFACE_REQUEST. + pub range_maps_msix_table: bool, + + /// MSI-X PBA – if the range maps MSI-X PBA. This must be reported only if locked by the LOCK_INTERFACE_REQUEST. + pub range_maps_msix_pba: bool, + + /// IS_NON_TEE_MEM – must be 1b if the range is non-TEE memory. + /// For attribute updatable ranges (see below), this field must indicate attribute of the range when the TDI was locked. + pub is_non_tee_mem: bool, + + /// IS_MEM_ATTR_UPDATABLE – must be 1b if the attributes of this range is updatable using SET_MMIO_ATTRIBUTE_REQUEST + pub is_mem_attr_updatable: bool, + #[bits(12)] + _reserved0: u16, +} + +/// PCI Express Base Specification Revision 6.3 Section 11.3.11 DEVICE_INTERFACE_REPORT +#[derive(KnownLayout, FromBytes, Immutable, Clone, Debug)] +pub struct TdispTdiReportMmioInterfaceInfo { + /// First 4K page with offset added + pub first_4k_page_offset: u64, + + /// Number of 4K pages in this range + pub num_4k_pages: u32, + + /// Range Attributes + pub flags: TdispTdiReportMmioFlags, + + /// Range ID – a device specific identifier for the specified range. + /// The range ID may be used to logically group one or more MMIO ranges into a larger range. + pub range_id: u16, +} + +static_assertions::const_assert_eq!(size_of::(), 0x10); + +/// PCI Express Base Specification Revision 6.3 Section 11.3.11 DEVICE_INTERFACE_REPORT +#[derive(KnownLayout, FromBytes, Immutable, Debug)] +#[repr(C)] +struct TdiReportStructSerialized { + pub interface_info: TdispTdiReportInterfaceInfo, + _reserved0: u16, + pub msi_x_message_control: u16, + pub lnr_control: u16, + pub tph_control: u32, + pub mmio_range_count: u32, + // Follows is a variable-sized # of `MmioInterfaceInfo` structs + // based on the value of `mmio_range_count`. +} + +static_assertions::const_assert_eq!(size_of::(), 0x10); + +/// The deserialized form of a TDI interface report. +#[derive(Debug)] +pub struct TdiReportStruct { + /// See: `TdispTdiReportInterfaceInfo` + pub interface_info: TdispTdiReportInterfaceInfo, + + /// MSI-X capability message control register state. Must be Clear if + /// a) capability is not supported or b) MSI-X table is not locked + pub msi_x_message_control: u16, + + /// LNR control register from LN Requester Extended Capability. + /// Must be Clear if LNR capability is not supported. LN is deprecated in PCIe Revision 6.0. + pub lnr_control: u16, + + /// TPH Requester Control Register from the TPH Requester Extended Capability. + /// Must be Clear if a) TPH capability is not support or b) MSI-X table is not locked + pub tph_control: u32, + + /// Each MMIO Range of the TDI is reported with the MMIO reporting offset added. + /// Base and size in units of 4K pages + pub mmio_interface_info: Vec, +} + +/// Reads a TDI interface report provided from the host into a struct. +pub fn deserialize_tdi_report(data: &[u8]) -> anyhow::Result { + // Deserialize the static part of the report. + let report_header = TdiReportStructSerialized::read_from_prefix(data) + .map_err(|e| anyhow::anyhow!("failed to deserialize TDI report header: {e:?}"))?; + let variable_portion_offset = report_header.1; + let report = report_header.0; + + // Deserialize the variable portion of the report. + let read_mmio_elems = <[TdispTdiReportMmioInterfaceInfo]>::ref_from_prefix_with_elems( + variable_portion_offset, + report.mmio_range_count as usize, + ) + .map_err(|e| anyhow::anyhow!("failed to deserialize TDI report mmio_interface_info: {e:?}"))?; + + // TDISP TODO: Parse the vendor specific info + let _vendor_specific_info = read_mmio_elems.1.to_vec(); + + Ok(TdiReportStruct { + interface_info: report.interface_info, + msi_x_message_control: report.msi_x_message_control, + lnr_control: report.lnr_control, + tph_control: report.tph_control, + mmio_interface_info: read_mmio_elems.0.to_vec(), + }) +} diff --git a/vm/devices/tdisp/src/lib.rs b/vm/devices/tdisp/src/lib.rs index de4d97dec6..3d6afbf54e 100644 --- a/vm/devices/tdisp/src/lib.rs +++ b/vm/devices/tdisp/src/lib.rs @@ -32,34 +32,45 @@ /// Protobuf serialization of guest commands and responses. pub mod serialize_proto; +/// Serialization code from PCI standard structures reported from the TDISP device directly. +pub mod devicereport; + #[cfg(test)] mod tests; +/// Mocks for the host interface and the emulator. +pub mod test_helpers; + use anyhow::Context; use parking_lot::Mutex; use std::sync::Arc; -use tdisp_proto::GuestToHostCommand; -use tdisp_proto::GuestToHostResponse; -use tdisp_proto::TdispCommandResponseBind; -use tdisp_proto::TdispCommandResponseGetDeviceInterfaceInfo; -use tdisp_proto::TdispCommandResponseGetTdiReport; -use tdisp_proto::TdispCommandResponseStartTdi; -use tdisp_proto::TdispCommandResponseUnbind; -use tdisp_proto::TdispDeviceInterfaceInfo; -use tdisp_proto::TdispGuestOperationErrorCode; -use tdisp_proto::TdispGuestProtocolType; -use tdisp_proto::TdispGuestUnbindReason; -use tdisp_proto::TdispReportType; -use tdisp_proto::TdispTdiState; -use tdisp_proto::guest_to_host_command::Command; -use tdisp_proto::guest_to_host_response::Response; -use thiserror::Error; +pub use tdisp_proto::GuestToHostCommand; +pub use tdisp_proto::GuestToHostCommandExt; +pub use tdisp_proto::GuestToHostResponse; +pub use tdisp_proto::GuestToHostResponseExt; +pub use tdisp_proto::TdispCommandResponseBind; +pub use tdisp_proto::TdispCommandResponseGetDeviceInterfaceInfo; +pub use tdisp_proto::TdispCommandResponseGetTdiReport; +pub use tdisp_proto::TdispCommandResponseStartTdi; +pub use tdisp_proto::TdispCommandResponseUnbind; +pub use tdisp_proto::TdispDeviceInterfaceInfo; +pub use tdisp_proto::TdispGuestOperationError; +pub use tdisp_proto::TdispGuestOperationErrorCode; +pub use tdisp_proto::TdispGuestProtocolType; +pub use tdisp_proto::TdispGuestUnbindReason; +pub use tdisp_proto::TdispReportType; +pub use tdisp_proto::TdispTdiState; +pub use tdisp_proto::guest_to_host_command::Command; +pub use tdisp_proto::guest_to_host_response::Response; + use tracing::instrument; /// Callback for receiving TDISP commands from the guest. pub type TdispCommandCallback = dyn Fn(&GuestToHostCommand) -> anyhow::Result<()> + Send + Sync; -/// Trait used by the emulator to call back into the host. +/// Describes the interface that host software should implement to provide TDISP +/// functionality for a device. These interfaces might dispatch to a physical +/// device, or might be implemented by a software emulator. pub trait TdispHostDeviceInterface: Send + Sync { /// Request versioning and protocol negotiation from the host. fn tdisp_negotiate_protocol( @@ -444,100 +455,6 @@ impl TdispHostStateMachine { } } -/// Error returned by TDISP operations dispatched by the guest. -#[derive(Error, Debug, Copy, Clone)] -#[expect(missing_docs)] -pub enum TdispGuestOperationError { - #[error("unknown error code")] - Unknown, - #[error("the operation was successful")] - Success, - #[error("the requested guest protocol type was not valid for this host")] - InvalidGuestProtocolRequest, - #[error("the current TDI state is incorrect for this operation")] - InvalidDeviceState, - #[error("the reason for this unbind is invalid")] - InvalidGuestUnbindReason, - #[error("invalid TDI command ID")] - InvalidGuestCommandId, - #[error("operation requested was not implemented")] - NotImplemented, - #[error("host failed to process command")] - HostFailedToProcessCommand, - #[error( - "the device was not in the Locked or Run state when the attestation report was requested" - )] - InvalidGuestAttestationReportState, - #[error("invalid attestation report type requested")] - InvalidGuestAttestationReportType, -} - -impl From for TdispGuestOperationError { - fn from(err_code: TdispGuestOperationErrorCode) -> Self { - match err_code { - TdispGuestOperationErrorCode::Unknown => TdispGuestOperationError::Unknown, - TdispGuestOperationErrorCode::Success => TdispGuestOperationError::Success, - TdispGuestOperationErrorCode::InvalidGuestProtocolRequest => { - TdispGuestOperationError::InvalidGuestProtocolRequest - } - TdispGuestOperationErrorCode::InvalidDeviceState => { - TdispGuestOperationError::InvalidDeviceState - } - TdispGuestOperationErrorCode::InvalidGuestUnbindReason => { - TdispGuestOperationError::InvalidGuestUnbindReason - } - TdispGuestOperationErrorCode::InvalidGuestCommandId => { - TdispGuestOperationError::InvalidGuestCommandId - } - TdispGuestOperationErrorCode::NotImplemented => { - TdispGuestOperationError::NotImplemented - } - TdispGuestOperationErrorCode::HostFailedToProcessCommand => { - TdispGuestOperationError::HostFailedToProcessCommand - } - TdispGuestOperationErrorCode::InvalidGuestAttestationReportState => { - TdispGuestOperationError::InvalidGuestAttestationReportState - } - TdispGuestOperationErrorCode::InvalidGuestAttestationReportType => { - TdispGuestOperationError::InvalidGuestAttestationReportType - } - } - } -} - -impl From for TdispGuestOperationErrorCode { - fn from(err: TdispGuestOperationError) -> Self { - match err { - TdispGuestOperationError::Unknown => TdispGuestOperationErrorCode::Unknown, - TdispGuestOperationError::Success => TdispGuestOperationErrorCode::Success, - TdispGuestOperationError::InvalidGuestProtocolRequest => { - TdispGuestOperationErrorCode::InvalidGuestProtocolRequest - } - TdispGuestOperationError::InvalidDeviceState => { - TdispGuestOperationErrorCode::InvalidDeviceState - } - TdispGuestOperationError::InvalidGuestUnbindReason => { - TdispGuestOperationErrorCode::InvalidGuestUnbindReason - } - TdispGuestOperationError::InvalidGuestCommandId => { - TdispGuestOperationErrorCode::InvalidGuestCommandId - } - TdispGuestOperationError::NotImplemented => { - TdispGuestOperationErrorCode::NotImplemented - } - TdispGuestOperationError::HostFailedToProcessCommand => { - TdispGuestOperationErrorCode::HostFailedToProcessCommand - } - TdispGuestOperationError::InvalidGuestAttestationReportState => { - TdispGuestOperationErrorCode::InvalidGuestAttestationReportState - } - TdispGuestOperationError::InvalidGuestAttestationReportType => { - TdispGuestOperationErrorCode::InvalidGuestAttestationReportType - } - } - } -} - /// Represents an interface by which guest commands can be dispatched to a /// backing TDISP state handler in the host. This could be an emulated TDISP device or an /// assigned TDISP device that is actually connected to the guest. @@ -629,11 +546,6 @@ impl TdispGuestRequestInterface for TdispHostStateMachine { match res { Ok(interface_info) => { - tracing::info!( - "Guest protocol negotiated successfully to: {:?}", - interface_info - ); - match TdispGuestProtocolType::from_i32(interface_info.guest_protocol_type) { Some(guest_protocol_type) => { if guest_protocol_type == TdispGuestProtocolType::Invalid { @@ -643,7 +555,10 @@ impl TdispGuestRequestInterface for TdispHostStateMachine { Err(TdispGuestOperationError::InvalidGuestProtocolRequest) } else { self.guest_protocol_type = guest_protocol_type; - tracing::info!("Guest protocol negotiated: {interface_info:?}"); + tracing::info!( + "Guest protocol negotiated successfully to: {:?}", + interface_info + ); Ok(interface_info) } } diff --git a/vm/devices/tdisp/src/test_helpers.rs b/vm/devices/tdisp/src/test_helpers.rs new file mode 100644 index 0000000000..5188f61d44 --- /dev/null +++ b/vm/devices/tdisp/src/test_helpers.rs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::TdispHostDeviceInterface; +use crate::TdispHostDeviceTargetEmulator; +use parking_lot::Mutex; +use std::sync::Arc; +use tdisp_proto::TdispDeviceInterfaceInfo; +use tdisp_proto::TdispGuestProtocolType; +use tdisp_proto::TdispReportType; + +/// Guest protocol that will be negotiated by the mock device. +pub const TDISP_MOCK_GUEST_PROTOCOL: TdispGuestProtocolType = TdispGuestProtocolType::AmdSevTioV1; + +/// Device features that will be negotiated by the mock device. +pub const TDISP_MOCK_SUPPORTED_FEATURES: u64 = 0xDEAD; + +/// Device ID that will be negotiated by the mock device. +pub const TDISP_MOCK_DEVICE_ID: u64 = 99; + +/// Implements the host side of the TDISP interface for the mock NullDevice. +pub struct NullTdispHostInterface {} +impl TdispHostDeviceInterface for NullTdispHostInterface { + fn tdisp_negotiate_protocol( + &mut self, + _requested_guest_protocol: TdispGuestProtocolType, + ) -> anyhow::Result { + Ok(TdispDeviceInterfaceInfo { + guest_protocol_type: TDISP_MOCK_GUEST_PROTOCOL as i32, + supported_features: TDISP_MOCK_SUPPORTED_FEATURES, + tdisp_device_id: TDISP_MOCK_DEVICE_ID, + }) + } + + fn tdisp_bind_device(&mut self) -> anyhow::Result<()> { + Ok(()) + } + + fn tdisp_start_device(&mut self) -> anyhow::Result<()> { + Ok(()) + } + + fn tdisp_unbind_device(&mut self) -> anyhow::Result<()> { + Ok(()) + } + + fn tdisp_get_device_report( + &mut self, + _report_type: TdispReportType, + ) -> anyhow::Result> { + Ok(vec![]) + } +} + +/// Implements the host side of the TDISP interface for a mock device that does nothing. +pub fn make_null_tdisp_interface(debug_device_id: &str) -> TdispHostDeviceTargetEmulator { + TdispHostDeviceTargetEmulator::new( + Arc::new(Mutex::new(NullTdispHostInterface {})), + debug_device_id, + ) +} diff --git a/vm/devices/tdisp/src/tests/endtoend_tests.rs b/vm/devices/tdisp/src/tests/endtoend_tests.rs index 95e3414c08..6e8408dabb 100644 --- a/vm/devices/tdisp/src/tests/endtoend_tests.rs +++ b/vm/devices/tdisp/src/tests/endtoend_tests.rs @@ -13,8 +13,8 @@ use crate::serialize_proto::deserialize_command; use crate::serialize_proto::deserialize_response; use crate::serialize_proto::serialize_command; use crate::serialize_proto::serialize_response; +use crate::test_helpers::TDISP_MOCK_GUEST_PROTOCOL; use crate::tests::mocks::LastCall; -use crate::tests::mocks::TDISP_MOCK_GUEST_PROTOCOL; use crate::tests::mocks::new_emulator; use tdisp_proto::GuestToHostCommand; use tdisp_proto::TdispCommandRequestBind; diff --git a/vm/devices/tdisp/src/tests/mocks.rs b/vm/devices/tdisp/src/tests/mocks.rs index 25faf5e260..413d937393 100644 --- a/vm/devices/tdisp/src/tests/mocks.rs +++ b/vm/devices/tdisp/src/tests/mocks.rs @@ -5,14 +5,15 @@ use crate::TdispGuestRequestInterface; use crate::TdispHostDeviceInterface; use crate::TdispHostDeviceTargetEmulator; use crate::TdispHostStateMachine; +use crate::test_helpers::TDISP_MOCK_DEVICE_ID; +use crate::test_helpers::TDISP_MOCK_GUEST_PROTOCOL; +use crate::test_helpers::TDISP_MOCK_SUPPORTED_FEATURES; use parking_lot::Mutex; use std::sync::Arc; use tdisp_proto::TdispDeviceInterfaceInfo; use tdisp_proto::TdispGuestProtocolType; use tdisp_proto::TdispReportType; -pub const TDISP_MOCK_GUEST_PROTOCOL: TdispGuestProtocolType = TdispGuestProtocolType::AmdSevTioV10; - #[derive(Debug, PartialEq, Clone)] pub enum LastCall { NegotiateProtocol, @@ -63,8 +64,8 @@ impl TdispHostDeviceInterface for TrackingHostInterface { *self.last_call.lock() = Some(LastCall::NegotiateProtocol); Ok(TdispDeviceInterfaceInfo { guest_protocol_type: TDISP_MOCK_GUEST_PROTOCOL as i32, - supported_features: 0xDEAD, - tdisp_device_id: 99, + supported_features: TDISP_MOCK_SUPPORTED_FEATURES, + tdisp_device_id: TDISP_MOCK_DEVICE_ID, }) } } diff --git a/vm/devices/tdisp/src/tests/mod.rs b/vm/devices/tdisp/src/tests/mod.rs index d540784644..1873336c96 100644 --- a/vm/devices/tdisp/src/tests/mod.rs +++ b/vm/devices/tdisp/src/tests/mod.rs @@ -4,7 +4,7 @@ //! Unit tests for the TDISP guest-to-host interface. /// Mocks for the host interface and the emulator. -mod mocks; +pub mod mocks; /// Unit tests for serialization and deserialization of TDISP guest-to-host commands and responses. pub mod serialize_tests; diff --git a/vm/devices/tdisp/src/tests/serialize_tests.rs b/vm/devices/tdisp/src/tests/serialize_tests.rs index 2a2b9fbcc9..26132ab637 100644 --- a/vm/devices/tdisp/src/tests/serialize_tests.rs +++ b/vm/devices/tdisp/src/tests/serialize_tests.rs @@ -9,7 +9,7 @@ use crate::serialize_proto::deserialize_command; use crate::serialize_proto::deserialize_response; use crate::serialize_proto::serialize_command; use crate::serialize_proto::serialize_response; -use crate::tests::mocks::TDISP_MOCK_GUEST_PROTOCOL; +use crate::test_helpers::TDISP_MOCK_GUEST_PROTOCOL; use tdisp_proto::GuestToHostCommand; use tdisp_proto::GuestToHostResponse; use tdisp_proto::TdispCommandRequestBind; diff --git a/vm/devices/tdisp_proto/Cargo.toml b/vm/devices/tdisp_proto/Cargo.toml index 9dcd1be369..deb70ddff3 100644 --- a/vm/devices/tdisp_proto/Cargo.toml +++ b/vm/devices/tdisp_proto/Cargo.toml @@ -8,7 +8,7 @@ rust-version.workspace = true [dependencies] inspect.workspace = true - +thiserror.workspace = true prost.workspace = true [build-dependencies] diff --git a/vm/devices/tdisp_proto/src/errorcode.rs b/vm/devices/tdisp_proto/src/errorcode.rs new file mode 100644 index 0000000000..67eb0ac77b --- /dev/null +++ b/vm/devices/tdisp_proto/src/errorcode.rs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::TdispGuestOperationErrorCode; +use thiserror::Error; + +/// Error returned by TDISP operations dispatched by the guest. +#[derive(Error, Debug, Copy, Clone)] +#[expect(missing_docs)] +pub enum TdispGuestOperationError { + #[error("unknown error code")] + Unknown, + #[error("the operation was successful")] + Success, + #[error("the requested guest protocol type was not valid for this host")] + InvalidGuestProtocolRequest, + #[error("the current TDI state is incorrect for this operation")] + InvalidDeviceState, + #[error("the reason for this unbind is invalid")] + InvalidGuestUnbindReason, + #[error("invalid TDI command ID")] + InvalidGuestCommandId, + #[error("operation requested was not implemented")] + NotImplemented, + #[error("host failed to process command")] + HostFailedToProcessCommand, + #[error( + "the device was not in the Locked or Run state when the attestation report was requested" + )] + InvalidGuestAttestationReportState, + #[error("invalid attestation report type requested")] + InvalidGuestAttestationReportType, +} + +impl From for TdispGuestOperationError { + fn from(err_code: TdispGuestOperationErrorCode) -> Self { + match err_code { + TdispGuestOperationErrorCode::Unknown => TdispGuestOperationError::Unknown, + TdispGuestOperationErrorCode::Success => TdispGuestOperationError::Success, + TdispGuestOperationErrorCode::InvalidGuestProtocolRequest => { + TdispGuestOperationError::InvalidGuestProtocolRequest + } + TdispGuestOperationErrorCode::InvalidDeviceState => { + TdispGuestOperationError::InvalidDeviceState + } + TdispGuestOperationErrorCode::InvalidGuestUnbindReason => { + TdispGuestOperationError::InvalidGuestUnbindReason + } + TdispGuestOperationErrorCode::InvalidGuestCommandId => { + TdispGuestOperationError::InvalidGuestCommandId + } + TdispGuestOperationErrorCode::NotImplemented => { + TdispGuestOperationError::NotImplemented + } + TdispGuestOperationErrorCode::HostFailedToProcessCommand => { + TdispGuestOperationError::HostFailedToProcessCommand + } + TdispGuestOperationErrorCode::InvalidGuestAttestationReportState => { + TdispGuestOperationError::InvalidGuestAttestationReportState + } + TdispGuestOperationErrorCode::InvalidGuestAttestationReportType => { + TdispGuestOperationError::InvalidGuestAttestationReportType + } + } + } +} + +impl From for TdispGuestOperationErrorCode { + fn from(err: TdispGuestOperationError) -> Self { + match err { + TdispGuestOperationError::Unknown => TdispGuestOperationErrorCode::Unknown, + TdispGuestOperationError::Success => TdispGuestOperationErrorCode::Success, + TdispGuestOperationError::InvalidGuestProtocolRequest => { + TdispGuestOperationErrorCode::InvalidGuestProtocolRequest + } + TdispGuestOperationError::InvalidDeviceState => { + TdispGuestOperationErrorCode::InvalidDeviceState + } + TdispGuestOperationError::InvalidGuestUnbindReason => { + TdispGuestOperationErrorCode::InvalidGuestUnbindReason + } + TdispGuestOperationError::InvalidGuestCommandId => { + TdispGuestOperationErrorCode::InvalidGuestCommandId + } + TdispGuestOperationError::NotImplemented => { + TdispGuestOperationErrorCode::NotImplemented + } + TdispGuestOperationError::HostFailedToProcessCommand => { + TdispGuestOperationErrorCode::HostFailedToProcessCommand + } + TdispGuestOperationError::InvalidGuestAttestationReportState => { + TdispGuestOperationErrorCode::InvalidGuestAttestationReportState + } + TdispGuestOperationError::InvalidGuestAttestationReportType => { + TdispGuestOperationErrorCode::InvalidGuestAttestationReportType + } + } + } +} diff --git a/vm/devices/tdisp_proto/src/lib.rs b/vm/devices/tdisp_proto/src/lib.rs index b2c2ef16a3..cdaa56eda3 100644 --- a/vm/devices/tdisp_proto/src/lib.rs +++ b/vm/devices/tdisp_proto/src/lib.rs @@ -5,10 +5,133 @@ #![expect(missing_docs)] #![forbid(unsafe_code)] +#![allow(unused_qualifications)] // Crates used by generated code. Reference them explicitly to ensure that // automated tools do not remove them. +use crate::guest_to_host_command::Command; +use crate::guest_to_host_response::Response; use inspect as _; use prost as _; +mod errorcode; +pub use errorcode::*; include!(concat!(env!("OUT_DIR"), "/tdisp.rs")); + +pub trait GuestToHostCommandExt { + /// Returns the command type name of the command. + fn get_type_name(&self) -> Option<&str>; +} + +impl GuestToHostCommandExt for GuestToHostCommand { + fn get_type_name(&self) -> Option<&str> { + match self.command { + Some(Command::GetDeviceInterfaceInfo(_)) => Some("GetDeviceInterfaceInfo"), + Some(Command::Bind(_)) => Some("Bind"), + Some(Command::StartTdi(_)) => Some("StartTdi"), + Some(Command::Unbind(_)) => Some("Unbind"), + Some(Command::GetTdiReport(_)) => Some("GetTdiReport"), + None => None, + } + } +} + +/// Implemented by each response payload type so that [`GuestToHostResponseExt::get_response`] +/// can extract it generically from a [`Response`] oneof variant. +pub trait GuestToHostResponseVariant: Sized { + fn from_response_variant(response: Response) -> Option; +} + +impl GuestToHostResponseVariant for TdispCommandResponseGetDeviceInterfaceInfo { + fn from_response_variant(response: Response) -> Option { + match response { + Response::GetDeviceInterfaceInfo(r) => Some(r), + _ => None, + } + } +} + +impl GuestToHostResponseVariant for TdispCommandResponseBind { + fn from_response_variant(response: Response) -> Option { + match response { + Response::Bind(r) => Some(r), + _ => None, + } + } +} + +impl GuestToHostResponseVariant for TdispCommandResponseGetTdiReport { + fn from_response_variant(response: Response) -> Option { + match response { + Response::GetTdiReport(r) => Some(r), + _ => None, + } + } +} + +impl GuestToHostResponseVariant for TdispCommandResponseStartTdi { + fn from_response_variant(response: Response) -> Option { + match response { + Response::StartTdi(r) => Some(r), + _ => None, + } + } +} + +impl GuestToHostResponseVariant for TdispCommandResponseUnbind { + fn from_response_variant(response: Response) -> Option { + match response { + Response::Unbind(r) => Some(r), + _ => None, + } + } +} + +/// Provides helper methods for common operations on [`GuestToHostResponse`]. +pub trait GuestToHostResponseExt { + /// Returns the error code of the response, if any. + fn get_error_code(&self) -> Option; + + /// Returns the packet type name of the response. + fn get_type_name(&self) -> Option<&str>; + + /// Consumes the response and returns the inner payload if the result is + /// [`TdispGuestOperationError::Success`] and the oneof variant matches `T`. + /// Returns the error code otherwise. + /// + /// # Example + /// ```ignore + /// let bind = resp.get_response::()?; + /// ``` + fn get_response(self) -> Result; +} + +impl GuestToHostResponseExt for GuestToHostResponse { + fn get_error_code(&self) -> Option { + TdispGuestOperationErrorCode::from_i32(self.result) + } + + fn get_type_name(&self) -> Option<&str> { + match self.response { + Some(Response::GetDeviceInterfaceInfo(_)) => Some("GetDeviceInterfaceInfo"), + Some(Response::Bind(_)) => Some("Bind"), + Some(Response::StartTdi(_)) => Some("StartTdi"), + Some(Response::Unbind(_)) => Some("Unbind"), + Some(Response::GetTdiReport(_)) => Some("GetTdiReport"), + None => None, + } + } + + fn get_response(self) -> Result { + match self.get_error_code() { + Some(TdispGuestOperationErrorCode::Success) => { + match self.response.and_then(T::from_response_variant) { + Some(r) => Ok(r), + None => Err(TdispGuestOperationErrorCode::Unknown.into()), + } + } + Some(err) => Err(err.into()), + None => Err(TdispGuestOperationErrorCode::Unknown.into()), + } + } +} diff --git a/vm/devices/tdisp_proto/src/tdisp.proto b/vm/devices/tdisp_proto/src/tdisp.proto index eda73ae15d..ca371a49e2 100644 --- a/vm/devices/tdisp_proto/src/tdisp.proto +++ b/vm/devices/tdisp_proto/src/tdisp.proto @@ -112,11 +112,11 @@ enum TdispGuestProtocolType { // Invalid guest protocol type. TDISP_GUEST_PROTOCOL_TYPE_INVALID = 0; - // Guest is utilizing OpenHCL's V1.0 TDISP interface for AMD® SEV-TIO - TDISP_GUEST_PROTOCOL_TYPE_AMD_SEV_TIO_V1_0 = 1; + // Guest is utilizing OpenHCL's V1 TDISP interface for AMD® SEV-TIO + TDISP_GUEST_PROTOCOL_TYPE_AMD_SEV_TIO_V1 = 1; - // Guest is utilizing OpenHCL's V1.0 TDISP interface for Intel® TDX Connect - TDISP_GUEST_PROTOCOL_TYPE_INTEL_TDX_CONNECT_V1_0 = 2; + // Guest is utilizing OpenHCL's V1 TDISP interface for Intel® TDX Connect + TDISP_GUEST_PROTOCOL_TYPE_INTEL_TDX_CONNECT_V1 = 2; } // ---------------------------------------------------------------------------- @@ -238,85 +238,3 @@ message TdispDeviceInterfaceInfo { // Device ID used to communicate with firmware for this particular device. uint64 tdisp_device_id = 3; } - -// ---------------------------------------------------------------------------- -// Device report structures -// ---------------------------------------------------------------------------- - -// PCI Express Base Specification Revision 6.3 Section 11.3.11 -// DEVICE_INTERFACE_REPORT interface info flags. -message TdispTdiReportInterfaceInfo { - // When true, device firmware updates are not permitted while in - // CONFIG_LOCKED or RUN. - bool firmware_update_allowed = 1; - - // TDI generates DMA requests without PASID. - bool generate_dma_without_pasid = 2; - - // TDI generates DMA requests with PASID. - bool generate_dma_with_pasid = 3; - - // ATS supported and enabled for the TDI. - bool ats_support_enabled = 4; - - // PRS supported and enabled for the TDI. - bool prs_support_enabled = 5; -} - -// PCI Express Base Specification Revision 6.3 Section 11.3.11 -// DEVICE_INTERFACE_REPORT MMIO range attribute flags. -message TdispTdiReportMmioFlags { - // MSI-X Table – if the range maps MSI-X table. - bool range_maps_msix_table = 1; - - // MSI-X PBA – if the range maps MSI-X PBA. - bool range_maps_msix_pba = 2; - - // IS_NON_TEE_MEM – must be true if the range is non-TEE memory. - bool is_non_tee_mem = 3; - - // IS_MEM_ATTR_UPDATABLE – must be true if the attributes of this range are - // updatable using SET_MMIO_ATTRIBUTE_REQUEST. - bool is_mem_attr_updatable = 4; -} - -// PCI Express Base Specification Revision 6.3 Section 11.3.11 -// DEVICE_INTERFACE_REPORT per-MMIO-range info. -// Note: range_id is uint32 here because protobuf has no uint16; callers -// should treat it as a 16-bit value. -message TdispTdiReportMmioInterfaceInfo { - // First 4K page with offset added. - uint64 first_4k_page_offset = 1; - - // Number of 4K pages in this range. - uint32 num_4k_pages = 2; - - // Range attributes. - TdispTdiReportMmioFlags flags = 3; - - // Range ID – a device-specific identifier for the specified range. - // Stored as uint32 because protobuf has no uint16; valid range is 0–65535. - uint32 range_id = 4; -} - -// The deserialized form of a TDI interface report. -// Note: msi_x_message_control and lnr_control are uint32 here because -// protobuf has no uint16; callers should treat them as 16-bit values. -message TdiReportStruct { - // Interface info flags. - TdispTdiReportInterfaceInfo interface_info = 1; - - // MSI-X capability message control register state. - // Stored as uint32; valid range is 0–65535. - uint32 msi_x_message_control = 2; - - // LNR control register from LN Requester Extended Capability. - // Stored as uint32; valid range is 0–65535. - uint32 lnr_control = 3; - - // TPH Requester Control Register from the TPH Requester Extended Capability. - uint32 tph_control = 4; - - // MMIO range info for each range reported by the TDI. - repeated TdispTdiReportMmioInterfaceInfo mmio_interface_info = 5; -} diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch/openhcl_servicing.rs b/vmm_tests/vmm_tests/tests/tests/multiarch/openhcl_servicing.rs index 6a141e1996..c41418eb62 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch/openhcl_servicing.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch/openhcl_servicing.rs @@ -864,6 +864,7 @@ async fn create_keepalive_test_config( .into_resource(), }], fault_config: fault_configuration, + enable_tdisp_tests: false, } .into_resource(), }) diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64.rs b/vmm_tests/vmm_tests/tests/tests/x86_64.rs index 39d7722fe7..bfc439b1d7 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64.rs @@ -8,9 +8,13 @@ mod openhcl_uefi; mod storage; use anyhow::Context; +use guid::Guid; +use mesh::CellUpdater; use net_backend_resources::mac_address::MacAddress; use net_backend_resources::null::NullHandle; use nvme_resources::NvmeControllerHandle; +use nvme_resources::NvmeFaultControllerHandle; +use nvme_resources::fault::FaultConfiguration; use openvmm_defs::config::DeviceVtl; use openvmm_defs::config::VpciDeviceConfig; use petri::ApicMode; @@ -213,3 +217,56 @@ async fn vpci_filter(config: PetriVmBuilder) -> anyhow::Res vm.wait_for_clean_teardown().await?; Ok(()) } + +#[openvmm_test(openhcl_linux_direct_x64)] +async fn vpci_relay_tdisp_device( + config: PetriVmBuilder, +) -> anyhow::Result<()> { + const NVME_INSTANCE: Guid = guid::guid!("dce4ebad-182f-46c0-8d30-8446c1c62ab3"); + + // Create a VPCI device to relay to VTL0 and run basic TDISP end-to-end + // tests on it. + let (vm, agent) = config + .with_openhcl_command_line("OPENHCL_ENABLE_VPCI_RELAY=1") + // Tells VPCI relay that it should take the device through a mock TDISP + // flow with the OpenVMM host. + .with_openhcl_command_line("OPENHCL_TEST_CONFIG=TDISP_VPCI_FLOW_TEST") + .with_vmbus_redirect(true) + .modify_backend(move |b| { + b.with_custom_config(|c| { + c.vpci_devices.extend([VpciDeviceConfig { + vtl: DeviceVtl::Vtl0, + instance_id: NVME_INSTANCE, + + // The NVMe fault controller device is a fake NVMe + // controller that is repurposed for use in the TDISP test + // flow. + resource: NvmeFaultControllerHandle { + subsystem_id: Guid::new_random(), + msix_count: 1, + max_io_queues: 1, + namespaces: Vec::new(), + fault_config: FaultConfiguration::new(CellUpdater::new(false).cell()), + enable_tdisp_tests: true, + } + .into_resource(), + }]) + }) + }) + .run() + .await?; + + let sh = agent.unix_shell(); + let lspci_output = cmd!(sh, "lspci").read().await?; + let devices = lspci_output + .lines() + .map(|line| line.trim().split_once(' ').ok_or_else(|| line.trim())) + .collect::>(); + + // The NVMe controller should be present after the HCL performs its TDISP test. + assert_eq!(devices, vec![Ok(("00:00.0", "Class 0108: 1414:00a9"))]); + + agent.power_off().await?; + vm.wait_for_clean_teardown().await?; + Ok(()) +}