Skip to content

Commit cee5ccd

Browse files
authored
feat(virtq): add packed virtio ring primitives (#1382)
* feat(virtq): add packed virtio ring primitives Add low-level packed virtqueue ring implementation in hyperlight_common::virtq, based on the virtio packed ring format. Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com> * fix(virtq): address code review Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com> * fix(virtq): make advance API of cursor private Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com> * fix(virtq): replace assert with debug_assert Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com> * fix(virtq): fix writable spelling Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com> * fix(virtq): address code review Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com> * fix(virtq): regenerate wit guest lock file Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com> * feat(virtq): address code review comments Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com> * feat(virtq): provide more context for mem errors Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com> * fix(virtq): convert debug asserts into errors Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com> --------- Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
1 parent da39363 commit cee5ccd

12 files changed

Lines changed: 4623 additions & 68 deletions

File tree

Cargo.lock

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/hyperlight_common/Cargo.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@ Hyperlight's components common to host and guest.
1515
workspace = true
1616

1717
[dependencies]
18-
flatbuffers = { version = "25.12.19", default-features = false }
18+
arbitrary = {version = "1.4.2", optional = true, features = ["derive"]}
1919
anyhow = { version = "1.0.102", default-features = false }
20+
bitflags = "2.10.0"
21+
bytemuck = { version = "1.24", features = ["derive"] }
22+
flatbuffers = { version = "25.12.19", default-features = false }
2023
log = "0.4.29"
21-
tracing = { version = "0.1.44", optional = true }
22-
arbitrary = {version = "1.4.2", optional = true, features = ["derive"]}
24+
smallvec = "1.15.1"
2325
spin = "0.10.0"
2426
thiserror = { version = "2.0.18", default-features = false }
27+
tracing = { version = "0.1.44", optional = true }
2528
tracing-core = { version = "0.1.36", default-features = false }
2629

2730
[features]
@@ -35,6 +38,10 @@ i686-guest = []
3538
nanvix-unstable = ["i686-guest"]
3639
guest-counter = []
3740

41+
[dev-dependencies]
42+
quickcheck = "1.0.3"
43+
rand = "0.9.2"
44+
3845
[lib]
3946
bench = false # see https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options
4047
doctest = false # reduce noise in test output

src/hyperlight_common/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ limitations under the License.
1818
#![cfg_attr(not(any(test, debug_assertions)), warn(clippy::expect_used))]
1919
#![cfg_attr(not(any(test, debug_assertions)), warn(clippy::unwrap_used))]
2020
// We use Arbitrary during fuzzing, which requires std
21-
#![cfg_attr(not(feature = "fuzzing"), no_std)]
21+
#![cfg_attr(not(any(feature = "fuzzing", test, miri)), no_std)]
2222

2323
extern crate alloc;
2424

@@ -50,3 +50,6 @@ pub mod vmem;
5050

5151
/// ELF note types for embedding hyperlight version metadata in guest binaries.
5252
pub mod version_note;
53+
54+
/// cbindgen:ignore
55+
pub mod virtq;
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
/*
2+
Copyright 2026 The Hyperlight Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
//! Memory Access Traits for Virtqueue Operations
18+
//!
19+
//! This module defines the [`MemOps`] trait that abstracts memory access patterns
20+
//! required by the virtqueue implementation. This allows the virtqueue code to
21+
//! work with different memory backends e.g. Host vs Guest.
22+
23+
use alloc::sync::Arc;
24+
25+
use bytemuck::Pod;
26+
27+
/// Backend-provided memory access for virtqueue.
28+
///
29+
/// # Safety
30+
///
31+
/// Implementations must ensure that:
32+
/// - Addresses accepted by these methods are translated according to the
33+
/// backend's memory model.
34+
/// - Invalid or inaccessible addresses are reported with `Self::Error` rather
35+
/// than causing undefined behavior.
36+
/// - Memory ordering guarantees are upheld as documented.
37+
/// - Typed reads/writes and atomic operations honor alignment and initialized
38+
/// memory requirements for the translated addresses.
39+
///
40+
/// [`RingProducer`]: super::RingProducer
41+
/// [`RingConsumer`]: super::RingConsumer
42+
pub unsafe trait MemOps {
43+
type Error;
44+
45+
/// Read bytes from physical memory.
46+
///
47+
/// Used for reading buffer contents pointed to by descriptors.
48+
///
49+
/// # Arguments
50+
///
51+
/// * `addr` - Guest physical address to read from
52+
/// * `dst` - Destination buffer to fill
53+
///
54+
/// Implementations must return an error if `addr` cannot be read for
55+
/// at least `dst.len()` bytes.
56+
fn read(&self, addr: u64, dst: &mut [u8]) -> Result<(), Self::Error>;
57+
58+
/// Write bytes to physical memory.
59+
///
60+
/// # Arguments
61+
///
62+
/// * `addr` - address to write to
63+
/// * `src` - Source data to write
64+
///
65+
/// Implementations must return an error if `addr` cannot be written for
66+
/// at least `src.len()` bytes.
67+
fn write(&self, addr: u64, src: &[u8]) -> Result<(), Self::Error>;
68+
69+
/// Load a u16 with acquire semantics.
70+
///
71+
/// Implementations must return an error if `addr` does not translate to a
72+
/// valid, aligned `AtomicU16` in shared memory.
73+
fn load_acquire(&self, addr: u64) -> Result<u16, Self::Error>;
74+
75+
/// Store a u16 with release semantics.
76+
///
77+
/// Implementations must return an error if `addr` does not translate to a
78+
/// valid, aligned `AtomicU16` in shared memory.
79+
fn store_release(&self, addr: u64, val: u16) -> Result<(), Self::Error>;
80+
81+
/// Get a direct read-only slice into shared memory.
82+
///
83+
/// # Safety
84+
///
85+
/// The caller must ensure:
86+
/// - `addr` is valid and points to at least `len` bytes.
87+
/// - The memory region is not concurrently modified for the lifetime of
88+
/// the returned slice. Caller must uphold this via protocol-level
89+
/// synchronisation, e.g. descriptor ownership transfer.
90+
///
91+
/// See also [`BufferOwner`]: super::BufferOwner
92+
unsafe fn as_slice(&self, addr: u64, len: usize) -> Result<&[u8], Self::Error>;
93+
94+
/// Get a direct mutable slice into shared memory.
95+
///
96+
/// # Safety
97+
///
98+
/// The caller must ensure:
99+
/// - `addr` is valid and points to at least `len` bytes.
100+
/// - No other references (shared or mutable) to this memory region exist
101+
/// for the lifetime of the returned slice.
102+
/// - Protocol-level synchronisation (e.g. descriptor ownership) guarantees
103+
/// exclusive access.
104+
#[allow(clippy::mut_from_ref)]
105+
unsafe fn as_mut_slice(&self, addr: u64, len: usize) -> Result<&mut [u8], Self::Error>;
106+
107+
/// Read a Pod type at the given pointer.
108+
///
109+
/// Implementations must return an error if `addr` is not valid, aligned,
110+
/// and initialized for `T`.
111+
fn read_val<T: Pod>(&self, addr: u64) -> Result<T, Self::Error> {
112+
let mut val = T::zeroed();
113+
let bytes = bytemuck::bytes_of_mut(&mut val);
114+
115+
self.read(addr, bytes)?;
116+
Ok(val)
117+
}
118+
119+
/// Write a Pod type at the given pointer.
120+
///
121+
/// Implementations must return an error if `addr` is not valid and aligned
122+
/// for `T`.
123+
fn write_val<T: Pod>(&self, addr: u64, val: T) -> Result<(), Self::Error> {
124+
let bytes = bytemuck::bytes_of(&val);
125+
self.write(addr, bytes)?;
126+
Ok(())
127+
}
128+
}
129+
130+
// SAFETY: Arc delegates all memory operations to the wrapped backend, preserving
131+
// that backend's MemOps contract.
132+
unsafe impl<T: MemOps> MemOps for Arc<T> {
133+
type Error = T::Error;
134+
135+
fn read(&self, addr: u64, dst: &mut [u8]) -> Result<(), Self::Error> {
136+
(**self).read(addr, dst)
137+
}
138+
139+
fn write(&self, addr: u64, src: &[u8]) -> Result<(), Self::Error> {
140+
(**self).write(addr, src)
141+
}
142+
143+
fn load_acquire(&self, addr: u64) -> Result<u16, Self::Error> {
144+
(**self).load_acquire(addr)
145+
}
146+
147+
fn store_release(&self, addr: u64, val: u16) -> Result<(), Self::Error> {
148+
(**self).store_release(addr, val)
149+
}
150+
151+
unsafe fn as_slice(&self, addr: u64, len: usize) -> Result<&[u8], Self::Error> {
152+
unsafe { (**self).as_slice(addr, len) }
153+
}
154+
155+
#[allow(clippy::mut_from_ref)]
156+
unsafe fn as_mut_slice(&self, addr: u64, len: usize) -> Result<&mut [u8], Self::Error> {
157+
unsafe { (**self).as_mut_slice(addr, len) }
158+
}
159+
}

0 commit comments

Comments
 (0)