use std::sync::Arc;
use virtio_bindings::virtio_mmio::{VIRTIO_MMIO_INT_CONFIG, VIRTIO_MMIO_INT_VRING};
use super::device::VirtioBlk;
use crate::vmm::PiMutex;
use crate::vmm::pci::{
ConfigSpace, PCI_COMMAND_MEMORY, PCI_COMMAND_WMASK, PCI_STATUS_CAP_LIST, PciFunction, REG_BAR0,
REG_CAP_PTR, REG_CLASS, REG_COMMAND, REG_DEVICE_ID, REG_INTERRUPT_LINE, REG_INTERRUPT_PIN,
REG_REVISION_ID, REG_STATUS, REG_SUBCLASS, REG_VENDOR_ID,
};
use crate::vmm::virtio_msix::{MSIX_TABLE_MAX, MsixRouteSink, MsixState, NO_VECTOR};
const VENDOR_ID: u16 = 0x1AF4;
const DEVICE_ID: u16 = 0x1042;
const REVISION: u8 = 0x01;
const CLASS_STORAGE: u8 = 0x01;
const SUBCLASS_SCSI: u8 = 0x00;
const INTERRUPT_PIN_INTA: u8 = 0x01;
const REGION_SIZE: u64 = 0x1000;
const COMMON_OFFSET: u64 = 0x0000;
const ISR_OFFSET: u64 = 0x1000;
const DEVICE_OFFSET: u64 = 0x2000;
const NOTIFY_OFFSET: u64 = 0x3000;
const MSIX_TABLE_OFFSET: u64 = 0x4000;
const MSIX_PBA_OFFSET: u64 = 0x5000;
const BAR0_SIZE: u64 = 0x8000;
const MSIX_ENTRY_SIZE: u64 = 16;
const _: () = assert!(
(MSIX_PBA_OFFSET - MSIX_TABLE_OFFSET) / MSIX_ENTRY_SIZE == MSIX_TABLE_MAX as u64,
"MSI-X table page must hold exactly MSIX_TABLE_MAX 16-byte entries"
);
const NOTIFY_OFF_MULTIPLIER: u32 = 4;
const BAR0_TYPE_BITS: u32 = 0x00;
const BAR0_LOW_WMASK: u32 = !((BAR0_SIZE as u32) - 1);
const CAP_VNDR: u8 = 0x09; const CFG_TYPE_COMMON: u8 = 1;
const CFG_TYPE_NOTIFY: u8 = 2;
const CFG_TYPE_ISR: u8 = 3;
const CFG_TYPE_DEVICE: u8 = 4;
const CAP_LEN_STD: u8 = 16; const CAP_LEN_NOTIFY: u8 = 20; const CAP_OFF_VNDR: u16 = 0;
const CAP_OFF_NEXT: u16 = 1;
const CAP_OFF_LEN: u16 = 2;
const CAP_OFF_CFG_TYPE: u16 = 3;
const CAP_OFF_BAR: u16 = 4;
const CAP_OFF_OFFSET: u16 = 8;
const CAP_OFF_LENGTH: u16 = 12;
const CAP_OFF_NOTIFY_MULT: u16 = 16;
const CAP_COMMON: u16 = 0x40;
const CAP_ISR: u16 = 0x50;
const CAP_DEVICE: u16 = 0x60;
const CAP_NOTIFY: u16 = 0x70;
const PCI_CAP_ID_MSIX: u8 = 0x11;
const CAP_MSIX: u16 = 0x84;
const MSIX_OFF_MSG_CTRL: u16 = 2;
const MSIX_OFF_TABLE: u16 = 4;
const MSIX_OFF_PBA: u16 = 8;
const MSIX_MSG_CTRL_WMASK: u16 = 0xC000;
const MSIX_BIR0: u32 = 0;
const CC_DEVICE_FEATURE_SELECT: u64 = 0x00;
const CC_DEVICE_FEATURE: u64 = 0x04;
const CC_DRIVER_FEATURE_SELECT: u64 = 0x08;
const CC_DRIVER_FEATURE: u64 = 0x0C;
const CC_MSIX_CONFIG: u64 = 0x10;
const CC_NUM_QUEUES: u64 = 0x12;
const CC_DEVICE_STATUS: u64 = 0x14;
const CC_CONFIG_GENERATION: u64 = 0x15;
const CC_QUEUE_SELECT: u64 = 0x16;
const CC_QUEUE_SIZE: u64 = 0x18;
const CC_QUEUE_MSIX_VECTOR: u64 = 0x1A;
const CC_QUEUE_ENABLE: u64 = 0x1C;
const CC_QUEUE_NOTIFY_OFF: u64 = 0x1E;
const CC_QUEUE_DESC_LO: u64 = 0x20;
const CC_QUEUE_DESC_HI: u64 = 0x24;
const CC_QUEUE_AVAIL_LO: u64 = 0x28;
const CC_QUEUE_AVAIL_HI: u64 = 0x2C;
const CC_QUEUE_USED_LO: u64 = 0x30;
const CC_QUEUE_USED_HI: u64 = 0x34;
pub(crate) struct VirtioBlkPci {
cfg: ConfigSpace,
blk: Arc<PiMutex<VirtioBlk>>,
bar_aperture: (u64, u64),
msix: Arc<PiMutex<MsixState>>,
route_sink: Option<Arc<dyn MsixRouteSink>>,
gsis: Vec<u32>,
bar_window_cache: Option<(u64, u64)>,
}
impl VirtioBlkPci {
pub(crate) fn new(
blk: Arc<PiMutex<VirtioBlk>>,
bar_aperture: (u64, u64),
msix: Arc<PiMutex<MsixState>>,
route_sink: Option<Arc<dyn MsixRouteSink>>,
gsis: Vec<u32>,
) -> Self {
debug_assert_eq!(
gsis.len(),
msix.lock().num_vectors(),
"MSI-X GSI count must equal the advertised vector count (num_vectors)"
);
if route_sink.is_some() {
blk.lock().set_msix_state(Arc::clone(&msix));
}
let mut cfg = ConfigSpace::new();
cfg.set_u16(REG_VENDOR_ID, VENDOR_ID);
cfg.set_u16(REG_DEVICE_ID, DEVICE_ID);
cfg.set_u8(REG_REVISION_ID, REVISION);
cfg.set_u8(REG_SUBCLASS, SUBCLASS_SCSI);
cfg.set_u8(REG_CLASS, CLASS_STORAGE);
cfg.set_u8(REG_INTERRUPT_PIN, INTERRUPT_PIN_INTA);
cfg.set_wmask_u16(REG_INTERRUPT_LINE, 0x00FF);
cfg.set_wmask_u16(REG_COMMAND, PCI_COMMAND_WMASK);
cfg.set_u16(REG_STATUS, PCI_STATUS_CAP_LIST);
cfg.set_u8(REG_CAP_PTR, CAP_COMMON as u8);
cfg.set_u32(REG_BAR0, BAR0_TYPE_BITS);
cfg.set_wmask_u32(REG_BAR0, BAR0_LOW_WMASK);
Self::write_caps(&mut cfg, route_sink.is_some(), gsis.len() as u16);
let mut this = Self {
cfg,
blk,
bar_aperture,
msix,
route_sink,
gsis,
bar_window_cache: None,
};
this.bar_window_cache = this.recompute_bar_window();
this
}
fn write_caps(cfg: &mut ConfigSpace, msix: bool, table_size: u16) {
Self::write_cap(cfg, CAP_COMMON, CAP_ISR, CFG_TYPE_COMMON, COMMON_OFFSET);
Self::write_cap(cfg, CAP_ISR, CAP_DEVICE, CFG_TYPE_ISR, ISR_OFFSET);
Self::write_cap(cfg, CAP_DEVICE, CAP_NOTIFY, CFG_TYPE_DEVICE, DEVICE_OFFSET);
let notify_next: u16 = if msix { CAP_MSIX } else { 0 };
Self::write_cap(cfg, CAP_NOTIFY, notify_next, CFG_TYPE_NOTIFY, NOTIFY_OFFSET);
cfg.set_u32(CAP_NOTIFY + CAP_OFF_NOTIFY_MULT, NOTIFY_OFF_MULTIPLIER);
if msix {
Self::write_msix_cap(cfg, table_size);
}
}
fn write_msix_cap(cfg: &mut ConfigSpace, table_size: u16) {
cfg.set_u8(CAP_MSIX, PCI_CAP_ID_MSIX);
cfg.set_u8(CAP_MSIX + 1, 0);
cfg.set_u16(CAP_MSIX + MSIX_OFF_MSG_CTRL, table_size - 1);
cfg.set_wmask_u16(CAP_MSIX + MSIX_OFF_MSG_CTRL, MSIX_MSG_CTRL_WMASK);
cfg.set_u32(
CAP_MSIX + MSIX_OFF_TABLE,
MSIX_TABLE_OFFSET as u32 | MSIX_BIR0,
);
cfg.set_u32(CAP_MSIX + MSIX_OFF_PBA, MSIX_PBA_OFFSET as u32 | MSIX_BIR0);
}
fn write_cap(cfg: &mut ConfigSpace, at: u16, next: u16, cfg_type: u8, region_off: u64) {
cfg.set_u8(at + CAP_OFF_VNDR, CAP_VNDR);
cfg.set_u8(at + CAP_OFF_NEXT, next as u8);
let cap_len = if cfg_type == CFG_TYPE_NOTIFY {
CAP_LEN_NOTIFY
} else {
CAP_LEN_STD
};
cfg.set_u8(at + CAP_OFF_LEN, cap_len);
cfg.set_u8(at + CAP_OFF_CFG_TYPE, cfg_type);
cfg.set_u8(at + CAP_OFF_BAR, 0); cfg.set_u32(at + CAP_OFF_OFFSET, region_off as u32);
cfg.set_u32(at + CAP_OFF_LENGTH, REGION_SIZE as u32);
}
fn put_le(val: u64, data: &mut [u8]) {
let bytes = val.to_le_bytes();
data.fill(0);
let n = data.len().min(8);
data[..n].copy_from_slice(&bytes[..n]);
}
fn get_u32(data: &[u8]) -> u32 {
let mut buf = [0u8; 4];
let n = data.len().min(4);
buf[..n].copy_from_slice(&data[..n]);
u32::from_le_bytes(buf)
}
fn clamp_vector(&self, v: u16) -> u16 {
if v == NO_VECTOR || v < self.gsis.len() as u16 {
v
} else {
NO_VECTOR
}
}
fn selected_queue_msix_vector(&self) -> u16 {
let sel = self.blk.lock().queue_select() as usize;
self.msix.lock().queue_vector(sel)
}
fn common_read(&self, off: u64, data: &mut [u8]) {
if off == CC_QUEUE_MSIX_VECTOR {
Self::put_le(self.selected_queue_msix_vector() as u64, data);
return;
}
let blk = self.blk.lock();
let val: u64 = match off {
CC_DEVICE_FEATURE_SELECT => blk.device_features_sel() as u64,
CC_DEVICE_FEATURE => blk.device_features_window() as u64,
CC_DRIVER_FEATURE_SELECT => blk.driver_features_sel() as u64,
CC_MSIX_CONFIG => self.msix.lock().config_vector() as u64,
CC_NUM_QUEUES => blk.num_queues() as u64,
CC_DEVICE_STATUS => blk.device_status() as u64,
CC_CONFIG_GENERATION => blk.config_generation() as u64,
CC_QUEUE_SELECT => blk.queue_select() as u64,
CC_QUEUE_SIZE => blk.queue_size() as u64,
CC_QUEUE_ENABLE => blk.queue_ready() as u64,
CC_QUEUE_NOTIFY_OFF => blk.queue_notify_off() as u64,
_ => 0,
};
Self::put_le(val, data);
}
fn common_write(&mut self, off: u64, data: &[u8]) {
let val = Self::get_u32(data);
let mut blk = self.blk.lock();
match off {
CC_DEVICE_FEATURE_SELECT => blk.set_device_features_sel(val),
CC_DRIVER_FEATURE_SELECT => blk.set_driver_features_sel(val),
CC_DRIVER_FEATURE => blk.set_driver_features_window(val),
CC_DEVICE_STATUS => blk.write_status(val & 0xFF),
CC_QUEUE_SELECT => blk.set_queue_select(val),
CC_QUEUE_SIZE => blk.set_queue_size(val as u16),
CC_QUEUE_ENABLE => blk.set_queue_ready(val != 0),
CC_QUEUE_DESC_LO => blk.set_queue_desc_addr(Some(val), None),
CC_QUEUE_DESC_HI => blk.set_queue_desc_addr(None, Some(val)),
CC_QUEUE_AVAIL_LO => blk.set_queue_avail_addr(Some(val), None),
CC_QUEUE_AVAIL_HI => blk.set_queue_avail_addr(None, Some(val)),
CC_QUEUE_USED_LO => blk.set_queue_used_addr(Some(val), None),
CC_QUEUE_USED_HI => blk.set_queue_used_addr(None, Some(val)),
CC_MSIX_CONFIG => {
let v = self.clamp_vector(val as u16);
self.msix.lock().set_config_vector(v);
}
CC_QUEUE_MSIX_VECTOR => {
let sel = blk.queue_select() as usize;
let v = self.clamp_vector(val as u16);
self.msix.lock().set_queue_vector(sel, v);
}
_ => {}
}
}
fn isr_read(&mut self, data: &mut [u8]) {
if data.is_empty() {
return;
}
let mut blk = self.blk.lock();
let isr = blk.interrupt_status();
let mut byte = 0u8;
if isr & VIRTIO_MMIO_INT_VRING != 0 {
byte |= 0x1;
}
if isr & VIRTIO_MMIO_INT_CONFIG != 0 {
byte |= 0x2;
}
blk.ack_interrupt(VIRTIO_MMIO_INT_VRING | VIRTIO_MMIO_INT_CONFIG);
data.fill(0);
if let Some(b) = data.first_mut() {
*b = byte;
}
}
fn notify_write(&mut self, off: u64) {
let idx = off / NOTIFY_OFF_MULTIPLIER as u64;
self.blk.lock().notify_queue(idx as u32);
}
fn msix_table_read(&self, rel: u64, data: &mut [u8]) {
let entry = (rel / MSIX_ENTRY_SIZE) as usize;
let dword = ((rel % MSIX_ENTRY_SIZE) / 4) as usize;
let val = self.msix.lock().table_dword(entry, dword);
Self::put_le(val as u64, data);
}
fn msix_table_write(&mut self, rel: u64, data: &[u8]) {
let entry = (rel / MSIX_ENTRY_SIZE) as usize;
let dword = ((rel % MSIX_ENTRY_SIZE) / 4) as usize;
let val = Self::get_u32(data);
let _ = self.msix.lock().write_table_dword(entry, dword, val);
self.reconcile_route(entry);
}
fn reconcile_route(&mut self, idx: usize) {
let deliverable = {
let m = self.msix.lock();
m.enabled() && m.vector_unmasked(idx)
};
if deliverable {
self.install_route(idx);
} else {
self.remove_route(idx);
}
}
fn reconcile_routes(&mut self) {
for idx in 0..self.gsis.len() {
self.reconcile_route(idx);
}
}
fn install_route(&mut self, idx: usize) {
let Some(sink) = self.route_sink.as_ref() else {
return;
};
let Some(&gsi) = self.gsis.get(idx) else {
return;
};
let msg = self.msix.lock().msi_message(idx);
if let Some(msg) = msg {
sink.set_route(gsi, Some(msg));
self.msix.lock().replay_pending(idx);
}
}
fn remove_route(&mut self, idx: usize) {
let Some(sink) = self.route_sink.as_ref() else {
return;
};
let Some(&gsi) = self.gsis.get(idx) else {
return;
};
sink.set_route(gsi, None);
}
fn write_touches(reg: u16, len: usize, target: u16, target_len: u16) -> bool {
let (r, r_end) = (reg as u32, reg as u32 + len as u32);
let (t, t_end) = (target as u32, target as u32 + target_len as u32);
r < t_end && t < r_end
}
fn msix_pba_read(&self, rel: u64, data: &mut [u8]) {
let base = rel as usize;
let m = self.msix.lock();
for (i, b) in data.iter_mut().enumerate() {
*b = m.pba_byte(base + i);
}
}
fn recompute_bar_window(&self) -> Option<(u64, u64)> {
let mut cmd = [0u8; 2];
self.cfg.read(REG_COMMAND, &mut cmd);
if u16::from_le_bytes(cmd) & PCI_COMMAND_MEMORY == 0 {
return None;
}
let mut lo = [0u8; 4];
self.cfg.read(REG_BAR0, &mut lo);
let base = (u32::from_le_bytes(lo) & !(BAR0_SIZE as u32 - 1)) as u64;
if base == 0 {
return None;
}
let (grant_start, grant_end) = self.bar_aperture;
if base < grant_start || base.saturating_add(BAR0_SIZE) > grant_end {
return None;
}
Some((base, BAR0_SIZE))
}
}
impl PciFunction for VirtioBlkPci {
fn config_read(&self, reg: u16, data: &mut [u8]) {
self.cfg.read(reg, data);
}
fn config_write(&mut self, reg: u16, data: &[u8]) {
self.cfg.write(reg, data);
if Self::write_touches(reg, data.len(), REG_COMMAND, 2)
|| Self::write_touches(reg, data.len(), REG_BAR0, 4)
{
self.bar_window_cache = self.recompute_bar_window();
}
let mc_reg = CAP_MSIX + MSIX_OFF_MSG_CTRL;
if Self::write_touches(reg, data.len(), mc_reg, 2) {
let mut mc = [0u8; 2];
self.cfg.read(mc_reg, &mut mc);
let msg_ctrl = u16::from_le_bytes(mc);
self.msix.lock().set_message_control(msg_ctrl);
self.reconcile_routes();
}
}
fn bar_window(&self) -> Option<(u64, u64)> {
self.bar_window_cache
}
fn bar_read(&mut self, offset: u64, data: &mut [u8]) {
match offset {
o if (COMMON_OFFSET..COMMON_OFFSET + REGION_SIZE).contains(&o) => {
self.common_read(o - COMMON_OFFSET, data);
}
ISR_OFFSET => {
self.isr_read(data);
}
o if (DEVICE_OFFSET..DEVICE_OFFSET + REGION_SIZE).contains(&o) => {
self.blk.lock().read_blk_config(o - DEVICE_OFFSET, data);
}
o if (MSIX_TABLE_OFFSET..MSIX_TABLE_OFFSET + REGION_SIZE).contains(&o) => {
self.msix_table_read(o - MSIX_TABLE_OFFSET, data);
}
o if (MSIX_PBA_OFFSET..MSIX_PBA_OFFSET + REGION_SIZE).contains(&o) => {
self.msix_pba_read(o - MSIX_PBA_OFFSET, data);
}
_ => data.fill(0),
}
}
fn bar_write(&mut self, offset: u64, data: &[u8]) {
match offset {
o if (COMMON_OFFSET..COMMON_OFFSET + REGION_SIZE).contains(&o) => {
self.common_write(o - COMMON_OFFSET, data);
}
o if (NOTIFY_OFFSET..NOTIFY_OFFSET + REGION_SIZE).contains(&o) => {
self.notify_write(o - NOTIFY_OFFSET);
}
o if (MSIX_TABLE_OFFSET..MSIX_TABLE_OFFSET + REGION_SIZE).contains(&o) => {
self.msix_table_write(o - MSIX_TABLE_OFFSET, data);
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::super::testing::{make_device, make_guest_mem};
use super::super::{DiskThrottle, S_ACK, S_DRV, S_FEAT, VIRTIO_BLK_SECTOR_SIZE};
use super::*;
use crate::vmm::virtio_msix::IrqSource;
use virtio_bindings::virtio_config::VIRTIO_F_VERSION_1;
use vmm_sys_util::eventfd::EventFd;
const GUEST_MEM: usize = 0x10_0000;
const CAP_BYTES: u64 = 0x10_0000; const TEST_BAR_APERTURE: (u64, u64) = (0xE010_0000, 0xFEC0_0000);
const TEST_BAR_BASE: u32 = 0xE010_0000; const TEST_ECAM_BASE: u32 = 0xE000_0000; const N_VECTORS: usize = 2;
#[derive(Default)]
#[allow(clippy::type_complexity)] struct MockRouteSink {
installs: std::sync::Mutex<Vec<(u32, Option<(u32, u32, u32)>)>>,
}
impl MsixRouteSink for MockRouteSink {
fn set_route(&self, gsi: u32, msg: Option<(u32, u32, u32)>) {
self.installs.lock().unwrap().push((gsi, msg));
}
}
fn test_gsis() -> Vec<u32> {
(0..N_VECTORS).map(|v| 40 + v as u32).collect()
}
fn build_blk() -> Arc<PiMutex<VirtioBlk>> {
let mut blk = make_device(CAP_BYTES, DiskThrottle::default());
blk.set_mem(make_guest_mem(GUEST_MEM));
Arc::new(PiMutex::new(blk))
}
fn new_pci() -> VirtioBlkPci {
let msix = Arc::new(PiMutex::new(MsixState::new(1, MSIX_TABLE_MAX)));
let sink: Arc<dyn MsixRouteSink> = Arc::new(MockRouteSink::default());
VirtioBlkPci::new(
build_blk(),
TEST_BAR_APERTURE,
msix,
Some(sink),
test_gsis(),
)
}
fn cfg8(pci: &VirtioBlkPci, reg: u16) -> u8 {
let mut b = [0u8; 1];
pci.config_read(reg, &mut b);
b[0]
}
fn cfg16(pci: &VirtioBlkPci, reg: u16) -> u16 {
let mut b = [0u8; 2];
pci.config_read(reg, &mut b);
u16::from_le_bytes(b)
}
fn cfg8u(pci: &VirtioBlkPci, reg: u16) -> u8 {
cfg8(pci, reg)
}
fn cc_w(pci: &mut VirtioBlkPci, cc: u64, val: u32) {
pci.bar_write(COMMON_OFFSET + cc, &val.to_le_bytes());
}
fn cc_r(pci: &mut VirtioBlkPci, cc: u64) -> u32 {
let mut b = [0u8; 4];
pci.bar_read(COMMON_OFFSET + cc, &mut b);
u32::from_le_bytes(b)
}
#[test]
fn pci_identity() {
let pci = new_pci();
assert_eq!(cfg16(&pci, REG_VENDOR_ID), 0x1AF4);
assert_eq!(cfg16(&pci, REG_DEVICE_ID), 0x1042); assert_eq!(cfg8(&pci, REG_REVISION_ID), 0x01);
assert_eq!(cfg8(&pci, REG_CLASS), 0x01); assert_eq!(cfg8(&pci, REG_SUBCLASS), 0x00); assert_eq!(cfg8(&pci, REG_INTERRUPT_PIN), 0x01); }
#[test]
fn cap_chain_advertises_msix() {
let pci = new_pci();
assert_ne!(cfg16(&pci, REG_STATUS) & PCI_STATUS_CAP_LIST, 0);
assert_eq!(cfg8(&pci, REG_CAP_PTR), CAP_COMMON as u8);
assert_eq!(cfg8u(&pci, CAP_NOTIFY + CAP_OFF_NEXT), CAP_MSIX as u8);
assert_eq!(cfg8(&pci, CAP_MSIX), PCI_CAP_ID_MSIX);
assert_eq!(
cfg16(&pci, CAP_MSIX + MSIX_OFF_MSG_CTRL) & 0x7FF,
(N_VECTORS as u16) - 1
);
}
#[test]
fn device_config_capacity_via_bar_device_region() {
let mut pci = new_pci();
let mut b = [0u8; 8];
pci.bar_read(DEVICE_OFFSET, &mut b);
assert_eq!(
u64::from_le_bytes(b),
CAP_BYTES / VIRTIO_BLK_SECTOR_SIZE as u64
);
}
#[test]
fn common_cfg_status_fsm_and_queue_config() {
let mut pci = new_pci();
cc_w(&mut pci, CC_DEVICE_STATUS, S_ACK);
cc_w(&mut pci, CC_DEVICE_STATUS, S_DRV);
cc_w(&mut pci, CC_DRIVER_FEATURE_SELECT, 1);
cc_w(
&mut pci,
CC_DRIVER_FEATURE,
1u32 << (VIRTIO_F_VERSION_1 - 32),
);
cc_w(&mut pci, CC_DEVICE_STATUS, S_FEAT);
assert_eq!(cc_r(&mut pci, CC_DEVICE_STATUS) & 0xFF, S_FEAT);
assert_eq!(cc_r(&mut pci, CC_NUM_QUEUES) & 0xFFFF, 1); cc_w(&mut pci, CC_QUEUE_SELECT, 0);
cc_w(&mut pci, CC_QUEUE_SIZE, 8);
assert_eq!(cc_r(&mut pci, CC_QUEUE_SIZE) & 0xFFFF, 8);
cc_w(&mut pci, CC_QUEUE_ENABLE, 1);
assert_eq!(cc_r(&mut pci, CC_QUEUE_ENABLE) & 0xFFFF, 1);
assert_eq!(cc_r(&mut pci, CC_QUEUE_NOTIFY_OFF) & 0xFFFF, 0); }
#[test]
fn isr_reads_zero_with_no_pending_interrupt() {
let mut pci = new_pci();
let mut b = [0u8; 1];
pci.bar_read(ISR_OFFSET, &mut b);
assert_eq!(b[0], 0);
}
#[test]
fn msix_enable_and_unmask_installs_route() {
let msix = Arc::new(PiMutex::new(MsixState::new(1, MSIX_TABLE_MAX)));
let sink = Arc::new(MockRouteSink::default());
let sink_dyn: Arc<dyn MsixRouteSink> = sink.clone();
let mut pci = VirtioBlkPci::new(
build_blk(),
TEST_BAR_APERTURE,
msix,
Some(sink_dyn),
test_gsis(),
);
pci.bar_write(MSIX_TABLE_OFFSET, &0xFEE0_0000u32.to_le_bytes()); pci.bar_write(MSIX_TABLE_OFFSET + 8, &0x4000u32.to_le_bytes()); pci.bar_write(MSIX_TABLE_OFFSET + 12, &0u32.to_le_bytes()); pci.config_write(CAP_MSIX + MSIX_OFF_MSG_CTRL, &0x8000u16.to_le_bytes());
let installs = sink.installs.lock().unwrap();
assert!(
installs
.iter()
.any(|(gsi, msg)| *gsi == 40 && msg.is_some()),
"enable+unmask installs vector 0's route at GSI 40: {installs:?}"
);
}
#[test]
fn bar_window_honors_crs_grant() {
let mut pci = new_pci();
pci.config_write(REG_BAR0, &TEST_BAR_BASE.to_le_bytes());
pci.config_write(REG_COMMAND, &PCI_COMMAND_MEMORY.to_le_bytes());
assert_eq!(pci.bar_window(), Some((TEST_BAR_BASE as u64, BAR0_SIZE)));
pci.config_write(REG_BAR0, &TEST_ECAM_BASE.to_le_bytes());
assert_eq!(pci.bar_window(), None);
}
#[test]
fn msix_vector_echo_readback() {
let mut pci = new_pci();
assert_eq!(cc_r(&mut pci, CC_MSIX_CONFIG) as u16, NO_VECTOR);
cc_w(&mut pci, CC_MSIX_CONFIG, 0);
assert_eq!(cc_r(&mut pci, CC_MSIX_CONFIG) as u16, 0);
cc_w(&mut pci, CC_QUEUE_SELECT, 0);
cc_w(&mut pci, CC_QUEUE_MSIX_VECTOR, 1);
assert_eq!(cc_r(&mut pci, CC_QUEUE_MSIX_VECTOR) as u16, 1);
assert_eq!(cc_r(&mut pci, CC_MSIX_CONFIG) as u16, 0);
}
#[test]
fn msix_vector_out_of_range_rejected() {
let mut pci = new_pci();
cc_w(&mut pci, CC_MSIX_CONFIG, N_VECTORS as u32);
assert_eq!(cc_r(&mut pci, CC_MSIX_CONFIG) as u16, NO_VECTOR);
cc_w(&mut pci, CC_QUEUE_SELECT, 0);
cc_w(&mut pci, CC_QUEUE_MSIX_VECTOR, 0xABCD);
assert_eq!(cc_r(&mut pci, CC_QUEUE_MSIX_VECTOR) as u16, NO_VECTOR);
}
#[test]
fn msix_table_roundtrip_and_bounds() {
let mut pci = new_pci();
let entry1 = MSIX_TABLE_OFFSET + MSIX_ENTRY_SIZE; let words = [0x1111_0000u32, 0x0000_2222, 0x3333_3333, 0x0000_0001];
for (i, w) in words.iter().enumerate() {
pci.bar_write(entry1 + (i as u64) * 4, &w.to_le_bytes());
}
for (i, w) in words.iter().enumerate() {
let mut b = [0u8; 4];
pci.bar_read(entry1 + (i as u64) * 4, &mut b);
assert_eq!(u32::from_le_bytes(b), *w, "table entry1 dword{i}");
}
let oob = MSIX_TABLE_OFFSET + N_VECTORS as u64 * MSIX_ENTRY_SIZE;
pci.bar_write(oob, &0xDEAD_BEEFu32.to_le_bytes());
let mut b = [0u8; 4];
pci.bar_read(oob, &mut b);
assert_eq!(u32::from_le_bytes(b), 0, "out-of-range table entry reads 0");
}
#[test]
fn msix_pba_reads_zero_and_read_only() {
let mut pci = new_pci();
pci.bar_write(MSIX_PBA_OFFSET, &0xFFu32.to_le_bytes());
let mut b = [0u8; 4];
pci.bar_read(MSIX_PBA_OFFSET, &mut b);
assert_eq!(
u32::from_le_bytes(b),
0,
"PBA reads 0 (no pending); write ignored"
);
}
const MC_ENABLE: u16 = 0x8000;
const MC_ENABLE_MASKALL: u16 = 0x8000 | 0x4000;
#[allow(clippy::type_complexity)]
fn build_msix() -> (
VirtioBlkPci,
Arc<PiMutex<MsixState>>,
[EventFd; N_VECTORS],
Arc<MockRouteSink>,
) {
let msix = Arc::new(PiMutex::new(MsixState::new(1, MSIX_TABLE_MAX)));
let evts: [EventFd; N_VECTORS] =
std::array::from_fn(|_| EventFd::new(libc::EFD_NONBLOCK).unwrap());
for (v, e) in evts.iter().enumerate() {
msix.lock().set_eventfd(v, e.try_clone().unwrap());
}
let sink = Arc::new(MockRouteSink::default());
let pci = VirtioBlkPci::new(
build_blk(),
TEST_BAR_APERTURE,
Arc::clone(&msix),
Some(Arc::clone(&sink) as Arc<dyn MsixRouteSink>),
test_gsis(),
);
(pci, msix, evts, sink)
}
fn enable_msix(pci: &mut VirtioBlkPci) {
cc_w(pci, CC_MSIX_CONFIG, 0);
cc_w(pci, CC_QUEUE_SELECT, 0);
cc_w(pci, CC_QUEUE_MSIX_VECTOR, 1);
pci.config_write(
CAP_MSIX + MSIX_OFF_MSG_CTRL,
&MC_ENABLE_MASKALL.to_le_bytes(),
);
for v in 0..N_VECTORS {
let base = MSIX_TABLE_OFFSET + (v as u64) * MSIX_ENTRY_SIZE;
pci.bar_write(base, &0xFEE0_0000u32.to_le_bytes()); pci.bar_write(base + 8, &(0x4000u32 + v as u32).to_le_bytes()); pci.bar_write(base + 12, &0u32.to_le_bytes()); }
pci.config_write(CAP_MSIX + MSIX_OFF_MSG_CTRL, &MC_ENABLE.to_le_bytes());
}
#[test]
fn msix_function_mask_clear_installs_routes() {
let (mut pci, _msix, _evts, sink) = build_msix();
enable_msix(&mut pci);
let installs = sink.installs.lock().unwrap();
for gsi in [40u32, 41] {
assert!(
installs.iter().any(|(g, m)| *g == gsi && m.is_some()),
"MASKALL-clear installs vector route at GSI {gsi}: {installs:?}"
);
}
}
#[test]
fn msix_vring_signal_fires_pends_and_replays() {
let (mut pci, msix, evts, _sink) = build_msix();
enable_msix(&mut pci);
msix.lock().signal(IrqSource::Vring { queue: 0 });
assert_eq!(evts[1].read().unwrap(), 1, "req-queue vector 1 fired");
assert!(
evts[0].read().is_err(),
"config vector 0 untouched (EAGAIN)"
);
assert_eq!(
msix.lock().pba_byte(0),
0,
"delivered live, nothing pending"
);
pci.bar_write(
MSIX_TABLE_OFFSET + MSIX_ENTRY_SIZE + 12,
&1u32.to_le_bytes(),
);
msix.lock().signal(IrqSource::Vring { queue: 0 });
assert!(
evts[1].read().is_err(),
"masked vector did not fire (EAGAIN)"
);
assert_eq!(
msix.lock().pba_byte(0) & (1 << 1),
1 << 1,
"pending bit set for the masked request-queue vector"
);
pci.bar_write(
MSIX_TABLE_OFFSET + MSIX_ENTRY_SIZE + 12,
&0u32.to_le_bytes(),
);
assert_eq!(
evts[1].read().unwrap(),
1,
"unmask replays the pending interrupt"
);
assert_eq!(
msix.lock().pba_byte(0) & (1 << 1),
0,
"pending cleared after replay"
);
}
#[test]
fn msix_config_signal_fires_config_vector() {
let (mut pci, msix, evts, _sink) = build_msix();
enable_msix(&mut pci);
msix.lock().signal(IrqSource::Config);
assert_eq!(evts[0].read().unwrap(), 1, "config vector 0 fired");
assert!(
evts[1].read().is_err(),
"req-queue vector 1 untouched (EAGAIN)"
);
}
}