@@ -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