use std::collections::VecDeque;
use virtio_bindings::virtio_config::{
VIRTIO_CONFIG_S_ACKNOWLEDGE, VIRTIO_CONFIG_S_DRIVER, VIRTIO_CONFIG_S_DRIVER_OK,
VIRTIO_CONFIG_S_FAILED, VIRTIO_CONFIG_S_FEATURES_OK, VIRTIO_F_VERSION_1,
};
use virtio_bindings::virtio_ids::VIRTIO_ID_CONSOLE;
const VIRTIO_CONSOLE_F_MULTIPORT: u32 = 1;
use virtio_bindings::virtio_mmio::{
VIRTIO_MMIO_CONFIG_GENERATION, VIRTIO_MMIO_DEVICE_FEATURES, VIRTIO_MMIO_DEVICE_FEATURES_SEL,
VIRTIO_MMIO_DEVICE_ID, VIRTIO_MMIO_DRIVER_FEATURES, VIRTIO_MMIO_DRIVER_FEATURES_SEL,
VIRTIO_MMIO_INT_VRING, VIRTIO_MMIO_INTERRUPT_ACK, VIRTIO_MMIO_INTERRUPT_STATUS,
VIRTIO_MMIO_MAGIC_VALUE, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, VIRTIO_MMIO_QUEUE_AVAIL_LOW,
VIRTIO_MMIO_QUEUE_DESC_HIGH, VIRTIO_MMIO_QUEUE_DESC_LOW, VIRTIO_MMIO_QUEUE_NOTIFY,
VIRTIO_MMIO_QUEUE_NUM, VIRTIO_MMIO_QUEUE_NUM_MAX, VIRTIO_MMIO_QUEUE_READY,
VIRTIO_MMIO_QUEUE_SEL, VIRTIO_MMIO_QUEUE_USED_HIGH, VIRTIO_MMIO_QUEUE_USED_LOW,
VIRTIO_MMIO_STATUS, VIRTIO_MMIO_VENDOR_ID, VIRTIO_MMIO_VERSION,
};
use virtio_queue::{Queue, QueueT};
use vm_memory::{Bytes, GuestMemoryMmap};
use vmm_sys_util::eventfd::EventFd;
use zerocopy::{FromBytes, IntoBytes};
const MMIO_MAGIC: u32 = 0x7472_6976; const MMIO_VERSION: u32 = 2; const VENDOR_ID: u32 = 0;
pub const VIRTIO_MMIO_SIZE: u64 = 0x1000;
pub const SIGNAL_VC_DUMP: u8 = 0xD1;
pub const SIGNAL_VC_SHUTDOWN: u8 = 0xD3;
pub const SIGNAL_BPF_WRITE_DONE: u8 = 0xBF;
pub const SIGNAL_ACCESSOR_READY: u8 = 0xAC;
pub use super::wire::NUM_PORTS;
const NUM_QUEUES: usize = 2 + 2 * NUM_PORTS as usize;
const QUEUE_MAX_SIZE: u16 = 256;
const PORT0_RXQ: usize = 0;
const PORT0_TXQ: usize = 1;
const C_IVQ: usize = 2; const C_OVQ: usize = 3; const PORT1_RXQ: usize = 4;
const PORT1_TXQ: usize = 5;
const PORT2_RXQ: usize = 6;
const PORT2_TXQ: usize = 7;
const TX_DESC_MAX: usize = 32 * 1024;
const TX_PER_CALL_MAX: usize = 256 * 1024;
const CONTROL_CHAINS_PER_CALL_MAX: usize = 32;
const RX_CHAINS_PER_CALL_MAX: usize = 64;
const S_ACK: u32 = VIRTIO_CONFIG_S_ACKNOWLEDGE;
const S_DRV: u32 = S_ACK | VIRTIO_CONFIG_S_DRIVER;
const S_FEAT: u32 = S_DRV | VIRTIO_CONFIG_S_FEATURES_OK;
#[cfg(test)]
const S_OK: u32 = S_FEAT | VIRTIO_CONFIG_S_DRIVER_OK;
pub use super::wire::VirtioConsoleControl;
pub const VIRTIO_CONSOLE_DEVICE_READY: u16 = super::wire::ControlEvent::DeviceReady.wire_value();
pub const VIRTIO_CONSOLE_PORT_ADD: u16 = super::wire::ControlEvent::PortAdd.wire_value();
#[allow(dead_code)]
pub const VIRTIO_CONSOLE_PORT_REMOVE: u16 = super::wire::ControlEvent::PortRemove.wire_value();
pub const VIRTIO_CONSOLE_PORT_READY: u16 = super::wire::ControlEvent::PortReady.wire_value();
pub const VIRTIO_CONSOLE_CONSOLE_PORT: u16 = super::wire::ControlEvent::ConsolePort.wire_value();
#[allow(dead_code)]
pub const VIRTIO_CONSOLE_RESIZE: u16 = super::wire::ControlEvent::Resize.wire_value();
pub const VIRTIO_CONSOLE_PORT_OPEN: u16 = super::wire::ControlEvent::PortOpen.wire_value();
pub const VIRTIO_CONSOLE_PORT_NAME: u16 = super::wire::ControlEvent::PortName.wire_value();
const VC_CONTROL_SIZE: usize = std::mem::size_of::<VirtioConsoleControl>();
const _: () = assert!(VC_CONTROL_SIZE == 8);
pub use super::wire::PORT1_NAME;
pub use super::wire::PORT2_NAME;
pub const PORT0_NAME: &str = "ktstr-console";
#[derive(Debug, Clone)]
enum ControlOut {
Cmd(VirtioConsoleControl),
Name { id: u32, name: &'static str },
}
impl ControlOut {
fn len(&self) -> usize {
match self {
ControlOut::Cmd(_) => VC_CONTROL_SIZE,
ControlOut::Name { name, .. } => VC_CONTROL_SIZE + name.len() + 1,
}
}
fn write_into(&self, dst: &mut Vec<u8>) {
match self {
ControlOut::Cmd(c) => dst.extend_from_slice(c.as_bytes()),
ControlOut::Name { id, name } => {
let hdr = VirtioConsoleControl {
id: *id,
event: VIRTIO_CONSOLE_PORT_NAME,
value: 1, };
dst.extend_from_slice(hdr.as_bytes());
dst.extend_from_slice(name.as_bytes());
dst.push(0);
}
}
}
}
struct Port {
tx_buf: VecDeque<u8>,
pending_rx: VecDeque<u8>,
opened: bool,
readied: bool,
name: &'static str,
}
impl Port {
const fn new(name: &'static str) -> Self {
Port {
tx_buf: VecDeque::new(),
pending_rx: VecDeque::new(),
opened: false,
readied: false,
name,
}
}
}
const fn queue_to_port(queue_idx: usize) -> Option<(usize, bool)> {
match queue_idx {
PORT0_RXQ => Some((0, false)),
PORT0_TXQ => Some((0, true)),
PORT1_RXQ => Some((1, false)),
PORT1_TXQ => Some((1, true)),
PORT2_RXQ => Some((2, false)),
PORT2_TXQ => Some((2, true)),
_ => None,
}
}
const fn port_queues(port_id: usize) -> (usize, usize) {
match port_id {
0 => (PORT0_RXQ, PORT0_TXQ),
1 => (PORT1_RXQ, PORT1_TXQ),
2 => (PORT2_RXQ, PORT2_TXQ),
_ => panic!("port_queues: port id out of range"),
}
}
const fn port_label(port_id: usize) -> &'static str {
match port_id {
0 => "port0",
1 => "port1",
2 => "port2",
_ => "port?",
}
}
pub struct VirtioConsole {
queues: [Queue; NUM_QUEUES],
queue_select: u32,
device_features_sel: u32,
driver_features_sel: u32,
driver_features: u64,
device_status: u32,
interrupt_status: u32,
config_generation: u32,
irq_evt: EventFd,
tx_evt: EventFd,
stats_tx_evt: EventFd,
mem: Option<GuestMemoryMmap>,
ports: [Port; NUM_PORTS as usize],
tx_scratch: Vec<u8>,
rx_scratch: Vec<u8>,
control_out: VecDeque<ControlOut>,
device_ready: bool,
}
impl Default for VirtioConsole {
fn default() -> Self {
Self::new()
}
}
impl VirtioConsole {
pub fn new() -> Self {
let irq_evt =
EventFd::new(libc::EFD_NONBLOCK).expect("failed to create virtio-console irq eventfd");
let tx_evt =
EventFd::new(libc::EFD_NONBLOCK).expect("failed to create virtio-console tx eventfd");
let stats_tx_evt = EventFd::new(libc::EFD_NONBLOCK)
.expect("failed to create virtio-console stats_tx eventfd");
VirtioConsole {
queues: [
Queue::new(QUEUE_MAX_SIZE).expect("valid queue size"),
Queue::new(QUEUE_MAX_SIZE).expect("valid queue size"),
Queue::new(QUEUE_MAX_SIZE).expect("valid queue size"),
Queue::new(QUEUE_MAX_SIZE).expect("valid queue size"),
Queue::new(QUEUE_MAX_SIZE).expect("valid queue size"),
Queue::new(QUEUE_MAX_SIZE).expect("valid queue size"),
Queue::new(QUEUE_MAX_SIZE).expect("valid queue size"),
Queue::new(QUEUE_MAX_SIZE).expect("valid queue size"),
],
queue_select: 0,
device_features_sel: 0,
driver_features_sel: 0,
driver_features: 0,
device_status: 0,
interrupt_status: 0,
config_generation: 0,
irq_evt,
tx_evt,
stats_tx_evt,
mem: None,
ports: [
Port::new(PORT0_NAME),
Port::new(PORT1_NAME),
Port::new(PORT2_NAME),
],
tx_scratch: Vec::new(),
rx_scratch: Vec::new(),
control_out: VecDeque::new(),
device_ready: false,
}
}
pub fn irq_evt(&self) -> &EventFd {
&self.irq_evt
}
pub fn tx_evt(&self) -> &EventFd {
&self.tx_evt
}
pub fn stats_tx_evt(&self) -> &EventFd {
&self.stats_tx_evt
}
pub fn set_mem(&mut self, mem: GuestMemoryMmap) {
self.mem = Some(mem);
}
fn device_features(&self) -> u64 {
(1u64 << VIRTIO_F_VERSION_1) | (1u64 << (VIRTIO_CONSOLE_F_MULTIPORT as u64))
}
fn selected_queue(&self) -> Option<usize> {
let idx = self.queue_select as usize;
if idx < NUM_QUEUES { Some(idx) } else { None }
}
fn signal_used(&mut self) {
self.interrupt_status |= VIRTIO_MMIO_INT_VRING;
if let Err(e) = self.irq_evt.write(1) {
tracing::warn!(%e, "virtio-console irq_evt.write failed");
}
}
fn multiport_negotiated(&self) -> bool {
self.driver_features & (1u64 << (VIRTIO_CONSOLE_F_MULTIPORT as u64)) != 0
}
fn queue_config_allowed(&self) -> bool {
self.device_status & S_FEAT == S_FEAT && self.device_status & VIRTIO_CONFIG_S_DRIVER_OK == 0
}
fn features_write_allowed(&self) -> bool {
self.device_status & S_DRV == S_DRV && self.device_status & VIRTIO_CONFIG_S_FEATURES_OK == 0
}
fn process_tx(&mut self, port_id: usize) -> bool {
let port_label = port_label(port_id);
let (_, queue_idx) = port_queues(port_id);
if self.device_status & VIRTIO_CONFIG_S_DRIVER_OK == 0 {
tracing::debug!(
queue = queue_idx,
status = self.device_status,
"virtio-console process_tx: DRIVER_OK not set; ignoring notify"
);
return false;
}
if port_id != 0 && !self.multiport_negotiated() {
tracing::warn!(
port = port_label,
"virtio-console process_tx: F_MULTIPORT not \
negotiated; ignoring notify on multiport-only TX queue"
);
return false;
}
let mem = match self.mem.as_ref() {
Some(m) => m,
None => return false,
};
let mut had_data = false;
let mut cumulative_bytes: usize = 0;
let q = &mut self.queues[queue_idx];
while let Some(chain) = q.pop_descriptor_chain(mem) {
let head = chain.head_index();
for desc in chain {
if !desc.is_write_only() {
let guest_addr = desc.addr();
let dlen = (desc.len() as usize).min(TX_DESC_MAX);
self.tx_scratch.clear();
self.tx_scratch.resize(dlen, 0);
let read_ok = match mem.read_slice(&mut self.tx_scratch, guest_addr) {
Ok(()) => {
self.ports[port_id]
.tx_buf
.extend(self.tx_scratch.iter().copied());
true
}
Err(e) => {
tracing::warn!(
port = port_label,
head,
dlen,
%e,
"virtio-console process_tx: read_slice failed \
(descriptor addr likely unmapped); dropping \
segment from this chain"
);
false
}
};
if read_ok && dlen > 0 {
had_data = true;
cumulative_bytes = cumulative_bytes.saturating_add(dlen);
}
}
}
if let Err(e) = q.add_used(mem, head, 0) {
tracing::warn!(
port = port_label,
head,
%e,
"virtio-console TX add_used failed (used-ring address \
likely unmapped); guest TX queue will eventually \
starve and this port will stop"
);
}
if cumulative_bytes >= TX_PER_CALL_MAX {
tracing::debug!(
port = port_label,
cumulative_bytes,
cap = TX_PER_CALL_MAX,
"virtio-console process_tx: per-call byte cap reached; \
remaining chains deferred to next notify"
);
break;
}
}
if had_data {
self.signal_used();
if port_id == 2 {
let _ = self.stats_tx_evt.write(1);
} else {
let _ = self.tx_evt.write(1);
}
}
had_data
}
fn drain_tx_into_capture_buf(&mut self, port_id: usize) {
let port_label = port_label(port_id);
let (_, queue_idx) = port_queues(port_id);
if self.device_status & VIRTIO_CONFIG_S_DRIVER_OK == 0 {
return;
}
if port_id != 0 && !self.multiport_negotiated() {
return;
}
let mem = match self.mem.as_ref() {
Some(m) => m,
None => return,
};
let mut cumulative_bytes: usize = 0;
let q = &mut self.queues[queue_idx];
while let Some(chain) = q.pop_descriptor_chain(mem) {
let head = chain.head_index();
for desc in chain {
if !desc.is_write_only() {
let guest_addr = desc.addr();
let dlen = (desc.len() as usize).min(TX_DESC_MAX);
self.tx_scratch.clear();
self.tx_scratch.resize(dlen, 0);
let read_ok = match mem.read_slice(&mut self.tx_scratch, guest_addr) {
Ok(()) => {
self.ports[port_id]
.tx_buf
.extend(self.tx_scratch.iter().copied());
true
}
Err(e) => {
tracing::warn!(
port = port_label,
head,
dlen,
%e,
"virtio-console reset-drain: read_slice failed \
(descriptor addr likely unmapped); dropping \
segment from this chain"
);
false
}
};
if read_ok && dlen > 0 {
cumulative_bytes = cumulative_bytes.saturating_add(dlen);
}
}
}
if let Err(e) = q.add_used(mem, head, 0) {
tracing::warn!(
port = port_label,
head,
%e,
"virtio-console reset-drain: add_used failed (used-ring \
address likely unmapped); descriptor leaks but the guest \
is rebooting so the leak has no observer"
);
}
if cumulative_bytes >= TX_PER_CALL_MAX {
tracing::debug!(
port = port_label,
cumulative_bytes,
cap = TX_PER_CALL_MAX,
"virtio-console reset-drain: per-call byte cap reached; \
remaining chains lost to queue reset"
);
break;
}
}
}
fn drain_port_tx(&mut self, port_id: usize) -> Vec<u8> {
let buf = &mut self.ports[port_id].tx_buf;
let cap = buf.capacity().min(256 * 1024);
let old = std::mem::replace(buf, VecDeque::with_capacity(cap));
Vec::from(old)
}
pub fn drain_output(&mut self) -> Vec<u8> {
self.drain_port_tx(0)
}
pub fn drain_bulk(&mut self) -> Vec<u8> {
self.drain_port_tx(1)
}
pub(crate) fn final_drain(&mut self) -> Vec<u8> {
let _ = self.process_tx(1);
self.drain_bulk()
}
pub fn push_back_bulk(&mut self, bytes: &[u8]) {
if bytes.is_empty() {
return;
}
let buf = &mut self.ports[1].tx_buf;
buf.reserve(bytes.len());
for &b in bytes.iter().rev() {
buf.push_front(b);
}
}
pub fn drain_port2_bulk(&mut self) -> Vec<u8> {
self.drain_port_tx(2)
}
#[cfg(test)]
pub(crate) fn output_for_test(&self) -> String {
let bytes: Vec<u8> = self.ports[0].tx_buf.iter().copied().collect();
String::from_utf8_lossy(&bytes).to_string()
}
#[cfg(test)]
pub(crate) fn inject_port2_tx_for_test(&mut self, bytes: &[u8]) {
self.ports[2].tx_buf.extend(bytes);
}
#[cfg(test)]
pub(crate) fn pending_rx_bytes_for_test(&self) -> Vec<u8> {
self.ports[0].pending_rx.iter().copied().collect()
}
pub fn queue_input(&mut self, data: &[u8]) {
tracing::debug!(bytes = data.len(), "virtio-console queue_input");
self.ports[0].pending_rx.extend(data);
self.drain_pending_rx(0);
}
pub(crate) fn queue_input_port1(&mut self, data: &[u8]) {
tracing::debug!(bytes = data.len(), "virtio-console queue_input_port1");
self.ports[1].pending_rx.extend(data);
self.drain_pending_rx(1);
}
pub(crate) fn queue_input_port2(&mut self, data: &[u8]) {
tracing::debug!(bytes = data.len(), "virtio-console queue_input_port2");
self.ports[2].pending_rx.extend(data);
self.drain_pending_rx(2);
}
pub(crate) fn clear_port2_pending_rx(&mut self) -> usize {
let pending = &mut self.ports[2].pending_rx;
let n = pending.len();
pending.clear();
n
}
fn drain_pending_rx(&mut self, port_id: usize) {
let port_label = port_label(port_id);
let (queue_idx, _) = port_queues(port_id);
if self.ports[port_id].pending_rx.is_empty() {
return;
}
if self.device_status & VIRTIO_CONFIG_S_DRIVER_OK == 0 {
tracing::debug!(
port = port_label,
pending = self.ports[port_id].pending_rx.len(),
status = self.device_status,
"virtio-console drain_pending_rx: DRIVER_OK not set; deferring"
);
return;
}
if port_id != 0 && !self.multiport_negotiated() {
tracing::warn!(
port = port_label,
pending = self.ports[port_id].pending_rx.len(),
"virtio-console drain_pending_rx: F_MULTIPORT \
not negotiated; deferring host→guest bytes"
);
return;
}
if port_id != 0 && !self.ports[port_id].opened {
tracing::debug!(
port = port_label,
pending = self.ports[port_id].pending_rx.len(),
"virtio-console drain_pending_rx: port not yet opened by guest; deferring"
);
return;
}
let mem = match self.mem.as_ref() {
Some(m) => m,
None => {
tracing::debug!(
port = port_label,
pending = self.ports[port_id].pending_rx.len(),
"virtio-console drain_pending_rx: no mem"
);
return;
}
};
if !self.queues[queue_idx].ready() {
tracing::debug!(
port = port_label,
pending = self.ports[port_id].pending_rx.len(),
"virtio-console drain_pending_rx: RX queue not ready"
);
return;
}
let q = &mut self.queues[queue_idx];
let mut total_written = 0u32;
let mut chains_drained = 0usize;
while !self.ports[port_id].pending_rx.is_empty() {
let Some(chain) = q.pop_descriptor_chain(mem) else {
break;
};
let head = chain.head_index();
let mut consumed_offset = 0usize;
let mut written = 0u32;
let mut chain_torn = false;
for desc in chain {
if desc.is_write_only() && consumed_offset < self.ports[port_id].pending_rx.len() {
let guest_addr = desc.addr();
let avail = desc.len() as usize;
let remaining = self.ports[port_id].pending_rx.len() - consumed_offset;
let chunk = remaining.min(avail);
self.rx_scratch.clear();
let (head_slice, tail_slice) = self.ports[port_id].pending_rx.as_slices();
let head_skip = consumed_offset.min(head_slice.len());
let tail_skip = consumed_offset - head_skip;
let head_avail = &head_slice[head_skip..];
let tail_avail = if tail_skip < tail_slice.len() {
&tail_slice[tail_skip..]
} else {
&[][..]
};
let h = head_avail.len().min(chunk);
self.rx_scratch.extend_from_slice(&head_avail[..h]);
if h < chunk {
let t = (chunk - h).min(tail_avail.len());
self.rx_scratch.extend_from_slice(&tail_avail[..t]);
}
if mem.write_slice(&self.rx_scratch, guest_addr).is_ok() {
let n = self.rx_scratch.len();
consumed_offset += n;
written += n as u32;
} else {
tracing::warn!(
port = port_label,
head,
written,
"virtio-console drain_pending_rx: write_slice failed \
mid-chain; breaking out to avoid partial-fill corruption"
);
chain_torn = true;
break;
}
}
}
if chain_torn {
if let Err(e) = q.add_used(mem, head, 0) {
tracing::warn!(
port = port_label,
head,
%e,
"virtio-console drain_pending_rx: add_used(0) \
after torn write failed; chain head leaked"
);
}
break;
}
if let Err(e) = q.add_used(mem, head, written) {
tracing::warn!(
port = port_label,
head,
written,
%e,
"virtio-console RX add_used failed (used-ring address \
likely unmapped); bytes preserved in pending_rx for \
retry on the next drain cycle"
);
break;
}
self.ports[port_id].pending_rx.drain(..consumed_offset);
total_written += written;
chains_drained += 1;
if chains_drained >= RX_CHAINS_PER_CALL_MAX {
tracing::debug!(
port = port_label,
chains_drained,
cap = RX_CHAINS_PER_CALL_MAX,
pending = self.ports[port_id].pending_rx.len(),
"virtio-console drain_pending_rx: per-call chain \
cap reached; remaining chains deferred to next notify"
);
break;
}
}
if total_written > 0 {
tracing::debug!(
port = port_label,
delivered = total_written,
pending = self.ports[port_id].pending_rx.len(),
"virtio-console drain_pending_rx: delivered to guest",
);
self.signal_used();
}
}
fn process_control_tx(&mut self) {
if self.device_status & VIRTIO_CONFIG_S_DRIVER_OK == 0 {
tracing::debug!("virtio-console c_ovq: DRIVER_OK not set; ignoring notify");
return;
}
if !self.multiport_negotiated() {
tracing::warn!(
"virtio-console c_ovq: F_MULTIPORT not negotiated; \
ignoring notify on multiport-only queue"
);
return;
}
let mut events: Vec<VirtioConsoleControl> = Vec::new();
let mut any_published = false;
{
let mem = match self.mem.as_ref() {
Some(m) => m,
None => return,
};
let q = &mut self.queues[C_OVQ];
let mut chains_drained = 0usize;
while let Some(chain) = q.pop_descriptor_chain(mem) {
let head = chain.head_index();
let mut total = 0u32;
let mut buf = [0u8; VC_CONTROL_SIZE];
let mut need = VC_CONTROL_SIZE;
let mut filled = 0usize;
for desc in chain {
if desc.is_write_only() || need == 0 {
continue;
}
let take = desc.len().min(need as u32) as usize;
if take == 0 {
continue;
}
if let Err(e) = mem.read_slice(&mut buf[filled..filled + take], desc.addr()) {
tracing::warn!(%e, head, "c_ovq read_slice failed");
break;
}
filled += take;
need -= take;
total += take as u32;
}
if filled == VC_CONTROL_SIZE
&& let Ok(c) = VirtioConsoleControl::read_from_bytes(&buf)
{
events.push(c);
}
match q.add_used(mem, head, total) {
Ok(()) => any_published = true,
Err(e) => {
tracing::warn!(
head,
total,
%e,
"virtio-console c_ovq add_used failed"
);
}
}
chains_drained += 1;
if chains_drained >= CONTROL_CHAINS_PER_CALL_MAX {
tracing::debug!(
chains_drained,
cap = CONTROL_CHAINS_PER_CALL_MAX,
"virtio-console process_control_tx: per-call chain \
cap reached; remaining chains deferred to next notify"
);
break;
}
}
}
for c in events {
self.handle_control_event(c);
}
if any_published {
self.signal_used();
}
self.drain_control_in();
}
fn handle_control_event(&mut self, c: VirtioConsoleControl) {
let id = c.id;
let event = c.event;
let value = c.value;
match event {
VIRTIO_CONSOLE_DEVICE_READY => {
if value != 1 {
tracing::warn!(value, "virtio-console DEVICE_READY value != 1");
return;
}
if self.device_ready {
tracing::warn!("virtio-console DEVICE_READY repeat ignored");
return;
}
self.device_ready = true;
for port_id in 0..NUM_PORTS {
self.control_out
.push_back(ControlOut::Cmd(VirtioConsoleControl {
id: port_id,
event: VIRTIO_CONSOLE_PORT_ADD,
value: 1,
}));
}
}
VIRTIO_CONSOLE_PORT_READY => {
if value != 1 {
if value == 0 {
tracing::error!(
id,
"virtio-console PORT_READY value=0: guest \
reports add_port failure for this port \
(kernel virtio_console.c add_port error \
path). The port will not function and the \
control handshake for this port will not \
complete."
);
} else {
tracing::warn!(id, value, "virtio-console PORT_READY != 1");
}
return;
}
if id >= NUM_PORTS {
tracing::warn!(id, "virtio-console PORT_READY for unknown port");
return;
}
if self.ports[id as usize].readied {
tracing::warn!(id, "virtio-console PORT_READY repeat ignored");
return;
}
self.ports[id as usize].readied = true;
let name = self.ports[id as usize].name;
if id == 0 {
self.control_out
.push_back(ControlOut::Cmd(VirtioConsoleControl {
id,
event: VIRTIO_CONSOLE_CONSOLE_PORT,
value: 1,
}));
self.control_out.push_back(ControlOut::Name { id, name });
self.control_out
.push_back(ControlOut::Cmd(VirtioConsoleControl {
id,
event: VIRTIO_CONSOLE_PORT_OPEN,
value: 1,
}));
} else {
self.control_out.push_back(ControlOut::Name { id, name });
self.control_out
.push_back(ControlOut::Cmd(VirtioConsoleControl {
id,
event: VIRTIO_CONSOLE_PORT_OPEN,
value: 1,
}));
}
}
VIRTIO_CONSOLE_PORT_OPEN => {
if id >= NUM_PORTS {
tracing::warn!(id, "virtio-console PORT_OPEN for unknown port");
return;
}
let now_open = value == 1;
let was_open = self.ports[id as usize].opened;
self.ports[id as usize].opened = now_open;
if now_open && !was_open && id != 0 {
self.drain_pending_rx(id as usize);
}
}
other => {
tracing::debug!(
id,
event = other,
value,
"virtio-console: unhandled c_ovq event"
);
}
}
}
fn drain_control_in(&mut self) {
if self.control_out.is_empty() {
return;
}
if self.device_status & VIRTIO_CONFIG_S_DRIVER_OK == 0 {
return;
}
if !self.multiport_negotiated() {
tracing::warn!(
pending = self.control_out.len(),
"virtio-console c_ivq: F_MULTIPORT not negotiated; \
deferring control messages"
);
return;
}
let mem = match self.mem.as_ref() {
Some(m) => m,
None => return,
};
if !self.queues[C_IVQ].ready() {
return;
}
let q = &mut self.queues[C_IVQ];
let mut any_published = false;
let mut scratch: Vec<u8> = Vec::with_capacity(64);
while let Some(msg) = self.control_out.front() {
let need = msg.len();
let Some(chain) = q.pop_descriptor_chain(mem) else {
break;
};
let head = chain.head_index();
let segs: Vec<(u64, usize)> = chain
.filter(|d| d.is_write_only())
.map(|d| (d.addr().0, d.len() as usize))
.collect();
let avail: usize = segs.iter().map(|(_, l)| *l).sum();
if avail < need {
tracing::warn!(
head,
avail,
need,
"virtio-console c_ivq: chain too small for control \
message; trying next chain"
);
if let Err(e) = q.add_used(mem, head, 0) {
tracing::warn!(head, %e, "virtio-console c_ivq add_used(0) failed");
} else {
any_published = true;
}
continue;
}
scratch.clear();
msg.write_into(&mut scratch);
let mut written = 0u32;
let mut idx = 0usize;
let mut torn = false;
for (gpa, seg_len) in &segs {
if idx >= scratch.len() {
break;
}
let chunk = (*seg_len).min(scratch.len() - idx);
if let Err(e) =
mem.write_slice(&scratch[idx..idx + chunk], vm_memory::GuestAddress(*gpa))
{
tracing::warn!(
head,
%e,
"virtio-console c_ivq write_slice failed mid-chain"
);
torn = true;
break;
}
idx += chunk;
written += chunk as u32;
}
if torn {
if let Err(e) = q.add_used(mem, head, 0) {
tracing::warn!(head, %e, "virtio-console c_ivq add_used after torn failed");
} else {
any_published = true;
}
break;
}
if let Err(e) = q.add_used(mem, head, written) {
tracing::warn!(
head,
written,
%e,
"virtio-console c_ivq add_used failed; control message lost"
);
break;
}
any_published = true;
self.control_out.pop_front();
}
if any_published {
self.signal_used();
}
}
pub fn mmio_read(&self, offset: u64, data: &mut [u8]) {
const CFG_BASE: u64 = 0x100;
const CFG_END: u64 = CFG_BASE + 12;
if (CFG_BASE..CFG_END).contains(&offset) {
self.config_read(offset - CFG_BASE, data);
return;
}
if data.len() != 4 {
for b in data.iter_mut() {
*b = 0xff;
}
return;
}
let val: u32 = match offset as u32 {
VIRTIO_MMIO_MAGIC_VALUE => MMIO_MAGIC,
VIRTIO_MMIO_VERSION => MMIO_VERSION,
VIRTIO_MMIO_DEVICE_ID => VIRTIO_ID_CONSOLE,
VIRTIO_MMIO_VENDOR_ID => VENDOR_ID,
VIRTIO_MMIO_DEVICE_FEATURES => {
let page = self.device_features_sel;
if page == 0 {
self.device_features() as u32
} else if page == 1 {
(self.device_features() >> 32) as u32
} else {
0
}
}
VIRTIO_MMIO_QUEUE_NUM_MAX => self
.selected_queue()
.map(|i| self.queues[i].max_size() as u32)
.unwrap_or(0),
VIRTIO_MMIO_QUEUE_READY => self
.selected_queue()
.map(|i| self.queues[i].ready() as u32)
.unwrap_or(0),
VIRTIO_MMIO_INTERRUPT_STATUS => self.interrupt_status,
VIRTIO_MMIO_STATUS => self.device_status,
VIRTIO_MMIO_CONFIG_GENERATION => self.config_generation,
_ => 0,
};
tracing::debug!(offset, val, "virtio-console mmio_read");
data.copy_from_slice(&val.to_le_bytes());
}
fn config_read(&self, offset: u64, data: &mut [u8]) {
let mut cfg = [0u8; 12];
cfg[4..8].copy_from_slice(&NUM_PORTS.to_le_bytes());
let start = offset as usize;
let end = start.saturating_add(data.len());
if end > cfg.len() {
for b in data.iter_mut() {
*b = 0xff;
}
return;
}
data.copy_from_slice(&cfg[start..end]);
}
pub fn mmio_write(&mut self, offset: u64, data: &[u8]) {
if (0x100..0x10c).contains(&offset) {
tracing::warn!(
offset,
len = data.len(),
"virtio-console: guest write to read-only config space ignored"
);
return;
}
if data.len() != 4 {
return;
}
let val = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
tracing::debug!(offset, val, "virtio-console mmio_write");
match offset as u32 {
VIRTIO_MMIO_DEVICE_FEATURES_SEL => self.device_features_sel = val,
VIRTIO_MMIO_DRIVER_FEATURES_SEL => self.driver_features_sel = val,
VIRTIO_MMIO_DRIVER_FEATURES => {
if !self.features_write_allowed() {
return;
}
let page = self.driver_features_sel;
if page == 0 {
self.driver_features =
(self.driver_features & 0xFFFF_FFFF_0000_0000) | val as u64;
} else if page == 1 {
self.driver_features =
(self.driver_features & 0x0000_0000_FFFF_FFFF) | ((val as u64) << 32);
}
}
VIRTIO_MMIO_QUEUE_SEL => self.queue_select = val,
VIRTIO_MMIO_QUEUE_NUM if self.queue_config_allowed() => {
if let Some(i) = self.selected_queue() {
self.queues[i].set_size(val as u16);
}
}
VIRTIO_MMIO_QUEUE_READY if self.queue_config_allowed() => {
if let Some(i) = self.selected_queue() {
self.queues[i].set_ready(val == 1);
}
}
VIRTIO_MMIO_QUEUE_NOTIFY => {
let idx = val as usize;
match idx {
C_IVQ => self.drain_control_in(),
C_OVQ => self.process_control_tx(),
_ => match queue_to_port(idx) {
Some((port_id, false)) => self.drain_pending_rx(port_id),
Some((port_id, true)) => {
let _ = self.process_tx(port_id);
}
None => {
tracing::debug!(idx, "virtio-console: notify on unused queue");
}
},
}
}
VIRTIO_MMIO_INTERRUPT_ACK => {
self.interrupt_status &= !val;
}
VIRTIO_MMIO_STATUS => {
if val == 0 {
self.reset();
} else {
self.set_status(val);
}
}
VIRTIO_MMIO_QUEUE_DESC_LOW if self.queue_config_allowed() => {
if let Some(i) = self.selected_queue() {
self.queues[i].set_desc_table_address(Some(val), None);
}
}
VIRTIO_MMIO_QUEUE_DESC_HIGH if self.queue_config_allowed() => {
if let Some(i) = self.selected_queue() {
self.queues[i].set_desc_table_address(None, Some(val));
}
}
VIRTIO_MMIO_QUEUE_AVAIL_LOW if self.queue_config_allowed() => {
if let Some(i) = self.selected_queue() {
self.queues[i].set_avail_ring_address(Some(val), None);
}
}
VIRTIO_MMIO_QUEUE_AVAIL_HIGH if self.queue_config_allowed() => {
if let Some(i) = self.selected_queue() {
self.queues[i].set_avail_ring_address(None, Some(val));
}
}
VIRTIO_MMIO_QUEUE_USED_LOW if self.queue_config_allowed() => {
if let Some(i) = self.selected_queue() {
self.queues[i].set_used_ring_address(Some(val), None);
}
}
VIRTIO_MMIO_QUEUE_USED_HIGH if self.queue_config_allowed() => {
if let Some(i) = self.selected_queue() {
self.queues[i].set_used_ring_address(None, Some(val));
}
}
_ => {}
}
}
fn set_status(&mut self, val: u32) {
let old = self.device_status;
if val & self.device_status != self.device_status {
tracing::warn!(
old,
val,
"virtio-console set_status: rejected (clears bits) — \
virtio-v1.2 §3.1.1 status bits are monotone within a \
driver session. Hostile-guest FSM violations surface at \
this log level (matches virtio-blk's set_status \
rejection-warn pattern)."
);
return;
}
let new_bits = val & !self.device_status;
if new_bits == VIRTIO_CONFIG_S_FAILED {
self.device_status = val;
tracing::warn!(
old,
new = val,
"virtio-console set_status: guest set FAILED status \
(virtio-v1.2 §2.1.1 bit 0x80 — driver gave up on \
device probe). Stored without further FSM advance."
);
return;
}
let valid = match new_bits {
VIRTIO_CONFIG_S_ACKNOWLEDGE => self.device_status == 0,
VIRTIO_CONFIG_S_DRIVER => self.device_status == S_ACK,
VIRTIO_CONFIG_S_FEATURES_OK => self.device_status == S_DRV,
VIRTIO_CONFIG_S_DRIVER_OK => self.device_status == S_FEAT,
_ => false,
};
if valid {
self.device_status = val;
tracing::debug!(old, new = val, "virtio-console set_status: accepted");
if new_bits == VIRTIO_CONFIG_S_DRIVER_OK {
if self.driver_features & (1u64 << (VIRTIO_CONSOLE_F_MULTIPORT as u64)) == 0 {
tracing::warn!(
driver_features = self.driver_features,
"virtio-console set_status DRIVER_OK: \
F_MULTIPORT (bit 1) not negotiated by \
driver. Multiport control protocol will \
not run; port 1 bulk channel will not \
function. Verify the guest kernel has \
CONFIG_VIRTIO_CONSOLE enabled and that \
feature negotiation completed before \
DRIVER_OK."
);
}
self.drain_pending_rx(0);
self.drain_control_in();
}
} else {
tracing::warn!(
old,
val,
"virtio-console set_status: rejected (invalid transition) — \
virtio-v1.2 §3.1.1 ordering: ACK → DRIVER → FEATURES_OK \
→ DRIVER_OK, one bit at a time. Hostile-guest FSM \
violations surface at this log level."
);
}
}
fn reset(&mut self) {
for port_id in 0..NUM_PORTS as usize {
self.drain_tx_into_capture_buf(port_id);
}
self.device_status = 0;
self.interrupt_status = 0;
self.queue_select = 0;
self.device_features_sel = 0;
self.driver_features_sel = 0;
self.driver_features = 0;
for port in &mut self.ports {
port.pending_rx.clear();
port.opened = false;
port.readied = false;
}
self.control_out.clear();
self.device_ready = false;
for q in &mut self.queues {
q.reset();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::io::AsRawFd;
use virtio_bindings::bindings::virtio_ring::{VRING_DESC_F_NEXT, VRING_DESC_F_WRITE};
use virtio_bindings::virtio_mmio::VIRTIO_MMIO_INT_CONFIG;
use virtio_queue::desc::{RawDescriptor, split::Descriptor as SplitDescriptor};
use virtio_queue::mock::MockSplitQueue;
use vm_memory::{Address, GuestAddress};
fn read_reg(dev: &VirtioConsole, offset: u32) -> u32 {
let mut buf = [0u8; 4];
dev.mmio_read(offset as u64, &mut buf);
u32::from_le_bytes(buf)
}
fn write_reg(dev: &mut VirtioConsole, offset: u32, val: u32) {
dev.mmio_write(offset as u64, &val.to_le_bytes());
}
fn init_device(dev: &mut VirtioConsole) {
write_reg(dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0);
write_reg(
dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << VIRTIO_CONSOLE_F_MULTIPORT,
);
write_reg(dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(dev, VIRTIO_MMIO_STATUS, S_FEAT);
write_reg(dev, VIRTIO_MMIO_STATUS, S_OK);
}
fn make_chain_test_mem() -> GuestMemoryMmap {
GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 2 << 20)])
.expect("create chain test guest mem")
}
fn wire_console_queue_to_mock(
dev: &mut VirtioConsole,
mock: &MockSplitQueue<GuestMemoryMmap>,
queue_idx: u32,
) {
write_reg(dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0);
write_reg(
dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << VIRTIO_CONSOLE_F_MULTIPORT,
);
write_reg(dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(dev, VIRTIO_MMIO_STATUS, S_FEAT);
write_reg(dev, VIRTIO_MMIO_QUEUE_SEL, queue_idx);
write_reg(dev, VIRTIO_MMIO_QUEUE_NUM, 16);
let desc = mock.desc_table_addr().0;
let avail = mock.avail_addr().0;
let used = mock.used_addr().0;
write_reg(dev, VIRTIO_MMIO_QUEUE_DESC_LOW, desc as u32);
write_reg(dev, VIRTIO_MMIO_QUEUE_DESC_HIGH, (desc >> 32) as u32);
write_reg(dev, VIRTIO_MMIO_QUEUE_AVAIL_LOW, avail as u32);
write_reg(dev, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, (avail >> 32) as u32);
write_reg(dev, VIRTIO_MMIO_QUEUE_USED_LOW, used as u32);
write_reg(dev, VIRTIO_MMIO_QUEUE_USED_HIGH, (used >> 32) as u32);
write_reg(dev, VIRTIO_MMIO_QUEUE_READY, 1);
write_reg(dev, VIRTIO_MMIO_STATUS, S_OK);
assert_eq!(
dev.device_status, S_OK,
"wire_console_queue_to_mock: FSM did not reach DRIVER_OK \
(got {:#x}) — feature negotiation likely regressed",
dev.device_status,
);
assert!(
dev.queues[queue_idx as usize].ready(),
"wire_console_queue_to_mock: queue {queue_idx} did not \
become ready after QUEUE_READY=1",
);
}
#[test]
fn magic_version_device_id() {
let dev = VirtioConsole::new();
assert_eq!(read_reg(&dev, VIRTIO_MMIO_MAGIC_VALUE), 0x7472_6976);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_VERSION), 2);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_DEVICE_ID), VIRTIO_ID_CONSOLE);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_VENDOR_ID), 0);
}
#[test]
fn device_features_advertises_multiport_and_v1() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_DEVICE_FEATURES_SEL, 0);
let lo = read_reg(&dev, VIRTIO_MMIO_DEVICE_FEATURES);
write_reg(&mut dev, VIRTIO_MMIO_DEVICE_FEATURES_SEL, 1);
let hi = read_reg(&dev, VIRTIO_MMIO_DEVICE_FEATURES);
let features = (hi as u64) << 32 | lo as u64;
assert_ne!(features & (1 << VIRTIO_F_VERSION_1), 0);
assert_ne!(features & (1u64 << (VIRTIO_CONSOLE_F_MULTIPORT as u64)), 0);
}
#[test]
fn config_space_max_nr_ports_at_offset_4() {
let dev = VirtioConsole::new();
let mut buf = [0u8; 4];
dev.mmio_read(0x100 + 4, &mut buf);
assert_eq!(u32::from_le_bytes(buf), NUM_PORTS);
}
#[test]
fn config_space_cols_rows_zero_without_f_size() {
let dev = VirtioConsole::new();
let mut buf = [0u8; 4];
dev.mmio_read(0x100, &mut buf);
assert_eq!(buf, [0, 0, 0, 0]);
}
#[test]
fn queue_num_max_for_six_queues() {
let mut dev = VirtioConsole::new();
for q in 0..NUM_QUEUES {
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, q as u32);
assert_eq!(
read_reg(&dev, VIRTIO_MMIO_QUEUE_NUM_MAX),
QUEUE_MAX_SIZE as u32,
"queue {q} should be available",
);
}
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, NUM_QUEUES as u32);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_QUEUE_NUM_MAX), 0);
}
#[test]
fn queue_ready_requires_features_ok() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, 0);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_READY, 1);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_QUEUE_READY), 0);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, 0);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_READY, 1);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_QUEUE_READY), 1);
}
#[test]
fn status_state_machine() {
let mut dev = VirtioConsole::new();
assert_eq!(read_reg(&dev, VIRTIO_MMIO_STATUS), 0);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
assert_eq!(dev.device_status, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
assert_eq!(dev.device_status, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_OK);
assert_eq!(
dev.device_status, S_DRV,
"skip FEATURES_OK must be rejected"
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
assert_eq!(
dev.device_status, S_DRV,
"clearing DRIVER bit must be rejected"
);
}
#[test]
fn status_reset_via_zero() {
let mut dev = VirtioConsole::new();
init_device(&mut dev);
assert_eq!(dev.device_status, S_OK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
assert_eq!(dev.device_status, 0);
}
#[test]
fn interrupt_status_and_ack() {
let mut dev = VirtioConsole::new();
assert_eq!(read_reg(&dev, VIRTIO_MMIO_INTERRUPT_STATUS), 0);
dev.interrupt_status = VIRTIO_MMIO_INT_VRING;
assert_eq!(
read_reg(&dev, VIRTIO_MMIO_INTERRUPT_STATUS),
VIRTIO_MMIO_INT_VRING
);
}
#[test]
fn interrupt_ack_clears_bits() {
let mut dev = VirtioConsole::new();
dev.interrupt_status = VIRTIO_MMIO_INT_VRING | VIRTIO_MMIO_INT_CONFIG;
write_reg(&mut dev, VIRTIO_MMIO_INTERRUPT_ACK, VIRTIO_MMIO_INT_VRING);
assert_eq!(
read_reg(&dev, VIRTIO_MMIO_INTERRUPT_STATUS),
VIRTIO_MMIO_INT_CONFIG
);
}
#[test]
fn non_4byte_read_returns_ff() {
let dev = VirtioConsole::new();
let mut buf = [0u8; 2];
dev.mmio_read(0, &mut buf);
assert_eq!(buf, [0xff, 0xff]);
}
#[test]
fn non_4byte_write_ignored() {
let mut dev = VirtioConsole::new();
dev.mmio_write(VIRTIO_MMIO_STATUS as u64, &[0x01, 0x00]);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_STATUS), 0);
}
#[test]
fn driver_features_gated_by_status() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES, 0xDEAD);
assert_eq!(dev.driver_features, 0);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES, 0xDEAD_BEEF);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES, 0xCAFE_BABE);
assert_eq!(dev.driver_features, 0xCAFE_BABE_DEAD_BEEF);
}
#[test]
fn features_rejected_after_features_ok() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES, 0xFFFF);
assert_eq!(dev.driver_features & 0xFFFF_FFFF, 0);
}
#[test]
fn queue_desc_addr_requires_features_ok() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, 0);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, 0x1000);
assert_ne!(dev.queues[0].desc_table(), 0x1000);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, 0);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, 0x1000);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_HIGH, 0);
assert_eq!(dev.queues[0].desc_table(), 0x1000);
}
#[test]
fn reset_clears_all_state() {
let mut dev = VirtioConsole::new();
init_device(&mut dev);
dev.interrupt_status = 0xFF;
dev.ports[0].tx_buf.extend(b"leftover0".iter().copied());
dev.ports[1].tx_buf.extend(b"leftover1".iter().copied());
dev.ports[2].tx_buf.extend(b"leftover2".iter().copied());
dev.ports[0].pending_rx.extend(b"pending0".iter().copied());
dev.ports[1].pending_rx.extend(b"pending1".iter().copied());
dev.ports[2].pending_rx.extend(b"pending2".iter().copied());
dev.ports[0].opened = true;
dev.ports[1].opened = true;
dev.ports[2].opened = true;
dev.device_ready = true;
dev.ports[0].readied = true;
dev.ports[1].readied = true;
dev.ports[2].readied = true;
dev.control_out
.push_back(ControlOut::Cmd(VirtioConsoleControl {
id: 0,
event: VIRTIO_CONSOLE_PORT_ADD,
value: 0,
}));
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_STATUS), 0);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_INTERRUPT_STATUS), 0);
assert_eq!(dev.queue_select, 0);
assert_eq!(dev.device_features_sel, 0);
assert_eq!(dev.driver_features, 0);
assert_eq!(
dev.ports[0].tx_buf.iter().copied().collect::<Vec<u8>>(),
b"leftover0",
"ports[0].tx_buf must survive reset (host-side capture buffer)"
);
assert_eq!(
dev.ports[1].tx_buf.iter().copied().collect::<Vec<u8>>(),
b"leftover1",
"ports[1].tx_buf must survive reset (host-side capture buffer)"
);
assert_eq!(
dev.ports[2].tx_buf.iter().copied().collect::<Vec<u8>>(),
b"leftover2",
"ports[2].tx_buf must survive reset (host-side capture buffer)"
);
assert!(dev.ports[0].pending_rx.is_empty());
assert!(dev.ports[1].pending_rx.is_empty());
assert!(dev.ports[2].pending_rx.is_empty());
for p in &dev.ports {
assert!(!p.opened);
}
assert!(!dev.device_ready);
for p in &dev.ports {
assert!(!p.readied);
}
assert!(dev.control_out.is_empty());
}
#[test]
fn config_generation_initially_zero() {
let dev = VirtioConsole::new();
assert_eq!(read_reg(&dev, VIRTIO_MMIO_CONFIG_GENERATION), 0);
}
#[test]
fn new_creates_eventfds() {
let dev = VirtioConsole::new();
assert!(dev.irq_evt().as_raw_fd() >= 0);
assert!(dev.tx_evt().as_raw_fd() >= 0);
assert!(dev.stats_tx_evt().as_raw_fd() >= 0);
let irq = dev.irq_evt().as_raw_fd();
let tx = dev.tx_evt().as_raw_fd();
let stats = dev.stats_tx_evt().as_raw_fd();
assert_ne!(irq, tx);
assert_ne!(irq, stats);
assert_ne!(tx, stats);
}
#[test]
fn output_empty_initially() {
let dev = VirtioConsole::new();
assert!(dev.output_for_test().is_empty());
}
#[test]
fn drain_output_empty() {
let mut dev = VirtioConsole::new();
assert!(dev.drain_output().is_empty());
}
#[test]
fn drain_bulk_empty() {
let mut dev = VirtioConsole::new();
assert!(dev.drain_bulk().is_empty());
}
#[test]
fn set_mem_stores_reference() {
let mut dev = VirtioConsole::new();
assert!(dev.mem.is_none());
let mem = GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap();
dev.set_mem(mem);
assert!(dev.mem.is_some());
}
#[test]
fn queue_input_no_mem_no_panic() {
let mut dev = VirtioConsole::new();
dev.queue_input(b"hello");
}
#[test]
fn unknown_register_returns_zero() {
let dev = VirtioConsole::new();
assert_eq!(read_reg(&dev, 0x300), 0);
}
#[test]
fn unknown_register_write_ignored() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, 0x300, 0xDEAD);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_STATUS), 0);
}
#[test]
fn invalid_queue_select_returns_zero() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, 99);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_QUEUE_NUM_MAX), 0);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_QUEUE_READY), 0);
}
#[test]
fn signal_used_sets_interrupt_and_writes_eventfd() {
let mut dev = VirtioConsole::new();
assert_eq!(dev.interrupt_status, 0);
dev.signal_used();
assert_ne!(dev.interrupt_status & VIRTIO_MMIO_INT_VRING, 0);
let val = dev.irq_evt.read().unwrap();
assert!(val > 0);
}
#[test]
fn features_page_2_returns_zero() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_DEVICE_FEATURES_SEL, 2);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_DEVICE_FEATURES), 0);
}
#[test]
fn tx_evt_silent_on_empty_process_tx() {
let mut dev = VirtioConsole::new();
let mem = GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x10000)]).unwrap();
dev.set_mem(mem);
let _ = dev.process_tx(0);
let _ = dev.process_tx(1);
assert!(dev.tx_evt.read().is_err());
}
#[test]
fn status_skip_acknowledge_rejected() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_STATUS, VIRTIO_CONFIG_S_DRIVER);
assert_eq!(dev.device_status, 0);
}
#[test]
fn queue_config_rejected_after_driver_ok() {
let mut dev = VirtioConsole::new();
init_device(&mut dev);
assert_eq!(dev.device_status, S_OK);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, 0);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NUM, 64);
assert_eq!(dev.queues[0].size(), QUEUE_MAX_SIZE);
}
#[test]
fn config_space_write_ignored() {
let mut dev = VirtioConsole::new();
let buf = 99u32.to_le_bytes();
dev.mmio_write(0x104, &buf);
let mut out = [0u8; 4];
dev.mmio_read(0x104, &mut out);
assert_eq!(u32::from_le_bytes(out), NUM_PORTS);
}
#[test]
fn handle_device_ready_enqueues_port_adds() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 0xFFFF_FFFF,
event: VIRTIO_CONSOLE_DEVICE_READY,
value: 1,
});
assert!(dev.device_ready);
assert_eq!(dev.control_out.len(), NUM_PORTS as usize);
for (i, msg) in dev.control_out.iter().enumerate() {
match msg {
ControlOut::Cmd(c) => {
let id = c.id;
let event = c.event;
let value = c.value;
assert_eq!(id, i as u32);
assert_eq!(event, VIRTIO_CONSOLE_PORT_ADD);
assert_eq!(value, 1);
}
_ => panic!("unexpected msg variant"),
}
}
}
#[test]
fn handle_port_ready_port0_console_announce() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 0,
event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
assert_eq!(dev.control_out.len(), 3);
let m0 = &dev.control_out[0];
let m1 = &dev.control_out[1];
let m2 = &dev.control_out[2];
match m0 {
ControlOut::Cmd(c) => {
let event = c.event;
let value = c.value;
assert_eq!(event, VIRTIO_CONSOLE_CONSOLE_PORT);
assert_eq!(value, 1);
}
_ => panic!("expected Cmd"),
}
match m1 {
ControlOut::Name { id, name } => {
assert_eq!(*id, 0);
assert_eq!(*name, PORT0_NAME);
}
_ => panic!("expected Name"),
}
match m2 {
ControlOut::Cmd(c) => {
let event = c.event;
let value = c.value;
assert_eq!(event, VIRTIO_CONSOLE_PORT_OPEN);
assert_eq!(value, 1);
}
_ => panic!("expected Cmd"),
}
}
#[test]
fn handle_port_ready_port1_name_then_open() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 1,
event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
assert_eq!(dev.control_out.len(), 2);
match &dev.control_out[0] {
ControlOut::Name { id, name } => {
assert_eq!(*id, 1);
assert_eq!(*name, PORT1_NAME);
}
_ => panic!("expected Name"),
}
match &dev.control_out[1] {
ControlOut::Cmd(c) => {
let event = c.event;
let value = c.value;
let id = c.id;
assert_eq!(id, 1);
assert_eq!(event, VIRTIO_CONSOLE_PORT_OPEN);
assert_eq!(value, 1);
}
_ => panic!("expected Cmd"),
}
}
#[test]
fn handle_port_open_tracks_state() {
let mut dev = VirtioConsole::new();
assert!(!dev.ports[1].opened);
dev.handle_control_event(VirtioConsoleControl {
id: 1,
event: VIRTIO_CONSOLE_PORT_OPEN,
value: 1,
});
assert!(dev.ports[1].opened);
dev.handle_control_event(VirtioConsoleControl {
id: 1,
event: VIRTIO_CONSOLE_PORT_OPEN,
value: 0,
});
assert!(!dev.ports[1].opened);
}
#[test]
fn handle_port_ready_unknown_port_ignored() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 99,
event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
assert!(dev.control_out.is_empty());
}
#[test]
fn vc_control_size_is_eight_bytes() {
assert_eq!(VC_CONTROL_SIZE, 8);
assert_eq!(std::mem::size_of::<VirtioConsoleControl>(), 8);
}
#[test]
fn vc_control_round_trip_through_bytes() {
let c = VirtioConsoleControl {
id: 0xDEAD_BEEF,
event: VIRTIO_CONSOLE_PORT_OPEN,
value: 1,
};
let bytes = c.as_bytes();
let back = VirtioConsoleControl::read_from_bytes(bytes).unwrap();
let id = back.id;
let event = back.event;
let value = back.value;
assert_eq!(id, 0xDEAD_BEEF);
assert_eq!(event, VIRTIO_CONSOLE_PORT_OPEN);
assert_eq!(value, 1);
}
#[test]
fn port1_tx_single_descriptor_lands_in_port1_buf() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let payload = b"port1 single descriptor TX";
mem.write_slice(payload, data_addr).expect("plant payload");
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
payload.len() as u32,
0,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, PORT1_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
let drained = dev.drain_bulk();
assert_eq!(
drained,
payload.to_vec(),
"port 1 TX must deliver the descriptor's bytes to drain_bulk verbatim",
);
assert!(
dev.drain_output().is_empty(),
"port 0 TX buffer must remain empty when only port 1 was notified",
);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(used_idx, 1, "exactly one used-ring entry expected");
assert_ne!(
dev.interrupt_status & VIRTIO_MMIO_INT_VRING,
0,
"INT_VRING must be set after a successful TX drain",
);
let irq_count = dev.irq_evt.read().expect("irq_evt was written");
assert!(
irq_count > 0,
"irq_evt counter must be non-zero after signal_used",
);
}
#[test]
fn port1_tx_multi_descriptor_chain_concatenates() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
const PAGE: u32 = 4096;
let segs: [(GuestAddress, u8); 4] = [
(GuestAddress(0x10000), 0xA1),
(GuestAddress(0x12000), 0xA2),
(GuestAddress(0x14000), 0xA3),
(GuestAddress(0x16000), 0xA4),
];
for (addr, fill) in &segs {
let buf = vec![*fill; PAGE as usize];
mem.write_slice(&buf, *addr).expect("plant segment");
}
let descs = [
RawDescriptor::from(SplitDescriptor::new(segs[0].0.0, PAGE, 0, 0)),
RawDescriptor::from(SplitDescriptor::new(segs[1].0.0, PAGE, 0, 0)),
RawDescriptor::from(SplitDescriptor::new(segs[2].0.0, PAGE, 0, 0)),
RawDescriptor::from(SplitDescriptor::new(segs[3].0.0, PAGE, 0, 0)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, PORT1_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
let drained = dev.drain_bulk();
assert_eq!(
drained.len(),
4 * PAGE as usize,
"drain_bulk length must equal sum of segment lengths",
);
for (i, (_, fill)) in segs.iter().enumerate() {
let start = i * PAGE as usize;
let end = start + PAGE as usize;
assert!(
drained[start..end].iter().all(|&b| b == *fill),
"segment {i} must hold fill {fill:#x} verbatim — chain order \
or per-descriptor append regressed",
);
}
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(
used_idx, 1,
"one chain → one used-ring entry, regardless of segment count",
);
}
#[test]
fn port1_tx_oversize_descriptor_truncates_to_tx_desc_max() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
const OVERSIZE: usize = TX_DESC_MAX * 2;
let mut payload = vec![0x55u8; TX_DESC_MAX];
payload.extend_from_slice(&vec![0x99u8; TX_DESC_MAX]);
assert_eq!(payload.len(), OVERSIZE);
mem.write_slice(&payload, data_addr).expect("plant payload");
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
OVERSIZE as u32,
0,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, PORT1_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
let drained = dev.drain_bulk();
assert_eq!(
drained.len(),
TX_DESC_MAX,
"oversize descriptor (len > TX_DESC_MAX) must truncate to TX_DESC_MAX",
);
assert!(
drained.iter().all(|&b| b == 0x55),
"truncated bytes must be the FIRST TX_DESC_MAX bytes \
of the descriptor (0x55), not anything past the cap",
);
}
#[test]
fn port1_tx_rejected_without_driver_ok() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let payload = b"this must NOT reach port1_tx_buf";
mem.write_slice(payload, data_addr).expect("plant payload");
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
payload.len() as u32,
0,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << VIRTIO_CONSOLE_F_MULTIPORT,
);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, PORT1_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NUM, 16);
let desc = mock.desc_table_addr().0;
let avail = mock.avail_addr().0;
let used = mock.used_addr().0;
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, desc as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_HIGH, (desc >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_LOW, avail as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, (avail >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_LOW, used as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_HIGH, (used >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_READY, 1);
assert_eq!(
dev.device_status & VIRTIO_CONFIG_S_DRIVER_OK,
0,
"precondition: DRIVER_OK must NOT be set",
);
assert!(
dev.queues[PORT1_TXQ].ready(),
"precondition: port 1 TX queue must be ready (the gate \
we are testing is the DRIVER_OK gate, not a not-ready \
gate)",
);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
assert!(
dev.drain_bulk().is_empty(),
"port1_tx_buf must remain empty — DRIVER_OK gate must \
reject pre-DRIVER_OK notify",
);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(
used_idx, 0,
"used.idx must be 0 — DRIVER_OK gate must skip add_used",
);
assert_eq!(
dev.interrupt_status, 0,
"interrupt_status must be 0 — DRIVER_OK gate must skip signal_used",
);
match dev.irq_evt.read() {
Ok(n) => panic!("irq_evt must NOT have been written, but read returned {n}"),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("unexpected irq_evt read error: {e}"),
}
}
#[test]
fn port0_tx_vs_port1_tx_routes_to_correct_buffer() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock0 = MockSplitQueue::create(&mem, GuestAddress(0x0), 16);
let mock1 = MockSplitQueue::create(&mem, GuestAddress(0x1000), 16);
let port0_data_addr = GuestAddress(0x10000);
let port1_data_addr = GuestAddress(0x20000);
let port0_payload = b"port0 console bytes";
let port1_payload = b"port1 bulk TLV bytes";
mem.write_slice(port0_payload, port0_data_addr)
.expect("plant port0 payload");
mem.write_slice(port1_payload, port1_data_addr)
.expect("plant port1 payload");
let port0_descs = [RawDescriptor::from(SplitDescriptor::new(
port0_data_addr.0,
port0_payload.len() as u32,
0,
0,
))];
let port1_descs = [RawDescriptor::from(SplitDescriptor::new(
port1_data_addr.0,
port1_payload.len() as u32,
0,
0,
))];
mock0
.build_desc_chain(&port0_descs)
.expect("build port0 chain");
mock1
.build_desc_chain(&port1_descs)
.expect("build port1 chain");
dev.set_mem(mem.clone());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << VIRTIO_CONSOLE_F_MULTIPORT,
);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, PORT0_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NUM, 16);
let d0 = mock0.desc_table_addr().0;
let a0 = mock0.avail_addr().0;
let u0 = mock0.used_addr().0;
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, d0 as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_HIGH, (d0 >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_LOW, a0 as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, (a0 >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_LOW, u0 as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_HIGH, (u0 >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_READY, 1);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, PORT1_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NUM, 16);
let d1 = mock1.desc_table_addr().0;
let a1 = mock1.avail_addr().0;
let u1 = mock1.used_addr().0;
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, d1 as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_HIGH, (d1 >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_LOW, a1 as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, (a1 >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_LOW, u1 as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_HIGH, (u1 >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_READY, 1);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_OK);
assert_eq!(
dev.device_status, S_OK,
"FSM did not reach DRIVER_OK after both queues configured",
);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT0_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
let port0_drained = dev.drain_output();
assert_eq!(
port0_drained,
port0_payload.to_vec(),
"port 0 TX bytes must route to port0_tx_buf — drain_output \
returns port0 bytes verbatim",
);
let port1_drained = dev.drain_bulk();
assert_eq!(
port1_drained,
port1_payload.to_vec(),
"port 1 TX bytes must route to port1_tx_buf — drain_bulk \
returns port1 bytes verbatim",
);
let port0_used_idx: u16 = mem
.read_obj(mock0.used_addr().checked_add(2).unwrap())
.expect("read port0 used.idx");
let port1_used_idx: u16 = mem
.read_obj(mock1.used_addr().checked_add(2).unwrap())
.expect("read port1 used.idx");
assert_eq!(
port0_used_idx, 1,
"port 0 used.idx must reflect 1 completion"
);
assert_eq!(
port1_used_idx, 1,
"port 1 used.idx must reflect 1 completion"
);
}
#[test]
fn port1_tx_per_call_cap_partial_drain() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
const N_CHAINS: usize = 9;
let mut descs: Vec<RawDescriptor> = Vec::with_capacity(N_CHAINS);
for i in 0..N_CHAINS {
let buf_addr = GuestAddress(0x10000 + (i as u64) * (TX_DESC_MAX as u64));
let fill = (i + 1) as u8;
let payload = vec![fill; TX_DESC_MAX];
mem.write_slice(&payload, buf_addr)
.expect("plant per-chain payload");
descs.push(RawDescriptor::from(SplitDescriptor::new(
buf_addr.0,
TX_DESC_MAX as u32,
0, 0,
)));
}
mock.add_desc_chains(&descs, 0)
.expect("publish 9 standalone chains");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, PORT1_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
let drained_first = dev.drain_bulk();
assert_eq!(
drained_first.len(),
TX_PER_CALL_MAX,
"first notify must drain exactly TX_PER_CALL_MAX bytes \
(8 × TX_DESC_MAX) — the per-call cap stops popping after \
the 8th chain"
);
for i in 0..8 {
let start = i * TX_DESC_MAX;
let end = start + TX_DESC_MAX;
let expected_fill = (i + 1) as u8;
assert!(
drained_first[start..end]
.iter()
.all(|&b| b == expected_fill),
"chain {i} bytes must be fill={expected_fill}; \
a regression that drained past the cap (or out of \
chain order) would surface a different byte here"
);
}
assert!(
!drained_first.contains(&9u8),
"9th chain (fill=9) must NOT appear in the first drain \
— the per-call cap must hold the 9th chain back"
);
let used_idx_first: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx after first notify");
assert_eq!(
used_idx_first, 8,
"used.idx must be 8 after first notify — the cap stopped \
popping after the 8th chain so only 8 add_used calls \
happened"
);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
let drained_second = dev.drain_bulk();
assert_eq!(
drained_second.len(),
TX_DESC_MAX,
"second notify must drain the remaining 9th chain \
(TX_DESC_MAX bytes) — the cap is per-call, not per-run"
);
assert!(
drained_second.iter().all(|&b| b == 9u8),
"second drain must contain the 9th chain's bytes (fill=9)"
);
let used_idx_second: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx after second notify");
assert_eq!(
used_idx_second, 9,
"used.idx must be 9 after second notify — every chain \
eventually drains, the cap only spreads them across \
multiple notifies"
);
}
#[test]
fn handle_device_ready_repeat_ignored() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 0,
event: VIRTIO_CONSOLE_DEVICE_READY,
value: 1,
});
assert!(
dev.device_ready,
"device_ready must be set after first message"
);
let after_first = dev.control_out.len();
assert_eq!(
after_first, NUM_PORTS as usize,
"first DEVICE_READY must enqueue exactly NUM_PORTS PORT_ADD frames",
);
dev.handle_control_event(VirtioConsoleControl {
id: 0,
event: VIRTIO_CONSOLE_DEVICE_READY,
value: 1,
});
assert_eq!(
dev.control_out.len(),
after_first,
"DEVICE_READY repeat must be a no-op — control_out length must \
remain at NUM_PORTS, otherwise a hostile guest can grow it \
unboundedly",
);
assert!(
dev.device_ready,
"device_ready must remain set after repeat"
);
}
#[test]
fn handle_port_ready_repeat_ignored_port0() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 0,
event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
assert!(
dev.ports[0].readied,
"port_readied[0] must be set after first PORT_READY"
);
let after_first = dev.control_out.len();
assert_eq!(
after_first, 3,
"first PORT_READY for port 0 must enqueue 3 frames \
(CONSOLE_PORT, PORT_NAME, PORT_OPEN)",
);
dev.handle_control_event(VirtioConsoleControl {
id: 0,
event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
assert_eq!(
dev.control_out.len(),
after_first,
"PORT_READY repeat for the same port must be a no-op — \
control_out length must remain at 3, otherwise a hostile \
guest can re-enqueue announce frames unboundedly",
);
}
#[test]
fn handle_port_ready_repeat_ignored_port1() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 1,
event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
assert!(dev.ports[1].readied);
let after_first = dev.control_out.len();
assert_eq!(
after_first, 2,
"first PORT_READY for port 1 must enqueue 2 frames (PORT_NAME, PORT_OPEN)",
);
dev.handle_control_event(VirtioConsoleControl {
id: 1,
event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
assert_eq!(
dev.control_out.len(),
after_first,
"PORT_READY repeat for port 1 must be a no-op",
);
}
#[test]
fn handle_port_ready_per_port_not_global() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 0,
event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
let after_port0 = dev.control_out.len();
assert_eq!(after_port0, 3, "port 0 announce: 3 frames");
dev.handle_control_event(VirtioConsoleControl {
id: 1,
event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
assert_eq!(
dev.control_out.len(),
5,
"PORT_READY for port 1 after PORT_READY for port 0 must \
enqueue port 1's announce frames — the gate is per-port",
);
assert!(dev.ports[0].readied);
assert!(dev.ports[1].readied);
}
#[test]
fn handle_port_ready_value_zero_skipped() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 0,
event: VIRTIO_CONSOLE_PORT_READY,
value: 0,
});
assert!(
dev.control_out.is_empty(),
"PORT_READY value=0 must NOT enqueue announce frames",
);
assert!(
!dev.ports[0].readied,
"PORT_READY value=0 must NOT set port_readied — the kernel \
may legitimately retry with value=1 after the host fixes \
the underlying issue",
);
}
#[test]
fn handle_port_ready_value_zero_then_one_completes() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 0,
event: VIRTIO_CONSOLE_PORT_READY,
value: 0,
});
assert!(dev.control_out.is_empty());
assert!(!dev.ports[0].readied);
dev.handle_control_event(VirtioConsoleControl {
id: 0,
event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
assert!(dev.ports[0].readied);
assert_eq!(
dev.control_out.len(),
3,
"PORT_READY value=1 after value=0 must enqueue the announce \
— the value=0 path must not poison the per-port gate",
);
}
#[test]
fn handle_port_ready_unknown_port_state_unchanged() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: NUM_PORTS, event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
assert!(dev.control_out.is_empty());
for p in &dev.ports {
assert!(
!p.readied,
"unknown-port PORT_READY must not flip any port readied flag",
);
}
}
#[test]
fn handle_port_open_unknown_port_ignored() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: NUM_PORTS,
event: VIRTIO_CONSOLE_PORT_OPEN,
value: 1,
});
dev.handle_control_event(VirtioConsoleControl {
id: 0xFFFF_FFFF,
event: VIRTIO_CONSOLE_PORT_OPEN,
value: 1,
});
for p in &dev.ports {
assert!(
!p.opened,
"unknown-port PORT_OPEN must not flip any port opened \
flag — the gate prevents out-of-bounds array access on \
a hostile id",
);
}
}
#[test]
fn handle_unhandled_event_absorbed() {
let mut dev = VirtioConsole::new();
let unhandled = [
VIRTIO_CONSOLE_PORT_ADD,
VIRTIO_CONSOLE_PORT_REMOVE,
VIRTIO_CONSOLE_CONSOLE_PORT,
VIRTIO_CONSOLE_RESIZE,
VIRTIO_CONSOLE_PORT_NAME,
0xBEEF,
];
for ev in unhandled {
dev.handle_control_event(VirtioConsoleControl {
id: 0,
event: ev,
value: 1,
});
}
assert!(
dev.control_out.is_empty(),
"unhandled events must NOT enqueue control_out",
);
assert!(
!dev.device_ready,
"unhandled events must NOT flip device_ready",
);
for p in &dev.ports {
assert!(
!p.opened,
"unhandled events must NOT flip any port opened flag"
);
assert!(
!p.readied,
"unhandled events must NOT flip any port readied flag"
);
}
}
#[test]
fn set_status_clear_acknowledge_rejected() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
assert_eq!(dev.device_status, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, VIRTIO_CONFIG_S_DRIVER);
assert_eq!(
dev.device_status, S_ACK,
"writing a value that clears ACKNOWLEDGE must be rejected — \
monotone gate (virtio-v1.2 §3.1.1)",
);
}
#[test]
fn set_status_clear_driver_keeps_ack_rejected() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
assert_eq!(dev.device_status, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
assert_eq!(
dev.device_status, S_DRV,
"writing ACK alone after advancing to DRIVER must be \
rejected — clears DRIVER bit",
);
}
#[test]
fn set_status_clear_driver_ok_rejected() {
let mut dev = VirtioConsole::new();
init_device(&mut dev);
assert_eq!(dev.device_status, S_OK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
assert_eq!(
dev.device_status, S_OK,
"writing S_FEAT after S_OK must be rejected — \
clears DRIVER_OK",
);
}
#[test]
fn set_status_idempotent_same_value_no_change() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
assert_eq!(dev.device_status, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
assert_eq!(
dev.device_status, S_ACK,
"re-writing the same status must leave device_status \
unchanged — no advance, no regression",
);
}
#[test]
fn set_status_two_bits_at_once_rejected() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_OK);
assert_eq!(
dev.device_status, S_DRV,
"advancing two FSM bits at once (FEATURES_OK + DRIVER_OK) \
must be rejected — the FSM advances one bit at a time",
);
}
#[test]
fn set_status_unknown_bit_rejected() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
assert_eq!(dev.device_status, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK | 0x10);
assert_eq!(
dev.device_status, S_ACK,
"writing a status with an unrecognised bit (0x10) must \
be rejected — only the defined ACK/DRIVER/FEATURES_OK/\
DRIVER_OK/FAILED transitions are accepted",
);
}
#[test]
fn set_status_failed_accepted_at_every_fsm_state() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_STATUS, VIRTIO_CONFIG_S_FAILED);
assert_eq!(
dev.device_status, VIRTIO_CONFIG_S_FAILED,
"FAILED from status=0 must be accepted",
);
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK | VIRTIO_CONFIG_S_FAILED);
assert_eq!(
dev.device_status,
S_ACK | VIRTIO_CONFIG_S_FAILED,
"FAILED from status=ACK must be accepted (ACK preserved)",
);
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV | VIRTIO_CONFIG_S_FAILED);
assert_eq!(
dev.device_status,
S_DRV | VIRTIO_CONFIG_S_FAILED,
"FAILED from status=DRV must be accepted (DRV preserved)",
);
let mut dev = VirtioConsole::new();
init_device(&mut dev);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_OK | VIRTIO_CONFIG_S_FAILED);
assert_eq!(
dev.device_status,
S_OK | VIRTIO_CONFIG_S_FAILED,
"FAILED from status=S_OK must be accepted (S_OK preserved)",
);
}
#[test]
fn set_status_failed_plus_unknown_bit_rejected() {
let mut dev = VirtioConsole::new();
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(
&mut dev,
VIRTIO_MMIO_STATUS,
S_ACK | VIRTIO_CONFIG_S_FAILED | 0x10,
);
assert_eq!(
dev.device_status, S_ACK,
"FAILED combined with a non-FAILED unknown bit must be \
rejected — the early-accept is gated on FAILED alone",
);
}
#[test]
fn mmio_read_oversized_fills_0xff() {
let dev = VirtioConsole::new();
let mut buf = [0u8; 8];
dev.mmio_read(VIRTIO_MMIO_VERSION as u64, &mut buf);
assert_eq!(
buf, [0xff; 8],
"8-byte read must fill with 0xff — the device is 4-byte \
register width per virtio-v1.2 §4.2.2",
);
}
#[test]
fn mmio_read_1byte_fills_0xff() {
let dev = VirtioConsole::new();
let mut buf = [0u8; 1];
dev.mmio_read(VIRTIO_MMIO_VERSION as u64, &mut buf);
assert_eq!(buf, [0xff], "1-byte read must fill with 0xff",);
}
#[test]
fn mmio_read_3byte_fills_0xff() {
let dev = VirtioConsole::new();
let mut buf = [0u8; 3];
dev.mmio_read(VIRTIO_MMIO_VERSION as u64, &mut buf);
assert_eq!(
buf,
[0xff, 0xff, 0xff],
"3-byte read must fill with 0xff — the device is exactly \
4-byte register width, not 'at least 4'",
);
}
#[test]
fn mmio_read_4byte_returns_register_value() {
let dev = VirtioConsole::new();
let mut buf = [0u8; 4];
dev.mmio_read(VIRTIO_MMIO_VERSION as u64, &mut buf);
assert_eq!(
u32::from_le_bytes(buf),
MMIO_VERSION,
"4-byte read at VIRTIO_MMIO_VERSION must return the \
actual register value, NOT the 0xff-fill defense",
);
}
#[test]
fn config_read_out_of_range_fills_0xff() {
let dev = VirtioConsole::new();
let mut buf = [0u8; 4];
dev.mmio_read(0x100 + 10, &mut buf);
assert_eq!(
buf, [0xff; 4],
"config_read past byte 11 must fill 0xff — the defense \
prevents a panic from cfg[start..end] when end > 12",
);
}
#[test]
fn config_read_one_byte_past_end_fills_0xff() {
let dev = VirtioConsole::new();
let mut buf = [0u8; 4];
dev.mmio_read(0x100 + 9, &mut buf);
assert_eq!(
buf, [0xff; 4],
"config_read with end one byte past struct must fill 0xff",
);
}
#[test]
fn config_read_at_exact_end_returns_data() {
let dev = VirtioConsole::new();
let mut buf = [0u8; 4];
dev.mmio_read(0x100 + 8, &mut buf);
assert_eq!(
buf, [0; 4],
"read at the last 4-byte slot of the config struct \
(emerg_wr, offset 8..12) must return actual data, NOT \
the 0xff out-of-range fill",
);
}
#[test]
fn config_read_1byte_inside_struct_returns_data() {
let dev = VirtioConsole::new();
let mut buf = [0u8; 1];
dev.mmio_read(0x100, &mut buf);
assert_eq!(
buf,
[0],
"1-byte read inside config struct must return actual \
data, not 0xff fill",
);
}
fn wire_port1_rxq_to_mock(dev: &mut VirtioConsole, mock: &MockSplitQueue<GuestMemoryMmap>) {
wire_console_queue_to_mock(dev, mock, PORT1_RXQ as u32);
}
fn open_port1(dev: &mut VirtioConsole) {
dev.handle_control_event(VirtioConsoleControl {
id: 1,
event: VIRTIO_CONSOLE_PORT_OPEN,
value: 1,
});
assert!(
dev.ports[1].opened,
"open_port1 helper precondition: PORT_OPEN(value=1) must \
set port_opened[1]"
);
}
#[test]
fn drain_port1_pending_rx_empty_pending_is_noop() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
dev.set_mem(mem.clone());
wire_port1_rxq_to_mock(&mut dev, &mock);
open_port1(&mut dev);
let data_addr = GuestAddress(0x10000);
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
64,
VRING_DESC_F_WRITE as u16,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
assert!(
dev.ports[1].pending_rx.is_empty(),
"precondition: port1_pending_rx must start empty"
);
let int_before = dev.interrupt_status;
dev.drain_pending_rx(1);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(
used_idx, 0,
"empty-pending fast-exit must not touch the queue"
);
assert_eq!(
dev.interrupt_status, int_before,
"empty-pending fast-exit must not call signal_used"
);
}
#[test]
fn drain_port1_pending_rx_defers_without_driver_ok() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
64,
VRING_DESC_F_WRITE as u16,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << VIRTIO_CONSOLE_F_MULTIPORT,
);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, PORT1_RXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NUM, 16);
let desc = mock.desc_table_addr().0;
let avail = mock.avail_addr().0;
let used = mock.used_addr().0;
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, desc as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_HIGH, (desc >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_LOW, avail as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, (avail >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_LOW, used as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_HIGH, (used >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_READY, 1);
dev.ports[1].opened = true;
let payload = b"snapshot reply bytes";
dev.ports[1].pending_rx.extend(payload.iter().copied());
dev.drain_pending_rx(1);
assert_eq!(
dev.ports[1].pending_rx.len(),
payload.len(),
"DRIVER_OK gate must hold bytes in pending_rx"
);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(used_idx, 0, "DRIVER_OK gate must skip add_used");
}
#[test]
fn drain_port1_pending_rx_defers_without_multiport() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
64,
VRING_DESC_F_WRITE as u16,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, PORT1_RXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NUM, 16);
let desc = mock.desc_table_addr().0;
let avail = mock.avail_addr().0;
let used = mock.used_addr().0;
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, desc as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_HIGH, (desc >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_LOW, avail as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, (avail >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_LOW, used as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_HIGH, (used >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_READY, 1);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_OK);
assert_eq!(
dev.device_status, S_OK,
"precondition: FSM must reach DRIVER_OK"
);
assert!(
!dev.multiport_negotiated(),
"precondition: F_MULTIPORT must NOT be negotiated"
);
dev.ports[1].opened = true;
let payload = b"reply bytes that must not leak";
dev.ports[1].pending_rx.extend(payload.iter().copied());
dev.drain_pending_rx(1);
assert_eq!(
dev.ports[1].pending_rx.len(),
payload.len(),
"F_MULTIPORT gate must hold bytes in pending_rx"
);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(used_idx, 0, "F_MULTIPORT gate must skip add_used");
}
#[test]
fn drain_port1_pending_rx_defers_until_port_open() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let payload = b"deferred snapshot reply";
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
64,
VRING_DESC_F_WRITE as u16,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_port1_rxq_to_mock(&mut dev, &mock);
assert!(
!dev.ports[1].opened,
"precondition: port_opened[1] must be false"
);
dev.ports[1].pending_rx.extend(payload.iter().copied());
dev.drain_pending_rx(1);
assert_eq!(
dev.ports[1].pending_rx.len(),
payload.len(),
"port_opened[1] gate must defer when guest has not opened port 1"
);
let used_idx_before: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx before open");
assert_eq!(used_idx_before, 0, "port_opened[1] gate must skip add_used");
open_port1(&mut dev);
assert!(
dev.ports[1].pending_rx.is_empty(),
"after PORT_OPEN, deferred bytes must drain"
);
let used_idx_after: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx after open");
assert_eq!(
used_idx_after, 1,
"after PORT_OPEN, the deferred chain must add_used"
);
let mut readback = vec![0u8; payload.len()];
mem.read_slice(&mut readback, data_addr)
.expect("read back delivered payload");
assert_eq!(
readback, payload,
"delivered bytes must match the queued payload verbatim"
);
}
#[test]
fn drain_port1_pending_rx_defers_without_mem() {
let mut dev = VirtioConsole::new();
init_device(&mut dev);
assert!(dev.mem.is_none(), "precondition: mem must be None");
dev.ports[1].opened = true;
let payload = b"reply bytes pre-set_mem";
dev.ports[1].pending_rx.extend(payload.iter().copied());
dev.drain_pending_rx(1);
assert_eq!(
dev.ports[1].pending_rx.len(),
payload.len(),
"no-mem gate must hold bytes in pending_rx"
);
}
#[test]
fn drain_port1_pending_rx_defers_when_queue_not_ready() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
dev.set_mem(mem);
init_device(&mut dev);
assert!(
!dev.queues[PORT1_RXQ].ready(),
"precondition: q4 must NOT be ready"
);
dev.ports[1].opened = true;
let payload = b"reply bytes before queue ready";
dev.ports[1].pending_rx.extend(payload.iter().copied());
dev.drain_pending_rx(1);
assert_eq!(
dev.ports[1].pending_rx.len(),
payload.len(),
"queue-not-ready gate must hold bytes in pending_rx"
);
}
#[test]
fn drain_port1_pending_rx_single_descriptor_happy_path() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let payload = b"snapshot reply payload bytes";
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
payload.len() as u32,
VRING_DESC_F_WRITE as u16,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_port1_rxq_to_mock(&mut dev, &mock);
open_port1(&mut dev);
dev.ports[1].pending_rx.extend(payload.iter().copied());
dev.drain_pending_rx(1);
assert!(
dev.ports[1].pending_rx.is_empty(),
"happy-path drain must consume all pending bytes"
);
let mut readback = vec![0u8; payload.len()];
mem.read_slice(&mut readback, data_addr)
.expect("read back delivered payload");
assert_eq!(
readback, payload,
"delivered bytes must equal the queued payload"
);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(used_idx, 1, "happy-path drain must add_used exactly once");
assert_ne!(
dev.interrupt_status & VIRTIO_MMIO_INT_VRING,
0,
"INT_VRING must be set after a non-zero drain"
);
let irq_count = dev.irq_evt.read().expect("irq_evt was written");
assert!(
irq_count > 0,
"irq_evt counter must be non-zero after signal_used"
);
}
#[test]
fn drain_port1_pending_rx_torn_write_publishes_head_with_zero_len() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let valid_addr = GuestAddress(0x10000);
let unmapped_addr = GuestAddress(4 << 20);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
valid_addr.0,
32,
(VRING_DESC_F_WRITE | VRING_DESC_F_NEXT) as u16,
1,
)),
RawDescriptor::from(SplitDescriptor::new(
unmapped_addr.0,
32,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build torn chain");
dev.set_mem(mem.clone());
wire_port1_rxq_to_mock(&mut dev, &mock);
open_port1(&mut dev);
let payload: Vec<u8> = (0..64u8).collect();
dev.ports[1].pending_rx.extend(payload.iter().copied());
let int_before = dev.interrupt_status;
let _ = dev.irq_evt.read();
dev.drain_pending_rx(1);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(
used_idx, 1,
"torn-write recovery must add_used the chain head (with len=0)"
);
let used_elem_len: u32 = mem
.read_obj(mock.used_addr().checked_add(8).unwrap())
.expect("read used elem 0 len");
assert_eq!(
used_elem_len, 0,
"torn-write recovery must publish len=0 for the chain head \
— a non-zero len would tell the guest the descriptor was \
fully filled, leading to data corruption"
);
assert_eq!(
dev.ports[1].pending_rx.len(),
payload.len(),
"torn-write recovery must preserve bytes in pending_rx \
for retry on the next drain cycle"
);
let preserved: Vec<u8> = dev.ports[1].pending_rx.iter().copied().collect();
assert_eq!(
preserved, payload,
"preserved bytes must be the original payload verbatim"
);
assert_eq!(
dev.interrupt_status, int_before,
"torn-only chain must not trigger signal_used (total_written=0)"
);
match dev.irq_evt.read() {
Ok(n) => panic!("irq_evt must NOT have been written, got {n}"),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("unexpected irq_evt read error: {e}"),
}
}
#[test]
fn drain_port1_pending_rx_torn_write_breaks_drain_loop() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let valid_addr_a = GuestAddress(0x10000);
let unmapped_addr = GuestAddress(4 << 20);
let valid_addr_b = GuestAddress(0x20000);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
valid_addr_a.0,
32,
(VRING_DESC_F_WRITE | VRING_DESC_F_NEXT) as u16,
1,
)),
RawDescriptor::from(SplitDescriptor::new(
unmapped_addr.0,
32,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
valid_addr_b.0,
32,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.add_desc_chains(&descs, 0).expect("publish two chains");
dev.set_mem(mem.clone());
wire_port1_rxq_to_mock(&mut dev, &mock);
open_port1(&mut dev);
let payload: Vec<u8> = (0..64u8).collect();
dev.ports[1].pending_rx.extend(payload.iter().copied());
dev.drain_pending_rx(1);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(
used_idx, 1,
"torn-write recovery must break the drain loop — chain 1 \
must remain unconsumed even though its descriptor is valid"
);
let mut readback_b = vec![0u8; 32];
mem.read_slice(&mut readback_b, valid_addr_b)
.expect("read back chain 1 data region");
assert!(
readback_b.iter().all(|&b| b == 0),
"chain 1's data region must be untouched — the drain loop \
must NOT have reached chain 1 after the torn break"
);
assert_eq!(
dev.ports[1].pending_rx.len(),
payload.len(),
"all bytes must remain in pending_rx (torn chain consumed nothing)"
);
dev.drain_pending_rx(1);
let used_idx_after: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx after second drain");
assert_eq!(
used_idx_after, 2,
"second drain must process chain 1 (used.idx 1 -> 2)"
);
let mut readback_b2 = vec![0u8; 32];
mem.read_slice(&mut readback_b2, valid_addr_b)
.expect("read chain 1 data after second drain");
assert_eq!(
readback_b2,
payload[..32],
"chain 1 must hold the first 32 bytes of the preserved payload"
);
assert_eq!(
dev.ports[1].pending_rx.len(),
payload.len() - 32,
"second drain must consume only chain 1's capacity (32 bytes)"
);
}
#[test]
fn drain_port1_pending_rx_torn_after_success_still_signals() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let valid_addr_a = GuestAddress(0x10000);
let valid_addr_b = GuestAddress(0x20000);
let unmapped_addr = GuestAddress(4 << 20);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
valid_addr_a.0,
32,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
valid_addr_b.0,
32,
(VRING_DESC_F_WRITE | VRING_DESC_F_NEXT) as u16,
2,
)),
RawDescriptor::from(SplitDescriptor::new(
unmapped_addr.0,
32,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.add_desc_chains(&descs, 0)
.expect("publish success+torn chains");
dev.set_mem(mem.clone());
wire_port1_rxq_to_mock(&mut dev, &mock);
open_port1(&mut dev);
let payload: Vec<u8> = (0..96u8).collect();
dev.ports[1].pending_rx.extend(payload.iter().copied());
let _ = dev.irq_evt.read();
dev.drain_pending_rx(1);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(
used_idx, 2,
"drain must add_used both chain 0 (len=32) and chain 1 \
(torn, len=0)"
);
assert_ne!(
dev.interrupt_status & VIRTIO_MMIO_INT_VRING,
0,
"signal_used must have been called because chain 0 \
delivered 32 bytes"
);
let irq_count = dev.irq_evt.read().expect("irq_evt was written");
assert!(
irq_count > 0,
"irq_evt counter must be non-zero after signal_used"
);
let mut readback_a = vec![0u8; 32];
mem.read_slice(&mut readback_a, valid_addr_a)
.expect("read chain 0 data");
assert_eq!(
readback_a,
payload[..32],
"chain 0 must hold the first 32 bytes of the payload"
);
assert_eq!(
dev.ports[1].pending_rx.len(),
payload.len() - 32,
"only chain 0's bytes were consumed from pending_rx"
);
let preserved: Vec<u8> = dev.ports[1].pending_rx.iter().copied().collect();
assert_eq!(
preserved,
payload[32..],
"preserved bytes must be exactly the suffix not delivered \
to chain 0 (chain 1's first descriptor's partial write \
does NOT consume from the deque)"
);
}
fn open_port2(dev: &mut VirtioConsole) {
dev.handle_control_event(VirtioConsoleControl {
id: 2,
event: VIRTIO_CONSOLE_PORT_OPEN,
value: 1,
});
assert!(
dev.ports[2].opened,
"open_port2 helper precondition: PORT_OPEN(value=1) must \
set port_opened[2]"
);
}
fn wire_port2_rxq_to_mock(dev: &mut VirtioConsole, mock: &MockSplitQueue<GuestMemoryMmap>) {
wire_console_queue_to_mock(dev, mock, PORT2_RXQ as u32);
}
#[test]
fn port2_tx_single_descriptor_lands_in_port2_buf() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let payload = b"port2 stats relay TX bytes";
mem.write_slice(payload, data_addr).expect("plant payload");
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
payload.len() as u32,
0,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, PORT2_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT2_TXQ as u32);
let drained = dev.drain_port2_bulk();
assert_eq!(
drained,
payload.to_vec(),
"port 2 TX must deliver the descriptor's bytes to \
drain_port2_bulk verbatim",
);
assert!(
dev.drain_output().is_empty(),
"port 0 TX buffer must remain empty when only port 2 was notified",
);
assert!(
dev.drain_bulk().is_empty(),
"port 1 TX buffer must remain empty when only port 2 was notified",
);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(used_idx, 1, "exactly one used-ring entry expected");
assert_ne!(
dev.interrupt_status & VIRTIO_MMIO_INT_VRING,
0,
"INT_VRING must be set after a successful TX drain",
);
let irq_count = dev.irq_evt.read().expect("irq_evt was written");
assert!(
irq_count > 0,
"irq_evt counter must be non-zero after signal_used",
);
}
#[test]
fn port2_tx_multi_descriptor_chain_concatenates() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
const PAGE: u32 = 4096;
let segs: [(GuestAddress, u8); 4] = [
(GuestAddress(0x10000), 0xB1),
(GuestAddress(0x12000), 0xB2),
(GuestAddress(0x14000), 0xB3),
(GuestAddress(0x16000), 0xB4),
];
for (addr, fill) in &segs {
let buf = vec![*fill; PAGE as usize];
mem.write_slice(&buf, *addr).expect("plant segment");
}
let descs = [
RawDescriptor::from(SplitDescriptor::new(segs[0].0.0, PAGE, 0, 0)),
RawDescriptor::from(SplitDescriptor::new(segs[1].0.0, PAGE, 0, 0)),
RawDescriptor::from(SplitDescriptor::new(segs[2].0.0, PAGE, 0, 0)),
RawDescriptor::from(SplitDescriptor::new(segs[3].0.0, PAGE, 0, 0)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, PORT2_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT2_TXQ as u32);
let drained = dev.drain_port2_bulk();
assert_eq!(
drained.len(),
4 * PAGE as usize,
"drain_port2_bulk length must equal sum of segment lengths",
);
for (i, (_, fill)) in segs.iter().enumerate() {
let start = i * PAGE as usize;
let end = start + PAGE as usize;
assert!(
drained[start..end].iter().all(|&b| b == *fill),
"segment {i} must hold fill {fill:#x} verbatim — chain order \
or per-descriptor append regressed",
);
}
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(used_idx, 1, "one chain → one used-ring entry on port 2 TX",);
}
#[test]
fn port2_tx_oversize_descriptor_truncates_to_tx_desc_max() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
const OVERSIZE: usize = TX_DESC_MAX * 2;
let mut payload = vec![0x55u8; TX_DESC_MAX];
payload.extend_from_slice(&vec![0x99u8; TX_DESC_MAX]);
assert_eq!(payload.len(), OVERSIZE);
mem.write_slice(&payload, data_addr).expect("plant payload");
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
OVERSIZE as u32,
0,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, PORT2_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT2_TXQ as u32);
let drained = dev.drain_port2_bulk();
assert_eq!(
drained.len(),
TX_DESC_MAX,
"oversize port-2 descriptor must truncate to TX_DESC_MAX",
);
assert!(
drained.iter().all(|&b| b == 0x55),
"truncated bytes must be the FIRST TX_DESC_MAX bytes \
(0x55), not anything past the cap",
);
}
#[test]
fn port2_tx_rejected_without_driver_ok() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let payload = b"this must NOT reach port2_tx_buf";
mem.write_slice(payload, data_addr).expect("plant payload");
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
payload.len() as u32,
0,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << VIRTIO_CONSOLE_F_MULTIPORT,
);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, PORT2_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NUM, 16);
let desc = mock.desc_table_addr().0;
let avail = mock.avail_addr().0;
let used = mock.used_addr().0;
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, desc as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_HIGH, (desc >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_LOW, avail as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, (avail >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_LOW, used as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_HIGH, (used >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_READY, 1);
assert_eq!(
dev.device_status & VIRTIO_CONFIG_S_DRIVER_OK,
0,
"precondition: DRIVER_OK must NOT be set",
);
assert!(
dev.queues[PORT2_TXQ].ready(),
"precondition: port 2 TX queue must be ready",
);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT2_TXQ as u32);
assert!(
dev.drain_port2_bulk().is_empty(),
"port2_tx_buf must remain empty — DRIVER_OK gate must \
reject pre-DRIVER_OK notify",
);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(
used_idx, 0,
"used.idx must be 0 — DRIVER_OK gate must skip add_used",
);
assert_eq!(
dev.interrupt_status, 0,
"interrupt_status must be 0 — DRIVER_OK gate must skip signal_used",
);
}
#[test]
fn port2_tx_rejected_without_multiport() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let payload = b"port2 bytes that must not leak without multiport";
mem.write_slice(payload, data_addr).expect("plant payload");
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
payload.len() as u32,
0,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, PORT2_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NUM, 16);
let desc = mock.desc_table_addr().0;
let avail = mock.avail_addr().0;
let used = mock.used_addr().0;
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, desc as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_HIGH, (desc >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_LOW, avail as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, (avail >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_LOW, used as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_HIGH, (used >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_READY, 1);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_OK);
assert_eq!(
dev.device_status, S_OK,
"precondition: FSM must reach DRIVER_OK"
);
assert!(
!dev.multiport_negotiated(),
"precondition: F_MULTIPORT must NOT be negotiated"
);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT2_TXQ as u32);
assert!(
dev.drain_port2_bulk().is_empty(),
"port2_tx_buf must remain empty — F_MULTIPORT gate must \
reject the notify",
);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(
used_idx, 0,
"used.idx must be 0 — F_MULTIPORT gate must skip add_used",
);
}
#[test]
fn port0_vs_port1_vs_port2_tx_routes_to_correct_buffer() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock0 = MockSplitQueue::create(&mem, GuestAddress(0x0), 16);
let mock1 = MockSplitQueue::create(&mem, GuestAddress(0x1000), 16);
let mock2 = MockSplitQueue::create(&mem, GuestAddress(0x2000), 16);
let port0_data_addr = GuestAddress(0x10000);
let port1_data_addr = GuestAddress(0x20000);
let port2_data_addr = GuestAddress(0x30000);
let port0_payload = b"port0 console bytes";
let port1_payload = b"port1 bulk TLV bytes";
let port2_payload = b"port2 stats relay bytes";
mem.write_slice(port0_payload, port0_data_addr)
.expect("plant port0 payload");
mem.write_slice(port1_payload, port1_data_addr)
.expect("plant port1 payload");
mem.write_slice(port2_payload, port2_data_addr)
.expect("plant port2 payload");
let port0_descs = [RawDescriptor::from(SplitDescriptor::new(
port0_data_addr.0,
port0_payload.len() as u32,
0,
0,
))];
let port1_descs = [RawDescriptor::from(SplitDescriptor::new(
port1_data_addr.0,
port1_payload.len() as u32,
0,
0,
))];
let port2_descs = [RawDescriptor::from(SplitDescriptor::new(
port2_data_addr.0,
port2_payload.len() as u32,
0,
0,
))];
mock0
.build_desc_chain(&port0_descs)
.expect("build port0 chain");
mock1
.build_desc_chain(&port1_descs)
.expect("build port1 chain");
mock2
.build_desc_chain(&port2_descs)
.expect("build port2 chain");
dev.set_mem(mem.clone());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 0);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << VIRTIO_CONSOLE_F_MULTIPORT,
);
write_reg(&mut dev, VIRTIO_MMIO_DRIVER_FEATURES_SEL, 1);
write_reg(
&mut dev,
VIRTIO_MMIO_DRIVER_FEATURES,
1 << (VIRTIO_F_VERSION_1 - 32),
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
for (q_idx, mock_ref) in [
(PORT0_TXQ, &mock0),
(PORT1_TXQ, &mock1),
(PORT2_TXQ, &mock2),
] {
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, q_idx as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NUM, 16);
let d = mock_ref.desc_table_addr().0;
let a = mock_ref.avail_addr().0;
let u = mock_ref.used_addr().0;
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, d as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_HIGH, (d >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_LOW, a as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_AVAIL_HIGH, (a >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_LOW, u as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_USED_HIGH, (u >> 32) as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_READY, 1);
}
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_OK);
assert_eq!(
dev.device_status, S_OK,
"FSM did not reach DRIVER_OK after all three queues configured",
);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT0_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT2_TXQ as u32);
let port0_drained = dev.drain_output();
assert_eq!(
port0_drained,
port0_payload.to_vec(),
"port 0 TX must route to port0_tx_buf",
);
let port1_drained = dev.drain_bulk();
assert_eq!(
port1_drained,
port1_payload.to_vec(),
"port 1 TX must route to port1_tx_buf",
);
let port2_drained = dev.drain_port2_bulk();
assert_eq!(
port2_drained,
port2_payload.to_vec(),
"port 2 TX must route to port2_tx_buf",
);
let port0_used_idx: u16 = mem
.read_obj(mock0.used_addr().checked_add(2).unwrap())
.expect("read port0 used.idx");
let port1_used_idx: u16 = mem
.read_obj(mock1.used_addr().checked_add(2).unwrap())
.expect("read port1 used.idx");
let port2_used_idx: u16 = mem
.read_obj(mock2.used_addr().checked_add(2).unwrap())
.expect("read port2 used.idx");
assert_eq!(port0_used_idx, 1);
assert_eq!(port1_used_idx, 1);
assert_eq!(
port2_used_idx, 1,
"port 2 used.idx must reflect 1 completion"
);
}
#[test]
fn port2_tx_wakes_stats_tx_evt_only() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let payload = b"port2 tx wakes stats_tx_evt";
mem.write_slice(payload, data_addr).expect("plant payload");
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
payload.len() as u32,
0,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, PORT2_TXQ as u32);
let _ = dev.tx_evt.read();
let _ = dev.stats_tx_evt.read();
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT2_TXQ as u32);
let stats_count = dev
.stats_tx_evt
.read()
.expect("stats_tx_evt was written by port-2 TX");
assert!(
stats_count > 0,
"port 2 TX must wake stats_tx_evt (count > 0)",
);
match dev.tx_evt.read() {
Ok(n) => {
panic!("tx_evt must NOT have been written by port-2 TX, but read returned {n}")
}
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("unexpected tx_evt read error: {e}"),
}
}
#[test]
fn port1_tx_wakes_tx_evt_only_not_stats_tx_evt() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let payload = b"port1 tx wakes tx_evt only";
mem.write_slice(payload, data_addr).expect("plant payload");
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
payload.len() as u32,
0,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_console_queue_to_mock(&mut dev, &mock, PORT1_TXQ as u32);
let _ = dev.tx_evt.read();
let _ = dev.stats_tx_evt.read();
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, PORT1_TXQ as u32);
let tx_count = dev.tx_evt.read().expect("tx_evt was written by port-1 TX");
assert!(tx_count > 0, "port 1 TX must wake tx_evt");
match dev.stats_tx_evt.read() {
Ok(n) => panic!(
"stats_tx_evt must NOT have been written by port-1 TX, but read returned {n}"
),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("unexpected stats_tx_evt read error: {e}"),
}
}
#[test]
fn three_eventfds_are_distinct_fds() {
let dev = VirtioConsole::new();
let irq_fd = dev.irq_evt().as_raw_fd();
let tx_fd = dev.tx_evt().as_raw_fd();
let stats_fd = dev.stats_tx_evt().as_raw_fd();
assert!(irq_fd >= 0, "irq_evt fd must be valid");
assert!(tx_fd >= 0, "tx_evt fd must be valid");
assert!(stats_fd >= 0, "stats_tx_evt fd must be valid");
assert_ne!(irq_fd, tx_fd, "irq_evt and tx_evt must be distinct fds");
assert_ne!(
irq_fd, stats_fd,
"irq_evt and stats_tx_evt must be distinct fds"
);
assert_ne!(
tx_fd, stats_fd,
"tx_evt and stats_tx_evt must be distinct fds — \
aliasing them would defeat the per-port wake \
separation",
);
}
#[test]
fn handle_device_ready_enqueues_port_add_for_port_2() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 0xFFFF_FFFF,
event: VIRTIO_CONSOLE_DEVICE_READY,
value: 1,
});
assert_eq!(dev.control_out.len(), NUM_PORTS as usize);
let port2_add = dev.control_out.iter().find(|m| match m {
ControlOut::Cmd(c) => c.id == 2 && c.event == VIRTIO_CONSOLE_PORT_ADD,
_ => false,
});
match port2_add {
Some(ControlOut::Cmd(c)) => {
let value = c.value;
assert_eq!(
value, 1,
"PORT_ADD value=1 matches QEMU \
(hw/char/virtio-serial-bus.c)",
);
}
Some(_) => panic!("PORT_ADD for id=2 must be a Cmd variant"),
None => panic!(
"DEVICE_READY must enqueue PORT_ADD for id=2 (port 2 is \
the scheduler-stats relay; missing it strands the port \
in the kernel's port-add-pending state)"
),
}
}
#[test]
fn handle_port_ready_port2_name_then_open() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 2,
event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
assert!(
dev.ports[2].readied,
"port_readied[2] must be set after PORT_READY for id=2"
);
assert_eq!(
dev.control_out.len(),
2,
"PORT_READY for id=2 must enqueue exactly 2 frames \
(PORT_NAME, PORT_OPEN) — same shape as port 1, distinct \
from port 0's 3-frame announce",
);
match &dev.control_out[0] {
ControlOut::Name { id, name } => {
assert_eq!(*id, 2);
assert_eq!(
*name, PORT2_NAME,
"PORT_NAME for id=2 must use PORT2_NAME (not \
PORT1_NAME or PORT0_NAME) — wrong name strands \
udev rules that disambiguate ports by name",
);
}
_ => panic!("first control_out frame must be Name for port 2"),
}
match &dev.control_out[1] {
ControlOut::Cmd(c) => {
let id = c.id;
let event = c.event;
let value = c.value;
assert_eq!(id, 2);
assert_eq!(event, VIRTIO_CONSOLE_PORT_OPEN);
assert_eq!(value, 1);
}
_ => panic!("second control_out frame must be Cmd for port 2 OPEN"),
}
}
#[test]
fn handle_port_open_tracks_state_for_port_2() {
let mut dev = VirtioConsole::new();
assert!(
!dev.ports[2].opened,
"precondition: port_opened[2] must start false"
);
dev.handle_control_event(VirtioConsoleControl {
id: 2,
event: VIRTIO_CONSOLE_PORT_OPEN,
value: 1,
});
assert!(
dev.ports[2].opened,
"PORT_OPEN(value=1) for id=2 must set port_opened[2]"
);
dev.handle_control_event(VirtioConsoleControl {
id: 2,
event: VIRTIO_CONSOLE_PORT_OPEN,
value: 0,
});
assert!(
!dev.ports[2].opened,
"PORT_OPEN(value=0) for id=2 must clear port_opened[2]"
);
}
#[test]
fn handle_port_ready_port2_value_zero_skipped() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 2,
event: VIRTIO_CONSOLE_PORT_READY,
value: 0,
});
assert!(
dev.control_out.is_empty(),
"PORT_READY(id=2, value=0) must NOT enqueue announce frames",
);
assert!(
!dev.ports[2].readied,
"PORT_READY(id=2, value=0) must NOT set port_readied[2] — \
a future legitimate value=1 must still complete",
);
}
#[test]
fn handle_port_ready_repeat_ignored_port2() {
let mut dev = VirtioConsole::new();
dev.handle_control_event(VirtioConsoleControl {
id: 2,
event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
assert!(dev.ports[2].readied);
let after_first = dev.control_out.len();
assert_eq!(
after_first, 2,
"first PORT_READY for port 2 must enqueue 2 frames (PORT_NAME, PORT_OPEN)",
);
dev.handle_control_event(VirtioConsoleControl {
id: 2,
event: VIRTIO_CONSOLE_PORT_READY,
value: 1,
});
assert_eq!(
dev.control_out.len(),
after_first,
"PORT_READY repeat for port 2 must be a no-op",
);
}
#[test]
fn reset_clears_port2_state_preserves_port2_tx_buf() {
let mut dev = VirtioConsole::new();
init_device(&mut dev);
dev.ports[2].tx_buf.extend(b"leftover2".iter().copied());
dev.ports[2]
.pending_rx
.extend(b"stale request bytes".iter().copied());
dev.ports[2].opened = true;
dev.ports[2].readied = true;
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
assert_eq!(
dev.ports[2].tx_buf.iter().copied().collect::<Vec<u8>>(),
b"leftover2",
"port2_tx_buf must survive reset (host-side capture buffer)",
);
assert!(
dev.ports[2].pending_rx.is_empty(),
"port2_pending_rx must be cleared on reset (no post-reset consumer)",
);
assert!(!dev.ports[2].opened, "port_opened[2] must reset to false",);
assert!(!dev.ports[2].readied, "port_readied[2] must reset to false",);
}
#[test]
fn drain_port2_pending_rx_empty_pending_is_noop() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
dev.set_mem(mem.clone());
wire_port2_rxq_to_mock(&mut dev, &mock);
open_port2(&mut dev);
let data_addr = GuestAddress(0x10000);
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
64,
VRING_DESC_F_WRITE as u16,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
assert!(
dev.ports[2].pending_rx.is_empty(),
"precondition: port2_pending_rx must start empty"
);
let int_before = dev.interrupt_status;
dev.drain_pending_rx(2);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(
used_idx, 0,
"empty-pending fast-exit must not touch the queue"
);
assert_eq!(
dev.interrupt_status, int_before,
"empty-pending fast-exit must not call signal_used"
);
}
#[test]
fn drain_port2_pending_rx_defers_until_port_open() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let payload = b"deferred scx_stats request";
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
64,
VRING_DESC_F_WRITE as u16,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_port2_rxq_to_mock(&mut dev, &mock);
assert!(
!dev.ports[2].opened,
"precondition: port_opened[2] must be false"
);
dev.ports[2].pending_rx.extend(payload.iter().copied());
dev.drain_pending_rx(2);
assert_eq!(
dev.ports[2].pending_rx.len(),
payload.len(),
"port_opened[2] gate must defer when guest has not opened port 2"
);
let used_idx_before: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx before open");
assert_eq!(used_idx_before, 0, "port_opened[2] gate must skip add_used");
open_port2(&mut dev);
assert!(
dev.ports[2].pending_rx.is_empty(),
"after PORT_OPEN(id=2), deferred bytes must drain"
);
let used_idx_after: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx after open");
assert_eq!(
used_idx_after, 1,
"after PORT_OPEN(id=2), the deferred chain must add_used"
);
let mut readback = vec![0u8; payload.len()];
mem.read_slice(&mut readback, data_addr)
.expect("read back delivered payload");
assert_eq!(
readback, payload,
"delivered bytes must match the queued payload verbatim"
);
}
#[test]
fn drain_port2_pending_rx_single_descriptor_happy_path() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let data_addr = GuestAddress(0x10000);
let payload = b"scx_stats request line\n";
let descs = [RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
payload.len() as u32,
VRING_DESC_F_WRITE as u16,
0,
))];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_port2_rxq_to_mock(&mut dev, &mock);
open_port2(&mut dev);
dev.ports[2].pending_rx.extend(payload.iter().copied());
dev.drain_pending_rx(2);
assert!(
dev.ports[2].pending_rx.is_empty(),
"happy-path drain must consume all pending bytes"
);
let mut readback = vec![0u8; payload.len()];
mem.read_slice(&mut readback, data_addr)
.expect("read back delivered payload");
assert_eq!(
readback, payload,
"delivered bytes must equal the queued payload"
);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(used_idx, 1, "happy-path drain must add_used exactly once");
assert_ne!(
dev.interrupt_status & VIRTIO_MMIO_INT_VRING,
0,
"INT_VRING must be set after a non-zero drain"
);
let irq_count = dev.irq_evt.read().expect("irq_evt was written");
assert!(
irq_count > 0,
"irq_evt counter must be non-zero after signal_used"
);
}
#[test]
fn drain_port2_pending_rx_torn_write_publishes_head_with_zero_len() {
let mut dev = VirtioConsole::new();
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let valid_addr = GuestAddress(0x10000);
let unmapped_addr = GuestAddress(4 << 20);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
valid_addr.0,
32,
(VRING_DESC_F_WRITE | VRING_DESC_F_NEXT) as u16,
1,
)),
RawDescriptor::from(SplitDescriptor::new(
unmapped_addr.0,
32,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build torn chain");
dev.set_mem(mem.clone());
wire_port2_rxq_to_mock(&mut dev, &mock);
open_port2(&mut dev);
let payload: Vec<u8> = (0..64u8).collect();
dev.ports[2].pending_rx.extend(payload.iter().copied());
let int_before = dev.interrupt_status;
let _ = dev.irq_evt.read();
dev.drain_pending_rx(2);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(
used_idx, 1,
"torn-write recovery must add_used the chain head (with len=0)"
);
let used_elem_len: u32 = mem
.read_obj(mock.used_addr().checked_add(8).unwrap())
.expect("read used elem 0 len");
assert_eq!(
used_elem_len, 0,
"torn-write recovery must publish len=0 for the chain head",
);
assert_eq!(
dev.ports[2].pending_rx.len(),
payload.len(),
"torn-write recovery must preserve bytes in pending_rx"
);
let preserved: Vec<u8> = dev.ports[2].pending_rx.iter().copied().collect();
assert_eq!(
preserved, payload,
"preserved bytes must be the original payload verbatim"
);
assert_eq!(
dev.interrupt_status, int_before,
"torn-only chain must not trigger signal_used (total_written=0)"
);
match dev.irq_evt.read() {
Ok(n) => panic!("irq_evt must NOT have been written, got {n}"),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("unexpected irq_evt read error: {e}"),
}
}
}