pub(crate) const IOAPIC_BASE: u64 = 0xFEC0_0000;
pub(crate) const IOAPIC_SIZE: u64 = 0x1000;
pub(crate) const NUM_PINS: usize = 24;
pub(crate) const IOREGSEL: u64 = 0x00; pub(crate) const IOWIN: u64 = 0x10; const IOEOI: u64 = 0x40;
const REG_ID: u32 = 0x00;
const REG_VER: u32 = 0x01;
pub(crate) const REG_REDTBL_BASE: u32 = 0x10;
const IOAPIC_VERSION: u32 = 0x20;
const RTE_VECTOR_MASK: u64 = 0xff;
const RTE_DELIV_MODE_SHIFT: u64 = 8;
const RTE_DELIV_MODE_MASK: u64 = 0x7;
const RTE_DEST_MODE_BIT: u64 = 1 << 11;
const RTE_DELIV_STATUS_BIT: u64 = 1 << 12; const RTE_REMOTE_IRR_BIT: u64 = 1 << 14; const RTE_TRIGGER_LEVEL_BIT: u64 = 1 << 15; const RTE_MASKED_BIT: u64 = 1 << 16;
const RTE_DESTID_0_7_SHIFT: u64 = 56;
const RTE_VIRT_DESTID_8_14_SHIFT: u64 = 49;
const RTE_VIRT_DESTID_8_14_MASK: u64 = 0x7f;
const RTE_RO_BITS: u64 = RTE_DELIV_STATUS_BIT | RTE_REMOTE_IRR_BIT;
const MSI_ADDR_BASE: u32 = 0xFEE0_0000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct MsiRoute {
pub address_lo: u32,
pub address_hi: u32,
pub data: u32,
}
pub(crate) struct Ioapic {
ioregsel: u8,
id: u32,
ioredtbl: [u64; NUM_PINS],
}
impl Ioapic {
pub(crate) fn new() -> Self {
Ioapic {
ioregsel: 0,
id: 0,
ioredtbl: [RTE_MASKED_BIT; NUM_PINS],
}
}
pub(crate) fn mmio_read(&self, offset: u64, data: &mut [u8]) {
let val = match offset {
IOREGSEL => self.ioregsel as u32,
IOWIN => self.read_indirect(self.ioregsel as u32),
_ => 0,
};
write_u32_le(data, val);
}
pub(crate) fn mmio_write(&mut self, offset: u64, data: &[u8]) -> bool {
let val = read_u32_le(data);
match offset {
IOREGSEL => {
self.ioregsel = val as u8;
false
}
IOWIN => self.write_indirect(self.ioregsel as u32, val),
IOEOI => {
self.end_of_interrupt(val as u8);
false
}
_ => false,
}
}
fn read_indirect(&self, reg: u32) -> u32 {
match reg {
REG_ID => self.id << 24,
REG_VER => IOAPIC_VERSION | (((NUM_PINS as u32) - 1) << 16),
_ => {
let (index, high) = redtbl_index(reg);
match self.ioredtbl.get(index) {
Some(&entry) if high => (entry >> 32) as u32,
Some(&entry) => entry as u32,
None => 0,
}
}
}
}
fn write_indirect(&mut self, reg: u32, val: u32) -> bool {
match reg {
REG_ID => {
self.id = (val >> 24) & 0xf;
false
}
REG_VER => false, _ => {
let (index, high) = redtbl_index(reg);
let Some(entry) = self.ioredtbl.get_mut(index) else {
return false;
};
let ro = *entry & RTE_RO_BITS;
if high {
*entry = (*entry & 0x0000_0000_ffff_ffff) | ((val as u64) << 32);
} else {
*entry = (*entry & 0xffff_ffff_0000_0000) | (val as u64);
}
*entry = (*entry & !RTE_RO_BITS) | ro;
true
}
}
}
pub(crate) fn redtbl_to_msi(&self, pin: usize) -> MsiRoute {
let entry = self.ioredtbl[pin];
let vector = (entry & RTE_VECTOR_MASK) as u32;
let delivery_mode = ((entry >> RTE_DELIV_MODE_SHIFT) & RTE_DELIV_MODE_MASK) as u32;
let dest_mode = ((entry & RTE_DEST_MODE_BIT) != 0) as u32;
let trigger_level = ((entry & RTE_TRIGGER_LEVEL_BIT) != 0) as u32;
let dest = self.destination(pin);
let address_lo = MSI_ADDR_BASE | ((dest & 0xff) << 12) | (dest_mode << 2);
let address_hi = (dest >> 8) << 8;
let data = vector | (delivery_mode << 8) | (trigger_level << 15);
MsiRoute {
address_lo,
address_hi,
data,
}
}
pub(crate) fn destination(&self, pin: usize) -> u32 {
let entry = self.ioredtbl[pin];
let destid_0_7 = ((entry >> RTE_DESTID_0_7_SHIFT) & 0xff) as u32;
let virt_destid_8_14 =
((entry >> RTE_VIRT_DESTID_8_14_SHIFT) & RTE_VIRT_DESTID_8_14_MASK) as u32;
destid_0_7 | (virt_destid_8_14 << 8)
}
pub(crate) fn is_masked(&self, pin: usize) -> bool {
self.ioredtbl[pin] & RTE_MASKED_BIT != 0
}
pub(crate) fn end_of_interrupt(&mut self, vector: u8) -> Vec<usize> {
let mut pending = Vec::new();
for (pin, entry) in self.ioredtbl.iter_mut().enumerate() {
let matches_vector = (*entry & RTE_VECTOR_MASK) as u8 == vector;
let level = *entry & RTE_TRIGGER_LEVEL_BIT != 0;
if matches_vector && level && (*entry & RTE_REMOTE_IRR_BIT != 0) {
*entry &= !RTE_REMOTE_IRR_BIT;
if *entry & RTE_MASKED_BIT == 0 {
pending.push(pin);
}
}
}
pending
}
pub(crate) fn gsi_routes(&self) -> Vec<(u32, MsiRoute)> {
(0..NUM_PINS)
.filter(|&pin| !self.is_masked(pin))
.map(|pin| (pin as u32, self.redtbl_to_msi(pin)))
.collect()
}
}
fn redtbl_index(reg: u32) -> (usize, bool) {
let off = reg.wrapping_sub(REG_REDTBL_BASE);
((off >> 1) as usize, off & 1 == 1)
}
fn read_u32_le(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 write_u32_le(data: &mut [u8], val: u32) {
let bytes = val.to_le_bytes();
let n = data.len().min(4);
data[..n].copy_from_slice(&bytes[..n]);
}
#[cfg(test)]
mod tests {
use super::*;
fn write_rte(io: &mut Ioapic, pin: usize, entry: u64) -> bool {
let lo_reg = (REG_REDTBL_BASE + 2 * pin as u32) as u8;
let hi_reg = lo_reg + 1;
io.mmio_write(IOREGSEL, &[lo_reg]);
let dirty_lo = io.mmio_write(IOWIN, &(entry as u32).to_le_bytes());
io.mmio_write(IOREGSEL, &[hi_reg]);
let dirty_hi = io.mmio_write(IOWIN, &((entry >> 32) as u32).to_le_bytes());
dirty_lo || dirty_hi
}
#[test]
fn version_register_reports_24_entries() {
let mut io = Ioapic::new();
io.mmio_write(IOREGSEL, &[REG_VER as u8]);
let mut buf = [0u8; 4];
io.mmio_read(IOWIN, &mut buf);
let ver = u32::from_le_bytes(buf);
assert_eq!(ver & 0xff, IOAPIC_VERSION, "version byte");
assert_eq!((ver >> 16) & 0xff, (NUM_PINS as u32) - 1, "max redir entry");
}
#[test]
fn entries_reset_masked() {
let io = Ioapic::new();
for pin in 0..NUM_PINS {
assert!(io.is_masked(pin), "pin {pin} must reset masked");
}
}
#[test]
fn gsi_routes_includes_unmasked_omits_masked() {
let mut io = Ioapic::new();
assert!(
io.gsi_routes().is_empty(),
"all-masked IOAPIC must yield no routes"
);
let rte4 = 0x30u64 | (1u64 << RTE_DESTID_0_7_SHIFT);
let rte6 = 0x40u64 | (2u64 << RTE_DESTID_0_7_SHIFT);
write_rte(&mut io, 4, rte4);
write_rte(&mut io, 6, rte6);
write_rte(&mut io, 4, rte4 | RTE_MASKED_BIT);
let routes = io.gsi_routes();
let gsis: Vec<u32> = routes.iter().map(|(g, _)| *g).collect();
assert_eq!(
routes.len(),
1,
"only the unmasked pin is routed; got {gsis:?}"
);
assert!(
gsis.contains(&6),
"unmasked pin 6 must be routed; got {gsis:?}"
);
assert!(
!gsis.contains(&4),
"re-masked pin 4 must be omitted; got {gsis:?}"
);
let (_, msi) = routes.iter().find(|(g, _)| *g == 6).expect("pin 6 route");
assert_eq!(msi.data & 0xff, 0x40, "pin 6 MSI carries vector 0x40");
}
#[test]
fn gsi_routes_identical_across_redundant_rte_rewrite() {
let mut io = Ioapic::new();
let rte = 0x40u64 | (2u64 << RTE_DESTID_0_7_SHIFT);
write_rte(&mut io, 6, rte);
let first = io.gsi_routes();
assert_eq!(first.len(), 1, "the unmasked pin is routed");
let dirty = write_rte(&mut io, 6, rte);
assert!(
dirty,
"a redtbl rewrite reports dirty even when the value is unchanged"
);
let second = io.gsi_routes();
assert_eq!(
first, second,
"a redundant RTE rewrite yields an identical route set"
);
}
#[test]
fn gsi_routes_unchanged_on_masked_high_word_then_changes_on_unmask() {
let mut io = Ioapic::new();
let pin = 6usize;
let lo_reg = (REG_REDTBL_BASE + 2 * pin as u32) as u8;
let hi_reg = lo_reg + 1;
let before = io.gsi_routes();
assert!(before.is_empty(), "all pins reset masked → empty route set");
io.mmio_write(IOREGSEL, &[hi_reg]);
let dirty_hi = io.mmio_write(IOWIN, &(2u32 << 24).to_le_bytes());
assert!(dirty_hi, "redtbl high-word write reports dirty");
assert_eq!(
io.gsi_routes(),
before,
"still masked after the high-word write → route set unchanged (dedup skips)"
);
io.mmio_write(IOREGSEL, &[lo_reg]);
let dirty_lo = io.mmio_write(IOWIN, &0x40u32.to_le_bytes());
assert!(dirty_lo, "redtbl low-word write reports dirty");
let after = io.gsi_routes();
assert_eq!(after.len(), 1, "unmasking installs the route");
}
#[test]
fn ioregsel_roundtrips() {
let mut io = Ioapic::new();
io.mmio_write(IOREGSEL, &[0x12]);
let mut buf = [0u8; 4];
io.mmio_read(IOREGSEL, &mut buf);
assert_eq!(u32::from_le_bytes(buf) & 0xff, 0x12);
}
#[test]
fn rte_write_is_routing_dirty_and_reads_back() {
let mut io = Ioapic::new();
let entry: u64 = 0x33;
let dirty = write_rte(&mut io, 6, entry);
assert!(dirty, "RTE write must report a routing change");
io.mmio_write(IOREGSEL, &[(REG_REDTBL_BASE + 12) as u8]);
let mut buf = [0u8; 4];
io.mmio_read(IOWIN, &mut buf);
assert_eq!(u32::from_le_bytes(buf) & 0xff, 0x33);
}
#[test]
fn ioregsel_write_is_not_routing_dirty() {
let mut io = Ioapic::new();
assert!(
!io.mmio_write(IOREGSEL, &[0x10]),
"selecting a reg is not a route change"
);
}
#[test]
fn translate_vector_delivery_trigger() {
let mut io = Ioapic::new();
let entry: u64 = 0x42 | (0b001 << 8) | RTE_TRIGGER_LEVEL_BIT;
write_rte(&mut io, 5, entry);
let msi = io.redtbl_to_msi(5);
assert_eq!(msi.data & 0xff, 0x42, "vector");
assert_eq!((msi.data >> 8) & 0x7, 0b001, "delivery mode");
assert_eq!((msi.data >> 15) & 1, 1, "trigger level");
}
#[test]
fn translate_low_destination() {
let mut io = Ioapic::new();
let entry: u64 = 0x20 | (0x07u64 << RTE_DESTID_0_7_SHIFT);
write_rte(&mut io, 7, entry);
assert_eq!(io.destination(7), 0x07);
let msi = io.redtbl_to_msi(7);
assert_eq!((msi.address_lo >> 12) & 0xff, 0x07);
assert_eq!(msi.address_lo & MSI_ADDR_BASE, MSI_ADDR_BASE);
assert_eq!(msi.address_lo & (1 << 2), 0, "physical dest mode");
assert_eq!(msi.address_hi, 0, "no high dest bits for dest<256");
}
#[test]
fn translate_wide_destination_above_255() {
let mut io = Ioapic::new();
let dest: u32 = 300;
let destid_0_7 = (dest & 0xff) as u64;
let virt_destid_8_14 = ((dest >> 8) & 0x7f) as u64;
let entry: u64 = 0x20
| (destid_0_7 << RTE_DESTID_0_7_SHIFT)
| (virt_destid_8_14 << RTE_VIRT_DESTID_8_14_SHIFT);
write_rte(&mut io, 6, entry);
assert_eq!(io.destination(6), 300, "reconstructed dest");
let msi = io.redtbl_to_msi(6);
let decoded = ((msi.address_lo >> 12) & 0xff) | (((msi.address_hi >> 8) & 0xff_ffff) << 8);
assert_eq!(decoded, 300, "MSI must round-trip the >255 destination");
assert_eq!(
msi.address_hi & 0xff,
0,
"addr_hi low byte must be zero (KVM requires it)"
);
}
#[test]
fn logical_dest_mode_sets_address_bit() {
let mut io = Ioapic::new();
let entry: u64 = 0x20 | RTE_DEST_MODE_BIT;
write_rte(&mut io, 5, entry);
let msi = io.redtbl_to_msi(5);
assert_ne!(
msi.address_lo & (1 << 2),
0,
"logical dest mode -> address_lo bit 2"
);
}
#[test]
fn remote_irr_is_read_only_to_guest() {
let mut io = Ioapic::new();
let entry: u64 = 0x20 | RTE_REMOTE_IRR_BIT | RTE_DELIV_STATUS_BIT;
write_rte(&mut io, 6, entry);
assert_eq!(
io.ioredtbl[6] & RTE_REMOTE_IRR_BIT,
0,
"remote_IRR must not be guest-writable"
);
assert_eq!(
io.ioredtbl[6] & RTE_DELIV_STATUS_BIT,
0,
"delivery_status must not be guest-writable"
);
}
#[test]
fn eoi_clears_remote_irr_on_matching_level_entry() {
let mut io = Ioapic::new();
let entry: u64 = 0x50 | RTE_TRIGGER_LEVEL_BIT;
write_rte(&mut io, 6, entry);
io.ioredtbl[6] |= RTE_REMOTE_IRR_BIT;
let pending = io.end_of_interrupt(0x50);
assert_eq!(
io.ioredtbl[6] & RTE_REMOTE_IRR_BIT,
0,
"EOI clears remote_IRR"
);
assert_eq!(
pending,
vec![6],
"unmasked still-level pin reported for re-injection"
);
}
#[test]
fn eoi_for_edge_entry_is_noop() {
let mut io = Ioapic::new();
let entry: u64 = 0x60; write_rte(&mut io, 7, entry);
let pending = io.end_of_interrupt(0x60);
assert!(pending.is_empty(), "edge EOI re-injects nothing");
}
#[test]
fn eoi_unknown_vector_is_harmless() {
let mut io = Ioapic::new();
let pending = io.end_of_interrupt(0xAB);
assert!(pending.is_empty());
}
#[test]
fn out_of_range_rte_read_is_zero_not_panic() {
let mut io = Ioapic::new();
io.mmio_write(IOREGSEL, &[0xfe]);
let mut buf = [0u8; 4];
io.mmio_read(IOWIN, &mut buf);
assert_eq!(u32::from_le_bytes(buf), 0);
}
#[test]
fn narrow_and_wide_access_helpers_are_total() {
assert_eq!(read_u32_le(&[0xaa]), 0x0000_00aa);
let mut one = [0u8; 1];
write_u32_le(&mut one, 0x1122_3344);
assert_eq!(one[0], 0x44, "LE low byte");
}
use proptest::prelude::*;
proptest! {
#[test]
fn arbitrary_rte_is_safe(raw in any::<u64>(), pin in 0usize..NUM_PINS) {
let mut io = Ioapic::new();
write_rte(&mut io, pin, raw);
prop_assert_eq!(io.ioredtbl[pin] & RTE_RO_BITS, 0, "guest must not set RO bits");
let msi = io.redtbl_to_msi(pin);
let dest = io.destination(pin);
let decoded =
((msi.address_lo >> 12) & 0xff) | (((msi.address_hi >> 8) & 0x00ff_ffff) << 8);
prop_assert_eq!(decoded, dest, "MSI must round-trip the destination");
prop_assert_eq!(msi.address_hi & 0xff, 0, "addr_hi low byte must be zero (KVM requires it)");
let _ = io.end_of_interrupt((raw & 0xff) as u8);
}
#[test]
fn arbitrary_mmio_is_safe(offset in 0u64..IOAPIC_SIZE, val in any::<u32>()) {
let mut io = Ioapic::new();
let _ = io.mmio_write(offset, &val.to_le_bytes());
let mut buf = [0u8; 4];
io.mmio_read(offset, &mut buf);
}
}
}