From 991b84473e4cb9a0d9d0f63a8104cd8f497191d5 Mon Sep 17 00:00:00 2001 From: changyuanl Date: Sat, 29 Aug 2026 23:23:36 -0700 Subject: [PATCH 1/5] fix(vfio): fix bar splitting algorithm Assisted-by: Antigravity:Gemini-3.7-Flash Signed-off-by: Changyuan Lyu --- alioth/src/vfio/pci.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/alioth/src/vfio/pci.rs b/alioth/src/vfio/pci.rs index df2e15d1..8cf677db 100644 --- a/alioth/src/vfio/pci.rs +++ b/alioth/src/vfio/pci.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::cmp::min; +use std::cmp::{max, min}; use std::iter::zip; use std::mem::size_of; use std::ops::Range; @@ -94,18 +94,19 @@ where { let table_pages = round_up_range(table_range.clone()); let pba_pages = round_up_range(pba_range.clone()); - let (excluded_page1, excluded_page2) = if table_pages.clone().eq(0..0) { + let (excluded_page1, excluded_page2) = if table_pages == (0..0) { (0..0, pba_pages) - } else if pba_pages.clone().eq(0..0) { + } else if pba_pages == (0..0) { (0..0, table_pages) - } else if table_pages.start <= pba_pages.start && table_pages.end >= pba_pages.start { - (0..0, table_pages.start..pba_pages.end) - } else if pba_pages.start <= table_pages.start && pba_pages.end >= table_pages.start { - (0..0, pba_pages.start..table_pages.end) - } else if table_pages.end < pba_pages.start { + } else if table_pages.end <= pba_pages.start { (table_pages, pba_pages) - } else { + } else if pba_pages.end <= table_pages.start { (pba_pages, table_pages) + } else { + ( + 0..0, + min(table_pages.start, pba_pages.start)..max(table_pages.end, pba_pages.end), + ) }; let mut region = MemRegion { callbacks: Mutex::new(vec![]), From f144d60fbb2f837b918a1a5323539f76b07cf5ea Mon Sep 17 00:00:00 2001 From: changyuanl Date: Sat, 29 Aug 2026 23:23:36 -0700 Subject: [PATCH 2/5] fix(vfio): avoid panics and infinite loops caused by malformed PCI caps Assisted-by: Antigravity:Gemini-3.7-Flash Signed-off-by: Changyuan Lyu --- alioth/src/vfio/pci.rs | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/alioth/src/vfio/pci.rs b/alioth/src/vfio/pci.rs index 8cf677db..e446f237 100644 --- a/alioth/src/vfio/pci.rs +++ b/alioth/src/vfio/pci.rs @@ -425,32 +425,38 @@ where log::error!("{}: invalid cap offset: {cap_offset:#x}", cdev.name); break; }; - let (cap_header, _) = PciCapHdr::ref_from_prefix(cap_buf).unwrap(); + let Ok((cap_header, _)) = PciCapHdr::ref_from_prefix(cap_buf) else { + log::error!( + "{}: invalid cap header at offset: {cap_offset:#x}", + cdev.name + ); + break; + }; if cap_header.id == PciCapId::MSIX { - let Ok((mut c, _)) = MsixCap::read_from_prefix(cap_buf) else { + if let Ok((mut c, _)) = MsixCap::read_from_prefix(cap_buf) { + c.control.set_enabled(false); + c.control.set_masked(false); + msix_info = Some((cap_offset, c.clone())); + } else { log::error!( "{}: MSIX capability is at an invalid offset: {cap_offset:#x}", cdev.name ); - continue; - }; - c.control.set_enabled(false); - c.control.set_masked(false); - msix_info = Some((cap_offset, c.clone())); + } } else if cap_header.id == PciCapId::MSI { - let Ok((mut c, _)) = MsiCapHdr::read_from_prefix(cap_buf) else { + if let Ok((mut c, _)) = MsiCapHdr::read_from_prefix(cap_buf) { + log::info!("{}: MSI cap header: {c:#x?}", cdev.name); + c.control.set_enable(false); + c.control.set_ext_msg_data_cap(true); + let multi_msg_cap = min(5, c.control.multi_msg_cap()); + c.control.set_multi_msg_cap(multi_msg_cap); + msi_info = Some((cap_offset, c)); + } else { log::error!( "{}: MSI capability is at an invalid offset: {cap_offset:#x}", cdev.name ); - continue; - }; - log::info!("{}: MSI cap header: {c:#x?}", cdev.name); - c.control.set_enable(false); - c.control.set_ext_msg_data_cap(true); - let multi_msg_cap = min(5, c.control.multi_msg_cap()); - c.control.set_multi_msg_cap(multi_msg_cap); - msi_info = Some((cap_offset, c)); + } } cap_offset = cap_header.next as usize; } From ecb0c10b116a7f3d597e191e71830c1837b5bc55 Mon Sep 17 00:00:00 2001 From: changyuanl Date: Sat, 5 Sep 2026 17:02:45 -0700 Subject: [PATCH 3/5] fix(vfio): split non-mappable MSI-X bar Assisted-by: Antigravity:Gemini-3.8-Flash Signed-off-by: Changyuan Lyu --- alioth/src/vfio/pci.rs | 117 ++++++++++++++++++++++++----------------- 1 file changed, 69 insertions(+), 48 deletions(-) diff --git a/alioth/src/vfio/pci.rs b/alioth/src/vfio/pci.rs index e446f237..5703fe2b 100644 --- a/alioth/src/vfio/pci.rs +++ b/alioth/src/vfio/pci.rs @@ -52,33 +52,63 @@ fn round_up_range(range: Range) -> Range { (align_down!(range.start, 12))..(align_up!(range.end, 12)) } -fn create_mapped_bar_pages( - dev: &VfioDev, - region: &VfioRegionInfo, +fn create_mapped_bar_pages( + fd: OwnedFd, + region_flags: VfioRegionInfoFlag, offset: u64, size: usize, + dma_buf: Option, ) -> Result<(ArcMemPages, Option)> { - let dma_buf = match dev.dev.get_dma_buf_fd(region.index, offset, size) { - Ok(fd) => Some(fd), - Err(e) => { - log::warn!("{}: failed to get dma buf fd: {e:?}", dev.name); - None - } - }; - let mut prot = 0; - if region.flags.contains(VfioRegionInfoFlag::READ) { + if region_flags.contains(VfioRegionInfoFlag::READ) { prot |= PROT_READ; } - if region.flags.contains(VfioRegionInfoFlag::WRITE) { + if region_flags.contains(VfioRegionInfoFlag::WRITE) { prot |= PROT_WRITE; } - let dev_fd = dev.dev.fd().try_clone()?; - let dev_fd_offset = region.offset + offset; - let mapped_pages = ArcMemPages::from_file(dev_fd, dev_fd_offset as i64, size, prot)?; + let mapped_pages = ArcMemPages::from_file(fd.into(), offset as i64, size, prot)?; Ok((mapped_pages, dma_buf)) } +fn create_device_range( + dev: Arc>, + region_info: &VfioRegionInfo, + offset: usize, + size: usize, +) -> Result +where + D: Device, +{ + if region_info.flags.contains(VfioRegionInfoFlag::MMAP) { + let dma_buf = match dev + .dev + .get_dma_buf_fd(region_info.index, offset as u64, size) + { + Ok(fd) => Some(fd), + Err(e) => { + log::warn!("{}: failed to get dma buf fd: {e:?}", dev.name); + None + } + }; + let (pages, dma_buf) = create_mapped_bar_pages( + dev.dev.fd().try_clone()?.into(), + region_info.flags, + region_info.offset + offset as u64, + size, + dma_buf, + )?; + Ok(MemRange::DevMem { pages, dma_buf }) + } else { + log::warn!("{}: region {} is not mappable", dev.name, region_info.index); + let pth = PthBarRegion { + cdev: dev, + size, + offset: offset as u64, + }; + Ok(MemRange::Emulated(Arc::new(pth))) + } +} + fn create_splitted_bar_region( dev: Arc>, region_info: &VfioRegionInfo, @@ -117,8 +147,12 @@ where ranges: vec![], }; if excluded_page1.start > 0 { - let (pages, dma_buf) = create_mapped_bar_pages(&dev, region_info, 0, excluded_page1.start)?; - region.ranges.push(MemRange::DevMem { pages, dma_buf }); + region.ranges.push(create_device_range( + dev.clone(), + region_info, + 0, + excluded_page1.start, + )?); } if excluded_page1.end - excluded_page1.start > 0 { region.ranges.push(MemRange::Emulated(Arc::new(MsixBarMmio { @@ -134,13 +168,12 @@ where }))); } if excluded_page2.start - excluded_page1.end > 0 { - let (pages, dma_buf) = create_mapped_bar_pages( - &dev, + region.ranges.push(create_device_range( + dev.clone(), region_info, - excluded_page1.end as u64, + excluded_page1.end, excluded_page2.start - excluded_page1.end, - )?; - region.ranges.push(MemRange::DevMem { pages, dma_buf }); + )?); } if excluded_page2.end - excluded_page2.start > 0 { region.ranges.push(MemRange::Emulated(Arc::new(MsixBarMmio { @@ -156,18 +189,17 @@ where }))); } if excluded_page2.end < region_info.size as usize { - let (pages, dma_buf) = create_mapped_bar_pages( - &dev, + region.ranges.push(create_device_range( + dev.clone(), region_info, - excluded_page2.end as u64, + excluded_page2.end, region_info.size as usize - excluded_page2.end, - )?; - region.ranges.push(MemRange::DevMem { pages, dma_buf }); + )?); } Ok(region) } -fn create_mappable_bar_region( +fn create_bar_region( cdev: Arc>, index: u32, region_info: &VfioRegionInfo, @@ -553,25 +585,14 @@ where if region_info.size == 0 { continue; } - let region = if region_info.flags.contains(VfioRegionInfoFlag::MMAP) { - create_mappable_bar_region( - cdev.clone(), - index, - ®ion_info, - msix_cap.as_ref(), - msix_table.clone(), - msi_sender.clone(), - )? - } else { - MemRegion::with_emulated( - Arc::new(PthBarRegion { - cdev: cdev.clone(), - size: region_info.size as usize, - offset: region_info.offset, - }), - MemRegionType::Hidden, - ) - }; + let region = create_bar_region( + cdev.clone(), + index, + ®ion_info, + msix_cap.as_ref(), + msix_table.clone(), + msi_sender.clone(), + )?; let index = index as usize; let bar_val = bar_vals[index]; if bar_val & BAR_IO == BAR_IO { From d81f0a9a816abc02e521d6c61274323c02e788ee Mon Sep 17 00:00:00 2001 From: Changyuan Lyu Date: Sun, 2 Aug 2026 09:47:41 -0700 Subject: [PATCH 4/5] refactor(vfio): abstract Device trait to support alternative backends Remove raw File descriptor dependency from Device trait to allow userspace implementations (like vfio-user). Split multiplexed set_irqs into higher-level methods. Assisted-by: Antigravity:Gemini-3.8-Flash Signed-off-by: Changyuan Lyu --- alioth/src/vfio/cdev.rs | 76 +++++++++++--- alioth/src/vfio/device.rs | 201 ++++++++++++++++++++++++++------------ alioth/src/vfio/group.rs | 58 +++++++++-- alioth/src/vfio/pci.rs | 126 ++++++++++++------------ 4 files changed, 315 insertions(+), 146 deletions(-) diff --git a/alioth/src/vfio/cdev.rs b/alioth/src/vfio/cdev.rs index 1c3c53d1..3ef2cc4e 100644 --- a/alioth/src/vfio/cdev.rs +++ b/alioth/src/vfio/cdev.rs @@ -13,25 +13,26 @@ // limitations under the License. use std::fmt::Debug; -use std::fs::{File, OpenOptions}; +use std::fs::OpenOptions; use std::mem::size_of; -use std::os::fd::AsRawFd; +use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd}; use std::path::Path; use std::sync::Arc; use snafu::ResultExt; use crate::sys::vfio::{ - VfioDeviceAttachIommufdPt, VfioDeviceBindIommufd, VfioDeviceDetachIommufdPt, - vfio_device_attach_iommufd_pt, vfio_device_bind_iommufd, vfio_device_detach_iommufd_pt, + VfioDeviceAttachIommufdPt, VfioDeviceBindIommufd, VfioDeviceDetachIommufdPt, VfioDeviceInfo, + VfioIrqInfo, VfioRegionInfo, vfio_device_attach_iommufd_pt, vfio_device_bind_iommufd, + vfio_device_detach_iommufd_pt, }; -use crate::vfio::device::Device; +use crate::vfio::device::{Device, Kdev}; use crate::vfio::iommu::Ioas; use crate::vfio::{Result, error}; #[derive(Debug)] pub struct Cdev { - fd: File, + kdev: Kdev, ioas: Option>, } @@ -44,7 +45,10 @@ impl Cdev { .context(error::AccessDevice { path: path.as_ref(), })?; - Ok(Cdev { fd, ioas: None }) + Ok(Cdev { + kdev: Kdev::new(fd), + ioas: None, + }) } } @@ -55,13 +59,13 @@ impl Cdev { iommufd: ioas.iommu.fd.as_raw_fd(), ..Default::default() }; - unsafe { vfio_device_bind_iommufd(&self.fd, &bind) }?; + unsafe { vfio_device_bind_iommufd(&self.kdev, &bind) }?; let attach = VfioDeviceAttachIommufdPt { argsz: size_of::() as u32, pt_id: ioas.id, ..Default::default() }; - unsafe { vfio_device_attach_iommufd_pt(&self.fd, &attach) }?; + unsafe { vfio_device_attach_iommufd_pt(&self.kdev, &attach) }?; self.ioas.replace(ioas); Ok(()) } @@ -69,27 +73,71 @@ impl Cdev { pub fn detach_iommu_ioas(&mut self) -> Result<()> { if self.ioas.is_none() { return Ok(()); - }; + } let detach = VfioDeviceDetachIommufdPt { argsz: size_of::() as u32, flags: 0, }; - unsafe { vfio_device_detach_iommufd_pt(&self.fd, &detach) }?; + unsafe { vfio_device_detach_iommufd_pt(&self.kdev, &detach) }?; self.ioas = None; Ok(()) } } impl Device for Cdev { - fn fd(&self) -> &File { - &self.fd + fn get_info(&self) -> Result { + self.kdev.get_info() + } + + fn get_region_info(&self, index: u32) -> Result { + self.kdev.get_region_info(index) + } + + fn get_irq_info(&self, index: u32) -> Result { + self.kdev.get_irq_info(index) + } + + fn reset(&self) -> Result<()> { + self.kdev.reset() + } + + fn set_irq_eventfd( + &self, + index: u32, + start: u32, + eventfds: &[Option>], + ) -> Result<()> { + self.kdev.set_irq_eventfd(index, start, eventfds) + } + + fn disable_irq(&self, index: u32) -> Result<()> { + self.kdev.disable_irq(index) + } + + fn read_region(&self, region: &VfioRegionInfo, offset: u64, buf: &mut [u8]) -> Result<()> { + self.kdev.read_region(region, offset, buf) + } + + fn write_region(&self, region: &VfioRegionInfo, offset: u64, buf: &[u8]) -> Result<()> { + self.kdev.write_region(region, offset, buf) + } + + fn get_region_mmap_fd(&self, index: u32) -> Result> { + self.kdev.get_region_mmap_fd(index) + } + + fn get_dma_buf_fd(&self, index: u32, offset: u64, size: usize) -> Result { + self.kdev.get_dma_buf_fd(index, offset, size) } } impl Drop for Cdev { fn drop(&mut self) { if let Err(e) = self.detach_iommu_ioas() { - log::error!("Cdev-{}: detaching ioas: {e:?}", self.fd.as_raw_fd()) + log::error!( + "Cdev-{}: detaching ioas: {e:?}", + self.kdev.as_fd().as_raw_fd() + ) } } } diff --git a/alioth/src/vfio/device.rs b/alioth/src/vfio/device.rs index d31ecbfe..b95732fd 100644 --- a/alioth/src/vfio/device.rs +++ b/alioth/src/vfio/device.rs @@ -15,114 +15,193 @@ use std::fmt::Debug; use std::fs::File; use std::mem::size_of; -use std::os::fd::{AsRawFd, FromRawFd, OwnedFd}; +use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd}; use std::os::unix::fs::FileExt; +use crate::errors::BoxTrace; use crate::mem; use crate::sys::vfio::{ DeviceFeature, VfioDeviceFeature, VfioDeviceFeatureDmaBuf, VfioDeviceFeatureFlag, - VfioDeviceInfo, VfioIrqInfo, VfioIrqSet, VfioIrqSetData, VfioIrqSetFlag, VfioPciIrq, - VfioRegionDmaRange, VfioRegionInfo, vfio_device_feature, vfio_device_get_info, + VfioDeviceInfo, VfioIrqInfo, VfioIrqSet, VfioIrqSetData, VfioIrqSetFlag, VfioRegionDmaRange, + VfioRegionInfo, VfioRegionInfoFlag, vfio_device_feature, vfio_device_get_info, vfio_device_get_irq_info, vfio_device_get_region_info, vfio_device_reset, vfio_device_set_irqs, }; use crate::vfio::Result; pub trait Device: Debug + Send + Sync + 'static { - fn fd(&self) -> &File; + fn get_info(&self) -> Result; + fn get_region_info(&self, index: u32) -> Result; + fn get_irq_info(&self, index: u32) -> Result; + fn reset(&self) -> Result<()>; - fn get_info(&self) -> Result { - let mut device_info = VfioDeviceInfo { - argsz: size_of::() as u32, - ..Default::default() + fn set_irq_eventfd( + &self, + index: u32, + start: u32, + eventfds: &[Option>], + ) -> Result<()>; + + fn disable_irq(&self, index: u32) -> Result<()>; + + fn read_region(&self, region: &VfioRegionInfo, offset: u64, buf: &mut [u8]) -> Result<()>; + fn write_region(&self, region: &VfioRegionInfo, offset: u64, buf: &[u8]) -> Result<()>; + + fn get_region_mmap_fd(&self, index: u32) -> Result>; + + fn get_dma_buf_fd(&self, index: u32, offset: u64, size: usize) -> Result; + + // Helper methods for single-value read/write + fn read(&self, region: &VfioRegionInfo, offset: u64, size: u8) -> mem::Result { + let mut bytes = [0u8; 8]; + let Some(buf) = bytes.get_mut(0..size as usize) else { + log::error!( + "vfio: invalid read: index = {}, offset = {offset:#x}, size = {size:#x}", + region.index + ); + return Ok(0); }; - unsafe { vfio_device_get_info(self.fd(), &mut device_info) }?; - Ok(device_info) + self.read_region(region, offset, buf) + .box_trace(crate::mem::error::Mmio)?; + Ok(u64::from_ne_bytes(bytes)) } - fn get_dma_buf_fd(&self, index: u32, offset: u64, size: usize) -> Result { - let req = VfioDeviceFeature { - argsz: size_of::>>() as u32, - flags: VfioDeviceFeatureFlag::new(DeviceFeature::DMA_BUF, true, false, false), - data: VfioDeviceFeatureDmaBuf { - region_index: index, - open_flags: (libc::O_RDWR | libc::O_CLOEXEC) as u32, - flags: 0, - nr_ranges: 1, - dma_ranges: [VfioRegionDmaRange { - offset, - length: size as u64, - }], - }, + fn write(&self, region: &VfioRegionInfo, offset: u64, size: u8, val: u64) -> mem::Result<()> { + let bytes = val.to_ne_bytes(); + let Some(buf) = bytes.get(..size as usize) else { + log::error!( + "vfio: invalid write: index = {}, offset = {offset:#x}, size = {size:#x}, val = {val:#x}", + region.index + ); + return Ok(()); }; - let fd = unsafe { vfio_device_feature(self.fd(), &req) }?; - Ok(unsafe { OwnedFd::from_raw_fd(fd) }) + self.write_region(region, offset, buf) + .box_trace(crate::mem::error::Mmio)?; + Ok(()) + } +} + +#[derive(Debug)] +pub(crate) struct Kdev { + fd: File, +} + +impl AsFd for Kdev { + fn as_fd(&self) -> BorrowedFd<'_> { + self.fd.as_fd() + } +} + +impl Kdev { + pub fn new(fd: File) -> Self { + Self { fd } + } +} + +impl Kdev { + pub fn get_info(&self) -> Result { + let mut info = VfioDeviceInfo { + argsz: size_of::() as u32, + ..Default::default() + }; + unsafe { vfio_device_get_info(&self.fd, &mut info) }?; + Ok(info) } - fn get_region_info(&self, index: u32) -> Result { - let mut region_config = VfioRegionInfo { + pub fn get_region_info(&self, index: u32) -> Result { + let mut region = VfioRegionInfo { argsz: size_of::() as u32, index, ..Default::default() }; - unsafe { vfio_device_get_region_info(self.fd(), &mut region_config) }?; - Ok(region_config) + unsafe { vfio_device_get_region_info(&self.fd, &mut region) }?; + Ok(region) } - fn get_irq_info(&self, index: u32) -> Result { + pub fn get_irq_info(&self, index: u32) -> Result { let mut irq_info = VfioIrqInfo { argsz: size_of::() as u32, index, ..Default::default() }; - unsafe { vfio_device_get_irq_info(self.fd(), &mut irq_info) }?; + unsafe { vfio_device_get_irq_info(&self.fd, &mut irq_info) }?; Ok(irq_info) } - fn set_irqs(&self, irq: &VfioIrqSet) -> Result<()> { - unsafe { vfio_device_set_irqs(self.fd(), irq) }?; + pub fn reset(&self) -> Result<()> { + unsafe { vfio_device_reset(&self.fd) }?; Ok(()) } - fn disable_all_irqs(&self, index: VfioPciIrq) -> Result<()> { - let vfio_irq_disable_all = VfioIrqSet { + pub fn set_irq_eventfd( + &self, + index: u32, + start: u32, + eventfds: &[Option>], + ) -> Result<()> { + let mut raw_fds = [-1; 2048]; + for (raw_fd, eventfd) in raw_fds.iter_mut().zip(eventfds) { + *raw_fd = eventfd.map(|fd| fd.as_raw_fd()).unwrap_or(-1); + } + let irq_set = VfioIrqSet { + argsz: (size_of::>() + eventfds.len() * size_of::()) as u32, + flags: VfioIrqSetFlag::DATA_EVENTFD | VfioIrqSetFlag::ACTION_TRIGGER, + index, + start, + count: eventfds.len() as u32, + data: VfioIrqSetData { eventfds: raw_fds }, + }; + unsafe { vfio_device_set_irqs(&self.fd, &irq_set) }?; + Ok(()) + } + + pub fn disable_irq(&self, index: u32) -> Result<()> { + let irq_set = VfioIrqSet { argsz: size_of::>() as u32, flags: VfioIrqSetFlag::DATA_NONE | VfioIrqSetFlag::ACTION_TRIGGER, - index: index.raw(), + index, start: 0, count: 0, data: VfioIrqSetData { eventfds: [] }, }; - self.set_irqs(&vfio_irq_disable_all) + unsafe { vfio_device_set_irqs(&self.fd, &irq_set) }?; + Ok(()) } - fn reset(&self) -> Result<()> { - unsafe { vfio_device_reset(self.fd()) }?; + pub fn read_region(&self, region: &VfioRegionInfo, offset: u64, buf: &mut [u8]) -> Result<()> { + self.fd.read_exact_at(buf, region.offset + offset)?; Ok(()) } - fn read(&self, offset: u64, size: u8) -> mem::Result { - let mut bytes = [0u8; 8]; - let Some(buf) = bytes.get_mut(0..size as usize) else { - log::error!( - "vfio-{}: invalid read: offset = {offset:#x}, size = {size:#x}", - self.fd().as_raw_fd() - ); - return Ok(0); - }; - self.fd().read_exact_at(buf, offset)?; - Ok(u64::from_ne_bytes(bytes)) + pub fn write_region(&self, region: &VfioRegionInfo, offset: u64, buf: &[u8]) -> Result<()> { + self.fd.write_all_at(buf, region.offset + offset)?; + Ok(()) } - fn write(&self, offset: u64, size: u8, val: u64) -> mem::Result<()> { - let bytes = val.to_ne_bytes(); - let Some(buf) = bytes.get(..size as usize) else { - log::error!( - "vfio-{}: invalid write: offset = {offset:#x}, size = {size:#x}, val = {val:#x}", - self.fd().as_raw_fd() - ); - return Ok(()); + pub fn get_region_mmap_fd(&self, index: u32) -> Result> { + let region_info = self.get_region_info(index)?; + if region_info.flags.contains(VfioRegionInfoFlag::MMAP) { + Ok(Some(self.fd.try_clone()?.into())) + } else { + Ok(None) + } + } + + pub fn get_dma_buf_fd(&self, index: u32, offset: u64, size: usize) -> Result { + let req = VfioDeviceFeature { + argsz: size_of::>>() as u32, + flags: VfioDeviceFeatureFlag::new(DeviceFeature::DMA_BUF, true, false, false), + data: VfioDeviceFeatureDmaBuf { + region_index: index, + open_flags: (libc::O_RDWR | libc::O_CLOEXEC) as u32, + flags: 0, + nr_ranges: 1, + dma_ranges: [VfioRegionDmaRange { + offset, + length: size as u64, + }], + }, }; - self.fd().write_all_at(buf, offset)?; - Ok(()) + let fd = unsafe { vfio_device_feature(&self.fd, &req) }?; + Ok(unsafe { OwnedFd::from_raw_fd(fd) }) } } diff --git a/alioth/src/vfio/group.rs b/alioth/src/vfio/group.rs index 6b35ca51..3a3897b4 100644 --- a/alioth/src/vfio/group.rs +++ b/alioth/src/vfio/group.rs @@ -14,17 +14,18 @@ use std::ffi::CString; use std::fs::File; -use std::os::fd::{AsRawFd, FromRawFd}; +use std::os::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd}; use std::path::Path; use std::sync::Arc; use snafu::ResultExt; use crate::sys::vfio::{ - VfioIommu, vfio_group_get_device_fd, vfio_group_set_container, vfio_group_unset_container, + VfioDeviceInfo, VfioIommu, VfioIrqInfo, VfioRegionInfo, vfio_group_get_device_fd, + vfio_group_set_container, vfio_group_unset_container, }; use crate::vfio::container::Container; -use crate::vfio::device::Device; +use crate::vfio::device::{Device, Kdev}; use crate::vfio::{Result, error}; #[derive(Debug)] @@ -72,7 +73,7 @@ impl Drop for Group { #[derive(Debug)] pub struct DevFd { - fd: File, + kdev: Kdev, _group: Arc, } @@ -80,15 +81,58 @@ impl DevFd { pub fn new(group: Arc, id: &str) -> Result { let id_c = CString::new(id).unwrap(); let fd = unsafe { vfio_group_get_device_fd(&group.fd, id_c.as_ptr()) }?; + let file = unsafe { File::from_raw_fd(fd) }; + let kdev = Kdev::new(file); Ok(DevFd { - fd: unsafe { File::from_raw_fd(fd) }, + kdev, _group: group, }) } } impl Device for DevFd { - fn fd(&self) -> &File { - &self.fd + fn get_info(&self) -> Result { + self.kdev.get_info() + } + + fn get_region_info(&self, index: u32) -> Result { + self.kdev.get_region_info(index) + } + + fn get_irq_info(&self, index: u32) -> Result { + self.kdev.get_irq_info(index) + } + + fn reset(&self) -> Result<()> { + self.kdev.reset() + } + + fn set_irq_eventfd( + &self, + index: u32, + start: u32, + eventfds: &[Option>], + ) -> Result<()> { + self.kdev.set_irq_eventfd(index, start, eventfds) + } + + fn disable_irq(&self, index: u32) -> Result<()> { + self.kdev.disable_irq(index) + } + + fn read_region(&self, region: &VfioRegionInfo, offset: u64, buf: &mut [u8]) -> Result<()> { + self.kdev.read_region(region, offset, buf) + } + + fn write_region(&self, region: &VfioRegionInfo, offset: u64, buf: &[u8]) -> Result<()> { + self.kdev.write_region(region, offset, buf) + } + + fn get_region_mmap_fd(&self, index: u32) -> Result> { + self.kdev.get_region_mmap_fd(index) + } + + fn get_dma_buf_fd(&self, index: u32, offset: u64, size: usize) -> Result { + self.kdev.get_dma_buf_fd(index, offset, size) } } diff --git a/alioth/src/vfio/pci.rs b/alioth/src/vfio/pci.rs index 5703fe2b..80ed89ea 100644 --- a/alioth/src/vfio/pci.rs +++ b/alioth/src/vfio/pci.rs @@ -13,11 +13,9 @@ // limitations under the License. use std::cmp::{max, min}; -use std::iter::zip; use std::mem::size_of; use std::ops::Range; -use std::os::fd::{AsFd, AsRawFd, OwnedFd}; -use std::os::unix::fs::FileExt; +use std::os::fd::{AsFd, OwnedFd}; use std::sync::Arc; use std::sync::atomic::AtomicU64; @@ -41,8 +39,7 @@ use crate::pci::config::{ }; use crate::pci::{self, Pci, PciBar}; use crate::sys::vfio::{ - VfioDeviceInfoFlag, VfioIrqSet, VfioIrqSetData, VfioIrqSetFlag, VfioPciIrq, VfioPciRegion, - VfioRegionInfo, VfioRegionInfoFlag, + VfioDeviceInfoFlag, VfioPciIrq, VfioPciRegion, VfioRegionInfo, VfioRegionInfoFlag, }; use crate::vfio::device::Device; use crate::vfio::{Result, error}; @@ -72,14 +69,16 @@ fn create_mapped_bar_pages( fn create_device_range( dev: Arc>, - region_info: &VfioRegionInfo, + region_info: Arc, offset: usize, size: usize, ) -> Result where D: Device, { - if region_info.flags.contains(VfioRegionInfoFlag::MMAP) { + if region_info.flags.contains(VfioRegionInfoFlag::MMAP) + && let Some(fd) = dev.dev.get_region_mmap_fd(region_info.index)? + { let dma_buf = match dev .dev .get_dma_buf_fd(region_info.index, offset as u64, size) @@ -91,7 +90,7 @@ where } }; let (pages, dma_buf) = create_mapped_bar_pages( - dev.dev.fd().try_clone()?.into(), + fd, region_info.flags, region_info.offset + offset as u64, size, @@ -104,6 +103,7 @@ where cdev: dev, size, offset: offset as u64, + region: region_info, }; Ok(MemRange::Emulated(Arc::new(pth))) } @@ -111,7 +111,7 @@ where fn create_splitted_bar_region( dev: Arc>, - region_info: &VfioRegionInfo, + region_info: Arc, table_range: Range, pba_range: Range, msix_table: Arc>, @@ -149,7 +149,7 @@ where if excluded_page1.start > 0 { region.ranges.push(create_device_range( dev.clone(), - region_info, + region_info.clone(), 0, excluded_page1.start, )?); @@ -162,7 +162,7 @@ where pba: Arc::new([]), pba_range: pba_range.clone(), cdev: dev.clone(), - cdev_offset: region_info.offset, + region: region_info.clone(), region_start: excluded_page1.start, region_size: excluded_page1.end - excluded_page1.start, }))); @@ -170,7 +170,7 @@ where if excluded_page2.start - excluded_page1.end > 0 { region.ranges.push(create_device_range( dev.clone(), - region_info, + region_info.clone(), excluded_page1.end, excluded_page2.start - excluded_page1.end, )?); @@ -183,7 +183,7 @@ where pba: Arc::new([]), pba_range, cdev: dev.clone(), - cdev_offset: region_info.offset, + region: region_info.clone(), region_start: excluded_page2.start, region_size: excluded_page2.end - excluded_page2.start, }))); @@ -191,7 +191,7 @@ where if excluded_page2.end < region_info.size as usize { region.ranges.push(create_device_range( dev.clone(), - region_info, + region_info.clone(), excluded_page2.end, region_info.size as usize - excluded_page2.end, )?); @@ -202,7 +202,7 @@ where fn create_bar_region( cdev: Arc>, index: u32, - region_info: &VfioRegionInfo, + region_info: Arc, msix_cap: Option<&MsixCap>, msix_table: Arc>, msi_sender: Arc, @@ -262,7 +262,8 @@ where #[derive(Debug)] struct PthConfigArea { - offset: u64, // offset to dev + offset: u64, // offset in config space + region: Arc, size: u64, dev: Arc>, } @@ -276,11 +277,13 @@ where } fn read(&self, offset: u64, size: u8) -> mem::Result { - self.dev.dev.read(self.offset + offset, size) + self.dev.dev.read(&self.region, self.offset + offset, size) } fn write(&self, offset: u64, size: u8, val: u64) -> mem::Result { - self.dev.dev.write(self.offset + offset, size, val)?; + self.dev + .dev + .write(&self.region, self.offset + offset, size, val)?; Ok(Action::None) } } @@ -348,6 +351,7 @@ pub struct PthBarRegion { cdev: Arc>, size: usize, offset: u64, + region: Arc, } impl Mmio for PthBarRegion @@ -359,19 +363,23 @@ where } fn read(&self, offset: u64, size: u8) -> mem::Result { + let addr = self.offset + offset; log::trace!( - "{}: emulated read at {offset:#x}, size={size}", - self.cdev.name + "{}: emulated read at region {}, offset {addr:#x}, size={size}", + self.cdev.name, + self.region.index, ); - self.cdev.dev.read(self.offset + offset, size) + self.cdev.dev.read(&self.region, addr, size) } fn write(&self, offset: u64, size: u8, val: u64) -> mem::Result { + let addr = self.offset + offset; log::trace!( - "{}: emulated write at {offset:#x}, val={val:#x}, size={size}", - self.cdev.name + "{}: emulated write at region {}, offset {addr:#x}, val={val:#x}, size={size}", + self.cdev.name, + self.region.index ); - self.cdev.dev.write(self.offset + offset, size, val)?; + self.cdev.dev.write(&self.region, addr, size, val)?; Ok(Action::None) } } @@ -424,18 +432,19 @@ where let msi_sender = Arc::new(msi_sender); - let region_config = cdev.dev.get_region_info(VfioPciRegion::CONFIG.raw())?; + let region_config = Arc::new(cdev.dev.get_region_info(VfioPciRegion::CONFIG.raw())?); let pci_command = Command::IO | Command::MEM | Command::BUS_MASTER | Command::INTX_DISABLE; cdev.dev.write( - region_config.offset + CommonHeader::OFFSET_COMMAND as u64, + ®ion_config, + CommonHeader::OFFSET_COMMAND as u64, CommonHeader::SIZE_COMMAND as u8, pci_command.bits() as _, )?; let mut buf = vec![0u32; region_config.size as usize >> 2]; let buf = buf.as_mut_bytes(); - cdev.dev.fd().read_at(buf, region_config.offset)?; + cdev.dev.read_region(®ion_config, 0, buf)?; let (mut dev_header, _) = DeviceHeader::read_from_prefix(buf).unwrap(); let header_type = dev_header.common.header_type.raw() & !(1 << 7); @@ -512,19 +521,12 @@ where .map(|_| msi_sender.create_irqfd()) .collect::, _>>()?; - let mut eventfds = [-1; 32]; - for (fd, irqfd) in zip(&mut eventfds, &irqfds) { - *fd = irqfd.as_fd().as_raw_fd(); + let mut eventfds = vec![]; + for irqfd in &irqfds { + eventfds.push(Some(irqfd.as_fd())); } - let set_eventfd = VfioIrqSet { - argsz: (size_of::>() + size_of::() * count) as u32, - flags: VfioIrqSetFlag::DATA_EVENTFD | VfioIrqSetFlag::ACTION_TRIGGER, - index: VfioPciIrq::MSI.raw(), - start: 0, - count: count as u32, - data: VfioIrqSetData { eventfds }, - }; - cdev.dev.set_irqs(&set_eventfd)?; + cdev.dev + .set_irq_eventfd(VfioPciIrq::MSI.raw(), 0, &eventfds)?; let mut msi_cap_mmio = MsiCapMmio::new(hdr.control, irqfds); msi_cap_mmio.set_next(hdr.header.next); @@ -539,9 +541,10 @@ where extra_areas.add( area_end, Box::new(PthConfigArea { - offset: region_config.offset + area_end, + offset: area_end, size: offset - area_end, dev: cdev.clone(), + region: region_config.clone(), }), )?; } @@ -552,9 +555,10 @@ where extra_areas.add( area_end, Box::new(PthConfigArea { - offset: region_config.offset + area_end, + offset: area_end, size: region_config.size - area_end, dev: cdev.clone(), + region: region_config.clone(), }), )?; } @@ -581,14 +585,14 @@ where let bar_vals = config_header.bars(); for index in VfioPciRegion::BAR0.raw()..=VfioPciRegion::BAR5.raw() { - let region_info = cdev.dev.get_region_info(index)?; + let region_info = Arc::new(cdev.dev.get_region_info(index)?); if region_info.size == 0 { continue; } let region = create_bar_region( cdev.clone(), index, - ®ion_info, + region_info, msix_cap.as_ref(), msix_table.clone(), msi_sender.clone(), @@ -622,7 +626,7 @@ where let is_irqfd = |e| matches!(e, &MsixTableMmioEntry::IrqFd(_)); if self.msix_table.entries.read().iter().any(is_irqfd) { let dev = &self.config.dev; - if let Err(e) = dev.dev.disable_all_irqs(VfioPciIrq::MSIX) { + if let Err(e) = dev.dev.disable_irq(VfioPciIrq::MSIX.raw()) { log::error!("{}: failed to disable MSIX IRQs: {e:?}", dev.name) } } @@ -644,7 +648,7 @@ where pba: Arc<[AtomicU64]>, // TODO pba_range: Range, cdev: Arc>, - cdev_offset: u64, + region: Arc, region_start: usize, region_size: usize, } @@ -686,26 +690,20 @@ where // subindex for the first time. // As long as the following set_irqs() succeeds, we can safely ignore // the error here. - let _ = self.cdev.dev.disable_all_irqs(VfioPciIrq::MSIX); + let _ = self.cdev.dev.disable_irq(VfioPciIrq::MSIX.raw()); - let mut eventfds = [-1; 2048]; + let mut eventfds = vec![None; entries.len()]; let mut count = 0; - for (index, (entry, fd)) in std::iter::zip(entries.iter(), &mut eventfds).enumerate() { - let MsixTableMmioEntry::IrqFd(irqfd) = entry else { - continue; - }; - count = index + 1; - *fd = irqfd.as_fd().as_raw_fd(); + for (index, (entry, fd)) in entries.iter().zip(&mut eventfds).enumerate() { + if let MsixTableMmioEntry::IrqFd(irqfd) = entry { + *fd = Some(irqfd.as_fd()); + count = index + 1; + } } - let vfio_irq_set_eventfd = VfioIrqSet { - argsz: (size_of::>() + size_of::() * count) as u32, - flags: VfioIrqSetFlag::DATA_EVENTFD | VfioIrqSetFlag::ACTION_TRIGGER, - index: VfioPciIrq::MSIX.raw(), - start: 0, - count: count as u32, - data: VfioIrqSetData { eventfds }, - }; - self.cdev.dev.set_irqs(&vfio_irq_set_eventfd) + eventfds.truncate(count); + self.cdev + .dev + .set_irq_eventfd(VfioPciIrq::MSIX.raw(), 0, &eventfds) } } @@ -729,7 +727,7 @@ where Ok(0) } else { log::trace!("{name}: emulated BAR read at {offset:#x}, size={size}",); - self.cdev.dev.read(self.cdev_offset + offset as u64, size) + self.cdev.dev.read(&self.region, offset as u64, size) } } @@ -750,7 +748,7 @@ where log::trace!("{name}: emulated BAR write at {offset:#x}, size={size}, val={val:#x}",); self.cdev .dev - .write(self.cdev_offset + offset as u64, size, val)?; + .write(&self.region, offset as u64, size, val)?; } Ok(Action::None) } From 083361d8de1d8531049134aee68cc9195394ab80 Mon Sep 17 00:00:00 2001 From: changyuanl Date: Thu, 3 Sep 2026 11:03:05 -0700 Subject: [PATCH 5/5] test(vfio): add tests for mod vfio Assisted-by: Antigravity:Gemini-3.7-Flash Signed-off-by: Changyuan Lyu --- alioth/src/vfio/cdev.rs | 4 + alioth/src/vfio/cdev_test.rs | 53 ++++ alioth/src/vfio/container.rs | 4 + alioth/src/vfio/container_test.rs | 40 +++ alioth/src/vfio/device.rs | 4 + alioth/src/vfio/device_test.rs | 283 +++++++++++++++++ alioth/src/vfio/group.rs | 4 + alioth/src/vfio/group_test.rs | 67 ++++ alioth/src/vfio/pci.rs | 4 + alioth/src/vfio/pci_test.rs | 509 ++++++++++++++++++++++++++++++ alioth/src/vfio/vfio.rs | 4 + alioth/src/vfio/vfio_test.rs | 47 +++ 12 files changed, 1023 insertions(+) create mode 100644 alioth/src/vfio/cdev_test.rs create mode 100644 alioth/src/vfio/container_test.rs create mode 100644 alioth/src/vfio/device_test.rs create mode 100644 alioth/src/vfio/group_test.rs create mode 100644 alioth/src/vfio/pci_test.rs create mode 100644 alioth/src/vfio/vfio_test.rs diff --git a/alioth/src/vfio/cdev.rs b/alioth/src/vfio/cdev.rs index 3ef2cc4e..c07e62cf 100644 --- a/alioth/src/vfio/cdev.rs +++ b/alioth/src/vfio/cdev.rs @@ -141,3 +141,7 @@ impl Drop for Cdev { } } } + +#[cfg(test)] +#[path = "cdev_test.rs"] +mod tests; diff --git a/alioth/src/vfio/cdev_test.rs b/alioth/src/vfio/cdev_test.rs new file mode 100644 index 00000000..f43845c4 --- /dev/null +++ b/alioth/src/vfio/cdev_test.rs @@ -0,0 +1,53 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io::Write; + +use assert_matches::assert_matches; + +use crate::sys::vfio::{VfioRegionInfo, VfioRegionInfoFlag}; +use crate::vfio::cdev::Cdev; +use crate::vfio::device::Device; + +#[test] +fn test_cdev_detach_when_not_attached() { + // Cdev detach when not attached returns Ok(()) + let tmp_cdev = tempfile::NamedTempFile::new().unwrap(); + let mut cdev = Cdev::new(tmp_cdev.path()).unwrap(); + assert_matches!(cdev.detach_iommu_ioas(), Ok(())); +} + +#[test] +fn test_cdev_device_impl() { + let mut fake = tempfile::NamedTempFile::new().unwrap(); + fake.write_all(&[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa]) + .unwrap(); + + let cdev = Cdev::new(fake.path()).unwrap(); + let region = VfioRegionInfo { + index: 0, + size: 8, + offset: 2, + flags: VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE, + ..Default::default() + }; + + let mut buf = [0u8; 4]; + cdev.read_region(®ion, 2, &mut buf).unwrap(); + assert_eq!(buf, [0x55, 0x66, 0x77, 0x88]); + + cdev.write_region(®ion, 0, &[0xaa, 0xbb]).unwrap(); + cdev.read_region(®ion, 0, &mut buf).unwrap(); + assert_eq!(buf, [0xaa, 0xbb, 0x55, 0x66]); +} diff --git a/alioth/src/vfio/container.rs b/alioth/src/vfio/container.rs index 0d57c4db..5ac7643b 100644 --- a/alioth/src/vfio/container.rs +++ b/alioth/src/vfio/container.rs @@ -138,3 +138,7 @@ impl LayoutChanged for UpdateContainerMapping { self.ram_removed(gpa, pages) } } + +#[cfg(test)] +#[path = "container_test.rs"] +mod tests; diff --git a/alioth/src/vfio/container_test.rs b/alioth/src/vfio/container_test.rs new file mode 100644 index 00000000..7f108d0f --- /dev/null +++ b/alioth/src/vfio/container_test.rs @@ -0,0 +1,40 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use assert_matches::assert_matches; + +use crate::sys::vfio::VfioIommu; +use crate::vfio::Error; +use crate::vfio::container::Container; + +#[test] +fn test_container_set_iommu_mismatch() { + let tmp_file = tempfile::NamedTempFile::new().unwrap(); + let container = Container::new(tmp_file.path()).unwrap(); + // Simulate container already having TYPE1 + *container.iommu.lock() = Some(VfioIommu::TYPE1); + + // Setting same IOMMU returns Ok(()) + assert_matches!(container.set_iommu(VfioIommu::TYPE1), Ok(())); + + // Setting different IOMMU returns SetContainerIommu error + assert_matches!( + container.set_iommu(VfioIommu::TYPE1_V2), + Err(Error::SetContainerIommu { + current: VfioIommu::TYPE1, + new: VfioIommu::TYPE1_V2, + .. + }) + ); +} diff --git a/alioth/src/vfio/device.rs b/alioth/src/vfio/device.rs index b95732fd..a2ae19e6 100644 --- a/alioth/src/vfio/device.rs +++ b/alioth/src/vfio/device.rs @@ -205,3 +205,7 @@ impl Kdev { Ok(unsafe { OwnedFd::from_raw_fd(fd) }) } } + +#[cfg(test)] +#[path = "device_test.rs"] +pub(crate) mod tests; diff --git a/alioth/src/vfio/device_test.rs b/alioth/src/vfio/device_test.rs new file mode 100644 index 00000000..7595eec1 --- /dev/null +++ b/alioth/src/vfio/device_test.rs @@ -0,0 +1,283 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; +use std::io::{self, ErrorKind}; +use std::os::fd::{AsRawFd, BorrowedFd, OwnedFd, RawFd}; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use assert_matches::assert_matches; +use parking_lot::{Mutex, RwLock}; + +use crate::sys::vfio::{ + VfioDeviceInfo, VfioDeviceInfoFlag, VfioIrqInfo, VfioPciRegion, VfioRegionInfo, + VfioRegionInfoFlag, +}; +use crate::vfio::Result; +use crate::vfio::device::Device; + +pub type RegionMap = RwLock)>>; +pub type IrqMap = RwLock>; +pub type MmapFdMap = RwLock>; +pub type IrqEventFdList = Mutex>)>>; + +#[derive(Debug)] +pub struct MockVfioDevice { + pub info: VfioDeviceInfo, + pub regions: RegionMap, + pub irqs: IrqMap, + pub mmap_fds: MmapFdMap, + pub resets: AtomicUsize, + pub irq_eventfds: IrqEventFdList, + pub disabled_irqs: Mutex>, +} + +impl Default for MockVfioDevice { + fn default() -> Self { + Self::new() + } +} + +impl MockVfioDevice { + pub fn new() -> Self { + let dev = Self { + info: VfioDeviceInfo { + argsz: std::mem::size_of::() as u32, + flags: VfioDeviceInfoFlag::RESET, + ..Default::default() + }, + regions: RwLock::new(HashMap::new()), + irqs: RwLock::new(HashMap::new()), + mmap_fds: RwLock::new(HashMap::new()), + resets: AtomicUsize::new(0), + irq_eventfds: Mutex::new(Vec::new()), + disabled_irqs: Mutex::new(Vec::new()), + }; + for index in 0..=5 { + dev.add_bar(index, 0, VfioRegionInfoFlag::empty(), 0); + } + dev + } + + pub fn add_region( + &self, + index: u32, + size: u64, + flags: VfioRegionInfoFlag, + offset: u64, + data: Vec, + ) -> VfioRegionInfo { + let info = VfioRegionInfo { + argsz: std::mem::size_of::() as u32, + flags, + index, + cap_offset: 0, + size, + offset, + }; + self.regions.write().insert(index, (info.clone(), data)); + info + } + + pub fn add_bar( + &self, + index: u32, + size: u64, + flags: VfioRegionInfoFlag, + offset: u64, + ) -> VfioRegionInfo { + self.add_region(index, size, flags, offset, vec![0u8; size as usize]) + } + + pub fn add_config(&self, config: Vec) -> VfioRegionInfo { + self.add_region( + VfioPciRegion::CONFIG.raw(), + config.len() as u64, + VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE, + 0, + config, + ) + } +} + +impl Device for MockVfioDevice { + fn get_info(&self) -> Result { + Ok(self.info.clone()) + } + + fn get_region_info(&self, index: u32) -> Result { + let regions = self.regions.read(); + let (info, _) = regions + .get(&index) + .ok_or_else(|| io::Error::from(ErrorKind::NotFound))?; + Ok(info.clone()) + } + + fn get_irq_info(&self, index: u32) -> Result { + let irqs = self.irqs.read(); + let info = irqs + .get(&index) + .ok_or_else(|| io::Error::from(ErrorKind::NotFound))?; + Ok(info.clone()) + } + + fn reset(&self) -> Result<()> { + self.resets.fetch_add(1, Ordering::SeqCst); + Ok(()) + } + + fn set_irq_eventfd( + &self, + index: u32, + start: u32, + eventfds: &[Option>], + ) -> Result<()> { + let raw_fds = eventfds + .iter() + .map(|fd| fd.as_ref().map(|f| f.as_raw_fd())) + .collect(); + self.irq_eventfds.lock().push((index, start, raw_fds)); + Ok(()) + } + + fn disable_irq(&self, index: u32) -> Result<()> { + self.disabled_irqs.lock().push(index); + Ok(()) + } + + fn read_region(&self, region: &VfioRegionInfo, offset: u64, buf: &mut [u8]) -> Result<()> { + let regions = self.regions.read(); + let (_, data) = regions + .get(®ion.index) + .ok_or_else(|| io::Error::from(ErrorKind::NotFound))?; + let offset = offset as usize; + let end = offset + buf.len(); + if end > data.len() { + return Err(io::Error::from(ErrorKind::UnexpectedEof).into()); + } + buf.copy_from_slice(&data[offset..end]); + Ok(()) + } + + fn write_region(&self, region: &VfioRegionInfo, offset: u64, buf: &[u8]) -> Result<()> { + let mut regions = self.regions.write(); + let (_, data) = regions + .get_mut(®ion.index) + .ok_or_else(|| io::Error::from(ErrorKind::NotFound))?; + let offset = offset as usize; + let end = offset + buf.len(); + if end > data.len() { + return Err(io::Error::from(ErrorKind::UnexpectedEof).into()); + } + data[offset..end].copy_from_slice(buf); + Ok(()) + } + + fn get_region_mmap_fd(&self, index: u32) -> Result> { + let mmap_fds = self.mmap_fds.read(); + if let Some(fd) = mmap_fds.get(&index) { + let cloned = fd.try_clone()?; + Ok(Some(cloned)) + } else { + Ok(None) + } + } + + fn get_dma_buf_fd(&self, _index: u32, _offset: u64, _size: usize) -> Result { + Err(std::io::Error::from(std::io::ErrorKind::Unsupported).into()) + } +} + +impl Device for Arc { + fn get_info(&self) -> Result { + (**self).get_info() + } + + fn get_region_info(&self, index: u32) -> Result { + (**self).get_region_info(index) + } + + fn get_irq_info(&self, index: u32) -> Result { + (**self).get_irq_info(index) + } + + fn reset(&self) -> Result<()> { + (**self).reset() + } + + fn set_irq_eventfd( + &self, + index: u32, + start: u32, + eventfds: &[Option>], + ) -> Result<()> { + (**self).set_irq_eventfd(index, start, eventfds) + } + + fn disable_irq(&self, index: u32) -> Result<()> { + (**self).disable_irq(index) + } + + fn read_region(&self, region: &VfioRegionInfo, offset: u64, buf: &mut [u8]) -> Result<()> { + (**self).read_region(region, offset, buf) + } + + fn write_region(&self, region: &VfioRegionInfo, offset: u64, buf: &[u8]) -> Result<()> { + (**self).write_region(region, offset, buf) + } + + fn get_region_mmap_fd(&self, index: u32) -> Result> { + (**self).get_region_mmap_fd(index) + } + + fn get_dma_buf_fd(&self, index: u32, offset: u64, size: usize) -> Result { + (**self).get_dma_buf_fd(index, offset, size) + } +} + +#[test] +fn test_device_read_write_helpers() { + let dev = MockVfioDevice::new(); + let region = dev.add_bar( + 0, + 64, + VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE, + 0, + ); + + // 1-byte read/write + assert_matches!(dev.write(®ion, 0, 1, 0xab), Ok(())); + assert_matches!(dev.read(®ion, 0, 1), Ok(0xab)); + + // 2-byte read/write + assert_matches!(dev.write(®ion, 2, 2, 0x1234), Ok(())); + assert_matches!(dev.read(®ion, 2, 2), Ok(0x1234)); + + // 4-byte read/write + assert_matches!(dev.write(®ion, 4, 4, 0xdead_beef), Ok(())); + assert_matches!(dev.read(®ion, 4, 4), Ok(0xdead_beef)); + + // 8-byte read/write + assert_matches!(dev.write(®ion, 8, 8, 0x0123_4567_89ab_cdef), Ok(())); + assert_matches!(dev.read(®ion, 8, 8), Ok(0x0123_4567_89ab_cdef)); + + // Invalid size (> 8 bytes) + assert_matches!(dev.write(®ion, 0, 16, 0), Ok(())); + assert_matches!(dev.read(®ion, 0, 16), Ok(0)); + + // Read/write out of bounds + assert_matches!(dev.write(®ion, 64, 1, 0xab), Err(_)); + assert_matches!(dev.read(®ion, 64, 1), Err(_)); +} diff --git a/alioth/src/vfio/group.rs b/alioth/src/vfio/group.rs index 3a3897b4..379721cb 100644 --- a/alioth/src/vfio/group.rs +++ b/alioth/src/vfio/group.rs @@ -136,3 +136,7 @@ impl Device for DevFd { self.kdev.get_dma_buf_fd(index, offset, size) } } + +#[cfg(test)] +#[path = "group_test.rs"] +mod tests; diff --git a/alioth/src/vfio/group_test.rs b/alioth/src/vfio/group_test.rs new file mode 100644 index 00000000..0ae95c83 --- /dev/null +++ b/alioth/src/vfio/group_test.rs @@ -0,0 +1,67 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::io::Write; +use std::sync::Arc; + +use assert_matches::assert_matches; + +use crate::sys::vfio::{VfioRegionInfo, VfioRegionInfoFlag}; +use crate::vfio::device::{Device, Kdev}; +use crate::vfio::group::{DevFd, Group}; + +#[test] +fn test_group_detach_when_not_attached() { + // Group detach when not attached returns Ok(()) + let tmp_group = tempfile::NamedTempFile::new().unwrap(); + let mut group = Group::new(tmp_group.path()).unwrap(); + assert_matches!(group.detach(), Ok(())); +} + +#[test] +fn test_dev_fd_device_impl() { + let fake_group = tempfile::NamedTempFile::new().unwrap(); + let mut fake_dev = tempfile::NamedTempFile::new().unwrap(); + + fake_dev + .write_all(&[0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88]) + .unwrap(); + + let group = Arc::new(Group::new(fake_group.path()).unwrap()); + let file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(fake_dev.path()) + .unwrap(); + let kdev = Kdev::new(file); + let dev_fd = DevFd { + kdev, + _group: group, + }; + let region = VfioRegionInfo { + index: 0, + size: 8, + offset: 0, + flags: VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE, + ..Default::default() + }; + + let mut buf = [0u8; 4]; + dev_fd.read_region(®ion, 2, &mut buf).unwrap(); + assert_eq!(buf, [0x33, 0x44, 0x55, 0x66]); + + dev_fd.write_region(®ion, 0, &[0xaa, 0xbb]).unwrap(); + dev_fd.read_region(®ion, 0, &mut buf).unwrap(); + assert_eq!(buf, [0xaa, 0xbb, 0x33, 0x44]); +} diff --git a/alioth/src/vfio/pci.rs b/alioth/src/vfio/pci.rs index 80ed89ea..5105ad8b 100644 --- a/alioth/src/vfio/pci.rs +++ b/alioth/src/vfio/pci.rs @@ -753,3 +753,7 @@ where Ok(Action::None) } } + +#[cfg(test)] +#[path = "pci_test.rs"] +mod tests; diff --git a/alioth/src/vfio/pci_test.rs b/alioth/src/vfio/pci_test.rs new file mode 100644 index 00000000..d5515ce0 --- /dev/null +++ b/alioth/src/vfio/pci_test.rs @@ -0,0 +1,509 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::sync::Arc; +use std::sync::atomic::Ordering; + +use assert_matches::assert_matches; +use rstest::rstest; +use zerocopy::{FromBytes, IntoBytes}; + +use crate::hv::tests::TestMsiSender; +use crate::mem::MemRange; +use crate::mem::emulated::{Action, Mmio}; +use crate::pci::cap::{ + MsiCapHdr, MsiMsgCtrl, MsixCap, MsixCapOffset, MsixMsgCtrl, PciCapHdr, PciCapId, +}; +use crate::pci::config::{ + BAR_IO, BAR_MEM32, Command, CommonHeader, DeviceHeader, HeaderType, Status, +}; +use crate::pci::{Pci, PciBar}; +use crate::sys::vfio::{VfioDeviceInfoFlag, VfioPciIrq, VfioPciRegion, VfioRegionInfoFlag}; +use crate::vfio::Error; +use crate::vfio::device::tests::MockVfioDevice; +use crate::vfio::pci::{PthBarRegion, VfioDev, VfioPciDev}; + +fn create_mock_pci_config_space() -> Vec { + let mut config = vec![0u8; 4096]; + + let header = DeviceHeader { + common: CommonHeader { + vendor: 0x1ae0, + device: 0x1234, + command: Command::empty(), + status: Status::CAP, + revision: 0x01, + prog_if: 0, + subclass: 0x00, + class: 0x02, // Network controller + cache_line_size: 0, + latency_timer: 0, + header_type: HeaderType::DEVICE, + bist: 0, + }, + bars: [0; 6], + cardbus_cis_pointer: 0, + subsystem_vendor: 0x8086, + subsystem: 0x5678, + expansion_rom: 0, + capability_pointer: 0x40, + reserved: [0; 7], + intx_line: 0, + intx_pin: 1, + min_gnt: 0, + max_lat: 0, + }; + config[0..64].copy_from_slice(header.as_bytes()); + + // MSI-X capability at offset 0x40 + let msix_cap = MsixCap { + header: PciCapHdr { + id: PciCapId::MSIX, + next: 0, + }, + control: MsixMsgCtrl::new(4), // 4 entries + table_offset: MsixCapOffset::new(0, 0), // BAR 0, offset 0 + pba_offset: MsixCapOffset::new(0x800, 0), // BAR 0, offset 0x800 + }; + config[0x40..0x40 + std::mem::size_of::()].copy_from_slice(msix_cap.as_bytes()); + + config +} + +#[test] +fn test_vfio_pci_dev_creation_and_config() { + let mock = Arc::new(MockVfioDevice::default()); + let config_bytes = create_mock_pci_config_space(); + mock.add_config(config_bytes); + mock.add_bar( + VfioPciRegion::BAR0.raw(), + 0x10000, + VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE, + 0x1_0000, + ); + + let msi_sender = TestMsiSender::default(); + let dev = VfioPciDev::new(Arc::from("test-vfio-pci"), mock.clone(), msi_sender).unwrap(); + + // Verify name + assert_eq!(dev.name(), "test-vfio-pci"); + + // Verify device reset was called + assert_eq!(mock.resets.load(Ordering::SeqCst), 1); + + // Verify PCI config reads + let config = dev.config(); + assert_matches!(config.read(0x00, 2), Ok(0x1ae0)); // Vendor ID + assert_matches!(config.read(0x02, 2), Ok(0x1234)); // Device ID + assert_matches!(config.read(0x08, 1), Ok(0x01)); // Revision + assert_matches!(config.read(0x0a, 2), Ok(0x0200)); // Class / Subclass + + // Verify PCI config writes + assert_matches!(config.write(0x00, 2, 0x1234), Ok(_)); + assert_matches!(config.read(0x00, 2), Ok(0x1ae0)); // Vendor ID + + // Verify command register was initialized with INTX_DISABLE on backend + let cmd_val = { + let regions = mock.regions.read(); + let config_data = ®ions[&VfioPciRegion::CONFIG.raw()].1; + u16::from_le_bytes(config_data[4..6].try_into().unwrap()) + }; + let expected_cmd = Command::IO | Command::MEM | Command::BUS_MASTER | Command::INTX_DISABLE; + assert_eq!(cmd_val, expected_cmd.bits()); + + // Config space write and read (extra config area) + assert_matches!(config.write(0x100, 4, 0xcafe_babe), Ok(Action::None)); + assert_matches!(config.read(0x100, 4), Ok(0xcafe_babe)); + + // Reset via Pci trait + assert_matches!(Pci::reset(&dev), Ok(())); + assert_eq!(mock.resets.load(Ordering::SeqCst), 2); +} + +#[test] +fn test_vfio_pci_dev_unsupported_header_type() { + let mock = Arc::new(MockVfioDevice::default()); + let mut config_bytes = create_mock_pci_config_space(); + // Set header type to PCI-to-PCI bridge (0x01) + config_bytes[0x0e] = 0x01; + + mock.add_config(config_bytes); + + let res = VfioPciDev::new(Arc::from("test-vfio"), mock, TestMsiSender::default()); + assert_matches!(res, Err(Error::NotSupportedHeader { ty: 1, .. })); +} + +#[test] +fn test_vfio_pci_dev_msix_bar_and_irqfd() { + let mock = Arc::new(MockVfioDevice::default()); + let config_bytes = create_mock_pci_config_space(); + mock.add_config(config_bytes); + mock.add_bar( + VfioPciRegion::BAR0.raw(), + 0x10000, + VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE, + 0x1_0000, + ); + + let msi_sender = TestMsiSender::default(); + let dev = VfioPciDev::new(Arc::from("test-vfio"), mock.clone(), msi_sender).unwrap(); + + let PciBar::Mem(bar0) = &dev.config.header.bars[0] else { + panic!("expected Mem BAR for BAR 0"); + }; + + // BAR 0 should have Emulated range for MSI-X table & PBA (page 0) and Emulated/DevMem for remainder + let MemRange::Emulated(msix_bar_mmio) = &bar0.ranges[0] else { + panic!("expected Emulated range for MSI-X table in BAR 0"); + }; + + // Test writing to MSI-X table: entry 0 + // addr_lo = 0xfee0_0000, addr_hi = 0, data = 0x20, control = 0 (unmasked) + assert_matches!(msix_bar_mmio.write(0x00, 4, 0xfee0_0000), Ok(Action::None)); + assert_matches!(msix_bar_mmio.write(0x04, 4, 0), Ok(Action::None)); + assert_matches!(msix_bar_mmio.write(0x08, 4, 0x20), Ok(Action::None)); + // Unmasking triggers irqfd enablement + assert_matches!(msix_bar_mmio.write(0x0c, 4, 0), Ok(Action::None)); + + // Verify MSI-X eventfd was set on mock device + let events = mock.irq_eventfds.lock().clone(); + assert!(!events.is_empty()); + assert_eq!(events.last().unwrap().0, VfioPciIrq::MSIX.raw()); + assert_eq!(events.last().unwrap().1, 0); // start = 0 + assert_eq!(events.last().unwrap().2.len(), 1); // 1 active entry + + // Test reading back from MSI-X table + assert_matches!(msix_bar_mmio.read(0x00, 4), Ok(0xfee0_0000)); + assert_matches!(msix_bar_mmio.read(0x08, 4), Ok(0x20)); + + // Test reading / writing PBA area (offset 0x800) + assert_matches!(msix_bar_mmio.read(0x800, 4), Ok(0)); + assert_matches!(msix_bar_mmio.write(0x800, 4, 0), Ok(Action::None)); + + // Test reading / writing emulated region outside table/PBA within page 0 (e.g. offset 0x900) + assert_matches!(msix_bar_mmio.write(0x900, 4, 0x1122_3344), Ok(Action::None)); + assert_matches!(msix_bar_mmio.read(0x900, 4), Ok(0x1122_3344)); + + // Test reset disables active MSI-X IRQs + assert_matches!(Pci::reset(&dev), Ok(())); + let disabled = mock.disabled_irqs.lock().clone(); + assert!(disabled.contains(&VfioPciIrq::MSIX.raw())); +} + +#[test] +fn test_vfio_pci_dev_msi_only() { + let mock = Arc::new(MockVfioDevice::default()); + let mut config = vec![0u8; 4096]; + + let header = DeviceHeader { + common: CommonHeader { + vendor: 0x10ec, + device: 0x8168, + status: Status::CAP, + header_type: HeaderType::DEVICE, + ..Default::default() + }, + capability_pointer: 0x50, + ..Default::default() + }; + config[0..64].copy_from_slice(header.as_bytes()); + + // MSI capability at offset 0x50 (Id: 0x05, next: 0) + let mut msi_cap = MsiCapHdr { + header: PciCapHdr { + id: PciCapId::MSI, + next: 0, + }, + control: MsiMsgCtrl(0), + }; + msi_cap.control.set_multi_msg_cap(2); // 4 messages + config[0x50..0x50 + std::mem::size_of::()].copy_from_slice(msi_cap.as_bytes()); + + mock.add_config(config); + + let msi_sender = TestMsiSender::default(); + let dev = VfioPciDev::new(Arc::from("test-msi"), mock.clone(), msi_sender).unwrap(); + + // Verify MSI irq eventfds were registered + let events = mock.irq_eventfds.lock().clone(); + assert_eq!(events.len(), 1); + assert_eq!(events[0].0, VfioPciIrq::MSI.raw()); + assert_eq!(events[0].2.len(), 4); // 4 messages + + // Verify MSI capability MMIO at offset 0x50 + let config = dev.config(); + assert_matches!(config.read(0x50, 1), Ok(val) if val == PciCapId::MSI.raw() as u64); +} + +#[test] +fn test_vfio_pci_dev_both_msi_and_msix() { + let mock = Arc::new(MockVfioDevice::default()); + let mut config = vec![0u8; 4096]; + + let header = DeviceHeader { + common: CommonHeader { + vendor: 0x1af4, + device: 0x1000, + status: Status::CAP, + header_type: HeaderType::DEVICE, + ..Default::default() + }, + capability_pointer: 0x40, + ..Default::default() + }; + config[0..64].copy_from_slice(header.as_bytes()); + + // MSI at 0x40 -> points to MSI-X at 0x60 + let msi_cap = MsiCapHdr { + header: PciCapHdr { + id: PciCapId::MSI, + next: 0x60, + }, + control: MsiMsgCtrl(0), + }; + config[0x40..0x40 + std::mem::size_of::()].copy_from_slice(msi_cap.as_bytes()); + + // MSI-X at 0x60 -> next 0 + let msix_cap = MsixCap { + header: PciCapHdr { + id: PciCapId::MSIX, + next: 0, + }, + control: MsixMsgCtrl::new(4), + table_offset: MsixCapOffset::new(0, 0), + pba_offset: MsixCapOffset::new(0x1000, 0), + }; + config[0x60..0x60 + std::mem::size_of::()].copy_from_slice(msix_cap.as_bytes()); + + mock.add_config(config); + mock.add_bar( + VfioPciRegion::BAR0.raw(), + 0x10000, + VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE, + 0, + ); + + let dev = VfioPciDev::new(Arc::from("test-both"), mock, TestMsiSender::default()).unwrap(); + + let config = dev.config(); + // MSI at 0x40 was masked with NullCap (id reads 0, next reads 0x60) + assert_matches!(config.read(0x40, 1), Ok(0)); + assert_matches!(config.read(0x41, 1), Ok(0x60)); + // MSI-X at 0x60 is active + assert_matches!(config.read(0x60, 1), Ok(val) if val == PciCapId::MSIX.raw() as u64); +} + +#[test] +fn test_vfio_pci_dev_bar_types_and_splitting() { + let mock = Arc::new(MockVfioDevice::default()); + let mut config = create_mock_pci_config_space(); + + // Set BAR 0 as Mem32, BAR 1 as IO BAR + let (mut header, _) = DeviceHeader::read_from_prefix(&config).unwrap(); + header.bars[0] = BAR_MEM32; + header.bars[1] = BAR_IO; + config[0..64].copy_from_slice(header.as_bytes()); + + mock.add_config(config); + mock.add_bar( + VfioPciRegion::BAR0.raw(), + 0x10000, + VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE, + 0, + ); + mock.add_bar( + VfioPciRegion::BAR1.raw(), + 0x100, + VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE, + 0, + ); + + let dev = VfioPciDev::new(Arc::from("test-bars"), mock, TestMsiSender::default()).unwrap(); + + // BAR 0 should be PciBar::Mem with 2 ranges (page 0 emulated table/PBA, and remaining range) + let PciBar::Mem(bar0) = &dev.config.header.bars[0] else { + panic!("expected Mem BAR for BAR 0"); + }; + assert_eq!(bar0.ranges.len(), 2); + + // BAR 1 should be PciBar::Io + assert_matches!(dev.config.header.bars[1], PciBar::Io(_)); +} + +#[test] +fn test_pth_bar_region_mmio() { + let mock = Arc::new(MockVfioDevice::default()); + let region_info = mock.add_bar( + 0, + 0x1000, + VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE, + 0x1000, + ); + + let vfio_dev = Arc::new(VfioDev { + name: Arc::from("test-dev"), + dev: mock.clone(), + flags: VfioDeviceInfoFlag::empty(), + }); + + let pth = PthBarRegion { + cdev: vfio_dev, + size: 0x1000, + offset: 0, + region: Arc::new(region_info), + }; + + assert_eq!(pth.size(), 0x1000); + assert_matches!(pth.write(0x10, 4, 0x1234_5678), Ok(Action::None)); + assert_matches!(pth.read(0x10, 4), Ok(0x1234_5678)); +} + +#[test] +fn test_vfio_pci_dev_mmap_bar() { + let mock = Arc::new(MockVfioDevice::default()); + let mut config = create_mock_pci_config_space(); + // Clear MSI-X capability so BAR 0 is not split + let (mut header, _) = DeviceHeader::read_from_prefix(&config).unwrap(); + header.common.status = Status::empty(); + header.capability_pointer = 0; + config[0..64].copy_from_slice(header.as_bytes()); + + mock.add_config(config); + + // Create temp file for mmap + let tmp_file = tempfile::tempfile().unwrap(); + tmp_file.set_len(0x10000).unwrap(); + mock.mmap_fds.write().insert(0, tmp_file.into()); + + // BAR 0 with MMAP flag + mock.add_bar( + VfioPciRegion::BAR0.raw(), + 0x10000, + VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE | VfioRegionInfoFlag::MMAP, + 0, + ); + + let dev = VfioPciDev::new(Arc::from("test-mmap"), mock, TestMsiSender::default()).unwrap(); + assert_eq!(dev.config.header.bars.len(), 6); + let PciBar::Mem(bar0) = &dev.config.header.bars[0] else { + panic!("expected Mem BAR"); + }; + assert_matches!(bar0.ranges[0], MemRange::DevMem { .. }); +} + +#[test] +fn test_vfio_pci_dev_bar_mem64() { + let mock = Arc::new(MockVfioDevice::default()); + let mut config = create_mock_pci_config_space(); + + let (mut header, _) = DeviceHeader::read_from_prefix(&config).unwrap(); + header.bars[0] = crate::pci::config::BAR_MEM64; + header.bars[1] = 0; + config[0..64].copy_from_slice(header.as_bytes()); + + mock.add_config(config); + mock.add_bar( + VfioPciRegion::BAR0.raw(), + 0x20000, + VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE, + 0, + ); + + let dev = VfioPciDev::new(Arc::from("test-bar64"), mock, TestMsiSender::default()).unwrap(); + assert_matches!(dev.config.header.bars[0], PciBar::Mem(_)); + assert_matches!(dev.config.header.bars[1], PciBar::Empty); +} + +#[rstest] +#[case(0x82)] // offset beyond config space +#[case(0x7f)] // insufficient config space +fn test_vfio_pci_cap_parsing_malformed(#[case] offset: u8) { + let mock = Arc::new(MockVfioDevice::default()); + let mut config = create_mock_pci_config_space(); + config.resize(0x80, 0); + + let (mut header, _) = DeviceHeader::read_from_prefix(&config).unwrap(); + header.capability_pointer = offset; + config[0..64].copy_from_slice(header.as_bytes()); + + mock.add_config(config); + + // Should complete successfully without infinite loop or panic + let dev = VfioPciDev::new( + Arc::from("test-malformed-cap"), + mock, + TestMsiSender::default(), + ) + .unwrap(); + assert_eq!(dev.name(), "test-malformed-cap"); +} + +#[test] +fn test_vfio_pci_disjoint_and_reversed_msix_bar() { + let mock = Arc::new(MockVfioDevice::default()); + let mut config = create_mock_pci_config_space(); + + // Table at 0x20000, PBA at 0x10000 on BAR 0 of size 0x40000 + let msix_cap = MsixCap { + header: PciCapHdr { + id: PciCapId::MSIX, + next: 0, + }, + control: MsixMsgCtrl::new(4), // 4 entries + table_offset: MsixCapOffset::new(0x20000, 0), // BAR 0, offset 0x20000 + pba_offset: MsixCapOffset::new(0x10000, 0), // BAR 0, offset 0x10000 + }; + config[0x40..0x40 + std::mem::size_of::()].copy_from_slice(msix_cap.as_bytes()); + + mock.add_config(config); + mock.add_bar( + VfioPciRegion::BAR0.raw(), + 0x40000, + VfioRegionInfoFlag::READ | VfioRegionInfoFlag::WRITE, + 0, + ); + + let dev = VfioPciDev::new( + Arc::from("test-disjoint-bar"), + mock.clone(), + TestMsiSender::default(), + ) + .unwrap(); + let PciBar::Mem(bar0) = &dev.config.header.bars[0] else { + panic!("expected Mem BAR"); + }; + + // BAR 0 should be split: [PthBarRegion, Emulated(PBA), PthBarRegion, Emulated(Table), PthBarRegion] + assert_eq!(bar0.ranges.len(), 5); + + let MemRange::Emulated(table_mmio) = &bar0.ranges[3] else { + panic!("expected Emulated table mmio"); + }; + assert!(table_mmio.size() > 0); + + // 1. Write masked entry (control = 1) -> does not activate irqfd + table_mmio.write(0x20000 - 0x20000, 4, 0xfee0_0000).unwrap(); // addr_lo + table_mmio.write(0x20004 - 0x20000, 4, 0x0).unwrap(); // addr_hi + table_mmio.write(0x20008 - 0x20000, 4, 0x40).unwrap(); // data + table_mmio.write(0x2000c - 0x20000, 4, 0x1).unwrap(); // control (masked) + assert!(mock.irq_eventfds.lock().is_empty()); + + // 2. Unmask entry (control = 0) -> activates irqfd + table_mmio.write(0x2000c - 0x20000, 4, 0x0).unwrap(); + assert_eq!(mock.irq_eventfds.lock().len(), 1); + + // 3. Write again while already IrqFd (control = 0) + table_mmio.write(0x2000c - 0x20000, 4, 0x0).unwrap(); + assert_eq!(mock.irq_eventfds.lock().len(), 1); +} diff --git a/alioth/src/vfio/vfio.rs b/alioth/src/vfio/vfio.rs index 0005adaa..6f6a92b5 100644 --- a/alioth/src/vfio/vfio.rs +++ b/alioth/src/vfio/vfio.rs @@ -85,3 +85,7 @@ pub struct VfioContainerSpec { /// Path to the vfio device. [default: /dev/vfio/vfio] pub dev_vfio: Option>, } + +#[cfg(test)] +#[path = "vfio_test.rs"] +mod tests; diff --git a/alioth/src/vfio/vfio_test.rs b/alioth/src/vfio/vfio_test.rs new file mode 100644 index 00000000..9dab7ff1 --- /dev/null +++ b/alioth/src/vfio/vfio_test.rs @@ -0,0 +1,47 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::vfio::{VfioCdevSpec, VfioContainerSpec, VfioGroupSpec, VfioIoasSpec}; + +#[test] +fn test_vfio_specs_deserialization() { + // VfioCdevSpec + let cdev_aco = "path=/dev/vfio/devices/vfio0,ioas=my_ioas"; + let cdev_spec: VfioCdevSpec = serde_aco::from_arg(cdev_aco).unwrap(); + assert_eq!(cdev_spec.path.to_str().unwrap(), "/dev/vfio/devices/vfio0"); + assert_eq!(cdev_spec.ioas.as_deref(), Some("my_ioas")); + + // VfioIoasSpec + let ioas_aco = "name=ioas0,dev_iommu=/dev/iommu"; + let ioas_spec: VfioIoasSpec = serde_aco::from_arg(ioas_aco).unwrap(); + assert_eq!(&*ioas_spec.name, "ioas0"); + assert_eq!(ioas_spec.dev_iommu.unwrap().to_str().unwrap(), "/dev/iommu"); + + // VfioGroupSpec + let group_aco = "path=/dev/vfio/12,devices=0000:06:0d.0,container=c0"; + let group_spec: VfioGroupSpec = serde_aco::from_arg(group_aco).unwrap(); + assert_eq!(group_spec.path.to_str().unwrap(), "/dev/vfio/12"); + assert_eq!(group_spec.devices.len(), 1); + assert_eq!(&*group_spec.devices[0], "0000:06:0d.0"); + assert_eq!(group_spec.container.as_deref(), Some("c0")); + + // VfioContainerSpec + let container_aco = "name=c0,dev_vfio=/dev/vfio/vfio"; + let container_spec: VfioContainerSpec = serde_aco::from_arg(container_aco).unwrap(); + assert_eq!(&*container_spec.name, "c0"); + assert_eq!( + container_spec.dev_vfio.unwrap().to_str().unwrap(), + "/dev/vfio/vfio" + ); +}