use std::sync::Arc;
use virtio_bindings::virtio_mmio::{VIRTIO_MMIO_INT_CONFIG, VIRTIO_MMIO_INT_VRING};
use super::device::{NUM_QUEUES, VirtioNet};
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 = 0x1041;
const REVISION: u8 = 0x01;
const CLASS_NETWORK: u8 = 0x02;
const SUBCLASS_ETHERNET: 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 _: () = assert!(
NUM_QUEUES == 2,
"NUM_QUEUES is the single-pair baseline (1 RX + 1 TX) the default \
single-queue device assumes. The live per-device queue count is dynamic \
(VirtioNet::num_queues) and the MSI-X table is sized from it; multiqueue \
is gated on the MSI-X transport in device_features (see above)."
);
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 VirtioNetPci {
cfg: ConfigSpace,
net: VirtioNet,
bar_aperture: (u64, u64),
msix: Arc<PiMutex<MsixState>>,
route_sink: Option<Arc<dyn MsixRouteSink>>,
gsis: Vec<u32>,
bar_window_cache: Option<(u64, u64)>,
}
impl VirtioNetPci {
pub(crate) fn new(
net: VirtioNet,
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)"
);
let mut net = net;
if route_sink.is_some() {
net.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_ETHERNET);
cfg.set_u8(REG_CLASS, CLASS_NETWORK);
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,
net,
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.net.queue_select() as usize;
self.msix.lock().queue_vector(sel)
}
fn common_read(&self, off: u64, data: &mut [u8]) {
let val: u64 = match off {
CC_DEVICE_FEATURE_SELECT => self.net.device_features_sel() as u64,
CC_DEVICE_FEATURE => self.net.device_features_window() as u64,
CC_DRIVER_FEATURE_SELECT => self.net.driver_features_sel() as u64,
CC_MSIX_CONFIG => self.msix.lock().config_vector() as u64,
CC_NUM_QUEUES => self.net.num_queues() as u64,
CC_DEVICE_STATUS => self.net.device_status() as u64,
CC_CONFIG_GENERATION => self.net.config_generation() as u64,
CC_QUEUE_SELECT => self.net.queue_select() as u64,
CC_QUEUE_SIZE => self.net.queue_size() as u64,
CC_QUEUE_MSIX_VECTOR => self.selected_queue_msix_vector() as u64,
CC_QUEUE_ENABLE => self.net.queue_ready() as u64,
CC_QUEUE_NOTIFY_OFF => self.net.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);
match off {
CC_DEVICE_FEATURE_SELECT => self.net.set_device_features_sel(val),
CC_DRIVER_FEATURE_SELECT => self.net.set_driver_features_sel(val),
CC_DRIVER_FEATURE => self.net.set_driver_features_window(val),
CC_DEVICE_STATUS => self.net.write_status(val & 0xFF),
CC_QUEUE_SELECT => self.net.set_queue_select(val),
CC_QUEUE_SIZE => self.net.set_queue_size(val as u16),
CC_QUEUE_ENABLE => self.net.set_queue_ready(val != 0),
CC_QUEUE_DESC_LO => self.net.set_queue_desc_addr(Some(val), None),
CC_QUEUE_DESC_HI => self.net.set_queue_desc_addr(None, Some(val)),
CC_QUEUE_AVAIL_LO => self.net.set_queue_avail_addr(Some(val), None),
CC_QUEUE_AVAIL_HI => self.net.set_queue_avail_addr(None, Some(val)),
CC_QUEUE_USED_LO => self.net.set_queue_used_addr(Some(val), None),
CC_QUEUE_USED_HI => self.net.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 = self.net.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 isr = self.net.interrupt_status();
let mut byte = 0u8;
if isr & VIRTIO_MMIO_INT_VRING != 0 {
byte |= 0x1;
}
if isr & VIRTIO_MMIO_INT_CONFIG != 0 {
byte |= 0x2;
}
self.net
.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.net.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 VirtioNetPci {
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.net.config_bytes((o - DEVICE_OFFSET) as usize, 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::*;
use crate::vmm::net_config::NetConfig;
use crate::vmm::virtio_net::device::{
QUEUE_MAX_SIZE, RXQ, S_ACK, S_DRV, S_FEAT, S_OK, TXQ, VIRTIO_NET_HDR_LEN,
};
use proptest::prelude::*;
use std::sync::atomic::Ordering;
use virtio_bindings::virtio_config::{VIRTIO_CONFIG_S_NEEDS_RESET, VIRTIO_F_VERSION_1};
use virtio_bindings::virtio_net::VIRTIO_NET_F_MAC;
use vm_memory::{Bytes, GuestAddress, GuestMemoryMmap};
use vmm_sys_util::eventfd::EventFd;
const GUEST_MEM_SIZE: usize = 0x10_0000;
const TX_DESC: u64 = 0x1000;
const TX_AVAIL: u64 = 0x2000;
const TX_USED: u64 = 0x3000;
const TX_BUF: u64 = 0x5000;
const RX_DESC: u64 = 0x6000;
const RX_AVAIL: u64 = 0x7000;
const RX_USED: u64 = 0x8000;
const RX_BUF: u64 = 0x9000;
const TEST_BAR_APERTURE: (u64, u64) = (0xE010_0000, 0xFEC0_0000);
const TEST_BAR_BASE: u32 = 0xE010_0000; const TEST_ECAM_BASE: u32 = 0xE000_0000;
fn test_mem() -> GuestMemoryMmap {
GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), GUEST_MEM_SIZE)]).unwrap()
}
#[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));
}
}
const SINGLE_PAIR_VECTORS: usize = NUM_QUEUES + 1;
fn test_gsis() -> Vec<u32> {
(0..SINGLE_PAIR_VECTORS).map(|v| 24 + v as u32).collect()
}
fn new_pci(net: VirtioNet) -> VirtioNetPci {
let msix = Arc::new(PiMutex::new(MsixState::new(NUM_QUEUES, MSIX_TABLE_MAX)));
let sink: Arc<dyn MsixRouteSink> = Arc::new(MockRouteSink::default());
VirtioNetPci::new(net, TEST_BAR_APERTURE, msix, Some(sink), test_gsis())
}
fn build(
mem: &GuestMemoryMmap,
) -> (
VirtioNetPci,
std::sync::Arc<super::super::VirtioNetCounters>,
) {
let mut net = VirtioNet::new(NetConfig::default());
net.set_mem(mem.clone());
let counters = net.counters();
(new_pci(net), counters)
}
fn cfg8(pci: &VirtioNetPci, reg: u16) -> u8 {
let mut b = [0u8; 1];
pci.config_read(reg, &mut b);
b[0]
}
fn cfg16(pci: &VirtioNetPci, reg: u16) -> u16 {
let mut b = [0u8; 2];
pci.config_read(reg, &mut b);
u16::from_le_bytes(b)
}
fn cfg32(pci: &VirtioNetPci, reg: u16) -> u32 {
let mut b = [0u8; 4];
pci.config_read(reg, &mut b);
u32::from_le_bytes(b)
}
fn cc_w(pci: &mut VirtioNetPci, cc: u64, val: u32) {
pci.bar_write(COMMON_OFFSET + cc, &val.to_le_bytes());
}
fn cc_r(pci: &mut VirtioNetPci, cc: u64) -> u32 {
let mut b = [0u8; 4];
pci.bar_read(COMMON_OFFSET + cc, &mut b);
u32::from_le_bytes(b)
}
fn drive_to_features_ok(pci: &mut VirtioNetPci) {
cc_w(pci, CC_DEVICE_STATUS, S_ACK);
cc_w(pci, CC_DEVICE_STATUS, S_DRV);
cc_w(pci, CC_DRIVER_FEATURE_SELECT, 0);
cc_w(pci, CC_DRIVER_FEATURE, 1u32 << VIRTIO_NET_F_MAC);
cc_w(pci, CC_DRIVER_FEATURE_SELECT, 1);
cc_w(pci, CC_DRIVER_FEATURE, 1u32 << (VIRTIO_F_VERSION_1 - 32));
cc_w(pci, CC_DEVICE_STATUS, S_FEAT);
}
fn program_queue(pci: &mut VirtioNetPci, q: u32, desc: u64, avail: u64, used: u64) {
cc_w(pci, CC_QUEUE_SELECT, q);
cc_w(pci, CC_QUEUE_SIZE, 4);
cc_w(pci, CC_QUEUE_DESC_LO, desc as u32);
cc_w(pci, CC_QUEUE_AVAIL_LO, avail as u32);
cc_w(pci, CC_QUEUE_USED_LO, used as u32);
cc_w(pci, CC_QUEUE_ENABLE, 1);
}
fn write_desc(
mem: &GuestMemoryMmap,
table: u64,
idx: u16,
addr: u64,
len: u32,
flags: u16,
next: u16,
) {
let off = table + (idx as u64) * 16;
let mut buf = [0u8; 16];
buf[0..8].copy_from_slice(&addr.to_le_bytes());
buf[8..12].copy_from_slice(&len.to_le_bytes());
buf[12..14].copy_from_slice(&flags.to_le_bytes());
buf[14..16].copy_from_slice(&next.to_le_bytes());
mem.write_slice(&buf, GuestAddress(off)).unwrap();
}
fn publish_avail(mem: &GuestMemoryMmap, avail: u64, head: u16) {
mem.write_slice(&head.to_le_bytes(), GuestAddress(avail + 4))
.unwrap();
mem.write_slice(&1u16.to_le_bytes(), GuestAddress(avail + 2))
.unwrap();
}
#[test]
fn config_identity_and_capability_chain() {
let mem = test_mem();
let (pci, _c) = build(&mem);
assert_eq!(cfg16(&pci, REG_VENDOR_ID), VENDOR_ID);
assert_eq!(cfg16(&pci, REG_DEVICE_ID), DEVICE_ID);
assert_eq!(cfg8(&pci, REG_REVISION_ID), REVISION);
assert_eq!(cfg8(&pci, REG_CLASS), CLASS_NETWORK);
assert_eq!(cfg8(&pci, REG_SUBCLASS), SUBCLASS_ETHERNET);
assert_eq!(cfg8(&pci, REG_INTERRUPT_PIN), INTERRUPT_PIN_INTA);
assert_ne!(cfg16(&pci, REG_STATUS) & PCI_STATUS_CAP_LIST, 0);
assert_eq!(cfg8(&pci, REG_CAP_PTR), CAP_COMMON as u8);
let chain = [
(
CAP_COMMON,
CAP_ISR as u8,
CFG_TYPE_COMMON,
CAP_LEN_STD,
COMMON_OFFSET,
),
(
CAP_ISR,
CAP_DEVICE as u8,
CFG_TYPE_ISR,
CAP_LEN_STD,
ISR_OFFSET,
),
(
CAP_DEVICE,
CAP_NOTIFY as u8,
CFG_TYPE_DEVICE,
CAP_LEN_STD,
DEVICE_OFFSET,
),
(
CAP_NOTIFY,
CAP_MSIX as u8,
CFG_TYPE_NOTIFY,
CAP_LEN_NOTIFY,
NOTIFY_OFFSET,
),
];
for (at, next, cfg_type, len, region_off) in chain {
assert_eq!(cfg8(&pci, at + CAP_OFF_VNDR), CAP_VNDR, "cap@{at:#x} vndr");
assert_eq!(cfg8(&pci, at + CAP_OFF_NEXT), next, "cap@{at:#x} next");
assert_eq!(cfg8(&pci, at + CAP_OFF_LEN), len, "cap@{at:#x} len");
assert_eq!(
cfg8(&pci, at + CAP_OFF_CFG_TYPE),
cfg_type,
"cap@{at:#x} cfg_type"
);
assert_eq!(cfg8(&pci, at + CAP_OFF_BAR), 0, "cap@{at:#x} bar");
assert_eq!(
cfg32(&pci, at + CAP_OFF_OFFSET),
region_off as u32,
"cap@{at:#x} offset"
);
assert_eq!(
cfg32(&pci, at + CAP_OFF_LENGTH),
REGION_SIZE as u32,
"cap@{at:#x} length"
);
}
assert_eq!(
cfg32(&pci, CAP_NOTIFY + CAP_OFF_NOTIFY_MULT),
NOTIFY_OFF_MULTIPLIER
);
assert_eq!(cfg8(&pci, CAP_MSIX), PCI_CAP_ID_MSIX, "msix cap id");
assert_eq!(cfg8(&pci, CAP_MSIX + 1), 0, "msix cap_next (last cap)");
assert_eq!(
cfg16(&pci, CAP_MSIX + MSIX_OFF_MSG_CTRL) & 0x07FF,
SINGLE_PAIR_VECTORS as u16 - 1,
"msix table size (N-1)"
);
assert_eq!(
cfg32(&pci, CAP_MSIX + MSIX_OFF_TABLE),
MSIX_TABLE_OFFSET as u32 | MSIX_BIR0,
"msix table offset|BIR"
);
assert_eq!(
cfg32(&pci, CAP_MSIX + MSIX_OFF_PBA),
MSIX_PBA_OFFSET as u32 | MSIX_BIR0,
"msix PBA offset|BIR"
);
}
#[test]
fn msix_vector_echo_readback() {
let mem = test_mem();
let (mut pci, _c) = build(&mem);
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, TXQ as u32);
cc_w(&mut pci, CC_QUEUE_MSIX_VECTOR, 1);
assert_eq!(cc_r(&mut pci, CC_QUEUE_MSIX_VECTOR) as u16, 1);
cc_w(&mut pci, CC_QUEUE_SELECT, RXQ as u32);
assert_eq!(cc_r(&mut pci, CC_QUEUE_MSIX_VECTOR) as u16, NO_VECTOR);
cc_w(&mut pci, CC_QUEUE_MSIX_VECTOR, 0);
assert_eq!(cc_r(&mut pci, CC_QUEUE_MSIX_VECTOR) as u16, 0);
cc_w(&mut pci, CC_QUEUE_SELECT, TXQ as u32);
assert_eq!(cc_r(&mut pci, CC_QUEUE_MSIX_VECTOR) as u16, 1);
}
#[test]
fn msix_vector_out_of_range_rejected() {
let mem = test_mem();
let (mut pci, _c) = build(&mem);
cc_w(&mut pci, CC_MSIX_CONFIG, SINGLE_PAIR_VECTORS as u32);
assert_eq!(cc_r(&mut pci, CC_MSIX_CONFIG) as u16, NO_VECTOR);
cc_w(&mut pci, CC_QUEUE_SELECT, TXQ as u32);
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 mem = test_mem();
let (mut pci, _c) = build(&mem);
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 + SINGLE_PAIR_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 mem = test_mem();
let (mut pci, _c) = build(&mem);
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"
);
}
#[allow(clippy::type_complexity)]
fn build_msix(
mem: &GuestMemoryMmap,
) -> (
VirtioNetPci,
Arc<super::super::VirtioNetCounters>,
Arc<PiMutex<MsixState>>,
[EventFd; SINGLE_PAIR_VECTORS],
Arc<MockRouteSink>,
) {
let mut net = VirtioNet::new(NetConfig::default());
net.set_mem(mem.clone());
let counters = net.counters();
let msix = Arc::new(PiMutex::new(MsixState::new(NUM_QUEUES, MSIX_TABLE_MAX)));
let evts: [EventFd; SINGLE_PAIR_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 = VirtioNetPci::new(
net,
TEST_BAR_APERTURE,
Arc::clone(&msix),
Some(Arc::clone(&sink) as Arc<dyn MsixRouteSink>),
test_gsis(),
);
(pci, counters, msix, evts, sink)
}
const MC_ENABLE: u16 = 0x8000;
const MC_ENABLE_MASKALL: u16 = 0x8000 | 0x4000;
fn program_msix_entries(pci: &mut VirtioNetPci, n: usize) {
for v in 0..n {
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()); }
}
fn enable_msix_each(pci: &mut VirtioNetPci) {
cc_w(pci, CC_MSIX_CONFIG, 0);
cc_w(pci, CC_QUEUE_SELECT, RXQ as u32);
cc_w(pci, CC_QUEUE_MSIX_VECTOR, 1);
cc_w(pci, CC_QUEUE_SELECT, TXQ as u32);
cc_w(pci, CC_QUEUE_MSIX_VECTOR, 2);
pci.config_write(
CAP_MSIX + MSIX_OFF_MSG_CTRL,
&MC_ENABLE_MASKALL.to_le_bytes(),
);
program_msix_entries(pci, SINGLE_PAIR_VECTORS);
pci.config_write(CAP_MSIX + MSIX_OFF_MSG_CTRL, &MC_ENABLE.to_le_bytes());
}
fn enable_msix_shared(pci: &mut VirtioNetPci) {
cc_w(pci, CC_MSIX_CONFIG, 0);
for q in [RXQ, TXQ] {
cc_w(pci, CC_QUEUE_SELECT, q as u32);
cc_w(pci, CC_QUEUE_MSIX_VECTOR, 1);
}
pci.config_write(
CAP_MSIX + MSIX_OFF_MSG_CTRL,
&MC_ENABLE_MASKALL.to_le_bytes(),
);
program_msix_entries(pci, 2);
pci.config_write(CAP_MSIX + MSIX_OFF_MSG_CTRL, &MC_ENABLE.to_le_bytes());
}
fn kick_tx_loopback(pci: &mut VirtioNetPci, mem: &GuestMemoryMmap, payload: &[u8]) {
let zero_hdr = [0u8; VIRTIO_NET_HDR_LEN];
mem.write_slice(&zero_hdr, GuestAddress(TX_BUF)).unwrap();
mem.write_slice(payload, GuestAddress(TX_BUF + VIRTIO_NET_HDR_LEN as u64))
.unwrap();
write_desc(
mem,
TX_DESC,
0,
TX_BUF,
(VIRTIO_NET_HDR_LEN + payload.len()) as u32,
0,
0,
);
publish_avail(mem, TX_AVAIL, 0);
write_desc(mem, RX_DESC, 0, RX_BUF, 256, 2, 0); publish_avail(mem, RX_AVAIL, 0);
pci.bar_write(
NOTIFY_OFFSET + (TXQ as u64) * NOTIFY_OFF_MULTIPLIER as u64,
&[0u8; 2],
);
}
#[test]
fn msix_enabled_delivers_per_queue_vectors() {
let mem = test_mem();
let (mut pci, counters, msix, evts, sink) = build_msix(&mem);
drive_to_features_ok(&mut pci);
program_queue(&mut pci, RXQ as u32, RX_DESC, RX_AVAIL, RX_USED);
program_queue(&mut pci, TXQ as u32, TX_DESC, TX_AVAIL, TX_USED);
enable_msix_each(&mut pci);
cc_w(&mut pci, CC_DEVICE_STATUS, S_OK);
kick_tx_loopback(&mut pci, &mem, &(0..42u8).collect::<Vec<_>>());
assert_eq!(counters.tx_packets(), 1, "TX drained");
assert_eq!(counters.rx_packets(), 1, "RX loopback delivered");
assert_eq!(evts[1].read().unwrap(), 1, "RX queue vector 1 fired once");
assert_eq!(evts[2].read().unwrap(), 1, "TX queue vector 2 fired once");
assert!(
evts[0].read().is_err(),
"config vector 0 untouched (EAGAIN)"
);
let mut isr = [0xFFu8; 1];
pci.bar_read(ISR_OFFSET, &mut isr);
assert_eq!(isr[0], 0, "MSI-X mode leaves the INTx ISR clear");
let installs = sink.installs.lock().unwrap();
for gsi in [24u32, 25, 26] {
assert!(
installs.iter().any(|(g, m)| *g == gsi && m.is_some()),
"vector route installed at GSI {gsi}"
);
}
assert_eq!(
msix.lock().pba_byte(0),
0,
"delivered live, nothing pending"
);
}
#[test]
fn msix_shared_fallback_coalesces_queues() {
let mem = test_mem();
let (mut pci, counters, msix, evts, _sink) = build_msix(&mem);
drive_to_features_ok(&mut pci);
program_queue(&mut pci, RXQ as u32, RX_DESC, RX_AVAIL, RX_USED);
program_queue(&mut pci, TXQ as u32, TX_DESC, TX_AVAIL, TX_USED);
enable_msix_shared(&mut pci);
cc_w(&mut pci, CC_DEVICE_STATUS, S_OK);
kick_tx_loopback(&mut pci, &mem, &[0x11u8; 16]);
assert_eq!(counters.tx_packets(), 1, "TX drained");
assert_eq!(counters.rx_packets(), 1, "RX loopback delivered");
assert_eq!(
evts[1].read().unwrap(),
2,
"shared queue vector fired per queue"
);
assert!(
evts[0].read().is_err(),
"config vector 0 untouched (EAGAIN)"
);
assert_eq!(
msix.lock().pba_byte(0),
0,
"delivered live, nothing pending"
);
}
#[test]
fn msix_masked_vector_pends_then_replays_on_unmask() {
let mem = test_mem();
let (mut pci, _counters, msix, evts, _sink) = build_msix(&mem);
drive_to_features_ok(&mut pci);
program_queue(&mut pci, RXQ as u32, RX_DESC, RX_AVAIL, RX_USED);
program_queue(&mut pci, TXQ as u32, TX_DESC, TX_AVAIL, TX_USED);
enable_msix_each(&mut pci);
cc_w(&mut pci, CC_DEVICE_STATUS, S_OK);
pci.bar_write(
MSIX_TABLE_OFFSET + MSIX_ENTRY_SIZE + 12,
&1u32.to_le_bytes(),
);
kick_tx_loopback(&mut pci, &mem, &[0x5Au8; 20]);
assert!(
evts[1].read().is_err(),
"masked RX vector did not fire (EAGAIN)"
);
assert_eq!(
msix.lock().pba_byte(0) & (1 << 1),
1 << 1,
"pending bit set for the masked RX queue vector"
);
assert_eq!(evts[2].read().unwrap(), 1, "TX vector delivered live");
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_function_mask_clear_replays_pending() {
let mem = test_mem();
let (mut pci, _counters, msix, evts, _sink) = build_msix(&mem);
drive_to_features_ok(&mut pci);
program_queue(&mut pci, RXQ as u32, RX_DESC, RX_AVAIL, RX_USED);
program_queue(&mut pci, TXQ as u32, TX_DESC, TX_AVAIL, TX_USED);
enable_msix_each(&mut pci);
cc_w(&mut pci, CC_DEVICE_STATUS, S_OK);
pci.config_write(
CAP_MSIX + MSIX_OFF_MSG_CTRL,
&MC_ENABLE_MASKALL.to_le_bytes(),
);
kick_tx_loopback(&mut pci, &mem, &[0x33u8; 24]);
assert!(
evts[1].read().is_err(),
"RX vector masked by MASKALL (EAGAIN)"
);
assert!(
evts[2].read().is_err(),
"TX vector masked by MASKALL (EAGAIN)"
);
assert_eq!(
msix.lock().pba_byte(0) & ((1 << 1) | (1 << 2)),
(1 << 1) | (1 << 2),
"both queue vectors pending under MASKALL"
);
pci.config_write(CAP_MSIX + MSIX_OFF_MSG_CTRL, &MC_ENABLE.to_le_bytes());
assert_eq!(
evts[1].read().unwrap(),
1,
"RX vector replayed on MASKALL clear"
);
assert_eq!(
evts[2].read().unwrap(),
1,
"TX vector replayed on MASKALL clear"
);
assert_eq!(
msix.lock().pba_byte(0) & ((1 << 1) | (1 << 2)),
0,
"pending bits cleared after replay"
);
}
#[test]
fn msix_cap_absent_without_route_sink() {
let mem = test_mem();
let mut net = VirtioNet::new(NetConfig::default());
net.set_mem(mem.clone());
let msix = Arc::new(PiMutex::new(MsixState::new(NUM_QUEUES, MSIX_TABLE_MAX)));
let pci = VirtioNetPci::new(
net,
TEST_BAR_APERTURE,
msix,
None,
vec![0; SINGLE_PAIR_VECTORS],
);
assert_eq!(
cfg8(&pci, CAP_NOTIFY + CAP_OFF_NEXT),
0,
"NOTIFY terminates the chain when MSI-X is not offered"
);
assert_eq!(
cfg8(&pci, CAP_MSIX),
0,
"no MSI-X cap written without a route sink"
);
assert_ne!(cfg16(&pci, REG_STATUS) & PCI_STATUS_CAP_LIST, 0);
}
#[test]
fn msix_config_vector_delivers_on_queue_poison() {
let mem = test_mem();
let (mut pci, _counters, _msix, evts, _sink) = build_msix(&mem);
drive_to_features_ok(&mut pci);
program_queue(&mut pci, RXQ as u32, RX_DESC, RX_AVAIL, RX_USED);
program_queue(&mut pci, TXQ as u32, TX_DESC, TX_AVAIL, TX_USED);
enable_msix_each(&mut pci);
cc_w(&mut pci, CC_DEVICE_STATUS, S_OK);
mem.write_slice(&1000u16.to_le_bytes(), GuestAddress(TX_AVAIL + 2))
.unwrap();
pci.bar_write(
NOTIFY_OFFSET + (TXQ as u64) * NOTIFY_OFF_MULTIPLIER as u64,
&[0u8; 2],
);
assert_eq!(
evts[0].read().unwrap(),
1,
"poison delivered to config vector 0"
);
assert!(
evts[1].read().is_err(),
"RX queue vector 1 not fired by a config-change/poison"
);
assert!(
evts[2].read().is_err(),
"TX queue vector 2 not fired by a config-change/poison"
);
let mut sb = [0u8; 1];
pci.bar_read(COMMON_OFFSET + CC_DEVICE_STATUS, &mut sb);
assert_ne!(
sb[0] as u32 & VIRTIO_CONFIG_S_NEEDS_RESET,
0,
"poison sets NEEDS_RESET"
);
let mut isr = [0xFFu8; 1];
pci.bar_read(ISR_OFFSET, &mut isr);
assert_eq!(isr[0], 0, "MSI-X mode leaves the INTx ISR clear on poison");
}
#[test]
fn reset_clears_msix_vector_assignments() {
let mem = test_mem();
let (mut pci, _counters, msix, _evts, _sink) = build_msix(&mem);
drive_to_features_ok(&mut pci);
enable_msix_each(&mut pci);
assert_eq!(msix.lock().config_vector(), 0, "config vector assigned");
assert_eq!(msix.lock().queue_vector(RXQ), 1, "RX queue vector assigned");
assert_eq!(msix.lock().queue_vector(TXQ), 2, "TX queue vector assigned");
cc_w(&mut pci, CC_DEVICE_STATUS, 0);
assert_eq!(
msix.lock().config_vector(),
NO_VECTOR,
"reset restores config vector to NO_VECTOR"
);
assert_eq!(
msix.lock().queue_vector(RXQ),
NO_VECTOR,
"reset restores RX vector"
);
assert_eq!(
msix.lock().queue_vector(TXQ),
NO_VECTOR,
"reset restores TX vector"
);
}
#[test]
fn msix_routes_install_on_function_unmask_edge() {
let mem = test_mem();
let (mut pci, _counters, _msix, _evts, sink) = build_msix(&mem);
drive_to_features_ok(&mut pci);
program_queue(&mut pci, RXQ as u32, RX_DESC, RX_AVAIL, RX_USED);
program_queue(&mut pci, TXQ as u32, TX_DESC, TX_AVAIL, TX_USED);
cc_w(&mut pci, CC_MSIX_CONFIG, 0);
cc_w(&mut pci, CC_QUEUE_SELECT, RXQ as u32);
cc_w(&mut pci, CC_QUEUE_MSIX_VECTOR, 1);
cc_w(&mut pci, CC_QUEUE_SELECT, TXQ as u32);
cc_w(&mut pci, CC_QUEUE_MSIX_VECTOR, 2);
pci.config_write(
CAP_MSIX + MSIX_OFF_MSG_CTRL,
&MC_ENABLE_MASKALL.to_le_bytes(),
);
program_msix_entries(&mut pci, SINGLE_PAIR_VECTORS);
assert!(
!sink
.installs
.lock()
.unwrap()
.iter()
.any(|(_, m)| m.is_some()),
"no route installed while function-masked (MASKALL set)"
);
pci.config_write(CAP_MSIX + MSIX_OFF_MSG_CTRL, &MC_ENABLE.to_le_bytes());
let installs = sink.installs.lock().unwrap();
for gsi in [24u32, 25, 26] {
assert!(
installs.iter().any(|(g, m)| *g == gsi && m.is_some()),
"vector route installed at GSI {gsi} on MASKALL-clear"
);
}
}
#[test]
fn msix_mask_edge_removes_route() {
let mem = test_mem();
let (mut pci, _counters, _msix, _evts, sink) = build_msix(&mem);
drive_to_features_ok(&mut pci);
program_queue(&mut pci, RXQ as u32, RX_DESC, RX_AVAIL, RX_USED);
program_queue(&mut pci, TXQ as u32, TX_DESC, TX_AVAIL, TX_USED);
enable_msix_each(&mut pci);
assert!(
sink.installs
.lock()
.unwrap()
.iter()
.any(|(g, m)| *g == 25 && m.is_some()),
"RX queue vector route installed after enable"
);
pci.bar_write(
MSIX_TABLE_OFFSET + MSIX_ENTRY_SIZE + 12,
&1u32.to_le_bytes(),
);
assert!(
sink.installs
.lock()
.unwrap()
.iter()
.any(|(g, m)| *g == 25 && m.is_none()),
"mask edge removes the queue vector's route via set_route(gsi, None)"
);
}
#[test]
fn bar_window_gated_on_memory_enable_and_nonzero_base() {
let mem = test_mem();
let (mut pci, _c) = build(&mem);
assert_eq!(pci.bar_window(), None, "no window before BAR program");
pci.config_write(REG_BAR0, &0xe010_0000u32.to_le_bytes());
assert_eq!(
pci.bar_window(),
None,
"no window until memory-space enabled"
);
let mut pci0 = build(&mem).0;
pci0.config_write(REG_COMMAND, &PCI_COMMAND_MEMORY.to_le_bytes());
assert_eq!(pci0.bar_window(), None, "no window with zero base");
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, &0xffff_ffffu32.to_le_bytes());
let probed = cfg32(&pci, REG_BAR0);
assert_eq!(probed & !(BAR0_SIZE as u32 - 1), !(BAR0_SIZE as u32 - 1));
assert_eq!(probed & 0xF, BAR0_TYPE_BITS, "type bits read-only");
}
#[test]
fn bar_window_rejects_base_outside_crs_grant() {
let mem = test_mem();
let (mut pci, _c) = build(&mem);
pci.config_write(REG_COMMAND, &PCI_COMMAND_MEMORY.to_le_bytes());
pci.config_write(REG_BAR0, &TEST_ECAM_BASE.to_le_bytes());
assert_eq!(
pci.bar_window(),
None,
"BAR base below the _CRS grant (over ECAM) must claim no window"
);
let past_end = (TEST_BAR_APERTURE.1 as u32) & !(BAR0_SIZE as u32 - 1);
pci.config_write(REG_BAR0, &past_end.to_le_bytes());
assert_eq!(
pci.bar_window(),
None,
"BAR window extending past the grant end must claim no window"
);
pci.config_write(REG_BAR0, &TEST_BAR_BASE.to_le_bytes());
assert_eq!(pci.bar_window(), Some((TEST_BAR_BASE as u64, BAR0_SIZE)));
}
#[test]
fn bar_window_cache_refreshes_on_command_and_bar0_writes() {
let mem = test_mem();
let (mut pci, _c) = build(&mem);
assert_eq!(pci.bar_window(), None, "reset: no cached window");
pci.config_write(REG_BAR0, &TEST_BAR_BASE.to_le_bytes());
assert_eq!(
pci.bar_window(),
None,
"BAR0 write refreshes the cache; memory decode off → still None"
);
pci.config_write(REG_COMMAND, &PCI_COMMAND_MEMORY.to_le_bytes());
assert_eq!(
pci.bar_window(),
Some((TEST_BAR_BASE as u64, BAR0_SIZE)),
"COMMAND.MEMORY write refreshes the cache → window appears"
);
pci.config_write(REG_BAR0, &TEST_ECAM_BASE.to_le_bytes());
assert_eq!(
pci.bar_window(),
None,
"BAR0 reprogram (out-of-grant) refreshes the cache → None, not stale"
);
pci.config_write(REG_BAR0, &TEST_BAR_BASE.to_le_bytes());
assert_eq!(
pci.bar_window(),
Some((TEST_BAR_BASE as u64, BAR0_SIZE)),
"BAR0 reprogram (in-grant) refreshes the cache → window restored"
);
pci.config_write(REG_COMMAND, &0u16.to_le_bytes());
assert_eq!(
pci.bar_window(),
None,
"clearing COMMAND.MEMORY refreshes the cache → no window"
);
}
#[test]
fn common_cfg_register_reads() {
let mem = test_mem();
let (mut pci, _c) = build(&mem);
assert_eq!(cc_r(&mut pci, CC_NUM_QUEUES), NUM_QUEUES as u32);
assert_eq!(cc_r(&mut pci, CC_MSIX_CONFIG) as u16, NO_VECTOR);
assert_eq!(cc_r(&mut pci, CC_QUEUE_MSIX_VECTOR) as u16, NO_VECTOR);
cc_w(&mut pci, CC_DEVICE_FEATURE_SELECT, 0);
assert_eq!(cc_r(&mut pci, CC_DEVICE_FEATURE_SELECT), 0);
assert_ne!(
cc_r(&mut pci, CC_DEVICE_FEATURE) & (1 << VIRTIO_NET_F_MAC),
0
);
cc_w(&mut pci, CC_DEVICE_FEATURE_SELECT, 1);
assert_ne!(
cc_r(&mut pci, CC_DEVICE_FEATURE) & (1 << (VIRTIO_F_VERSION_1 - 32)),
0
);
cc_w(&mut pci, CC_DEVICE_FEATURE_SELECT, 2);
assert_eq!(cc_r(&mut pci, CC_DEVICE_FEATURE), 0, "sel=2 window reads 0");
cc_w(&mut pci, CC_DEVICE_FEATURE_SELECT, 0xFFFF_FFFF);
assert_eq!(cc_r(&mut pci, CC_DEVICE_FEATURE), 0, "sel=u32::MAX reads 0");
cc_w(&mut pci, CC_DEVICE_FEATURE_SELECT, 0);
let offered = cc_r(&mut pci, CC_DEVICE_FEATURE);
cc_w(&mut pci, CC_DEVICE_FEATURE, 0xDEAD_BEEF);
assert_eq!(
cc_r(&mut pci, CC_DEVICE_FEATURE),
offered,
"device_feature is read-only; the write must be dropped"
);
assert_eq!(
cc_r(&mut pci, CC_DRIVER_FEATURE),
0,
"driver_feature reads 0"
);
assert_eq!(cc_r(&mut pci, CC_QUEUE_DESC_LO), 0, "queue_desc_lo reads 0");
assert_eq!(cc_r(&mut pci, CC_QUEUE_DESC_HI), 0, "queue_desc_hi reads 0");
cc_w(&mut pci, CC_QUEUE_SELECT, TXQ as u32);
assert_eq!(cc_r(&mut pci, CC_QUEUE_SELECT), TXQ as u32);
assert_eq!(cc_r(&mut pci, CC_QUEUE_NOTIFY_OFF), TXQ as u32);
cc_w(&mut pci, CC_QUEUE_SELECT, NUM_QUEUES as u32);
assert_eq!(
cc_r(&mut pci, CC_QUEUE_SIZE),
0,
"OOR selector: queue_size 0"
);
assert_eq!(
cc_r(&mut pci, CC_QUEUE_ENABLE),
0,
"OOR selector: queue_enable 0"
);
assert_eq!(
cc_r(&mut pci, CC_QUEUE_NOTIFY_OFF),
0,
"OOR selector: notify_off 0"
);
}
#[test]
fn queue_size_reads_back_programmed_not_max() {
let mem = test_mem();
let (mut pci, _c) = build(&mem);
drive_to_features_ok(&mut pci);
cc_w(&mut pci, CC_QUEUE_SELECT, TXQ as u32);
assert_eq!(
cc_r(&mut pci, CC_QUEUE_SIZE),
QUEUE_MAX_SIZE as u32,
"resets to max before the guest writes",
);
cc_w(&mut pci, CC_QUEUE_SIZE, 8);
assert_eq!(
cc_r(&mut pci, CC_QUEUE_SIZE),
8,
"read-back returns the programmed size, not the max",
);
}
#[test]
fn common_cfg_ring_addresses_merge_low_and_high_dwords() {
let mem = test_mem();
let mut net = VirtioNet::new(NetConfig::default());
net.set_mem(mem.clone());
let mut pci = new_pci(net);
drive_to_features_ok(&mut pci); cc_w(&mut pci, CC_QUEUE_SELECT, TXQ as u32);
cc_w(&mut pci, CC_QUEUE_DESC_LO, 0x0000_1000);
cc_w(&mut pci, CC_QUEUE_DESC_HI, 0x0000_ABCD);
cc_w(&mut pci, CC_QUEUE_AVAIL_LO, 0x0000_2000);
cc_w(&mut pci, CC_QUEUE_AVAIL_HI, 0x0000_0001);
cc_w(&mut pci, CC_QUEUE_USED_LO, 0x0000_3000);
cc_w(&mut pci, CC_QUEUE_USED_HI, 0x0000_0002);
let (desc, avail, used) = pci.net.selected_queue_ring_addrs().unwrap();
assert_eq!(
desc, 0x0000_ABCD_0000_1000,
"DESC high dword in high 32 bits"
);
assert_eq!(
avail, 0x0000_0001_0000_2000,
"AVAIL high dword in high 32 bits"
);
assert_eq!(
used, 0x0000_0002_0000_3000,
"USED high dword in high 32 bits"
);
}
#[test]
fn status_fsm_and_natural_width_reads() {
let mem = test_mem();
let (mut pci, _c) = build(&mem);
drive_to_features_ok(&mut pci);
let mut sb = [0u8; 1];
pci.bar_read(COMMON_OFFSET + CC_DEVICE_STATUS, &mut sb);
assert_eq!(sb[0] as u32, S_FEAT & 0xFF);
let mut generation = [0u8; 1];
pci.bar_read(COMMON_OFFSET + CC_CONFIG_GENERATION, &mut generation);
assert_eq!(
generation[0], 0,
"config_generation reads 0 (no config change)"
);
}
#[test]
fn common_cfg_straddling_read_serves_base_register_zero_filling_overhang() {
let mem = test_mem();
let (mut pci, _c) = build(&mem);
drive_to_features_ok(&mut pci);
let mut wide = [0xAAu8; 4];
pci.bar_read(COMMON_OFFSET + CC_NUM_QUEUES, &mut wide);
assert_eq!(
u32::from_le_bytes(wide),
NUM_QUEUES as u32,
"straddling read must serve only num_queues and zero-fill the \
overhang; device_status @0x14 must not be served into byte 2"
);
}
#[test]
fn device_config_region_serves_mac_and_drops_writes() {
let mac = [0x52, 0x54, 0x00, 0xAB, 0xCD, 0xEF];
let mem = test_mem();
let mut net = VirtioNet::new(NetConfig::default().mac(mac));
net.set_mem(mem.clone());
let mut pci = new_pci(net);
let mut got = [0u8; 6];
pci.bar_read(DEVICE_OFFSET, &mut got);
assert_eq!(got, mac, "device-config region serves the MAC at offset 0");
pci.bar_write(DEVICE_OFFSET, &[0xFF; 6]);
let mut after = [0u8; 6];
pci.bar_read(DEVICE_OFFSET, &mut after);
assert_eq!(after, mac, "device-config writes must be dropped");
let mut nb = [0xAAu8; 4];
pci.bar_read(NOTIFY_OFFSET, &mut nb);
assert_eq!(nb, [0u8; 4], "notify region reads return 0");
}
#[test]
fn loopback_through_facade_sets_isr_and_read_clears() {
let mem = test_mem();
let (mut pci, counters) = build(&mem);
drive_to_features_ok(&mut pci);
program_queue(&mut pci, RXQ as u32, RX_DESC, RX_AVAIL, RX_USED);
program_queue(&mut pci, TXQ as u32, TX_DESC, TX_AVAIL, TX_USED);
cc_w(&mut pci, CC_DEVICE_STATUS, S_OK);
let payload: Vec<u8> = (0..42u8).collect();
let zero_hdr = [0u8; VIRTIO_NET_HDR_LEN];
mem.write_slice(&zero_hdr, GuestAddress(TX_BUF)).unwrap();
mem.write_slice(&payload, GuestAddress(TX_BUF + VIRTIO_NET_HDR_LEN as u64))
.unwrap();
write_desc(
&mem,
TX_DESC,
0,
TX_BUF,
(VIRTIO_NET_HDR_LEN + payload.len()) as u32,
0,
0,
);
publish_avail(&mem, TX_AVAIL, 0);
write_desc(&mem, RX_DESC, 0, RX_BUF, 256, 2, 0); publish_avail(&mem, RX_AVAIL, 0);
pci.bar_write(
NOTIFY_OFFSET + (TXQ as u64) * NOTIFY_OFF_MULTIPLIER as u64,
&[0u8; 2],
);
assert_eq!(counters.tx_packets(), 1, "TX drained through the facade");
assert_eq!(counters.rx_packets(), 1, "RX loopback delivered");
let mut delivered = vec![0u8; VIRTIO_NET_HDR_LEN + payload.len()];
mem.read_slice(&mut delivered, GuestAddress(RX_BUF))
.unwrap();
assert_eq!(&delivered[VIRTIO_NET_HDR_LEN..], payload.as_slice());
let mut isr = [0u8; 1];
pci.bar_read(ISR_OFFSET, &mut isr);
assert_eq!(isr[0] & 0x1, 0x1, "ISR queue-interrupt bit set after drain");
let mut isr2 = [0xFFu8; 1];
pci.bar_read(ISR_OFFSET, &mut isr2);
assert_eq!(isr2[0], 0, "ISR read-to-clear: second read is 0");
}
#[test]
fn isr_non_base_in_region_read_does_not_clear() {
let mem = test_mem();
let (mut pci, counters) = build(&mem);
drive_to_features_ok(&mut pci);
program_queue(&mut pci, RXQ as u32, RX_DESC, RX_AVAIL, RX_USED);
program_queue(&mut pci, TXQ as u32, TX_DESC, TX_AVAIL, TX_USED);
cc_w(&mut pci, CC_DEVICE_STATUS, S_OK);
let zero_hdr = [0u8; VIRTIO_NET_HDR_LEN];
mem.write_slice(&zero_hdr, GuestAddress(TX_BUF)).unwrap();
write_desc(&mem, TX_DESC, 0, TX_BUF, VIRTIO_NET_HDR_LEN as u32, 0, 0);
publish_avail(&mem, TX_AVAIL, 0);
write_desc(&mem, RX_DESC, 0, RX_BUF, 256, 2, 0); publish_avail(&mem, RX_AVAIL, 0);
pci.bar_write(
NOTIFY_OFFSET + (TXQ as u64) * NOTIFY_OFF_MULTIPLIER as u64,
&[0u8; 2],
);
assert!(
counters.tx_packets() >= 1,
"TX drained, ISR should be pending"
);
let mut stray = [0xFFu8; 1];
pci.bar_read(ISR_OFFSET + 4, &mut stray);
assert_eq!(stray[0], 0, "non-base ISR-region read returns 0");
let mut isr = [0u8; 1];
pci.bar_read(ISR_OFFSET, &mut isr);
assert_eq!(
isr[0] & 0x1,
0x1,
"stray ISR-region read must NOT have cleared the pending bit"
);
}
#[test]
fn two_consecutive_kicks_each_set_and_clear_isr() {
let mem = test_mem();
let (mut pci, counters) = build(&mem);
drive_to_features_ok(&mut pci);
program_queue(&mut pci, RXQ as u32, RX_DESC, RX_AVAIL, RX_USED);
program_queue(&mut pci, TXQ as u32, TX_DESC, TX_AVAIL, TX_USED);
cc_w(&mut pci, CC_DEVICE_STATUS, S_OK);
let hdr = [0u8; VIRTIO_NET_HDR_LEN];
let payload = [0x5Au8; 20];
let total = (VIRTIO_NET_HDR_LEN + payload.len()) as u32;
for round in 0u16..2 {
let tx_buf = TX_BUF + (round as u64) * 0x200;
let rx_buf = RX_BUF + (round as u64) * 0x400;
mem.write_slice(&hdr, GuestAddress(tx_buf)).unwrap();
mem.write_slice(&payload, GuestAddress(tx_buf + VIRTIO_NET_HDR_LEN as u64))
.unwrap();
write_desc(&mem, TX_DESC, round, tx_buf, total, 0, 0);
write_desc(&mem, RX_DESC, round, rx_buf, 256, 2, 0);
for avail in [TX_AVAIL, RX_AVAIL] {
mem.write_slice(
&round.to_le_bytes(),
GuestAddress(avail + 4 + (round as u64) * 2),
)
.unwrap();
mem.write_slice(&(round + 1).to_le_bytes(), GuestAddress(avail + 2))
.unwrap();
}
pci.bar_write(
NOTIFY_OFFSET + (TXQ as u64) * NOTIFY_OFF_MULTIPLIER as u64,
&[0u8; 2],
);
let mut isr = [0u8; 1];
pci.bar_read(ISR_OFFSET, &mut isr);
assert_eq!(
isr[0] & 0x1,
0x1,
"round {round}: VRING ISR bit set after kick"
);
let mut isr_again = [0xFFu8; 1];
pci.bar_read(ISR_OFFSET, &mut isr_again);
assert_eq!(isr_again[0], 0, "round {round}: ISR read-to-clear");
}
assert_eq!(
counters.tx_packets(),
2,
"both kicks each drained a TX packet"
);
assert_eq!(
counters.rx_packets(),
2,
"both loopbacks each delivered to RX"
);
}
#[test]
fn notify_out_of_range_index_is_dropped() {
let mem = test_mem();
let (mut pci, counters) = build(&mem);
drive_to_features_ok(&mut pci);
program_queue(&mut pci, RXQ as u32, RX_DESC, RX_AVAIL, RX_USED);
program_queue(&mut pci, TXQ as u32, TX_DESC, TX_AVAIL, TX_USED);
cc_w(&mut pci, CC_DEVICE_STATUS, S_OK);
pci.bar_write(NOTIFY_OFFSET + REGION_SIZE - 4, &[0u8; 2]);
assert_eq!(
counters.tx_packets(),
0,
"out-of-range notify drains nothing"
);
}
fn read_used_idx(mem: &GuestMemoryMmap, used_base: u64) -> u16 {
let mut b = [0u8; 2];
mem.read_slice(&mut b, GuestAddress(used_base + 2)).unwrap();
u16::from_le_bytes(b)
}
fn progress_sum(c: &super::super::VirtioNetCounters) -> u64 {
c.tx_packets.load(Ordering::Relaxed)
+ c.rx_packets.load(Ordering::Relaxed)
+ c.tx_chain_invalid.load(Ordering::Relaxed)
+ c.tx_oversize_dropped.load(Ordering::Relaxed)
+ c.rx_chain_invalid.load(Ordering::Relaxed)
+ c.rx_write_failed.load(Ordering::Relaxed)
+ c.tx_dropped_no_rx_buffer.load(Ordering::Relaxed)
+ c.tx_dropped_rx_poisoned.load(Ordering::Relaxed)
+ c.tx_add_used_failures.load(Ordering::Relaxed)
+ c.rx_add_used_failures.load(Ordering::Relaxed)
+ c.invalid_avail_idx_count.load(Ordering::Relaxed)
}
proptest! {
#![proptest_config(ProptestConfig { cases: 256, max_shrink_iters: 1024, ..ProptestConfig::default() })]
#[test]
fn facade_tx_chain_progress_under_random_descriptors(
descs in prop::collection::vec(
(0u64..(1u64 << 24), 0u32..(8 * 1024 * 1024), 0u16..8, any::<u16>()),
1..=8usize,
),
) {
let mem = test_mem();
let mut net = VirtioNet::new(NetConfig::default());
net.set_mem(mem.clone());
let counters = net.counters();
let mut pci = new_pci(net);
drive_to_features_ok(&mut pci);
program_queue(&mut pci, RXQ as u32, RX_DESC, RX_AVAIL, RX_USED);
program_queue(&mut pci, TXQ as u32, TX_DESC, TX_AVAIL, TX_USED);
cc_w(&mut pci, CC_DEVICE_STATUS, S_OK);
for (i, (addr, len, flags, next)) in descs.iter().enumerate() {
write_desc(&mem, TX_DESC, i as u16, *addr, *len, *flags, *next);
}
publish_avail(&mem, TX_AVAIL, 0);
write_desc(&mem, RX_DESC, 0, RX_BUF, 2048, 2, 0); publish_avail(&mem, RX_AVAIL, 0);
let before_tx = read_used_idx(&mem, TX_USED);
let before_rx = read_used_idx(&mem, RX_USED);
let before = progress_sum(&counters);
pci.bar_write(
NOTIFY_OFFSET + (TXQ as u64) * NOTIFY_OFF_MULTIPLIER as u64,
&[0u8; 2],
);
let after_tx = read_used_idx(&mem, TX_USED);
let after_rx = read_used_idx(&mem, RX_USED);
let after = progress_sum(&counters);
prop_assert!(after >= before, "event counters must be monotonic");
let progress = (after - before)
+ after_tx.wrapping_sub(before_tx) as u64
+ after_rx.wrapping_sub(before_rx) as u64;
prop_assert!(
progress >= 1,
"no visible progress through the PCI decode: tx_used_delta={} \
rx_used_delta={} counter_delta={} (chain len={})",
after_tx.wrapping_sub(before_tx),
after_rx.wrapping_sub(before_rx),
after - before,
descs.len(),
);
}
#[test]
fn facade_msix_tx_chain_progress_under_random_descriptors(
descs in prop::collection::vec(
(0u64..(1u64 << 24), 0u32..(8 * 1024 * 1024), 0u16..8, any::<u16>()),
1..=8usize,
),
) {
let mem = test_mem();
let (mut pci, counters, _msix, _evts, _sink) = build_msix(&mem);
drive_to_features_ok(&mut pci);
program_queue(&mut pci, RXQ as u32, RX_DESC, RX_AVAIL, RX_USED);
program_queue(&mut pci, TXQ as u32, TX_DESC, TX_AVAIL, TX_USED);
enable_msix_shared(&mut pci);
cc_w(&mut pci, CC_DEVICE_STATUS, S_OK);
for (i, (addr, len, flags, next)) in descs.iter().enumerate() {
write_desc(&mem, TX_DESC, i as u16, *addr, *len, *flags, *next);
}
publish_avail(&mem, TX_AVAIL, 0);
write_desc(&mem, RX_DESC, 0, RX_BUF, 2048, 2, 0); publish_avail(&mem, RX_AVAIL, 0);
let before_tx = read_used_idx(&mem, TX_USED);
let before_rx = read_used_idx(&mem, RX_USED);
let before = progress_sum(&counters);
pci.bar_write(
NOTIFY_OFFSET + (TXQ as u64) * NOTIFY_OFF_MULTIPLIER as u64,
&[0u8; 2],
);
let after_tx = read_used_idx(&mem, TX_USED);
let after_rx = read_used_idx(&mem, RX_USED);
let after = progress_sum(&counters);
prop_assert!(after >= before, "event counters must be monotonic");
let progress = (after - before)
+ after_tx.wrapping_sub(before_tx) as u64
+ after_rx.wrapping_sub(before_rx) as u64;
prop_assert!(
progress >= 1,
"no visible progress through the MSI-X PCI decode (chain len={})",
descs.len(),
);
}
}
}