diff --git a/docs/virtio-host-guest-communication.md b/docs/virtio-host-guest-communication.md index 8b94c8896..1b4b6a2e6 100644 --- a/docs/virtio-host-guest-communication.md +++ b/docs/virtio-host-guest-communication.md @@ -382,7 +382,7 @@ external `ByteChunks`: * `VecBytes` values copy into a contiguous `Vec`. * Multiple `Bytes` clones or slices backed by one owner keep one slot live. * The final owner unmaps its alias and returns the virtual range for reuse. - Its lease releases the scratch slot only in the slot's current generation. + Its lease releases the slot in its original pool. Producer reset releases allocations still owned by queue bookkeeping. After both producers reset and before H2G prefill, every live pool slot belongs to @@ -394,13 +394,15 @@ handles. The host resets both consumers before processing more queue traffic. ### Retained virtual addresses Both pools share one guest-global alias allocator. Its state is captured with -the mappings and keeps retained ranges reserved across pool generations. +the mappings and keeps retained ranges reserved across pool swaps. Each completed `GuestMapping` owns a separate page-aligned range. Mapping occurs when the guest receives the buffer. Capture, restore, and cloning preserve its pointer and contents. Retained aliases reach captured memory after restore. The first transport -entry advances the pool generations and recycles slots outside posted chains. +entry swaps allocators that hold retained slots, preserving posted reservations. +Retained leases keep their original pools, so their release cannot free slots +in the new pool. Retained values keep their aliases until their final owner drops. Reusing freed virtual ranges bounds page-table growth by the alias high-water mark. @@ -497,7 +499,7 @@ leases. Posted chains keep their reservations, preserving the submitted request. Descriptors and cursors stay unchanged. Retained aliases keep their captured mappings without payload copying. -Source continuation keeps the pool generation and live scratch leases intact. +Source continuation keeps the pool instances and live scratch leases intact. Checkpointing alone does not detach retained payloads from scratch. Result completion prefills free H2G slots. This requires no preparatory guest diff --git a/src/hyperlight_common/src/virtq/buffer.rs b/src/hyperlight_common/src/virtq/buffer.rs index cecdbd82f..cdd08bacc 100644 --- a/src/hyperlight_common/src/virtq/buffer.rs +++ b/src/hyperlight_common/src/virtq/buffer.rs @@ -245,12 +245,10 @@ impl Buf for SegmentsBuf<'_> { } } -/// A buffer allocation returned on drop while its pool generation is current. +/// A buffer allocation returned to its original pool on drop. pub struct BufferLease { /// The pool that allocated the buffer. pool: SlotPool, - /// The pool generation at the time of allocation. - generation: u64, /// The buffer's start address and full allocation capacity. allocation: Allocation, } @@ -258,13 +256,7 @@ pub struct BufferLease { impl BufferLease { /// Create a new buffer lease from a pool and allocation. pub fn new(pool: SlotPool, allocation: Allocation) -> Self { - let generation = pool.generation(); - - Self { - pool, - allocation, - generation, - } + Self { pool, allocation } } /// The buffer's start address and full allocation capacity. @@ -275,10 +267,6 @@ impl BufferLease { impl Drop for BufferLease { fn drop(&mut self) { - if self.generation != self.pool.generation() { - return; - } - if let Err(error) = self.pool.dealloc(self.allocation.addr) { log::error!("Failed to release a virtqueue buffer: {error}"); debug_assert!(false, "BufferLease deallocation failed: {error}"); @@ -315,38 +303,42 @@ mod tests { } #[test] - fn retired_lease_does_not_release_a_reused_slot() { - let pool = SlotPool::new(SlotLayout::new(0x1000, 4096, 1).unwrap()).unwrap(); - let allocation = pool.alloc(4096).unwrap(); - let retained = BufferLease::new(pool.clone(), allocation); - - // SAFETY: This test has no views or queue reservations. - unsafe { pool.retire_generation(core::iter::empty()) }.unwrap(); - let reused = pool.alloc(4096).unwrap(); - assert_eq!(reused.addr, allocation.addr); + fn old_leases_do_not_release_a_reused_slot() { + let mut pool = SlotPool::new(SlotLayout::new(0x1000, 4096, 1).unwrap()).unwrap(); + let mut allocation = pool.alloc(4096).unwrap(); + let mut retained = Vec::new(); + + for _ in 0..3 { + retained.push(BufferLease::new(pool.clone(), allocation)); + pool = pool.swap(core::iter::empty()).unwrap(); + let reused = pool.alloc(4096).unwrap(); + assert_eq!(reused.addr, allocation.addr); + allocation = reused; + } drop(retained); assert_eq!(pool.num_free(), 0); - drop(BufferLease::new(pool.clone(), reused)); + drop(BufferLease::new(pool.clone(), allocation)); assert_eq!(pool.num_free(), 1); } #[test] - fn generation_recovery_preserves_queue_reservations() { + fn swap_preserves_queue_reservations() { let pool = SlotPool::new(SlotLayout::new(0x1000, 4096, 2).unwrap()).unwrap(); let reserved = pool.alloc(4096).unwrap(); let captured = pool.alloc(4096).unwrap(); let retained = BufferLease::new(pool.clone(), captured); - // SAFETY: No view exists. The iterator names the only queue reservation. - unsafe { pool.retire_generation(core::iter::once(reserved.addr)) }.unwrap(); - assert_eq!(pool.num_free(), 1); - assert_eq!(pool.alloc(4096).unwrap().addr, captured.addr); + let swapped = pool.swap(core::iter::once(reserved.addr)).unwrap(); + assert_eq!(swapped.num_free(), 1); + assert_eq!(swapped.alloc(4096).unwrap().addr, captured.addr); drop(retained); - assert_eq!(pool.num_free(), 0); + assert_eq!(swapped.num_free(), 0); + swapped.dealloc(reserved.addr).unwrap(); + swapped.dealloc(captured.addr).unwrap(); + assert_eq!(swapped.num_free(), 2); pool.dealloc(reserved.addr).unwrap(); - pool.dealloc(captured.addr).unwrap(); assert_eq!(pool.num_free(), 2); } diff --git a/src/hyperlight_common/src/virtq/pool/slot.rs b/src/hyperlight_common/src/virtq/pool/slot.rs index f6d404b1c..9283439f2 100644 --- a/src/hyperlight_common/src/virtq/pool/slot.rs +++ b/src/hyperlight_common/src/virtq/pool/slot.rs @@ -104,7 +104,7 @@ struct Tier { /// Number of slots in this tier. count: usize, /// Free slot addresses, popped/pushed LIFO. - free: SmallVec<[u64; 64]>, + free: SmallVec<[u64; 16]>, /// One bit per slot index; set means the slot is currently handed out. allocated: FixedBitSet, } @@ -115,10 +115,6 @@ impl Tier { free.try_reserve_exact(layout.slot_count) .map_err(|_| AllocError::Bookkeeping)?; - for i in 0..layout.slot_count { - free.push(layout.base_addr + (i * layout.slot_size as usize) as u64); - } - Ok(Self { base_addr: layout.base_addr, slot_size: layout.slot_size, @@ -186,7 +182,7 @@ impl Tier { Ok(()) } - /// Keep one queue-owned slot unavailable during generation recovery. + /// Keep one queue-owned slot unavailable for a pool swap. fn reserve_addr(&mut self, addr: u64) -> Result<(), AllocError> { let slot = self.slot_of(addr)?; @@ -198,7 +194,7 @@ impl Tier { Ok(()) } - /// Rebuild the free list from the recovered ownership bitmap. + /// Populate the free list from the allocation bitmap. fn rebuild_free(&mut self) { self.free.clear(); @@ -241,7 +237,6 @@ impl Tier { struct Inner { lower: Option, upper: Tier, - generation: u64, } impl Inner { @@ -250,7 +245,6 @@ impl Inner { return Ok(Self { lower: None, upper: Tier::from_layout(upper)?, - generation: 0, }); }; @@ -273,14 +267,12 @@ impl Inner { return Ok(Self { lower: None, upper: Tier::from_layout(layout)?, - generation: 0, }); } Ok(Self { lower: Some(Tier::from_layout(lower)?), upper: Tier::from_layout(upper)?, - generation: 0, }) } @@ -311,47 +303,6 @@ impl Inner { self.upper.dealloc_addr(addr) } - /// Start a new generation and rebuild allocation state from `reserved`. - fn retire_generation( - &mut self, - reserved: impl Iterator + Clone, - ) -> Result<(), AllocError> { - let generation = self.generation.checked_add(1).ok_or(AllocError::Overflow)?; - - #[cfg(debug_assertions)] - for addr in reserved.clone() { - debug_assert!( - self.allocation_len(addr).is_ok(), - "address {addr:#x} is not a live pool slot" - ); - } - - self.generation = generation; - self.upper.allocated.clear(); - - if let Some(lower) = &mut self.lower { - lower.allocated.clear(); - } - - for addr in reserved { - if let Some(lower) = &mut self.lower - && lower.contains(addr) - { - lower.reserve_addr(addr)?; - } else { - self.upper.reserve_addr(addr)?; - } - } - - self.upper.rebuild_free(); - - if let Some(lower) = &mut self.lower { - lower.rebuild_free(); - } - - Ok(()) - } - fn allocation_len(&self, addr: u64) -> Result { if let Some(lower) = &self.lower && lower.contains(addr) @@ -419,23 +370,32 @@ pub struct SlotPool { } impl SlotPool { - /// Generation owning the current scratch allocations. - pub(crate) fn generation(&self) -> u64 { - self.inner.borrow().generation + /// Whether both handles share allocation state. + pub(crate) fn ptr_eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.inner, &other.inner) } - /// Retire existing leases and rebuild the pool around the supplied reservations. - /// - /// # Safety + /// Create allocation state for a pool swap with the supplied live slots reserved. /// - /// Retired views must already use backing independent of these scratch slots. - /// `reserved` must name every other live allocation exactly once. - /// No allocation or peer access may race this call. - pub(crate) unsafe fn retire_generation( - &self, - reserved: impl Iterator + Clone, - ) -> Result<(), AllocError> { - self.inner.borrow_mut().retire_generation(reserved) + /// Invalid or duplicate reservations leave this pool unchanged. + pub(crate) fn swap(&self, reserved: impl Iterator) -> Result { + let current = self.inner.borrow(); + let (lower, upper) = current.layouts(); + let mut inner = Inner::new(lower, upper)?; + + for addr in reserved { + if let Some(lower) = &mut inner.lower + && lower.contains(addr) + { + lower.allocation_len(addr)?; + lower.reserve_addr(addr)?; + } else { + inner.upper.allocation_len(addr)?; + inner.upper.reserve_addr(addr)?; + } + } + + Ok(Self::from_inner(inner)) } /// Create a single-tier recycling pool from exact slot placement. @@ -453,10 +413,18 @@ impl SlotPool { } fn from_layouts(lower: Option, upper: SlotLayout) -> Result { - let inner = Inner::new(lower, upper)?; - Ok(Self { + Ok(Self::from_inner(Inner::new(lower, upper)?)) + } + + fn from_inner(mut inner: Inner) -> Self { + inner.upper.rebuild_free(); + if let Some(lower) = &mut inner.lower { + lower.rebuild_free(); + } + + Self { inner: Rc::new(RefCell::new(inner)), - }) + } } /// Return every live slot address in deterministic tier and index order. diff --git a/src/hyperlight_common/src/virtq/pool/tests.rs b/src/hyperlight_common/src/virtq/pool/tests.rs index 2dba194e1..9138e3422 100644 --- a/src/hyperlight_common/src/virtq/pool/tests.rs +++ b/src/hyperlight_common/src/virtq/pool/tests.rs @@ -52,6 +52,88 @@ fn test_tiered_slot_pool_reports_layouts() { assert_eq!(pool.slot_addr(4), None); } +#[test] +fn swap_preserves_tier_layouts_and_reservations() { + for (lower_count, upper_count) in [(2, 2), (16, 11), (17, 17), (64, 65)] { + let pool = make_tiered_slot_pool(lower_count, upper_count); + let lower = pool.alloc(256).unwrap(); + let upper = pool.alloc(4096).unwrap(); + let retained = pool.alloc(256).unwrap(); + let original_live = pool.live_addrs(); + + let swapped = pool.swap([lower.addr, upper.addr].into_iter()).unwrap(); + + assert!(!swapped.ptr_eq(&pool)); + assert_eq!(swapped.layouts(), pool.layouts()); + assert_eq!(pool.live_addrs(), original_live); + assert_eq!(swapped.live_addrs(), vec![lower.addr, upper.addr]); + assert_eq!(swapped.num_free_lower(), lower_count - 1); + assert_eq!(swapped.num_free_upper(), upper_count - 1); + assert_eq!(swapped.alloc(256).unwrap().addr, retained.addr); + let free_upper = swapped.alloc(4096).unwrap(); + assert_ne!(free_upper.addr, upper.addr); + assert_eq!(swapped.num_free(), lower_count + upper_count - 4); + assert_eq!(pool.live_addrs(), original_live); + } +} + +#[test] +fn swap_preserves_free_order_across_inline_storage_boundary() { + for count in [1, 8, 11, 16, 17, 32, 64, 65] { + let pool = make_slot_pool(count, 256); + assert_eq!(pool.num_free(), count); + let allocations: Vec<_> = (0..count).map(|_| pool.alloc(256).unwrap()).collect(); + let reserved = allocations[0].addr; + let swapped = pool.swap(core::iter::once(reserved)).unwrap(); + assert_eq!(swapped.num_free(), count - 1); + + for allocation in &allocations[1..] { + assert_eq!(swapped.alloc(256).unwrap().addr, allocation.addr); + } + assert!(matches!(swapped.alloc(256), Err(AllocError::NoSpace))); + + for allocation in &allocations { + pool.dealloc(allocation.addr).unwrap(); + } + assert_eq!(pool.num_free(), count); + assert_eq!(swapped.num_free(), 0); + + swapped.dealloc(reserved).unwrap(); + assert_eq!(swapped.alloc(256).unwrap().addr, reserved); + for allocation in &allocations { + swapped.dealloc(allocation.addr).unwrap(); + } + assert_eq!(swapped.num_free(), count); + } +} + +#[test] +fn swap_rejects_invalid_reservations_without_mutation() { + let pool = make_slot_pool(2, 64); + let allocation = pool.alloc(64).unwrap(); + let original_live = pool.live_addrs(); + + assert!(matches!( + pool.swap([allocation.addr, allocation.addr].into_iter()), + Err(AllocError::InvalidArg) + )); + for addr in [ + 0, + pool.base_addr(), + allocation.addr + 1, + allocation.addr + 64, + ] { + assert!(matches!( + pool.swap(core::iter::once(addr)), + Err(AllocError::InvalidFree(_, _)) + )); + assert_eq!(pool.live_addrs(), original_live); + assert_eq!(pool.num_free(), 1); + } + pool.dealloc(allocation.addr).unwrap(); + assert_eq!(pool.num_free(), 2); +} + #[test] fn test_tiered_slot_pool_combines_contiguous_equal_sized_layouts() { let lower = SlotLayout::new(0x80000, 0x100, 2).unwrap(); diff --git a/src/hyperlight_common/src/virtq/producer.rs b/src/hyperlight_common/src/virtq/producer.rs index 403178811..cfb18b742 100644 --- a/src/hyperlight_common/src/virtq/producer.rs +++ b/src/hyperlight_common/src/virtq/producer.rs @@ -240,27 +240,43 @@ where self.inner.mem() } - /// Reclaim retained scratch slots while preserving in-flight chains. + /// Reclaim scratch slots held by retained views without changing in-flight chains. + /// + /// Retained views keep their original pools. Buffered completions or chains + /// from another allocator return [`VirtqError::InvalidState`]. /// /// # Safety /// - /// Every live allocation outside this producer's in-flight chains must - /// belong to a returned view with backing independent of reused scratch. + /// Allocations held outside this producer must have backing independent of + /// reused scratch. After a swap, old pool handles must not allocate again. /// No unsent chains or builders may remain. Peer accesses must be stopped. pub unsafe fn reclaim_slots(&mut self) -> Result<(), VirtqError> { + let live = &mut self.inflight.live; + // All pending used chains must be drained before we can swap pools if !self.pending.is_empty() { return Err(VirtqError::InvalidState); } - let reserved = self - .inflight - .live + // inflight chains must have the same pool as the producer before we swap + if live.iter().any(|inf| !self.pool.ptr_eq(&inf.chain.pool)) { + return Err(VirtqError::InvalidState); + } + + let reserved = live .iter() - .flat_map(|inflight| inflight.chain.buffers.iter().map(|buffer| buffer.addr)); + .flat_map(|inf| inf.chain.buffers.iter().map(|buf| buf.addr)); + + // No retained slots need reclaiming when all live slots are in-flight. + if self.pool.num_live() == reserved.clone().count() { + return Ok(()); + } + + let pool = self.pool.swap(reserved)?; + for inf in live { + inf.chain.pool = pool.clone(); + } - // SAFETY: In-flight records preserve all non-retired allocations. - // The caller guarantees independent backing and excludes untracked chains. - unsafe { self.pool.retire_generation(reserved)? }; + self.pool = pool; Ok(()) } @@ -1070,7 +1086,7 @@ struct BufferEntry { /// Buffer ownership passed from [`SendChain`] to [`Inflight`] at publication. /// -/// Keeps the original memory backend and pool for reply mappings and slot release. +/// Keeps the original memory backend and the pool owning its slot reservations. struct OwnedChain { /// Original backend for writing payloads and mapping completed replies. mem: M, @@ -1584,6 +1600,13 @@ mod tests { producer.submit(chain).unwrap(); + // SAFETY: No views exist and the consumer has not accessed the ring. + assert!(matches!( + unsafe { producer.reclaim_slots() }, + Err(VirtqError::InvalidState) + )); + assert!(producer.pool().ptr_eq(&replacement)); + assert!(orig_gen.upgrade().is_some()); let (recv, reply) = poll_received(&mut consumer); @@ -1809,14 +1832,114 @@ mod tests { assert!(producer.poll().unwrap().is_none()); } + #[test] + fn pool_swap_skips_pools_without_retained_allocations() { + let ring = make_ring(8); + let (mut producer, _consumer) = make_virtq_pair(&ring, 64); + let pool = producer.pool().clone(); + + // SAFETY: No allocations or peer accesses exist. + unsafe { producer.reclaim_slots() }.unwrap(); + assert!(producer.pool().ptr_eq(&pool)); + + let chain = producer.chain().writable(64).build().unwrap(); + producer.submit(chain).unwrap(); + // SAFETY: Only the posted chain owns an allocation. The peer stays stopped. + unsafe { producer.reclaim_slots() }.unwrap(); + assert!(producer.pool().ptr_eq(&pool)); + + // SAFETY: The consumer has not accessed the ring and stays inactive. + unsafe { producer.reset() }.unwrap(); + assert_eq!(pool.num_live(), 0); + } + + #[test] + fn pool_swap_preserves_posted_requests_and_ring_state() { + let ring = make_ring(8); + let mem = ring.mem(); + let (mut producer, mut consumer) = make_virtq_pair(&ring, 64); + + let original = producer.pool().clone(); + let allocation = original.alloc(64).unwrap(); + let mut retained = vec![BufferLease::new(original.clone(), allocation)]; + + let mut tokens = Vec::new(); + for _ in 0..2 { + let chain = producer.chain().writable(64).build().unwrap(); + tokens.push(producer.submit(chain).unwrap()); + } + + let (recv, reply) = poll_received(&mut consumer); + let ReplyChain::Writable(mut reply) = reply else { + panic!("expected writable reply"); + }; + reply.write_all(b"first").unwrap(); + consumer.complete(recv, reply).unwrap(); + + let mut ring_before = vec![0; Layout::query_size(ring.len())]; + mem.read(mem.base_addr(), &mut ring_before).unwrap(); + + for round in 0..2 { + let previous = producer.pool().clone(); + if round != 0 { + let allocation = previous.alloc(64).unwrap(); + retained.push(BufferLease::new(previous.clone(), allocation)); + } + // SAFETY: The retained lease has no views. No peer handles remain, + // and the consumer stays stopped during the swap. + unsafe { producer.reclaim_slots() }.unwrap(); + assert!(!producer.pool().ptr_eq(&previous)); + + let mut ring_after = vec![0; ring_before.len()]; + mem.read(mem.base_addr(), &mut ring_after).unwrap(); + assert_eq!(ring_after, ring_before); + } + + let pool = producer.pool().clone(); + let reused = pool.alloc(64).unwrap(); + assert_eq!(reused.addr, allocation.addr); + + drop(retained); + assert_eq!(pool.num_live(), 3); + + let first = producer.poll().unwrap().unwrap(); + assert_eq!(first.token(), tokens[0]); + + let first = first.into_bytes().unwrap(); + assert_eq!(first.as_ref(), b"first"); + + drop(first); + assert_eq!(pool.num_live(), 2); + + let (recv, reply) = poll_received(&mut consumer); + let ReplyChain::Writable(mut reply) = reply else { + panic!("expected writable reply"); + }; + + reply.write_all(b"second").unwrap(); + consumer.complete(recv, reply).unwrap(); + + let second = producer.poll().unwrap().unwrap(); + assert_eq!(second.token(), tokens[1]); + + let second = second.into_bytes().unwrap(); + assert_eq!(second.as_ref(), b"second"); + + drop(second); + pool.dealloc(reused.addr).unwrap(); + assert_eq!(pool.num_live(), 0); + } + #[test] fn reset_reclaims_inflight_slots_and_reuses_ring() { let ring = make_ring(8); let mem = ring.mem(); + let pool_base = mem.base_addr() + Layout::query_size(ring.len()) as u64 + 0x100; let pool_layout = SlotLayout::new(pool_base, 64, ring.len()).unwrap(); let pool = SlotPool::new(pool_layout).unwrap(); let notifier = TestNotifier::new(); + let mut producer = VirtqProducer::new(ring.layout(), mem, notifier, pool.clone()); for _ in 0..ring.len() { @@ -1833,13 +1956,6 @@ mod tests { assert_eq!(producer.num_free(), ring.len()); assert_eq!(pool.num_free(), ring.len()); assert!(producer.inflight.live.is_empty()); - assert!( - producer - .inflight - .by_id - .iter() - .all(|slot| *slot == InflightTable::::VACANT) - ); for _ in 0..ring.len() { let chain = producer.chain().writable(64).build().unwrap(); @@ -1888,7 +2004,7 @@ mod tests { } #[test] - fn reset_rejects_buffered_writable_completion() { + fn reset_and_pool_swap_reject_buffered_writable_completion() { let ring = make_ring(8); let (mut producer, mut consumer, _notifier) = make_test_producer(&ring); let chain = producer.chain().writable(64).build().unwrap(); @@ -1903,6 +2019,15 @@ mod tests { consumer.complete(recv, reply).unwrap(); producer.reclaim().unwrap(); + let pool = producer.pool().clone(); + // SAFETY: No allocations are held outside the producer. The consumer + // completed its handles and stays inactive. + assert!(matches!( + unsafe { producer.reclaim_slots() }, + Err(VirtqError::InvalidState) + )); + assert!(producer.pool().ptr_eq(&pool)); + // SAFETY: The consumer completed its handles and stays inactive. assert!(matches!( unsafe { producer.reset() }, diff --git a/src/hyperlight_guest/src/transport/backing.rs b/src/hyperlight_guest/src/transport/backing.rs index a6c7d5d4b..fc47d05e0 100644 --- a/src/hyperlight_guest/src/transport/backing.rs +++ b/src/hyperlight_guest/src/transport/backing.rs @@ -15,7 +15,7 @@ use itertools::Itertools; use super::SyncWrap; use crate::paging; -/// Reservations are captured with their aliases and survive pool generations. +/// Reservations are captured with their aliases and survive pool swaps. static ALLOCATOR: SyncWrap> = SyncWrap(RefCell::new(AliasAllocator::new())); // Permissions for transport alias mappings. const PERMISSIONS: BasicMapping = BasicMapping { diff --git a/src/hyperlight_guest/src/transport/context.rs b/src/hyperlight_guest/src/transport/context.rs index f9219d2d7..7794739cd 100644 --- a/src/hyperlight_guest/src/transport/context.rs +++ b/src/hyperlight_guest/src/transport/context.rs @@ -101,7 +101,7 @@ pub struct GuestContext { g2h_producer: G2hProducer, /// Host-to-guest driver. h2g_producer: H2gProducer, - /// Snapshot lineage whose scratch slots belong to the current pool generation. + /// Snapshot lineage whose scratch slots belong to the current pools. generation: u64, /// Size of each prefilled H2G buffer. h2g_slot_size: usize, @@ -383,9 +383,8 @@ impl GuestContext { return; } - // SAFETY: A changed generation denotes restore with independent captured - // backing. Canonical checkpoints leave only posted chains as reservations. - // Guest entry excludes peer access. + // SAFETY: Restore gives retained views independent captured backing. + // Only posted chains access scratch. Guest entry excludes peer access. unsafe { self.g2h_producer .reclaim_slots() diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index fea250fc6..dd98517a0 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -1000,7 +1000,7 @@ fn retained_host_reply_bytes_survive_restore() { /// Captured aliases survive scratch reuse and further snapshot generations. #[test] -fn retained_byte_chunks_survive_pool_generation_reuse() { +fn retained_byte_chunks_survive_pool_swap() { let mut sandbox = SandboxBuilder::from_file(hyperlight_testing::simple_guest_as_pathbuf()) .guest_log_level(tracing_core::LevelFilter::OFF) .host_function("HostEchoByteChunks", |value: Vec| value)