Skip to content

Commit 95cfb6c

Browse files
committed
feat(virtq): replace guest-to-host calls with virtqueue
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
1 parent 3c68454 commit 95cfb6c

18 files changed

Lines changed: 451 additions & 112 deletions

File tree

Cargo.lock

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

src/hyperlight_common/src/layout.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ pub const SCRATCH_TOP_H2G_RING_GVA_OFFSET: u64 = 0x28;
4141
pub const SCRATCH_TOP_G2H_QUEUE_DEPTH_OFFSET: u64 = 0x30;
4242
pub const SCRATCH_TOP_H2G_QUEUE_DEPTH_OFFSET: u64 = 0x32;
4343
pub const SCRATCH_TOP_VIRTQ_POOL_PAGES_OFFSET: u64 = 0x34;
44+
pub const SCRATCH_TOP_VIRTQ_GENERATION_OFFSET: u64 = 0x36;
4445
pub const SCRATCH_TOP_EXN_STACK_OFFSET: u64 = 0x40;
4546

4647
const _: () = {
@@ -51,7 +52,8 @@ const _: () = {
5152
assert!(SCRATCH_TOP_H2G_RING_GVA_OFFSET + 8 <= SCRATCH_TOP_G2H_QUEUE_DEPTH_OFFSET);
5253
assert!(SCRATCH_TOP_G2H_QUEUE_DEPTH_OFFSET + 2 <= SCRATCH_TOP_H2G_QUEUE_DEPTH_OFFSET);
5354
assert!(SCRATCH_TOP_H2G_QUEUE_DEPTH_OFFSET + 2 <= SCRATCH_TOP_VIRTQ_POOL_PAGES_OFFSET);
54-
assert!(SCRATCH_TOP_VIRTQ_POOL_PAGES_OFFSET + 2 <= SCRATCH_TOP_EXN_STACK_OFFSET);
55+
assert!(SCRATCH_TOP_VIRTQ_POOL_PAGES_OFFSET + 2 <= SCRATCH_TOP_VIRTQ_GENERATION_OFFSET);
56+
assert!(SCRATCH_TOP_VIRTQ_GENERATION_OFFSET + 2 <= SCRATCH_TOP_EXN_STACK_OFFSET);
5557
assert!(SCRATCH_TOP_EXN_STACK_OFFSET % 0x10 == 0);
5658
};
5759

src/hyperlight_guest/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Provides only the essential building blocks for interacting with the host enviro
1515
anyhow = { version = "1.0.102", default-features = false }
1616
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
1717
hyperlight-common = { workspace = true, default-features = false }
18+
bytemuck = { version = "1.24", features = ["derive"] }
1819
flatbuffers = { version= "25.12.19", default-features = false }
1920
tracing = { version = "0.1.44", default-features = false, features = ["attributes"] }
2021

src/hyperlight_guest/src/error.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ limitations under the License.
1717
use alloc::format;
1818
use alloc::string::{String, ToString as _};
1919

20-
use anyhow;
21-
use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
20+
pub(crate) use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
21+
use hyperlight_common::flatbuffer_wrappers::guest_error::GuestError;
2222
use hyperlight_common::func::Error as FuncError;
23-
use serde_json;
23+
use hyperlight_common::virtq::VirtqError;
24+
use {anyhow, serde_json};
2425

2526
pub type Result<T> = core::result::Result<T, HyperlightGuestError>;
2627

@@ -81,6 +82,24 @@ impl From<FuncError> for HyperlightGuestError {
8182
}
8283
}
8384

85+
impl From<VirtqError> for HyperlightGuestError {
86+
fn from(e: VirtqError) -> Self {
87+
Self {
88+
kind: ErrorCode::GuestError,
89+
message: format!("virtq: {e}"),
90+
}
91+
}
92+
}
93+
94+
impl From<GuestError> for HyperlightGuestError {
95+
fn from(e: GuestError) -> Self {
96+
Self {
97+
kind: e.code,
98+
message: e.message,
99+
}
100+
}
101+
}
102+
84103
/// Extension trait to add context to `Option<T>` and `Result<T, E>` types in guest code,
85104
/// converting them to `Result<T, HyperlightGuestError>`.
86105
///

