Skip to content

Commit 48c0843

Browse files
committed
feat(virtq): implement G2H reply backlog guard
Signed-off-by: Tomasz Andrzejak <andreiltd@gmail.com>
1 parent 9a9c579 commit 48c0843

2 files changed

Lines changed: 52 additions & 2 deletions

File tree

src/hyperlight_common/src/virtq/producer.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,12 @@ where
384384
self.inner.used_cursor()
385385
}
386386

387+
/// Number of free (unsubmitted) descriptors in the ring.
388+
#[inline]
389+
pub fn num_free(&self) -> usize {
390+
self.inner.num_free()
391+
}
392+
387393
/// Configure event suppression for used buffer notifications.
388394
///
389395
/// This controls when the device (consumer) signals us about completed buffers:

src/hyperlight_guest/src/virtq/context.rs

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ pub struct GuestContext {
7979
h2g_slot_size: usize,
8080
/// snapshot generation counter
8181
generation: u32,
82+
/// Number of H2G requests received that still need a G2H response.
83+
pending_replies: u32,
8284
/// used by cabi
8385
last_host_result: Option<Result<ReturnValue>>,
8486
}
@@ -106,6 +108,7 @@ impl GuestContext {
106108
g2h_response_cap,
107109
h2g_slot_size,
108110
generation,
111+
pending_replies: 0,
109112
last_host_result: None,
110113
};
111114

@@ -114,6 +117,9 @@ impl GuestContext {
114117
}
115118

116119
/// Call a host function via the G2H virtqueue.
120+
///
121+
/// The reply guard is checked before submitting the readwrite chain
122+
/// to ensure G2H capacity is reserved for pending responses.
117123
pub fn call_host_function<T: TryFrom<ReturnValue>>(
118124
&mut self,
119125
function_name: &str,
@@ -139,6 +145,9 @@ impl GuestContext {
139145

140146
let entry_len = VirtqMsgHeader::SIZE + payload.len();
141147

148+
// Reply guard: readwrite chains use 2 descriptors, leave room for pending replies.
149+
self.ensure_reply_capacity(2)?;
150+
142151
let token = match self.try_send_readwrite(hdr_bytes, payload, entry_len) {
143152
Ok(tok) => tok,
144153
Err(e) if e.is_transient() => {
@@ -191,6 +200,9 @@ impl GuestContext {
191200
/// Each descriptor carries a [`VirtqMsgHeader`] with `payload_len` for
192201
/// that chunk. If [`MsgFlags::MORE`](hyperlight_common::virtq::msg::MsgFlags::MORE)
193202
/// is set, more descriptors follow.
203+
///
204+
/// Increments the reply guard counter so that subsequent G2H sends
205+
/// reserve capacity for the response.
194206
pub fn recv_h2g_call(&mut self) -> Result<FunctionCall> {
195207
let Some(first) = self.h2g_producer.poll()? else {
196208
bail!("H2G: no pending call");
@@ -209,6 +221,9 @@ impl GuestContext {
209221

210222
let chunk_len = hdr.payload_len as usize;
211223

224+
// Track that we owe a response on G2H.
225+
self.pending_replies = self.pending_replies.saturating_add(1);
226+
212227
if !hdr.has_more() {
213228
// Single-descriptor fast path
214229
let payload = &data[VirtqMsgHeader::SIZE..VirtqMsgHeader::SIZE + chunk_len];
@@ -250,8 +265,11 @@ impl GuestContext {
250265

251266
/// Send the result of a host-to-guest call back to the host via the
252267
/// G2H queue, then refill H2G descriptor slots until the ring is full.
268+
///
269+
/// Decrements the reply guard counter after a successful send.
253270
pub fn send_h2g_result(&mut self, payload: &[u8]) -> Result<()> {
254271
self.send_g2h_oneshot(MsgKind::Response, payload)?;
272+
self.pending_replies = self.pending_replies.saturating_sub(1);
255273
self.prefill_h2g()
256274
}
257275

@@ -343,16 +361,42 @@ impl GuestContext {
343361
}
344362
}
345363

364+
/// Ensure the G2H ring has enough free descriptors to accommodate
365+
/// both the requested send (`need_descs`) and all pending replies.
366+
fn ensure_reply_capacity(&mut self, need_descs: usize) -> Result<()> {
367+
let reserved = self.pending_replies as usize;
368+
loop {
369+
let free = self.g2h_producer.num_free();
370+
if free >= need_descs + reserved {
371+
return Ok(());
372+
}
373+
374+
self.g2h_producer.notify_backpressure();
375+
let reclaimed = self.g2h_producer.reclaim()?;
376+
if reclaimed == 0 {
377+
// No progress - host hasn't completed any entries yet.
378+
// Fall through and let the send path handle backpressure
379+
// via its own retry logic.
380+
return Ok(());
381+
}
382+
}
383+
}
384+
346385
/// Send a one-way message on the G2H queue ReadOnly and no completion.
347386
///
348-
/// If the pool or ring is full, triggers backpressure, VM exit so
349-
/// the host can drain, then retries once.
387+
/// For non-response sends, the reply guard is checked first to
388+
/// ensure enough G2H capacity is reserved for pending replies.
350389
fn send_g2h_oneshot(&mut self, kind: MsgKind, payload: &[u8]) -> Result<()> {
351390
let reqid = REQUEST_ID.fetch_add(1, Relaxed);
352391
let hdr = VirtqMsgHeader::new(kind, reqid, payload.len() as u32);
353392
let hdr_bytes = bytemuck::bytes_of(&hdr);
354393
let entry_len = VirtqMsgHeader::SIZE + payload.len();
355394

395+
// Reply guard: non-response sends must leave room for pending replies.
396+
if kind != MsgKind::Response {
397+
self.ensure_reply_capacity(1)?;
398+
}
399+
356400
// First attempt
357401
match self.try_send_readonly(hdr_bytes, payload, entry_len) {
358402
Ok(_) => return Ok(()),

0 commit comments

Comments
 (0)