From 76dbfdb71b860e8c97324292b6db924d18a9b0a5 Mon Sep 17 00:00:00 2001 From: changyuanl Date: Sat, 29 Aug 2026 14:33:16 -0700 Subject: [PATCH] test(virtio): add tests for virtio-pci Assisted-by: Antigravity:Gemini-3.7-Flash Signed-off-by: Changyuan Lyu --- alioth/src/hv/hv_test.rs | 66 ++++- alioth/src/virtio/dev/dev.rs | 2 +- alioth/src/virtio/pci_test.rs | 528 +++++++++++++++++++++++++++++++++- 3 files changed, 585 insertions(+), 11 deletions(-) diff --git a/alioth/src/hv/hv_test.rs b/alioth/src/hv/hv_test.rs index 3140ae96..9f16e825 100644 --- a/alioth/src/hv/hv_test.rs +++ b/alioth/src/hv/hv_test.rs @@ -12,11 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::io::{self, ErrorKind}; use std::os::fd::{AsFd, BorrowedFd}; +use std::sync::Arc; use parking_lot::{Condvar, Mutex, RwLock}; +use snafu::ResultExt; -use crate::hv::{IrqFd, IrqSender, MsiSender, Result}; +use crate::hv::{IoeventFd, IrqFd, IrqSender, MsiSender, Result, error}; #[derive(Debug)] struct TestIrqFdInner { @@ -115,18 +118,73 @@ impl IrqSender for TestIrqSender { #[derive(Debug, Default)] pub struct TestMsiSender { - pub messages: std::sync::Arc>>, + pub messages: Arc>>, + pub fail_mode: Option, } impl MsiSender for TestMsiSender { type IrqFd = TestIrqFd; - fn send(&self, addr: u64, data: u32) -> std::result::Result<(), crate::hv::Error> { + fn send(&self, addr: u64, data: u32) -> Result<()> { + if let Some(kind) = self.fail_mode { + return Err(io::Error::from(kind)).context(error::SendInterrupt); + } self.messages.lock().push((addr, data)); Ok(()) } - fn create_irqfd(&self) -> std::result::Result { + fn create_irqfd(&self) -> Result { + if let Some(kind) = self.fail_mode { + return Err(io::Error::from(kind)).context(error::IrqFd); + } Ok(TestIrqFd::default()) } } + +#[derive(Debug, Default)] +pub struct TestIoeventFd; + +impl AsFd for TestIoeventFd { + fn as_fd(&self) -> BorrowedFd<'_> { + unsafe { BorrowedFd::borrow_raw(0) } + } +} + +impl IoeventFd for TestIoeventFd {} + +#[derive(Debug, Default, PartialEq, Eq)] +pub struct RegisteredAddr { + pub gpa: u64, + pub len: u8, + pub data: Option, +} + +#[derive(Debug, Default)] +pub struct TestIoeventFdRegistry { + pub registered: Arc>>, + pub deregistered: Arc>, + pub fail_mode: Option, +} + +impl super::IoeventFdRegistry for TestIoeventFdRegistry { + type IoeventFd = TestIoeventFd; + + fn create(&self) -> Result { + if let Some(kind) = self.fail_mode { + return Err(io::Error::from(kind)).context(error::IoeventFd); + } + Ok(TestIoeventFd) + } + + fn register(&self, _fd: &Self::IoeventFd, gpa: u64, len: u8, data: Option) -> Result<()> { + self.registered + .lock() + .push(RegisteredAddr { gpa, len, data }); + Ok(()) + } + + fn deregister(&self, _fd: &Self::IoeventFd) -> Result<()> { + *self.deregistered.lock() += 1; + Ok(()) + } +} diff --git a/alioth/src/virtio/dev/dev.rs b/alioth/src/virtio/dev/dev.rs index 2839b55f..44d182b0 100644 --- a/alioth/src/virtio/dev/dev.rs +++ b/alioth/src/virtio/dev/dev.rs @@ -158,7 +158,7 @@ where pub shared_mem_regions: Option>, pub notifier: Arc, pub event_tx: Sender>, - worker_handle: Option>, + pub(crate) worker_handle: Option>, } impl VirtioDevice diff --git a/alioth/src/virtio/pci_test.rs b/alioth/src/virtio/pci_test.rs index 21b2f801..f32b4396 100644 --- a/alioth/src/virtio/pci_test.rs +++ b/alioth/src/virtio/pci_test.rs @@ -12,26 +12,38 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::io::ErrorKind; use std::mem::size_of; +use std::os::fd::AsRawFd; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering}; use assert_matches::assert_matches; -use parking_lot::RwLock; +use flume::Receiver; +use parking_lot::{Mutex, RwLock}; use rstest::rstest; -use crate::hv::tests::{TestIrqFd, TestMsiSender}; +use crate::hv::IoeventFd; +use crate::hv::tests::{ + RegisteredAddr, TestIoeventFd, TestIoeventFdRegistry, TestIrqFd, TestMsiSender, +}; use crate::mem::emulated::{Action, Mmio}; -use crate::pci::cap::{MsixTableEntry, MsixTableMmio, MsixTableMmioEntry}; +use crate::mem::{self, MemRange, MemRegion, MemRegionEntry, MemRegionType}; +use crate::pci::cap::{ + MsixTableEntry, MsixTableMmio, MsixTableMmioEntry, MsixVectorCtrl, PciCap, PciCapId, +}; +use crate::pci::config::{BAR_MEM32, BAR_MEM64, BAR_PREFETCHABLE, PciConfigArea}; +use crate::pci::{Pci, PciBar}; use crate::sync::notifier::Notifier; -use crate::virtio::dev::{Register, WakeEvent}; +use crate::virtio::dev::{Register, VirtioDevice, WakeEvent}; use crate::virtio::pci::{ - PciIrqSender, VIRTIO_MSI_NO_VECTOR, VirtioCommonCfg, VirtioPciMsixVector, VirtioPciRegister, + PciIrqSender, VIRTIO_MSI_NO_VECTOR, VirtioCommonCfg, VirtioPciCap, VirtioPciCap64, + VirtioPciDevice, VirtioPciMsixVector, VirtioPciNotifyCap, VirtioPciRegister, VirtioPciRegisterMmio, }; use crate::virtio::queue::{QUEUE_SIZE_MAX, QueueReg}; use crate::virtio::tests::FakeIoeventFd; -use crate::virtio::{DevStatus, VirtioFeature}; +use crate::virtio::{DevStatus, DeviceId, IrqSender, VirtioFeature}; type TestMmio = VirtioPciRegisterMmio; type TestWakeReceiver = flume::Receiver, FakeIoeventFd>>; @@ -1017,3 +1029,507 @@ fn test_mmio_size(#[case] num_queues: usize, #[case] expected_size: u64) { let (mmio, _) = create_test_mmio(queues); assert_eq!(mmio.size(), expected_size); } + +#[derive(Debug, Clone)] +struct TestDevConfig { + size: u64, +} + +impl Mmio for TestDevConfig { + fn size(&self) -> u64 { + self.size + } + + fn read(&self, _offset: u64, _size: u8) -> mem::Result { + Ok(0x42) + } + + fn write(&self, _offset: u64, _size: u8, _val: u64) -> mem::Result { + Ok(Action::None) + } +} + +fn create_test_virtio_device( + id: DeviceId, + config_size: u64, + shared_mem: Option>, + num_queues: usize, +) -> (VirtioDevice, Receiver>) +where + S: IrqSender, + E: IoeventFd, +{ + let (event_tx, event_rx) = flume::unbounded(); + let notifier = Arc::new(Notifier::new().unwrap()); + let queue_regs = (0..num_queues) + .map(|_| QueueReg::default()) + .collect::>(); + let dev = VirtioDevice { + name: "test-dev".into(), + id, + device_config: Arc::new(TestDevConfig { size: config_size }), + device_feature: (VirtioFeature::VERSION_1 | VirtioFeature::RING_PACKED).bits(), + queue_regs, + shared_mem_regions: shared_mem, + notifier, + event_tx, + worker_handle: None, + }; + (dev, event_rx) +} + +#[test] +fn test_pci_irq_sender_config_and_queue_irq() { + let msix_table = Arc::new(MsixTableMmio { + entries: RwLock::new( + vec![ + MsixTableMmioEntry::Entry(MsixTableEntry { + addr_lo: 0xfee0_0000, + addr_hi: 0, + data: 0x20, + control: MsixVectorCtrl(0), // unmasked + }), + MsixTableMmioEntry::Entry(MsixTableEntry { + addr_lo: 0xfee0_0000, + addr_hi: 0, + data: 0x21, + control: MsixVectorCtrl(1), // masked + }), + MsixTableMmioEntry::Entry(MsixTableEntry { + addr_lo: 0xfee0_0000, + addr_hi: 0x1, + data: 0x22, + control: MsixVectorCtrl(0), // unmasked 64-bit + }), + ] + .into_boxed_slice(), + ), + }); + let messages = Arc::new(Mutex::new(Vec::new())); + let msi_sender = TestMsiSender { + messages: messages.clone(), + ..Default::default() + }; + let irq_sender = PciIrqSender { + msix_vector: VirtioPciMsixVector { + config: AtomicU16::new(VIRTIO_MSI_NO_VECTOR), + queues: vec![ + AtomicU16::new(VIRTIO_MSI_NO_VECTOR), + AtomicU16::new(VIRTIO_MSI_NO_VECTOR), + ], + }, + msix_table: msix_table.clone(), + msi_sender, + }; + + // Config IRQ when VIRTIO_MSI_NO_VECTOR: no send + irq_sender.config_irq(); + assert!(messages.lock().is_empty()); + + // Config IRQ to vector 0: sends MSI + irq_sender.msix_vector.config.store(0, Ordering::Release); + irq_sender.config_irq(); + assert_eq!(*messages.lock(), vec![(0xfee0_0000, 0x20)]); + messages.lock().clear(); + + // Config IRQ to vector 1 (masked): no send + irq_sender.msix_vector.config.store(1, Ordering::Release); + irq_sender.config_irq(); + assert!(messages.lock().is_empty()); + + // Config IRQ to vector 2 (64-bit high address): sends to 0x1_fee0_0000 + irq_sender.msix_vector.config.store(2, Ordering::Release); + irq_sender.config_irq(); + assert_eq!(*messages.lock(), vec![(0x1_fee0_0000, 0x22)]); + messages.lock().clear(); + + // Config IRQ to vector 99 (invalid / out of bounds): logs error, does not send + irq_sender.msix_vector.config.store(99, Ordering::Release); + irq_sender.config_irq(); + assert!(messages.lock().is_empty()); + + // Queue IRQ when invalid index (e.g. 5): logs error + irq_sender.queue_irq(5); + assert!(messages.lock().is_empty()); + + // Queue IRQ when queue vector is VIRTIO_MSI_NO_VECTOR: does nothing + irq_sender.queue_irq(0); + assert!(messages.lock().is_empty()); + + // Queue IRQ for queue 0 with vector 0: sends MSI + irq_sender.msix_vector.queues[0].store(0, Ordering::Release); + irq_sender.queue_irq(0); + assert_eq!(*messages.lock(), vec![(0xfee0_0000, 0x20)]); + messages.lock().clear(); + + // Queue IRQ for queue 1 with vector 1 (masked): does not send + irq_sender.msix_vector.queues[1].store(1, Ordering::Release); + irq_sender.queue_irq(1); + assert!(messages.lock().is_empty()); +} + +#[test] +fn test_pci_irq_sender_error_sending() { + let msix_table = Arc::new(MsixTableMmio { + entries: RwLock::new( + vec![MsixTableMmioEntry::Entry(MsixTableEntry { + addr_lo: 0xfee0_0000, + addr_hi: 0, + data: 0x20, + control: MsixVectorCtrl(0), + })] + .into_boxed_slice(), + ), + }); + let irq_sender = PciIrqSender { + msix_vector: VirtioPciMsixVector { + config: AtomicU16::new(0), + queues: vec![], + }, + msix_table, + msi_sender: TestMsiSender { + fail_mode: Some(std::io::ErrorKind::Other), + ..Default::default() + }, + }; + // Should handle error gracefully without panicking + irq_sender.config_irq(); +} + +#[test] +fn test_pci_irq_sender_irqfd() { + let msix_table = Arc::new(MsixTableMmio { + entries: RwLock::new( + vec![ + MsixTableMmioEntry::Entry(MsixTableEntry { + addr_lo: 0xfee0_0000, + addr_hi: 0, + data: 0x20, + control: MsixVectorCtrl(0), + }), + MsixTableMmioEntry::Entry(MsixTableEntry { + addr_lo: 0xfee0_1000, + addr_hi: 0, + data: 0x21, + control: MsixVectorCtrl(0), + }), + ] + .into_boxed_slice(), + ), + }); + let irq_sender = PciIrqSender { + msix_vector: VirtioPciMsixVector { + config: AtomicU16::new(VIRTIO_MSI_NO_VECTOR), + queues: vec![AtomicU16::new(VIRTIO_MSI_NO_VECTOR), AtomicU16::new(1)], + }, + msix_table: msix_table.clone(), + msi_sender: TestMsiSender::default(), + }; + + // config_irqfd with invalid vector returns Err + assert!(irq_sender.config_irqfd(|_| Ok(())).is_err()); + + // config_irqfd with valid vector 0 transforms Entry into IrqFd + irq_sender.msix_vector.config.store(0, Ordering::Release); + let r = irq_sender.config_irqfd(|fd| Ok(fd.as_raw_fd())); + assert_matches!(r, Ok(_)); + assert_matches!(msix_table.entries.read()[0], MsixTableMmioEntry::IrqFd(_)); + + // Subsequent config_irqfd call reuses existing IrqFd + let r2 = irq_sender.config_irqfd(|fd| Ok(fd.as_raw_fd())); + assert_matches!(r2, Ok(_)); + + // queue_irqfd with invalid queue index returns Err + assert!(irq_sender.queue_irqfd(99, |_| Ok(())).is_err()); + + // queue_irqfd for queue 0 (vector VIRTIO_MSI_NO_VECTOR) returns Err + assert!(irq_sender.queue_irqfd(0, |_| Ok(())).is_err()); + + // queue_irqfd for queue 1 (vector 1) transforms Entry into IrqFd + let qr = irq_sender.queue_irqfd(1, |fd| Ok(fd.as_raw_fd())); + assert_matches!(qr, Ok(_)); + assert_matches!(msix_table.entries.read()[1], MsixTableMmioEntry::IrqFd(_)); + + // config_irqfd when create_irqfd fails returns Err + let msix_table_entry_only = Arc::new(MsixTableMmio { + entries: RwLock::new( + vec![MsixTableMmioEntry::Entry(MsixTableEntry::default())].into_boxed_slice(), + ), + }); + let fail_sender = PciIrqSender { + msix_vector: VirtioPciMsixVector { + config: AtomicU16::new(0), + queues: vec![], + }, + msix_table: msix_table_entry_only, + msi_sender: TestMsiSender { + fail_mode: Some(std::io::ErrorKind::Other), + ..Default::default() + }, + }; + assert!(fail_sender.config_irqfd(|_| Ok(())).is_err()); +} + +#[rstest] +#[case(DeviceId::NET, 0x02, 0x00, 0x1041)] +#[case(DeviceId::BLOCK, 0x01, 0x00, 0x1042)] +#[case(DeviceId::FILE_SYSTEM, 0x01, 0x80, 0x105a)] +#[case(DeviceId::SOCKET, 0x02, 0x80, 0x1053)] +#[case(DeviceId::ENTROPY, 0xff, 0x00, 0x1044)] +#[case(DeviceId(99), 0xff, 0x00, 0x1040 + 99)] +fn test_virtio_pci_device_classes( + #[case] id: DeviceId, + #[case] expected_class: u8, + #[case] expected_subclass: u8, + #[case] expected_dev_id: u16, +) { + let (dev, _rx) = + create_test_virtio_device::, TestIoeventFd>(id, 0, None, 2); + let pci_dev = VirtioPciDevice::new( + dev, + TestMsiSender::default(), + TestIoeventFdRegistry::default(), + ) + .unwrap(); + + let config = pci_dev.config(); + // Vendor ID + assert_matches!(config.read(0x00, 2), Ok(0x1af4)); + // Device ID + assert_matches!(config.read(0x02, 2), Ok(val) if val == expected_dev_id as u64); + // Revision + assert_matches!(config.read(0x08, 1), Ok(1)); + // Class and subclass + let class_code = ((expected_class as u64) << 8) | (expected_subclass as u64); + assert_matches!(config.read(0x0a, 2), Ok(val) if val == class_code); + // Header Type + assert_matches!(config.read(0x0e, 1), Ok(0x00)); + // Subsystem Vendor & ID + assert_matches!( + config.read(0x2c, 4), + Ok(val) if val == ((expected_dev_id as u64) << 16) + ); + // Name + assert_eq!(Pci::name(&pci_dev), "test-dev"); + + // Queue reset write + assert_matches!( + pci_dev + .registers + .write(VirtioCommonCfg::OFFSET_QUEUE_RESET as u64, 2, 1,), + Ok(Action::None) + ); +} + +#[test] +fn test_virtio_pci_device_with_config_and_shared_memory_prefetchable() { + let shared_mem = Arc::new(MemRegion { + ranges: vec![MemRange::Span(0x1000), MemRange::Span(0x2_0000_0000)], + entries: vec![ + MemRegionEntry { + size: 0x1000, + type_: MemRegionType::Hidden, + }, + MemRegionEntry { + size: 0x2_0000_0000, + type_: MemRegionType::Hidden, + }, + ], + callbacks: Mutex::new(vec![]), + }); + let (dev, _rx) = create_test_virtio_device::, TestIoeventFd>( + DeviceId::FILE_SYSTEM, + 32, + Some(shared_mem), + 2, + ); + let pci_dev = VirtioPciDevice::new( + dev, + TestMsiSender::default(), + TestIoeventFdRegistry::default(), + ) + .unwrap(); + + // BAR 0 should be MEM32 + assert_matches!(pci_dev.config.header.bars[0], PciBar::Mem(_)); + assert_matches!(pci_dev.config.read(0x10, 4), Ok(val) if val as u32 == BAR_MEM32); + + // BAR 2 should be MEM64 | PREFETCHABLE + assert_matches!(pci_dev.config.header.bars[2], PciBar::Mem(_)); + assert_matches!( + pci_dev.config.read(0x18, 4), + Ok(val) if val as u32 == (BAR_MEM64 | BAR_PREFETCHABLE) + ); + + // Caps list should contain MSI-X, Common, ISR, Notify, Device Config, and 2 Shared Memory caps + let mut cap_offset = pci_dev.config.read(0x34, 1).unwrap(); + let mut cap_types = Vec::new(); + while cap_offset != 0 { + let cap_id = pci_dev.config.read(cap_offset, 1).unwrap(); + let next = pci_dev.config.read(cap_offset + 1, 1).unwrap(); + let cfg_type = if cap_id == PciCapId::VENDOR.raw() as u64 { + Some(pci_dev.config.read(cap_offset + 3, 1).unwrap()) + } else { + None + }; + cap_types.push((cap_id, cfg_type)); + cap_offset = next; + } + + assert_eq!(cap_types.len(), 7); // MSIX, Common, ISR, Notify, Device, SharedMem0, SharedMem1 +} + +#[test] +fn test_virtio_pci_device_shared_memory_non_prefetchable() { + let shared_mem = Arc::new(MemRegion { + ranges: vec![MemRange::Emulated(Arc::new(TestDevConfig { size: 0x1000 }))], + entries: vec![MemRegionEntry { + size: 0x1000, + type_: MemRegionType::Hidden, + }], + callbacks: Mutex::new(vec![]), + }); + let (dev, _rx) = create_test_virtio_device::, TestIoeventFd>( + DeviceId::FILE_SYSTEM, + 0, + Some(shared_mem), + 1, + ); + let pci_dev = VirtioPciDevice::new( + dev, + TestMsiSender::default(), + TestIoeventFdRegistry::default(), + ) + .unwrap(); + + // BAR 2 should be MEM32 when non-prefetchable (emulated ranges) + assert_matches!(pci_dev.config.read(0x18, 4), Ok(val) if val as u32 == BAR_MEM32); +} + +#[test] +fn test_virtio_pci_device_ioeventfd_callback() { + let (dev, _rx) = create_test_virtio_device::, TestIoeventFd>( + DeviceId::NET, + 0, + None, + 2, + ); + let registry = TestIoeventFdRegistry::default(); + let registered = registry.registered.clone(); + let deregistered = registry.deregistered.clone(); + let pci_dev = VirtioPciDevice::new(dev, TestMsiSender::default(), registry).unwrap(); + + let PciBar::Mem(bar0) = &pci_dev.config.header.bars[0] else { + panic!("expected Mem BAR"); + }; + + let callbacks = bar0.callbacks.lock(); + assert_eq!(callbacks.len(), 2); + + // Test mapped callback + let base_addr = 0x2000_0000; + assert_matches!(callbacks[0].mapped(base_addr), Ok(())); + let registered = registered.lock(); + assert_eq!(registered.len(), 2); + let expected_notify_base = + base_addr + (12 << 10) + VirtioPciRegister::OFFSET_QUEUE_NOTIFY as u64; + assert_eq!( + registered[0], + RegisteredAddr { + gpa: expected_notify_base, + len: 0, + data: None + } + ); + assert_eq!( + registered[1], + RegisteredAddr { + gpa: expected_notify_base + 4, + len: 0, + data: None + } + ); + + // Test unmapped callback + assert_matches!(callbacks[0].unmapped(), Ok(())); + assert_eq!(*deregistered.lock(), 2); +} + +#[rstest] +#[case(Some(ErrorKind::Unsupported))] +#[case(Some(ErrorKind::PermissionDenied))] +fn test_virtio_pci_device_ioeventfd_fallback(#[case] fail_mode: Option) { + let (dev, _rx) = create_test_virtio_device::, TestIoeventFd>( + DeviceId::NET, + 0, + None, + 1, + ); + let registry = TestIoeventFdRegistry { + fail_mode, + ..Default::default() + }; + let pci_dev = VirtioPciDevice::new(dev, TestMsiSender::default(), registry).unwrap(); + + assert!(pci_dev.registers.ioeventfds.is_none()); +} + +#[test] +fn test_virtio_pci_device_pci_reset() { + let (dev, event_rx) = create_test_virtio_device::, TestIoeventFd>( + DeviceId::NET, + 0, + None, + 1, + ); + let pci_dev = VirtioPciDevice::new( + dev, + TestMsiSender::default(), + TestIoeventFdRegistry::default(), + ) + .unwrap(); + + // Set queue enabled and device status + pci_dev.registers.queues[0] + .enabled + .store(true, Ordering::Release); + pci_dev + .registers + .reg + .status + .store(DevStatus::DRIVER_OK.bits(), Ordering::Release); + + // Call Pci::reset + assert_matches!(pci_dev.reset(), Ok(())); + + // Device status should be cleared + assert_eq!(pci_dev.registers.reg.status.load(Ordering::Acquire), 0); + // Queue should be disabled + assert!(!pci_dev.registers.queues[0].enabled.load(Ordering::Acquire)); + // Reset event sent + assert_matches!(event_rx.try_recv(), Ok(WakeEvent::Reset)); +} + +#[test] +fn test_virtio_pci_cap_traits() { + let mut cap = VirtioPciCap::default(); + PciCap::set_next(&mut cap, 0x50); + assert_eq!(cap.header.next, 0x50); + assert_matches!(PciConfigArea::reset(&cap), Ok(())); + assert_matches!(cap.read(0, 1), Ok(0)); + assert_matches!(cap.write(0, 1, 0), Ok(Action::None)); + + let mut cap64 = VirtioPciCap64::default(); + PciCap::set_next(&mut cap64, 0x60); + assert_eq!(cap64.cap.header.next, 0x60); + assert_matches!(PciConfigArea::reset(&cap64), Ok(())); + assert_matches!(cap64.read(0, 1), Ok(0)); + assert_matches!(cap64.write(0, 1, 0), Ok(Action::None)); + + let mut notify_cap = VirtioPciNotifyCap::default(); + PciCap::set_next(&mut notify_cap, 0x70); + assert_eq!(notify_cap.cap.header.next, 0x70); + assert_matches!(PciConfigArea::reset(¬ify_cap), Ok(())); + assert_matches!(notify_cap.read(0, 1), Ok(0)); + assert_matches!(notify_cap.write(0, 1, 0), Ok(Action::None)); +}