Skip to content

Commit bd2fd96

Browse files
committed
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>
1 parent d9a280f commit bd2fd96

File tree

8 files changed

+4308
-4
lines changed

8 files changed

+4308
-4
lines changed

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]
@@ -33,6 +36,10 @@ mem_profile = []
3336
std = ["thiserror/std", "log/std", "tracing/std"]
3437
nanvix-unstable = []
3538

39+
[dev-dependencies]
40+
quickcheck = "1.0.3"
41+
rand = "0.9.2"
42+
3643
[lib]
3744
bench = false # see https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options
3845
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: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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+
/// - Pointers passed to methods are valid for the duration of the call
33+
/// - Memory ordering guarantees are upheld as documented
34+
/// - Reads and writes don't cause undefined behavior (alignment, validity)
35+
///
36+
/// [`RingProducer`]: super::RingProducer
37+
/// [`RingConsumer`]: super::RingConsumer
38+
pub trait MemOps {
39+
type Error;
40+
41+
/// Read bytes from physical memory.
42+
///
43+
/// Used for reading buffer contents pointed to by descriptors.
44+
///
45+
/// # Arguments
46+
///
47+
/// * `addr` - Guest physical address to read from
48+
/// * `dst` - Destination buffer to fill
49+
///
50+
/// # Returns
51+
///
52+
/// Number of bytes actually read (should equal `dst.len()` on success).
53+
///
54+
/// # Safety
55+
///
56+
/// The caller must ensure `paddr` is valid and points to at least `dst.len()` bytes.
57+
fn read(&self, addr: u64, dst: &mut [u8]) -> Result<usize, Self::Error>;
58+
59+
/// Write bytes to physical memory.
60+
///
61+
/// # Arguments
62+
///
63+
/// * `addr` - address to write to
64+
/// * `src` - Source data to write
65+
///
66+
/// # Returns
67+
///
68+
/// Number of bytes actually written (should equal `src.len()` on success).
69+
///
70+
/// # Safety
71+
///
72+
/// The caller must ensure `paddr` is valid and points to at least `src.len()` bytes.
73+
fn write(&self, addr: u64, src: &[u8]) -> Result<usize, Self::Error>;
74+
75+
/// Load a u16 with acquire semantics.
76+
///
77+
/// # Safety
78+
///
79+
/// `addr` must translate to a valid, aligned `AtomicU16` in shared memory.
80+
fn load_acquire(&self, addr: u64) -> Result<u16, Self::Error>;
81+
82+
/// Store a u16 with release semantics.
83+
///
84+
/// # Safety
85+
///
86+
/// `addr` must translate to a valid `AtomicU16` in shared memory.
87+
fn store_release(&self, addr: u64, val: u16) -> Result<(), Self::Error>;
88+
89+
/// Get a direct read-only slice into shared memory.
90+
///
91+
/// # Safety
92+
///
93+
/// The caller must ensure:
94+
/// - `addr` is valid and points to at least `len` bytes.
95+
/// - The memory region is not concurrently modified for the lifetime of
96+
/// the returned slice. Caller must uphold this via protocol-level
97+
/// synchronisation, e.g. descriptor ownership transfer.
98+
///
99+
/// See also [`BufferOwner`]: super::BufferOwner
100+
unsafe fn as_slice(&self, addr: u64, len: usize) -> Result<&[u8], Self::Error>;
101+
102+
/// Get a direct mutable slice into shared memory.
103+
///
104+
/// # Safety
105+
///
106+
/// The caller must ensure:
107+
/// - `addr` is valid and points to at least `len` bytes.
108+
/// - No other references (shared or mutable) to this memory region exist
109+
/// for the lifetime of the returned slice.
110+
/// - Protocol-level synchronisation (e.g. descriptor ownership) guarantees
111+
/// exclusive access.
112+
#[allow(clippy::mut_from_ref)]
113+
unsafe fn as_mut_slice(&self, addr: u64, len: usize) -> Result<&mut [u8], Self::Error>;
114+
115+
/// Read a Pod type at the given pointer.
116+
///
117+
/// # Safety
118+
///
119+
/// The caller must ensure `addr` is valid, aligned, and translates to initialized memory.
120+
fn read_val<T: Pod>(&self, addr: u64) -> Result<T, Self::Error> {
121+
let mut val = T::zeroed();
122+
let bytes = bytemuck::bytes_of_mut(&mut val);
123+
124+
self.read(addr, bytes)?;
125+
Ok(val)
126+
}
127+
128+
/// Write a Pod type at the given pointer.
129+
///
130+
/// # Safety
131+
///
132+
/// The caller ensures that `ptr` is valid.
133+
fn write_val<T: Pod>(&self, addr: u64, val: T) -> Result<(), Self::Error> {
134+
let bytes = bytemuck::bytes_of(&val);
135+
self.write(addr, bytes)?;
136+
Ok(())
137+
}
138+
}
139+
140+
impl<T: MemOps> MemOps for Arc<T> {
141+
type Error = T::Error;
142+
143+
fn read(&self, addr: u64, dst: &mut [u8]) -> Result<usize, Self::Error> {
144+
(**self).read(addr, dst)
145+
}
146+
147+
fn write(&self, addr: u64, src: &[u8]) -> Result<usize, Self::Error> {
148+
(**self).write(addr, src)
149+
}
150+
151+
fn load_acquire(&self, addr: u64) -> Result<u16, Self::Error> {
152+
(**self).load_acquire(addr)
153+
}
154+
155+
fn store_release(&self, addr: u64, val: u16) -> Result<(), Self::Error> {
156+
(**self).store_release(addr, val)
157+
}
158+
159+
unsafe fn as_slice(&self, addr: u64, len: usize) -> Result<&[u8], Self::Error> {
160+
unsafe { (**self).as_slice(addr, len) }
161+
}
162+
163+
#[allow(clippy::mut_from_ref)]
164+
unsafe fn as_mut_slice(&self, addr: u64, len: usize) -> Result<&mut [u8], Self::Error> {
165+
unsafe { (**self).as_mut_slice(addr, len) }
166+
}
167+
}

0 commit comments

Comments
 (0)