diff --git a/.github/workflows/Fuzzing.yml b/.github/workflows/Fuzzing.yml index bcf14dcfd..411acb4f7 100644 --- a/.github/workflows/Fuzzing.yml +++ b/.github/workflows/Fuzzing.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - target: ['fuzz_host_print', 'fuzz_guest_call', 'fuzz_host_call', 'fuzz_guest_estimate_trace_event', 'fuzz_guest_trace'] + target: ['fuzz_host_print', 'fuzz_guest_call', 'fuzz_host_call', 'fuzz_guest_estimate_trace_event', 'fuzz_guest_trace', 'fuzz_virtq_packed_ring'] uses: ./.github/workflows/dep_fuzzing.yml with: target: ${{ matrix.target }} diff --git a/.github/workflows/ValidatePullRequest.yml b/.github/workflows/ValidatePullRequest.yml index 8da277e07..410541e71 100644 --- a/.github/workflows/ValidatePullRequest.yml +++ b/.github/workflows/ValidatePullRequest.yml @@ -214,7 +214,7 @@ jobs: if: ${{ !cancelled() && !failure() }} strategy: matrix: - target: ['fuzz_host_print', 'fuzz_guest_call', 'fuzz_host_call', 'fuzz_guest_estimate_trace_event', 'fuzz_guest_trace'] + target: ['fuzz_host_print', 'fuzz_guest_call', 'fuzz_host_call', 'fuzz_guest_estimate_trace_event', 'fuzz_guest_trace', 'fuzz_virtq_packed_ring'] arch: - X64 # arm64 fuzzing runs on the daily schedule (DailyArm64.yml) instead of on diff --git a/Justfile b/Justfile index 1eb4dd3a0..5112f2f88 100644 --- a/Justfile +++ b/Justfile @@ -218,6 +218,7 @@ like-ci config=default-target hypervisor="kvm": just fuzz-like-ci fuzz_host_call {{config}} {{hypervisor}} just fuzz-like-ci fuzz_guest_estimate_trace_event {{config}} {{hypervisor}} just fuzz-like-ci fuzz_guest_trace {{config}} {{hypervisor}} + just fuzz-like-ci fuzz_virtq_packed_ring {{config}} {{hypervisor}} @# spelling typos diff --git a/fuzz/.gitignore b/fuzz/.gitignore index 1a45eee77..8f252bad6 100644 --- a/fuzz/.gitignore +++ b/fuzz/.gitignore @@ -1,4 +1,7 @@ target -corpus +corpus/* +!corpus/fuzz_virtq_packed_ring/ +corpus/fuzz_virtq_packed_ring/* +!corpus/fuzz_virtq_packed_ring/*.bin artifacts coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 9a29bf8a2..c2bdfe1cf 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -48,6 +48,13 @@ test = false doc = false bench = false +[[bin]] +name = "fuzz_virtq_packed_ring" +path = "fuzz_targets/virtq_packed_ring.rs" +test = false +doc = false +bench = false + [features] default = [] trace = ["hyperlight-host/trace_guest", "hyperlight-common/trace_guest"] diff --git a/fuzz/README.md b/fuzz/README.md index 879f42446..b08611786 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -10,7 +10,7 @@ which evaluates to the following command `cargo +nightly fuzz run fuzz_host_prin As per Microsoft's Offensive Research & Security Engineering (MORSE) team, all host exposed functions that receive or interact with guest data must be continuously fuzzed for, at least, 500 million fuzz test cases without any crashes. Because `cargo-fuzz` doesn't support setting a maximum number of iterations; instead, we use the `--max_total_time` flag to set a maximum time to run the fuzzer. We have a GitHub action (acting like a CRON job) that runs the fuzzers for 24 hours every week. -Currently, we fuzz the parameters and return type to a hardcoded `PrintOutput` guest function, and the `HostPrint` host function. We plan to add more fuzzers in the future. +Currently, we fuzz the parameters and return type to a hardcoded `PrintOutput` guest function, the `HostPrint` host function, and the packed virtqueue ring parser. We plan to add more fuzzers in the future. ## On Failure diff --git a/fuzz/corpus/fuzz_virtq_packed_ring/event-off-qsize.bin b/fuzz/corpus/fuzz_virtq_packed_ring/event-off-qsize.bin new file mode 100644 index 000000000..b1d4805da Binary files /dev/null and b/fuzz/corpus/fuzz_virtq_packed_ring/event-off-qsize.bin differ diff --git a/fuzz/corpus/fuzz_virtq_packed_ring/indirect-head.bin b/fuzz/corpus/fuzz_virtq_packed_ring/indirect-head.bin new file mode 100644 index 000000000..6658350c7 Binary files /dev/null and b/fuzz/corpus/fuzz_virtq_packed_ring/indirect-head.bin differ diff --git a/fuzz/corpus/fuzz_virtq_packed_ring/long-chain.bin b/fuzz/corpus/fuzz_virtq_packed_ring/long-chain.bin new file mode 100644 index 000000000..15fab2d68 Binary files /dev/null and b/fuzz/corpus/fuzz_virtq_packed_ring/long-chain.bin differ diff --git a/fuzz/corpus/fuzz_virtq_packed_ring/oob-id.bin b/fuzz/corpus/fuzz_virtq_packed_ring/oob-id.bin new file mode 100644 index 000000000..a4763db5d Binary files /dev/null and b/fuzz/corpus/fuzz_virtq_packed_ring/oob-id.bin differ diff --git a/fuzz/corpus/fuzz_virtq_packed_ring/valid-single-writable.bin b/fuzz/corpus/fuzz_virtq_packed_ring/valid-single-writable.bin new file mode 100644 index 000000000..ca7f32fd6 Binary files /dev/null and b/fuzz/corpus/fuzz_virtq_packed_ring/valid-single-writable.bin differ diff --git a/fuzz/corpus/fuzz_virtq_packed_ring/writable-before-readable.bin b/fuzz/corpus/fuzz_virtq_packed_ring/writable-before-readable.bin new file mode 100644 index 000000000..d2cb9973f Binary files /dev/null and b/fuzz/corpus/fuzz_virtq_packed_ring/writable-before-readable.bin differ diff --git a/fuzz/fuzz_targets/virtq_packed_ring.rs b/fuzz/fuzz_targets/virtq_packed_ring.rs new file mode 100644 index 000000000..bcde1bfc4 --- /dev/null +++ b/fuzz/fuzz_targets/virtq_packed_ring.rs @@ -0,0 +1,271 @@ +/* +Copyright 2026 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#![no_main] + +use std::cell::UnsafeCell; +use std::num::NonZeroU16; +use std::ops::Range; +use std::rc::Rc; + +use hyperlight_common::virtq::{Descriptor, Layout, MemOps, RingConsumer}; +use libfuzzer_sys::{Corpus, fuzz_target}; + +const DEFAULT_QUEUE_SIZE: usize = 16; +const MAX_QUEUE_SIZE: usize = 64; +const MAX_DESCS: usize = 64; +const PAYLOAD_SIZE: usize = 4096; +const BASE_ADDR: u64 = 0x1000; +const HEADER_SIZE: usize = 16; +const DESC_SIZE: usize = 12; + +#[derive(Clone, Debug)] +struct FuzzDesc { + addr_offset: u32, + len: u32, + id: u16, + flags: u16, +} + +#[derive(Clone, Debug)] +struct FuzzCase { + queue_size: usize, + driver_event_off_wrap: u16, + driver_event_flags: u16, + written_len: u32, + poll_count: usize, + descs: Vec, +} + +#[derive(Clone)] +struct FuzzMem { + inner: Rc, +} + +struct FuzzMemInner { + storage: UnsafeCell>, + base_addr: u64, +} + +impl FuzzMem { + fn new(base_addr: u64, size: usize) -> Self { + Self { + inner: Rc::new(FuzzMemInner { + storage: UnsafeCell::new(vec![0; size]), + base_addr, + }), + } + } + + fn range(&self, addr: u64, len: usize) -> Result, ()> { + let offset = addr.checked_sub(self.inner.base_addr).ok_or(())? as usize; + let end = offset.checked_add(len).ok_or(())?; + let storage_len = unsafe { &*self.inner.storage.get() }.len(); + if end > storage_len { + return Err(()); + } + + Ok(offset..end) + } +} + +// SAFETY: `FuzzMem` bounds-checks every translated address against its owned +// backing storage and reports failures instead of dereferencing invalid memory. +unsafe impl MemOps for FuzzMem { + type Error = (); + + fn read(&self, addr: u64, dst: &mut [u8]) -> Result<(), Self::Error> { + let range = self.range(addr, dst.len())?; + let storage = unsafe { &*self.inner.storage.get() }; + dst.copy_from_slice(&storage[range]); + Ok(()) + } + + fn write(&self, addr: u64, src: &[u8]) -> Result<(), Self::Error> { + let range = self.range(addr, src.len())?; + let storage = unsafe { &mut *self.inner.storage.get() }; + storage[range].copy_from_slice(src); + Ok(()) + } + + fn load_acquire(&self, addr: u64) -> Result { + let mut bytes = [0; 2]; + self.read(addr, &mut bytes)?; + Ok(u16::from_le_bytes(bytes)) + } + + fn store_release(&self, addr: u64, val: u16) -> Result<(), Self::Error> { + self.write(addr, &val.to_le_bytes()) + } + + unsafe fn as_slice(&self, addr: u64, len: usize) -> Result<&[u8], Self::Error> { + let range = self.range(addr, len)?; + let storage = unsafe { &*self.inner.storage.get() }; + Ok(&storage[range]) + } + + #[allow(clippy::mut_from_ref)] + unsafe fn as_mut_slice(&self, addr: u64, len: usize) -> Result<&mut [u8], Self::Error> { + let range = self.range(addr, len)?; + let storage = unsafe { &mut *self.inner.storage.get() }; + Ok(&mut storage[range]) + } +} + +fn write_driver_event(mem: &FuzzMem, layout: Layout, off_wrap: u16, flags: u16) -> Result<(), ()> { + mem.write( + layout.drv_evt_addr(), + &[ + (off_wrap & 0xff) as u8, + (off_wrap >> 8) as u8, + (flags & 0xff) as u8, + (flags >> 8) as u8, + ], + ) +} + +/// Parse a compact little-endian packed-ring blob: +/// +/// ```text +/// u16 queue_size +/// u16 desc_count +/// u16 driver_event_off_wrap +/// u16 driver_event_flags +/// u32 written_len +/// u8 poll_count +/// u8 reserved[3] +/// desc[desc_count]: +/// u32 addr_offset +/// u32 len +/// u16 id +/// u16 flags +/// ``` +fn parse_case(data: &[u8]) -> Option { + if data.len() < HEADER_SIZE { + return None; + } + + let read_u16 = |i: usize| u16::from_le_bytes([data[i], data[i + 1]]); + let read_u32 = |i: usize| u32::from_le_bytes([data[i], data[i + 1], data[i + 2], data[i + 3]]); + + let raw_queue_size = read_u16(0); + let queue_size = normalize_queue_size(raw_queue_size); + let desc_count = usize::from(read_u16(2)).min(MAX_DESCS).min(queue_size); + + let driver_event_off_wrap = read_u16(4); + let driver_event_flags = read_u16(6); + let written_len = read_u32(8); + let poll_count = usize::from(data[12]).min(8); + + let desc_bytes = desc_count.checked_mul(DESC_SIZE)?; + if data.len() < HEADER_SIZE.checked_add(desc_bytes)? { + return None; + } + + let mut descs = Vec::with_capacity(desc_count); + let mut offset = HEADER_SIZE; + + for _ in 0..desc_count { + descs.push(FuzzDesc { + addr_offset: read_u32(offset), + len: read_u32(offset + 4), + id: read_u16(offset + 8), + flags: read_u16(offset + 10), + }); + offset += DESC_SIZE; + } + + Some(FuzzCase { + queue_size, + driver_event_off_wrap, + driver_event_flags, + written_len, + poll_count, + descs, + }) +} + +fn normalize_queue_size(raw: u16) -> usize { + let raw = usize::from(raw); + if raw == 0 || !raw.is_power_of_two() { + return DEFAULT_QUEUE_SIZE; + } + + raw.min(MAX_QUEUE_SIZE) +} + +fn run_case(case: FuzzCase) -> Corpus { + let Some(num_descs) = NonZeroU16::new(case.queue_size as u16) else { + return Corpus::Reject; + }; + + let ring_size = Layout::query_size(case.queue_size); + let mem = FuzzMem::new(BASE_ADDR, ring_size + PAYLOAD_SIZE); + let layout = match unsafe { Layout::from_base(BASE_ADDR, num_descs) } { + Ok(layout) => layout, + Err(_) => return Corpus::Reject, + }; + + if write_driver_event( + &mem, + layout, + case.driver_event_off_wrap, + case.driver_event_flags, + ) + .is_err() + { + return Corpus::Reject; + } + + let payload_base = BASE_ADDR + ring_size as u64; + for (idx, fuzz_desc) in case.descs.iter().enumerate() { + let payload_offset = fuzz_desc.addr_offset as usize % PAYLOAD_SIZE; + let desc = Descriptor { + addr: payload_base + payload_offset as u64, + len: fuzz_desc.len, + id: fuzz_desc.id, + flags: fuzz_desc.flags, + }; + let desc_addr = layout.desc_table_addr() + idx as u64 * Descriptor::SIZE as u64; + if mem.write_val(desc_addr, desc).is_err() { + return Corpus::Reject; + } + } + + let mut consumer = RingConsumer::new(layout, mem); + for _ in 0..case.poll_count { + let Ok((id, _chain)) = consumer.poll_available() else { + break; + }; + + if consumer + .submit_used_with_notify(id, case.written_len) + .is_err() + { + break; + } + } + + Corpus::Keep +} + +fuzz_target!(|data: &[u8]| -> Corpus { + let Some(case) = parse_case(data) else { + return Corpus::Reject; + }; + + run_case(case) +}); diff --git a/src/hyperlight_common/src/virtq/consumer.rs b/src/hyperlight_common/src/virtq/consumer.rs index a71ff7cae..d4fe0ff45 100644 --- a/src/hyperlight_common/src/virtq/consumer.rs +++ b/src/hyperlight_common/src/virtq/consumer.rs @@ -697,6 +697,46 @@ mod tests { assert!(producer.poll().unwrap().is_some()); } + #[test] + fn test_villain_indirect_descriptor_does_not_mark_high_level_inflight() { + let ring = make_ring(16); + let mem = ring.mem(); + let mut consumer = VirtqConsumer::new(ring.layout(), mem, TestNotifier::new()); + + let mut desc = Descriptor::new(0x1000, 16, 0, DescFlags::INDIRECT); + desc.mark_avail(true); + ring.write_desc(0, desc); + + assert!(matches!( + consumer.poll(1024), + Err(VirtqError::RingError(RingError::BadChain)) + )); + assert_eq!(consumer.inflight.count_ones(..), 0); + assert_eq!(consumer.inner.num_inflight(), 0); + } + + #[test] + fn test_villain_bad_chain_does_not_mark_high_level_inflight() { + let ring = make_ring(16); + let mem = ring.mem(); + let mut consumer = VirtqConsumer::new(ring.layout(), mem, TestNotifier::new()); + + let mut first = Descriptor::new(0x1000, 16, 0, DescFlags::NEXT | DescFlags::WRITE); + first.mark_avail(true); + ring.write_desc(0, first); + + let mut second = Descriptor::new(0x2000, 16, 0, DescFlags::empty()); + second.mark_avail(true); + ring.write_desc(1, second); + + assert!(matches!( + consumer.poll(1024), + Err(VirtqError::RingError(RingError::BadChain)) + )); + assert_eq!(consumer.inflight.count_ones(..), 0); + assert_eq!(consumer.inner.num_inflight(), 0); + } + #[test] fn test_writable_chain_writes_single_segment() { let ring = make_ring(16); diff --git a/src/hyperlight_common/src/virtq/desc.rs b/src/hyperlight_common/src/virtq/desc.rs index c1af05060..9383f0acc 100644 --- a/src/hyperlight_common/src/virtq/desc.rs +++ b/src/hyperlight_common/src/virtq/desc.rs @@ -29,7 +29,7 @@ bitflags! { /// Descriptor flags as defined by VIRTIO specification. /// /// Note: The implementation never follows the indirect-table interpretation, - /// so INDIRECT bit is effectively ignored. + /// so descriptors carrying INDIRECT are rejected as malformed. #[repr(transparent)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct DescFlags: u16 { diff --git a/src/hyperlight_common/src/virtq/producer.rs b/src/hyperlight_common/src/virtq/producer.rs index 00abf0cd0..1d3750e6c 100644 --- a/src/hyperlight_common/src/virtq/producer.rs +++ b/src/hyperlight_common/src/virtq/producer.rs @@ -1003,7 +1003,7 @@ fn checked_descriptor_len(len: usize) -> Result { #[cfg(test)] mod tests { use super::*; - use crate::virtq::ring::tests::{TestMem, make_ring}; + use crate::virtq::ring::tests::{TestMem, make_consumer, make_ring}; use crate::virtq::test_utils::*; fn poll_received( @@ -1726,6 +1726,45 @@ mod tests { assert!(matches!(producer.poll(), Err(VirtqError::MemoryReadError))); } + #[test] + fn test_villain_used_len_exceeding_writable_capacity_is_rejected_and_released() { + let ring = make_ring(16); + let (mut producer, _consumer, _notifier) = make_test_producer(&ring); + let mut ring_consumer = make_consumer(&ring); + + let se = producer.chain().writable(4).build().unwrap(); + producer.submit(se).unwrap(); + + let (id, _) = ring_consumer.poll_available().unwrap(); + ring_consumer.submit_used(id, 8).unwrap(); + + assert!(matches!(producer.poll(), Err(VirtqError::InvalidState))); + assert_eq!(producer.inner.num_inflight(), 0); + + let se = producer.chain().writable(4).build().unwrap(); + producer.submit(se).unwrap(); + assert_eq!(producer.inner.num_inflight(), 1); + } + + #[test] + fn test_villain_used_descriptor_with_invalid_id_is_rejected() { + let ring = make_ring(16); + let (mut producer, _consumer, _notifier) = make_test_producer(&ring); + + let se = producer.chain().writable(4).build().unwrap(); + producer.submit(se).unwrap(); + + let mut desc = Descriptor::new(0, 0, ring.len() as u16, DescFlags::empty()); + desc.mark_used(true); + ring.write_desc(0, desc); + + assert!(matches!( + producer.poll(), + Err(VirtqError::RingError(RingError::InvalidState)) + )); + assert_eq!(producer.inner.num_inflight(), 1); + } + #[test] fn test_virtq_producer_reset() { let ring = make_ring(16); diff --git a/src/hyperlight_common/src/virtq/ring.rs b/src/hyperlight_common/src/virtq/ring.rs index 12aa8800b..060c70765 100644 --- a/src/hyperlight_common/src/virtq/ring.rs +++ b/src/hyperlight_common/src/virtq/ring.rs @@ -1039,6 +1039,10 @@ impl RingConsumer { let head_desc = Descriptor::read_body(&self.mem, head_addr, flags) .map_err(|_| RingError::mem_err(MemOp::ReadDesc, head_addr))?; + if flags.contains(DescFlags::INDIRECT) { + return Err(RingError::BadChain); + } + // Build chain (head + tails), tracking readable/writable split inline. let mut elements = SmallVec::<[BufferElement; 16]>::new(); let mut pos = self.avail_cursor; @@ -1066,6 +1070,10 @@ impl RingConsumer { .mem .read_val(addr) .map_err(|_| RingError::mem_err(MemOp::ReadDesc, addr))?; + if desc.flags().contains(DescFlags::INDIRECT) { + return Err(RingError::BadChain); + } + let elem = BufferElement::from(&desc); if elem.writable { @@ -1359,6 +1367,10 @@ fn should_notify(evt: EventSuppression, ring_len: u16, old: RingCursor, new: Rin EventFlags::ENABLE => true, EventFlags::DESC => { let mut off = evt.desc_event_off(); + if off >= ring_len { + return false; + } + let wrap = evt.desc_event_wrap(); if wrap != new.wrap() { @@ -3421,6 +3433,795 @@ pub(crate) mod tests { } } +// Adopted from https://github.com/weltling/virtio-villain/tree/main/tests/packed +#[cfg(test)] +mod virtio_villain { + use super::tests::{OwnedRing, make_consumer, make_producer, make_ring}; + use super::*; + + fn raw_desc(id: u16, len: u32, flags: DescFlags) -> Descriptor { + Descriptor::new(0x1000 + u64::from(id) * 0x10, len, id, flags) + } + + fn avail_desc(id: u16, len: u32, flags: DescFlags) -> Descriptor { + let mut desc = raw_desc(id, len, flags); + desc.mark_avail(true); + desc + } + + fn write_avail(ring: &OwnedRing, idx: u16, id: u16, flags: DescFlags) { + write_avail_len(ring, idx, id, 16, flags); + } + + fn write_avail_len(ring: &OwnedRing, idx: u16, id: u16, len: u32, flags: DescFlags) { + ring.write_desc(idx, avail_desc(id, len, flags)); + } + + fn write_driver_event(ring: &OwnedRing, off_wrap: u16, flags: u16) { + write_event(ring, ring.layout().drv_evt_addr(), off_wrap, flags); + } + + fn write_device_event(ring: &OwnedRing, off_wrap: u16, flags: u16) { + write_event(ring, ring.layout().dev_evt_addr(), off_wrap, flags); + } + + fn write_event(ring: &OwnedRing, addr: u64, off_wrap: u16, flags: u16) { + ring.mem() + .write( + addr, + &[ + (off_wrap & 0xff) as u8, + (off_wrap >> 8) as u8, + (flags & 0xff) as u8, + (flags >> 8) as u8, + ], + ) + .unwrap(); + } + + #[test] + fn p0003_chain_with_next_past_ring_is_rejected() { + let ring = make_ring(4); + let mut consumer = make_consumer(&ring); + + for idx in 0..ring.len() as u16 { + write_avail(&ring, idx, 0, DescFlags::NEXT); + } + + assert!(matches!( + consumer.poll_available(), + Err(RingError::BadChain) + )); + } + + #[test] + fn p0005_indirect_without_feature_is_rejected() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_avail(&ring, 0, 0, DescFlags::INDIRECT); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::BadChain) + )); + } + + #[test] + fn p0006_indirect_with_bad_flags_is_rejected() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_avail(&ring, 0, 0, DescFlags::INDIRECT | DescFlags::WRITE); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::BadChain) + )); + } + + #[test] + fn p0007_indirect_tail_in_mixed_chain_is_rejected() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_avail(&ring, 0, 0, DescFlags::NEXT); + write_avail(&ring, 1, 0, DescFlags::INDIRECT | DescFlags::WRITE); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::BadChain) + )); + } + + #[test] + fn p0008_long_chain_is_consumable_at_ring_layer() { + let ring = make_ring(16); + let mut consumer = make_consumer(&ring); + + for idx in 0..12u16 { + let flags = if idx == 11 { + DescFlags::WRITE + } else { + DescFlags::NEXT | DescFlags::WRITE + }; + write_avail(&ring, idx, 0, flags); + } + + let (id, chain) = consumer.poll_available().unwrap(); + assert_eq!(id, 0); + assert_eq!(chain.len(), 12); + } + + #[test] + fn p0009_unaligned_descriptor_address_is_preserved() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + let mut desc = Descriptor::new(0x1003, 16, 0, DescFlags::empty()); + desc.mark_avail(true); + ring.write_desc(0, desc); + + let (_, chain) = consumer.poll_available().unwrap(); + assert_eq!(chain.elems()[0].addr, 0x1003); + } + + #[test] + fn p0010_writable_before_readable_is_rejected() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_avail(&ring, 0, 0, DescFlags::NEXT); + write_avail(&ring, 1, 0, DescFlags::NEXT | DescFlags::WRITE); + write_avail(&ring, 2, 0, DescFlags::NEXT); + write_avail(&ring, 3, 0, DescFlags::WRITE); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::BadChain) + )); + } + + #[test] + fn p0011_stale_wrap_descriptor_is_ignored() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + let mut desc = raw_desc(0, 16, DescFlags::empty()); + desc.mark_avail(false); + ring.write_desc(0, desc); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::WouldBlock) + )); + } + + #[test] + fn p0012_multidescriptor_oob_id_is_rejected() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + let bad_id = ring.len() as u16; + + write_avail(&ring, 0, bad_id, DescFlags::NEXT); + write_avail(&ring, 1, bad_id, DescFlags::WRITE); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::InvalidState) + )); + } + + #[test] + fn p0013_duplicate_inflight_buffer_id_is_rejected() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_avail(&ring, 0, 0, DescFlags::NEXT); + write_avail(&ring, 1, 0, DescFlags::WRITE); + write_avail(&ring, 2, 0, DescFlags::NEXT); + write_avail(&ring, 3, 0, DescFlags::WRITE); + + let (first_id, first_chain) = consumer.poll_available().unwrap(); + assert_eq!(first_id, 0); + assert_eq!(first_chain.len(), 2); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::InvalidState) + )); + } + + #[test] + fn p0014_reserved_driver_event_flags_do_not_block_processing() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_driver_event(&ring, 0, 3); + write_avail(&ring, 0, 0, DescFlags::empty()); + + let (id, chain) = consumer.poll_available().unwrap(); + assert_eq!(id, 0); + assert_eq!(chain.len(), 1); + } + + #[test] + fn p0015_wrap_at_zero_is_consumed_correctly() { + let ring = make_ring(2); + let mut producer = make_producer(&ring); + let mut consumer = make_consumer(&ring); + + producer.submit_one(0x1000, 16, false).unwrap(); + producer.submit_one(0x2000, 16, false).unwrap(); + + assert_eq!(producer.avail_cursor().head(), 0); + assert!(!producer.avail_cursor().wrap()); + + assert_eq!(consumer.poll_available().unwrap().1.elems()[0].addr, 0x1000); + assert_eq!(consumer.poll_available().unwrap().1.elems()[0].addr, 0x2000); + assert_eq!(consumer.avail_cursor().head(), 0); + assert!(!consumer.avail_cursor().wrap()); + } + + #[test] + fn p0016_queue_size_one_next_chain_is_rejected() { + let ring = make_ring(1); + let mut consumer = make_consumer(&ring); + + write_avail(&ring, 0, 0, DescFlags::NEXT); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::BadChain) + )); + } + + #[test] + fn p0017_stale_driver_event_wrap_suppresses_notification() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_driver_event(&ring, 0, EventFlags::DESC.bits()); + write_avail(&ring, 0, 0, DescFlags::empty()); + + let (id, _) = consumer.poll_available().unwrap(); + + assert!(!consumer.submit_used_with_notify(id, 0).unwrap()); + } + + #[test] + fn p0018_pre_used_descriptor_is_ignored() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + let mut desc = raw_desc(0, 16, DescFlags::empty()); + desc.mark_used(true); + ring.write_desc(0, desc); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::WouldBlock) + )); + } + + #[test] + fn p0019_driver_event_wrap_mismatch_suppresses_notification() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_driver_event(&ring, 1, EventFlags::DESC.bits()); + write_avail(&ring, 0, 0, DescFlags::empty()); + + let (id, _) = consumer.poll_available().unwrap(); + + assert!(!consumer.submit_used_with_notify(id, 0).unwrap()); + } + + #[test] + fn p0020_ring_full_reports_backpressure() { + let ring = make_ring(4); + let mut producer = make_producer(&ring); + + for idx in 0..4 { + producer.submit_one(0x1000 + idx * 0x10, 16, false).unwrap(); + } + + assert!(matches!( + producer.submit_one(0x2000, 16, false), + Err(RingError::WouldBlock) + )); + } + + #[test] + fn p0022_indirect_reused_id_is_rejected_before_id_reuse() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_avail(&ring, 0, 0, DescFlags::INDIRECT); + write_avail(&ring, 1, 0, DescFlags::empty()); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::BadChain) + )); + } + + #[test] + fn p0023_desc_event_mode_suppresses_before_later_slot() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_driver_event(&ring, 0x8001, EventFlags::DESC.bits()); + write_avail(&ring, 0, 0, DescFlags::empty()); + + let (id, _) = consumer.poll_available().unwrap(); + + assert!(!consumer.submit_used_with_notify(id, 0).unwrap()); + } + + #[test] + fn p0024_single_slot_indirect_is_rejected() { + let ring = make_ring(1); + let mut consumer = make_consumer(&ring); + + write_avail(&ring, 0, 0, DescFlags::INDIRECT); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::BadChain) + )); + } + + #[test] + fn p0025_used_completions_can_arrive_out_of_order() { + let ring = make_ring(8); + let mut producer = make_producer(&ring); + let mut consumer = make_consumer(&ring); + + let id0 = producer.submit_one(0x1000, 16, true).unwrap(); + let id1 = producer.submit_one(0x2000, 16, true).unwrap(); + let id2 = producer.submit_one(0x3000, 16, true).unwrap(); + + let (dev0, _) = consumer.poll_available().unwrap(); + let (dev1, _) = consumer.poll_available().unwrap(); + let (dev2, _) = consumer.poll_available().unwrap(); + + consumer.submit_used(dev2, 32).unwrap(); + consumer.submit_used(dev0, 16).unwrap(); + consumer.submit_used(dev1, 24).unwrap(); + + assert_eq!(producer.poll_used().unwrap().id, id2); + assert_eq!(producer.poll_used().unwrap().id, id0); + assert_eq!(producer.poll_used().unwrap().id, id1); + } + + #[test] + fn p0026_all_descriptor_flags_set_is_not_available() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + ring.write_desc(0, raw_desc(0, 16, DescFlags::from_bits_truncate(u16::MAX))); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::WouldBlock) + )); + } + + #[test] + fn p0028_multi_wrap_round_trip_remains_ordered() { + let ring = make_ring(2); + let mut producer = make_producer(&ring); + let mut consumer = make_consumer(&ring); + + for idx in 0..8u64 { + let id = producer.submit_one(0x1000 + idx * 0x10, 16, false).unwrap(); + let (dev_id, _) = consumer.poll_available().unwrap(); + assert_eq!(dev_id, id); + consumer.submit_used(dev_id, idx as u32).unwrap(); + assert_eq!(producer.poll_used().unwrap().id, id); + } + } + + #[test] + fn p0029_desc_event_idx_zero_notifies() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_driver_event(&ring, 0x8000, EventFlags::DESC.bits()); + write_avail(&ring, 0, 0, DescFlags::empty()); + + let (id, _) = consumer.poll_available().unwrap(); + + assert!(consumer.submit_used_with_notify(id, 0).unwrap()); + } + + #[test] + fn p0031_reserved_driver_event_flags_suppress_notification() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_driver_event(&ring, 0, 3); + write_avail(&ring, 0, 0, DescFlags::empty()); + + let (id, _) = consumer.poll_available().unwrap(); + + assert!(!consumer.submit_used_with_notify(id, 0).unwrap()); + } + + #[test] + fn p0032_unaligned_indirect_descriptor_is_rejected() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + let mut desc = Descriptor::new(0x1003, 16, 0, DescFlags::INDIRECT); + desc.mark_avail(true); + ring.write_desc(0, desc); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::BadChain) + )); + } + + #[test] + fn p0033_scatter_gather_chain_is_preserved() { + let ring = make_ring(16); + let mut consumer = make_consumer(&ring); + + for idx in 0..8u16 { + let flags = if idx == 7 { + DescFlags::WRITE + } else { + DescFlags::NEXT + }; + write_avail(&ring, idx, 0, flags); + } + + let (_, chain) = consumer.poll_available().unwrap(); + assert_eq!(chain.len(), 8); + assert_eq!(chain.readables().len(), 7); + assert_eq!(chain.writables().len(), 1); + } + + #[test] + fn p0034_page_boundary_indirect_descriptor_is_rejected() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + let mut desc = Descriptor::new(0x1ff0, 16, 0, DescFlags::INDIRECT); + desc.mark_avail(true); + ring.write_desc(0, desc); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::BadChain) + )); + } + + #[test] + fn p0035_both_avail_and_used_clear_is_ignored() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + ring.write_desc(0, raw_desc(0, 16, DescFlags::empty())); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::WouldBlock) + )); + } + + #[test] + fn p0037_event_flags_can_cycle_between_batches() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + for (idx, (flags, expect_notify)) in [ + (EventFlags::ENABLE.bits(), true), + (EventFlags::DISABLE.bits(), false), + (EventFlags::ENABLE.bits(), true), + ] + .into_iter() + .enumerate() + { + write_driver_event(&ring, 0, flags); + write_avail(&ring, idx as u16, idx as u16, DescFlags::empty()); + let (id, _) = consumer.poll_available().unwrap(); + assert_eq!( + consumer.submit_used_with_notify(id, 0).unwrap(), + expect_notify + ); + } + } + + #[test] + fn p0038_many_wraps_on_minimum_queue_size() { + let ring = make_ring(1); + let mut producer = make_producer(&ring); + let mut consumer = make_consumer(&ring); + + for idx in 0..8u64 { + let id = producer.submit_one(0x1000 + idx * 0x10, 16, false).unwrap(); + let (dev_id, _) = consumer.poll_available().unwrap(); + assert_eq!(dev_id, id); + consumer.submit_used(dev_id, 0).unwrap(); + assert_eq!(producer.poll_used().unwrap().id, id); + } + } + + #[test] + fn p0039_reserved_device_event_flags_suppress_avail_notification() { + let ring = make_ring(8); + let mut producer = make_producer(&ring); + + write_device_event(&ring, 0, 3); + + assert!( + !producer + .submit_one_with_notify(0x1000, 16, false) + .unwrap() + .notify + ); + } + + #[test] + fn p0040_indirect_with_next_is_rejected() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_avail(&ring, 0, 0, DescFlags::INDIRECT | DescFlags::NEXT); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::BadChain) + )); + } + + #[test] + fn p0041_driver_event_offset_at_queue_size_suppresses_notification() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_driver_event(&ring, ring.len() as u16, EventFlags::DESC.bits()); + write_avail(&ring, 0, 0, DescFlags::empty()); + + let (id, _) = consumer.poll_available().unwrap(); + + assert!(!consumer.submit_used_with_notify(id, 0).unwrap()); + } + + #[test] + fn p0042_empty_ring_has_no_available_work() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::WouldBlock) + )); + } + + #[test] + fn p0043_id_equal_to_queue_size_is_rejected() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_avail(&ring, 0, ring.len() as u16, DescFlags::empty()); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::InvalidState) + )); + } + + #[test] + fn p0044_avail_and_used_set_is_not_available() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + ring.write_desc(0, raw_desc(0, 16, DescFlags::AVAIL | DescFlags::USED)); + + assert!(!consumer.peek_available().unwrap()); + assert!(matches!( + consumer.poll_available(), + Err(RingError::WouldBlock) + )); + } + + #[test] + fn p0045_zero_length_descriptor_is_consumable() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_avail_len(&ring, 0, 0, 0, DescFlags::WRITE); + + let (id, chain) = consumer.poll_available().unwrap(); + assert_eq!(id, 0); + assert_eq!(chain.writables()[0].len, 0); + } + + #[test] + fn p0046_max_length_descriptor_is_consumable_at_ring_layer() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_avail_len(&ring, 0, 0, u32::MAX, DescFlags::WRITE); + + let (id, chain) = consumer.poll_available().unwrap(); + assert_eq!(id, 0); + assert_eq!(chain.writables()[0].len, u32::MAX); + } + + #[test] + fn p0047_zero_descriptor_address_is_preserved_at_ring_layer() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + let mut desc = Descriptor::new(0, 16, 0, DescFlags::WRITE); + desc.mark_avail(true); + ring.write_desc(0, desc); + + let (id, chain) = consumer.poll_available().unwrap(); + assert_eq!(id, 0); + assert_eq!(chain.writables()[0].addr, 0); + } + + #[test] + fn p0049_indirect_entry_with_phase_bits_is_rejected() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + ring.write_desc( + 0, + raw_desc( + 0, + 16, + DescFlags::INDIRECT | DescFlags::AVAIL | DescFlags::USED, + ), + ); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::WouldBlock) + )); + } + + #[test] + fn p0050_nested_indirect_is_rejected_at_head() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_avail(&ring, 0, 0, DescFlags::INDIRECT); + + assert!(matches!( + consumer.poll_available(), + Err(RingError::BadChain) + )); + } + + #[test] + fn p0051_write_flag_controls_readable_writable_split() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_avail(&ring, 0, 0, DescFlags::NEXT); + write_avail(&ring, 1, 0, DescFlags::WRITE); + + let (_, chain) = consumer.poll_available().unwrap(); + assert_eq!(chain.readables().len(), 1); + assert_eq!(chain.writables().len(), 1); + } + + #[test] + fn p0052_batch_completion_signals_each_chain_head() { + let ring = make_ring(8); + let mut producer = make_producer(&ring); + let mut consumer = make_consumer(&ring); + + let id0 = producer.submit_one(0x1000, 16, true).unwrap(); + let id1 = producer.submit_one(0x2000, 16, true).unwrap(); + + let (dev0, _) = consumer.poll_available().unwrap(); + let (dev1, _) = consumer.poll_available().unwrap(); + consumer.submit_used(dev0, 16).unwrap(); + consumer.submit_used(dev1, 32).unwrap(); + + assert_eq!(producer.poll_used().unwrap().id, id0); + assert_eq!(producer.poll_used().unwrap().id, id1); + } + + #[test] + fn p0053_descriptor_without_avail_bit_is_ignored() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + ring.write_desc(0, raw_desc(0, 16, DescFlags::WRITE)); + + assert!(!consumer.peek_available().unwrap()); + assert!(matches!( + consumer.poll_available(), + Err(RingError::WouldBlock) + )); + } + + #[test] + fn p0054_event_suppression_struct_layout_matches_packed_ring() { + assert_eq!(EventSuppression::SIZE, 4); + assert_eq!(EventSuppression::WRAP_OFFSET, 0); + assert_eq!(EventSuppression::FLAGS_OFFSET, 2); + } + + #[test] + fn p0055_descriptor_after_chain_tail_is_ignored() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + write_avail(&ring, 0, 0, DescFlags::empty()); + write_avail(&ring, 1, 1, DescFlags::WRITE); + + let (id, chain) = consumer.poll_available().unwrap(); + assert_eq!(id, 0); + assert_eq!(chain.len(), 1); + } + + #[test] + fn p0056_next_chain_order_is_preserved() { + let ring = make_ring(8); + let mut consumer = make_consumer(&ring); + + for idx in 0..3u16 { + let mut desc = Descriptor::new( + 0x1000 + u64::from(idx) * 0x100, + 16, + 0, + if idx == 2 { + DescFlags::WRITE + } else { + DescFlags::NEXT + }, + ); + desc.mark_avail(true); + ring.write_desc(idx, desc); + } + + let (_, chain) = consumer.poll_available().unwrap(); + assert_eq!(chain.elems()[0].addr, 0x1000); + assert_eq!(chain.elems()[1].addr, 0x1100); + assert_eq!(chain.elems()[2].addr, 0x1200); + } + + #[test] + fn p0057_device_event_enable_requests_avail_notification() { + let ring = make_ring(8); + let mut producer = make_producer(&ring); + + write_device_event(&ring, 0, EventFlags::ENABLE.bits()); + + assert!( + producer + .submit_one_with_notify(0x1000, 16, false) + .unwrap() + .notify + ); + } + + #[test] + fn p0058_used_descriptor_carries_id_and_len() { + let ring = make_ring(8); + let mut producer = make_producer(&ring); + let mut consumer = make_consumer(&ring); + + let id = producer.submit_one(0x1000, 16, true).unwrap(); + let (dev_id, _) = consumer.poll_available().unwrap(); + consumer.submit_used(dev_id, 42).unwrap(); + + let used = producer.poll_used().unwrap(); + assert_eq!(used.id, id); + assert_eq!(used.len, 42); + } +} + #[cfg(test)] mod fuzz { use quickcheck::{Arbitrary, Gen, QuickCheck};