From f9596edd12f414f79da0372f6eec60283b1dee37 Mon Sep 17 00:00:00 2001 From: Andrew Walbran Date: Fri, 25 Oct 2024 16:36:32 +0100 Subject: [PATCH 1/8] Add hypercall-based PCI CAM for x86-64 pKVM. --- src/transport/mod.rs | 2 + src/transport/x86_64.rs | 124 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/transport/x86_64.rs diff --git a/src/transport/mod.rs b/src/transport/mod.rs index b9fb7dfa..aed22670 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -5,6 +5,8 @@ pub mod fake; pub mod mmio; pub mod pci; mod some; +#[cfg(target_arch = "x86_64")] +pub mod x86_64; use crate::{PhysAddr, Result, PAGE_SIZE}; use bitflags::{bitflags, Flags}; diff --git a/src/transport/x86_64.rs b/src/transport/x86_64.rs new file mode 100644 index 00000000..d8e1eb4c --- /dev/null +++ b/src/transport/x86_64.rs @@ -0,0 +1,124 @@ +//! x86-64 specific transports. + +use super::pci::bus::{Cam, ConfigurationAccess, DeviceFunction}; +use core::arch::asm; + +/// This CPUID returns the signature and should be used to determine if VM is running under pKVM, +/// KVM or not. See the Linux header `arch/x86/include/uapi/asm/kvm_para.h`. +const KVM_CPUID_SIGNATURE: u32 = 0x40000000; + +// See `include/uapi/linux/kvm_para.h`. (These hypercalls numbers can change depending on the +// upstream progress.) +const KVM_HC_PKVM_OP: u32 = 20; +const PKVM_GHC_IOREAD: u32 = KVM_HC_PKVM_OP + 3; +const PKVM_GHC_IOWRITE: u32 = KVM_HC_PKVM_OP + 4; + +const PKVM_SIGNATURE: &[u8] = b"PKVM"; + +/// A PCI configuration access mechanism using hypercalls implemented by the x86-64 pKVM hypervisor. +pub struct HypCam { + /// The physical base address of the PCI root complex. + phys_base: usize, + cam: Cam, +} + +impl HypCam { + /// Creates a new `HypCam` for the PCI root complex at the given physical base address. + pub fn new(phys_base: usize, cam: Cam) -> Self { + Self { phys_base, cam } + } + + /// Returns whether we are running under pKVM by checking the CPU ID signature. + pub fn is_pkvm() -> bool { + cpuid_signature() == PKVM_SIGNATURE + } +} + +impl ConfigurationAccess for HypCam { + fn read_word(&self, device_function: DeviceFunction, register_offset: u8) -> u32 { + let address = self.cam.cam_offset(device_function, register_offset); + hyp_io_read(self.phys_base + (address as usize), 4) + } + + fn write_word(&mut self, device_function: DeviceFunction, register_offset: u8, data: u32) { + let address = self.cam.cam_offset(device_function, register_offset); + hyp_io_write(self.phys_base + (address as usize), 4, data); + } + + unsafe fn unsafe_clone(&self) -> Self { + Self { + phys_base: self.phys_base, + cam: self.cam, + } + } +} + +/// Gets the signature CPU ID. +fn cpuid_signature() -> [u8; 4] { + let signature: u32; + unsafe { + // The argument for cpuid is passed via rax and in case of KVM_CPUID_SIGNATURE returned via + // rbx, rcx and rdx. Ideally using a named argument in inline asm for rbx would be more + // straightforward, but when "rbx" is directly used LLVM complains that it is used + // internally. + // + // Therefore use r8 instead and push rbx to the stack before making cpuid call, store + // rbx content to r8 as use it as inline asm output and pop the rbx. + asm!( + "push rbx", + "cpuid", + "mov r8, rbx", + "pop rbx", + in("eax") KVM_CPUID_SIGNATURE, + out("r8") signature, + out("rcx") _, + out("rdx") _, + ); + }; + signature.to_le_bytes() +} + +/// Asks the hypervisor to perform an IO read at the given physical address. +fn hyp_io_read(address: usize, size: usize) -> u32 { + // Arguments for vmcall are passed via rax, rbx, rcx and rdx. Ideally using a named argument in + // the inline asm for rbx would be more straightforward, but when "rbx" is used directly LLVM + // complains that it is used internally. + // + // Therefore use r8 temporary, push rbx to the stack, perform proper call and pop rbx + // again + let data; + unsafe { + asm!( + "push rbx", + "mov rbx, r8", + "vmcall", + "pop rbx", + inout("rax") PKVM_GHC_IOREAD => data, + in("r8") address, + in("rcx") size, + ); + } + data +} + +/// Asks the hypervisor to perform an IO write at the given physical address. +fn hyp_io_write(address: usize, size: usize, data: u32) { + unsafe { + // Arguments for vmcall are passed via rax, rbx, rcx and rdx. Ideally using a named argument + // in the inline asm for rbx would be more straightforward but when "rbx" is used directly + // used LLVM complains that it is used internally. + // + // Therefore use r8 temporary, push rbx to the stack, perform proper call and pop rbx + // again + asm!( + "push rbx", + "mov rbx, r8", + "vmcall", + "pop rbx", + in("rax") PKVM_GHC_IOWRITE, + in("r8") address, + in("rcx") size, + in("rdx") data, + ); + } +} From f3e9631bf1112252c356689f5bcdacbeac8b95c6 Mon Sep 17 00:00:00 2001 From: Andrew Walbran Date: Tue, 26 Nov 2024 17:48:15 +0000 Subject: [PATCH 2/8] Don't assume that misaligned address in error is virtual. --- src/transport/pci.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/transport/pci.rs b/src/transport/pci.rs index c33e87cf..e14112d9 100644 --- a/src/transport/pci.rs +++ b/src/transport/pci.rs @@ -447,7 +447,7 @@ fn get_bar_region( let vaddr = unsafe { H::mmio_phys_to_virt(paddr, struct_info.length as usize) }; if vaddr.as_ptr() as usize % align_of::() != 0 { return Err(VirtioPciError::Misaligned { - vaddr, + address: vaddr.as_ptr() as usize, alignment: align_of::(), }); } @@ -494,13 +494,11 @@ pub enum VirtioPciError { /// The offset for some capability was greater than the length of the BAR. #[error("Capability offset greater than BAR length.")] BarOffsetOutOfRange, - /// The virtual address was not aligned as expected. - #[error( - "Virtual address {vaddr:#018?} was not aligned to a {alignment} byte boundary as expected." - )] + /// The address was not aligned as expected. + #[error("Address {address:#018} was not aligned to a {alignment} byte boundary as expected.")] Misaligned { - /// The virtual address in question. - vaddr: NonNull, + /// The address in question. + address: usize, /// The expected alignment in bytes. alignment: usize, }, From 815a18716408e6b8676a4fc0e15e6044702ec701 Mon Sep 17 00:00:00 2001 From: Andrew Walbran Date: Tue, 26 Nov 2024 17:49:19 +0000 Subject: [PATCH 3/8] Use u64 for hyp_io_* functions. --- src/transport/x86_64.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/transport/x86_64.rs b/src/transport/x86_64.rs index d8e1eb4c..ceda094b 100644 --- a/src/transport/x86_64.rs +++ b/src/transport/x86_64.rs @@ -37,12 +37,12 @@ impl HypCam { impl ConfigurationAccess for HypCam { fn read_word(&self, device_function: DeviceFunction, register_offset: u8) -> u32 { let address = self.cam.cam_offset(device_function, register_offset); - hyp_io_read(self.phys_base + (address as usize), 4) + hyp_io_read(self.phys_base + (address as usize), 4) as u32 } fn write_word(&mut self, device_function: DeviceFunction, register_offset: u8, data: u32) { let address = self.cam.cam_offset(device_function, register_offset); - hyp_io_write(self.phys_base + (address as usize), 4, data); + hyp_io_write(self.phys_base + (address as usize), 4, data.into()); } unsafe fn unsafe_clone(&self) -> Self { @@ -79,7 +79,7 @@ fn cpuid_signature() -> [u8; 4] { } /// Asks the hypervisor to perform an IO read at the given physical address. -fn hyp_io_read(address: usize, size: usize) -> u32 { +fn hyp_io_read(address: usize, size: usize) -> u64 { // Arguments for vmcall are passed via rax, rbx, rcx and rdx. Ideally using a named argument in // the inline asm for rbx would be more straightforward, but when "rbx" is used directly LLVM // complains that it is used internally. @@ -93,7 +93,7 @@ fn hyp_io_read(address: usize, size: usize) -> u32 { "mov rbx, r8", "vmcall", "pop rbx", - inout("rax") PKVM_GHC_IOREAD => data, + inout("rax") u64::from(PKVM_GHC_IOREAD) => data, in("r8") address, in("rcx") size, ); @@ -102,7 +102,7 @@ fn hyp_io_read(address: usize, size: usize) -> u32 { } /// Asks the hypervisor to perform an IO write at the given physical address. -fn hyp_io_write(address: usize, size: usize, data: u32) { +fn hyp_io_write(address: usize, size: usize, data: u64) { unsafe { // Arguments for vmcall are passed via rax, rbx, rcx and rdx. Ideally using a named argument // in the inline asm for rbx would be more straightforward but when "rbx" is used directly From 86f2ce01b9613f8c8f1f0839ad02c2ae9e2112a2 Mon Sep 17 00:00:00 2001 From: Andrew Walbran Date: Tue, 26 Nov 2024 17:50:04 +0000 Subject: [PATCH 4/8] Add HypPciTransport. --- src/transport/pci.rs | 62 ++++---- src/transport/x86_64.rs | 342 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 372 insertions(+), 32 deletions(-) diff --git a/src/transport/pci.rs b/src/transport/pci.rs index e14112d9..32ec00c1 100644 --- a/src/transport/pci.rs +++ b/src/transport/pci.rs @@ -21,7 +21,7 @@ use core::{ use zerocopy::{FromBytes, Immutable, IntoBytes}; /// The PCI vendor ID for VirtIO devices. -const VIRTIO_VENDOR_ID: u16 = 0x1af4; +pub const VIRTIO_VENDOR_ID: u16 = 0x1af4; /// The offset to add to a VirtIO device ID to get the corresponding PCI device ID. const PCI_DEVICE_ID_OFFSET: u16 = 0x1040; @@ -35,24 +35,24 @@ const TRANSITIONAL_ENTROPY_SOURCE: u16 = 0x1005; const TRANSITIONAL_9P_TRANSPORT: u16 = 0x1009; /// The offset of the bar field within `virtio_pci_cap`. -const CAP_BAR_OFFSET: u8 = 4; +pub(crate) const CAP_BAR_OFFSET: u8 = 4; /// The offset of the offset field with `virtio_pci_cap`. -const CAP_BAR_OFFSET_OFFSET: u8 = 8; +pub(crate) const CAP_BAR_OFFSET_OFFSET: u8 = 8; /// The offset of the `length` field within `virtio_pci_cap`. -const CAP_LENGTH_OFFSET: u8 = 12; +pub(crate) const CAP_LENGTH_OFFSET: u8 = 12; /// The offset of the`notify_off_multiplier` field within `virtio_pci_notify_cap`. -const CAP_NOTIFY_OFF_MULTIPLIER_OFFSET: u8 = 16; +pub(crate) const CAP_NOTIFY_OFF_MULTIPLIER_OFFSET: u8 = 16; /// Common configuration. -const VIRTIO_PCI_CAP_COMMON_CFG: u8 = 1; +pub const VIRTIO_PCI_CAP_COMMON_CFG: u8 = 1; /// Notifications. -const VIRTIO_PCI_CAP_NOTIFY_CFG: u8 = 2; +pub const VIRTIO_PCI_CAP_NOTIFY_CFG: u8 = 2; /// ISR Status. -const VIRTIO_PCI_CAP_ISR_CFG: u8 = 3; +pub const VIRTIO_PCI_CAP_ISR_CFG: u8 = 3; /// Device specific configuration. -const VIRTIO_PCI_CAP_DEVICE_CFG: u8 = 4; +pub const VIRTIO_PCI_CAP_DEVICE_CFG: u8 = 4; -fn device_type(pci_device_id: u16) -> DeviceType { +pub(crate) fn device_type(pci_device_id: u16) -> DeviceType { match pci_device_id { TRANSITIONAL_NETWORK => DeviceType::Network, TRANSITIONAL_BLOCK => DeviceType::Block, @@ -394,34 +394,34 @@ impl Drop for PciTransport { /// `virtio_pci_common_cfg`, see 4.1.4.3 "Common configuration structure layout". #[repr(C)] -struct CommonCfg { - device_feature_select: Volatile, - device_feature: ReadOnly, - driver_feature_select: Volatile, - driver_feature: Volatile, - msix_config: Volatile, - num_queues: ReadOnly, - device_status: Volatile, - config_generation: ReadOnly, - queue_select: Volatile, - queue_size: Volatile, - queue_msix_vector: Volatile, - queue_enable: Volatile, - queue_notify_off: Volatile, - queue_desc: Volatile, - queue_driver: Volatile, - queue_device: Volatile, +pub(crate) struct CommonCfg { + pub device_feature_select: Volatile, + pub device_feature: ReadOnly, + pub driver_feature_select: Volatile, + pub driver_feature: Volatile, + pub msix_config: Volatile, + pub num_queues: ReadOnly, + pub device_status: Volatile, + pub config_generation: ReadOnly, + pub queue_select: Volatile, + pub queue_size: Volatile, + pub queue_msix_vector: Volatile, + pub queue_enable: Volatile, + pub queue_notify_off: Volatile, + pub queue_desc: Volatile, + pub queue_driver: Volatile, + pub queue_device: Volatile, } /// Information about a VirtIO structure within some BAR, as provided by a `virtio_pci_cap`. #[derive(Clone, Debug, Eq, PartialEq)] -struct VirtioCapabilityInfo { +pub(crate) struct VirtioCapabilityInfo { /// The bar in which the structure can be found. - bar: u8, + pub bar: u8, /// The offset within the bar. - offset: u32, + pub offset: u32, /// The length in bytes of the structure within the bar. - length: u32, + pub length: u32, } fn get_bar_region( diff --git a/src/transport/x86_64.rs b/src/transport/x86_64.rs index ceda094b..2d2392e2 100644 --- a/src/transport/x86_64.rs +++ b/src/transport/x86_64.rs @@ -1,7 +1,21 @@ //! x86-64 specific transports. -use super::pci::bus::{Cam, ConfigurationAccess, DeviceFunction}; +use super::{ + pci::{ + bus::{Cam, ConfigurationAccess, DeviceFunction, PciRoot, PCI_CAP_ID_VNDR}, + device_type, CommonCfg, VirtioCapabilityInfo, VirtioPciError, CAP_BAR_OFFSET, + CAP_BAR_OFFSET_OFFSET, CAP_LENGTH_OFFSET, CAP_NOTIFY_OFF_MULTIPLIER_OFFSET, + VIRTIO_PCI_CAP_COMMON_CFG, VIRTIO_PCI_CAP_DEVICE_CFG, VIRTIO_PCI_CAP_ISR_CFG, + VIRTIO_PCI_CAP_NOTIFY_CFG, VIRTIO_VENDOR_ID, + }, + DeviceStatus, DeviceType, Transport, +}; +use crate::{ + hal::{Hal, PhysAddr}, + Error, +}; use core::arch::asm; +use zerocopy::{FromBytes, Immutable, IntoBytes}; /// This CPUID returns the signature and should be used to determine if VM is running under pKVM, /// KVM or not. See the Linux header `arch/x86/include/uapi/asm/kvm_para.h`. @@ -15,6 +29,9 @@ const PKVM_GHC_IOWRITE: u32 = KVM_HC_PKVM_OP + 4; const PKVM_SIGNATURE: &[u8] = b"PKVM"; +/// The maximum number of bytes that can be read or written by a single IO hypercall. +const HYP_IO_MAX: usize = 8; + /// A PCI configuration access mechanism using hypercalls implemented by the x86-64 pKVM hypervisor. pub struct HypCam { /// The physical base address of the PCI root complex. @@ -53,6 +70,301 @@ impl ConfigurationAccess for HypCam { } } +macro_rules! configread { + ($common_cfg:expr, $field:ident) => { + $common_cfg.read(core::mem::offset_of!(CommonCfg, $field)) + }; +} + +macro_rules! configwrite { + ($common_cfg:expr, $field:ident, $value:expr) => { + $common_cfg.write(core::mem::offset_of!(CommonCfg, $field), $value) + }; +} + +/// PCI transport for VirtIO using hypercalls implemented by the x86-64 pKVM hypervisor for IO BARs. +#[derive(Debug)] +pub struct HypPciTransport { + device_type: DeviceType, + /// The bus, device and function identifier for the VirtIO device. + device_function: DeviceFunction, + /// The common configuration structure within some BAR. + common_cfg: HypIoRegion, + /// The start of the queue notification region within some BAR. + notify_region: HypIoRegion, + notify_off_multiplier: u32, + /// The ISR status register within some BAR. + isr_status: HypIoRegion, + /// The VirtIO device-specific configuration within some BAR. + config_space: Option, +} + +impl HypPciTransport { + /// Constructs a new x86-64 pKVM PCI VirtIO transport for the given device function on the given + /// PCI root controller. + pub fn new( + root: &mut PciRoot, + device_function: DeviceFunction, + ) -> Result { + let device_vendor = root.configuration_access.read_word(device_function, 0); + let device_id = (device_vendor >> 16) as u16; + let vendor_id = device_vendor as u16; + if vendor_id != VIRTIO_VENDOR_ID { + return Err(VirtioPciError::InvalidVendorId(vendor_id)); + } + let device_type = device_type(device_id); + + // Find the PCI capabilities we need. + let mut common_cfg = None; + let mut notify_cfg = None; + let mut notify_off_multiplier = 0; + let mut isr_cfg = None; + let mut device_cfg = None; + for capability in root.capabilities(device_function) { + if capability.id != PCI_CAP_ID_VNDR { + continue; + } + let cap_len = capability.private_header as u8; + let cfg_type = (capability.private_header >> 8) as u8; + if cap_len < 16 { + continue; + } + let struct_info = VirtioCapabilityInfo { + bar: root + .configuration_access + .read_word(device_function, capability.offset + CAP_BAR_OFFSET) + as u8, + offset: root + .configuration_access + .read_word(device_function, capability.offset + CAP_BAR_OFFSET_OFFSET), + length: root + .configuration_access + .read_word(device_function, capability.offset + CAP_LENGTH_OFFSET), + }; + + match cfg_type { + VIRTIO_PCI_CAP_COMMON_CFG if common_cfg.is_none() => { + common_cfg = Some(struct_info); + } + VIRTIO_PCI_CAP_NOTIFY_CFG if cap_len >= 20 && notify_cfg.is_none() => { + notify_cfg = Some(struct_info); + notify_off_multiplier = root.configuration_access.read_word( + device_function, + capability.offset + CAP_NOTIFY_OFF_MULTIPLIER_OFFSET, + ); + } + VIRTIO_PCI_CAP_ISR_CFG if isr_cfg.is_none() => { + isr_cfg = Some(struct_info); + } + VIRTIO_PCI_CAP_DEVICE_CFG if device_cfg.is_none() => { + device_cfg = Some(struct_info); + } + _ => {} + } + } + + let common_cfg = get_bar_region::( + root, + device_function, + &common_cfg.ok_or(VirtioPciError::MissingCommonConfig)?, + )?; + + let notify_cfg = notify_cfg.ok_or(VirtioPciError::MissingNotifyConfig)?; + if notify_off_multiplier % 2 != 0 { + return Err(VirtioPciError::InvalidNotifyOffMultiplier( + notify_off_multiplier, + )); + } + let notify_region = get_bar_region::(root, device_function, ¬ify_cfg)?; + + let isr_status = get_bar_region::( + root, + device_function, + &isr_cfg.ok_or(VirtioPciError::MissingIsrConfig)?, + )?; + + let config_space = if let Some(device_cfg) = device_cfg { + Some(get_bar_region::( + root, + device_function, + &device_cfg, + )?) + } else { + None + }; + + Ok(Self { + device_type, + device_function, + common_cfg, + notify_region, + notify_off_multiplier, + isr_status, + config_space, + }) + } +} + +impl Transport for HypPciTransport { + fn device_type(&self) -> DeviceType { + self.device_type + } + + fn read_device_features(&mut self) -> u64 { + configwrite!(self.common_cfg, device_feature_select, 0); + let device_features_low: u32 = configread!(self.common_cfg, device_feature); + configwrite!(self.common_cfg, device_feature_select, 1); + let device_features_high: u32 = configread!(self.common_cfg, device_feature); + (device_features_high as u64) << 32 | device_features_low as u64 + } + + fn write_driver_features(&mut self, driver_features: u64) { + configwrite!(self.common_cfg, driver_feature_select, 0); + configwrite!(self.common_cfg, driver_feature, driver_features as u32); + configwrite!(self.common_cfg, driver_feature_select, 1); + configwrite!( + self.common_cfg, + driver_feature, + (driver_features >> 32) as u32 + ); + } + + fn max_queue_size(&mut self, queue: u16) -> u32 { + configwrite!(self.common_cfg, queue_select, queue); + let queue_size: u16 = configread!(self.common_cfg, queue_size); + queue_size.into() + } + + fn notify(&mut self, queue: u16) { + configwrite!(self.common_cfg, queue_select, queue); + // TODO: Consider caching this somewhere (per queue). + let queue_notify_off: u16 = configread!(self.common_cfg, queue_notify_off); + + let offset_bytes = usize::from(queue_notify_off) * self.notify_off_multiplier as usize; + self.notify_region.write(offset_bytes, queue); + } + + fn get_status(&self) -> DeviceStatus { + let status: u8 = configread!(self.common_cfg, device_status); + DeviceStatus::from_bits_truncate(status.into()) + } + + fn set_status(&mut self, status: DeviceStatus) { + configwrite!(self.common_cfg, device_status, status.bits() as u8); + } + + fn set_guest_page_size(&mut self, _guest_page_size: u32) { + // No-op, the PCI transport doesn't care. + } + + fn requires_legacy_layout(&self) -> bool { + false + } + + fn queue_set( + &mut self, + queue: u16, + size: u32, + descriptors: PhysAddr, + driver_area: PhysAddr, + device_area: PhysAddr, + ) { + configwrite!(self.common_cfg, queue_select, queue); + configwrite!(self.common_cfg, queue_size, size as u16); + configwrite!(self.common_cfg, queue_desc, descriptors as u64); + configwrite!(self.common_cfg, queue_driver, driver_area as u64); + configwrite!(self.common_cfg, queue_device, device_area as u64); + configwrite!(self.common_cfg, queue_enable, 1); + } + + fn queue_unset(&mut self, _queue: u16) { + // The VirtIO spec doesn't allow queues to be unset once they have been set up for the PCI + // transport, so this is a no-op. + } + + fn queue_used(&mut self, queue: u16) -> bool { + configwrite!(self.common_cfg, queue_select, queue); + let queue_enable: u16 = configread!(self.common_cfg, queue_enable); + queue_enable == 1 + } + + fn ack_interrupt(&mut self) -> bool { + // Safe because the common config pointer is valid and we checked in get_bar_region that it + // was aligned. + // Reading the ISR status resets it to 0 and causes the device to de-assert the interrupt. + let isr_status: u8 = self.isr_status.read(0); + // TODO: Distinguish between queue interrupt and device configuration interrupt. + isr_status & 0x3 != 0 + } + + fn read_config_generation(&self) -> u32 { + configread!(self.common_cfg, config_generation) + } + + fn read_config_space(&self, offset: usize) -> Result { + assert!(align_of::() <= 4, + "Driver expected config space alignment of {} bytes, but VirtIO only guarantees 4 byte alignment.", + align_of::()); + assert_eq!(offset % align_of::(), 0); + + let config_space = self.config_space.ok_or(Error::ConfigSpaceMissing)?; + if config_space.size < offset + size_of::() { + Err(Error::ConfigSpaceTooSmall) + } else { + Ok(config_space.read(offset)) + } + } + + fn write_config_space( + &mut self, + offset: usize, + value: T, + ) -> Result<(), Error> { + assert!(align_of::() <= 4, + "Driver expected config space alignment of {} bytes, but VirtIO only guarantees 4 byte alignment.", + align_of::()); + assert_eq!(offset % align_of::(), 0); + + let config_space = self.config_space.ok_or(Error::ConfigSpaceMissing)?; + if config_space.size < offset + size_of::() { + Err(Error::ConfigSpaceTooSmall) + } else { + config_space.write(offset, value); + Ok(()) + } + } +} + +fn get_bar_region( + root: &mut PciRoot, + device_function: DeviceFunction, + struct_info: &VirtioCapabilityInfo, +) -> Result { + let bar_info = root.bar_info(device_function, struct_info.bar)?; + let (bar_address, bar_size) = bar_info + .memory_address_size() + .ok_or(VirtioPciError::UnexpectedIoBar)?; + if bar_address == 0 { + return Err(VirtioPciError::BarNotAllocated(struct_info.bar)); + } + if struct_info.offset + struct_info.length > bar_size + || size_of::() > struct_info.length as usize + { + return Err(VirtioPciError::BarOffsetOutOfRange); + } + let paddr = bar_address as PhysAddr + struct_info.offset as PhysAddr; + if paddr % align_of::() != 0 { + return Err(VirtioPciError::Misaligned { + address: paddr, + alignment: align_of::(), + }); + } + Ok(HypIoRegion { + paddr, + size: struct_info.length as usize, + }) +} + /// Gets the signature CPU ID. fn cpuid_signature() -> [u8; 4] { let signature: u32; @@ -122,3 +434,31 @@ fn hyp_io_write(address: usize, size: usize, data: u64) { ); } } + +/// A region of physical address space which may be accessed by IO read and/or write hypercalls. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct HypIoRegion { + /// The physical address of the start of the IO region. + paddr: usize, + /// The size of the IO region in bytes. + size: usize, +} + +impl HypIoRegion { + fn read(self, offset: usize) -> T { + assert!(offset + size_of::() <= self.size); + assert!(size_of::() < HYP_IO_MAX); + + let data = hyp_io_read(self.paddr + offset, size_of::()); + T::read_from_prefix(data.as_bytes()).unwrap().0 + } + + fn write(self, offset: usize, value: T) { + assert!(offset + size_of::() <= self.size); + assert!(size_of::() < HYP_IO_MAX); + + let mut data = 0; + data.as_mut_bytes()[..size_of::()].copy_from_slice(value.as_bytes()); + hyp_io_write(self.paddr + offset, size_of::(), data); + } +} From 38c1c30fc1dd63c90bf6622e68ff2e8b2f445a7a Mon Sep 17 00:00:00 2001 From: Andrew Walbran Date: Tue, 26 Nov 2024 17:55:47 +0000 Subject: [PATCH 5/8] Include HypPciTransport in SomeTransport too. --- src/transport/some.rs | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/transport/some.rs b/src/transport/some.rs index 1d824b13..30bb324d 100644 --- a/src/transport/some.rs +++ b/src/transport/some.rs @@ -10,6 +10,9 @@ pub enum SomeTransport { Mmio(MmioTransport), /// A PCI transport. Pci(PciTransport), + /// An x86-64 pKVM PCI transport. + #[cfg(target_arch = "x86_64")] + HypPci(super::x86_64::HypPciTransport), } impl From for SomeTransport { @@ -29,6 +32,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.device_type(), Self::Pci(pci) => pci.device_type(), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.device_type(), } } @@ -36,6 +41,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.read_device_features(), Self::Pci(pci) => pci.read_device_features(), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.read_device_features(), } } @@ -43,6 +50,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.write_driver_features(driver_features), Self::Pci(pci) => pci.write_driver_features(driver_features), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.write_driver_features(driver_features), } } @@ -50,6 +59,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.max_queue_size(queue), Self::Pci(pci) => pci.max_queue_size(queue), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.max_queue_size(queue), } } @@ -57,6 +68,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.notify(queue), Self::Pci(pci) => pci.notify(queue), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.notify(queue), } } @@ -64,6 +77,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.get_status(), Self::Pci(pci) => pci.get_status(), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.get_status(), } } @@ -71,6 +86,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.set_status(status), Self::Pci(pci) => pci.set_status(status), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.set_status(status), } } @@ -78,6 +95,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.set_guest_page_size(guest_page_size), Self::Pci(pci) => pci.set_guest_page_size(guest_page_size), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.set_guest_page_size(guest_page_size), } } @@ -85,6 +104,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.requires_legacy_layout(), Self::Pci(pci) => pci.requires_legacy_layout(), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.requires_legacy_layout(), } } @@ -99,6 +120,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.queue_set(queue, size, descriptors, driver_area, device_area), Self::Pci(pci) => pci.queue_set(queue, size, descriptors, driver_area, device_area), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.queue_set(queue, size, descriptors, driver_area, device_area), } } @@ -106,6 +129,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.queue_unset(queue), Self::Pci(pci) => pci.queue_unset(queue), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.queue_unset(queue), } } @@ -113,6 +138,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.queue_used(queue), Self::Pci(pci) => pci.queue_used(queue), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.queue_used(queue), } } @@ -120,6 +147,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.ack_interrupt(), Self::Pci(pci) => pci.ack_interrupt(), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.ack_interrupt(), } } @@ -127,6 +156,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.read_config_generation(), Self::Pci(pci) => pci.read_config_generation(), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.read_config_generation(), } } @@ -134,6 +165,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.read_config_space(offset), Self::Pci(pci) => pci.read_config_space(offset), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.read_config_space(offset), } } @@ -145,6 +178,8 @@ impl Transport for SomeTransport { match self { Self::Mmio(mmio) => mmio.write_config_space(offset, value), Self::Pci(pci) => pci.write_config_space(offset, value), + #[cfg(target_arch = "x86_64")] + Self::HypPci(pci) => pci.write_config_space(offset, value), } } } From 7831d8500b9f98661203653703bfbd711d2fdd16 Mon Sep 17 00:00:00 2001 From: Andrew Walbran Date: Wed, 4 Dec 2024 09:35:23 +0000 Subject: [PATCH 6/8] Move pKVM hypercalls to a submodule. --- src/transport/x86_64.rs | 84 ++---------------------------- src/transport/x86_64/hypercalls.rs | 83 +++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 81 deletions(-) create mode 100644 src/transport/x86_64/hypercalls.rs diff --git a/src/transport/x86_64.rs b/src/transport/x86_64.rs index 2d2392e2..63590d83 100644 --- a/src/transport/x86_64.rs +++ b/src/transport/x86_64.rs @@ -1,5 +1,7 @@ //! x86-64 specific transports. +mod hypercalls; + use super::{ pci::{ bus::{Cam, ConfigurationAccess, DeviceFunction, PciRoot, PCI_CAP_ID_VNDR}, @@ -14,19 +16,9 @@ use crate::{ hal::{Hal, PhysAddr}, Error, }; -use core::arch::asm; +use hypercalls::{cpuid_signature, hyp_io_read, hyp_io_write}; use zerocopy::{FromBytes, Immutable, IntoBytes}; -/// This CPUID returns the signature and should be used to determine if VM is running under pKVM, -/// KVM or not. See the Linux header `arch/x86/include/uapi/asm/kvm_para.h`. -const KVM_CPUID_SIGNATURE: u32 = 0x40000000; - -// See `include/uapi/linux/kvm_para.h`. (These hypercalls numbers can change depending on the -// upstream progress.) -const KVM_HC_PKVM_OP: u32 = 20; -const PKVM_GHC_IOREAD: u32 = KVM_HC_PKVM_OP + 3; -const PKVM_GHC_IOWRITE: u32 = KVM_HC_PKVM_OP + 4; - const PKVM_SIGNATURE: &[u8] = b"PKVM"; /// The maximum number of bytes that can be read or written by a single IO hypercall. @@ -365,76 +357,6 @@ fn get_bar_region( }) } -/// Gets the signature CPU ID. -fn cpuid_signature() -> [u8; 4] { - let signature: u32; - unsafe { - // The argument for cpuid is passed via rax and in case of KVM_CPUID_SIGNATURE returned via - // rbx, rcx and rdx. Ideally using a named argument in inline asm for rbx would be more - // straightforward, but when "rbx" is directly used LLVM complains that it is used - // internally. - // - // Therefore use r8 instead and push rbx to the stack before making cpuid call, store - // rbx content to r8 as use it as inline asm output and pop the rbx. - asm!( - "push rbx", - "cpuid", - "mov r8, rbx", - "pop rbx", - in("eax") KVM_CPUID_SIGNATURE, - out("r8") signature, - out("rcx") _, - out("rdx") _, - ); - }; - signature.to_le_bytes() -} - -/// Asks the hypervisor to perform an IO read at the given physical address. -fn hyp_io_read(address: usize, size: usize) -> u64 { - // Arguments for vmcall are passed via rax, rbx, rcx and rdx. Ideally using a named argument in - // the inline asm for rbx would be more straightforward, but when "rbx" is used directly LLVM - // complains that it is used internally. - // - // Therefore use r8 temporary, push rbx to the stack, perform proper call and pop rbx - // again - let data; - unsafe { - asm!( - "push rbx", - "mov rbx, r8", - "vmcall", - "pop rbx", - inout("rax") u64::from(PKVM_GHC_IOREAD) => data, - in("r8") address, - in("rcx") size, - ); - } - data -} - -/// Asks the hypervisor to perform an IO write at the given physical address. -fn hyp_io_write(address: usize, size: usize, data: u64) { - unsafe { - // Arguments for vmcall are passed via rax, rbx, rcx and rdx. Ideally using a named argument - // in the inline asm for rbx would be more straightforward but when "rbx" is used directly - // used LLVM complains that it is used internally. - // - // Therefore use r8 temporary, push rbx to the stack, perform proper call and pop rbx - // again - asm!( - "push rbx", - "mov rbx, r8", - "vmcall", - "pop rbx", - in("rax") PKVM_GHC_IOWRITE, - in("r8") address, - in("rcx") size, - in("rdx") data, - ); - } -} - /// A region of physical address space which may be accessed by IO read and/or write hypercalls. #[derive(Clone, Copy, Debug, Eq, PartialEq)] struct HypIoRegion { diff --git a/src/transport/x86_64/hypercalls.rs b/src/transport/x86_64/hypercalls.rs new file mode 100644 index 00000000..5b5b686a --- /dev/null +++ b/src/transport/x86_64/hypercalls.rs @@ -0,0 +1,83 @@ +//! Hypercalls for x86-64 pKVM. + +use core::arch::asm; + +/// This CPUID returns the signature and should be used to determine if VM is running under pKVM, +/// KVM or not. See the Linux header `arch/x86/include/uapi/asm/kvm_para.h`. +const KVM_CPUID_SIGNATURE: u32 = 0x40000000; + +// See `include/uapi/linux/kvm_para.h`. (These hypercalls numbers can change depending on the +// upstream progress.) +const KVM_HC_PKVM_OP: u32 = 20; +const PKVM_GHC_IOREAD: u32 = KVM_HC_PKVM_OP + 3; +const PKVM_GHC_IOWRITE: u32 = KVM_HC_PKVM_OP + 4; + +/// Gets the signature CPU ID. +pub fn cpuid_signature() -> [u8; 4] { + let signature: u32; + unsafe { + // The argument for cpuid is passed via rax and in case of KVM_CPUID_SIGNATURE returned via + // rbx, rcx and rdx. Ideally using a named argument in inline asm for rbx would be more + // straightforward, but when "rbx" is directly used LLVM complains that it is used + // internally. + // + // Therefore use r8 instead and push rbx to the stack before making cpuid call, store + // rbx content to r8 as use it as inline asm output and pop the rbx. + asm!( + "push rbx", + "cpuid", + "mov r8, rbx", + "pop rbx", + in("eax") KVM_CPUID_SIGNATURE, + out("r8") signature, + out("rcx") _, + out("rdx") _, + ); + }; + signature.to_le_bytes() +} + +/// Asks the hypervisor to perform an IO read at the given physical address. +pub fn hyp_io_read(address: usize, size: usize) -> u64 { + // Arguments for vmcall are passed via rax, rbx, rcx and rdx. Ideally using a named argument in + // the inline asm for rbx would be more straightforward, but when "rbx" is used directly LLVM + // complains that it is used internally. + // + // Therefore use r8 temporary, push rbx to the stack, perform proper call and pop rbx + // again + let data; + unsafe { + asm!( + "push rbx", + "mov rbx, r8", + "vmcall", + "pop rbx", + inout("rax") u64::from(PKVM_GHC_IOREAD) => data, + in("r8") address, + in("rcx") size, + ); + } + data +} + +/// Asks the hypervisor to perform an IO write at the given physical address. +pub fn hyp_io_write(address: usize, size: usize, data: u64) { + unsafe { + // Arguments for vmcall are passed via rax, rbx, rcx and rdx. Ideally using a named argument + // in the inline asm for rbx would be more straightforward but when "rbx" is used directly + // used LLVM complains that it is used internally. + // + // Therefore use r8 temporary, push rbx to the stack, perform proper call and pop rbx + // again + asm!( + "push rbx", + "mov rbx, r8", + "vmcall", + "pop rbx", + in("rax") PKVM_GHC_IOWRITE, + in("r8") address, + in("rcx") size, + in("rdx") data, + ); + } +} From b30fcd4b9f97992a20e6e91e812d8a1b8ff3a7b2 Mon Sep 17 00:00:00 2001 From: Andrew Walbran Date: Wed, 4 Dec 2024 09:39:37 +0000 Subject: [PATCH 7/8] Move x86-64 pKVM PCI CAM to submodule. --- src/transport/x86_64.rs | 46 ++++--------------------------------- src/transport/x86_64/cam.rs | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 42 deletions(-) create mode 100644 src/transport/x86_64/cam.rs diff --git a/src/transport/x86_64.rs b/src/transport/x86_64.rs index 63590d83..cbd1850c 100644 --- a/src/transport/x86_64.rs +++ b/src/transport/x86_64.rs @@ -1,10 +1,11 @@ //! x86-64 specific transports. +mod cam; mod hypercalls; use super::{ pci::{ - bus::{Cam, ConfigurationAccess, DeviceFunction, PciRoot, PCI_CAP_ID_VNDR}, + bus::{ConfigurationAccess, DeviceFunction, PciRoot, PCI_CAP_ID_VNDR}, device_type, CommonCfg, VirtioCapabilityInfo, VirtioPciError, CAP_BAR_OFFSET, CAP_BAR_OFFSET_OFFSET, CAP_LENGTH_OFFSET, CAP_NOTIFY_OFF_MULTIPLIER_OFFSET, VIRTIO_PCI_CAP_COMMON_CFG, VIRTIO_PCI_CAP_DEVICE_CFG, VIRTIO_PCI_CAP_ISR_CFG, @@ -16,52 +17,13 @@ use crate::{ hal::{Hal, PhysAddr}, Error, }; -use hypercalls::{cpuid_signature, hyp_io_read, hyp_io_write}; +pub use cam::HypCam; +use hypercalls::{hyp_io_read, hyp_io_write}; use zerocopy::{FromBytes, Immutable, IntoBytes}; -const PKVM_SIGNATURE: &[u8] = b"PKVM"; - /// The maximum number of bytes that can be read or written by a single IO hypercall. const HYP_IO_MAX: usize = 8; -/// A PCI configuration access mechanism using hypercalls implemented by the x86-64 pKVM hypervisor. -pub struct HypCam { - /// The physical base address of the PCI root complex. - phys_base: usize, - cam: Cam, -} - -impl HypCam { - /// Creates a new `HypCam` for the PCI root complex at the given physical base address. - pub fn new(phys_base: usize, cam: Cam) -> Self { - Self { phys_base, cam } - } - - /// Returns whether we are running under pKVM by checking the CPU ID signature. - pub fn is_pkvm() -> bool { - cpuid_signature() == PKVM_SIGNATURE - } -} - -impl ConfigurationAccess for HypCam { - fn read_word(&self, device_function: DeviceFunction, register_offset: u8) -> u32 { - let address = self.cam.cam_offset(device_function, register_offset); - hyp_io_read(self.phys_base + (address as usize), 4) as u32 - } - - fn write_word(&mut self, device_function: DeviceFunction, register_offset: u8, data: u32) { - let address = self.cam.cam_offset(device_function, register_offset); - hyp_io_write(self.phys_base + (address as usize), 4, data.into()); - } - - unsafe fn unsafe_clone(&self) -> Self { - Self { - phys_base: self.phys_base, - cam: self.cam, - } - } -} - macro_rules! configread { ($common_cfg:expr, $field:ident) => { $common_cfg.read(core::mem::offset_of!(CommonCfg, $field)) diff --git a/src/transport/x86_64/cam.rs b/src/transport/x86_64/cam.rs new file mode 100644 index 00000000..30438c03 --- /dev/null +++ b/src/transport/x86_64/cam.rs @@ -0,0 +1,42 @@ +use super::hypercalls::{cpuid_signature, hyp_io_read, hyp_io_write}; +use crate::transport::pci::bus::{Cam, ConfigurationAccess, DeviceFunction}; + +const PKVM_SIGNATURE: &[u8] = b"PKVM"; + +/// A PCI configuration access mechanism using hypercalls implemented by the x86-64 pKVM hypervisor. +pub struct HypCam { + /// The physical base address of the PCI root complex. + phys_base: usize, + cam: Cam, +} + +impl HypCam { + /// Creates a new `HypCam` for the PCI root complex at the given physical base address. + pub fn new(phys_base: usize, cam: Cam) -> Self { + Self { phys_base, cam } + } + + /// Returns whether we are running under pKVM by checking the CPU ID signature. + pub fn is_pkvm() -> bool { + cpuid_signature() == PKVM_SIGNATURE + } +} + +impl ConfigurationAccess for HypCam { + fn read_word(&self, device_function: DeviceFunction, register_offset: u8) -> u32 { + let address = self.cam.cam_offset(device_function, register_offset); + hyp_io_read(self.phys_base + (address as usize), 4) as u32 + } + + fn write_word(&mut self, device_function: DeviceFunction, register_offset: u8, data: u32) { + let address = self.cam.cam_offset(device_function, register_offset); + hyp_io_write(self.phys_base + (address as usize), 4, data.into()); + } + + unsafe fn unsafe_clone(&self) -> Self { + Self { + phys_base: self.phys_base, + cam: self.cam, + } + } +} From 38f3e5182389f022b5b0f1fec1e339f585263521 Mon Sep 17 00:00:00 2001 From: Andrew Walbran Date: Wed, 4 Dec 2024 09:43:45 +0000 Subject: [PATCH 8/8] Move HypIoRegion to hypercalls submodule too. --- src/transport/x86_64.rs | 33 +----------------------------- src/transport/x86_64/hypercalls.rs | 32 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/transport/x86_64.rs b/src/transport/x86_64.rs index cbd1850c..f12f7f91 100644 --- a/src/transport/x86_64.rs +++ b/src/transport/x86_64.rs @@ -18,12 +18,9 @@ use crate::{ Error, }; pub use cam::HypCam; -use hypercalls::{hyp_io_read, hyp_io_write}; +use hypercalls::HypIoRegion; use zerocopy::{FromBytes, Immutable, IntoBytes}; -/// The maximum number of bytes that can be read or written by a single IO hypercall. -const HYP_IO_MAX: usize = 8; - macro_rules! configread { ($common_cfg:expr, $field:ident) => { $common_cfg.read(core::mem::offset_of!(CommonCfg, $field)) @@ -318,31 +315,3 @@ fn get_bar_region( size: struct_info.length as usize, }) } - -/// A region of physical address space which may be accessed by IO read and/or write hypercalls. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -struct HypIoRegion { - /// The physical address of the start of the IO region. - paddr: usize, - /// The size of the IO region in bytes. - size: usize, -} - -impl HypIoRegion { - fn read(self, offset: usize) -> T { - assert!(offset + size_of::() <= self.size); - assert!(size_of::() < HYP_IO_MAX); - - let data = hyp_io_read(self.paddr + offset, size_of::()); - T::read_from_prefix(data.as_bytes()).unwrap().0 - } - - fn write(self, offset: usize, value: T) { - assert!(offset + size_of::() <= self.size); - assert!(size_of::() < HYP_IO_MAX); - - let mut data = 0; - data.as_mut_bytes()[..size_of::()].copy_from_slice(value.as_bytes()); - hyp_io_write(self.paddr + offset, size_of::(), data); - } -} diff --git a/src/transport/x86_64/hypercalls.rs b/src/transport/x86_64/hypercalls.rs index 5b5b686a..38962a29 100644 --- a/src/transport/x86_64/hypercalls.rs +++ b/src/transport/x86_64/hypercalls.rs @@ -1,6 +1,7 @@ //! Hypercalls for x86-64 pKVM. use core::arch::asm; +use zerocopy::{FromBytes, Immutable, IntoBytes}; /// This CPUID returns the signature and should be used to determine if VM is running under pKVM, /// KVM or not. See the Linux header `arch/x86/include/uapi/asm/kvm_para.h`. @@ -12,6 +13,9 @@ const KVM_HC_PKVM_OP: u32 = 20; const PKVM_GHC_IOREAD: u32 = KVM_HC_PKVM_OP + 3; const PKVM_GHC_IOWRITE: u32 = KVM_HC_PKVM_OP + 4; +/// The maximum number of bytes that can be read or written by a single IO hypercall. +const HYP_IO_MAX: usize = 8; + /// Gets the signature CPU ID. pub fn cpuid_signature() -> [u8; 4] { let signature: u32; @@ -81,3 +85,31 @@ pub fn hyp_io_write(address: usize, size: usize, data: u64) { ); } } + +/// A region of physical address space which may be accessed by IO read and/or write hypercalls. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct HypIoRegion { + /// The physical address of the start of the IO region. + pub paddr: usize, + /// The size of the IO region in bytes. + pub size: usize, +} + +impl HypIoRegion { + pub fn read(self, offset: usize) -> T { + assert!(offset + size_of::() <= self.size); + assert!(size_of::() < HYP_IO_MAX); + + let data = hyp_io_read(self.paddr + offset, size_of::()); + T::read_from_prefix(data.as_bytes()).unwrap().0 + } + + pub fn write(self, offset: usize, value: T) { + assert!(offset + size_of::() <= self.size); + assert!(size_of::() < HYP_IO_MAX); + + let mut data = 0; + data.as_mut_bytes()[..size_of::()].copy_from_slice(value.as_bytes()); + hyp_io_write(self.paddr + offset, size_of::(), data); + } +}