#![cfg(test)]
#![allow(unused_imports)]
use super::testing::*;
use super::*;
use std::io::Write;
use std::sync::atomic::Ordering;
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 magic_version_device_id() {
let dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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_BLOCK);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_VENDOR_ID), 0);
}
#[test]
fn advertised_features_include_size_max_seg_max_blk_size_flush() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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 & (1u64 << VIRTIO_F_VERSION_1), 0);
assert_ne!(features & (1u64 << VIRTIO_BLK_F_BLK_SIZE), 0);
assert_ne!(features & (1u64 << VIRTIO_BLK_F_SEG_MAX), 0);
assert_ne!(features & (1u64 << VIRTIO_BLK_F_SIZE_MAX), 0);
assert_ne!(features & (1u64 << VIRTIO_BLK_F_FLUSH), 0);
}
#[test]
fn advertised_features_include_event_idx() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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 & (1u64 << VIRTIO_RING_F_EVENT_IDX), 0);
}
#[test]
fn capacity_in_config_space() {
let dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
assert_eq!(dev.capacity_sectors(), 524_288);
let mut buf = [0u8; 8];
dev.mmio_read(0x100, &mut buf);
assert_eq!(u64::from_le_bytes(buf), 524_288);
}
#[test]
fn blk_size_in_config_space() {
let dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
let mut buf = [0u8; 4];
dev.mmio_read(0x100 + 0x14, &mut buf);
assert_eq!(u32::from_le_bytes(buf), VIRTIO_BLK_SECTOR_SIZE);
}
#[test]
fn reset_bumps_config_generation() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
let gen0 = read_reg(&dev, VIRTIO_MMIO_CONFIG_GENERATION);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
let gen1 = read_reg(&dev, VIRTIO_MMIO_CONFIG_GENERATION);
assert_eq!(gen1, gen0.wrapping_add(1));
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
let gen2 = read_reg(&dev, VIRTIO_MMIO_CONFIG_GENERATION);
assert_eq!(gen2, gen1.wrapping_add(1));
}
#[test]
fn reset_rebuilds_throttle_buckets() {
let throttle = DiskThrottle {
iops: std::num::NonZeroU64::new(4),
bytes_per_sec: std::num::NonZeroU64::new(8192),
iops_burst_capacity: None,
bytes_burst_capacity: None,
};
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, throttle);
let now = std::time::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);
assert!(dev.worker.state_mut().ops_bucket.consume(4));
assert!(dev.worker.state_mut().bytes_bucket.consume(8192));
dev.worker
.state_mut()
.ops_bucket
.set_last_refill_for_test(now);
dev.worker
.state_mut()
.bytes_bucket
.set_last_refill_for_test(now);
assert!(
!dev.worker.state_mut().ops_bucket.can_consume(1),
"ops bucket must be drained before reset",
);
assert!(
!dev.worker.state_mut().bytes_bucket.can_consume(1),
"bytes bucket must be drained before reset",
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
assert!(
dev.worker.state_mut().ops_bucket.can_consume(4),
"ops bucket must be refilled to capacity by reset",
);
assert!(
dev.worker.state_mut().bytes_bucket.can_consume(8192),
"bytes bucket must be refilled to capacity by reset",
);
}
#[test]
fn reset_clears_queue_next_avail() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
dev.worker.queues[REQ_QUEUE].set_next_avail(7);
assert_eq!(dev.worker.queues[REQ_QUEUE].next_avail(), 7);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
assert_eq!(
dev.worker.queues[REQ_QUEUE].next_avail(),
0,
"reset must zero next_avail (Queue::reset behaviour)",
);
}
#[test]
fn reset_drains_irq_evt_pending_count() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
dev.irq_evt().write(1).expect("seed irq eventfd counter");
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
match dev.irq_evt().read() {
Ok(n) => panic!("expected post-reset irq_evt counter drained, but read returned {n}",),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("unexpected irq_evt read error after reset: {e}"),
}
}
#[test]
fn reset_clears_interrupt_status() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
dev.interrupt_status
.store(VIRTIO_MMIO_INT_VRING, Ordering::Release);
assert_eq!(
read_reg(&dev, VIRTIO_MMIO_INTERRUPT_STATUS),
VIRTIO_MMIO_INT_VRING,
"pre-reset: bit set as a precondition",
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
assert_eq!(
read_reg(&dev, VIRTIO_MMIO_INTERRUPT_STATUS),
0,
"reset must clear interrupt_status (Phase 3)",
);
}
#[test]
fn reset_clears_mem_unset_warned_latch() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
dev.mem_unset_warned.store(true, Ordering::Relaxed);
assert!(dev.mem_unset_warned.load(Ordering::Relaxed));
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
assert!(
!dev.mem_unset_warned.load(Ordering::Relaxed),
"reset must re-arm the queue-notify-before-set_mem latch",
);
}
#[test]
fn reset_then_reactivate_processes_new_chain() {
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 1");
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,
"first chain must complete S_OK"
);
assert_eq!(
dev.counters().reads_completed.load(Ordering::Relaxed),
1,
"first chain bumps reads_completed to 1",
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
assert_eq!(
dev.device_status.load(Ordering::Acquire),
0,
"device_status must zero on reset"
);
let avail_idx_addr = mock.avail_addr().checked_add(2).unwrap();
let used_idx_addr = mock.used_addr().checked_add(2).unwrap();
mem.write_obj(0u16, avail_idx_addr).unwrap();
mem.write_obj(0u16, used_idx_addr).unwrap();
mem.write_slice(&[0xEEu8], status_addr).unwrap();
mock.build_desc_chain(&descs).expect("build chain 2");
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,
"post-reset chain must complete S_OK"
);
let c = dev.counters();
assert_eq!(
c.reads_completed.load(Ordering::Relaxed),
2,
"reads_completed is cumulative across reset (1 pre + 1 post)",
);
assert_eq!(c.io_errors.load(Ordering::Relaxed), 0);
let used_idx: u16 = mem.read_obj(used_idx_addr).expect("read used.idx");
assert_eq!(
used_idx, 1,
"used.idx must be 1 — only the post-reset chain is on \
the freshly-rebound used ring",
);
}
#[test]
fn reset_preserves_counters() {
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 c = dev.counters();
let pre_reads = c.reads_completed.load(Ordering::Relaxed);
let pre_bytes_read = c.bytes_read.load(Ordering::Relaxed);
let pre_io_errors = c.io_errors.load(Ordering::Relaxed);
let pre_throttled = c.throttled_count.load(Ordering::Relaxed);
assert_eq!(pre_reads, 1, "precondition: one read completed");
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
assert_eq!(
c.reads_completed.load(Ordering::Relaxed),
pre_reads,
"reads_completed must persist across reset",
);
assert_eq!(
c.bytes_read.load(Ordering::Relaxed),
pre_bytes_read,
"bytes_read must persist across reset",
);
assert_eq!(
c.io_errors.load(Ordering::Relaxed),
pre_io_errors,
"io_errors must persist across reset",
);
assert_eq!(
c.throttled_count.load(Ordering::Relaxed),
pre_throttled,
"throttled_count must persist across reset",
);
}
#[test]
fn reset_rebuilds_throttle_then_stalls_on_second_chain() {
let cap = 4096u64;
let throttle = DiskThrottle {
iops: std::num::NonZeroU64::new(1),
bytes_per_sec: None,
iops_burst_capacity: None,
bytes_burst_capacity: None,
};
let f = make_backed_file_with_pattern(cap, 0xAB);
let mut dev = VirtioBlk::new(f, cap, throttle);
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_a = GuestAddress(0x6000);
let status_addr_b = GuestAddress(0x6100);
let status_addr_c = GuestAddress(0x6200);
write_blk_header(&mem, header_addr, VIRTIO_BLK_T_IN, 0);
let descs_chain_a = [
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_a.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.build_desc_chain(&descs_chain_a)
.expect("build chain A");
dev.set_mem(mem.clone());
wire_device_to_mock(&mut dev, &mock);
let now = std::time::Instant::now();
dev.worker
.state_mut()
.ops_bucket
.set_last_refill_for_test(now);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
assert_eq!(
dev.counters().reads_completed.load(Ordering::Relaxed),
1,
"chain A must complete (the only iops token granted)",
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
let avail_idx_addr = mock.avail_addr().checked_add(2).unwrap();
let used_idx_addr = mock.used_addr().checked_add(2).unwrap();
mem.write_obj(0u16, avail_idx_addr).unwrap();
mem.write_obj(0u16, used_idx_addr).unwrap();
wire_device_to_mock(&mut dev, &mock);
let now2 = std::time::Instant::now();
dev.worker
.state_mut()
.ops_bucket
.set_last_refill_for_test(now2);
mem.write_slice(&[0xEEu8], status_addr_b).unwrap();
mem.write_slice(&[0xEEu8], status_addr_c).unwrap();
let descs_chain_b = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
virtio_bindings::bindings::virtio_ring::VRING_DESC_F_NEXT as u16,
4,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16
| virtio_bindings::bindings::virtio_ring::VRING_DESC_F_NEXT as u16,
5,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr_b.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.add_desc_chains(&descs_chain_b, 3)
.expect("add chain B at offset 3");
let descs_chain_c = [
RawDescriptor::from(SplitDescriptor::new(
header_addr.0,
VIRTIO_BLK_OUTHDR_SIZE as u32,
virtio_bindings::bindings::virtio_ring::VRING_DESC_F_NEXT as u16,
7,
)),
RawDescriptor::from(SplitDescriptor::new(
data_addr.0,
512,
VRING_DESC_F_WRITE as u16
| virtio_bindings::bindings::virtio_ring::VRING_DESC_F_NEXT as u16,
8,
)),
RawDescriptor::from(SplitDescriptor::new(
status_addr_c.0,
1,
VRING_DESC_F_WRITE as u16,
0,
)),
];
mock.add_desc_chains(&descs_chain_c, 6)
.expect("add chain C at offset 6");
let now3 = std::time::Instant::now();
dev.worker
.state_mut()
.ops_bucket
.set_last_refill_for_test(now3);
let pre_throttled = dev.counters().throttled_count.load(Ordering::Relaxed);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NOTIFY, REQ_QUEUE as u32);
let c = dev.counters();
assert_eq!(
c.reads_completed.load(Ordering::Relaxed),
2,
"reads_completed: chain A pre-reset (1) + chain B post-reset (1)",
);
assert_eq!(
c.throttled_count.load(Ordering::Relaxed),
pre_throttled + 1,
"chain C must stall: rebuilt bucket has capacity=1 (iops=1) \
and chain B consumed it",
);
let mut sb = [0u8; 1];
mem.read_slice(&mut sb, status_addr_b).unwrap();
assert_eq!(sb[0], VIRTIO_BLK_S_OK as u8, "chain B must complete S_OK");
let mut sc = [0u8; 1];
mem.read_slice(&mut sc, status_addr_c).unwrap();
assert_eq!(
sc[0], 0xEE,
"chain C status must remain at sentinel (stall does not write status)",
);
}
#[test]
fn reset_blocks_post_reset_queue_config() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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, 0xDEAD_BEEF);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
assert_eq!(dev.device_status.load(Ordering::Acquire), 0);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, 0xCAFE_BABE);
}
#[test]
fn reset_drains_multi_write_irq_evt() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
dev.irq_evt().write(1).expect("seed irq eventfd #1");
dev.irq_evt().write(1).expect("seed irq eventfd #2");
dev.irq_evt().write(1).expect("seed irq eventfd #3");
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
match dev.irq_evt().read() {
Ok(n) => panic!("expected post-reset irq_evt counter drained, but read returned {n}",),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("unexpected irq_evt read error after reset: {e}"),
}
}
#[test]
fn drain_skipped_when_queue_not_ready() {
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);
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(&mut dev, &mock);
dev.worker.queues[REQ_QUEUE].set_ready(false);
assert!(
!dev.worker.queues[REQ_QUEUE].ready(),
"precondition: queue must be not-ready before notify",
);
let c = dev.counters();
let pre_reads = c.reads_completed.load(Ordering::Relaxed);
let pre_writes = c.writes_completed.load(Ordering::Relaxed);
let pre_flushes = c.flushes_completed.load(Ordering::Relaxed);
let pre_io_errors = c.io_errors.load(Ordering::Relaxed);
let pre_throttled = c.throttled_count.load(Ordering::Relaxed);
let pre_bytes_read = c.bytes_read.load(Ordering::Relaxed);
let pre_bytes_written = c.bytes_written.load(Ordering::Relaxed);
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 — drain must be a \
no-op when queue not ready",
);
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 — gate must skip add_used",);
assert_eq!(c.reads_completed.load(Ordering::Relaxed), pre_reads);
assert_eq!(c.writes_completed.load(Ordering::Relaxed), pre_writes);
assert_eq!(c.flushes_completed.load(Ordering::Relaxed), pre_flushes);
assert_eq!(c.io_errors.load(Ordering::Relaxed), pre_io_errors);
assert_eq!(c.throttled_count.load(Ordering::Relaxed), pre_throttled);
assert_eq!(c.bytes_read.load(Ordering::Relaxed), pre_bytes_read);
assert_eq!(c.bytes_written.load(Ordering::Relaxed), pre_bytes_written);
match dev.irq_evt().read() {
Ok(n) => panic!(
"expected irq_evt not fired (counter=0/WouldBlock), but \
read returned {n} — the gate must not call \
irq_evt.write",
),
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("unexpected irq_evt read error: {e}"),
}
}
#[test]
fn seg_max_in_config_space() {
let dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
let mut buf = [0u8; 4];
dev.mmio_read(0x100 + 0x0C, &mut buf);
assert_eq!(u32::from_le_bytes(buf), VIRTIO_BLK_SEG_MAX);
}
#[test]
fn config_space_struct_layout_byte_for_byte() {
let dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
let mut bytes = [0u8; VIRTIO_BLK_CONFIG_SIZE];
dev.mmio_read(0x100, &mut bytes);
let capacity = u64::from_le_bytes(bytes[0x00..0x08].try_into().unwrap());
let size_max = u32::from_le_bytes(bytes[0x08..0x0C].try_into().unwrap());
let seg_max = u32::from_le_bytes(bytes[0x0C..0x10].try_into().unwrap());
let geometry = &bytes[0x10..0x14];
let blk_size = u32::from_le_bytes(bytes[0x14..0x18].try_into().unwrap());
assert_eq!(
capacity,
VIRTIO_BLK_DEFAULT_CAPACITY_BYTES / VIRTIO_BLK_SECTOR_SIZE as u64,
"capacity mismatch — repr(C, packed) layout drift?",
);
assert_eq!(size_max, VIRTIO_BLK_SIZE_MAX, "size_max layout drift");
assert_eq!(seg_max, VIRTIO_BLK_SEG_MAX, "seg_max layout drift");
assert_eq!(
geometry, &[0u8; 4],
"F_GEOMETRY not advertised; geometry must be zero",
);
assert_eq!(blk_size, VIRTIO_BLK_SECTOR_SIZE, "blk_size layout drift");
}
#[test]
fn config_space_zero_past_struct_size() {
let dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
let mut buf = [0xffu8; 16];
dev.mmio_read(0x100 + VIRTIO_BLK_CONFIG_SIZE as u64, &mut buf);
assert!(
buf.iter().all(|&b| b == 0),
"config-space read past struct size must be zero-filled, got {:02x?}",
buf,
);
let mut buf = [0xffu8; 8];
dev.mmio_read(0x100 + 0x14, &mut buf);
assert_eq!(
u32::from_le_bytes(buf[0..4].try_into().unwrap()),
VIRTIO_BLK_SECTOR_SIZE,
"first 4 bytes must be blk_size",
);
assert_eq!(
&buf[4..],
&[0u8; 4],
"trailing 4 bytes (offset 0x18..0x1C) must zero-fill past struct end",
);
}
#[test]
fn config_space_struct_size_matches_kernel_uapi() {
assert_eq!(
VIRTIO_BLK_CONFIG_SIZE, 24,
"VirtioBlkConfig must be 24 bytes (capacity 8 + size_max 4 + \
seg_max 4 + geometry 4 + blk_size 4) per the kernel uapi \
layout. Mismatch implies repr(C, packed) drift.",
);
assert_eq!(
std::mem::align_of::<VirtioBlkConfig>(),
1,
"repr(C, packed) must produce alignment 1",
);
assert_eq!(
std::mem::align_of::<VirtioBlkGeometry>(),
1,
"geometry sub-struct must also be packed to align 1",
);
}
#[test]
fn config_space_writes_silently_dropped() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
let before = dev.capacity_sectors();
dev.mmio_write(0x100, &[0xff, 0xff, 0xff, 0xff]);
assert_eq!(dev.capacity_sectors(), before);
}
#[test]
fn queue_num_max() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, 0);
assert_eq!(
read_reg(&dev, VIRTIO_MMIO_QUEUE_NUM_MAX),
QUEUE_MAX_SIZE as u32
);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, 1);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_QUEUE_NUM_MAX), 0);
}
#[test]
fn status_state_machine() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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.load(Ordering::Acquire), S_DRV);
}
#[test]
fn features_ok_rejected_without_version_1() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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_BLK_F_BLK_SIZE,
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
assert_eq!(
dev.device_status.load(Ordering::Acquire),
S_DRV,
"FEATURES_OK must be rejected when VIRTIO_F_VERSION_1 is not negotiated",
);
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);
assert_eq!(
dev.device_status.load(Ordering::Acquire),
S_FEAT,
"FEATURES_OK must be accepted once VIRTIO_F_VERSION_1 is in driver_features",
);
}
#[test]
fn status_reset_via_zero() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, 0);
assert_eq!(dev.device_status.load(Ordering::Acquire), 0);
}
#[test]
fn capacity_custom_size() {
let dev = make_device(256 * 1024 * 1024, DiskThrottle::default());
assert_eq!(dev.capacity_sectors(), 256 * 1024 * 1024 / 512);
}
#[test]
fn counters_initially_zero() {
let dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
let c = dev.counters();
assert_eq!(c.reads_completed.load(Ordering::Relaxed), 0);
assert_eq!(c.writes_completed.load(Ordering::Relaxed), 0);
assert_eq!(c.flushes_completed.load(Ordering::Relaxed), 0);
assert_eq!(c.bytes_read.load(Ordering::Relaxed), 0);
assert_eq!(c.bytes_written.load(Ordering::Relaxed), 0);
assert_eq!(c.throttled_count.load(Ordering::Relaxed), 0);
assert_eq!(c.io_errors.load(Ordering::Relaxed), 0);
}
#[test]
fn counters_arc_shared_with_caller() {
let dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
let c1 = dev.counters();
let c2 = dev.counters();
c1.reads_completed.store(42, Ordering::Relaxed);
assert_eq!(c2.reads_completed.load(Ordering::Relaxed), 42);
}
#[test]
fn counters_accessors_match_atomic_state() {
let counters = VirtioBlkCounters::default();
counters.reads_completed.store(1, Ordering::Relaxed);
counters.writes_completed.store(2, Ordering::Relaxed);
counters.flushes_completed.store(3, Ordering::Relaxed);
counters.bytes_read.store(4, Ordering::Relaxed);
counters.bytes_written.store(5, Ordering::Relaxed);
counters.throttled_count.store(6, Ordering::Relaxed);
counters.io_errors.store(7, Ordering::Relaxed);
counters
.currently_throttled_gauge
.store(8, Ordering::Relaxed);
assert_eq!(counters.reads_completed(), 1, "reads_completed accessor");
assert_eq!(counters.writes_completed(), 2, "writes_completed accessor");
assert_eq!(
counters.flushes_completed(),
3,
"flushes_completed accessor"
);
assert_eq!(counters.bytes_read(), 4, "bytes_read accessor");
assert_eq!(counters.bytes_written(), 5, "bytes_written accessor");
assert_eq!(counters.throttled_count(), 6, "throttled_count accessor");
assert_eq!(counters.io_errors(), 7, "io_errors accessor");
assert_eq!(
counters.currently_throttled_gauge(),
8,
"currently_throttled_gauge accessor",
);
}
#[test]
fn features_ok_rejection_visible_via_mmio_read() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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_BLK_F_BLK_SIZE,
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_FEAT);
let status = read_reg(&dev, VIRTIO_MMIO_STATUS);
assert_eq!(
status, S_DRV,
"MMIO STATUS read-back must show FEATURES_OK is unset \
when VIRTIO_F_VERSION_1 was not negotiated",
);
assert_ne!(
status & VIRTIO_CONFIG_S_FEATURES_OK,
VIRTIO_CONFIG_S_FEATURES_OK,
"FEATURES_OK bit must NOT be set in MMIO read-back",
);
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);
let status = read_reg(&dev, VIRTIO_MMIO_STATUS);
assert_eq!(
status, S_FEAT,
"MMIO STATUS read-back must show FEATURES_OK is set \
once VIRTIO_F_VERSION_1 was negotiated",
);
}
#[test]
fn set_mem_twice_keeps_first_instance() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
let mem_a = make_guest_mem(4096);
let mem_b = make_guest_mem(8192);
dev.set_mem(mem_a);
let first_ptr = dev.mem.get().expect("set_mem populated OnceLock") as *const GuestMemoryMmap;
dev.set_mem(mem_b);
let after_ptr = dev.mem.get().expect("OnceLock still populated") as *const GuestMemoryMmap;
assert_eq!(
first_ptr, after_ptr,
"OnceLock must retain the first GuestMemoryMmap; set_mem \
must not overwrite on the second call",
);
}
#[test]
fn handle_flush_no_mem_no_panic() {
let mut f = tempfile().unwrap();
f.write_all(&[0u8; 1024]).unwrap();
f.sync_data().expect("tempfile sync_data must succeed");
}
#[test]
fn ok_status_constant_distinct_from_ioerr() {
assert_eq!(VIRTIO_BLK_S_OK, 0);
assert_eq!(VIRTIO_BLK_S_IOERR, 1);
assert_eq!(VIRTIO_BLK_S_UNSUPP, 2);
}
#[test]
fn interrupt_status_and_ack() {
let dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
assert_eq!(read_reg(&dev, VIRTIO_MMIO_INTERRUPT_STATUS), 0);
dev.interrupt_status
.store(VIRTIO_MMIO_INT_VRING, Ordering::Release);
assert_eq!(
read_reg(&dev, VIRTIO_MMIO_INTERRUPT_STATUS),
VIRTIO_MMIO_INT_VRING
);
}
#[test]
fn interrupt_ack_clears_bits() {
use virtio_bindings::virtio_mmio::VIRTIO_MMIO_INT_CONFIG;
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
dev.interrupt_status.store(
VIRTIO_MMIO_INT_VRING | VIRTIO_MMIO_INT_CONFIG,
Ordering::Release,
);
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 = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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 = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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 = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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 = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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 = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, 0);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_DESC_LOW, 0x1000);
assert_ne!(dev.worker.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.worker.queues[0].desc_table(), 0x1000);
}
#[test]
fn unknown_register_returns_zero() {
let dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
assert_eq!(read_reg(&dev, 0x300), 0);
}
#[test]
fn unknown_register_write_ignored() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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 = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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 features_page_2_returns_zero() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
write_reg(&mut dev, VIRTIO_MMIO_DEVICE_FEATURES_SEL, 2);
assert_eq!(read_reg(&dev, VIRTIO_MMIO_DEVICE_FEATURES), 0);
}
#[test]
fn status_skip_acknowledge_rejected() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, VIRTIO_CONFIG_S_DRIVER);
assert_eq!(dev.device_status.load(Ordering::Acquire), 0);
}
#[tracing_test::traced_test]
#[test]
fn status_idempotent_rewrite_is_noop() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
assert_eq!(dev.device_status.load(Ordering::Acquire), S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
assert_eq!(
dev.device_status.load(Ordering::Acquire),
S_ACK,
"idempotent re-write must NOT alter device_status",
);
assert!(
!logs_contain("illegal FSM transition"),
"idempotent re-write must NOT emit the illegal-transition warn",
);
assert!(
!logs_contain("attempted to clear"),
"idempotent re-write must NOT emit the clear-bit warn",
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
assert_eq!(dev.device_status.load(Ordering::Acquire), S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
assert_eq!(dev.device_status.load(Ordering::Acquire), S_DRV);
assert!(
!logs_contain("illegal FSM transition"),
"S_DRV re-write must NOT emit the illegal-transition warn",
);
}
#[tracing_test::traced_test]
#[test]
fn status_multi_bit_transition_rejected_and_logged() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(
&mut dev,
VIRTIO_MMIO_STATUS,
S_ACK | VIRTIO_CONFIG_S_DRIVER | VIRTIO_CONFIG_S_FEATURES_OK,
);
assert_eq!(
dev.device_status.load(Ordering::Acquire),
S_ACK,
"multi-bit transition must NOT advance device_status",
);
assert!(
logs_contain("illegal FSM transition"),
"multi-bit transition must emit the illegal-transition warn so \
a buggy driver surfaces in the operator log",
);
}
#[tracing_test::traced_test]
#[test]
fn status_clear_bit_rejected_and_logged() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_DRV);
assert_eq!(dev.device_status.load(Ordering::Acquire), S_DRV);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK);
assert_eq!(
dev.device_status.load(Ordering::Acquire),
S_DRV,
"clear-bit attempt must NOT alter device_status",
);
assert!(
logs_contain("attempted to clear"),
"clear-bit attempt must emit the dedicated clear-bit warn",
);
assert!(
!logs_contain("illegal FSM transition"),
"clear-bit path must NOT emit the generic illegal-transition warn — \
the dedicated clear-bit warn is the right diagnostic",
);
}
#[test]
fn queue_config_rejected_after_driver_ok() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
init_device(&mut dev);
assert_eq!(dev.device_status.load(Ordering::Acquire), S_OK);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_SEL, 0);
write_reg(&mut dev, VIRTIO_MMIO_QUEUE_NUM, 64);
assert_eq!(dev.worker.queues[0].size(), QUEUE_MAX_SIZE);
}
#[test]
fn set_status_failed_accepted_at_every_fsm_state() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
write_reg(&mut dev, VIRTIO_MMIO_STATUS, VIRTIO_CONFIG_S_FAILED);
assert_eq!(
dev.device_status.load(Ordering::Acquire),
VIRTIO_CONFIG_S_FAILED,
"FAILED from status=0 must be accepted",
);
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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.load(Ordering::Acquire),
S_ACK | VIRTIO_CONFIG_S_FAILED,
"FAILED from status=S_ACK must be accepted (S_ACK preserved)",
);
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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.load(Ordering::Acquire),
S_DRV | VIRTIO_CONFIG_S_FAILED,
"FAILED from status=S_DRV must be accepted (S_DRV preserved)",
);
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
init_device(&mut dev);
assert_eq!(dev.device_status.load(Ordering::Acquire), S_OK);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_OK | VIRTIO_CONFIG_S_FAILED);
assert_eq!(
dev.device_status.load(Ordering::Acquire),
S_OK | VIRTIO_CONFIG_S_FAILED,
"FAILED from status=S_OK must be accepted (S_OK preserved)",
);
}
#[tracing_test::traced_test]
#[test]
fn set_status_failed_plus_unknown_bit_rejected() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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.load(Ordering::Acquire),
S_ACK,
"FAILED combined with a non-FAILED unknown bit must be \
rejected — the early-accept is gated on FAILED alone",
);
assert!(
logs_contain("illegal FSM transition"),
"FAILED+unknown-bit must emit the FSM-ladder rejection warn",
);
}
#[test]
fn set_status_failed_accepted_on_top_of_needs_reset() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
dev.device_status
.fetch_or(VIRTIO_CONFIG_S_NEEDS_RESET, Ordering::SeqCst);
assert_eq!(
dev.device_status.load(Ordering::Acquire),
VIRTIO_CONFIG_S_NEEDS_RESET,
);
let current = dev.device_status.load(Ordering::Acquire);
write_reg(
&mut dev,
VIRTIO_MMIO_STATUS,
current | VIRTIO_CONFIG_S_FAILED,
);
assert_eq!(
dev.device_status.load(Ordering::Acquire),
VIRTIO_CONFIG_S_NEEDS_RESET | VIRTIO_CONFIG_S_FAILED,
"FAILED must land alongside NEEDS_RESET when the kernel \
follows the get_status | FAILED write sequence",
);
}
#[tracing_test::traced_test]
#[test]
fn set_status_failed_idempotent_rewrite_is_noop() {
let mut dev = make_device(VIRTIO_BLK_DEFAULT_CAPACITY_BYTES, DiskThrottle::default());
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.load(Ordering::Acquire),
S_ACK | VIRTIO_CONFIG_S_FAILED,
"FAILED accepted on first write",
);
write_reg(&mut dev, VIRTIO_MMIO_STATUS, S_ACK | VIRTIO_CONFIG_S_FAILED);
assert_eq!(
dev.device_status.load(Ordering::Acquire),
S_ACK | VIRTIO_CONFIG_S_FAILED,
"idempotent FAILED re-write must NOT alter device_status",
);
assert!(
!logs_contain("illegal FSM transition"),
"idempotent FAILED re-write must NOT emit the illegal-transition warn",
);
assert!(
!logs_contain("attempted to clear"),
"idempotent FAILED re-write must NOT emit the clear-bit warn",
);
}