src/hyperlight_guest/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub mod error;
2525
pub mod exit;
2626
pub mod layout;
2727
pub mod prim_alloc;
28-
pub mod virtq_mem;
28+
pub mod virtq;
2929

3030
pub mod guest_handle {
3131
pub mod handle;
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+
//! Guest virtqueue context.
18+
19+
use alloc::sync::Arc;
20+
use alloc::vec::Vec;
21+
use core::num::NonZeroU16;
22+
use core::sync::atomic::AtomicU16;
23+
use core::sync::atomic::Ordering::Relaxed;
24+
25+
use flatbuffers::FlatBufferBuilder;
26+
use hyperlight_common::flatbuffer_wrappers::function_call::{FunctionCall, FunctionCallType};
27+
use hyperlight_common::flatbuffer_wrappers::function_types::{
28+
FunctionCallResult, ParameterValue, ReturnType, ReturnValue,
29+
};
30+
use hyperlight_common::flatbuffer_wrappers::util::estimate_flatbuffer_capacity;
31+
use hyperlight_common::outb::OutBAction;
32+
use hyperlight_common::virtq::msg::{MsgKind, VirtqMsgHeader};
33+
use hyperlight_common::virtq::{BufferPool, Layout, Notifier, QueueStats, VirtqProducer};
34+
35+
use super::GuestMemOps;
36+
use crate::bail;
37+
use crate::error::Result;
38+
39+
static REQUEST_ID: AtomicU16 = AtomicU16::new(0);
40+
const MAX_RESPONSE_CAP: usize = 4096;
41+
42+
/// Guest-side notifier that triggers a VM exit via outb.
43+
#[derive(Clone, Copy)]
44+
pub struct GuestNotifier;
45+
46+
impl Notifier for GuestNotifier {
47+
fn notify(&self, _stats: QueueStats) {
48+
unsafe { crate::exit::out32(OutBAction::VirtqNotify as u16, 0) };
49+
}
50+
}
51+
52+
/// Type alias for the guest-side G2H producer.
53+
pub type G2hProducer = VirtqProducer<GuestMemOps, GuestNotifier, Arc<BufferPool>>;
54+
55+
/// Virtqueue runtime state for guest-host communication.
56+
pub struct GuestContext {
57+
g2h_pool: Arc<BufferPool>,
58+
g2h_producer: G2hProducer,
59+
generation: u16,
60+
}
61+
62+
impl GuestContext {
63+
/// Create a new context with a G2H queue.
64+
///
65+
/// # Safety
66+
///
67+
/// `ring_gva` must point to valid, zeroed ring memory.
68+
/// `pool_gva` must point to valid, zeroed memory.
69+
pub unsafe fn new(
70+
ring_gva: u64,
71+
num_descs: u16,
72+
pool_gva: u64,
73+
pool_size: usize,
74+
generation: u16,
75+
) -> Self {
76+
let pool = Arc::new(
77+
BufferPool::new(pool_gva, pool_size).expect("failed to create G2H buffer pool"),
78+
);
79+
let nz = NonZeroU16::new(num_descs).expect("G2H queue depth must be non-zero");
80+
let layout = unsafe { Layout::from_base(ring_gva, nz) }.expect("invalid G2H ring layout");
81+
let producer = VirtqProducer::new(layout, GuestMemOps, GuestNotifier, pool.clone());
82+
83+
Self {
84+
g2h_pool: pool,
85+
g2h_producer: producer,
86+
generation,
87+
}
88+
}
89+
90+
/// Call a host function via the G2H virtqueue.
91+
pub fn call_host_function<T: TryFrom<ReturnValue>>(
92+
&mut self,
93+
function_name: &str,
94+
parameters: Option<Vec<ParameterValue>>,
95+
return_type: ReturnType,
96+
) -> Result<T> {
97+
let params = parameters.as_deref().unwrap_or_default();
98+
let estimated_capacity = estimate_flatbuffer_capacity(function_name, params);
99+
100+
let fc = FunctionCall::new(
101+
function_name.into(),
102+
parameters,
103+
FunctionCallType::Host,
104+
return_type,
105+
);
106+
107+
let mut builder = FlatBufferBuilder::with_capacity(estimated_capacity);
108+
let payload = fc.encode(&mut builder);
109+
110+
let reqid = REQUEST_ID.fetch_add(1, Relaxed);
111+
let hdr = VirtqMsgHeader::new(MsgKind::Request, reqid, payload.len() as u32);
112+
let hdr_bytes = bytemuck::bytes_of(&hdr);
113+
114+
let entry_len = VirtqMsgHeader::SIZE + payload.len();
115+
116+
let mut entry = self
117+
.g2h_producer
118+
.chain()
119+
.entry(entry_len)
120+
.completion(MAX_RESPONSE_CAP)
121+
.build()?;
122+
123+
entry.write_all(hdr_bytes)?;
124+
entry.write_all(payload)?;
125+
self.g2h_producer.submit(entry)?;
126+
127+
let Some(completion) = self.g2h_producer.poll()? else {
128+
bail!("G2H: no completion received");
129+
};
130+
131+
let result_bytes = &completion.data;
132+
if result_bytes.len() > MAX_RESPONSE_CAP {
133+
bail!("G2H: response is too large");
134+
}
135+
136+
let payload_bytes = &result_bytes[VirtqMsgHeader::SIZE..];
137+
let Ok(fcr) = FunctionCallResult::try_from(payload_bytes) else {
138+
bail!("G2H: malformed response");
139+
};
140+
141+
let ret = fcr.into_inner()?;
142+
let Ok(ret) = T::try_from(ret) else {
143+
bail!("G2H: host return value type mismatch");
144+
};
145+
146+
Ok(ret)
147+
}
148+
149+
/// Reset ring and pool state after snapshot restore.
150+
pub(super) fn reset(&mut self, new_generation: u16) {
151+
self.g2h_producer.reset();
152+
self.g2h_pool.reset();
153+
self.generation = new_generation;
154+
}
155+
156+
pub(super) fn generation(&self) -> u16 {
157+
self.generation
158+
}
159+
}
Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,29 +31,21 @@ impl MemOps for GuestMemOps {
3131
type Error = Infallible;
3232

3333
fn read(&self, addr: u64, dst: &mut [u8]) -> Result<usize, Self::Error> {
34-
let src = addr as *const u8;
35-
unsafe {
36-
ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), dst.len());
37-
}
34+
unsafe { ptr::copy_nonoverlapping(addr as *const u8, dst.as_mut_ptr(), dst.len()) };
3835
Ok(dst.len())
3936
}
4037

