Skip to main content

virtio/
queue.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Core virtio queue implementation, without any notification mechanisms, async
5//! support, or other transport-specific details.
6
7mod packed;
8mod split;
9use crate::VirtioQueueCallbackWork;
10use crate::spec::VirtioDeviceFeatures;
11use crate::spec::queue as spec;
12use guestmem::GuestMemory;
13use guestmem::GuestMemoryError;
14use inspect::Inspect;
15use packed::PackedQueueCompleteWork;
16pub use packed::PackedQueueCompletionContext;
17use packed::PackedQueueGetWork;
18use spec::DescriptorFlags;
19use spec::PackedDescriptor;
20use spec::SplitDescriptor;
21use split::SplitQueueCompleteWork;
22use split::SplitQueueGetWork;
23use thiserror::Error;
24use zerocopy::FromBytes;
25use zerocopy::Immutable;
26use zerocopy::IntoBytes;
27use zerocopy::KnownLayout;
28
29pub(crate) fn descriptor_offset(index: u16) -> u64 {
30    index as u64 * size_of::<SplitDescriptor>() as u64
31}
32
33pub(crate) fn read_descriptor<T: IntoBytes + FromBytes + Immutable + KnownLayout>(
34    queue_desc: &GuestMemory,
35    index: u16,
36) -> Result<T, QueueError> {
37    queue_desc
38        .read_plain::<T>(descriptor_offset(index))
39        .map_err(QueueError::Memory)
40}
41
42/// In-flight (consumed-but-not-completed) buffers for a split queue: the
43/// difference of the free-running avail/used head counters.
44///
45/// Errors with [`QueueError::InvalidSavedState`] if that exceeds the queue size
46/// (a corrupt/malicious state — restore is a host trust boundary).
47fn split_in_flight(avail: u16, used: u16, queue_size: u16) -> Result<u16, QueueError> {
48    let in_flight = avail.wrapping_sub(used);
49    if in_flight > queue_size {
50        return Err(QueueError::InvalidSavedState {
51            avail_index: avail,
52            used_index: used,
53            queue_size,
54        });
55    }
56    Ok(in_flight)
57}
58
59/// In-flight (consumed-but-not-completed) descriptors for a packed queue, from
60/// its saved cursors (ring index in bits 0..14, lap/wrap bit in bit 15).
61/// Projecting each cursor onto a doubled ring `[0, 2 * queue_size)` turns the
62/// span from `used` to `avail` into a modular distance; the lap bit tells an
63/// empty ring from a full one.
64///
65/// Errors with [`QueueError::InvalidSavedState`] if the cursors imply more than a
66/// full ring in flight (a corrupt/malicious state — restore is a trust boundary).
67fn packed_in_flight(avail: u16, used: u16, queue_size: u16) -> Result<u16, QueueError> {
68    if avail & 0x7fff >= queue_size || used & 0x7fff >= queue_size {
69        return Err(QueueError::InvalidSavedState {
70            avail_index: avail,
71            used_index: used,
72            queue_size,
73        });
74    }
75    let ring = queue_size as i32;
76    let position = |cursor: u16| (cursor & 0x7FFF) as i32 + i32::from(cursor & 0x8000 != 0) * ring;
77    let in_flight = (position(avail) - position(used)).rem_euclid(2 * ring);
78    if in_flight > ring {
79        return Err(QueueError::InvalidSavedState {
80            avail_index: avail,
81            used_index: used,
82            queue_size,
83        });
84    }
85    Ok(in_flight as u16)
86}
87
88/// Saved progress state for a single virtio queue.
89///
90/// For split queues: `avail_index` and `used_index` are plain ring indices.
91/// For packed queues: bit 15 of each carries the wrap counter
92/// (`index | (wrap_counter << 15)`), matching the vhost-user wire format.
93#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, mesh::payload::Protobuf)]
94#[mesh(package = "virtio.queue")]
95pub struct QueueState {
96    #[mesh(1)]
97    pub avail_index: u16,
98    #[mesh(2)]
99    pub used_index: u16,
100}
101
102#[derive(Debug, Error)]
103pub enum QueueError {
104    #[error("error accessing queue memory")]
105    Memory(#[source] GuestMemoryError),
106    #[error("an indirect descriptor had the indirect flag set")]
107    DoubleIndirect,
108    #[error("a descriptor chain is too long or has a cycle")]
109    TooLong,
110    #[error("Invalid queue size {0}. Must be a power of 2.")]
111    InvalidQueueSize(u16),
112    #[error(
113        "guest oversubscribed queue size {queue_size}: {in_flight} already in flight, \
114         {requested} more requested"
115    )]
116    TooManyInFlightDescriptors {
117        in_flight: u16,
118        requested: u16,
119        queue_size: u16,
120    },
121    #[error(
122        "corrupt queue saved state: avail index {avail_index:#x}, used index {used_index:#x}, \
123         queue size {queue_size}"
124    )]
125    InvalidSavedState {
126        avail_index: u16,
127        used_index: u16,
128        queue_size: u16,
129    },
130}
131
132struct QueueDescriptor {
133    address: u64,
134    length: u32,
135    flags: DescriptorFlags,
136    buffer_id: Option<u16>,
137    next: Option<u16>,
138}
139
140enum QueueCompletionContext {
141    Split,
142    Packed(PackedQueueCompletionContext),
143}
144
145/// The minimal state required to publish a completion to the used ring.
146///
147/// This is everything `complete_descriptor` needs — notably *not* the payload
148/// buffer descriptors, which are only used while the device is reading/writing
149/// guest memory. Devices that must buffer completions (e.g. in-order
150/// publication) can hold a `QueueCompletion` instead of a full
151/// `VirtioQueueCallbackWork` to avoid retaining the payload.
152pub struct QueueCompletion {
153    context: QueueCompletionContext,
154    descriptor_index: u16,
155}
156
157impl QueueCompletion {
158    pub fn descriptor_index(&self) -> u16 {
159        self.descriptor_index
160    }
161
162    fn in_flight_cost(&self) -> u16 {
163        match &self.context {
164            QueueCompletionContext::Split => 1,
165            QueueCompletionContext::Packed(context) => context.descriptor_count(),
166        }
167    }
168}
169
170#[derive(Debug, Inspect)]
171#[inspect(tag = "type")]
172enum QueueGetWorkInner {
173    Split(#[inspect(flatten)] SplitQueueGetWork),
174    Packed(#[inspect(flatten)] PackedQueueGetWork),
175}
176
177#[derive(Debug, Inspect)]
178#[inspect(tag = "type")]
179enum QueueCompleteWorkInner {
180    Split(#[inspect(flatten)] SplitQueueCompleteWork),
181    Packed(#[inspect(flatten)] PackedQueueCompleteWork),
182}
183
184#[derive(Debug, Copy, Clone, Default, inspect::Inspect)]
185pub struct QueueParams {
186    pub size: u16,
187    pub enable: bool,
188    #[inspect(hex)]
189    pub desc_addr: u64,
190    #[inspect(hex)]
191    pub avail_addr: u64,
192    #[inspect(hex)]
193    pub used_addr: u64,
194}
195
196#[derive(Debug, Inspect)]
197pub(crate) struct QueueCoreGetWork {
198    queue_desc: GuestMemory,
199    queue_size: u16,
200    features: VirtioDeviceFeatures,
201    mem: GuestMemory,
202    #[inspect(flatten)]
203    inner: QueueGetWorkInner,
204    /// Whether kick notification is currently armed.
205    armed: bool,
206    /// Whether a fatal error has retired the fetch side of the queue.
207    ///
208    /// Every [`QueueError`] raised while fetching work means the guest violated
209    /// the queue protocol, and the offending chain is rejected *without* being
210    /// consumed — the available index does not move. Re-reading the ring would
211    /// therefore hit the same descriptors and raise the same error forever, so a
212    /// caller that logs the error and keeps polling would spin without making
213    /// progress. Latch the failure instead: report it once, then report the
214    /// queue as permanently empty.
215    ///
216    /// This only retires fetching. Descriptors already consumed can still be
217    /// completed.
218    failed: bool,
219    /// Consumed-but-not-completed ring capacity, in the format's native unit:
220    /// buffers (heads) for split, descriptors (slots) for packed — the two forms
221    /// of the virtio "Queue Size" limit (spec §2.7.1 vs §2.8.1).
222    /// [`try_peek_work`](Self::try_peek_work) rejects work that would push this
223    /// past the queue size, so it stays in `0..=queue_size`.
224    in_flight: u16,
225}
226
227impl QueueCoreGetWork {
228    pub fn avail_index(&self) -> u16 {
229        match &self.inner {
230            QueueGetWorkInner::Split(split) => split.last_avail_index(),
231            QueueGetWorkInner::Packed(packed) => packed.avail_state(),
232        }
233    }
234
235    pub fn new(
236        features: VirtioDeviceFeatures,
237        mem: GuestMemory,
238        params: QueueParams,
239        initial_state: Option<QueueState>,
240    ) -> Result<Self, QueueError> {
241        // Both ring layouts cap queue size at 2^15 (spec §2.7.1, §2.8.1).
242        if params.size == 0 || params.size > 1 << 15 {
243            return Err(QueueError::InvalidQueueSize(params.size));
244        }
245        let initial_avail = initial_state.map(|s| s.avail_index);
246        // Split queues require power-of-2 sizes (virtio spec §2.7.1).
247        // Packed queues do not (§2.8.10.1).
248        if !features.ring_packed() && !params.size.is_power_of_two() {
249            return Err(QueueError::InvalidQueueSize(params.size));
250        }
251        let queue_desc = mem
252            .subrange(params.desc_addr, descriptor_offset(params.size), true)
253            .map_err(QueueError::Memory)?;
254        let inner = if features.ring_packed() {
255            let (index, wrap) = match initial_avail {
256                Some(v) => (v & 0x7FFF, (v >> 15) != 0),
257                None => (0, true),
258            };
259            QueueGetWorkInner::Packed(PackedQueueGetWork::new(
260                features,
261                mem.clone(),
262                params,
263                index,
264                wrap,
265            )?)
266        } else {
267            let index = initial_avail.unwrap_or(0);
268            QueueGetWorkInner::Split(SplitQueueGetWork::new(
269                features,
270                mem.clone(),
271                params,
272                index,
273            )?)
274        };
275        // Seed from the restored cursors in the matching unit (heads for split,
276        // descriptors for packed) so the running count stays consistent; a fresh
277        // queue is zero. Both reject a state implying more than a full ring
278        // in flight, since restore is a host trust boundary.
279        let in_flight = match initial_state {
280            None => 0,
281            Some(state) if features.ring_packed() => {
282                packed_in_flight(state.avail_index, state.used_index, params.size)?
283            }
284            Some(state) => split_in_flight(state.avail_index, state.used_index, params.size)?,
285        };
286        Ok(Self {
287            queue_desc,
288            queue_size: params.size,
289            features,
290            mem,
291            inner,
292            armed: false,
293            failed: false,
294            in_flight,
295        })
296    }
297
298    /// Whether a fatal error has retired the fetch side of this queue. Once
299    /// set, no further work will ever be returned. See [`Self::failed`].
300    pub fn failed(&self) -> bool {
301        self.failed
302    }
303
304    pub fn try_next_work(&mut self) -> Result<Option<VirtioQueueCallbackWork>, QueueError> {
305        match self.try_peek_work() {
306            Ok(Some(work)) => {
307                self.advance(work.completion());
308                Ok(Some(work))
309            }
310            r => r,
311        }
312    }
313
314    /// Like [`try_next_work`](Self::try_next_work), but does not advance
315    /// the available index. The caller must call [`advance`](Self::advance) to
316    /// consume the peeked descriptor and move to the next one. Calling this
317    /// again without advancing will return the same descriptor, but note that
318    /// the guest may have modified the descriptor memory in the meantime.
319    ///
320    /// Returns `Ok(None)` forever once the queue has failed, so that the error
321    /// is reported exactly once.
322    pub fn try_peek_work(&mut self) -> Result<Option<VirtioQueueCallbackWork>, QueueError> {
323        if self.failed {
324            return Ok(None);
325        }
326        let r = self.try_peek_work_inner();
327        if r.is_err() {
328            self.failed = true;
329        }
330        r
331    }
332
333    fn try_peek_work_inner(&mut self) -> Result<Option<VirtioQueueCallbackWork>, QueueError> {
334        let index = match &mut self.inner {
335            QueueGetWorkInner::Split(split) => split.is_available()?,
336            QueueGetWorkInner::Packed(packed) => packed.is_available()?,
337        };
338        let Some(index) = index else { return Ok(None) };
339        self.suppress_if_armed();
340        let work = self.work_from_index(index)?;
341
342        // Reject a guest that oversubscribes the ring: the whole chain must fit
343        // in the remaining capacity, not just "we aren't already full" (spec caps
344        // in flight at the queue size — §2.7.1/§2.8.1). A compliant guest,
345        // bounded by ring space, never trips this; a misbehaving one (split
346        // reusing avail slots, or packed re-stamping wrap flags early) is caught
347        // before it can grow host tracking or overlap in-flight descriptors.
348        let requested = work.completion().in_flight_cost();
349        if requested > self.queue_size - self.in_flight {
350            return Err(QueueError::TooManyInFlightDescriptors {
351                in_flight: self.in_flight,
352                requested,
353                queue_size: self.queue_size,
354            });
355        }
356
357        Ok(Some(work))
358    }
359
360    /// Arms kick notification so the guest will send a doorbell when new work
361    /// is available. Returns `true` if armed successfully (caller should
362    /// sleep), or `false` if new data arrived during arming (caller should
363    /// retry by calling [`try_next_work`](Self::try_next_work) again).
364    ///
365    /// If already armed, this is a no-op and returns `true`.
366    pub fn arm_for_kick(&mut self) -> bool {
367        if self.armed {
368            return true;
369        }
370        let r = match &mut self.inner {
371            QueueGetWorkInner::Split(split) => split.arm_kick(),
372            QueueGetWorkInner::Packed(packed) => packed.arm_kick(),
373        };
374        match r {
375            Ok(true) => {
376                self.armed = true;
377                true
378            }
379            Ok(false) => false,
380            Err(err) => {
381                tracelimit::error_ratelimited!(
382                    error = &err as &dyn std::error::Error,
383                    "failed to arm kick"
384                );
385                // On error, behave as if armed to avoid a busy loop in callers
386                // that treat `false` as "retry immediately".
387                self.armed = true;
388                true
389            }
390        }
391    }
392
393    /// If kicks are armed, suppress them. Called automatically when work is
394    /// found so the guest doesn't send unnecessary doorbells while draining.
395    fn suppress_if_armed(&mut self) {
396        if self.armed {
397            self.armed = false;
398            let r = match &self.inner {
399                QueueGetWorkInner::Split(split) => split.suppress_kicks(),
400                QueueGetWorkInner::Packed(packed) => packed.suppress_kicks(),
401            };
402
403            if let Err(err) = r {
404                tracelimit::error_ratelimited!(
405                    error = &err as &dyn std::error::Error,
406                    "failed to suppress kicks"
407                );
408            }
409        }
410    }
411
412    /// Advances the available index after a successful
413    /// [`try_peek_work`](Self::try_peek_work) call.
414    pub fn advance(&mut self, completion: &QueueCompletion) {
415        let cost = completion.in_flight_cost();
416        match &mut self.inner {
417            QueueGetWorkInner::Split(split) => split.advance(),
418            QueueGetWorkInner::Packed(packed) => {
419                let QueueCompletionContext::Packed(ctx) = &completion.context else {
420                    unreachable!();
421                };
422                packed.advance(ctx.descriptor_count());
423            }
424        }
425        // The chain-fits bound in [`try_peek_work`] keeps this within the queue
426        // size; `checked_add` only guards against corrupt accounting.
427        self.in_flight = self
428            .in_flight
429            .checked_add(cost)
430            .expect("in-flight count overflowed");
431    }
432
433    /// Releases the ring capacity held by a consumed work item.
434    pub fn work_completed(&mut self, completion: &QueueCompletion) {
435        self.in_flight = self
436            .in_flight
437            .checked_sub(completion.in_flight_cost())
438            .expect("completed more than was consumed");
439    }
440
441    fn work_from_index(&mut self, index: u16) -> Result<VirtioQueueCallbackWork, QueueError> {
442        if let QueueGetWorkInner::Split(split) = &mut self.inner {
443            let descriptor_index = split.get_available_descriptor_index(index)?;
444            let payload = self
445                .reader(descriptor_index)
446                .collect::<Result<Vec<_>, _>>()?;
447            Ok(VirtioQueueCallbackWork::from_parts(
448                QueueCompletion {
449                    descriptor_index,
450                    context: QueueCompletionContext::Split,
451                },
452                payload,
453            ))
454        } else {
455            let (payload, last_primary_desc_index) = {
456                let mut reader = self.reader(index);
457                (
458                    (&mut reader).collect::<Result<Vec<_>, _>>()?,
459                    reader.last_primary_desc_index(),
460                )
461            };
462            let last = self.descriptor(&self.queue_desc, last_primary_desc_index, None)?;
463            let count = if last_primary_desc_index >= index {
464                last_primary_desc_index - index + 1
465            } else {
466                // Wrapped around the end of the queue.
467                self.queue_size - index + last_primary_desc_index + 1
468            };
469            let completion_context = PackedQueueCompletionContext::new(&last, count);
470            Ok(VirtioQueueCallbackWork::from_parts(
471                QueueCompletion {
472                    context: QueueCompletionContext::Packed(completion_context),
473                    descriptor_index: index,
474                },
475                payload,
476            ))
477        }
478    }
479
480    fn reader(&mut self, descriptor_index: u16) -> DescriptorReader<'_> {
481        DescriptorReader {
482            chain: DescriptorChain::new(self, self.features.ring_indirect_desc(), descriptor_index),
483        }
484    }
485
486    fn descriptor(
487        &self,
488        desc_queue: &GuestMemory,
489        index: u16,
490        active_indirect_len: Option<u16>,
491    ) -> Result<QueueDescriptor, QueueError> {
492        let descriptor = match self.inner {
493            QueueGetWorkInner::Split(_) => {
494                let descriptor: SplitDescriptor = read_descriptor(desc_queue, index)?;
495                QueueDescriptor {
496                    address: descriptor.address.get(),
497                    length: descriptor.length.get(),
498                    flags: descriptor.flags(),
499                    buffer_id: None,
500                    next: if descriptor.flags().next() {
501                        Some(descriptor.next.get())
502                    } else {
503                        None
504                    },
505                }
506            }
507            QueueGetWorkInner::Packed(_) => {
508                let descriptor: PackedDescriptor = read_descriptor(desc_queue, index)?;
509                QueueDescriptor {
510                    address: descriptor.address.get(),
511                    length: descriptor.length.get(),
512                    flags: descriptor.flags(),
513                    buffer_id: Some(descriptor.buffer_id.get()),
514                    next: if let Some(active_indirect_len) = active_indirect_len {
515                        // Packed descriptors consume all of the indirect
516                        // descriptors based on the buffer length, regardless
517                        // of the NEXT flag.
518                        let next = index.wrapping_add(1);
519                        if next < active_indirect_len {
520                            Some(next)
521                        } else {
522                            None
523                        }
524                    } else if descriptor.flags().next() {
525                        // Packed ring descriptors are sequential and wrap
526                        // at queue_size.
527                        let next = index.wrapping_add(1);
528                        if next >= self.queue_size {
529                            Some(0)
530                        } else {
531                            Some(next)
532                        }
533                    } else {
534                        None
535                    },
536                }
537            }
538        };
539        Ok(descriptor)
540    }
541
542    fn size(&self) -> u16 {
543        self.queue_size
544    }
545}
546
547#[derive(Debug, Inspect)]
548pub(crate) struct QueueCoreCompleteWork {
549    #[inspect(flatten)]
550    inner: QueueCompleteWorkInner,
551}
552
553impl QueueCoreCompleteWork {
554    pub fn new(
555        features: VirtioDeviceFeatures,
556        mem: GuestMemory,
557        params: QueueParams,
558        initial_state: Option<QueueState>,
559    ) -> Result<Self, QueueError> {
560        let initial_used = initial_state.map(|s| s.used_index);
561        let inner = if features.ring_packed() {
562            let (index, wrap) = match initial_used {
563                Some(v) => (v & 0x7FFF, (v >> 15) != 0),
564                None => (0, true),
565            };
566            QueueCompleteWorkInner::Packed(PackedQueueCompleteWork::new(
567                features,
568                mem.clone(),
569                params,
570                index,
571                wrap,
572            )?)
573        } else {
574            let index = initial_used.unwrap_or(0);
575            QueueCompleteWorkInner::Split(SplitQueueCompleteWork::new(
576                features,
577                mem.clone(),
578                params,
579                index,
580            )?)
581        };
582        Ok(Self { inner })
583    }
584
585    pub fn used_index(&self) -> u16 {
586        match &self.inner {
587            QueueCompleteWorkInner::Split(split) => split.last_used_index(),
588            QueueCompleteWorkInner::Packed(packed) => packed.used_state(),
589        }
590    }
591
592    pub fn complete_descriptor(
593        &mut self,
594        completion: &QueueCompletion,
595        bytes_written: u32,
596    ) -> Result<bool, QueueError> {
597        match &mut self.inner {
598            QueueCompleteWorkInner::Split(split) => {
599                split.complete_descriptor(completion.descriptor_index, bytes_written)
600            }
601            QueueCompleteWorkInner::Packed(packed) => {
602                let QueueCompletionContext::Packed(context) = &completion.context else {
603                    panic!("mismatched queue completion context for packed queue");
604                };
605                packed.complete_descriptor(context, bytes_written)
606            }
607        }
608    }
609}
610
611pub(crate) fn new_queue(
612    features: VirtioDeviceFeatures,
613    mem: GuestMemory,
614    params: QueueParams,
615    initial_state: Option<QueueState>,
616) -> Result<(QueueCoreGetWork, QueueCoreCompleteWork), QueueError> {
617    let get_work = QueueCoreGetWork::new(features, mem.clone(), params, initial_state)?;
618    let complete_work = QueueCoreCompleteWork::new(features, mem.clone(), params, initial_state)?;
619    Ok((get_work, complete_work))
620}
621
622struct DescriptorReader<'a> {
623    chain: DescriptorChain<'a>,
624}
625
626impl DescriptorReader<'_> {
627    pub fn last_primary_desc_index(&self) -> u16 {
628        self.chain.last_primary_desc_index()
629    }
630}
631
632pub struct VirtioQueuePayload {
633    pub writeable: bool,
634    pub address: u64,
635    pub length: u32,
636}
637
638impl Iterator for DescriptorReader<'_> {
639    type Item = Result<VirtioQueuePayload, QueueError>;
640
641    fn next(&mut self) -> Option<Self::Item> {
642        self.chain.next().map(|descriptor| {
643            descriptor.map(|descriptor| VirtioQueuePayload {
644                writeable: descriptor.flags.write(),
645                address: descriptor.address,
646                length: descriptor.length,
647            })
648        })
649    }
650}
651
652struct DescriptorChain<'a> {
653    queue: &'a QueueCoreGetWork,
654    /// Maximum chain length — always the original ring queue size (spec §2.7.5.3.1).
655    queue_size: u16,
656    indirect_support: bool,
657    indirect_queue: Option<GuestMemory>,
658    /// Entry count of the active indirect table, if any.
659    indirect_table_len: Option<u16>,
660    descriptor_index: Option<u16>,
661    last_primary_desc_index: u16,
662    num_read: u16,
663}
664
665impl<'a> DescriptorChain<'a> {
666    fn new(queue: &'a QueueCoreGetWork, indirect_support: bool, descriptor_index: u16) -> Self {
667        Self {
668            queue,
669            queue_size: queue.size(),
670            indirect_support,
671            indirect_queue: None,
672            indirect_table_len: None,
673            descriptor_index: Some(descriptor_index),
674            last_primary_desc_index: descriptor_index,
675            num_read: 0,
676        }
677    }
678
679    fn next_descriptor(&mut self) -> Result<Option<QueueDescriptor>, QueueError> {
680        let Some(descriptor_index) = self.descriptor_index else {
681            return Ok(None);
682        };
683        let descriptor = self.queue.descriptor(
684            self.indirect_queue
685                .as_ref()
686                .unwrap_or(&self.queue.queue_desc),
687            descriptor_index,
688            self.indirect_table_len,
689        )?;
690        let descriptor = if !self.indirect_support || !descriptor.flags.indirect() {
691            if self.indirect_queue.is_none() {
692                self.last_primary_desc_index = descriptor_index;
693            }
694            descriptor
695        } else {
696            if self.indirect_queue.is_some() {
697                return Err(QueueError::DoubleIndirect);
698            }
699            let indirect_queue = self.indirect_queue.insert(
700                self.queue
701                    .mem
702                    .subrange(descriptor.address, descriptor.length as u64, true)
703                    .map_err(QueueError::Memory)?,
704            );
705            self.descriptor_index = Some(0);
706            let indirect_len = (descriptor.length / size_of::<SplitDescriptor>() as u32) as u16;
707            self.indirect_table_len = Some(indirect_len);
708            self.queue
709                .descriptor(indirect_queue, 0, Some(indirect_len))?
710        };
711
712        self.num_read += 1;
713        self.descriptor_index = descriptor.next;
714        // A descriptor chain must not exceed the queue size (virtio spec
715        // §2.7.5.3.1). Reject chains that hit this limit—this also catches
716        // cycles in the descriptor ring.
717        if self.descriptor_index.is_some() && self.num_read == self.queue_size {
718            return Err(QueueError::TooLong);
719        }
720        Ok(Some(descriptor))
721    }
722
723    pub fn last_primary_desc_index(&self) -> u16 {
724        self.last_primary_desc_index
725    }
726}
727
728impl Iterator for DescriptorChain<'_> {
729    type Item = Result<QueueDescriptor, QueueError>;
730
731    fn next(&mut self) -> Option<Self::Item> {
732        self.next_descriptor().transpose()
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use super::QueueError;
739    use super::packed_in_flight;
740
741    /// Encodes a packed cursor as it is stored in `QueueState`:
742    /// `index | (wrap_counter << 15)`.
743    fn cursor(index: u16, wrap: bool) -> u16 {
744        index | (u16::from(wrap) << 15)
745    }
746
747    #[test]
748    fn packed_in_flight_decode() {
749        let qs = 8;
750
751        // Empty ring: cursors equal, same wrap.
752        assert_eq!(
753            packed_in_flight(cursor(2, false), cursor(2, false), qs).unwrap(),
754            0
755        );
756        assert_eq!(
757            packed_in_flight(cursor(5, true), cursor(5, true), qs).unwrap(),
758            0
759        );
760
761        // Partial, same wrap: avail ahead of used within the same lap.
762        assert_eq!(
763            packed_in_flight(cursor(5, false), cursor(2, false), qs).unwrap(),
764            3
765        );
766
767        // Full ring: cursors equal index, opposite wrap.
768        assert_eq!(
769            packed_in_flight(cursor(3, true), cursor(3, false), qs).unwrap(),
770            8
771        );
772        assert_eq!(
773            packed_in_flight(cursor(0, false), cursor(0, true), qs).unwrap(),
774            8
775        );
776
777        // Avail has wrapped past the end while used trails in the prior lap.
778        // used at 6, avail at 1 (next lap) → slots 6, 7, 0 in flight.
779        assert_eq!(
780            packed_in_flight(cursor(1, true), cursor(6, false), qs).unwrap(),
781            3
782        );
783    }
784
785    #[test]
786    fn packed_in_flight_rejects_corrupt_state() {
787        // Same wrap but avail index behind used index implies more than a full
788        // ring in flight — an impossible, corrupt saved state. It must be
789        // rejected with an error rather than panicking, since the restore path
790        // is a host trust boundary.
791        assert!(matches!(
792            packed_in_flight(cursor(1, false), cursor(6, false), 8),
793            Err(QueueError::InvalidSavedState { queue_size: 8, .. })
794        ));
795    }
796}