Skip to content

Commit 528a5c0

Browse files
committed
feat(virtq): create G2H producer during guest init
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
1 parent 02e8a70 commit 528a5c0

9 files changed

Lines changed: 216 additions & 56 deletions

File tree

src/hyperlight_common/src/layout.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,18 @@ pub const SCRATCH_TOP_G2H_RING_GVA_OFFSET: u64 = 0x20;
4040
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;
43+
pub const SCRATCH_TOP_VIRTQ_POOL_PAGES_OFFSET: u64 = 0x34;
4344
pub const SCRATCH_TOP_EXN_STACK_OFFSET: u64 = 0x40;
4445

45-
// fields must not overlap, and exception stack address must be 16-byte aligned.
4646
const _: () = {
4747
assert!(SCRATCH_TOP_SIZE_OFFSET + 8 <= SCRATCH_TOP_ALLOCATOR_OFFSET);
4848
assert!(SCRATCH_TOP_ALLOCATOR_OFFSET + 8 <= SCRATCH_TOP_SNAPSHOT_PT_GPA_BASE_OFFSET);
4949
assert!(SCRATCH_TOP_SNAPSHOT_PT_GPA_BASE_OFFSET + 8 <= SCRATCH_TOP_G2H_RING_GVA_OFFSET);
5050
assert!(SCRATCH_TOP_G2H_RING_GVA_OFFSET + 8 <= SCRATCH_TOP_H2G_RING_GVA_OFFSET);
5151
assert!(SCRATCH_TOP_H2G_RING_GVA_OFFSET + 8 <= SCRATCH_TOP_G2H_QUEUE_DEPTH_OFFSET);
5252
assert!(SCRATCH_TOP_G2H_QUEUE_DEPTH_OFFSET + 2 <= SCRATCH_TOP_H2G_QUEUE_DEPTH_OFFSET);
53-
assert!(SCRATCH_TOP_H2G_QUEUE_DEPTH_OFFSET + 2 <= SCRATCH_TOP_EXN_STACK_OFFSET);
53+
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);
5455
assert!(SCRATCH_TOP_EXN_STACK_OFFSET % 0x10 == 0);
5556
};
5657

@@ -70,9 +71,13 @@ pub fn scratch_base_gva(size: usize) -> u64 {
7071
(MAX_GVA - size + 1) as u64
7172
}
7273

