#![cfg(test)]
#![allow(unused_imports)]
use super::testing::*;
use super::*;
use std::io::{Seek, Write};
use std::num::NonZeroU64;
use std::os::unix::fs::FileExt;
use std::sync::atomic::Ordering;
use std::time::Instant;
use tempfile::tempfile;
use virtio_bindings::bindings::virtio_ring::VRING_DESC_F_WRITE;
use virtio_queue::desc::{RawDescriptor, split::Descriptor as SplitDescriptor};
use virtio_queue::mock::MockSplitQueue;
use vm_memory::Address;
#[test]
fn seg_max_dropped_no_publish() {
use virtio_bindings::bindings::virtio_ring::VRING_DESC_F_NEXT;
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 256);
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
let header_addr = GuestAddress(0x10000);
let status_addr = GuestAddress(0x20000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let total_descs: u16 = 132;
let data_descs: u16 = total_descs - 2;
let mut descs = Vec::new();
descs.push(RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
VRING_DESC_F_NEXT as u16,
1,
)));
for i in 0..data_descs {
descs.push(RawDescriptor::from(SplitDescriptor::new(
0x40000 + i as u64 * 8,
8,
VRING_DESC_F_WRITE as u16 | VRING_DESC_F_NEXT as u16,
i + 2,
)));
}
descs.push(RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)));
mem.write_slice(&[0xEEu8], status_addr).unwrap();
mock.add_desc_chains(&descs, 0).expect("add chain");
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(used_idx, 0, "SEG_MAX drop must NOT advance used.idx");
let c = dev.counters();
assert!(c.io_errors.load(Ordering::Relaxed) >= 1);
assert_eq!(c.reads_completed.load(Ordering::Relaxed), 0);
assert_eq!(c.throttled_count.load(Ordering::Relaxed), 0);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(
s[0], 0xEE,
"SEG_MAX drop must leave status descriptor untouched",
);
}
#[test]
fn header_read_obj_failure_returns_ioerr() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x200000);
let status_addr = GuestAddress(0x4000);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(
s[0], VIRTIO_BLK_S_IOERR as u8,
"header read failure must surface as S_IOERR",
);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(used_idx, 1);
let c = dev.counters();
assert!(c.io_errors.load(Ordering::Relaxed) >= 1);
assert_eq!(c.reads_completed.load(Ordering::Relaxed), 0);
}
#[test]
fn size_max_oversized_data_desc_rejected() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x8000);
let status_addr = GuestAddress(0x9000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let oversize: u32 = (1u32 << 20) + 1;
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
oversize,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(s[0], VIRTIO_BLK_S_IOERR as u8);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(used_idx, 1);
let c = dev.counters();
assert_eq!(c.io_errors.load(Ordering::Relaxed), 1);
assert_eq!(c.reads_completed.load(Ordering::Relaxed), 0);
assert_eq!(c.throttled_count.load(Ordering::Relaxed), 0);
}
#[test]
fn zero_data_t_in_returns_ioerr() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let status_addr = GuestAddress(0x5000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(s[0], VIRTIO_BLK_S_IOERR as u8);
let c = dev.counters();
assert_eq!(c.io_errors.load(Ordering::Relaxed), 1);
assert_eq!(c.reads_completed.load(Ordering::Relaxed), 0);
assert_eq!(c.throttled_count.load(Ordering::Relaxed), 0);
}
#[test]
fn sub_sector_data_len_returns_ioerr() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
513,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(s[0], VIRTIO_BLK_S_IOERR as u8);
let c = dev.counters();
assert_eq!(c.io_errors.load(Ordering::Relaxed), 1);
assert_eq!(c.reads_completed.load(Ordering::Relaxed), 0);
assert_eq!(c.throttled_count.load(Ordering::Relaxed), 0);
}
#[test]
fn direction_violation_t_in_with_ro_data_returns_ioerr() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
0, 0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(s[0], VIRTIO_BLK_S_IOERR as u8);
let c = dev.counters();
assert_eq!(c.io_errors.load(Ordering::Relaxed), 1);
assert_eq!(c.reads_completed.load(Ordering::Relaxed), 0);
assert_eq!(
c.throttled_count.load(Ordering::Relaxed),
0,
"direction violation must NOT touch throttle bucket",
);
}
#[test]
fn direction_violation_t_out_with_writable_data_returns_ioerr() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
let sentinel = vec![0xCDu8; 512];
mem.write_slice(&sentinel, data_addr).unwrap();
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_OUT, 1);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16, 0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(s[0], VIRTIO_BLK_S_IOERR as u8);
let c = dev.counters();
assert_eq!(c.io_errors.load(Ordering::Relaxed), 1);
assert_eq!(
c.writes_completed.load(Ordering::Relaxed),
0,
"direction-violating T_OUT must NOT count as a completed write",
);
assert_eq!(
c.bytes_written.load(Ordering::Relaxed),
0,
"direction-violating T_OUT must NOT touch the backing file",
);
assert_eq!(
c.throttled_count.load(Ordering::Relaxed),
0,
"direction violation must NOT touch throttle bucket",
);
let mut data_check = vec![0u8; 512];
mem.read_slice(&mut data_check, data_addr).unwrap();
assert!(
data_check.iter().all(|&b| b == 0xCDu8),
"data segment sentinel must be intact — device must not run the read or write path",
);
}
#[test]
fn status_write_slice_failure_no_add_used() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let status_addr = GuestAddress(0x300000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_FLUSH, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(
used_idx, 0,
"status write failure must skip add_used (status-write-success gate); used.idx stays at 0",
);
let c = dev.counters();
assert!(
c.io_errors.load(Ordering::Relaxed) >= 1,
"status write failure bumps io_errors",
);
}
#[test]
fn add_used_err_path_baseline_io_errors_zero() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let used_idx: u16 = mem
.read_obj(mock.used_addr().checked_add(2).unwrap())
.expect("read used.idx");
assert_eq!(used_idx, 1);
let c = dev.counters();
assert_eq!(
c.io_errors.load(Ordering::Relaxed),
0,
"successful add_used must NOT bump io_errors",
);
assert_eq!(c.reads_completed.load(Ordering::Relaxed), 1);
}
#[test]
fn ro_flush_full_chain_returns_ok_increments_counter() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0x00);
let mut dev = VirtioBlk::with_options(f, cap, DiskThrottle::default(), true);
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let status_addr = GuestAddress(0x5000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_FLUSH, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(s[0], VIRTIO_BLK_S_OK as u8);
let c = dev.counters();
assert_eq!(c.flushes_completed.load(Ordering::Relaxed), 1);
assert_eq!(c.io_errors.load(Ordering::Relaxed), 0);
assert_eq!(c.throttled_count.load(Ordering::Relaxed), 0);
}
#[test]
fn multi_byte_status_writes_to_last_byte() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
mem.write_slice(&[0xAA, 0xBB, 0xCC, 0xDD], status_addr)
.unwrap();
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
4, VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut buf = [0u8; 4];
mem.read_slice(&mut buf, status_addr).unwrap();
assert_eq!(
buf[0], 0xAA,
"first byte of multi-byte status must be untouched"
);
assert_eq!(buf[1], 0xBB);
assert_eq!(buf[2], 0xCC);
assert_eq!(
buf[3], VIRTIO_BLK_S_OK as u8,
"status byte must be at the LAST byte (offset len-1)",
);
}
#[test]
fn zero_capacity_read_returns_ioerr() {
let cap = 0u64;
let f = tempfile().unwrap();
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(s[0], VIRTIO_BLK_S_IOERR as u8);
let c = dev.counters();
assert!(c.io_errors.load(Ordering::Relaxed) >= 1);
assert_eq!(c.reads_completed.load(Ordering::Relaxed), 0);
}
#[test]
fn partial_data_read_zero_pads_remainder() {
let cap = 4096u64;
let mut f = tempfile().unwrap();
f.set_len(100).unwrap();
f.write_all(&[0xA5; 100]).unwrap();
f.rewind().unwrap();
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
let pre = vec![0xFFu8; 512];
mem.write_slice(&pre, data_addr).unwrap();
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(s[0], VIRTIO_BLK_S_OK as u8);
let mut buf = [0u8; 512];
mem.read_slice(&mut buf, data_addr).unwrap();
assert!(
buf[..100].iter().all(|&b| b == 0xA5),
"first 100 bytes must match backing file pattern",
);
assert!(
buf[100..].iter().all(|&b| b == 0),
"bytes 100..512 must be zero-padded",
);
}
#[test]
fn write_sector_overflow_returns_ioerr() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0x00);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_OUT, u64::MAX);
let payload = vec![0xCDu8; 512];
mem.write_slice(&payload, data_addr).unwrap();
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(data_addr.0, 512, 0, 0)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(s[0], VIRTIO_BLK_S_IOERR as u8);
let c = dev.counters();
assert!(c.io_errors.load(Ordering::Relaxed) >= 1);
assert_eq!(c.writes_completed.load(Ordering::Relaxed), 0);
}
#[test]
fn flush_sync_data_baseline_ok_path() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0x00);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let status_addr = GuestAddress(0x5000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_FLUSH, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let c = dev.counters();
assert_eq!(c.flushes_completed.load(Ordering::Relaxed), 1);
assert_eq!(c.io_errors.load(Ordering::Relaxed), 0);
}
#[test]
fn validation_gates_do_not_consume_throttle_tokens() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let throttle = DiskThrottle {
iops: std::num::NonZeroU64::new(1),
bytes_per_sec: None,
iops_burst_capacity: None,
bytes_burst_capacity: None,
};
let mut dev = VirtioBlk::new(f, cap, throttle);
dev.worker
.state_mut()
.ops_bucket
.set_last_refill_for_test(std::time::Instant::now());
assert!(dev.worker.state_mut().ops_bucket.consume(1));
dev.worker
.state_mut()
.ops_bucket
.set_last_refill_for_test(std::time::Instant::now());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
513, VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let c = dev.counters();
assert_eq!(
c.io_errors.load(Ordering::Relaxed),
1,
"sub-sector gate must bump io_errors",
);
assert_eq!(
c.throttled_count.load(Ordering::Relaxed),
0,
"validation gate must NOT consume throttle tokens; \
throttled_count must stay at 0 even with bucket drained",
);
}
#[test]
fn ro_flush_and_normal_flush_both_increment_counter() {
{
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0x00);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let status_addr = GuestAddress(0x5000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_FLUSH, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
assert_eq!(
dev.counters().flushes_completed.load(Ordering::Relaxed),
1,
"normal flush must increment flushes_completed",
);
}
{
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0x00);
let mut dev = VirtioBlk::with_options(f, cap, DiskThrottle::default(), true);
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let status_addr = GuestAddress(0x5000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_FLUSH, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
assert_eq!(
dev.counters().flushes_completed.load(Ordering::Relaxed),
1,
"RO flush must increment flushes_completed (counter symmetry)",
);
}
}
#[test]
fn process_requests_fires_irqfd_on_legacy_path() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
assert!(
dev.irq_evt.read().is_err(),
"before process_requests, irq_evt must not be signalled",
);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let val = dev
.irq_evt
.read()
.expect("irq_evt must be readable after notify");
assert_eq!(
val, 1,
"irq_evt counter must be exactly 1 after a single chain drain"
);
assert_ne!(
dev.interrupt_status.load(Ordering::Acquire) & VIRTIO_MMIO_INT_VRING,
0
);
}
#[test]
fn event_idx_suppresses_irqfd_when_threshold_unreached() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let qsize = 16u16;
let mock = MockSplitQueue::create(&mem, GuestAddress(0), qsize);
let used_event = used_event_addr(mock.avail_addr(), qsize);
mem.write_obj::<u16>(u16::to_le(u16::MAX), used_event)
.expect("plant used_event");
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock_with_event_idx(&mut dev, &mock, qsize, GuestAddress(0x10000));
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(s[0], VIRTIO_BLK_S_OK as u8);
assert_eq!(dev.counters().reads_completed.load(Ordering::Relaxed), 1,);
assert_ne!(
dev.interrupt_status.load(Ordering::Acquire) & VIRTIO_MMIO_INT_VRING,
0,
"interrupt_status bit must be set when chain published",
);
let status = read_reg(&dev, VIRTIO_MMIO_INTERRUPT_STATUS);
assert_eq!(status & 1, 1);
assert!(
dev.irq_evt.read().is_err(),
"irq_evt must be unsignalled when used_event threshold not crossed",
);
}
#[test]
fn event_idx_fires_irqfd_when_threshold_reached() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let qsize = 16u16;
let mock = MockSplitQueue::create(&mem, GuestAddress(0), qsize);
let used_event = used_event_addr(mock.avail_addr(), qsize);
mem.write_obj::<u16>(u16::to_le(0), used_event)
.expect("plant used_event");
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock_with_event_idx(&mut dev, &mock, qsize, GuestAddress(0x10000));
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let val = dev
.irq_evt
.read()
.expect("irq_evt must be readable when threshold reached");
assert_eq!(
val, 1,
"irq_evt counter must be exactly 1 after a single chain completion",
);
assert_ne!(
dev.interrupt_status.load(Ordering::Acquire) & VIRTIO_MMIO_INT_VRING,
0
);
}
#[test]
fn event_idx_multi_chain_drain_fires_once() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let qsize = 16u16;
let mock = MockSplitQueue::create(&mem, GuestAddress(0), qsize);
let used_event = used_event_addr(mock.avail_addr(), qsize);
mem.write_obj::<u16>(u16::to_le(0), used_event)
.expect("plant used_event");
for i in 0..3u64 {
let header_addr = GuestAddress(0x4000 + i * 0x1000);
let data_addr = GuestAddress(0x8000 + i * 0x1000);
let status_addr = GuestAddress(0xC000 + i * 0x100);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
}
dev.set_mem(mem.clone());
wire_device_to_mock_with_event_idx(&mut dev, &mock, qsize, GuestAddress(0x10000));
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
assert_eq!(dev.counters().reads_completed.load(Ordering::Relaxed), 3,);
let val = dev
.irq_evt
.read()
.expect("irq_evt must be readable after multi-chain drain");
assert_eq!(
val, 1,
"irq_evt must fire exactly once for a multi-chain drain \
(V6: needs_notification consulted once at end of drain)",
);
}
#[test]
fn event_idx_multi_chain_drain_suppresses_below_threshold() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let qsize = 16u16;
let mock = MockSplitQueue::create(&mem, GuestAddress(0), qsize);
let used_event = used_event_addr(mock.avail_addr(), qsize);
mem.write_obj::<u16>(u16::to_le(10), used_event)
.expect("plant used_event");
for i in 0..3u64 {
let header_addr = GuestAddress(0x4000 + i * 0x1000);
let data_addr = GuestAddress(0x8000 + i * 0x1000);
let status_addr = GuestAddress(0xC000 + i * 0x100);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
}
dev.set_mem(mem.clone());
wire_device_to_mock_with_event_idx(&mut dev, &mock, qsize, GuestAddress(0x10000));
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
assert_eq!(
dev.counters().reads_completed.load(Ordering::Relaxed),
3,
"all 3 chains must complete in the single QUEUE_NOTIFY drain",
);
let used_idx: u16 = mem
.read_obj(GuestAddress(0x10000).checked_add(2).unwrap())
.expect("read device used.idx at override addr");
assert_eq!(
used_idx, 3,
"exactly three used-ring entries expected after 3-chain drain",
);
assert_ne!(
dev.interrupt_status.load(Ordering::Acquire) & VIRTIO_MMIO_INT_VRING,
0,
"interrupt_status bit must be set after 3 completions \
even when irqfd suppressed",
);
let status = read_reg(&dev, VIRTIO_MMIO_INTERRUPT_STATUS);
assert_eq!(status & 1, 1);
assert!(
dev.irq_evt.read().is_err(),
"irq_evt must be unsignalled when post-drain next_used \
stays below used_event threshold",
);
}
#[test]
fn legacy_path_fires_irqfd_every_drain() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let qsize = 16u16;
let mock = MockSplitQueue::create(&mem, GuestAddress(0), qsize);
let used_event = used_event_addr(mock.avail_addr(), qsize);
mem.write_obj::<u16>(u16::to_le(u16::MAX), used_event)
.expect("plant used_event");
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let val = dev
.irq_evt
.read()
.expect("irq_evt must be readable on legacy path");
assert_eq!(
val, 1,
"legacy path must fire irq_evt unconditionally — used_event \
is irrelevant when EVENT_IDX is not negotiated",
);
let status = read_reg(&dev, VIRTIO_MMIO_INTERRUPT_STATUS);
assert_eq!(status & 1, 1);
}
#[test]
fn outer_loop_drains_two_pre_queued_chains_in_one_call() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let qsize = 16u16;
let mock = MockSplitQueue::create(&mem, GuestAddress(0), qsize);
let used_event = used_event_addr(mock.avail_addr(), qsize);
mem.write_obj::<u16>(u16::to_le(0), used_event)
.expect("plant used_event");
let header_addr = GuestAddress(0x4000);
let status_addr = GuestAddress(0x4100);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_FLUSH, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain 1");
mock.build_desc_chain(&descs).expect("build chain 2");
dev.set_mem(mem.clone());
wire_device_to_mock_with_event_idx(&mut dev, &mock, qsize, GuestAddress(0x10000));
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let c = dev.counters();
assert_eq!(
c.flushes_completed.load(Ordering::Relaxed),
2,
"both pre-queued FLUSH chains must complete in a single \
process_requests call",
);
let used_idx: u16 = mem
.read_obj(GuestAddress(0x10000).checked_add(2).unwrap())
.expect("read device used.idx at override addr");
assert_eq!(
used_idx, 2,
"exactly two used-ring entries expected after two-chain drain",
);
let val = dev
.irq_evt
.read()
.expect("irq_evt readable after two-chain drain");
assert_eq!(
val, 1,
"exactly one irq_evt write expected — needs_notification \
consulted once after the drain settles",
);
}
#[test]
fn enable_notification_err_breaks_outer_and_fires_irqfd_fail_safe() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = GuestMemoryMmap::from_ranges(&[
(GuestAddress(0), 0x20000),
(GuestAddress(0x30000), 0x10000),
])
.expect("create multi-region guest mem");
let qsize = 16u16;
let mock = MockSplitQueue::create(&mem, GuestAddress(0), qsize);
let header_addr = GuestAddress(0x4000);
let status_addr = GuestAddress(0x5000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_FLUSH, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock_with_event_idx(&mut dev, &mock, qsize, GuestAddress(0x1FF7C));
assert!(
dev.irq_evt.read().is_err(),
"irq_evt must not be signalled before notify",
);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let c = dev.counters();
assert_eq!(
c.flushes_completed.load(Ordering::Relaxed),
1,
"FLUSH must complete before the enable_notification bail",
);
let used_idx: u16 = mem
.read_obj(GuestAddress(0x1FF7C).checked_add(2).unwrap())
.expect("read device used.idx at override addr");
assert_eq!(
used_idx, 1,
"add_used must have run before the enable_notification bail",
);
let val = dev
.irq_evt
.read()
.expect("irq_evt must fire fail-safe after enable_notification bail");
assert_eq!(
val, 1,
"irq_evt must fire exactly once after the bail (V8 \
interrupt_status bit + needs_notification gate)",
);
assert_ne!(
dev.interrupt_status.load(Ordering::Acquire) & VIRTIO_MMIO_INT_VRING,
0,
"interrupt_status bit must be set when chain published, \
independent of the enable_notification bail",
);
}
#[test]
fn enable_notification_err_on_stall_path_breaks_outer_cleanly() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let throttle = DiskThrottle {
iops: std::num::NonZeroU64::new(1),
bytes_per_sec: None,
iops_burst_capacity: None,
bytes_burst_capacity: None,
};
let mut dev = VirtioBlk::new(f, cap, throttle);
let mem = GuestMemoryMmap::from_ranges(&[
(GuestAddress(0), 0x20000),
(GuestAddress(0x30000), 0x10000),
])
.expect("create multi-region guest mem");
let qsize = 16u16;
let mock = MockSplitQueue::create(&mem, GuestAddress(0), qsize);
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
mem.write_slice(&[0xEEu8], status_addr).unwrap();
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock_with_event_idx(&mut dev, &mock, qsize, GuestAddress(0x1FF7C));
let now = std::time::Instant::now();
dev.worker
.state_mut()
.ops_bucket
.set_last_refill_for_test(now);
assert!(dev.worker.state_mut().ops_bucket.consume(1));
dev.worker
.state_mut()
.ops_bucket
.set_last_refill_for_test(now);
assert!(
!dev.worker.state_mut().ops_bucket.can_consume(1),
"precondition: ops bucket must be drained so the chain stalls",
);
let c = dev.counters();
assert_eq!(c.throttled_count.load(Ordering::Relaxed), 0);
assert_eq!(c.currently_throttled_gauge.load(Ordering::Relaxed), 0);
assert_eq!(c.reads_completed.load(Ordering::Relaxed), 0);
assert_eq!(c.io_errors.load(Ordering::Relaxed), 0);
assert!(!dev.worker.state().currently_stalled);
assert!(
dev.irq_evt.read().is_err(),
"irq_evt must be unsignalled before notify",
);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(
s[0], 0xEE,
"status byte must remain at sentinel — stall must not write status",
);
let used_idx: u16 = mem
.read_obj(GuestAddress(0x1FF7C).checked_add(2).unwrap())
.expect("read device used.idx at override addr");
assert_eq!(used_idx, 0, "used.idx must be 0 — stall must skip add_used",);
assert_eq!(
c.throttled_count.load(Ordering::Relaxed),
1,
"stall event must be recorded once",
);
assert_eq!(
c.currently_throttled_gauge.load(Ordering::Relaxed),
1,
"gauge must increment on the false→true transition",
);
assert_eq!(c.reads_completed.load(Ordering::Relaxed), 0);
assert_eq!(c.io_errors.load(Ordering::Relaxed), 0);
assert!(
dev.worker.state().currently_stalled,
"currently_stalled flag must be true post-stall",
);
assert_eq!(
dev.interrupt_status.load(Ordering::Acquire) & VIRTIO_MMIO_INT_VRING,
0,
"interrupt_status bit must be clear — stall does not \
enter the V8 post-drain block",
);
assert!(
dev.irq_evt.read().is_err(),
"irq_evt must be unsignalled — stall does not fire irqfd",
);
assert_eq!(
dev.worker.queues[REQ_QUEUE].next_avail(),
0,
"queue cursor must be rewound to 0 — set_next_avail \
rolled the pop back so the chain re-pops on retry",
);
}
#[test]
fn next_avail_zero_rollback_wraps_to_u16_max() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
dev.worker.queues[REQ_QUEUE].set_next_avail(0);
assert_eq!(dev.worker.queues[REQ_QUEUE].next_avail(), 0);
let prev = dev.worker.queues[REQ_QUEUE].next_avail();
dev.worker.queues[REQ_QUEUE].set_next_avail(prev.wrapping_sub(1));
assert_eq!(
dev.worker.queues[REQ_QUEUE].next_avail(),
u16::MAX,
"next_avail rollback at prev=0 must wrap to u16::MAX, \
matching the virtio ring's u16 modular semantics",
);
let prev = dev.worker.queues[REQ_QUEUE].next_avail();
dev.worker.queues[REQ_QUEUE].set_next_avail(prev.wrapping_sub(1));
assert_eq!(
dev.worker.queues[REQ_QUEUE].next_avail(),
u16::MAX - 1,
"subsequent rollback at prev=u16::MAX must land at u16::MAX-1",
);
}
#[test]
fn fragmented_header_returns_ioerr() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0x00);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_part1_addr = GuestAddress(0x4000);
let header_part2_addr = GuestAddress(0x4008);
let status_addr = GuestAddress(0x5000);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_part1_addr.0,
8, 0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(header_part2_addr.0, 8, 0, 0)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(
s[0], VIRTIO_BLK_S_IOERR as u8,
"fragmented header (first desc < OUTHDR_SIZE) must IOERR",
);
let c = dev.counters();
assert_eq!(c.io_errors.load(Ordering::Relaxed), 1);
assert_eq!(c.reads_completed.load(Ordering::Relaxed), 0);
}
#[test]
fn event_idx_error_chain_suppressed_when_threshold_unreached() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0x00);
let mut dev = VirtioBlk::new(f, cap, DiskThrottle::default());
let mem = make_chain_test_mem();
let qsize = 16u16;
let mock = MockSplitQueue::create(&mem, GuestAddress(0), qsize);
let used_event = used_event_addr(mock.avail_addr(), qsize);
mem.write_obj::<u16>(u16::to_le(u16::MAX), used_event)
.expect("plant used_event");
let header_part1_addr = GuestAddress(0x4000);
let header_part2_addr = GuestAddress(0x4008);
let status_addr = GuestAddress(0x5000);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_part1_addr.0,
8, 0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(header_part2_addr.0, 8, 0, 0)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock_with_event_idx(&mut dev, &mock, qsize, GuestAddress(0x10000));
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(
s[0], VIRTIO_BLK_S_IOERR as u8,
"fragmented header must produce IOERR even on EVENT_IDX path",
);
let c = dev.counters();
assert_eq!(
c.io_errors.load(Ordering::Relaxed),
1,
"fragmented-header reject must bump io_errors exactly once",
);
let used_idx: u16 = mem
.read_obj(GuestAddress(0x10000).checked_add(2).unwrap())
.expect("read device used.idx at override addr");
assert_eq!(
used_idx, 1,
"error chain must still be add_used'd so the guest sees \
the IOERR status — V8 + the publish_completion contract",
);
assert_ne!(
dev.interrupt_status.load(Ordering::Acquire) & VIRTIO_MMIO_INT_VRING,
0,
"interrupt_status bit must be set after error chain \
completes, independent of irqfd gate",
);
let status = read_reg(&dev, VIRTIO_MMIO_INTERRUPT_STATUS);
assert_eq!(status & 1, 1);
assert!(
dev.irq_evt.read().is_err(),
"irq_evt must be unsignalled — error completions route \
through the same needs_notification gate as success \
completions, and used_event=u16::MAX was unreached",
);
}
#[test]
fn size_max_advertised_in_config_space() {
let dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
let mut buf = [0u8; 4];
dev.mmio_read(0x100 + 0x08, &mut buf);
assert_eq!(
u32::from_le_bytes(buf),
VIRTIO_BLK_SIZE_MAX,
"config-space size_max must equal VIRTIO_BLK_SIZE_MAX (1 MB)",
);
}
#[test]
fn validation_gate_does_not_consume_throttle_tokens() {
let cap = 4096u64;
let f = make_backed_file_with_pattern(cap, 0xAB);
let throttle = DiskThrottle {
iops: NonZeroU64::new(1000),
bytes_per_sec: NonZeroU64::new(1_000_000),
iops_burst_capacity: None,
bytes_burst_capacity: None,
};
let mut dev = VirtioBlk::new(f, cap, throttle);
let now = Instant::now();
dev.worker
.state_mut()
.ops_bucket
.set_last_refill_for_test(now);
dev.worker
.state_mut()
.bytes_bucket
.set_last_refill_for_test(now);
let ops_avail_before = dev.worker.state_mut().ops_bucket.available;
let bytes_avail_before = dev.worker.state_mut().bytes_bucket.available;
assert_eq!(
ops_avail_before, 1000,
"ops bucket must be seeded at capacity (1000) on construction",
);
assert_eq!(
bytes_avail_before, 1_000_000,
"bytes bucket must be seeded at capacity (1_000_000) on construction",
);
let mem = make_chain_test_mem();
let mock = MockSplitQueue::create(&mem, GuestAddress(0), 16);
let header_addr = GuestAddress(0x4000);
let data_addr = GuestAddress(0x5000);
let status_addr = GuestAddress(0x6000);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
0,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
513,
VRING_DESC_F_WRITE as u16,
0,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs).expect("build chain");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
let now2 = Instant::now();
dev.worker
.state_mut()
.ops_bucket
.set_last_refill_for_test(now2);
dev.worker
.state_mut()
.bytes_bucket
.set_last_refill_for_test(now2);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let mut s = [0u8; 1];
mem.read_slice(&mut s, status_addr).unwrap();
assert_eq!(
s[0], VIRTIO_BLK_S_IOERR as u8,
"sub-sector chain must be rejected by the sector-alignment validation gate",
);
let c = dev.counters();
assert_eq!(
c.io_errors.load(Ordering::Relaxed),
1,
"validation gate bumps io_errors exactly once",
);
assert_eq!(
c.reads_completed.load(Ordering::Relaxed),
0,
"rejected chain must not count as a completed read",
);
assert_eq!(
c.throttled_count.load(Ordering::Relaxed),
0,
"validation rejection is not a throttle stall — \
throttled_count must stay 0",
);
assert_eq!(
dev.worker.state_mut().ops_bucket.available,
ops_avail_before,
"ops bucket `available` must be unchanged across a \
validation rejection — gate ordering requires the \
validation check to fire BEFORE token consumption",
);
assert_eq!(
dev.worker.state_mut().bytes_bucket.available,
bytes_avail_before,
"bytes bucket `available` must be unchanged across a \
validation rejection — gate ordering requires the \
validation check to fire BEFORE token consumption",
);
}