Skip to main content

hyperlight_common/virtq/
desc.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 The Hyperlight Authors.
3
4//! Virtqueue Descriptor Types
5//!
6//! This module defines the descriptor format for packed virtqueues as specified
7//! in VIRTIO 1.1+. Each descriptor represents a memory buffer in a scatter-gather
8//! list that the device will read from or write to.
9
10use bitflags::bitflags;
11use bytemuck::{Pod, Zeroable};
12
13use super::MemOps;
14
15bitflags! {
16    /// Descriptor flags as defined by VIRTIO specification.
17    ///
18    /// Note: The implementation never follows the indirect-table interpretation,
19    /// so descriptors carrying INDIRECT are rejected as malformed.
20    #[repr(transparent)]
21    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22    pub struct DescFlags: u16 {
23        /// This marks a buffer as continuing via the next field.
24        const NEXT     = 1 << 0;
25        /// This marks a buffer as device write-only (otherwise device read-only).
26        const WRITE    = 1 << 1;
27        /// This means the buffer contains a list of buffer descriptors (unsupported here).
28        const INDIRECT = 1 << 2;
29        /// Available flag for packed virtqueue wrap counter.
30        const AVAIL    = 1 << 7;
31        /// Used flag for packed virtqueue wrap counter.
32        const USED     = 1 << 15;
33    }
34}
35
36impl DescFlags {
37    /// Was a descriptor carrying these flags made available by the driver in
38    /// the round identified by `wrap`?
39    #[inline]
40    pub fn is_avail(self, wrap: bool) -> bool {
41        let avail = self.contains(DescFlags::AVAIL);
42        let used = self.contains(DescFlags::USED);
43        avail == wrap && used != wrap
44    }
45
46    /// Was a descriptor carrying these flags marked used by the device in the
47    /// round identified by `wrap`?
48    #[inline]
49    pub fn is_used(self, wrap: bool) -> bool {
50        let avail = self.contains(DescFlags::AVAIL);
51        let used = self.contains(DescFlags::USED);
52        avail == wrap && used == wrap
53    }
54}
55
56#[repr(C)]
57#[derive(Clone, Copy, Debug, Pod, Zeroable, PartialEq, Eq, Hash)]
58pub struct Descriptor {
59    /// Physical address of the buffer.
60    pub addr: u64,
61    /// Length of the buffer in bytes.
62    /// For used descriptors, this contains bytes written by device.
63    pub len: u32,
64    /// Buffer ID - used to correlate completions with submissions.
65    /// All descriptors in a chain share the same ID.
66    pub id: u16,
67    /// Flags (NEXT, WRITE, INDIRECT, AVAIL, USED).
68    pub flags: u16,
69}
70
71const _: () = assert!(core::mem::size_of::<Descriptor>() == 16);
72const _: () = assert!(Descriptor::ALIGN == 16);
73const _: () = assert!(Descriptor::ADDR_OFFSET == 0);
74const _: () = assert!(Descriptor::LEN_OFFSET == 8);
75const _: () = assert!(Descriptor::ID_OFFSET == 12);
76const _: () = assert!(Descriptor::FLAGS_OFFSET == 14);
77
78impl Descriptor {
79    // VIRTIO spec requires 16-byte alignment for descriptors
80    pub const ALIGN: usize = 16;
81    pub const SIZE: usize = core::mem::size_of::<Self>();
82
83    pub const ADDR_OFFSET: usize = core::mem::offset_of!(Self, addr);
84    pub const LEN_OFFSET: usize = core::mem::offset_of!(Self, len);
85    pub const ID_OFFSET: usize = core::mem::offset_of!(Self, id);
86    pub const FLAGS_OFFSET: usize = core::mem::offset_of!(Self, flags);
87
88    pub fn new(addr: u64, len: u32, id: u16, flags: DescFlags) -> Self {
89        Self {
90            addr,
91            len,
92            id,
93            flags: flags.bits(),
94        }
95    }
96
97    /// Get flags as a [`DescFlags`] bitfield.
98    #[inline]
99    pub fn flags(&self) -> DescFlags {
100        DescFlags::from_bits_truncate(self.flags)
101    }
102
103    /// Did the driver make this descriptor available in the current driver round?
104    #[inline]
105    pub fn is_avail(&self, wrap: bool) -> bool {
106        self.flags().is_avail(wrap)
107    }
108
109    /// Did the device mark this descriptor used in the current device round?
110    #[inline]
111    pub fn is_used(&self, wrap: bool) -> bool {
112        self.flags().is_used(wrap)
113    }
114
115    /// Is this descriptor writable by the device?
116    #[inline]
117    pub fn is_writable(&self) -> bool {
118        self.flags().contains(DescFlags::WRITE)
119    }
120
121    /// Does this descriptor point to a next descriptor in the chain?
122    #[inline]
123    pub fn is_next(&self) -> bool {
124        self.flags().contains(DescFlags::NEXT)
125    }
126
127    /// Mark descriptor as available according to the driver's wrap bit.
128    /// As per the packed-virtqueue description:
129    /// - set AVAIL bit to `driver_wrap`
130    /// - set USED bit to `!driver_wrap` (inverse)
131    #[inline]
132    pub fn mark_avail(&mut self, wrap: bool) {
133        if wrap {
134            self.flags |= DescFlags::AVAIL.bits();
135            self.flags &= !DescFlags::USED.bits();
136        } else {
137            self.flags &= !DescFlags::AVAIL.bits();
138            self.flags |= DescFlags::USED.bits();
139        }
140    }
141
142    /// Mark descriptor as used according to the device's wrap bit.
143    /// As per spec: set both USED and AVAIL bits to match device_wrap
144    #[inline]
145    pub fn mark_used(&mut self, wrap: bool) {
146        if wrap {
147            self.flags |= DescFlags::USED.bits();
148            self.flags |= DescFlags::AVAIL.bits();
149        } else {
150            self.flags &= !DescFlags::USED.bits();
151            self.flags &= !DescFlags::AVAIL.bits();
152        }
153    }
154
155    /// Write a descriptor to memory with release semantics for flags at the given base pointer
156    ///
157    /// This is the primary synchronization point for publishing descriptors.
158    ///
159    /// # Invariant
160    ///
161    /// The caller must ensure that `addr` is valid for writes of Descriptor
162    pub fn write_release<M: MemOps>(&self, mem: &M, addr: u64) -> Result<(), M::Error> {
163        mem.write_val(addr + Self::ADDR_OFFSET as u64, self.addr)?;
164        mem.write_val(addr + Self::LEN_OFFSET as u64, self.len)?;
165        mem.write_val(addr + Self::ID_OFFSET as u64, self.id)?;
166        // Flags written last with release semantics
167        mem.store_release(addr + Self::FLAGS_OFFSET as u64, self.flags)?;
168        Ok(())
169    }
170
171    /// Acquire-load only the flags word - the packed-ring publish point -
172    /// without reading the descriptor body.
173    pub fn read_flags_acquire<M: MemOps>(mem: &M, addr: u64) -> Result<DescFlags, M::Error> {
174        let flags = mem.load_acquire(addr + Self::FLAGS_OFFSET as u64)?;
175        Ok(DescFlags::from_bits_truncate(flags))
176    }
177
178    /// Read the descriptor body (`addr`/`len`/`id`) and combine it with flags
179    /// already obtained from [`read_flags_acquire`](Self::read_flags_acquire).
180    pub fn read_body<M: MemOps>(mem: &M, addr: u64, flags: DescFlags) -> Result<Self, M::Error> {
181        let addr_val: u64 = mem.read_val(addr + Self::ADDR_OFFSET as u64)?;
182        let len: u32 = mem.read_val(addr + Self::LEN_OFFSET as u64)?;
183        let id: u16 = mem.read_val(addr + Self::ID_OFFSET as u64)?;
184
185        Ok(Self {
186            addr: addr_val,
187            len,
188            id,
189            flags: flags.bits(),
190        })
191    }
192}
193
194/// A table of descriptors stored in shared memory.
195#[derive(Debug, Clone, Copy)]
196pub struct DescTable {
197    base_addr: u64,
198    len: usize,
199}
200
201impl DescTable {
202    pub const DEFAULT_LEN: usize = 256;
203
204    /// Create a descriptor table from shared memory.
205    ///
206    /// # Safety
207    ///
208    /// - `base_addr` must be valid for reads and writes of `len` descriptors
209    /// - `base_addr` must be properly aligned for `Descriptor`
210    /// - `len` must not exceed `u16::MAX`
211    /// - memory must remain valid for the lifetime of this table
212    pub unsafe fn from_raw_parts(base_addr: u64, len: usize) -> Self {
213        debug_assert!(base_addr.is_multiple_of(Descriptor::ALIGN as u64));
214        debug_assert!(len <= u16::MAX as usize);
215
216        Self { base_addr, len }
217    }
218
219    /// Get view into descriptor at index or None if idx is out of bounds
220    pub fn desc_addr(&self, idx: u16) -> Option<u64> {
221        if idx >= self.len as u16 {
222            return None;
223        }
224
225        Some(self.base_addr + (idx as u64 * Descriptor::SIZE as u64))
226    }
227
228    /// Get number of descriptors in table
229    pub fn len(&self) -> usize {
230        self.len
231    }
232
233    /// Is the descriptor table empty?
234    pub fn is_empty(&self) -> bool {
235        self.len == 0
236    }
237
238    pub const fn default_len() -> usize {
239        Self::DEFAULT_LEN
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    #[test]
248    fn mark_avail_sets_bits_correctly_wrap_true() {
249        let mut d = Descriptor::zeroed();
250        d.flags = DescFlags::WRITE.bits() | DescFlags::NEXT.bits();
251        d.mark_avail(true);
252        let f = d.flags();
253        assert!(f.contains(DescFlags::AVAIL));
254        assert!(!f.contains(DescFlags::USED));
255        assert!(f.contains(DescFlags::WRITE));
256        assert!(f.contains(DescFlags::NEXT));
257    }
258
259    #[test]
260    fn mark_avail_sets_bits_correctly_wrap_false() {
261        let mut d = Descriptor::zeroed();
262        d.mark_avail(false);
263        let f = d.flags();
264        assert!(!f.contains(DescFlags::AVAIL));
265        assert!(f.contains(DescFlags::USED));
266    }
267
268    #[test]
269    fn mark_used_sets_both_bits_match_wrap_true() {
270        let mut d = Descriptor::zeroed();
271        d.mark_used(true);
272        let f = d.flags();
273        assert!(f.contains(DescFlags::AVAIL));
274        assert!(f.contains(DescFlags::USED));
275    }
276
277    #[test]
278    fn mark_used_sets_both_bits_match_wrap_false() {
279        let mut d = Descriptor::zeroed();
280        d.mark_used(false);
281        let f = d.flags();
282        assert!(!f.contains(DescFlags::AVAIL));
283        assert!(!f.contains(DescFlags::USED));
284    }
285
286    #[test]
287    fn is_avail_and_is_used() {
288        let mut d = Descriptor::zeroed();
289        d.mark_avail(true);
290        assert!(d.is_avail(true));
291        assert!(!d.is_used(true));
292        d.mark_used(true);
293        assert!(d.is_used(true));
294        assert!(!d.is_avail(true));
295        d.mark_avail(false);
296        assert!(d.is_avail(false));
297        assert!(!d.is_used(false));
298        d.mark_used(false);
299        assert!(d.is_used(false));
300        assert!(!d.is_avail(false));
301    }
302
303    #[test]
304    fn writable_and_next_helpers() {
305        let mut d = Descriptor::zeroed();
306        d.flags = (DescFlags::WRITE | DescFlags::NEXT).bits();
307        assert!(d.is_writable());
308        assert!(d.is_next());
309        d.flags = 0;
310        assert!(!d.is_writable());
311        assert!(!d.is_next());
312    }
313
314    #[test]
315    fn avail_then_used_wrap_flip_sequence() {
316        let mut d = Descriptor::zeroed();
317        d.mark_avail(true);
318        assert!(d.is_avail(true));
319        d.mark_used(false);
320        assert!(d.is_used(false));
321        assert!(!d.is_avail(false));
322        d.mark_avail(true);
323        assert!(d.is_avail(true));
324    }
325
326    #[test]
327    fn desc_table_get_out_of_bounds() {
328        // Allocate with extra space to guarantee 16-byte alignment
329        // (Descriptor requires ALIGN=16 but repr(C) only gives 8).
330        let mut buf = vec![0u8; 4 * Descriptor::SIZE + Descriptor::ALIGN];
331        let base = buf.as_mut_ptr() as usize;
332        let aligned = (base + Descriptor::ALIGN - 1) & !(Descriptor::ALIGN - 1);
333        let table = unsafe { DescTable::from_raw_parts(aligned as u64, 4) };
334        assert!(table.desc_addr(3).is_some());
335        assert!(table.desc_addr(4).is_none());
336    }
337}