4138
fn write(&self, addr: u64, src: &[u8]) -> Result<usize, Self::Error> {
42-
let dst = addr as *mut u8;
43-
unsafe {
44-
ptr::copy_nonoverlapping(src.as_ptr(), dst, src.len());
45-
}
39+
unsafe { ptr::copy_nonoverlapping(src.as_ptr(), addr as *mut u8, src.len()) };
4640
Ok(src.len())
4741
}
4842

4943
fn load_acquire(&self, addr: u64) -> Result<u16, Self::Error> {
50-
let ptr = addr as *const AtomicU16;
51-
Ok(unsafe { (*ptr).load(Ordering::Acquire) })
44+
Ok(unsafe { (*(addr as *const AtomicU16)).load(Ordering::Acquire) })
5245
}
5346

5447
fn store_release(&self, addr: u64, val: u16) -> Result<(), Self::Error> {
55-
let ptr = addr as *const AtomicU16;
56-
unsafe { (*ptr).store(val, Ordering::Release) };
48+
unsafe { (*(addr as *const AtomicU16)).store(val, Ordering::Release) };
5749
Ok(())
5850
}
5951

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
//! Guest-side virtqueue support.
18+
//!
19+
//! Global context is installed once via [`set_global_context`] and
20+
//! accessed via [`with_context`].
21+
22+
pub mod context;
23+
pub mod mem;
24+
25+
use core::cell::UnsafeCell;
26+
use core::sync::atomic::{AtomicU8, Ordering};
27+
28+
use context::GuestContext;
29+
use hyperlight_common::layout::{SCRATCH_TOP_VIRTQ_GENERATION_OFFSET, scratch_top_ptr};
30+
pub use mem::GuestMemOps;
31+
32+
// Init state machine
33+
const UNINITIALIZED: u8 = 0;
34+
const INITIALIZED: u8 = 1;
35+
static INIT_STATE: AtomicU8 = AtomicU8::new(UNINITIALIZED);
36+
37+
/// Check if the global context has been initialized.
38+
pub fn is_initialized() -> bool {
39+
INIT_STATE.load(Ordering::Acquire) == INITIALIZED
40+
}
41+
42+
// Storage: UnsafeCell guarded by atomic init state.
43+
struct SyncWrap<T>(T);
44+
unsafe impl<T> Sync for SyncWrap<T> {}
45+
46+
static GLOBAL_CONTEXT: SyncWrap<UnsafeCell<Option<GuestContext>>> = SyncWrap(UnsafeCell::new(None));
47+
48+
/// Access the global guest context via closure.
49+
///
50+
/// # Panics
51+
///
52+
/// Panics if the context has not been initialized.
53+
pub fn with_context<R>(f: impl FnOnce(&mut GuestContext) -> R) -> R {
54+
assert!(
55+
INIT_STATE.load(Ordering::Acquire) == INITIALIZED,
56+
"guest context not initialized"
57+
);
58+
let ctx = unsafe { &mut *GLOBAL_CONTEXT.0.get() };
59+
f(ctx.as_mut().unwrap())
60+
}
61+
62+
/// Install the global guest context. Called once during guest init.
63+
///
64+
/// # Panics
65+
///
66+
/// Panics if called more than once.
67+
pub fn set_global_context(ctx: GuestContext) {
68+
if INIT_STATE
69+
.compare_exchange(
70+
UNINITIALIZED,
71+
INITIALIZED,
72+
Ordering::SeqCst,
73+
Ordering::SeqCst,
74+
)
75+
.is_err()
76+
{
77+
panic!("guest context already initialized");
78+
}
79+
unsafe { *GLOBAL_CONTEXT.0.get() = Some(ctx) };
80+
}
81+
82+
/// Reset the global context if a snapshot restore was detected.
83+
/// Compares the virtq generation counter in scratch-top metadata.
84+
pub fn reset_global_context() {
85+
if !is_initialized() {
86+
return;
87+
}
88+
let current_gen = read_gen();
89+
with_context(|ctx| {
90+
if current_gen != ctx.generation() {
91+
ctx.reset(current_gen);
92+
}
93+
});
94+
}
95+
96+
/// Read the current virtqueue generation from scratch-top metadata.
97+
fn read_gen() -> u16 {
98+
unsafe { *scratch_top_ptr::<u16>(SCRATCH_TOP_VIRTQ_GENERATION_OFFSET) }
99+
}

src/hyperlight_guest_bin/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ and third-party code used by our C-API needed to build a native hyperlight-guest
1414
"""
1515

1616
[features]
17-
default = ["libc", "printf", "macros"]
17+
default = ["libc", "printf", "macros", "virtq"]
1818
libc = [] # compile musl libc
1919
printf = [ "libc" ] # compile printf
20+
virtq = [] # use virtqueue for guest-to-host calls
2021
trace_guest = ["hyperlight-common/trace_guest", "hyperlight-guest/trace_guest", "hyperlight-guest-tracing/trace"]
2122
mem_profile = ["hyperlight-common/mem_profile"]
2223
macros = ["dep:hyperlight-guest-macro", "dep:linkme"]

0 commit comments

Comments
 (0)