diff --git a/Cargo.lock b/Cargo.lock index f3c4d0a3c1..73ed8a181e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4999,6 +4999,15 @@ dependencies = [ "vmcore", ] +[[package]] +name = "openhcl_tdisp" +version = "0.0.0" +dependencies = [ + "anyhow", + "inspect", + "tdisp", +] + [[package]] name = "openssl" version = "0.10.73" @@ -7073,6 +7082,20 @@ dependencies = [ "x86defs", ] +[[package]] +name = "tdisp" +version = "0.0.0" +dependencies = [ + "anyhow", + "bitfield-struct 0.11.0", + "open_enum", + "parking_lot", + "static_assertions", + "thiserror 2.0.16", + "tracing", + "zerocopy 0.8.25", +] + [[package]] name = "tdx_guest_device" version = "0.0.0" @@ -7841,6 +7864,7 @@ dependencies = [ "nvme_resources", "openhcl_attestation_protocol", "openhcl_dma_manager", + "openhcl_tdisp", "pal", "pal_async", "pal_uring", diff --git a/Cargo.toml b/Cargo.toml index 8a4ed18ed6..bfff8fc5a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -167,6 +167,7 @@ lower_vtl_permissions_guard = { path = "openhcl/lower_vtl_permissions_guard" } minimal_rt = { path = "openhcl/minimal_rt" } minimal_rt_build = { path = "openhcl/minimal_rt_build" } openhcl_dma_manager = { path = "openhcl/openhcl_dma_manager" } +openhcl_tdisp = { path = "openhcl/openhcl_tdisp" } sidecar_client = { path = "openhcl/sidecar_client" } sidecar_defs = { path = "openhcl/sidecar_defs" } tee_call = { path = "openhcl/tee_call" } @@ -252,6 +253,7 @@ nvme_resources = { path = "vm/devices/storage/nvme_resources" } nvme_spec = { path = "vm/devices/storage/nvme_spec" } nvme_test = { path = "vm/devices/storage/nvme_test" } storage_string = { path = "vm/devices/storage/storage_string" } +tdisp = { path = "vm/devices/tdisp" } vmswitch = { path = "vm/devices/net/vmswitch" } pci_bus = { path = "vm/devices/pci/pci_bus" } pci_core = { path = "vm/devices/pci/pci_core" } diff --git a/openhcl/openhcl_tdisp/Cargo.toml b/openhcl/openhcl_tdisp/Cargo.toml new file mode 100644 index 0000000000..926cecaf0f --- /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] +inspect.workspace = true +tdisp.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..d43a9c59f6 --- /dev/null +++ b/openhcl/openhcl_tdisp/src/lib.rs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! +//! This module provides resources and traits for a TDISP client device +//! interface for assigned devices in OpenHCL. +//! +//! See: `vm/tdisp` for more information. + +pub use tdisp::TDISP_INTERFACE_VERSION_MAJOR; +pub use tdisp::TDISP_INTERFACE_VERSION_MINOR; +pub use tdisp::TdispCommandId; + +use inspect::Inspect; +use std::future::Future; +use tdisp::GuestToHostCommand; +use tdisp::GuestToHostResponse; +use tdisp::TdispGuestUnbindReason; +use tdisp::devicereport::TdiReportStruct; +use tdisp::devicereport::TdispReportType; + +/// Represents a TDISP device assigned to a guest partition. This trait allows +/// the guest to send TDISP commands to the host through the backing interface. +/// [TDISP TODO] Change out `anyhow` for a `TdispError` type? +pub trait ClientDevice: Send + Sync + Inspect { + /// Send a TDISP command to the host through the backing interface. + fn tdisp_command_to_host( + &self, + command: GuestToHostCommand, + ) -> anyhow::Result; + + /// Checks if the device is TDISP capable and returns the device interface info if so. + fn tdisp_get_device_interface_info(&self) -> anyhow::Result; + + /// Bind the device to the current partition and transition to Locked. + fn tdisp_bind_interface(&self) -> anyhow::Result<()>; +} + +/// Represents a TDISP device assigned to a guest partition that can be used to +/// send TDISP commands to the host through a backing interface. +pub trait VpciTdispInterface: 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 from the Locked state to the Run 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; +} diff --git a/openhcl/underhill_core/Cargo.toml b/openhcl/underhill_core/Cargo.toml index 3021b22335..1e65a09c68 100644 --- a/openhcl/underhill_core/Cargo.toml +++ b/openhcl/underhill_core/Cargo.toml @@ -75,6 +75,7 @@ netvsp.workspace = true nvme_driver.workspace = true nvme_resources.workspace = true openhcl_dma_manager.workspace = true +openhcl_tdisp.workspace = true scsi_core.workspace = true scsidisk.workspace = true scsidisk_resources.workspace = true diff --git a/openhcl/underhill_core/src/lib.rs b/openhcl/underhill_core/src/lib.rs index 280748b40c..23950e5824 100644 --- a/openhcl/underhill_core/src/lib.rs +++ b/openhcl/underhill_core/src/lib.rs @@ -27,7 +27,6 @@ mod vp; mod vpci; mod worker; mod wrapped_partition; - // `pub` so that the missing_docs warning fires for options without // documentation. pub use options::Options; @@ -67,6 +66,7 @@ use mesh_worker::WorkerHost; use mesh_worker::WorkerHostRunner; use mesh_worker::launch_local_worker; use mesh_worker::register_workers; +use openhcl_tdisp as _; use pal_async::DefaultDriver; use pal_async::DefaultPool; use pal_async::task::Spawn; diff --git a/vm/devices/tdisp/Cargo.toml b/vm/devices/tdisp/Cargo.toml new file mode 100644 index 0000000000..1d5cb32300 --- /dev/null +++ b/vm/devices/tdisp/Cargo.toml @@ -0,0 +1,20 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[package] +name = "tdisp" +edition.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow.workspace = true +bitfield-struct.workspace = true +open_enum.workspace = true +parking_lot.workspace = true +static_assertions.workspace = true +thiserror.workspace = true +tracing.workspace = true +zerocopy.workspace = true + +[lints] +workspace = true diff --git a/vm/devices/tdisp/src/command.rs b/vm/devices/tdisp/src/command.rs new file mode 100644 index 0000000000..de0feaecb8 --- /dev/null +++ b/vm/devices/tdisp/src/command.rs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::TdispGuestOperationError; +use crate::TdispTdiState; +use open_enum::open_enum; +use zerocopy::FromBytes; +use zerocopy::Immutable; +use zerocopy::IntoBytes; +use zerocopy::KnownLayout; + +/// Represents a TDISP command sent from the guest to the host. +#[derive(Debug, Copy, Clone)] +pub struct GuestToHostCommand { + /// Device ID of the target device. + pub device_id: u64, + /// The command ID. + pub command_id: TdispCommandId, + /// The payload of the command if it has one. + pub payload: TdispCommandRequestPayload, +} + +/// Represents a response from a TDISP command sent to the host by a guest. +#[derive(Debug, Clone)] +pub struct GuestToHostResponse { + /// The command ID. + pub command_id: TdispCommandId, + /// The result status of the command. + pub result: TdispGuestOperationError, + /// The state of the TDI before the command was executed. + pub tdi_state_before: TdispTdiState, + /// The state of the TDI after the command was executed. + pub tdi_state_after: TdispTdiState, + /// The payload of the response if it has one. + pub payload: TdispCommandResponsePayload, +} + +open_enum! { + /// Represents the command type for a packet sent from the guest to the host or + /// the response from the host to the guest. + pub enum TdispCommandId: u64 { + /// Invalid command id. + UNKNOWN = 0, + + /// Request the device's TDISP interface information. + GET_DEVICE_INTERFACE_INFO = 1, + + /// Bind the device to the current partition and transition to Locked. + BIND = 2, + + /// Get the TDI report for attestation from the host for the device. + GET_TDI_REPORT = 3, + + /// Transition the device to the Start state after successful attestation. + START_TDI = 4, + + /// Unbind the device from the partition, reverting it back to the Unlocked state. + UNBIND = 5, + } +} + +/// Represents the TDISP device interface information, such as the version and supported features. +#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)] +pub struct TdispDeviceInterfaceInfo { + /// The major version for the interface. This does not necessarily match to a TDISP specification version. + /// [TDISP TODO] dead_code + pub interface_version_major: u32, + + /// The minor version for the interface. This does not necessarily match to a TDISP specification version. + /// [TDISP TODO] dead_code + pub interface_version_minor: u32, + + /// [TDISP TODO] Placeholder for bitfield advertising feature set capabilities. + pub supported_features: u64, + + /// Device ID used to communicate with firmware for this particular device. + pub tdisp_device_id: u64, +} + +/// Serialized to and from the payload field of a TdispCommandResponse +#[derive(Debug, Clone)] +pub enum TdispCommandResponsePayload { + /// No payload. + None, + + /// TdispCommandId::GetDeviceInterfaceInfo + GetDeviceInterfaceInfo(TdispDeviceInterfaceInfo), + + /// TdispCommandId::GetTdiReport + GetTdiReport(TdispCommandResponseGetTdiReport), +} + +/// Serialized to and from the payload field of a TdispCommandRequest +#[derive(Debug, Copy, Clone)] +pub enum TdispCommandRequestPayload { + /// No payload. + None, + + /// TdispCommandId::Unbind + Unbind(TdispCommandRequestUnbind), + + /// TdispCommandId::GetTdiReport + GetTdiReport(TdispCommandRequestGetTdiReport), +} + +/// Represents a request to unbind the device back to the Unlocked state. +#[derive(Debug, Copy, Clone, FromBytes, IntoBytes, KnownLayout, Immutable)] +pub struct TdispCommandRequestUnbind { + /// The reason for the unbind. See: `TdispGuestUnbindReason` + pub unbind_reason: u64, +} + +/// Represents a request to get a specific device report form the TDI. +#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)] +pub struct TdispCommandRequestGetTdiReport { + /// The type of report to request. + /// See: `TdispDeviceReportType`` + pub report_type: u32, +} + +/// Represents the payload of the resposne for a TdispCommandId::GetTdiReport. +#[derive(Debug, Clone)] +pub struct TdispCommandResponseGetTdiReport { + /// The type of report requested. + /// See: `TdispDeviceReportType`` + pub report_type: u32, + + /// The buffer containing the requested report. + pub report_buffer: Vec, +} + +/// Represents the serialized form of a TdispCommandRequestGetTdiReport. +#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)] +pub struct TdispSerializedCommandRequestGetTdiReport { + /// The type of report to request. See: `TdispDeviceReportType`` + pub report_type: u32, + + /// The size of the report buffer. + pub report_buffer_size: u32, + // The remainder of the `report_buffer_size` bytes to follow are the bytes of the returned report. +} diff --git a/vm/devices/tdisp/src/devicereport.rs b/vm/devices/tdisp/src/devicereport.rs new file mode 100644 index 0000000000..342f20c6f8 --- /dev/null +++ b/vm/devices/tdisp/src/devicereport.rs @@ -0,0 +1,161 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use bitfield_struct::bitfield; +use open_enum::open_enum; +use zerocopy::FromBytes; +use zerocopy::Immutable; +use zerocopy::KnownLayout; + +open_enum! { + /// Represents a type of report that can be requested from the TDI (VF). + pub enum TdispReportType: u32 { + /// Invalid report type. All usages of this report type should be treated as an error. + INVALID = 0, + + /// Guest requests the guest device ID of the TDI. + GUEST_DEVICE_ID = 1, + + /// Guest requests the interface report of the TDI. + INTERFACE_REPORT = 2, + + /// Guest requests the certificate chain of the physical device. + CERTIFICATE_CHAIN = 3, + + /// Guest requests the measurements of the physical device. + MEASUREMENTS = 4, + + /// Guest requests whether the physical device is registered. + /// [TDISP TODO] Remove this report type? Doesn't seem to serve a purpose. + IS_REGISTERED = 5, + } +} + +/// 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 new file mode 100644 index 0000000000..4883a83d2e --- /dev/null +++ b/vm/devices/tdisp/src/lib.rs @@ -0,0 +1,755 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! +//! TDISP is a standardized interface for end-to-end encryption and attestation +//! of trusted assigned devices to confidential/isolated partitions. This crate +//! implements structures and interfaces for the host and guest to prepare and +//! assign trusted devices. Examples of technologies that implement TDISP +//! include: +//! - Intel® "TDX Connect" +//! - AMD SEV-TIO +//! +//! This crate is primarily used to implement the host side of the guest-to-host +//! interface for TDISP as well as the serialization of guest-to-host commands for both +//! the host and HCL. +//! +//! These structures and interfaces are used by the host virtualization stack +//! to prepare and assign trusted devices to guest partitions. +//! +//! The host is responsible for dispatching guest commands to this machinery by +//! creating a `TdispHostDeviceTargetEmulator` and calling through appropriate +//! trait methods to pass guest commands received from the guest to the emulator. +//! +//! This crate will handle incoming guest message structs and manage the state transitions +//! of the TDISP device and ensure valid transitions are made. Once a valid transition is made, the +//! `TdispHostDeviceTargetEmulator` will call back into the host through the +//! `TdispHostDeviceInterface` trait to allow the host to perform platform actions +//! such as binding the device to a guest partition or retrieving attestation reports. +//! It is the responsibility of the host to provide a `TdispHostDeviceInterface` +//! implementation that performs the necessary platform actions. + +/// Commands and responses for the TDISP guest-to-host interface. +pub mod command; + +/// Retrieval and parsing of device reports. +pub mod devicereport; + +/// Serialization of guest commands and responses. +pub mod serialize; + +pub use command::GuestToHostCommand; +pub use command::GuestToHostResponse; +pub use command::TdispCommandId; +pub use command::TdispCommandResponsePayload; +pub use command::TdispDeviceInterfaceInfo; + +use anyhow::Context; +use open_enum::open_enum; +use parking_lot::Mutex; +use std::sync::Arc; +use thiserror::Error; + +use crate::command::TdispCommandRequestPayload; +use crate::command::TdispCommandResponseGetTdiReport; +use crate::devicereport::TdispReportType; + +/// Major version of the TDISP guest-to-host interface. +pub const TDISP_INTERFACE_VERSION_MAJOR: u32 = 1; + +/// Minor version of the TDISP guest-to-host interface. +pub const TDISP_INTERFACE_VERSION_MINOR: u32 = 0; + +/// Callback for receiving TDISP commands from the guest. +pub type TdispCommandCallback = dyn Fn(&GuestToHostCommand) -> anyhow::Result<()> + Send + Sync; + +open_enum! { + /// Represents the state of the TDISP host device emulator. + pub enum TdispTdiState: u64 { + /// The TDISP state is not initialized or indeterminate. + UNINITIALIZED = 0, + + /// `TDI.Unlocked`` - The device is in its default "reset" state. Resources can be configured + /// and no functionality can be used. Attestation cannot take place until the device has + /// been locked. + UNLOCKED = 1, + + /// `TDI.Locked`` - The device resources have been locked and attestation can take place. The + /// device's resources have been mapped and configured in hardware, but the device has not + /// been attested. Private DMA and MMIO will not be functional until the resources have + /// been accepted into the guest context. Unencrypted "bounced" operations are still allowed. + LOCKED = 2, + + /// `TDI.Run`` - The device is no longer functional for unencrypted operations. Device resources + /// are locked but encrypted operations might not be functional. The device + /// will not be functional for encrypted operations until it has been fully validated by the guest + /// calling to firmware to accept resources. + RUN = 3, + } +} + +/// Trait used by the emulator to call back into the host. +pub trait TdispHostDeviceInterface: Send + Sync { + /// Bind a tdi device to the current partition. Transitions device to the Locked + /// state from Unlocked. + fn tdisp_bind_device(&mut self) -> anyhow::Result<()>; + + /// Start a bound device by transitioning it to the Run state from the Locked state. + /// This allows attestation and resources to be accepted into the guest context. + fn tdisp_start_device(&mut self) -> anyhow::Result<()>; + + /// Unbind a tdi device from the current partition. + fn tdisp_unbind_device(&mut self) -> anyhow::Result<()>; + + /// Get a device interface report for the device. + fn tdisp_get_device_report(&mut self, _report_type: TdispReportType) + -> anyhow::Result>; +} + +/// Trait added to host virtual devices to dispatch TDISP commands from guests. +pub trait TdispHostDeviceTarget: Send + Sync { + /// Dispatch a TDISP command from a guest. + fn tdisp_handle_guest_command( + &mut self, + _command: GuestToHostCommand, + ) -> anyhow::Result; +} + +/// An emulator which runs the TDISP state machine for a synthetic device. +pub struct TdispHostDeviceTargetEmulator { + machine: TdispHostStateMachine, + debug_device_id: String, +} + +impl TdispHostDeviceTargetEmulator { + /// Create a new emulator which runs the TDISP state machine for a synthetic device. + pub fn new( + host_interface: Arc>, + debug_device_id: &str, + ) -> Self { + Self { + machine: TdispHostStateMachine::new(host_interface), + debug_device_id: debug_device_id.to_owned(), + } + } + + /// Set the debug device ID string. + pub fn set_debug_device_id(&mut self, debug_device_id: &str) { + self.machine.set_debug_device_id(debug_device_id.to_owned()); + self.debug_device_id = debug_device_id.to_owned(); + } + + /// Print a debug message to the log. + fn debug_print(&self, msg: String) { + self.machine.debug_print(&msg); + } + + /// Print an error message to the log. + fn error_print(&self, msg: String) { + self.machine.error_print(&msg); + } + + /// Reset the emulator. + pub fn reset(&self) {} + + /// Get the device interface info for this device. + fn get_device_interface_info(&self) -> TdispDeviceInterfaceInfo { + TdispDeviceInterfaceInfo { + interface_version_major: TDISP_INTERFACE_VERSION_MAJOR, + interface_version_minor: TDISP_INTERFACE_VERSION_MINOR, + supported_features: 0, + tdisp_device_id: 0, + } + } +} + +impl TdispHostDeviceTarget for TdispHostDeviceTargetEmulator { + fn tdisp_handle_guest_command( + &mut self, + command: GuestToHostCommand, + ) -> anyhow::Result { + self.debug_print(format!( + "tdisp_handle_guest_command: command = {:?}", + command + )); + + let mut error = TdispGuestOperationError::Success; + let mut payload = TdispCommandResponsePayload::None; + let state_before = self.machine.state(); + match command.command_id { + TdispCommandId::GET_DEVICE_INTERFACE_INFO => { + let interface_info = self.get_device_interface_info(); + payload = TdispCommandResponsePayload::GetDeviceInterfaceInfo(interface_info); + } + TdispCommandId::BIND => { + let bind_res = self.machine.request_lock_device_resources(); + if let Err(err) = bind_res { + error = err; + } else { + payload = TdispCommandResponsePayload::None; + } + } + TdispCommandId::START_TDI => { + let start_tdi_res = self.machine.request_start_tdi(); + if let Err(err) = start_tdi_res { + error = err; + } else { + payload = TdispCommandResponsePayload::None; + } + } + TdispCommandId::UNBIND => { + let unbind_reason: TdispGuestUnbindReason = match command.payload { + TdispCommandRequestPayload::Unbind(payload) => { + TdispGuestUnbindReason(payload.unbind_reason) + } + _ => TdispGuestUnbindReason::UNKNOWN, + }; + let unbind_res = self.machine.request_unbind(unbind_reason); + if let Err(err) = unbind_res { + error = err; + } + } + TdispCommandId::GET_TDI_REPORT => { + let report_type = match &command.payload { + TdispCommandRequestPayload::GetTdiReport(payload) => { + TdispReportType(payload.report_type) + } + _ => TdispReportType::INVALID, + }; + + let report_buffer = self.machine.request_attestation_report(report_type); + if let Err(err) = report_buffer { + error = err; + } else { + payload = TdispCommandResponsePayload::GetTdiReport( + TdispCommandResponseGetTdiReport { + report_type: report_type.0, + report_buffer: report_buffer + .context("expecting report buffer from request_attestation_report") + .unwrap(), + }, + ); + } + } + TdispCommandId::UNKNOWN => { + error = TdispGuestOperationError::InvalidGuestCommandId; + } + _ => { + error = TdispGuestOperationError::InvalidGuestCommandId; + } + } + let state_after = self.machine.state(); + + match error { + TdispGuestOperationError::Success => { + self.debug_print("tdisp_handle_guest_command: Success".to_owned()); + } + _ => { + self.error_print(format!("tdisp_handle_guest_command: Error: {error:?}")); + } + } + + let resp = GuestToHostResponse { + command_id: command.command_id, + result: error, + tdi_state_before: state_before, + tdi_state_after: state_after, + payload, + }; + + self.debug_print(format!("tdisp_handle_guest_command: response = {resp:?}")); + + Ok(resp) + } +} + +/// Trait implemented by TDISP-capable devices on the client side. This includes devices that +/// are assigned to isolated partitions other than the host. +pub trait TdispClientDevice: Send + Sync { + /// Send a TDISP command to the host for this device. + /// [TDISP TODO] Async? Better handling of device_id in GuestToHostCommand? + fn tdisp_command_to_host(&self, command: GuestToHostCommand) -> anyhow::Result<()>; +} + +/// The number of states to keep in the state history for debug. +const TDISP_STATE_HISTORY_LEN: usize = 10; + +/// The reason for an `Unbind` call. This can be guest or host initiated. +/// `Unbind` can be called any time during the assignment flow. +/// This is used for telemetry and debugging. +#[derive(Debug)] +pub enum TdispUnbindReason { + /// Unknown reason. + Unknown(anyhow::Error), + + /// The device was unbound manually by the guest or host for a non-error reason. + GuestInitiated(TdispGuestUnbindReason), + + /// The device attempted to perform an invalid state transition. + ImpossibleStateTransition(anyhow::Error), + + /// The guest tried to transition the device to the Locked state while the device was not + /// in the Unlocked state. + InvalidGuestTransitionToLocked, + + /// The guest tried to transition the device to the Run state while the device was not + /// in the Locked state. + InvalidGuestTransitionToRun, + + /// The guest tried to retrieve the attestation report while the device was not in the + /// Locked or Run state. + InvalidGuestGetAttestationReportState, + + /// The guest tried to accept the attestation report while the device was not in the + /// Locked or Run state. + InvalidGuestAcceptAttestationReportState, + + /// The guest tried to unbind the device while the device with an unbind reason that is + /// not recognized as a valid guest unbind reason. The unbind still succeeds but the + /// recorded reason is discarded. + InvalidGuestUnbindReason(anyhow::Error), +} + +open_enum! { + /// For a guest initiated unbind, the guest can provide a reason for the unbind. + pub enum TdispGuestUnbindReason: u64 { + /// The guest requested to unbind the device for an unspecified reason. + UNKNOWN = 0, + + /// The guest requested to unbind the device because the device is being detached. + GRACEFUL = 1, + } +} + +/// The state machine for the TDISP assignment flow for a device on the host. Both the guest and host +/// synchronize this state machine with each other as they move through the assignment flow. +pub struct TdispHostStateMachine { + /// The current state of the TDISP device emulator. + current_state: TdispTdiState, + /// A record of the last states the device was in. + state_history: Vec, + /// The device ID of the device being assigned. + debug_device_id: String, + /// A record of the last unbind reasons for the device. + unbind_reason_history: Vec, + /// Calls back into the host to perform TDISP actions. + host_interface: Arc>, +} + +impl TdispHostStateMachine { + /// Create a new TDISP state machine with the `Unlocked` state. + pub fn new(host_interface: Arc>) -> Self { + Self { + current_state: TdispTdiState::UNLOCKED, + state_history: Vec::new(), + debug_device_id: "".to_owned(), + unbind_reason_history: Vec::new(), + host_interface, + } + } + + /// Set the debug device ID string. + pub fn set_debug_device_id(&mut self, debug_device_id: String) { + self.debug_device_id = debug_device_id; + } + + /// Print a debug message to the log. + fn debug_print(&self, msg: &str) { + tracing::debug!(msg = format!("[TdispEmu] [{}] {}", self.debug_device_id, msg)); + } + + /// Print an error message to the log. + fn error_print(&self, msg: &str) { + tracing::error!(msg = format!("[TdispEmu] [{}] {}", self.debug_device_id, msg)); + } + + /// Get the current state of the TDI. + fn state(&self) -> TdispTdiState { + self.current_state + } + + /// Check if the state machine can transition to the new state. This protects the underlying state machinery + /// while higher level transition machinery tries to avoid these conditions. If the new state is impossible, + /// `false` is returned. + fn is_valid_state_transition(&self, new_state: &TdispTdiState) -> bool { + match (self.current_state, *new_state) { + // Valid forward progress states from Unlocked -> Run + (TdispTdiState::UNLOCKED, TdispTdiState::LOCKED) => true, + (TdispTdiState::LOCKED, TdispTdiState::RUN) => true, + + // Device can always return to the Unlocked state with `Unbind` + (TdispTdiState::RUN, TdispTdiState::UNLOCKED) => true, + (TdispTdiState::LOCKED, TdispTdiState::UNLOCKED) => true, + (TdispTdiState::UNLOCKED, TdispTdiState::UNLOCKED) => true, + + // Every other state transition is invalid + _ => false, + } + } + + /// Transitions the state machine to the new state if it is valid. If the new state is invalid, + /// the state of the device is reset to the `Unlocked` state. + fn transition_state_to(&mut self, new_state: TdispTdiState) -> anyhow::Result<()> { + self.debug_print(&format!( + "Request to transition from {:?} -> {:?}", + self.current_state, new_state + )); + + // Ensure the state transition is valid + if !self.is_valid_state_transition(&new_state) { + self.debug_print(&format!( + "Invalid state transition {:?} -> {:?}", + self.current_state, new_state + )); + return Err(anyhow::anyhow!( + "Invalid state transition {:?} -> {:?}", + self.current_state, + new_state + )); + } + + // Record the state history + if self.state_history.len() == TDISP_STATE_HISTORY_LEN { + self.state_history.remove(0); + } + self.state_history.push(self.current_state); + + // Transition to the new state + self.current_state = new_state; + self.debug_print(&format!("Transitioned to {:?}", self.current_state)); + + Ok(()) + } + + /// Transition the device to the `Unlocked` state regardless of the current state. + fn unbind_all(&mut self, reason: TdispUnbindReason) -> anyhow::Result<()> { + self.debug_print(&format!("Unbind called with reason {:?}", reason)); + + // All states can be reset to the Unlocked state. This can only happen if the + // state is corrupt beyond the state machine. + if let Err(reason) = self.transition_state_to(TdispTdiState::UNLOCKED) { + return Err(anyhow::anyhow!( + "Impossible state machine violation during TDISP Unbind: {:?}", + reason + )); + } + + // Call back into the host to bind the device. + let res = self + .host_interface + .lock() + .tdisp_unbind_device() + .context("host failed to unbind TDI"); + + if let Err(e) = res { + self.error_print(format!("Failed to unbind TDI: {:?}", e).as_str()); + return Err(e); + } + + // Record the unbind reason + if self.unbind_reason_history.len() == TDISP_STATE_HISTORY_LEN { + self.unbind_reason_history.remove(0); + } + self.unbind_reason_history.push(reason); + + Ok(()) + } +} + +/// 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 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, +} + +open_enum! { + /// Error returned by TDISP operations dispatched by the guest. + pub enum TdispGuestOperationErrorCode: u64 { + /// Unknown error code. + UNKNOWN = 0, + + /// The operation was successful. + SUCCESS = 1, + + /// The current TDI state is incorrect for this operation. + INVALID_DEVICE_STATE = 2, + + /// The reason for this unbind is invalid. + INVALID_GUEST_UNBIND_REASON = 3, + + /// Invalid TDI command ID. + INVALID_GUEST_COMMAND_ID = 4, + + /// Operation requested was not implemented. + NOT_IMPLEMENTED = 5, + + /// Host failed to process command. + HOST_FAILED_TO_PROCESS_COMMAND = 6, + + /// The device was not in the Locked or Run state when the attestation report was requested. + INVALID_GUEST_ATTESTATION_REPORT_STATE = 7, + + /// Invalid attestation report type requested. + INVALID_GUEST_ATTESTATION_REPORT_TYPE = 8, + } +} + +impl From for TdispGuestOperationError { + fn from(err_code: TdispGuestOperationErrorCode) -> Self { + match err_code { + TdispGuestOperationErrorCode::UNKNOWN => TdispGuestOperationError::Unknown, + TdispGuestOperationErrorCode::SUCCESS => TdispGuestOperationError::Success, + TdispGuestOperationErrorCode::INVALID_DEVICE_STATE => { + TdispGuestOperationError::InvalidDeviceState + } + TdispGuestOperationErrorCode::INVALID_GUEST_UNBIND_REASON => { + TdispGuestOperationError::InvalidGuestUnbindReason + } + TdispGuestOperationErrorCode::INVALID_GUEST_COMMAND_ID => { + TdispGuestOperationError::InvalidGuestCommandId + } + TdispGuestOperationErrorCode::NOT_IMPLEMENTED => { + TdispGuestOperationError::NotImplemented + } + TdispGuestOperationErrorCode::HOST_FAILED_TO_PROCESS_COMMAND => { + TdispGuestOperationError::HostFailedToProcessCommand + } + TdispGuestOperationErrorCode::INVALID_GUEST_ATTESTATION_REPORT_STATE => { + TdispGuestOperationError::InvalidGuestAttestationReportState + } + TdispGuestOperationErrorCode::INVALID_GUEST_ATTESTATION_REPORT_TYPE => { + TdispGuestOperationError::InvalidGuestAttestationReportType + } + _ => TdispGuestOperationError::Unknown, + } + } +} + +impl From for TdispGuestOperationErrorCode { + fn from(err: TdispGuestOperationError) -> Self { + match err { + TdispGuestOperationError::Unknown => TdispGuestOperationErrorCode::UNKNOWN, + TdispGuestOperationError::Success => TdispGuestOperationErrorCode::SUCCESS, + TdispGuestOperationError::InvalidDeviceState => { + TdispGuestOperationErrorCode::INVALID_DEVICE_STATE + } + TdispGuestOperationError::InvalidGuestUnbindReason => { + TdispGuestOperationErrorCode::INVALID_GUEST_UNBIND_REASON + } + TdispGuestOperationError::InvalidGuestCommandId => { + TdispGuestOperationErrorCode::INVALID_GUEST_COMMAND_ID + } + TdispGuestOperationError::NotImplemented => { + TdispGuestOperationErrorCode::NOT_IMPLEMENTED + } + TdispGuestOperationError::HostFailedToProcessCommand => { + TdispGuestOperationErrorCode::HOST_FAILED_TO_PROCESS_COMMAND + } + TdispGuestOperationError::InvalidGuestAttestationReportState => { + TdispGuestOperationErrorCode::INVALID_GUEST_ATTESTATION_REPORT_STATE + } + TdispGuestOperationError::InvalidGuestAttestationReportType => { + TdispGuestOperationErrorCode::INVALID_GUEST_ATTESTATION_REPORT_TYPE + } + } + } +} + +/// 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. +pub trait TdispGuestRequestInterface { + /// Transition the device from the Unlocked to Locked state. This takes place after the + /// device has been assigned to the guest partition and the resources for the device have + /// been configured by the guest by not yet validated. + /// The device will in the `Locked` state can still perform unencrypted operations until it has + /// been transitioned to the `Run` state. The device will be attested and moved to the `Run` state. + /// + /// Attempting to transition the device to the `Locked` state while the device is not in the + /// `Unlocked` state will cause an error and unbind the device. + fn request_lock_device_resources(&mut self) -> Result<(), TdispGuestOperationError>; + + /// Transition the device from the Locked to the Run state. This takes place after the + /// device has been assigned resources and the resources have been locked to the guest. + /// The device will then transition to the `Run` state, where it will be non-functional + /// until the guest undergoes attestation and resources are accepted into the guest context. + /// + /// Attempting to transition the device to the `Run` state while the device is not in the + /// `Locked` state will cause an error and unbind the device. + fn request_start_tdi(&mut self) -> Result<(), TdispGuestOperationError>; + + /// Retrieves the attestation report for the device when the device is in the `Locked` or + /// `Run` state. The device resources will not be functional until the + /// resources have been accepted into the guest while the device is in the + /// `Run` state. + /// + /// Attempting to retrieve the attestation report while the device is not in + /// the `Locked` or `Run` state will cause an error and unbind the device. + fn request_attestation_report( + &mut self, + report_type: TdispReportType, + ) -> Result, TdispGuestOperationError>; + + /// Guest initiates a graceful unbind of the device. The guest might + /// initiate an unbind for a variety of reasons: + /// - Device is being detached/deactivated and is no longer needed in a functional state + /// - Device is powering down or entering a reset + /// + /// The device will transition to the `Unlocked` state. The guest can call + /// this function at any time in any state to reset the device to the + /// `Unlocked` state. + fn request_unbind( + &mut self, + reason: TdispGuestUnbindReason, + ) -> Result<(), TdispGuestOperationError>; +} + +impl TdispGuestRequestInterface for TdispHostStateMachine { + fn request_lock_device_resources(&mut self) -> Result<(), TdispGuestOperationError> { + // If the guest attempts to transition the device to the Locked state while the device + // is not in the Unlocked state, the device is reset to the Unlocked state. + if self.current_state != TdispTdiState::UNLOCKED { + self.error_print( + "Unlocked to Locked state called while device was not in Unlocked state.", + ); + + self.unbind_all(TdispUnbindReason::InvalidGuestTransitionToLocked) + .map_err(|_| TdispGuestOperationError::HostFailedToProcessCommand)?; + return Err(TdispGuestOperationError::InvalidDeviceState); + } + + self.debug_print( + "Device bind requested, trying to transition from Unlocked to Locked state", + ); + + // Call back into the host to bind the device. + let res = self + .host_interface + .lock() + .tdisp_bind_device() + .context("failed to call to bind TDI"); + + if let Err(e) = res { + self.error_print(format!("Failed to bind TDI: {e:?}").as_str()); + return Err(TdispGuestOperationError::HostFailedToProcessCommand); + } + + self.debug_print("Device transition from Unlocked to Locked state"); + self.transition_state_to(TdispTdiState::LOCKED).unwrap(); + Ok(()) + } + + fn request_start_tdi(&mut self) -> Result<(), TdispGuestOperationError> { + if self.current_state != TdispTdiState::LOCKED { + self.error_print("StartTDI called while device was not in Locked state."); + self.unbind_all(TdispUnbindReason::InvalidGuestTransitionToRun) + .map_err(|_| TdispGuestOperationError::HostFailedToProcessCommand)?; + + return Err(TdispGuestOperationError::InvalidDeviceState); + } + + self.debug_print("Device start requested, trying to transition from Locked to Run state"); + + // Call back into the host to bind the device. + let res = self + .host_interface + .lock() + .tdisp_start_device() + .context("failed to call to start TDI"); + + if let Err(e) = res { + self.error_print(format!("Failed to start TDI: {e:?}").as_str()); + return Err(TdispGuestOperationError::HostFailedToProcessCommand); + } + + self.debug_print("Device transition from Locked to Run state"); + self.transition_state_to(TdispTdiState::RUN).unwrap(); + + Ok(()) + } + + fn request_attestation_report( + &mut self, + report_type: TdispReportType, + ) -> Result, TdispGuestOperationError> { + if self.current_state != TdispTdiState::LOCKED && self.current_state != TdispTdiState::RUN { + self.error_print( + "Request to retrieve attestation report called while device was not in Locked or Run state.", + ); + self.unbind_all(TdispUnbindReason::InvalidGuestGetAttestationReportState) + .map_err(|_| TdispGuestOperationError::HostFailedToProcessCommand)?; + + return Err(TdispGuestOperationError::InvalidGuestAttestationReportState); + } + + if report_type == TdispReportType::INVALID { + self.error_print("Invalid report type TdispReportId::INVALID requested"); + return Err(TdispGuestOperationError::InvalidGuestAttestationReportType); + } + + let report_buffer = self + .host_interface + .lock() + .tdisp_get_device_report(report_type) + .context("failed to call to get device report from host"); + + if let Err(e) = report_buffer { + self.error_print(format!("Failed to get device report from host: {e:?}").as_str()); + return Err(TdispGuestOperationError::HostFailedToProcessCommand); + } + + self.debug_print("Retrieve attestation report called successfully"); + Ok(report_buffer.unwrap()) + } + + fn request_unbind( + &mut self, + reason: TdispGuestUnbindReason, + ) -> Result<(), TdispGuestOperationError> { + // The guest can provide a reason for the unbind. If the unbind reason isn't valid for a guest (such as + // if the guest says it is unbinding due to a host-related error), the reason is discarded and InvalidGuestUnbindReason + // is recorded in the unbind history. + let reason = match reason { + TdispGuestUnbindReason::GRACEFUL => TdispUnbindReason::GuestInitiated(reason), + _ => { + self.error_print( + format!("Invalid guest unbind reason {} requested", reason.0).as_str(), + ); + TdispUnbindReason::InvalidGuestUnbindReason(anyhow::anyhow!( + "Invalid guest unbind reason {} requested", + reason.0 + )) + } + }; + + self.debug_print(&format!( + "Guest request to unbind succeeds while device is in {:?} (reason: {:?})", + self.current_state, reason + )); + + self.unbind_all(reason) + .map_err(|_| TdispGuestOperationError::HostFailedToProcessCommand)?; + + Ok(()) + } +} diff --git a/vm/devices/tdisp/src/serialize.rs b/vm/devices/tdisp/src/serialize.rs new file mode 100644 index 0000000000..64054669c7 --- /dev/null +++ b/vm/devices/tdisp/src/serialize.rs @@ -0,0 +1,311 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, TryFromBytes}; + +use crate::command::TdispCommandRequestGetTdiReport; +use crate::command::TdispCommandRequestPayload; +use crate::command::TdispCommandRequestUnbind; +use crate::command::TdispCommandResponseGetTdiReport; +use crate::command::TdispSerializedCommandRequestGetTdiReport; + +use crate::GuestToHostCommand; +use crate::GuestToHostResponse; +use crate::TdispCommandId; +use crate::TdispCommandResponsePayload; +use crate::TdispDeviceInterfaceInfo; +use crate::TdispGuestOperationError; +use crate::TdispGuestOperationErrorCode; +use crate::TdispTdiState; + +/// Serialized form of the header for a GuestToHostCommand packet +#[repr(C)] +#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)] +pub struct GuestToHostCommandSerializedHeader { + /// The logical TDISP device ID of the device that the command is being sent to. + pub device_id: u64, + + /// The command ID of the command that is being sent. See: `TdispCommandId` + pub command_id: u64, +} + +/// Serialized form of the header for a GuestToHostResponse packet +#[repr(C)] +#[derive(Debug, Clone, Copy, FromBytes, IntoBytes, KnownLayout, Immutable)] +pub struct GuestToHostResponseSerializedHeader { + /// The command ID of the command that was processed. See: `TdispCommandId` + pub command_id: u64, + + /// The result of the command. See: `TdispGuestOperationError` + pub result: u64, + + /// The TDI state before the command was processed. See: `TdispTdiState` + pub tdi_state_before: u64, + + /// The TDI state after the command was processed. See: `TdispTdiState` + pub tdi_state_after: u64, +} + +/// Trait used to serialize a command or response header into its serializable form. +trait SerializeHeader { + type SerializedHeader; + + fn to_serializable_header(&self) -> Self::SerializedHeader; +} + +/// Trait used to deserialize a command or response header from its serializable form. +trait DeserializeHeader { + type DeserializedHeader; + + fn from_serializable_header(header: &Self::DeserializedHeader) -> Self; +} + +impl SerializeHeader for GuestToHostCommand { + type SerializedHeader = GuestToHostCommandSerializedHeader; + + fn to_serializable_header(&self) -> Self::SerializedHeader { + GuestToHostCommandSerializedHeader { + device_id: self.device_id, + command_id: self.command_id.0, + } + } +} + +impl SerializeHeader for GuestToHostResponse { + type SerializedHeader = GuestToHostResponseSerializedHeader; + + fn to_serializable_header(&self) -> Self::SerializedHeader { + let serialized_err_code: TdispGuestOperationErrorCode = self.result.into(); + GuestToHostResponseSerializedHeader { + command_id: self.command_id.0, + result: serialized_err_code.0, + tdi_state_before: self.tdi_state_before.0, + tdi_state_after: self.tdi_state_after.0, + } + } +} + +impl DeserializeHeader for GuestToHostCommand { + type DeserializedHeader = GuestToHostCommandSerializedHeader; + + fn from_serializable_header(header: &Self::DeserializedHeader) -> Self { + GuestToHostCommand { + device_id: header.device_id, + command_id: TdispCommandId(header.command_id), + payload: TdispCommandRequestPayload::None, + } + } +} + +impl DeserializeHeader for GuestToHostResponse { + type DeserializedHeader = GuestToHostResponseSerializedHeader; + + fn from_serializable_header(header: &Self::DeserializedHeader) -> Self { + let serialized_err_code: TdispGuestOperationErrorCode = + TdispGuestOperationErrorCode(header.result); + GuestToHostResponse { + command_id: TdispCommandId(header.command_id), + result: serialized_err_code.into(), + tdi_state_before: TdispTdiState(header.tdi_state_before), + tdi_state_after: TdispTdiState(header.tdi_state_after), + payload: TdispCommandResponsePayload::None, + } + } +} + +/// Trait implemented by the guest-to-host command and response structs to allow serialization and deserialization. +pub trait SerializePacket: Sized { + /// Serialize the struct to a byte vector. + fn serialize_to_bytes(self) -> Vec; + + /// Deserialize a byte slice into a struct. + fn deserialize_from_bytes(bytes: &[u8]) -> Result; +} + +impl SerializePacket for GuestToHostCommand { + fn serialize_to_bytes(self) -> Vec { + let header = self.to_serializable_header(); + let bytes = header.as_bytes(); + tracing::debug!(msg = format!("serialize_to_bytes: header={:?}", header)); + tracing::debug!(msg = format!("serialize_to_bytes: {:?}", bytes)); + + let mut bytes = bytes.to_vec(); + match self.payload { + TdispCommandRequestPayload::None => {} + TdispCommandRequestPayload::Unbind(info) => bytes.extend_from_slice(info.as_bytes()), + TdispCommandRequestPayload::GetTdiReport(info) => { + bytes.extend_from_slice(info.as_bytes()) + } + }; + + bytes + } + + fn deserialize_from_bytes(bytes: &[u8]) -> Result { + let header_length = size_of::(); + tracing::debug!(msg = format!("deserialize_from_bytes: header_length={header_length}")); + tracing::debug!(msg = format!("deserialize_from_bytes: {:?}", bytes)); + + let header_bytes = &bytes[0..header_length]; + tracing::debug!(msg = format!("deserialize_from_bytes: header_bytes={:?}", header_bytes)); + + let header = + GuestToHostCommandSerializedHeader::try_ref_from_bytes(header_bytes).map_err(|e| { + anyhow::anyhow!("failed to deserialize GuestToHostCommand header: {:?}", e) + })?; + + let payload_slice = &bytes[header_length..]; + + let mut packet: Self = GuestToHostCommand::from_serializable_header(header); + + if !payload_slice.is_empty() { + let payload = match packet.command_id { + TdispCommandId::UNBIND => TdispCommandRequestPayload::Unbind( + TdispCommandRequestUnbind::try_read_from_bytes(payload_slice).map_err(|e| { + anyhow::anyhow!("failed to deserialize TdispCommandRequestUnbind: {:?}", e) + })?, + ), + TdispCommandId::BIND => TdispCommandRequestPayload::None, + TdispCommandId::GET_DEVICE_INTERFACE_INFO => TdispCommandRequestPayload::None, + TdispCommandId::START_TDI => TdispCommandRequestPayload::None, + TdispCommandId::GET_TDI_REPORT => TdispCommandRequestPayload::GetTdiReport( + TdispCommandRequestGetTdiReport::try_read_from_bytes(payload_slice).map_err( + |e| { + anyhow::anyhow!( + "failed to deserialize TdispCommandRequestGetTdiReport: {:?}", + e + ) + }, + )?, + ), + TdispCommandId::UNKNOWN => { + return Err(anyhow::anyhow!( + "Unknown payload type for command id {:?} while deserializing GuestToHostCommand", + header.command_id + )); + } + _ => { + return Err(anyhow::anyhow!( + "Unknown payload type for command id {:?} while deserializing GuestToHostCommand", + header.command_id + )); + } + }; + + packet.payload = payload; + } + + Ok(packet) + } +} + +impl SerializePacket for GuestToHostResponse { + fn serialize_to_bytes(self) -> Vec { + let header = self.to_serializable_header(); + let bytes = header.as_bytes(); + + let mut bytes = bytes.to_vec(); + match self.payload { + TdispCommandResponsePayload::None => {} + TdispCommandResponsePayload::GetDeviceInterfaceInfo(info) => { + bytes.extend_from_slice(info.as_bytes()) + } + TdispCommandResponsePayload::GetTdiReport(info) => { + let header = TdispSerializedCommandRequestGetTdiReport { + report_type: info.report_type, + report_buffer_size: info.report_buffer.len() as u32, + }; + + bytes.extend_from_slice(header.as_bytes()); + bytes.extend_from_slice(info.report_buffer.as_bytes()); + } + }; + + bytes + } + + // [TDISP TODO] Clean up this serialization code to be a bit more generic. + fn deserialize_from_bytes(bytes: &[u8]) -> Result { + let header_length = size_of::(); + let header = + GuestToHostResponseSerializedHeader::try_ref_from_bytes(&bytes[0..header_length]) + .map_err(|e| { + anyhow::anyhow!("failed to deserialize GuestToHostResponse header: {:?}", e) + })?; + + let mut packet: Self = GuestToHostResponse::from_serializable_header(header); + + // If the result is not success, then we don't need to deserialize the payload. + match packet.result { + TdispGuestOperationError::Success => {} + _ => { + return Ok(packet); + } + } + + let payload_slice = &bytes[header_length..]; + + if !payload_slice.is_empty() { + let payload = match packet.command_id { + TdispCommandId::GET_DEVICE_INTERFACE_INFO => { + TdispCommandResponsePayload::GetDeviceInterfaceInfo( + TdispDeviceInterfaceInfo::try_read_from_bytes(payload_slice).map_err( + |e| { + anyhow::anyhow!( + "failed to deserialize TdispDeviceInterfaceInfo: {:?}", + e + ) + }, + )?, + ) + } + TdispCommandId::BIND => TdispCommandResponsePayload::None, + TdispCommandId::UNBIND => TdispCommandResponsePayload::None, + TdispCommandId::START_TDI => TdispCommandResponsePayload::None, + TdispCommandId::GET_TDI_REPORT => { + // Peel off the header from the payload + let payload_header_len = size_of::(); + let payload_header_slice = &payload_slice[0..payload_header_len]; + + // Read the header + let payload_header = + TdispSerializedCommandRequestGetTdiReport::try_read_from_bytes( + payload_header_slice, + ) + .map_err(|e| { + anyhow::anyhow!( + "failed to deserialize TdispSerializedCommandRequestGetTdiReport: {:?}", + e + ) + })?; + + // Determine the number of bytes to read from the payload for the report buffer + let payload_bytes = &payload_slice[payload_header_len + ..(payload_header_len + payload_header.report_buffer_size as usize)]; + + // Convert this to the response type + TdispCommandResponsePayload::GetTdiReport(TdispCommandResponseGetTdiReport { + report_type: payload_header.report_type, + report_buffer: payload_bytes.to_vec(), + }) + } + TdispCommandId::UNKNOWN => { + return Err(anyhow::anyhow!( + "invalid command id in GuestToHostResponse: {:?}", + header.result + )); + } + _ => { + return Err(anyhow::anyhow!( + "invalid command id in GuestToHostResponse: {:?}", + header.result + )); + } + }; + + packet.payload = payload; + } + + Ok(packet) + } +}