use squib_core::{GuestAddress, GuestMemory};
use thiserror::Error;
pub const MAX_QUEUE_SIZE: u16 = 32768;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct QueueIndex(pub u16);
pub const VIRTQ_DESC_F_NEXT: u16 = 1;
pub const VIRTQ_DESC_F_WRITE: u16 = 2;
pub const VIRTQ_DESC_F_INDIRECT: u16 = 4;
#[derive(Debug, Clone)]
pub struct Queue {
pub max_size: u16,
pub size: u16,
pub ready: bool,
pub desc_table_addr: GuestAddress,
pub avail_ring_addr: GuestAddress,
pub used_ring_addr: GuestAddress,
pub next_avail_idx: u16,
pub next_used_idx: u16,
}
impl Queue {
#[must_use]
pub fn new(max_size: u16) -> Self {
Self {
max_size,
size: max_size,
ready: false,
desc_table_addr: GuestAddress(0),
avail_ring_addr: GuestAddress(0),
used_ring_addr: GuestAddress(0),
next_avail_idx: 0,
next_used_idx: 0,
}
}
pub fn set_size(&mut self, requested: u16) {
let clamped = requested.min(self.max_size);
self.size = if clamped == 0 {
1
} else if clamped.is_power_of_two() {
clamped
} else {
clamped
.checked_next_power_of_two()
.map_or(1u16 << 15, |p| (p >> 1).max(1))
};
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.ready
&& self.size > 0
&& self.size <= self.max_size
&& self.desc_table_addr.raw() != 0
&& self.avail_ring_addr.raw() != 0
&& self.used_ring_addr.raw() != 0
}
pub fn reset(&mut self) {
self.size = self.max_size;
self.ready = false;
self.desc_table_addr = GuestAddress(0);
self.avail_ring_addr = GuestAddress(0);
self.used_ring_addr = GuestAddress(0);
self.next_avail_idx = 0;
self.next_used_idx = 0;
}
fn avail_idx<M: GuestMemory + ?Sized>(&self, mem: &M) -> Result<u16, QueueError> {
let addr = GuestAddress(self.avail_ring_addr.raw() + 2);
Ok(mem.read_u16_le(addr)?)
}
fn avail_ring_entry<M: GuestMemory + ?Sized>(
&self,
mem: &M,
i: u16,
) -> Result<u16, QueueError> {
let off = u64::from(i % self.size) * 2;
let addr = GuestAddress(self.avail_ring_addr.raw() + 4 + off);
Ok(mem.read_u16_le(addr)?)
}
pub fn pop_avail<M: GuestMemory + ?Sized>(
&mut self,
mem: &M,
) -> Result<Option<DescriptorChain>, QueueError> {
if !self.is_valid() {
return Err(QueueError::QueueNotReady);
}
let driver_idx = self.avail_idx(mem)?;
if driver_idx == self.next_avail_idx {
return Ok(None);
}
let head_idx = self.avail_ring_entry(mem, self.next_avail_idx)?;
if head_idx >= self.size {
return Err(QueueError::DescriptorOutOfRange {
head: head_idx,
size: self.size,
});
}
self.next_avail_idx = self.next_avail_idx.wrapping_add(1);
Ok(Some(DescriptorChain {
desc_table_addr: self.desc_table_addr,
queue_size: self.size,
head_index: head_idx,
current: head_idx,
length: 0,
done: false,
}))
}
pub fn push_used<M: GuestMemory + ?Sized>(
&mut self,
mem: &M,
head_index: u16,
bytes_written: u32,
) -> Result<(), QueueError> {
if !self.is_valid() {
return Err(QueueError::QueueNotReady);
}
let slot = self.next_used_idx % self.size;
let elem_addr = GuestAddress(self.used_ring_addr.raw() + 4 + u64::from(slot) * 8);
mem.write_u32_le(elem_addr, u32::from(head_index))?;
mem.write_u32_le(GuestAddress(elem_addr.raw() + 4), bytes_written)?;
self.next_used_idx = self.next_used_idx.wrapping_add(1);
let used_idx_addr = GuestAddress(self.used_ring_addr.raw() + 2);
mem.write_u16_le(used_idx_addr, self.next_used_idx)?;
Ok(())
}
}
#[derive(Debug)]
pub struct DescriptorChain {
desc_table_addr: GuestAddress,
queue_size: u16,
head_index: u16,
current: u16,
length: u16,
done: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct Descriptor {
pub addr: GuestAddress,
pub len: u32,
pub flags: u16,
pub next: u16,
}
impl Descriptor {
#[must_use]
pub fn is_write_only(&self) -> bool {
self.flags & VIRTQ_DESC_F_WRITE != 0
}
#[must_use]
pub fn has_next(&self) -> bool {
self.flags & VIRTQ_DESC_F_NEXT != 0
}
}
impl DescriptorChain {
#[must_use]
pub fn head_index(&self) -> u16 {
self.head_index
}
pub fn next_descriptor<M: GuestMemory + ?Sized>(
&mut self,
mem: &M,
) -> Result<Option<Descriptor>, QueueError> {
if self.done {
return Ok(None);
}
if self.current >= self.queue_size {
return Err(QueueError::DescriptorOutOfRange {
head: self.current,
size: self.queue_size,
});
}
if self.length >= self.queue_size {
return Err(QueueError::ChainTooLong {
size: self.queue_size,
});
}
let off = u64::from(self.current) * 16;
let base = GuestAddress(self.desc_table_addr.raw() + off);
let addr = mem.read_u64_le(base)?;
let len = mem.read_u32_le(GuestAddress(base.raw() + 8))?;
let flags = mem.read_u16_le(GuestAddress(base.raw() + 12))?;
let next = mem.read_u16_le(GuestAddress(base.raw() + 14))?;
let desc = Descriptor {
addr: GuestAddress(addr),
len,
flags,
next,
};
self.length = self.length.wrapping_add(1);
if desc.has_next() {
self.current = next;
} else {
self.done = true;
}
Ok(Some(desc))
}
pub fn collect<M: GuestMemory + ?Sized>(
mut self,
mem: &M,
) -> Result<Vec<Descriptor>, QueueError> {
let mut out = Vec::with_capacity(usize::from(self.queue_size).min(8));
while let Some(d) = self.next_descriptor(mem)? {
out.push(d);
}
Ok(out)
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum QueueError {
#[error("queue not ready")]
QueueNotReady,
#[error("descriptor index {head} >= queue_size {size}")]
DescriptorOutOfRange {
head: u16,
size: u16,
},
#[error("descriptor chain length exceeds queue_size {size} (cycle?)")]
ChainTooLong {
size: u16,
},
#[error("queue memory error: {0}")]
Memory(#[from] squib_core::Error),
}
#[cfg(test)]
mod tests {
use squib_core::SliceGuestMemory;
use super::*;
fn mem() -> SliceGuestMemory {
SliceGuestMemory::new(GuestAddress(0x4000_0000), 0x1_0000)
}
fn setup_queue(mem: &SliceGuestMemory) -> Queue {
let mut q = Queue::new(64);
q.size = 8;
q.desc_table_addr = GuestAddress(0x4000_0000);
q.avail_ring_addr = GuestAddress(0x4000_0800);
q.used_ring_addr = GuestAddress(0x4000_1000);
q.ready = true;
let zeros = vec![0u8; 0x100];
mem.write(q.desc_table_addr, &zeros).unwrap();
mem.write(q.avail_ring_addr, &zeros).unwrap();
mem.write(q.used_ring_addr, &zeros).unwrap();
q
}
fn write_desc(
mem: &SliceGuestMemory,
q: &Queue,
idx: u16,
addr: u64,
len: u32,
flags: u16,
next: u16,
) {
let base = q.desc_table_addr.raw() + u64::from(idx) * 16;
mem.write_u32_le(GuestAddress(base), addr as u32).unwrap();
mem.write_u32_le(GuestAddress(base + 4), (addr >> 32) as u32)
.unwrap();
mem.write_u32_le(GuestAddress(base + 8), len).unwrap();
mem.write_u16_le(GuestAddress(base + 12), flags).unwrap();
mem.write_u16_le(GuestAddress(base + 14), next).unwrap();
}
#[test]
fn test_should_clamp_queue_size_to_power_of_two() {
let mut q = Queue::new(256);
q.set_size(200);
assert_eq!(q.size, 128);
q.set_size(256);
assert_eq!(q.size, 256);
q.set_size(1024); assert_eq!(q.size, 256);
}
#[test]
fn test_should_reject_pop_before_ready() {
let m = mem();
let mut q = Queue::new(8);
let err = q.pop_avail(&m).unwrap_err();
assert!(matches!(err, QueueError::QueueNotReady));
}
#[test]
fn test_should_yield_no_descriptor_when_avail_idx_unchanged() {
let m = mem();
let mut q = setup_queue(&m);
assert!(q.pop_avail(&m).unwrap().is_none());
}
#[test]
fn test_should_walk_single_descriptor_chain() {
let m = mem();
let mut q = setup_queue(&m);
write_desc(&m, &q, 0, 0x1234, 16, 0, 0);
m.write_u16_le(GuestAddress(q.avail_ring_addr.raw() + 4), 0)
.unwrap();
m.write_u16_le(GuestAddress(q.avail_ring_addr.raw() + 2), 1)
.unwrap();
let chain = q.pop_avail(&m).unwrap().expect("chain available");
let descs = chain.collect(&m).unwrap();
assert_eq!(descs.len(), 1);
assert_eq!(descs[0].addr.raw(), 0x1234);
assert_eq!(descs[0].len, 16);
}
#[test]
fn test_should_walk_two_descriptor_chain() {
let m = mem();
let mut q = setup_queue(&m);
write_desc(&m, &q, 0, 0x1000, 16, VIRTQ_DESC_F_NEXT, 1);
write_desc(&m, &q, 1, 0x2000, 32, VIRTQ_DESC_F_WRITE, 0);
m.write_u16_le(GuestAddress(q.avail_ring_addr.raw() + 4), 0)
.unwrap();
m.write_u16_le(GuestAddress(q.avail_ring_addr.raw() + 2), 1)
.unwrap();
let chain = q.pop_avail(&m).unwrap().expect("chain available");
let descs = chain.collect(&m).unwrap();
assert_eq!(descs.len(), 2);
assert_eq!(descs[0].addr.raw(), 0x1000);
assert!(!descs[0].is_write_only());
assert!(descs[0].has_next());
assert_eq!(descs[1].addr.raw(), 0x2000);
assert!(descs[1].is_write_only());
assert!(!descs[1].has_next());
}
#[test]
fn test_should_reject_descriptor_index_out_of_range() {
let m = mem();
let mut q = setup_queue(&m);
m.write_u16_le(GuestAddress(q.avail_ring_addr.raw() + 4), 9)
.unwrap();
m.write_u16_le(GuestAddress(q.avail_ring_addr.raw() + 2), 1)
.unwrap();
let err = q.pop_avail(&m).unwrap_err();
assert!(matches!(err, QueueError::DescriptorOutOfRange { .. }));
}
#[test]
fn test_should_break_descriptor_chain_cycle_with_chain_too_long() {
let m = mem();
let mut q = setup_queue(&m);
write_desc(&m, &q, 0, 0x1000, 16, VIRTQ_DESC_F_NEXT, 1);
write_desc(&m, &q, 1, 0x2000, 16, VIRTQ_DESC_F_NEXT, 0);
m.write_u16_le(GuestAddress(q.avail_ring_addr.raw() + 4), 0)
.unwrap();
m.write_u16_le(GuestAddress(q.avail_ring_addr.raw() + 2), 1)
.unwrap();
let chain = q.pop_avail(&m).unwrap().expect("chain available");
let err = chain.collect(&m).unwrap_err();
assert!(matches!(err, QueueError::ChainTooLong { .. }));
}
#[test]
fn test_should_publish_used_ring_with_head_index_and_byte_count() {
let m = mem();
let mut q = setup_queue(&m);
q.push_used(&m, 5, 128).unwrap();
let elem_addr = GuestAddress(q.used_ring_addr.raw() + 4);
let head = m.read_u32_le(elem_addr).unwrap();
let len = m.read_u32_le(GuestAddress(elem_addr.raw() + 4)).unwrap();
assert_eq!(head, 5);
assert_eq!(len, 128);
let used_idx = m
.read_u16_le(GuestAddress(q.used_ring_addr.raw() + 2))
.unwrap();
assert_eq!(used_idx, 1);
}
#[test]
fn test_should_reset_queue_back_to_max_size_and_unready() {
let mut q = Queue::new(8);
q.size = 4;
q.ready = true;
q.next_avail_idx = 3;
q.reset();
assert_eq!(q.size, 8);
assert!(!q.ready);
assert_eq!(q.next_avail_idx, 0);
}
}