Skip to main content

arcbox_virtio_core/
split_queue.rs

1//! Unified split-virtqueue over guest physical memory.
2//!
3//! `SplitQueue` is the single queue abstraction every ArcBox VirtIO device and
4//! worker uses to walk the available ring, parse descriptor chains, publish to
5//! the used ring, and decide whether to interrupt the guest. It replaces the
6//! previously-duplicated implementations (`queue::VirtQueue` local rings,
7//! `queue_guest::GuestMemoryVirtQueue` raw pointers, the per-device inline GPA
8//! walks, `arcbox-vmm::virtqueue_util`, and the net-TX inline used-ring write).
9//!
10//! All addresses are guest physical addresses (GPAs); the backing
11//! [`GuestMemWriter`] performs the single `gpa_base` translation, so devices
12//! never subtract `gpa_base` themselves.
13//!
14//! Three correctness properties live here exactly once:
15//! - **Cycle-safe chain walk** — bounded by `size`, so a malformed/malicious
16//!   `next` cycle cannot spin the host thread.
17//! - **Acquire on consume** — the `Acquire` fence is placed *after* reading
18//!   `avail.idx` and *before* reading the ring/descriptors, pairing with the
19//!   guest's `Release` on `avail.idx`.
20//! - **Spec-correct notification suppression** — a full `SeqCst` fence between
21//!   publishing `used.idx` and reading `used_event` closes the StoreLoad race
22//!   that otherwise lets the device read a stale `used_event`, suppress a
23//!   needed IRQ, and leave the guest asleep in WFI forever.
24
25use std::sync::Arc;
26use std::sync::atomic::{Ordering, fence};
27
28use virtio_bindings::virtio_ring;
29
30use crate::QueueConfig;
31use crate::guest_mem::GuestMemWriter;
32use crate::queue::flags;
33
34/// Available-ring flag: the guest requests no interrupt on used-buffer
35/// consumption (honored only when EVENT_IDX is not negotiated).
36const VRING_AVAIL_F_NO_INTERRUPT: u16 = virtio_ring::VRING_AVAIL_F_NO_INTERRUPT as u16;
37
38/// A single descriptor read from the descriptor table. `addr` is a GPA.
39#[derive(Debug, Clone, Copy)]
40pub struct VirtqDesc {
41    /// Guest physical address of the buffer.
42    pub addr: u64,
43    /// Buffer length in bytes.
44    pub len: u32,
45    /// Descriptor flags (NEXT / WRITE / INDIRECT).
46    pub flags: u16,
47    /// Index of the next descriptor in the chain (valid iff NEXT is set).
48    pub next: u16,
49}
50
51impl VirtqDesc {
52    /// Whether the device may write this buffer (guest read buffer).
53    #[must_use]
54    pub const fn is_write(&self) -> bool {
55        self.flags & flags::WRITE != 0
56    }
57
58    /// Whether the chain continues at [`Self::next`].
59    #[must_use]
60    pub const fn has_next(&self) -> bool {
61        self.flags & flags::NEXT != 0
62    }
63}
64
65/// A popped descriptor chain: the head index (used as the used-ring id) and the
66/// descriptors walked from it.
67#[derive(Debug)]
68pub struct DescChain {
69    /// Head descriptor index — the id written back to the used ring.
70    pub head_idx: u16,
71    /// Descriptors in chain order.
72    pub descriptors: Vec<VirtqDesc>,
73}
74
75/// Allocation-free walk of a descriptor chain (see [`SplitQueue::chain_iter`]).
76///
77/// Yields `VirtqDesc` by value, following `next` links and bounded by the queue
78/// size so a malformed/malicious cyclic chain terminates.
79pub struct ChainIter<'a> {
80    queue: &'a SplitQueue,
81    idx: u16,
82    /// Remaining iterations before the cycle guard trips (= queue size).
83    ttl: u16,
84    done: bool,
85}
86
87impl Iterator for ChainIter<'_> {
88    type Item = VirtqDesc;
89
90    fn next(&mut self) -> Option<VirtqDesc> {
91        if self.done || self.ttl == 0 || self.idx >= self.queue.size {
92            return None;
93        }
94        self.ttl -= 1;
95        let desc = self.queue.read_descriptor(self.idx);
96        if desc.has_next() {
97            self.idx = desc.next;
98        } else {
99            self.done = true;
100        }
101        Some(desc)
102    }
103}
104
105/// A VirtIO split virtqueue backed by guest physical memory.
106pub struct SplitQueue {
107    mem: Arc<GuestMemWriter>,
108    queue_idx: u16,
109    size: u16,
110    desc_gpa: u64,
111    avail_gpa: u64,
112    used_gpa: u64,
113    /// Next available-ring index the device will consume.
114    last_avail_idx: u16,
115    /// Device's view of `used.idx` (the device is the sole writer).
116    used_idx: u16,
117    /// Whether VIRTIO_F_EVENT_IDX was negotiated.
118    event_idx: bool,
119}
120
121impl SplitQueue {
122    /// Builds a queue from the MMIO-configured ring addresses. Called at
123    /// `QUEUE_READY` time; `last_avail_idx`/`used_idx` start fresh (the guest
124    /// sets up zeroed rings before marking the queue ready).
125    #[must_use]
126    pub fn new(
127        mem: Arc<GuestMemWriter>,
128        queue_idx: u16,
129        cfg: &QueueConfig,
130        event_idx: bool,
131    ) -> Self {
132        // Seed used_idx from the guest's used ring so a transiently
133        // reconstructed queue resumes where the device left off; a freshly
134        // set-up QUEUE_READY ring reads 0.
135        let used_idx = mem.read_u16(cfg.used_addr as usize + 2);
136        Self {
137            mem,
138            queue_idx,
139            size: cfg.size,
140            desc_gpa: cfg.desc_addr,
141            avail_gpa: cfg.avail_addr,
142            used_gpa: cfg.used_addr,
143            last_avail_idx: 0,
144            used_idx,
145            event_idx,
146        }
147    }
148
149    /// The next available-ring index this queue will consume.
150    #[must_use]
151    pub const fn last_avail_idx(&self) -> u16 {
152        self.last_avail_idx
153    }
154
155    /// Seeds the available-ring cursor. Used when a device persists its avail
156    /// position across calls and reconstructs the queue transiently.
157    pub const fn set_last_avail_idx(&mut self, idx: u16) {
158        self.last_avail_idx = idx;
159    }
160
161    /// Queue index within the device.
162    #[must_use]
163    pub const fn queue_idx(&self) -> u16 {
164        self.queue_idx
165    }
166
167    /// Queue size (number of descriptors).
168    #[must_use]
169    pub const fn size(&self) -> u16 {
170        self.size
171    }
172
173    /// Guest memory accessor for reading/writing descriptor buffers by GPA.
174    #[must_use]
175    pub fn mem(&self) -> &GuestMemWriter {
176        &self.mem
177    }
178
179    /// Enable or disable EVENT_IDX notification suppression.
180    pub fn set_event_idx(&mut self, enabled: bool) {
181        self.event_idx = enabled;
182    }
183
184    /// Reads `avail.idx` (the guest's published producer index).
185    fn avail_idx(&self) -> u16 {
186        // avail layout: flags(2) | idx(2) | ring[size] | used_event(2)
187        self.mem.read_u16(self.avail_gpa as usize + 2)
188    }
189
190    /// Reads the available-ring entry (a descriptor head index) at `pos`.
191    fn avail_ring_entry(&self, pos: u16) -> u16 {
192        let off = self.avail_gpa as usize + 4 + (pos % self.size) as usize * 2;
193        self.mem.read_u16(off)
194    }
195
196    /// Reads descriptor `idx` from the descriptor table.
197    fn read_descriptor(&self, idx: u16) -> VirtqDesc {
198        // Each descriptor is 16 bytes: addr(8) | len(4) | flags(2) | next(2).
199        let base = self.desc_gpa as usize + idx as usize * 16;
200        VirtqDesc {
201            addr: self.mem.read_u64(base),
202            len: self.mem.read_u32(base + 8),
203            flags: self.mem.read_u16(base + 12),
204            next: self.mem.read_u16(base + 14),
205        }
206    }
207
208    /// Whether there are available descriptors to process.
209    #[must_use]
210    pub fn has_avail(&self) -> bool {
211        self.avail_idx() != self.last_avail_idx
212    }
213
214    /// Pops the next available descriptor chain, or `None` if the ring is empty.
215    ///
216    /// The chain walk is bounded by `size`: a cyclic `next` chain from a
217    /// malformed or malicious guest terminates instead of spinning.
218    pub fn pop_avail(&mut self) -> Option<DescChain> {
219        let head_idx = self.next_avail_head()?;
220        let descriptors = self.chain_iter(head_idx).collect();
221        Some(DescChain {
222            head_idx,
223            descriptors,
224        })
225    }
226
227    /// Pops the next available chain head, advancing the avail cursor, or
228    /// returns `None` if the ring is empty. Pair with [`Self::chain_iter`] for
229    /// an allocation-free chain walk on per-packet hot paths.
230    pub fn next_avail_head(&mut self) -> Option<u16> {
231        let avail_idx = self.avail_idx();
232        if avail_idx == self.last_avail_idx {
233            return None;
234        }
235        // Acquire pairs with the guest's Release on avail.idx: read the index
236        // first, then fence, then read the ring entry and descriptors so they
237        // observe the guest's writes. (queue_guest fenced before the idx read —
238        // the wrong side — which ordered nothing useful.)
239        fence(Ordering::Acquire);
240        let head_idx = self.avail_ring_entry(self.last_avail_idx);
241        self.last_avail_idx = self.last_avail_idx.wrapping_add(1);
242        Some(head_idx)
243    }
244
245    /// Returns an allocation-free iterator over the descriptor chain starting
246    /// at `head`. Bounded by the queue size so a cyclic `next` chain
247    /// terminates. Each yielded descriptor's `addr` is a GPA; access its buffer
248    /// through [`Self::mem`].
249    #[must_use]
250    pub fn chain_iter(&self, head: u16) -> ChainIter<'_> {
251        ChainIter {
252            queue: self,
253            idx: head,
254            ttl: self.size,
255            done: false,
256        }
257    }
258
259    /// Writes one used-ring entry at the current `used.idx` slot (no idx bump).
260    fn write_used_entry(&self, head_idx: u16, len: u32) {
261        // used layout: flags(2) | idx(2) | ring[size]{id(4),len(4)} | avail_event(2)
262        let off = self.used_gpa as usize + 4 + (self.used_idx % self.size) as usize * 8;
263        self.mem.write_u32(off, u32::from(head_idx));
264        self.mem.write_u32(off + 4, len);
265    }
266
267    /// Publishes a single completion. Returns whether the guest must be
268    /// interrupted (see [`Self::should_notify`]).
269    pub fn push_used(&mut self, head_idx: u16, len: u32) -> bool {
270        let old_used = self.used_idx;
271        self.write_used_entry(head_idx, len);
272        self.used_idx = self.used_idx.wrapping_add(1);
273        // Release: the entry data must be visible before the guest observes the
274        // advanced used.idx.
275        fence(Ordering::Release);
276        self.mem
277            .write_u16(self.used_gpa as usize + 2, self.used_idx);
278        self.should_notify(old_used, self.used_idx)
279    }
280
281    /// Publishes a batch of completions with a single `used.idx` update and one
282    /// suppression check. Returns whether to interrupt the guest.
283    pub fn push_used_batch(&mut self, completions: &[(u16, u32)]) -> bool {
284        if completions.is_empty() {
285            return false;
286        }
287        let old_used = self.used_idx;
288        for &(head_idx, len) in completions {
289            self.write_used_entry(head_idx, len);
290            self.used_idx = self.used_idx.wrapping_add(1);
291        }
292        fence(Ordering::Release);
293        self.mem
294            .write_u16(self.used_gpa as usize + 2, self.used_idx);
295        self.should_notify(old_used, self.used_idx)
296    }
297
298    /// Publishes `avail_event = last consumed avail index` into the used ring,
299    /// so an EVENT_IDX guest kicks on its very next available entry. Call after
300    /// draining a guest→host queue so an isolated late entry can't sit
301    /// undrained behind kick suppression.
302    pub fn write_avail_event(&self) {
303        let off = self.used_gpa as usize + 4 + self.size as usize * 8;
304        self.mem.write_u16(off, self.last_avail_idx);
305    }
306
307    /// Publishes `avail_event = the guest's current avail.idx`, requesting a
308    /// kick only for entries posted beyond everything currently visible.
309    ///
310    /// For polling consumers that never need guest kicks (the net RX inject
311    /// thread re-reads `avail.idx` on every attempt), this maximally
312    /// suppresses the guest's QUEUE_NOTIFY MMIO exits, whereas
313    /// [`Self::write_avail_event`] (consumed cursor) is for drain-then-sleep
314    /// consumers that must be kicked for the very next entry.
315    ///
316    /// The `Release` fence orders prior used-ring writes before the
317    /// `avail_event` store, pairing with the guest's acquire on the used ring.
318    pub fn write_avail_event_current(&self) {
319        let avail_idx = self.avail_idx();
320        fence(Ordering::Release);
321        let off = self.used_gpa as usize + 4 + self.size as usize * 8;
322        self.mem.write_u16(off, avail_idx);
323    }
324
325    /// Re-arms guest→host notifications and reports whether the guest made more
326    /// entries available while we were draining.
327    ///
328    /// Publishes `avail_event` (so an EVENT_IDX guest kicks on its next entry),
329    /// inserts a SeqCst StoreLoad barrier, then re-reads `avail.idx`. Returns
330    /// `true` if the ring advanced past what we consumed — the caller must drain
331    /// again. This is the device half of the EVENT_IDX handshake: without the
332    /// barrier + re-check, a buffer the guest adds (and skips the kick for,
333    /// having read the pre-update `avail_event`) just as we stop polling sits
334    /// undrained forever. Mirrors `virtio-queue`/libkrun `enable_notification`.
335    #[must_use]
336    pub fn enable_notification(&self) -> bool {
337        self.write_avail_event();
338        fence(Ordering::SeqCst);
339        self.avail_idx() != self.last_avail_idx
340    }
341
342    /// Decides whether to interrupt the guest after advancing `used.idx` from
343    /// `old_used` to `new_used`.
344    fn should_notify(&self, old_used: u16, new_used: u16) -> bool {
345        if old_used == new_used {
346            return false;
347        }
348        if self.event_idx {
349            // Full barrier: order the `used.idx` store (in push_used*) before
350            // the `used_event` load below. Without this StoreLoad barrier the
351            // load may be reordered ahead of the store on weakly-ordered ISAs
352            // (ARM64) and read a *stale* used_event, suppressing a needed IRQ —
353            // the guest then sleeps in WFI forever waiting for a completion it
354            // already received. This is the root-cause class of the HV
355            // cold-boot completion-notification hang.
356            fence(Ordering::SeqCst);
357            let used_event = self
358                .mem
359                .read_u16(self.avail_gpa as usize + 4 + self.size as usize * 2);
360            vring_need_event(used_event, new_used, old_used)
361        } else {
362            // No EVENT_IDX: honor the avail ring's NO_INTERRUPT flag.
363            let avail_flags = self.mem.read_u16(self.avail_gpa as usize);
364            avail_flags & VRING_AVAIL_F_NO_INTERRUPT == 0
365        }
366    }
367}
368
369// SAFETY: `mem` is an `Arc<GuestMemWriter>`, which is itself `Send + Sync` over
370// a VM-lifetime mapping; the remaining fields are plain integers. A given queue
371// is only ever driven by one thread at a time (the vCPU thread that took the
372// QUEUE_NOTIFY exit, or the device's dedicated worker).
373unsafe impl Send for SplitQueue {}
374unsafe impl Sync for SplitQueue {}
375
376/// VirtIO spec §2.7.7.2: the device should notify iff `event_idx` lies in the
377/// half-open interval `(old_idx, new_idx]` (modulo 2^16).
378fn vring_need_event(event_idx: u16, new_idx: u16, old_idx: u16) -> bool {
379    new_idx.wrapping_sub(event_idx).wrapping_sub(1) < new_idx.wrapping_sub(old_idx)
380}
381
382#[cfg(test)]
383mod tests {
384    use super::*;
385
386    const RAM: usize = 0x1_0000; // 64 KiB
387    const SIZE: u16 = 8;
388
389    // Ring layout within the test RAM (offsets from gpa_base).
390    const DESC_OFF: u64 = 0x1000;
391    const AVAIL_OFF: u64 = 0x2000;
392    const USED_OFF: u64 = 0x3000;
393    const DATA_OFF: u64 = 0x4000;
394
395    /// Backing RAM plus the GPA base it is mapped at. Keeps the buffer alive
396    /// for the lifetime of any `SplitQueue` built over it.
397    struct TestRam {
398        buf: Vec<u8>,
399        gpa_base: u64,
400    }
401
402    impl TestRam {
403        fn new(gpa_base: u64) -> Self {
404            Self {
405                buf: vec![0u8; RAM],
406                gpa_base,
407            }
408        }
409
410        fn mem(&mut self) -> Arc<GuestMemWriter> {
411            // SAFETY: buf outlives every queue built in these tests; access is
412            // single-threaded.
413            unsafe {
414                Arc::new(GuestMemWriter::new(
415                    self.buf.as_mut_ptr(),
416                    self.buf.len(),
417                    self.gpa_base as usize,
418                ))
419            }
420        }
421
422        fn cfg(&self) -> QueueConfig {
423            QueueConfig {
424                desc_addr: self.gpa_base + DESC_OFF,
425                avail_addr: self.gpa_base + AVAIL_OFF,
426                used_addr: self.gpa_base + USED_OFF,
427                size: SIZE,
428                ready: true,
429                gpa_base: self.gpa_base,
430            }
431        }
432
433        fn off(&self, gpa: u64) -> usize {
434            (gpa - self.gpa_base) as usize
435        }
436
437        fn w16(&mut self, gpa: u64, v: u16) {
438            let o = self.off(gpa);
439            self.buf[o..o + 2].copy_from_slice(&v.to_le_bytes());
440        }
441
442        fn r16(&self, gpa: u64) -> u16 {
443            let o = (gpa - self.gpa_base) as usize;
444            u16::from_le_bytes([self.buf[o], self.buf[o + 1]])
445        }
446
447        fn r32(&self, gpa: u64) -> u32 {
448            let o = (gpa - self.gpa_base) as usize;
449            u32::from_le_bytes([
450                self.buf[o],
451                self.buf[o + 1],
452                self.buf[o + 2],
453                self.buf[o + 3],
454            ])
455        }
456
457        fn write_desc(&mut self, idx: u16, addr: u64, len: u32, flags: u16, next: u16) {
458            let base = self.gpa_base + DESC_OFF + u64::from(idx) * 16;
459            let o = self.off(base);
460            self.buf[o..o + 8].copy_from_slice(&addr.to_le_bytes());
461            self.buf[o + 8..o + 12].copy_from_slice(&len.to_le_bytes());
462            self.buf[o + 12..o + 14].copy_from_slice(&flags.to_le_bytes());
463            self.buf[o + 14..o + 16].copy_from_slice(&next.to_le_bytes());
464        }
465
466        fn set_avail(&mut self, pos: u16, head: u16) {
467            self.w16(self.gpa_base + AVAIL_OFF + 4 + u64::from(pos) * 2, head);
468        }
469
470        fn set_avail_idx(&mut self, idx: u16) {
471            self.w16(self.gpa_base + AVAIL_OFF + 2, idx);
472        }
473
474        fn set_used_event(&mut self, v: u16) {
475            self.w16(self.gpa_base + AVAIL_OFF + 4 + u64::from(SIZE) * 2, v);
476        }
477
478        fn set_avail_flags(&mut self, v: u16) {
479            self.w16(self.gpa_base + AVAIL_OFF, v);
480        }
481
482        fn used_idx(&self) -> u16 {
483            self.r16(self.gpa_base + USED_OFF + 2)
484        }
485
486        fn used_entry(&self, slot: u16) -> (u32, u32) {
487            let base = self.gpa_base + USED_OFF + 4 + u64::from(slot) * 8;
488            (self.r32(base), self.r32(base + 4))
489        }
490
491        fn avail_event(&self) -> u16 {
492            self.r16(self.gpa_base + USED_OFF + 4 + u64::from(SIZE) * 8)
493        }
494    }
495
496    fn queue(ram: &mut TestRam, event_idx: bool) -> SplitQueue {
497        let cfg = ram.cfg();
498        SplitQueue::new(ram.mem(), 0, &cfg, event_idx)
499    }
500
501    #[test]
502    fn pop_single_descriptor() {
503        let mut ram = TestRam::new(0);
504        ram.write_desc(0, ram.gpa_base + DATA_OFF, 256, 0, 0);
505        ram.set_avail(0, 0);
506        ram.set_avail_idx(1);
507
508        let mut q = queue(&mut ram, false);
509        assert!(q.has_avail());
510        let chain = q.pop_avail().unwrap();
511        assert_eq!(chain.head_idx, 0);
512        assert_eq!(chain.descriptors.len(), 1);
513        assert_eq!(chain.descriptors[0].addr, ram.gpa_base + DATA_OFF);
514        assert_eq!(chain.descriptors[0].len, 256);
515        assert!(!q.has_avail());
516    }
517
518    #[test]
519    fn pop_descriptor_chain() {
520        let mut ram = TestRam::new(0);
521        ram.write_desc(0, ram.gpa_base + DATA_OFF, 16, flags::NEXT, 1);
522        ram.write_desc(
523            1,
524            ram.gpa_base + DATA_OFF + 16,
525            16,
526            flags::NEXT | flags::WRITE,
527            2,
528        );
529        ram.write_desc(2, ram.gpa_base + DATA_OFF + 32, 16, flags::WRITE, 0);
530        ram.set_avail(0, 0);
531        ram.set_avail_idx(1);
532
533        let mut q = queue(&mut ram, false);
534        let chain = q.pop_avail().unwrap();
535        assert_eq!(chain.descriptors.len(), 3);
536        assert!(!chain.descriptors[0].is_write());
537        assert!(chain.descriptors[1].is_write());
538        assert!(!chain.descriptors[2].has_next());
539    }
540
541    #[test]
542    fn cyclic_chain_terminates() {
543        // 0 -> 1 -> 0, both with NEXT set. A bounded walk must terminate.
544        let mut ram = TestRam::new(0);
545        ram.write_desc(0, ram.gpa_base + DATA_OFF, 16, flags::NEXT, 1);
546        ram.write_desc(1, ram.gpa_base + DATA_OFF + 16, 16, flags::NEXT, 0);
547        ram.set_avail(0, 0);
548        ram.set_avail_idx(1);
549
550        let mut q = queue(&mut ram, false);
551        let chain = q.pop_avail().unwrap();
552        assert!(chain.descriptors.len() <= SIZE as usize);
553    }
554
555    #[test]
556    fn out_of_range_next_stops_walk() {
557        let mut ram = TestRam::new(0);
558        ram.write_desc(0, ram.gpa_base + DATA_OFF, 16, flags::NEXT, 99);
559        ram.set_avail(0, 0);
560        ram.set_avail_idx(1);
561
562        let mut q = queue(&mut ram, false);
563        let chain = q.pop_avail().unwrap();
564        assert_eq!(chain.descriptors.len(), 1);
565    }
566
567    #[test]
568    fn chain_iter_walks_allocation_free() {
569        let mut ram = TestRam::new(0);
570        ram.write_desc(0, ram.gpa_base + DATA_OFF, 16, flags::NEXT, 1);
571        ram.write_desc(
572            1,
573            ram.gpa_base + DATA_OFF + 16,
574            32,
575            flags::NEXT | flags::WRITE,
576            2,
577        );
578        ram.write_desc(2, ram.gpa_base + DATA_OFF + 48, 64, flags::WRITE, 0);
579        ram.set_avail(0, 0);
580        ram.set_avail_idx(1);
581
582        let mut q = queue(&mut ram, false);
583        let head = q.next_avail_head().unwrap();
584        assert_eq!(head, 0);
585        let descs: Vec<_> = q.chain_iter(head).collect();
586        assert_eq!(descs.len(), 3);
587        assert_eq!(descs[0].len, 16);
588        assert!(!descs[0].is_write());
589        assert!(descs[1].is_write());
590        assert_eq!(descs[2].len, 64);
591        assert!(!descs[2].has_next());
592        // Cursor advanced exactly one entry.
593        assert!(q.next_avail_head().is_none());
594    }
595
596    #[test]
597    fn chain_iter_cycle_terminates() {
598        let mut ram = TestRam::new(0);
599        ram.write_desc(0, ram.gpa_base + DATA_OFF, 16, flags::NEXT, 1);
600        ram.write_desc(1, ram.gpa_base + DATA_OFF + 16, 16, flags::NEXT, 0);
601        ram.set_avail(0, 0);
602        ram.set_avail_idx(1);
603
604        let mut q = queue(&mut ram, false);
605        let head = q.next_avail_head().unwrap();
606        assert!(q.chain_iter(head).count() <= SIZE as usize);
607    }
608
609    #[test]
610    fn push_used_writes_entry_and_idx() {
611        let mut ram = TestRam::new(0);
612        let mut q = queue(&mut ram, false);
613        let notify = q.push_used(5, 1024);
614        assert!(notify); // no EVENT_IDX, no NO_INTERRUPT -> always notify
615        assert_eq!(ram.used_idx(), 1);
616        assert_eq!(ram.used_entry(0), (5, 1024));
617    }
618
619    #[test]
620    fn push_used_batch_single_idx_update() {
621        let mut ram = TestRam::new(0);
622        let mut q = queue(&mut ram, false);
623        let notify = q.push_used_batch(&[(0, 100), (1, 200), (2, 300)]);
624        assert!(notify);
625        assert_eq!(ram.used_idx(), 3);
626        assert_eq!(ram.used_entry(0), (0, 100));
627        assert_eq!(ram.used_entry(2), (2, 300));
628    }
629
630    #[test]
631    fn push_used_batch_empty_is_noop() {
632        let mut ram = TestRam::new(0);
633        let mut q = queue(&mut ram, false);
634        assert!(!q.push_used_batch(&[]));
635        assert_eq!(ram.used_idx(), 0);
636    }
637
638    #[test]
639    fn no_interrupt_flag_suppresses_without_event_idx() {
640        let mut ram = TestRam::new(0);
641        ram.set_avail_flags(VRING_AVAIL_F_NO_INTERRUPT);
642        let mut q = queue(&mut ram, false);
643        assert!(!q.push_used(0, 16));
644    }
645
646    #[test]
647    fn event_idx_suppresses_until_event_reached() {
648        let mut ram = TestRam::new(0);
649        // Guest: "notify me when used.idx reaches 3".
650        ram.set_used_event(2);
651        let mut q = queue(&mut ram, true);
652        // used.idx 0 -> 1: event 2 not in (0,1] -> suppress.
653        assert!(!q.push_used(0, 16));
654        // 1 -> 2: event 2 not in (1,2] -> suppress.
655        assert!(!q.push_used(1, 16));
656        // 2 -> 3: event 2 in (2,3] -> notify.
657        assert!(q.push_used(2, 16));
658    }
659
660    #[test]
661    fn write_avail_event_publishes_last_avail() {
662        let mut ram = TestRam::new(0);
663        ram.write_desc(0, ram.gpa_base + DATA_OFF, 16, 0, 0);
664        ram.set_avail(0, 0);
665        ram.set_avail_idx(1);
666        let mut q = queue(&mut ram, true);
667        let _ = q.pop_avail();
668        q.write_avail_event();
669        assert_eq!(ram.avail_event(), 1); // consumed avail index
670    }
671
672    #[test]
673    fn write_avail_event_current_publishes_guest_avail_idx() {
674        let mut ram = TestRam::new(0);
675        ram.write_desc(0, ram.gpa_base + DATA_OFF, 16, 0, 0);
676        ram.set_avail(0, 0);
677        // Guest has posted 5 entries; the device consumed only 1.
678        ram.set_avail_idx(5);
679        let mut q = queue(&mut ram, true);
680        let _ = q.pop_avail();
681        q.write_avail_event_current();
682        // A polling consumer wants kicks only past everything posted.
683        assert_eq!(ram.avail_event(), 5);
684    }
685
686    /// The bug `queue_guest::GuestMemoryVirtQueue` had: assuming GPA 0 maps to
687    /// `ram_base`. `SplitQueue` resolves every GPA through `GuestMemWriter`, so
688    /// a non-zero `gpa_base` (the ARM RAM layout) round-trips correctly.
689    #[test]
690    fn nonzero_gpa_base_resolves_correctly() {
691        let mut ram = TestRam::new(0x4000_0000);
692        ram.write_desc(0, ram.gpa_base + DATA_OFF, 512, 0, 0);
693        ram.set_avail(0, 0);
694        ram.set_avail_idx(1);
695
696        let mut q = queue(&mut ram, false);
697        let chain = q.pop_avail().unwrap();
698        assert_eq!(chain.descriptors[0].addr, 0x4000_0000 + DATA_OFF);
699        assert_eq!(chain.descriptors[0].len, 512);
700
701        q.push_used(0, 512);
702        assert_eq!(ram.used_idx(), 1);
703        assert_eq!(ram.used_entry(0), (0, 512));
704    }
705
706    #[test]
707    fn vring_need_event_formula() {
708        // event just completed -> notify
709        assert!(vring_need_event(0, 1, 0));
710        // already past event -> no notify
711        assert!(!vring_need_event(3, 6, 5));
712        // wrap-around
713        assert!(vring_need_event(65535, 0, 65534));
714    }
715}