use vmm_sys_util::eventfd::EventFd;
pub(crate) const MSIX_TABLE_MAX: usize = 256;
pub(crate) const MSIX_ENTRY_DWORDS: usize = 4;
const VECTOR_CTRL_DWORD: usize = 3;
const VECTOR_CTRL_MASK_BIT: u32 = 0x1;
pub(crate) const NO_VECTOR: u16 = 0xFFFF;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum IrqSource {
Vring { queue: usize },
Config,
}
pub(crate) trait MsixRouteSink: Send + Sync {
fn set_route(&self, gsi: u32, msg: Option<(u32, u32, u32)>);
}
pub(crate) struct MsixState {
eventfds: Vec<Option<EventFd>>,
table: Vec<[u32; MSIX_ENTRY_DWORDS]>,
pba: Vec<u8>,
enabled: bool,
function_mask: bool,
config_vector: u16,
queue_vectors: Vec<u16>,
}
impl MsixState {
pub(crate) fn new(num_queues: usize, max_vectors: usize) -> Self {
let num_vectors = (num_queues + 1).min(max_vectors);
let mut table = vec![[0u32; MSIX_ENTRY_DWORDS]; num_vectors];
for entry in &mut table {
entry[VECTOR_CTRL_DWORD] = VECTOR_CTRL_MASK_BIT;
}
MsixState {
eventfds: (0..num_vectors).map(|_| None).collect(),
table,
pba: vec![0u8; num_vectors.div_ceil(8)],
enabled: false,
function_mask: false,
config_vector: NO_VECTOR,
queue_vectors: vec![NO_VECTOR; num_queues],
}
}
pub(crate) fn num_vectors(&self) -> usize {
self.table.len()
}
pub(crate) fn enabled(&self) -> bool {
self.enabled
}
pub(crate) fn signal(&mut self, source: IrqSource) {
let vector = match source {
IrqSource::Config => self.config_vector,
IrqSource::Vring { queue } => {
self.queue_vectors.get(queue).copied().unwrap_or(NO_VECTOR)
}
};
self.fire(vector);
}
fn fire(&mut self, vector: u16) {
if vector == NO_VECTOR {
return;
}
let idx = vector as usize;
if idx >= self.table.len() {
return;
}
if !self.enabled {
return;
}
if self.function_mask || self.vector_masked(idx) {
self.pba[idx / 8] |= 1 << (idx % 8);
return;
}
if let Some(evt) = &self.eventfds[idx]
&& let Err(e) = evt.write(1)
{
tracing::warn!(%e, vector = idx, "virtio MSI-X eventfd write failed");
}
}
pub(crate) fn replay_pending(&mut self, idx: usize) -> bool {
if idx >= self.table.len() || self.pba[idx / 8] & (1 << (idx % 8)) == 0 {
return false;
}
if self.function_mask || self.vector_masked(idx) || !self.enabled {
return false;
}
if let Some(evt) = &self.eventfds[idx] {
if let Err(e) = evt.write(1) {
tracing::warn!(%e, vector = idx, "virtio MSI-X pending replay write failed");
return false;
}
self.pba[idx / 8] &= !(1 << (idx % 8));
return true;
}
false
}
fn vector_masked(&self, idx: usize) -> bool {
self.table
.get(idx)
.is_none_or(|e| e[VECTOR_CTRL_DWORD] & VECTOR_CTRL_MASK_BIT != 0)
}
pub(crate) fn set_message_control(&mut self, msg_ctrl: u16) {
self.enabled = msg_ctrl & 0x8000 != 0;
self.function_mask = msg_ctrl & 0x4000 != 0;
}
pub(crate) fn set_config_vector(&mut self, vector: u16) {
self.config_vector = vector;
}
pub(crate) fn config_vector(&self) -> u16 {
self.config_vector
}
pub(crate) fn set_queue_vector(&mut self, queue: usize, vector: u16) {
if let Some(slot) = self.queue_vectors.get_mut(queue) {
*slot = vector;
}
}
pub(crate) fn queue_vector(&self, queue: usize) -> u16 {
self.queue_vectors.get(queue).copied().unwrap_or(NO_VECTOR)
}
pub(crate) fn reset_virtio_assignments(&mut self) {
self.config_vector = NO_VECTOR;
self.queue_vectors.fill(NO_VECTOR);
}
pub(crate) fn table_dword(&self, entry: usize, dword: usize) -> u32 {
self.table
.get(entry)
.and_then(|e| e.get(dword))
.copied()
.unwrap_or(0)
}
pub(crate) fn write_table_dword(&mut self, entry: usize, dword: usize, val: u32) -> bool {
let Some(slot) = self.table.get_mut(entry).and_then(|e| e.get_mut(dword)) else {
return false;
};
if dword == VECTOR_CTRL_DWORD {
let was_masked = *slot & VECTOR_CTRL_MASK_BIT != 0;
*slot = val;
let now_masked = val & VECTOR_CTRL_MASK_BIT != 0;
was_masked && !now_masked
} else {
*slot = val;
false
}
}
pub(crate) fn vector_unmasked(&self, idx: usize) -> bool {
!self.function_mask && !self.vector_masked(idx)
}
pub(crate) fn msi_message(&self, idx: usize) -> Option<(u32, u32, u32)> {
self.table.get(idx).map(|e| (e[0], e[1], e[2]))
}
pub(crate) fn set_eventfd(&mut self, idx: usize, evt: EventFd) -> Option<EventFd> {
let slot = self.eventfds.get_mut(idx)?;
slot.replace(evt)
}
pub(crate) fn pba_byte(&self, byte_off: usize) -> u8 {
self.pba.get(byte_off).copied().unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::fd::AsRawFd;
const NUM_QUEUES: usize = 2;
const SINGLE_PAIR_VECTORS: usize = NUM_QUEUES + 1;
fn drain(evt: &EventFd) -> u64 {
evt.read().unwrap_or(0)
}
fn enabled_state() -> (MsixState, [EventFd; SINGLE_PAIR_VECTORS]) {
let mut s = 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, evt) in evts.iter().enumerate() {
s.set_eventfd(v, evt.try_clone().unwrap());
}
s.set_config_vector(0);
for q in 0..NUM_QUEUES {
s.set_queue_vector(q, (q + 1) as u16);
}
for e in 0..SINGLE_PAIR_VECTORS {
s.write_table_dword(e, 0, 0xFEE0_0000);
s.write_table_dword(e, 2, 0x4000 + e as u32);
assert!(s.write_table_dword(e, VECTOR_CTRL_DWORD, 0)); }
s.set_message_control(0x8000); (s, evts)
}
#[test]
fn reset_state_is_masked_and_disabled() {
let s = MsixState::new(NUM_QUEUES, MSIX_TABLE_MAX);
assert_eq!(
s.num_vectors(),
SINGLE_PAIR_VECTORS,
"config + one vector per virtqueue"
);
assert!(!s.enabled());
assert_eq!(s.config_vector(), NO_VECTOR);
for q in 0..NUM_QUEUES {
assert_eq!(s.queue_vector(q), NO_VECTOR);
}
for e in 0..SINGLE_PAIR_VECTORS {
assert_eq!(s.table_dword(e, VECTOR_CTRL_DWORD), VECTOR_CTRL_MASK_BIT);
assert!(!s.vector_unmasked(e));
}
assert_eq!(s.pba_byte(0), 0);
}
#[test]
fn num_vectors_is_num_queues_plus_one() {
let s = MsixState::new(8, MSIX_TABLE_MAX);
assert_eq!(s.num_vectors(), 9);
}
#[test]
fn vector_count_caps_at_max_vectors() {
let s = MsixState::new(1000, 8);
assert_eq!(
s.num_vectors(),
8,
"advertised vectors capped at max_vectors"
);
assert_eq!(s.queue_vector(999), NO_VECTOR, "every queue still tracked");
}
#[test]
fn per_queue_vectors_deliver_independently() {
let (mut s, evts) = enabled_state();
s.signal(IrqSource::Vring { queue: 0 });
assert_eq!(drain(&evts[1]), 1, "queue 0 fires its own vector 1");
assert_eq!(drain(&evts[0]), 0, "config vector untouched");
assert_eq!(drain(&evts[2]), 0, "queue 1's vector untouched");
s.signal(IrqSource::Vring { queue: 1 });
assert_eq!(drain(&evts[2]), 1, "queue 1 fires its own vector 2");
assert_eq!(drain(&evts[1]), 0, "queue 0's vector untouched");
s.signal(IrqSource::Config);
assert_eq!(drain(&evts[0]), 1, "config fires vector 0");
assert_eq!(s.pba_byte(0), 0, "no pending while unmasked");
}
#[test]
fn multi_pair_queues_deliver_to_distinct_vectors() {
let mut s = MsixState::new(5, MSIX_TABLE_MAX);
assert_eq!(
s.num_vectors(),
6,
"config + one vector per vq (cvq slot spare)"
);
let evts: [EventFd; 5] = std::array::from_fn(|_| EventFd::new(libc::EFD_NONBLOCK).unwrap());
for (v, evt) in evts.iter().enumerate() {
s.set_eventfd(v, evt.try_clone().unwrap());
}
s.set_config_vector(0);
for q in 0..4 {
s.set_queue_vector(q, (q + 1) as u16);
}
for e in 0..5 {
s.write_table_dword(e, 0, 0xFEE0_0000);
s.write_table_dword(e, 2, 0x4000 + e as u32);
assert!(s.write_table_dword(e, VECTOR_CTRL_DWORD, 0));
}
s.set_message_control(0x8000);
s.signal(IrqSource::Vring { queue: 2 });
assert_eq!(drain(&evts[3]), 1, "queue 2 (RX pair 1) fires vector 3");
for (v, evt) in evts.iter().enumerate() {
if v != 3 {
assert_eq!(drain(evt), 0, "queue 2 fired only vector 3, not {v}");
}
}
s.signal(IrqSource::Vring { queue: 3 });
assert_eq!(drain(&evts[4]), 1, "queue 3 (TX pair 1) fires vector 4");
for (v, evt) in evts.iter().enumerate() {
if v != 4 {
assert_eq!(drain(evt), 0, "queue 3 fired only vector 4, not {v}");
}
}
s.signal(IrqSource::Vring { queue: 0 });
assert_eq!(drain(&evts[1]), 1, "queue 0 (RX pair 0) fires vector 1");
s.signal(IrqSource::Vring { queue: 1 });
assert_eq!(drain(&evts[2]), 1, "queue 1 (TX pair 0) fires vector 2");
s.signal(IrqSource::Vring { queue: 4 });
for (v, evt) in evts.iter().enumerate() {
assert_eq!(drain(evt), 0, "control vq delivers no MSI-X (vector {v})");
}
assert_eq!(s.pba_byte(0), 0, "all delivered live, nothing pending");
}
#[test]
fn masked_vector_records_pending_then_replays() {
let (mut s, evts) = enabled_state();
assert!(!s.write_table_dword(1, VECTOR_CTRL_DWORD, VECTOR_CTRL_MASK_BIT));
s.signal(IrqSource::Vring { queue: 0 });
assert_eq!(drain(&evts[1]), 0, "masked vector does not fire");
assert_eq!(
s.pba_byte(0) & (1 << 1),
1 << 1,
"pending bit set for vector 1"
);
let unmasked = s.write_table_dword(1, VECTOR_CTRL_DWORD, 0);
assert!(unmasked, "clearing mask bit is an unmask edge");
assert!(s.replay_pending(1), "pending replays on unmask");
assert_eq!(drain(&evts[1]), 1, "replay delivered exactly once");
assert_eq!(s.pba_byte(0) & (1 << 1), 0, "pending cleared after replay");
assert!(!s.replay_pending(1), "no double replay");
}
#[test]
fn function_mask_suppresses_all_vectors() {
let (mut s, evts) = enabled_state();
s.set_message_control(0x8000 | 0x4000); s.signal(IrqSource::Vring { queue: 0 });
s.signal(IrqSource::Vring { queue: 1 });
s.signal(IrqSource::Config);
for (v, evt) in evts.iter().enumerate() {
assert_eq!(drain(evt), 0, "vector {v} suppressed under function mask");
}
assert_eq!(
s.pba_byte(0),
(1 << 0) | (1 << 1) | (1 << 2),
"config + both queue vectors pending under function mask"
);
}
#[test]
fn no_vector_assignment_is_silent() {
let (mut s, evts) = enabled_state();
for q in 0..NUM_QUEUES {
s.set_queue_vector(q, NO_VECTOR);
}
s.signal(IrqSource::Vring { queue: 0 });
s.signal(IrqSource::Vring { queue: 1 });
assert_eq!(drain(&evts[1]), 0, "unassigned queue 0 delivers nothing");
assert_eq!(drain(&evts[2]), 0, "unassigned queue 1 delivers nothing");
assert_eq!(s.pba_byte(0), 0, "and records no pending");
}
#[test]
fn disabled_state_does_not_fire() {
let (mut s, evts) = enabled_state();
s.set_message_control(0); assert!(!s.enabled());
s.signal(IrqSource::Vring { queue: 0 });
assert_eq!(drain(&evts[1]), 0);
assert_eq!(s.pba_byte(0), 0);
}
#[test]
fn msi_message_reads_programmed_dwords() {
let (s, _evts) = enabled_state();
assert_eq!(s.msi_message(0), Some((0xFEE0_0000, 0, 0x4000)));
assert_eq!(s.msi_message(1), Some((0xFEE0_0000, 0, 0x4001)));
assert_eq!(s.msi_message(2), Some((0xFEE0_0000, 0, 0x4002)));
assert_eq!(s.msi_message(SINGLE_PAIR_VECTORS), None, "out of range");
}
#[test]
fn out_of_range_vector_is_inert() {
let mut s = MsixState::new(NUM_QUEUES, MSIX_TABLE_MAX);
s.set_message_control(0x8000);
s.set_config_vector(99); s.signal(IrqSource::Config);
assert_eq!(
s.pba_byte(0),
0,
"out-of-range vector neither fires nor pends"
);
}
#[test]
fn eventfd_replace_returns_prior() {
let mut s = MsixState::new(NUM_QUEUES, MSIX_TABLE_MAX);
let a = EventFd::new(libc::EFD_NONBLOCK).unwrap();
let b = EventFd::new(libc::EFD_NONBLOCK).unwrap();
let a_raw = a.as_raw_fd();
assert!(s.set_eventfd(0, a).is_none(), "first install has no prior");
let prior = s.set_eventfd(0, b).expect("second install returns prior");
assert_eq!(prior.as_raw_fd(), a_raw, "prior eventfd returned");
assert!(
s.set_eventfd(SINGLE_PAIR_VECTORS, EventFd::new(0).unwrap())
.is_none(),
"OOB index"
);
}
}