74+
pub const fn scratch_top_ptr<T>(offset: u64) -> *mut T {
75+
(MAX_GVA as u64 - offset + 1) as *mut T
76+
}
77+
7378
/// Compute the byte offset from the scratch base to the G2H ring.
7479
///
75-
/// TODO(ring): Remove input/output
80+
/// TODO(virtq): Remove input/output
7681
pub const fn g2h_ring_scratch_offset(input_data_size: usize, output_data_size: usize) -> usize {
7782
let io_off = input_data_size + output_data_size;
7883
let align = crate::virtq::Descriptor::ALIGN;

src/hyperlight_common/src/virtq/pool.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,30 @@ pub trait BufferProvider {
126126
fn resize(&self, old_alloc: Allocation, new_len: usize) -> Result<Allocation, AllocError>;
127127
}
128128

129+
impl<T: BufferProvider> BufferProvider for alloc::rc::Rc<T> {
130+
fn alloc(&self, len: usize) -> Result<Allocation, AllocError> {
131+
(**self).alloc(len)
132+
}
133+
fn dealloc(&self, alloc: Allocation) -> Result<(), AllocError> {
134+
(**self).dealloc(alloc)
135+
}
136+
fn resize(&self, old_alloc: Allocation, new_len: usize) -> Result<Allocation, AllocError> {
137+
(**self).resize(old_alloc, new_len)
138+
}
139+
}
140+
141+
impl<T: BufferProvider> BufferProvider for alloc::sync::Arc<T> {
142+
fn alloc(&self, len: usize) -> Result<Allocation, AllocError> {
143+
(**self).alloc(len)
144+
}
145+
fn dealloc(&self, alloc: Allocation) -> Result<(), AllocError> {
146+
(**self).dealloc(alloc)
147+
}
148+
fn resize(&self, old_alloc: Allocation, new_len: usize) -> Result<Allocation, AllocError> {
149+
(**self).resize(old_alloc, new_len)
150+
}
151+
}
152+
129153
/// The owner of a mapped buffer, ensuring its lifetime.
130154
///
131155
/// Holds a pool allocation and provides direct access to the underlying

src/hyperlight_guest_bin/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ pub mod host_comm;
5252
pub mod memory;
5353
#[cfg(target_arch = "x86_64")]
5454
pub mod paging;
55-
mod virtq_init;
55+
mod virtq;
5656

5757
// Globals
5858
#[cfg(all(feature = "mem_profile", target_arch = "x86_64"))]
@@ -237,7 +237,7 @@ pub(crate) extern "C" fn generic_init(
237237
}
238238

239239
// Initialize virtqueues
240-
virtq_init::init_virtqueues();
240+
virtq::init_virtqueues();
241241

242242
// set up the logger
243243
let guest_log_level_filter =
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 initialization.
18+
//!
19+
//! Zeroes ring memory and creates VirtqProducer instances by allocating
20+
//! buffer pool pages from the scratch page allocator.
21+
22+
pub(crate) mod state;
23+
24+
use hyperlight_common::layout::{
25+
SCRATCH_TOP_G2H_QUEUE_DEPTH_OFFSET, SCRATCH_TOP_G2H_RING_GVA_OFFSET,
26+
SCRATCH_TOP_H2G_QUEUE_DEPTH_OFFSET, SCRATCH_TOP_H2G_RING_GVA_OFFSET,
27+
SCRATCH_TOP_VIRTQ_POOL_PAGES_OFFSET, scratch_top_ptr,
28+
};
29+
use hyperlight_common::mem::PAGE_SIZE_USIZE;
30+
use hyperlight_common::virtq::Layout as VirtqLayout;
31+
use hyperlight_guest::prim_alloc::alloc_phys_pages;
32+
33+
use crate::paging::phys_to_virt;
34+
35+
/// Initialize virtqueue producers for G2H and H2G queues.
36+
pub(crate) fn init_virtqueues() {
37+
let g2h_gva = unsafe { *scratch_top_ptr::<u64>(SCRATCH_TOP_G2H_RING_GVA_OFFSET) };
38+
let g2h_depth = unsafe { *scratch_top_ptr::<u16>(SCRATCH_TOP_G2H_QUEUE_DEPTH_OFFSET) };
39+
let h2g_gva = unsafe { *scratch_top_ptr::<u64>(SCRATCH_TOP_H2G_RING_GVA_OFFSET) };
40+
let h2g_depth = unsafe { *scratch_top_ptr::<u16>(SCRATCH_TOP_H2G_QUEUE_DEPTH_OFFSET) };
41+
let pool_pages = unsafe { *scratch_top_ptr::<u16>(SCRATCH_TOP_VIRTQ_POOL_PAGES_OFFSET) } as u64;
42+
43+
assert!(g2h_depth > 0 && h2g_depth > 0);
44+
assert!(g2h_gva != 0 && h2g_gva != 0);
45+
assert!(pool_pages > 0);
46+
47+
// Zero ring memory
48+
let g2h_ring_size = VirtqLayout::query_size(g2h_depth as usize);
49+
unsafe { core::ptr::write_bytes(g2h_gva as *mut u8, 0, g2h_ring_size) };
50+
51+
let h2g_ring_size = VirtqLayout::query_size(h2g_depth as usize);
52+
unsafe { core::ptr::write_bytes(h2g_gva as *mut u8, 0, h2g_ring_size) };
53+
54+
// Allocate buffer pool from physical pages
55+
let pool_gpa = unsafe { alloc_phys_pages(pool_pages) };
56+
let pool_ptr = phys_to_virt(pool_gpa).expect("failed to map pool pages");
57+
let pool_gva = pool_ptr as u64;
58+
let pool_size = pool_pages as usize * PAGE_SIZE_USIZE;
59+
unsafe { core::ptr::write_bytes(pool_ptr, 0, pool_size) };
60+
61+
// Create G2H producer
62+
unsafe {
63+
state::init_g2h_producer(g2h_gva, g2h_depth, pool_gva, pool_size);
64+
}
65+
66+
// TODO(virtq): add other direction's producer
67+
let _ = (h2g_gva, h2g_depth);
68+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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 state and initialization.
18+
//!
19+
//! Holds the global VirtqProducer instances for G2H and H2G queues.
20+
//! The producers are created during guest init (from `hyperlight_guest_bin`)
21+
//! and used by the guest host-call path in `host_comm`.
22+
23+
use alloc::rc::Rc;
24+
use core::cell::RefCell;
25+
use core::num::NonZeroU16;
26+
27+
use hyperlight_common::virtq::{BufferPool, Layout, Notifier, QueueStats, VirtqProducer};
28+
use hyperlight_guest::virtq_mem::GuestMemOps;
29+
30+
/// Wrapper to mark types as Sync for single-threaded guest execution.
31+
struct SyncWrap<T>(T);
32+
33+
// SAFETY: guest execution is single-threaded.
34+
unsafe impl<T> Sync for SyncWrap<T> {}
35+
36+
/// Guest-side notifier (no-op).
37+
#[derive(Clone, Copy)]
38+
pub struct GuestNotifier;
39+
40+
impl Notifier for GuestNotifier {
41+
fn notify(&self, _stats: QueueStats) {}
42+
}
43+
44+
/// Type alias for the guest-side producer.
45+
pub type GuestProducer = VirtqProducer<GuestMemOps, GuestNotifier, Rc<BufferPool>>;
46+
/// Global G2H producer instance, initialized during guest init.
47+
static G2H_PRODUCER: SyncWrap<RefCell<Option<GuestProducer>>> = SyncWrap(RefCell::new(None));
48+
49+
/// Borrow the G2H producer mutably.
50+
///
51+
/// # Panics
52+
///
53+
/// Panics if the G2H producer has not been initialized or is already
54+
/// borrowed.
55+
pub fn with_g2h_producer<R>(f: impl FnOnce(&mut GuestProducer) -> R) -> R {
56+
let mut guard = G2H_PRODUCER.0.borrow_mut();
57+
let producer = guard.as_mut().expect("G2H producer not initialized");
58+
f(producer)
59+
}
60+
61+
/// Initialize the G2H producer
62+
///
63+
/// # Safety
64+
///
65+
/// The ring GVA must point to valid, zeroed ring memory of the
66+
/// appropriate size. The pool GVA must point to valid, zeroed memory.
67+
pub unsafe fn init_g2h_producer(ring_gva: u64, num_descs: u16, pool_gva: u64, pool_size: usize) {
68+
let nz = NonZeroU16::new(num_descs).expect("G2H queue depth must be non-zero");
69+
let pool = BufferPool::new(pool_gva, pool_size).expect("failed to create G2H buffer pool");
70+
71+
let layout = unsafe { Layout::from_base(ring_gva, nz) }.expect("invalid G2H ring layout");
72+
let producer = VirtqProducer::new(layout, GuestMemOps, GuestNotifier, Rc::new(pool));
73+
74+
*G2H_PRODUCER.0.borrow_mut() = Some(producer);
75+
}

src/hyperlight_guest_bin/src/virtq_init.rs

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/hyperlight_host/src/mem/mgr.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,10 @@ impl SandboxMemoryManager<HostSharedMemory> {
574574
scratch_size - SCRATCH_TOP_H2G_QUEUE_DEPTH_OFFSET as usize,
575575
self.layout.sandbox_memory_config.get_h2g_queue_depth() as u16,
576576
)?;
577+
self.scratch_mem.write::<u16>(
578+
scratch_size - SCRATCH_TOP_VIRTQ_POOL_PAGES_OFFSET as usize,
579+
self.layout.sandbox_memory_config.get_virtq_pool_pages() as u16,
580+
)?;
577581

578582
// Copy the page tables into the scratch region
579583
let snapshot_pt_end = self.shared_mem.mem_size();

src/hyperlight_host/src/sandbox/config.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ pub struct SandboxConfiguration {
8080
/// Number of descriptors for the host-to-guest virtqueue. Must be a power of 2.
8181
/// Default: 32
8282
h2g_queue_depth: usize,
83+
/// Number of physical pages to allocate for each virtqueue's buffer pool.
84+
/// Default: 8 pages (32KB).
85+
virtq_pool_pages: usize,
8386
}
8487

8588
impl SandboxConfiguration {
@@ -103,6 +106,8 @@ impl SandboxConfiguration {
103106
pub const DEFAULT_G2H_QUEUE_DEPTH: usize = 64;
104107
/// The default H2G virtqueue depth (number of descriptors, must be power of 2)
105108
pub const DEFAULT_H2G_QUEUE_DEPTH: usize = 32;
109+
/// The default number of physical pages per virtqueue buffer pool
110+
pub const DEFAULT_VIRTQ_POOL_PAGES: usize = 8;
106111

107112
#[allow(clippy::too_many_arguments)]
108113
/// Create a new configuration for a sandbox with the given sizes.
@@ -126,6 +131,7 @@ impl SandboxConfiguration {
126131
interrupt_vcpu_sigrtmin_offset,
127132
g2h_queue_depth: Self::DEFAULT_G2H_QUEUE_DEPTH,
128133
h2g_queue_depth: Self::DEFAULT_H2G_QUEUE_DEPTH,
134+
virtq_pool_pages: Self::DEFAULT_VIRTQ_POOL_PAGES,
129135
#[cfg(gdb)]
130136
guest_debug_info,
131137
#[cfg(crashdump)]
@@ -231,6 +237,26 @@ impl SandboxConfiguration {
231237
self.h2g_queue_depth
232238
}
233239

240+
/// Get the number of physical pages per virtqueue buffer pool.
241+
pub fn get_virtq_pool_pages(&self) -> usize {
242+
self.virtq_pool_pages
243+
}
244+
245+
/// Set the G2H virtqueue depth (number of descriptors, must be power of 2).
246+
pub fn set_g2h_queue_depth(&mut self, depth: usize) {
247+
self.g2h_queue_depth = depth;
248+
}
249+
250+
/// Set the H2G virtqueue depth (number of descriptors, must be power of 2).
251+
pub fn set_h2g_queue_depth(&mut self, depth: usize) {
252+
self.h2g_queue_depth = depth;
253+
}
254+
255+
/// Set the number of physical pages per virtqueue buffer pool.
256+
pub fn set_virtq_pool_pages(&mut self, pages: usize) {
257+
self.virtq_pool_pages = pages;
258+
}
259+
234260
/// Set the size of the scratch regiong
235261
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
236262
pub fn set_scratch_size(&mut self, scratch_size: usize) {

src/hyperlight_host/tests/integration_test.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,9 @@ fn guest_malloc_abort() {
545545

546546
let mut cfg = SandboxConfiguration::default();
547547
cfg.set_heap_size(heap_size);
548+
cfg.set_g2h_queue_depth(2);
549+
cfg.set_h2g_queue_depth(2);
550+
cfg.set_virtq_pool_pages(2);
548551
with_rust_sandbox_cfg(cfg, |mut sbox2| {
549552
let err = sbox2
550553
.call::<i32>(
@@ -621,6 +624,9 @@ fn guest_panic_no_alloc() {
621624

622625
let mut cfg = SandboxConfiguration::default();
623626
cfg.set_heap_size(heap_size);
627+
cfg.set_g2h_queue_depth(2);
628+
cfg.set_h2g_queue_depth(2);
629+
cfg.set_virtq_pool_pages(2);
624630
with_rust_sandbox_cfg(cfg, |mut sbox| {
625631
let res = sbox
626632
.call::<i32>(
@@ -1680,7 +1686,9 @@ fn exception_handler_installation_and_validation() {
16801686
/// This validates that the exception handling path does not require heap allocations.
16811687
#[test]
16821688
fn fill_heap_and_cause_exception() {
1683-
with_rust_sandbox(|mut sandbox| {
1689+
let mut cfg = SandboxConfiguration::default();
1690+
cfg.set_virtq_pool_pages(2);
1691+
with_rust_sandbox_cfg(cfg, |mut sandbox| {
16841692
let result = sandbox.call::<()>("FillHeapAndCauseException", ());
16851693

16861694
// The call should fail with an exception error since there's no handler installed

0 commit comments

Comments
 (0)