Skip to main content

hyperlight_host/mem/
shared_mem.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2025 The Hyperlight Authors.
3
4use std::any::type_name;
5use std::ffi::c_void;
6use std::io::Error;
7use std::mem::{align_of, size_of};
8#[cfg(unix)]
9use std::ptr::null_mut;
10use std::sync::{Arc, RwLock};
11
12use bytemuck::Pod;
13use thiserror::Error;
14use tracing::{Span, instrument};
15#[cfg(target_os = "windows")]
16use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
17#[cfg(target_os = "windows")]
18use windows::Win32::System::Memory::PAGE_READWRITE;
19#[cfg(target_os = "windows")]
20use windows::Win32::System::Memory::{
21    CreateFileMappingA, FILE_MAP_ALL_ACCESS, MEM_PRESERVE_PLACEHOLDER, MEM_RELEASE,
22    MEM_REPLACE_PLACEHOLDER, MEM_RESERVE, MEM_RESERVE_PLACEHOLDER, MEMORY_MAPPED_VIEW_ADDRESS,
23    MapViewOfFile, MapViewOfFile3, PAGE_NOACCESS, PAGE_PROTECTION_FLAGS, PAGE_READONLY,
24    UnmapViewOfFile, VIRTUAL_ALLOCATION_TYPE, VIRTUAL_FREE_TYPE, VirtualAlloc2, VirtualFree,
25    VirtualProtect,
26};
27#[cfg(target_os = "windows")]
28use windows::core::PCSTR;
29
30use super::memory_region::{
31    HostGuestMemoryRegion, MemoryRegion, MemoryRegionFlags, MemoryRegionKind, MemoryRegionType,
32};
33use crate::log_then_return;
34
35type Result<T> = core::result::Result<T, SharedMemoryError>;
36
37/// Whether a [`StackError`] was encountered whilst pushing or popping
38/// from the guest stack
39#[derive(Debug)]
40pub enum StackOp {
41    /// The error was encountered while pushing to the guest stack
42    Push,
43    /// The error was encountered while popping from the guest stack
44    Pop,
45}
46/// An error related to the stack discipline of guest I/O
47#[derive(Error, Debug)]
48pub enum StackError {
49    /// The stack pointer for a stack entry was out-of-bounds for the
50    /// stack
51    #[error(
52        "Unable to {0:?} data from buffer: Stack pointer is out of bounds. Stack pointer: {1}, Buffer size: {2}"
53    )]
54    SpOob(StackOp, usize, usize),
55
56    /// The back pointer for a stack entry was corrupt
57    #[error("Corrupt buffer back-pointer: element offset {0} is outside valid range [8, {1}].")]
58    CorruptBackPointer(usize, usize),
59
60    /// A stack entry size prefix was too large for necessary
61    /// operations on it to remain in the range of a u32
62    #[error("Corrupt buffer size prefix: value {0} overflows when adding 4-byte header.")]
63    OverflowingPrefix(u32),
64
65    /// It was not possible to convert a stack entry size prefix into
66    /// a usize. This should be impossible on all currently supported
67    /// architectures, since usize is 64 bits on all of them.
68    #[error("Prefix too large: {0}")]
69    PrefixTooLarge(std::num::TryFromIntError),
70
71    /// A stack entry size prefix is larger than its
72    /// logically-enclosing element
73    #[error(
74        "Corrupt buffer size prefix: flatbuffer claims {0} bytes but the element slot is only {1} bytes."
75    )]
76    CorruptPrefix(usize, usize),
77
78    /// An error was encountered during a routine error conversion
79    /// that should have been infallible
80    #[error("pop_buffer_into: failed to convert buffer to {0}")]
81    ConvertError(String),
82
83    /// There was not enough free space available on the stack for an
84    /// element to be pushed
85    #[error("Not enough space in buffer to push data. Required: {0}, Available: {1}")]
86    BufferFullError(usize, usize),
87}
88/// This is just an alias for std::backtrace::Backtrace that we
89/// introduce to stop thiserror from using its backtrace
90/// functionality, which depends on nightly APIs.
91type ThisErrorHackBacktrace = std::backtrace::Backtrace;
92
93/// An error encountered while setting up or manipulating a shared memory region
94#[derive(Error, Debug)]
95pub enum SharedMemoryError {
96    /// Some operation on the shared memory attempted to read or write
97    /// out of bounds
98    #[error("Cannot access a value with size {0} at offset {1} in memory of size {2}")]
99    Bounds(usize, usize, usize),
100
101    /// When creating a memory with contents from a file, metadata for
102    /// that file could not be read
103    #[error("Could not access metadata for file: {0}")]
104    FileMetadata(std::io::Error),
105
106    /// When creating a memory with contents from a file, that file
107    /// was logically larger than the range of a usize.
108    #[error("File size exceeded usize: {0}")]
109    FileTooLarge(std::num::TryFromIntError),
110
111    /// The locking discipline used to enforce temporary exclusive
112    /// access by the host to a shared memory failed in some way
113    #[error("Could not acquire memory lock: {0} at {1}")]
114    LockError(String, ThisErrorHackBacktrace),
115
116    /// A request to allocate a shared memory could not be fulfilled
117    /// by the host operating system
118    #[error("Memory Allocation Failed with OS Error {0:?}.")]
119    MemoryAllocationFailed(Option<i32>),
120
121    /// A ruqest to allocate a shared shared memory had an invalid
122    /// size, due either to bounds or alignment.
123    #[error(
124        "Memory request does not satisfy constraints: 0x{1:x} < 0x{0:x} <= 0x{2:x} && 0x{0:x} % 0x{3:x} = 0"
125    )]
126    MemoryRequest(usize, usize, usize, usize),
127
128    /// An request to mmap a file or create an anonymous memory could
129    /// not be fulfilled by the host operating system
130    #[error("mmap failed with os error {0:?}")]
131    MmapFailed(Option<i32>),
132
133    /// A request to change the host-side permissions on the guard
134    /// pages of a shared memory region could not be fulfilled by the
135    /// host operating system
136    #[error("mprotect failed with os error {0:?}")]
137    MprotectFailed(Option<i32>),
138
139    /// A Windows virtual memory API call failed
140    #[cfg(target_os = "windows")]
141    #[error("Windows API Error Result {0:?}")]
142    WindowsAPIError(#[from] windows_result::Error),
143
144    /// Calling code attempted to take exclusive (write) access to a
145    /// [`ReadonlySharedMemory`].
146    #[error("Cannot take exclusive access to a ReadonlySharedMemory")]
147    ReadonlySharedMemoryExclusiveRequest,
148
149    /// The stack discipline of guest I/O was violated in some way
150    #[error("{0}")]
151    Stack(#[from] StackError),
152
153    /// An error was encountered when trying to convert a slice of raw
154    /// bytes into some logical data
155    #[error("Error reading slice {0}")]
156    TryFromSlice(#[from] std::array::TryFromSliceError),
157
158    /// An error was encountered during a routine error conversion
159    /// that should have been infallible
160    #[error("Error reading int: {0}")]
161    TryFromInt(#[from] std::num::TryFromIntError),
162}
163impl<T> From<std::sync::TryLockError<T>> for SharedMemoryError {
164    fn from(e: std::sync::TryLockError<T>) -> SharedMemoryError {
165        SharedMemoryError::LockError(format!("{:?}", e), std::backtrace::Backtrace::capture())
166    }
167}
168
169/// Makes sure that the given `offset` and `size` are within the bounds of the memory with size `mem_size`.
170macro_rules! bounds_check {
171    ($offset:expr, $size:expr, $mem_size:expr) => {
172        if $offset.checked_add($size).is_none_or(|end| end > $mem_size) {
173            return Err(SharedMemoryError::Bounds($offset, $size, $mem_size));
174        }
175    };
176}
177
178/// generates a reader function for the given type
179macro_rules! generate_reader {
180    ($fname:ident, $ty:ty) => {
181        /// Read a value of type `$ty` from the memory at the given offset.
182        #[allow(dead_code)]
183        #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
184        pub(crate) fn $fname(&self, offset: usize) -> Result<$ty> {
185            let data = self.as_slice();
186            bounds_check!(offset, std::mem::size_of::<$ty>(), data.len());
187            Ok(<$ty>::from_le_bytes(
188                data[offset..offset + std::mem::size_of::<$ty>()].try_into()?,
189            ))
190        }
191    };
192}
193
194/// generates a writer function for the given type
195macro_rules! generate_writer {
196    ($fname:ident, $ty:ty) => {
197        /// Write a value of type `$ty` to the memory at the given offset.
198        #[allow(dead_code)]
199        pub(crate) fn $fname(&mut self, offset: usize, value: $ty) -> Result<()> {
200            let data = self.as_mut_slice();
201            bounds_check!(offset, std::mem::size_of::<$ty>(), data.len());
202            data[offset..offset + std::mem::size_of::<$ty>()].copy_from_slice(&value.to_le_bytes());
203            Ok(())
204        }
205    };
206}
207
208/// A representation of a host mapping of a shared memory region,
209/// which will be released when this structure is Drop'd. This is not
210/// individually Clone (since it holds ownership of the mapping), or
211/// Send or Sync, since it doesn't ensure any particular synchronization.
212#[derive(Debug)]
213pub struct HostMapping {
214    #[cfg(not(target_os = "windows"))]
215    mmap: Mmap,
216    #[cfg(target_os = "windows")]
217    mapping: WindowsMapping,
218}
219
220/// Windows-side flavors of a [`HostMapping`].
221#[cfg(target_os = "windows")]
222#[derive(Debug)]
223enum WindowsMapping {
224    /// `[guard][blob][guard]` carved from a single anonymous file
225    /// mapping created via `CreateFileMappingA(INVALID_HANDLE_VALUE)`
226    /// and mapped with `MapViewOfFile`.
227    ///
228    /// ```text
229    /// |<------------------ view ------------------>|
230    /// [   guard   ][         blob         ][   guard   ]
231    /// ```
232    Anonymous {
233        view: MappedView,
234        file_mapping: FileMapping,
235    },
236    /// File-backed: a `VirtualAlloc2` placeholder is split in three.
237    /// The middle slot is replaced by a `MapViewOfFile3` view of the
238    /// file. The flanking placeholder slots remain unmapped and act
239    /// as the guard pages.
240    ///
241    /// ```text
242    /// [ leading ][         view         ][ trailing ]
243    /// ```
244    FileBacked {
245        leading: Placeholder,
246        view: MappedView,
247        trailing: Placeholder,
248        file_mapping: FileMapping,
249    },
250}
251
252impl HostMapping {
253    /// Base address of the host mapping, including the surrounding guard pages.
254    pub(crate) fn ptr(&self) -> *mut u8 {
255        #[cfg(not(target_os = "windows"))]
256        {
257            self.mmap.base as *mut u8
258        }
259        #[cfg(target_os = "windows")]
260        match &self.mapping {
261            WindowsMapping::Anonymous { view, .. } => view.addr as *mut u8,
262            WindowsMapping::FileBacked { leading, .. } => leading.addr as *mut u8,
263        }
264    }
265
266    /// Total size of the host mapping, including the surrounding guard pages.
267    pub(crate) fn size(&self) -> usize {
268        #[cfg(not(target_os = "windows"))]
269        {
270            self.mmap.len
271        }
272        #[cfg(target_os = "windows")]
273        match &self.mapping {
274            WindowsMapping::Anonymous { view, .. } => view.len,
275            WindowsMapping::FileBacked {
276                leading,
277                view,
278                trailing,
279                ..
280            } => leading.size + view.len + trailing.size,
281        }
282    }
283
284    /// Win32 file-mapping handle backing this mapping.
285    #[cfg(target_os = "windows")]
286    pub(crate) fn file_mapping_handle(&self) -> HANDLE {
287        match &self.mapping {
288            WindowsMapping::Anonymous { file_mapping, .. }
289            | WindowsMapping::FileBacked { file_mapping, .. } => file_mapping.0,
290        }
291    }
292}
293
294/// RAII guard for an `mmap` reservation. Calls `munmap` on drop.
295#[cfg(unix)]
296#[derive(Debug)]
297struct Mmap {
298    base: *mut c_void,
299    len: usize,
300}
301
302#[cfg(unix)]
303impl Drop for Mmap {
304    fn drop(&mut self) {
305        // SAFETY: `self.base` and `self.len` are exactly what was
306        // returned by the `mmap` that produced this `Mmap`, and that
307        // mapping has not been unmapped (we own it).
308        unsafe {
309            if libc::munmap(self.base, self.len) != 0 {
310                tracing::error!(
311                    "Mmap::drop: munmap failed: {:?}",
312                    std::io::Error::last_os_error()
313                );
314            }
315        }
316    }
317}
318
319/// RAII guard for a Win32 mapped view. Calls `UnmapViewOfFile` on drop.
320#[cfg(target_os = "windows")]
321#[derive(Debug)]
322struct MappedView {
323    addr: *mut c_void,
324    len: usize,
325}
326
327#[cfg(target_os = "windows")]
328impl Drop for MappedView {
329    fn drop(&mut self) {
330        let view = MEMORY_MAPPED_VIEW_ADDRESS { Value: self.addr };
331        // Plain `UnmapViewOfFile` fully releases the address range.
332        // `UnmapViewOfFile2(MEM_PRESERVE_PLACEHOLDER)` would convert
333        // it back into a placeholder for remapping, which is not
334        // what we want: the surrounding guard `Placeholder`s release
335        // their slots independently on drop.
336        // SAFETY: `self.addr` is the base address returned by the
337        // `MapViewOfFile` call that produced this `MappedView`, and
338        // the view has not been unmapped (we own it).
339        if let Err(e) = unsafe { UnmapViewOfFile(view) } {
340            tracing::error!(
341                "MappedView::drop(addr={:?}, len={}) UnmapViewOfFile failed: {:?}",
342                self.addr,
343                self.len,
344                e
345            );
346        }
347    }
348}
349
350/// Owns a Win32 file-mapping `HANDLE`. Calls `CloseHandle` on drop.
351#[cfg(target_os = "windows")]
352#[derive(Debug)]
353struct FileMapping(HANDLE);
354
355#[cfg(target_os = "windows")]
356impl Drop for FileMapping {
357    fn drop(&mut self) {
358        // SAFETY: `self.0` is a valid HANDLE returned by
359        // `CreateFileMappingA` that has not been closed (we own it).
360        unsafe {
361            if let Err(e) = CloseHandle(self.0) {
362                tracing::error!(
363                    "FileMapping::drop(handle={:?}) CloseHandle failed: {:?}",
364                    self.0,
365                    e
366                );
367            }
368        }
369    }
370}
371
372/// RAII guard for a `VirtualAlloc2` placeholder reservation. Owns
373/// the `[addr, addr + size)` range until split into smaller
374/// placeholders, replaced by a mapped view, or dropped (which calls
375/// `VirtualFree(MEM_RELEASE)`).
376#[cfg(target_os = "windows")]
377#[derive(Debug)]
378pub(crate) struct Placeholder {
379    addr: *mut c_void,
380    size: usize,
381}
382
383#[cfg(target_os = "windows")]
384impl Placeholder {
385    fn reserve(size: usize) -> Result<Self> {
386        // SAFETY: `VirtualAlloc2` with `MEM_RESERVE |
387        // MEM_RESERVE_PLACEHOLDER` and `PAGE_NOACCESS` only reserves
388        // address space. No pages are committed and no access is
389        // granted, so the call has no preconditions and the returned
390        // reservation cannot be misused from safe code.
391        let addr = unsafe {
392            VirtualAlloc2(
393                None,
394                None,
395                size,
396                VIRTUAL_ALLOCATION_TYPE(MEM_RESERVE.0 | MEM_RESERVE_PLACEHOLDER.0),
397                PAGE_NOACCESS.0,
398                None,
399            )
400        };
401        if addr.is_null() {
402            log_then_return!(SharedMemoryError::MemoryAllocationFailed(
403                Error::last_os_error().raw_os_error()
404            ));
405        }
406        Ok(Placeholder { addr, size })
407    }
408
409    fn split_front(self, front_size: usize) -> Result<(Placeholder, Placeholder)> {
410        debug_assert!(front_size > 0 && front_size < self.size);
411        debug_assert!(front_size.is_multiple_of(page_size::get()));
412        // SAFETY: `self` owns the placeholder reservation at
413        // `[self.addr, self.addr + self.size)`. `MEM_RELEASE |
414        // MEM_PRESERVE_PLACEHOLDER` is the Win32 idiom for splitting
415        // a placeholder in two: no memory is released, the
416        // reservation is just carved at `front_size`.
417        if let Err(e) = unsafe {
418            VirtualFree(
419                self.addr,
420                front_size,
421                VIRTUAL_FREE_TYPE(MEM_RELEASE.0 | MEM_PRESERVE_PLACEHOLDER.0),
422            )
423        } {
424            // `self` drops here, releasing the unsplit reservation.
425            log_then_return!(SharedMemoryError::WindowsAPIError(e.clone()));
426        }
427        let addr = self.addr;
428        let total = self.size;
429        // Forget the parent so its `Drop` does not release the two
430        // child slots as one.
431        std::mem::forget(self);
432        let front = Placeholder {
433            addr,
434            size: front_size,
435        };
436        let back = Placeholder {
437            // SAFETY: `front_size < total`, so `addr + front_size`
438            // is in-bounds of the original reservation.
439            addr: unsafe { (addr as *mut u8).add(front_size) as *mut c_void },
440            size: total - front_size,
441        };
442        Ok((front, back))
443    }
444
445    fn split_into_three(
446        self,
447        front_size: usize,
448        middle_size: usize,
449    ) -> Result<(Placeholder, Placeholder, Placeholder)> {
450        let (front, rest) = self.split_front(front_size)?;
451        let (middle, back) = rest.split_front(middle_size)?;
452        Ok((front, middle, back))
453    }
454
455    fn map_file_view(self, file_mapping: HANDLE) -> Result<MappedView> {
456        // SAFETY: `self` owns the placeholder slot at
457        // `[self.addr, self.addr + self.size)`.
458        // `MEM_REPLACE_PLACEHOLDER` requires the target range to be
459        // an existing placeholder of exactly that size, which the
460        // type system guarantees here. On success, ownership of the
461        // range transfers to the returned `MappedView`; the caller
462        // releases the file mapping handle via `FileMapping`.
463        let mapped = unsafe {
464            MapViewOfFile3(
465                file_mapping,
466                None,
467                Some(self.addr),
468                0,
469                self.size,
470                MEM_REPLACE_PLACEHOLDER,
471                PAGE_READONLY.0,
472                None,
473            )
474        };
475        if mapped.Value.is_null() {
476            // `self` drops here, releasing the placeholder.
477            log_then_return!(SharedMemoryError::MemoryAllocationFailed(
478                Error::last_os_error().raw_os_error()
479            ));
480        }
481        let addr = self.addr;
482        let len = self.size;
483        std::mem::forget(self);
484        Ok(MappedView { addr, len })
485    }
486}
487
488#[cfg(target_os = "windows")]
489impl Drop for Placeholder {
490    fn drop(&mut self) {
491        // SAFETY: `self.addr` is the base of a placeholder
492        // reservation we own. `MEM_RELEASE` with size 0 releases the
493        // entire reservation.
494        if let Err(e) = unsafe { VirtualFree(self.addr, 0, VIRTUAL_FREE_TYPE(MEM_RELEASE.0)) } {
495            tracing::error!(
496                "Placeholder::drop(addr={:?}, size={}) VirtualFree failed: {:?}",
497                self.addr,
498                self.size,
499                e
500            );
501        }
502    }
503}
504
505/// A trait that abstracts over the particular kind of SharedMemory,
506/// used when invoking operations from Rust that absolutely must have
507/// exclusive control over the shared memory for correctness +
508/// performance, like snapshotting.
509pub trait SharedMemory {
510    /// Return a readonly reference to the host mapping backing this SharedMemory
511    fn region(&self) -> &HostMapping;
512
513    /// Return the base address of the host mapping of this
514    /// region. Following the general Rust philosophy, this does not
515    /// need to be marked as `unsafe` because doing anything with this
516    /// pointer itself requires `unsafe`.
517    fn base_addr(&self) -> usize {
518        self.region().ptr() as usize + page_size::get()
519    }
520
521    /// Return the base address of the host mapping of this region as
522    /// a pointer. Following the general Rust philosophy, this does
523    /// not need to be marked as `unsafe` because doing anything with
524    /// this pointer itself requires `unsafe`.
525    fn base_ptr(&self) -> *mut u8 {
526        self.region().ptr().wrapping_add(page_size::get())
527    }
528
529    /// Return the length of usable memory contained in `self`.
530    /// The returned size does not include the size of the surrounding
531    /// guard pages.
532    fn mem_size(&self) -> usize {
533        self.region().size() - 2 * page_size::get()
534    }
535
536    /// Return the raw base address of the host mapping, including the
537    /// guard pages.
538    fn raw_ptr(&self) -> *mut u8 {
539        self.region().ptr()
540    }
541
542    /// Return the raw size of the host mapping, including the guard
543    /// pages.
544    fn raw_mem_size(&self) -> usize {
545        self.region().size()
546    }
547
548    /// Extract a base address that can be mapped into a VM for this
549    /// SharedMemory.
550    ///
551    /// On Linux this returns a raw `usize` pointer. On Windows it
552    /// returns a [`HostRegionBase`](super::memory_region::HostRegionBase)
553    /// that carries the file-mapping handle metadata needed by WHP.
554    fn host_region_base(&self) -> <HostGuestMemoryRegion as MemoryRegionKind>::HostBaseType {
555        #[cfg(not(windows))]
556        {
557            self.base_addr()
558        }
559        #[cfg(windows)]
560        {
561            super::memory_region::HostRegionBase {
562                from_handle: self.region().file_mapping_handle().into(),
563                handle_base: self.region().ptr() as usize,
564                handle_size: self.region().size(),
565                offset: page_size::get(),
566            }
567        }
568    }
569
570    /// Return the end address of the host region (base + usable size).
571    fn host_region_end(&self) -> <HostGuestMemoryRegion as MemoryRegionKind>::HostBaseType {
572        <HostGuestMemoryRegion as MemoryRegionKind>::add(self.host_region_base(), self.mem_size())
573    }
574
575    /// Run some code with exclusive access to the SharedMemory
576    /// underlying this.  If the SharedMemory is not an
577    /// ExclusiveSharedMemory, any concurrent accesses to the relevant
578    /// HostSharedMemory/GuestSharedMemory may make this fail, or be
579    /// made to fail by this, and should be avoided.
580    fn with_exclusivity<T, F: FnOnce(&mut ExclusiveSharedMemory) -> T>(
581        &mut self,
582        f: F,
583    ) -> Result<T>;
584
585    /// Run some code that is allowed to access the contents of the
586    /// SharedMemory as if it is a normal slice.  By default, this is
587    /// implemented via [`SharedMemory::with_exclusivity`], which is
588    /// the correct implementation for a memory that can be mutated,
589    /// but a [`ReadonlySharedMemory`], can support this.
590    fn with_contents<T, F: FnOnce(&[u8]) -> T>(&mut self, f: F) -> Result<T> {
591        self.with_exclusivity(|m| f(m.as_slice()))
592    }
593}
594
595fn mapping_at(
596    s: &impl SharedMemory,
597    gpa: u64,
598    size: usize,
599    region_type: MemoryRegionType,
600    flags: MemoryRegionFlags,
601) -> MemoryRegion {
602    let guest_base = gpa as usize;
603
604    MemoryRegion {
605        guest_region: guest_base..(guest_base + size),
606        host_region: s.host_region_base()
607            ..<HostGuestMemoryRegion as MemoryRegionKind>::add(s.host_region_base(), size),
608        region_type,
609        flags,
610    }
611}
612
613/// These three structures represent various phases of the lifecycle of
614/// a memory buffer that is shared with the guest. An
615/// ExclusiveSharedMemory is used for certain operations that
616/// unrestrictedly write to the shared memory, including setting it up
617/// and taking snapshots.
618#[derive(Debug)]
619pub struct ExclusiveSharedMemory {
620    region: Arc<HostMapping>,
621}
622unsafe impl Send for ExclusiveSharedMemory {}
623
624impl ExclusiveSharedMemory {
625    /// Helper function used to abstract common checks from Windows
626    /// and Linux implementations of [`ExclusiveSharedMemory::new()`]
627    fn total_size(min_size_bytes: usize) -> Result<usize> {
628        if min_size_bytes > 0 &&
629            // guard page around the memory
630            let Some(total_size) = min_size_bytes.checked_add(2 * page_size::get()) &&
631            total_size % page_size::get() == 0 &&
632            // usize and isize are guaranteed to be the same size, and
633            // isize::MAX should be positive, so this cast should be
634            // safe.
635            total_size <= isize::MAX as usize
636        {
637            Ok(total_size)
638        } else {
639            Err(SharedMemoryError::MemoryRequest(
640                min_size_bytes,
641                2,
642                isize::MAX as usize - 2 * page_size::get(),
643                page_size::get(),
644            ))
645        }
646    }
647
648    /// Create a new region of shared memory with the given minimum
649    /// size in bytes. The region will be surrounded by guard pages.
650    ///
651    /// Return `Err` if shared memory could not be allocated.
652    #[cfg(unix)]
653    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
654    pub fn new(min_size_bytes: usize) -> Result<Self> {
655        use libc::{
656            MAP_ANONYMOUS, MAP_FAILED, MAP_PRIVATE, PROT_READ, PROT_WRITE, c_int, mmap, off_t,
657            size_t,
658        };
659
660        let total_size = Self::total_size(min_size_bytes)?;
661
662        #[cfg(not(miri))]
663        use libc::{MAP_NORESERVE, PROT_NONE, mprotect};
664
665        // allocate the memory
666        #[cfg(not(miri))]
667        let flags = MAP_ANONYMOUS | MAP_PRIVATE | MAP_NORESERVE;
668        #[cfg(miri)]
669        let flags = MAP_ANONYMOUS | MAP_PRIVATE;
670
671        let addr = unsafe {
672            mmap(
673                null_mut(),
674                total_size as size_t,
675                PROT_READ | PROT_WRITE,
676                flags,
677                -1 as c_int,
678                0 as off_t,
679            )
680        };
681        if addr == MAP_FAILED {
682            log_then_return!(SharedMemoryError::MmapFailed(
683                Error::last_os_error().raw_os_error()
684            ));
685        }
686        let mmap = Mmap {
687            base: addr,
688            len: total_size,
689        };
690
691        // protect the guard pages
692        #[cfg(not(miri))]
693        {
694            let res = unsafe { mprotect(mmap.base, page_size::get(), PROT_NONE) };
695            if res != 0 {
696                return Err(SharedMemoryError::MprotectFailed(
697                    Error::last_os_error().raw_os_error(),
698                ));
699            }
700            let res = unsafe {
701                mprotect(
702                    (mmap.base as *const u8).add(total_size - page_size::get()) as *mut c_void,
703                    page_size::get(),
704                    PROT_NONE,
705                )
706            };
707            if res != 0 {
708                return Err(SharedMemoryError::MprotectFailed(
709                    Error::last_os_error().raw_os_error(),
710                ));
711            }
712        }
713
714        Ok(Self {
715            // HostMapping is only non-Send/Sync because raw pointers
716            // are not ("as a lint", as the Rust docs say). We don't
717            // want to mark HostMapping Send/Sync immediately, because
718            // that could socially imply that it's "safe" to use
719            // unsafe accesses from multiple threads at once. Instead, we
720            // directly impl Send and Sync on this type. Since this
721            // type does have Send and Sync manually impl'd, the Arc
722            // is not pointless as the lint suggests.
723            #[allow(clippy::arc_with_non_send_sync)]
724            region: Arc::new(HostMapping { mmap }),
725        })
726    }
727
728    /// Create a new region of shared memory with the given minimum
729    /// size in bytes. The region will be surrounded by guard pages.
730    ///
731    /// Return `Err` if shared memory could not be allocated.
732    #[cfg(target_os = "windows")]
733    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
734    pub fn new(min_size_bytes: usize) -> Result<Self> {
735        let total_size = Self::total_size(min_size_bytes)?;
736
737        let mut dwmaximumsizehigh = 0;
738        let mut dwmaximumsizelow = 0;
739
740        if std::mem::size_of::<usize>() == 8 {
741            dwmaximumsizehigh = (total_size >> 32) as u32;
742            dwmaximumsizelow = (total_size & 0xFFFFFFFF) as u32;
743        }
744
745        // Allocate the memory use CreateFileMapping instead of VirtualAlloc
746        // This allows us to map the memory into the surrogate process using MapViewOfFile2
747
748        let flags = PAGE_READWRITE;
749
750        let handle = unsafe {
751            CreateFileMappingA(
752                INVALID_HANDLE_VALUE,
753                None,
754                flags,
755                dwmaximumsizehigh,
756                dwmaximumsizelow,
757                PCSTR::null(),
758            )?
759        };
760
761        if handle.is_invalid() {
762            log_then_return!(SharedMemoryError::MemoryAllocationFailed(
763                Error::last_os_error().raw_os_error()
764            ));
765        }
766        let file_mapping = FileMapping(handle);
767
768        let file_map = FILE_MAP_ALL_ACCESS;
769        let addr = unsafe { MapViewOfFile(file_mapping.0, file_map, 0, 0, 0) };
770
771        if addr.Value.is_null() {
772            log_then_return!(SharedMemoryError::MemoryAllocationFailed(
773                Error::last_os_error().raw_os_error()
774            ));
775        }
776        let view = MappedView {
777            addr: addr.Value,
778            len: total_size,
779        };
780
781        // Set the first and last pages to be guard pages
782
783        let mut unused_out_old_prot_flags = PAGE_PROTECTION_FLAGS(0);
784
785        // If the following calls to VirtualProtect are changed make sure to update the calls to VirtualProtectEx in surrogate_process_manager.rs
786
787        let first_guard_page_start = view.addr;
788        if let Err(e) = unsafe {
789            VirtualProtect(
790                first_guard_page_start,
791                page_size::get(),
792                PAGE_NOACCESS,
793                &mut unused_out_old_prot_flags,
794            )
795        } {
796            log_then_return!(SharedMemoryError::WindowsAPIError(e.clone()));
797        }
798
799        let last_guard_page_start = unsafe { view.addr.add(total_size - page_size::get()) };
800        if let Err(e) = unsafe {
801            VirtualProtect(
802                last_guard_page_start,
803                page_size::get(),
804                PAGE_NOACCESS,
805                &mut unused_out_old_prot_flags,
806            )
807        } {
808            log_then_return!(SharedMemoryError::WindowsAPIError(e.clone()));
809        }
810
811        Ok(Self {
812            // HostMapping is only non-Send/Sync because raw pointers
813            // are not ("as a lint", as the Rust docs say). We don't
814            // want to mark HostMapping Send/Sync immediately, because
815            // that could socially imply that it's "safe" to use
816            // unsafe accesses from multiple threads at once. Instead, we
817            // directly impl Send and Sync on this type. Since this
818            // type does have Send and Sync manually impl'd, the Arc
819            // is not pointless as the lint suggests.
820            #[allow(clippy::arc_with_non_send_sync)]
821            region: Arc::new(HostMapping {
822                mapping: WindowsMapping::Anonymous { view, file_mapping },
823            }),
824        })
825    }
826
827    /// Internal helper method to get the backing memory as a mutable slice.
828    ///
829    /// # Safety
830    /// As per std::slice::from_raw_parts_mut:
831    /// - self.base_addr() must be valid for both reads and writes for
832    ///   self.mem_size() * mem::size_of::<u8>() many bytes, and it
833    ///   must be properly aligned.
834    ///
835    ///   The rules on validity are still somewhat unspecified, but we
836    ///   assume that the result of our calls to mmap/CreateFileMappings may
837    ///   be considered a single "allocated object". The use of
838    ///   non-atomic accesses is alright from a Safe Rust standpoint,
839    ///   because SharedMemoryBuilder is  not Sync.
840    /// - self.base_addr() must point to self.mem_size() consecutive
841    ///   properly initialized values of type u8
842    ///
843    ///   Again, the exact provenance restrictions on what is
844    ///   considered to be initialized values are unclear, but we make
845    ///   sure to use mmap(MAP_ANONYMOUS) and
846    ///   CreateFileMapping(SEC_COMMIT), so the pages in question are
847    ///   zero-initialized, which we hope counts for u8.
848    /// - The memory referenced by the returned slice must not be
849    ///   accessed through any other pointer (not derived from the
850    ///   return value) for the duration of the lifetime 'a. Both read
851    ///   and write accesses are forbidden.
852    ///
853    ///   Accesses from Safe Rust necessarily follow this rule,
854    ///   because the returned slice's lifetime is the same as that of
855    ///   a mutable borrow of self.
856    /// - The total size self.mem_size() * mem::size_of::<u8>() of the
857    ///   slice must be no larger than isize::MAX, and adding that
858    ///   size to data must not "wrap around" the address space. See
859    ///   the safety documentation of pointer::offset.
860    ///
861    ///   This is ensured by a check in ::new()
862    pub(super) fn as_mut_slice(&mut self) -> &mut [u8] {
863        unsafe { std::slice::from_raw_parts_mut(self.base_ptr(), self.mem_size()) }
864    }
865
866    /// Internal helper method to get the backing memory as a slice.
867    ///
868    /// # Safety
869    /// See the discussion on as_mut_slice, with the third point
870    /// replaced by:
871    /// - The memory referenced by the returned slice must not be
872    ///   mutated for the duration of lifetime 'a, except inside an
873    ///   UnsafeCell.
874    ///
875    ///   Host accesses from Safe Rust necessarily follow this rule,
876    ///   because the returned slice's lifetime is the same as that of
877    ///   a borrow of self, preventing mutations via other methods.
878    #[instrument(skip_all, parent = Span::current(), level= "Trace")]
879    pub fn as_slice<'a>(&'a self) -> &'a [u8] {
880        unsafe { std::slice::from_raw_parts(self.base_ptr(), self.mem_size()) }
881    }
882
883    /// Copy the entire contents of `self` into a `Vec<u8>`, then return it
884    #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
885    #[cfg(test)]
886    pub(crate) fn copy_all_to_vec(&self) -> Result<Vec<u8>> {
887        let data = self.as_slice();
888        Ok(data.to_vec())
889    }
890
891    /// Copies all bytes from `src` to `self` starting at offset
892    #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
893    pub fn copy_from_slice(&mut self, src: &[u8], offset: usize) -> Result<()> {
894        let data = self.as_mut_slice();
895        bounds_check!(offset, src.len(), data.len());
896        data[offset..offset + src.len()].copy_from_slice(src);
897        Ok(())
898    }
899
900    generate_reader!(read_u8, u8);
901    generate_reader!(read_i8, i8);
902    generate_reader!(read_u16, u16);
903    generate_reader!(read_i16, i16);
904    generate_reader!(read_u32, u32);
905    generate_reader!(read_i32, i32);
906    generate_reader!(read_u64, u64);
907    generate_reader!(read_i64, i64);
908    generate_reader!(read_usize, usize);
909    generate_reader!(read_isize, isize);
910
911    generate_writer!(write_u8, u8);
912    generate_writer!(write_i8, i8);
913    generate_writer!(write_u16, u16);
914    generate_writer!(write_i16, i16);
915    generate_writer!(write_u32, u32);
916    generate_writer!(write_i32, i32);
917    generate_writer!(write_u64, u64);
918    generate_writer!(write_i64, i64);
919    generate_writer!(write_usize, usize);
920    generate_writer!(write_isize, isize);
921
922    /// Convert the ExclusiveSharedMemory, which may be freely
923    /// modified, into a GuestSharedMemory, which may be somewhat
924    /// freely modified (mostly by the guest), and a HostSharedMemory,
925    /// which may only make certain kinds of accesses that do not race
926    /// in the presence of malicious code inside the guest mutating
927    /// the GuestSharedMemory.
928    pub fn build(self) -> (HostSharedMemory, GuestSharedMemory) {
929        let lock = Arc::new(RwLock::new(()));
930        let hshm = HostSharedMemory {
931            region: self.region.clone(),
932            lock: lock.clone(),
933        };
934        (
935            hshm,
936            GuestSharedMemory {
937                region: self.region.clone(),
938                lock,
939            },
940        )
941    }
942
943    /// Gets the file handle of the shared memory region for this Sandbox
944    #[cfg(target_os = "windows")]
945    pub fn get_mmap_file_handle(&self) -> HANDLE {
946        self.region.file_mapping_handle()
947    }
948}
949
950impl SharedMemory for ExclusiveSharedMemory {
951    fn region(&self) -> &HostMapping {
952        &self.region
953    }
954    fn with_exclusivity<T, F: FnOnce(&mut ExclusiveSharedMemory) -> T>(
955        &mut self,
956        f: F,
957    ) -> Result<T> {
958        Ok(f(self))
959    }
960}
961
962/// A GuestSharedMemory is used to represent
963/// the reference to all-of-memory that is taken by the virtual cpu.
964/// Because of the memory model limitations that affect
965/// HostSharedMemory, it is likely fairly important (to ensure that
966/// our UB remains limited to interaction with an external compilation
967/// unit that likely can't be discovered by the compiler) that _rust_
968/// users do not perform racy accesses to the guest communication
969/// buffers that are also accessed by HostSharedMemory.
970#[derive(Debug)]
971pub struct GuestSharedMemory {
972    region: Arc<HostMapping>,
973    /// The lock that indicates this shared memory is being used by non-Rust code
974    ///
975    /// This lock _must_ be held whenever the guest is executing,
976    /// because it prevents the host from converting its
977    /// HostSharedMemory to an ExclusiveSharedMemory. Since the guest
978    /// may arbitrarily mutate the shared memory, only synchronized
979    /// accesses from Rust should be allowed!
980    ///
981    /// We cannot enforce this in the type system, because the memory
982    /// is mapped in to the VM at VM creation time.
983    pub lock: Arc<RwLock<()>>,
984}
985unsafe impl Send for GuestSharedMemory {}
986
987impl GuestSharedMemory {
988    /// Create a [`super::memory_region::MemoryRegion`] structure
989    /// suitable for mapping this region into a VM
990    pub(crate) fn mapping_at(
991        &self,
992        guest_base: u64,
993        region_type: MemoryRegionType,
994    ) -> MemoryRegion {
995        let flags = match region_type {
996            MemoryRegionType::Scratch => {
997                MemoryRegionFlags::READ | MemoryRegionFlags::WRITE | MemoryRegionFlags::EXECUTE
998            }
999            #[cfg(unshared_snapshot_mem)]
1000            MemoryRegionType::Snapshot => {
1001                MemoryRegionFlags::READ | MemoryRegionFlags::WRITE | MemoryRegionFlags::EXECUTE
1002            }
1003            #[allow(clippy::panic)]
1004            // This will not ever actually panic: the only places this
1005            // is called are HyperlightVm::update_snapshot_mapping and
1006            // HyperlightVm::update_scratch_mapping. The latter
1007            // statically uses the Scratch region type, and the former
1008            // does not use this at all when the unshared_snapshot_mem
1009            // feature is not set, since in that case the scratch
1010            // mapping type is ReadonlySharedMemory, not
1011            // GuestSharedMemory.
1012            _ => panic!(
1013                "GuestSharedMemory::mapping_at should only be used for Scratch or Snapshot regions"
1014            ),
1015        };
1016        mapping_at(self, guest_base, self.mem_size(), region_type, flags)
1017    }
1018}
1019
1020impl SharedMemory for GuestSharedMemory {
1021    fn region(&self) -> &HostMapping {
1022        &self.region
1023    }
1024    fn with_exclusivity<T, F: FnOnce(&mut ExclusiveSharedMemory) -> T>(
1025        &mut self,
1026        f: F,
1027    ) -> Result<T> {
1028        let guard = self.lock.try_write()?;
1029        let mut excl = ExclusiveSharedMemory {
1030            region: self.region.clone(),
1031        };
1032        let ret = f(&mut excl);
1033        drop(excl);
1034        drop(guard);
1035        Ok(ret)
1036    }
1037}
1038
1039/// A HostSharedMemory allows synchronized accesses to guest
1040/// communication buffers, allowing it to be used concurrently with a
1041/// GuestSharedMemory.
1042///
1043/// # Concurrency model
1044///
1045/// Given future requirements for asynchronous I/O with a minimum
1046/// amount of copying (e.g. WASIp3 streams), we would like it to be
1047/// possible to safely access these buffers concurrently with the
1048/// guest, ensuring that (1) data is read appropriately if the guest
1049/// is well-behaved; and (2) the host's behaviour is defined
1050/// regardless of whether or not the guest is well-behaved.
1051///
1052/// The ideal (future) flow for a guest->host message is something like
1053///   - Guest writes (unordered) bytes describing a work item into a buffer
1054///   - Guest reveals buffer via a release-store of a pointer into an
1055///     MMIO ring-buffer
1056///   - Host acquire-loads the buffer pointer from the "MMIO" ring
1057///     buffer
1058///   - Host (unordered) reads the bytes from the buffer
1059///   - Host performs validation of those bytes and uses them
1060///
1061/// Unfortunately, there appears to be no way to do this with defined
1062/// behaviour in present Rust (see
1063/// e.g. <https://github.com/rust-lang/unsafe-code-guidelines/issues/152>).
1064/// Rust does not yet have its own defined memory model, but in the
1065/// interim, it is widely treated as inheriting the current C/C++
1066/// memory models.  The most immediate problem is that regardless of
1067/// anything else, under those memory models \[1, p. 17-18; 2, p. 88\],
1068///
1069///   > The execution of a program contains a _data race_ if it
1070///   > contains two [C++23: "potentially concurrent"] conflicting
1071///   > actions [C23: "in different threads"], at least one of which
1072///   > is not atomic, and neither happens before the other [C++23: ",
1073///   > except for the special case for signal handlers described
1074///   > below"].  Any such data race results in undefined behavior.
1075///
1076/// Consequently, if a misbehaving guest fails to correctly
1077/// synchronize its stores with the host, the host's innocent loads
1078/// will trigger undefined behaviour for the entire program, including
1079/// the host.  Note that this also applies if the guest makes an
1080/// unsynchronized read of a location that the host is writing!
1081///
1082/// Despite Rust's de jure inheritance of the C memory model at the
1083/// present time, the compiler in many cases de facto adheres to LLVM
1084/// semantics, so it is worthwhile to consider what LLVM does in this
1085/// case as well.  According to the the LangRef \[3\] memory model,
1086/// loads which are involved in a race that includes at least one
1087/// non-atomic access (whether the load or a store) return `undef`,
1088/// making them roughly equivalent to reading uninitialized
1089/// memory. While this is much better, it is still bad.
1090///
1091/// Considering a different direction, recent C++ papers have seemed
1092/// to lean towards using `volatile` for similar use cases. For
1093/// example, in P1152R0 \[4\], JF Bastien notes that
1094///
1095///   > We’ve shown that volatile is purposely defined to denote
1096///   > external modifications. This happens for:
1097///   >   - Shared memory with untrusted code, where volatile is the
1098///   >     right way to avoid time-of-check time-of-use (ToCToU)
1099///   >     races which lead to security bugs such as \[PWN2OWN\] and
1100///   >     \[XENXSA155\].
1101///
1102/// Unfortunately, although this paper was adopted for C++20 (and,
1103/// sadly, mostly un-adopted for C++23, although that does not concern
1104/// us), the paper did not actually redefine volatile accesses or data
1105/// races to prevent volatile accesses from racing with other accesses
1106/// and causing undefined behaviour.  P1382R1 \[5\] would have amended
1107/// the wording of the data race definition to specifically exclude
1108/// volatile, but, unfortunately, despite receiving a
1109/// generally-positive reception at its first WG21 meeting more than
1110/// five years ago, it has not progressed.
1111///
1112/// Separately from the data race issue, there is also a concern that
1113/// according to the various memory models in use, there may be ways
1114/// in which the guest can semantically obtain uninitialized memory
1115/// and write it into the shared buffer, which may also result in
1116/// undefined behaviour on reads.  The degree to which this is a
1117/// concern is unclear, however, since it is unclear to what degree
1118/// the Rust abstract machine's conception of uninitialized memory
1119/// applies to the sandbox.  Returning briefly to the LLVM level,
1120/// rather than the Rust level, this, combined with the fact that
1121/// racing loads in LLVM return `undef`, as discussed above, we would
1122/// ideally `llvm.freeze` the result of any load out of the sandbox.
1123///
1124/// It would furthermore be ideal if we could run the flatbuffers
1125/// parsing code directly on the guest memory, in order to avoid
1126/// unnecessary copies.  That is unfortunately probably not viable at
1127/// the present time: because the generated flatbuffers parsing code
1128/// doesn't use atomic or volatile accesses, it is likely to introduce
1129/// double-read vulnerabilities.
1130///
1131/// In short, none of the Rust-level operations available to us do the
1132/// right thing, at the Rust spec level or the LLVM spec level. Our
1133/// major remaining options are therefore:
1134///   - Choose one of the options that is available to us, and accept
1135///     that we are doing something unsound according to the spec, but
1136///     hope that no reasonable compiler could possibly notice.
1137///   - Use inline assembly per architecture, for which we would only
1138///     need to worry about the _architecture_'s memory model (which
1139///     is far less demanding).
1140///
1141/// The leading candidate for the first option would seem to be to
1142/// simply use volatile accesses; there seems to be wide agreement
1143/// that this _should_ be a valid use case for them (even if it isn't
1144/// now), and projects like Linux and rust-vmm already use C11
1145/// `volatile` for this purpose.  It is also worth noting that because
1146/// we still do need to synchronize with the guest when it _is_ being
1147/// well-behaved, we would ideally use volatile acquire loads and
1148/// volatile release stores for interacting with the stack pointer in
1149/// the guest in this case.  Unfortunately, while those operations are
1150/// defined in LLVM, they are not presently exposed to Rust. While
1151/// atomic fences that are not associated with memory accesses
1152/// ([`std::sync::atomic::fence`]) might at first glance seem to help with
1153/// this problem, they unfortunately do not \[6\]:
1154///
1155///    > A fence ‘A’ which has (at least) Release ordering semantics,
1156///    > synchronizes with a fence ‘B’ with (at least) Acquire
1157///    > semantics, if and only if there exist operations X and Y,
1158///    > both operating on some atomic object ‘M’ such that A is
1159///    > sequenced before X, Y is sequenced before B and Y observes
1160///    > the change to M. This provides a happens-before dependence
1161///    > between A and B.
1162///
1163/// Note that the X and Y must be to an _atomic_ object.
1164///
1165/// We consequently assume that there has been a strong architectural
1166/// fence on a vmenter/vmexit between data being read and written.
1167/// This is unsafe (not guaranteed in the type system)!
1168///
1169/// \[1\] N3047 C23 Working Draft. <https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3047.pdf>
1170/// \[2\] N4950 C++23 Working Draft. <https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/n4950.pdf>
1171/// \[3\] LLVM Language Reference Manual, Memory Model for Concurrent Operations. <https://llvm.org/docs/LangRef.html#memmodel>
1172/// \[4\] P1152R0: Deprecating `volatile`. JF Bastien. <https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1152r0.html>
1173/// \[5\] P1382R1: `volatile_load<T>` and `volatile_store<T>`. JF Bastien, Paul McKenney, Jeffrey Yasskin, and the indefatigable TBD. <https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1382r1.pdf>
1174/// \[6\] Documentation for std::sync::atomic::fence. <https://doc.rust-lang.org/std/sync/atomic/fn.fence.html>
1175///
1176/// # Note \[Keeping mappings in sync between userspace and the guest\]
1177///
1178/// When using this structure with mshv on Linux, it is necessary to
1179/// be a little bit careful: since the hypervisor is not directly
1180/// integrated with the host kernel virtual memory subsystem, it is
1181/// easy for the memory region in userspace to get out of sync with
1182/// the memory region mapped into the guest.  Generally speaking, when
1183/// the [`SharedMemory`] is mapped into a partition, the MSHV kernel
1184/// module will call `pin_user_pages(FOLL_PIN|FOLL_WRITE)` on it,
1185/// which will eagerly do any CoW, etc needing to obtain backing pages
1186/// pinned in memory, and then map precisely those backing pages into
1187/// the virtual machine. After that, the backing pages mapped into the
1188/// VM will not change until the region is unmapped or remapped.  This
1189/// means that code in this module needs to be very careful to avoid
1190/// changing the backing pages of the region in the host userspace,
1191/// since that would result in hyperlight-host's view of the memory
1192/// becoming completely divorced from the view of the VM.
1193#[derive(Clone, Debug)]
1194pub struct HostSharedMemory {
1195    region: Arc<HostMapping>,
1196    lock: Arc<RwLock<()>>,
1197}
1198unsafe impl Send for HostSharedMemory {}
1199
1200impl HostSharedMemory {
1201    /// Read a [`Pod`] value of type `T`, whose representation is the same
1202    /// between the sandbox and the host.
1203    pub fn read<T: Pod>(&self, offset: usize) -> Result<T> {
1204        bounds_check!(offset, std::mem::size_of::<T>(), self.mem_size());
1205        let mut ret = T::zeroed();
1206        self.copy_to_slice(bytemuck::bytes_of_mut(&mut ret), offset)?;
1207        Ok(ret)
1208    }
1209
1210    /// Write a [`Pod`] value of type `T`, whose representation is the same
1211    /// between the sandbox and the host.
1212    pub fn write<T: Pod>(&self, offset: usize, data: T) -> Result<()> {
1213        bounds_check!(offset, std::mem::size_of::<T>(), self.mem_size());
1214        self.copy_from_slice(bytemuck::bytes_of(&data), offset)
1215    }
1216
1217    /// Copy the contents of the slice into the sandbox at the
1218    /// specified offset
1219    pub fn copy_to_slice(&self, slice: &mut [u8], offset: usize) -> Result<()> {
1220        bounds_check!(offset, slice.len(), self.mem_size());
1221        let base = self.base_ptr().wrapping_add(offset);
1222        let guard = self.lock.try_read()?;
1223
1224        const CHUNK: usize = size_of::<u128>();
1225        let len = slice.len();
1226        let mut i = 0;
1227
1228        // Handle unaligned head bytes until we reach u128 alignment.
1229        // Note: align_offset can return usize::MAX if alignment is impossible.
1230        // In that case, head_len = len via .min(), so we fall back to byte-by-byte
1231        // operations for the entire slice.
1232        let align_offset = base.align_offset(align_of::<u128>());
1233        let head_len = align_offset.min(len);
1234        while i < head_len {
1235            unsafe {
1236                slice[i] = base.add(i).read_volatile();
1237            }
1238            i += 1;
1239        }
1240
1241        // Read aligned u128 chunks
1242        // SAFETY: After processing head_len bytes, base.add(i) is u128-aligned.
1243        // We use write_unaligned for the destination since the slice may not be u128-aligned.
1244        let dst = slice.as_mut_ptr();
1245        while i + CHUNK <= len {
1246            unsafe {
1247                let value = (base.add(i) as *const u128).read_volatile();
1248                std::ptr::write_unaligned(dst.add(i) as *mut u128, value);
1249            }
1250            i += CHUNK;
1251        }
1252
1253        // Handle remaining tail bytes
1254        while i < len {
1255            unsafe {
1256                slice[i] = base.add(i).read_volatile();
1257            }
1258            i += 1;
1259        }
1260
1261        drop(guard);
1262        Ok(())
1263    }
1264
1265    /// Copy the contents of the sandbox at the specified offset into
1266    /// the slice
1267    pub fn copy_from_slice(&self, slice: &[u8], offset: usize) -> Result<()> {
1268        bounds_check!(offset, slice.len(), self.mem_size());
1269        let base = self.base_ptr().wrapping_add(offset);
1270        let guard = self.lock.try_read()?;
1271
1272        const CHUNK: usize = size_of::<u128>();
1273        let len = slice.len();
1274        let mut i = 0;
1275
1276        // Handle unaligned head bytes until we reach u128 alignment.
1277        // Note: align_offset can return usize::MAX if alignment is impossible.
1278        // In that case, head_len = len via .min(), so we fall back to byte-by-byte
1279        // operations for the entire slice.
1280        let align_offset = base.align_offset(align_of::<u128>());
1281        let head_len = align_offset.min(len);
1282        while i < head_len {
1283            unsafe {
1284                base.add(i).write_volatile(slice[i]);
1285            }
1286            i += 1;
1287        }
1288
1289        // Write aligned u128 chunks
1290        // SAFETY: After processing head_len bytes, base.add(i) is u128-aligned.
1291        // We use read_unaligned for the source since the slice may not be u128-aligned.
1292        let src = slice.as_ptr();
1293        while i + CHUNK <= len {
1294            unsafe {
1295                let value = std::ptr::read_unaligned(src.add(i) as *const u128);
1296                (base.add(i) as *mut u128).write_volatile(value);
1297            }
1298            i += CHUNK;
1299        }
1300
1301        // Handle remaining tail bytes
1302        while i < len {
1303            unsafe {
1304                base.add(i).write_volatile(slice[i]);
1305            }
1306            i += 1;
1307        }
1308
1309        drop(guard);
1310        Ok(())
1311    }
1312
1313    /// Fill the memory in the range `[offset, offset + len)` with `value`
1314    #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
1315    pub fn fill(&mut self, value: u8, offset: usize, len: usize) -> Result<()> {
1316        bounds_check!(offset, len, self.mem_size());
1317        let base = self.base_ptr().wrapping_add(offset);
1318        let guard = self.lock.try_read()?;
1319
1320        const CHUNK: usize = size_of::<u128>();
1321        let value_u128 = u128::from_ne_bytes([value; CHUNK]);
1322        let mut i = 0;
1323
1324        // Handle unaligned head bytes until we reach u128 alignment.
1325        // Note: align_offset can return usize::MAX if alignment is impossible.
1326        // In that case, head_len = len via .min(), so we fall back to byte-by-byte
1327        // operations for the entire slice.
1328        let align_offset = base.align_offset(align_of::<u128>());
1329        let head_len = align_offset.min(len);
1330        while i < head_len {
1331            unsafe {
1332                base.add(i).write_volatile(value);
1333            }
1334            i += 1;
1335        }
1336
1337        // Write aligned u128 chunks
1338        // SAFETY: After processing head_len bytes, base.add(i) is u128-aligned
1339        while i + CHUNK <= len {
1340            unsafe {
1341                (base.add(i) as *mut u128).write_volatile(value_u128);
1342            }
1343            i += CHUNK;
1344        }
1345
1346        // Handle remaining tail bytes
1347        while i < len {
1348            unsafe {
1349                base.add(i).write_volatile(value);
1350            }
1351            i += 1;
1352        }
1353
1354        drop(guard);
1355        Ok(())
1356    }
1357
1358    /// Pushes the given data onto shared memory to the buffer at the given offset.
1359    /// NOTE! buffer_start_offset must point to the beginning of the buffer
1360    #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
1361    pub fn push_buffer(
1362        &mut self,
1363        buffer_start_offset: usize,
1364        buffer_size: usize,
1365        data: &[u8],
1366    ) -> Result<()> {
1367        let stack_pointer_rel = self.read::<u64>(buffer_start_offset)? as usize;
1368
1369        if stack_pointer_rel > buffer_size || stack_pointer_rel < 8 {
1370            Err(StackError::SpOob(
1371                StackOp::Push,
1372                stack_pointer_rel,
1373                buffer_size,
1374            ))?;
1375        }
1376
1377        let size_required = data.len() + 8;
1378        let size_available = buffer_size - stack_pointer_rel;
1379
1380        if size_required > size_available {
1381            Err(StackError::BufferFullError(size_required, size_available))?;
1382        }
1383
1384        // get absolute
1385        let stack_pointer_abs = stack_pointer_rel + buffer_start_offset;
1386
1387        // write the actual data to the top of stack
1388        self.copy_from_slice(data, stack_pointer_abs)?;
1389
1390        // write the offset to the newly written data, to the top of stack.
1391        // this is used when popping the stack, to know how far back to jump
1392        self.write::<u64>(stack_pointer_abs + data.len(), stack_pointer_rel as u64)?;
1393
1394        // update stack pointer to point to the next free address
1395        self.write::<u64>(
1396            buffer_start_offset,
1397            (stack_pointer_rel + data.len() + 8) as u64,
1398        )?;
1399        Ok(())
1400    }
1401
1402    /// Pops the given given buffer into a `T` and returns it.
1403    /// NOTE! the data must be a size-prefixed flatbuffer, and
1404    /// buffer_start_offset must point to the beginning of the buffer
1405    pub fn try_pop_buffer_into<T>(
1406        &mut self,
1407        buffer_start_offset: usize,
1408        buffer_size: usize,
1409    ) -> Result<T>
1410    where
1411        T: for<'b> TryFrom<&'b [u8]>,
1412    {
1413        // get the stackpointer
1414        let stack_pointer_rel = self.read::<u64>(buffer_start_offset)? as usize;
1415
1416        if stack_pointer_rel > buffer_size || stack_pointer_rel < 16 {
1417            Err(StackError::SpOob(
1418                StackOp::Pop,
1419                stack_pointer_rel,
1420                buffer_size,
1421            ))?;
1422        }
1423
1424        // make it absolute
1425        let last_element_offset_abs = stack_pointer_rel + buffer_start_offset;
1426
1427        // go back 8 bytes to get offset to element on top of stack
1428        let last_element_offset_rel: usize =
1429            self.read::<u64>(last_element_offset_abs - 8)? as usize;
1430
1431        // Validate element offset (guest-writable): must be in [8, stack_pointer_rel - 16]
1432        // to leave room for the 8-byte back-pointer plus at least 8 bytes of element data
1433        // (the minimum for a size-prefixed flatbuffer: 4-byte prefix + 4-byte root offset).
1434        if last_element_offset_rel > stack_pointer_rel.saturating_sub(16)
1435            || last_element_offset_rel < 8
1436        {
1437            Err(StackError::CorruptBackPointer(
1438                last_element_offset_rel,
1439                stack_pointer_rel.saturating_sub(16),
1440            ))?;
1441        }
1442
1443        // make it absolute
1444        let last_element_offset_abs = last_element_offset_rel + buffer_start_offset;
1445
1446        // Max bytes the element can span (excluding the 8-byte back-pointer).
1447        let max_element_size = stack_pointer_rel - last_element_offset_rel - 8;
1448
1449        // Get the size of the flatbuffer buffer from memory
1450        let fb_buffer_size = {
1451            let raw_prefix = self.read::<u32>(last_element_offset_abs)?;
1452            // flatbuffer byte arrays are prefixed by 4 bytes indicating
1453            // the remaining size; add 4 for the prefix itself.
1454            let total = raw_prefix
1455                .checked_add(4)
1456                .ok_or(StackError::OverflowingPrefix(raw_prefix))?;
1457            usize::try_from(total).map_err(StackError::PrefixTooLarge)?
1458        };
1459
1460        if fb_buffer_size > max_element_size {
1461            Err(StackError::CorruptPrefix(fb_buffer_size, max_element_size))?;
1462        }
1463
1464        let mut result_buffer = vec![0; fb_buffer_size];
1465
1466        self.copy_to_slice(&mut result_buffer, last_element_offset_abs)?;
1467        let to_return = T::try_from(result_buffer.as_slice())
1468            .map_err(|_| StackError::ConvertError(type_name::<T>().to_string()))?;
1469
1470        // update the stack pointer to point to the element we just popped off since that is now free
1471        self.write::<u64>(buffer_start_offset, last_element_offset_rel as u64)?;
1472
1473        // zero out the memory we just popped off
1474        let num_bytes_to_zero = stack_pointer_rel - last_element_offset_rel;
1475        self.fill(0, last_element_offset_abs, num_bytes_to_zero)?;
1476
1477        Ok(to_return)
1478    }
1479}
1480
1481impl HostSharedMemory {
1482    /// Reset this memory region to all-zeros, choosing the fastest
1483    /// strategy for the current platform and hypervisor configuration.
1484    ///
1485    /// On Linux/KVM (without mshv3), uses `MADV_DONTNEED` for lazy
1486    /// zeroing.  On Linux/mshv3, falls through to `fill(0)`.
1487    ///
1488    /// On Windows, zeroing via `fill(0)` is prohibitively expensive
1489    /// for large regions (e.g. 448 MiB scratch).  Instead, the
1490    /// mapping is replaced with a fresh demand-zero allocation.
1491    /// Returns `Some(GuestSharedMemory)` when the mapping was
1492    /// replaced (the caller must update the VM mapping), or `None`
1493    /// when zeroed in place.
1494    ///
1495    // TODO: Find the break-even point between zero-in-place and
1496    // replace for each hypervisor and use a size-based heuristic
1497    // instead of a compile-time platform check.
1498    pub(crate) fn zero_or_replace(&mut self) -> Result<Option<GuestSharedMemory>> {
1499        #[cfg(target_os = "windows")]
1500        {
1501            let new_mem = ExclusiveSharedMemory::new(self.mem_size())?;
1502            let (hscratch, gscratch) = new_mem.build();
1503            *self = hscratch;
1504            Ok(Some(gscratch))
1505        }
1506        #[cfg(not(target_os = "windows"))]
1507        {
1508            self.with_exclusivity(|e| {
1509                #[allow(unused_mut)]
1510                let mut do_copy = true;
1511                // TODO: Find a similar lazy zeroing approach that works on MSHV.
1512                //       (See Note [Keeping mappings in sync between userspace and the guest])
1513                #[cfg(all(feature = "kvm", not(any(feature = "mshv3"))))]
1514                unsafe {
1515                    let ret = libc::madvise(
1516                        e.region.ptr() as *mut libc::c_void,
1517                        e.region.size(),
1518                        libc::MADV_DONTNEED,
1519                    );
1520                    if ret == 0 {
1521                        do_copy = false;
1522                    }
1523                }
1524                if do_copy {
1525                    e.as_mut_slice().fill(0);
1526                }
1527            })?;
1528            Ok(None)
1529        }
1530    }
1531}
1532
1533impl SharedMemory for HostSharedMemory {
1534    fn region(&self) -> &HostMapping {
1535        &self.region
1536    }
1537    fn with_exclusivity<T, F: FnOnce(&mut ExclusiveSharedMemory) -> T>(
1538        &mut self,
1539        f: F,
1540    ) -> Result<T> {
1541        let guard = self.lock.try_write()?;
1542        let mut excl = ExclusiveSharedMemory {
1543            region: self.region.clone(),
1544        };
1545        let ret = f(&mut excl);
1546        drop(excl);
1547        drop(guard);
1548        Ok(ret)
1549    }
1550}
1551
1552/// A ReadonlySharedMemory is a different kind of shared memory,
1553/// separate from the exclusive/host/guest lifecycle, used to
1554/// represent read-only mappings of snapshot pages into the guest
1555/// efficiently.
1556#[derive(Clone, Debug)]
1557pub struct ReadonlySharedMemory {
1558    region: Arc<HostMapping>,
1559    /// Number of bytes from the start of the blob that `mapping_at`
1560    /// exposes to the guest. Production callers pass the size of the
1561    /// guest-visible prefix of the snapshot blob; the remainder of
1562    /// the blob is the page-table tail that lives only host-side.
1563    #[cfg_attr(unshared_snapshot_mem, allow(dead_code))]
1564    guest_mapped_size: usize,
1565}
1566// Safety: HostMapping is only non-Send/Sync (causing
1567// ReadonlySharedMemory to not be automatically Send/Sync) because raw
1568// pointers are not ("as a lint", as the Rust docs say). We don't want
1569// to mark HostMapping Send/Sync immediately, because that could
1570// socially imply that it's "safe" to use unsafe accesses from
1571// multiple threads at once in more cases, including ones that don't
1572// actually ensure immutability/synchronisation. Since
1573// ReadonlySharedMemory can only be accessed by reading, and reading
1574// concurrently from multiple threads is not racy,
1575// ReadonlySharedMemory can be Send and Sync.
1576unsafe impl Send for ReadonlySharedMemory {}
1577unsafe impl Sync for ReadonlySharedMemory {}
1578
1579impl ReadonlySharedMemory {
1580    pub(crate) fn from_bytes(contents: &[u8], guest_mapped_size: usize) -> Result<Self> {
1581        if guest_mapped_size == 0
1582            || guest_mapped_size > contents.len()
1583            || !guest_mapped_size.is_multiple_of(page_size::get())
1584        {
1585            return Err(SharedMemoryError::MemoryRequest(
1586                guest_mapped_size,
1587                0,
1588                contents.len(),
1589                page_size::get(),
1590            ));
1591        }
1592        let mut anon =
1593            ExclusiveSharedMemory::new(contents.len().next_multiple_of(page_size::get()))?;
1594        anon.copy_from_slice(contents, 0)?;
1595        Ok(ReadonlySharedMemory {
1596            region: anon.region,
1597            guest_mapped_size,
1598        })
1599    }
1600
1601    /// The number of bytes that should be mapped into guest PA space.
1602    #[cfg(not(unshared_snapshot_mem))]
1603    pub(crate) fn guest_mapped_size(&self) -> usize {
1604        self.guest_mapped_size
1605    }
1606
1607    /// Create a `ReadonlySharedMemory` backed by a file on disk.
1608    ///
1609    /// The file's length must be a non-zero multiple of `PAGE_SIZE`.
1610    /// `guest_mapped_size` must be a non-zero multiple of `PAGE_SIZE`
1611    /// no greater than the file's length.
1612    pub(crate) fn from_file(file: &std::fs::File, guest_mapped_size: usize) -> Result<Self> {
1613        let len: usize = file
1614            .metadata()
1615            .map_err(SharedMemoryError::FileMetadata)?
1616            .len()
1617            .try_into()
1618            .map_err(SharedMemoryError::FileTooLarge)?;
1619
1620        if len == 0 || !len.is_multiple_of(page_size::get()) {
1621            return Err(SharedMemoryError::MemoryRequest(
1622                len,
1623                0,
1624                usize::MAX,
1625                page_size::get(),
1626            ));
1627        }
1628
1629        if guest_mapped_size == 0
1630            || guest_mapped_size > len
1631            || !guest_mapped_size.is_multiple_of(page_size::get())
1632        {
1633            return Err(SharedMemoryError::MemoryRequest(
1634                guest_mapped_size,
1635                0,
1636                len,
1637                page_size::get(),
1638            ));
1639        }
1640
1641        let region = Self::map_file(file, len)?;
1642        Ok(ReadonlySharedMemory {
1643            region,
1644            guest_mapped_size,
1645        })
1646    }
1647
1648    /// Linux: reserve `[guard][blob][guard]` as one anonymous
1649    /// `PROT_NONE` mapping, then `MAP_FIXED` the file over the
1650    /// middle slot.
1651    #[cfg(unix)]
1652    fn map_file(file: &std::fs::File, len: usize) -> Result<Arc<HostMapping>> {
1653        use std::os::unix::io::AsRawFd;
1654
1655        #[cfg(mshv3)]
1656        use libc::PROT_WRITE;
1657        use libc::{
1658            MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED, MAP_NORESERVE, MAP_PRIVATE, PROT_NONE, PROT_READ,
1659            mmap, off_t, size_t,
1660        };
1661
1662        let total_size =
1663            len.checked_add(2 * page_size::get())
1664                .ok_or(SharedMemoryError::MemoryRequest(
1665                    len,
1666                    0,
1667                    usize::MAX - 2 * page_size::get(),
1668                    1,
1669                ))?;
1670
1671        let fd = file.as_raw_fd();
1672
1673        // 1. Reserve the full `[guard][blob][guard]` address range as
1674        //    one anonymous `PROT_NONE` mapping. This pins the layout
1675        //    and gives us a single RAII owner for the whole region.
1676        // SAFETY: anonymous `mmap` with a null address has no
1677        // preconditions; the kernel picks the address.
1678        let base = unsafe {
1679            mmap(
1680                null_mut(),
1681                total_size as size_t,
1682                PROT_NONE,
1683                MAP_ANONYMOUS | MAP_PRIVATE | MAP_NORESERVE,
1684                -1,
1685                0 as off_t,
1686            )
1687        };
1688        if base == MAP_FAILED {
1689            return Err(SharedMemoryError::MmapFailed(
1690                std::io::Error::last_os_error().raw_os_error(),
1691            ));
1692        }
1693        let reservation = Mmap {
1694            base,
1695            len: total_size,
1696        };
1697
1698        // 2. Overlay the file content on the middle slot with
1699        //    `MAP_FIXED`. `MAP_PRIVATE` keeps the mapping detached
1700        //    from the underlying file.
1701        //
1702        //    MSHV's map_user_memory requires host-writable pages
1703        //    (the kernel module calls `pin_user_pages(FOLL_PIN|FOLL_WRITE)`
1704        //    on the region when it is mapped into the partition).
1705        //    KVM accepts read-only host pages for read-only guest slots.
1706        #[cfg(mshv3)]
1707        let file_prot = PROT_READ | PROT_WRITE;
1708        #[cfg(not(mshv3))]
1709        let file_prot = PROT_READ;
1710        // SAFETY: `total_size = len + 2 * PAGE_SIZE_USIZE`, so
1711        // `base + PAGE_SIZE_USIZE` is in-bounds of the reservation.
1712        let usable_ptr = unsafe { (base as *mut u8).add(page_size::get()) };
1713        // SAFETY: `usable_ptr..usable_ptr + len` lies entirely within
1714        // the reservation owned by `reservation`. `MAP_FIXED`
1715        // replaces that sub-range in place; on failure the
1716        // surrounding anonymous mapping is unaffected and
1717        // `reservation` releases it on drop.
1718        let mapped = unsafe {
1719            mmap(
1720                usable_ptr as *mut c_void,
1721                len as size_t,
1722                file_prot,
1723                MAP_PRIVATE | MAP_FIXED | MAP_NORESERVE,
1724                fd,
1725                0 as off_t,
1726            )
1727        };
1728        if mapped == MAP_FAILED {
1729            return Err(SharedMemoryError::MmapFailed(
1730                std::io::Error::last_os_error().raw_os_error(),
1731            ));
1732        }
1733
1734        // 3. The first and last pages keep their `PROT_NONE` from the
1735        //    anonymous reservation, so no extra `mprotect` is needed.
1736
1737        #[allow(clippy::arc_with_non_send_sync)]
1738        Ok(Arc::new(HostMapping { mmap: reservation }))
1739    }
1740
1741    /// Windows: reserve `[guard][blob][guard]` as one
1742    /// `VirtualAlloc2` placeholder, split the middle slot out, and
1743    /// `MapViewOfFile3` the file over the middle slot.
1744    #[cfg(target_os = "windows")]
1745    fn map_file(file: &std::fs::File, len: usize) -> Result<Arc<HostMapping>> {
1746        use std::os::windows::io::AsRawHandle;
1747
1748        let total_size =
1749            len.checked_add(2 * page_size::get())
1750                .ok_or(SharedMemoryError::MemoryRequest(
1751                    len,
1752                    0,
1753                    usize::MAX - 2 * page_size::get(),
1754                    1,
1755                ))?;
1756
1757        let file_handle = HANDLE(file.as_raw_handle());
1758
1759        // 1. Reserve the full `[guard][blob][guard]` address range
1760        //    as one `VirtualAlloc2` placeholder.
1761        let whole = Placeholder::reserve(total_size)?;
1762
1763        // 2. Split the placeholder into three adjacent slots. The
1764        //    leading and trailing slots stay unmapped and act as
1765        //    guard pages. The middle slot will receive the file view.
1766        let (leading, middle, trailing) = whole.split_into_three(page_size::get(), len)?;
1767
1768        // 3. Create a read-only file mapping section over the file.
1769        // SAFETY: `file_handle` is a valid file HANDLE borrowed from
1770        // `file` for the duration of this call. `CreateFileMappingA`
1771        // returns a new handle that we wrap in `FileMapping` below.
1772        let raw_handle =
1773            unsafe { CreateFileMappingA(file_handle, None, PAGE_READONLY, 0, 0, PCSTR::null()) }?;
1774        if raw_handle.is_invalid() {
1775            log_then_return!(SharedMemoryError::MemoryAllocationFailed(
1776                Error::last_os_error().raw_os_error()
1777            ));
1778        }
1779        let file_mapping = FileMapping(raw_handle);
1780
1781        // 4. Replace the middle placeholder slot with a view of the
1782        //    file mapping via `MapViewOfFile3(MEM_REPLACE_PLACEHOLDER)`.
1783        let view = middle.map_file_view(raw_handle)?;
1784
1785        #[allow(clippy::arc_with_non_send_sync)]
1786        Ok(Arc::new(HostMapping {
1787            mapping: WindowsMapping::FileBacked {
1788                leading,
1789                view,
1790                trailing,
1791                file_mapping,
1792            },
1793        }))
1794    }
1795
1796    pub(crate) fn as_slice(&self) -> &[u8] {
1797        unsafe { std::slice::from_raw_parts(self.base_ptr(), self.mem_size()) }
1798    }
1799
1800    #[cfg(unshared_snapshot_mem)]
1801    pub(crate) fn copy_to_writable(&self) -> Result<ExclusiveSharedMemory> {
1802        let mut writable = ExclusiveSharedMemory::new(self.mem_size())?;
1803        writable.copy_from_slice(self.as_slice(), 0)?;
1804        Ok(writable)
1805    }
1806
1807    #[cfg(not(unshared_snapshot_mem))]
1808    pub(crate) fn build(self) -> (Self, Self) {
1809        (self.clone(), self)
1810    }
1811
1812    #[cfg(not(unshared_snapshot_mem))]
1813    pub(crate) fn mapping_at(
1814        &self,
1815        guest_base: u64,
1816        region_type: MemoryRegionType,
1817    ) -> MemoryRegion {
1818        #[allow(clippy::panic)]
1819        // This will not ever actually panic: the only place this is
1820        // called is HyperlightVm::update_snapshot_mapping, which
1821        // always calls it with the Snapshot region type.
1822        if region_type != MemoryRegionType::Snapshot {
1823            panic!("ReadonlySharedMemory::mapping_at should only be used for Snapshot regions");
1824        }
1825        mapping_at(
1826            self,
1827            guest_base,
1828            self.guest_mapped_size(),
1829            region_type,
1830            MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
1831        )
1832    }
1833}
1834
1835impl SharedMemory for ReadonlySharedMemory {
1836    fn region(&self) -> &HostMapping {
1837        &self.region
1838    }
1839    // Trait defaults work for `base_addr`, `base_ptr`, and
1840    // `mem_size`: each `ReadonlySharedMemory` has the
1841    // `[guard][blob][guard]` layout.
1842    //
1843    // `host_region_base` differs per Windows mapping flavour:
1844    //  * `WindowsMapping::Anonymous` (`from_bytes`): file mapping
1845    //    spans the whole region. Same as the trait default.
1846    //  * `WindowsMapping::FileBacked` (`from_file`): file mapping
1847    //    covers only the blob. Expose only the blob to the surrogate.
1848    #[cfg(windows)]
1849    fn host_region_base(&self) -> <HostGuestMemoryRegion as MemoryRegionKind>::HostBaseType {
1850        match &self.region().mapping {
1851            WindowsMapping::Anonymous { .. } => super::memory_region::HostRegionBase {
1852                from_handle: self.region().file_mapping_handle().into(),
1853                handle_base: self.region().ptr() as usize,
1854                handle_size: self.region().size(),
1855                offset: page_size::get(),
1856            },
1857            WindowsMapping::FileBacked { .. } => super::memory_region::HostRegionBase {
1858                from_handle: self.region().file_mapping_handle().into(),
1859                handle_base: self.base_ptr() as usize,
1860                handle_size: self.mem_size(),
1861                offset: 0,
1862            },
1863        }
1864    }
1865    // There's no way to get exclusive (and therefore writable) access
1866    // to a ReadonlySharedMemory.
1867    fn with_exclusivity<T, F: FnOnce(&mut ExclusiveSharedMemory) -> T>(
1868        &mut self,
1869        _: F,
1870    ) -> Result<T> {
1871        Err(SharedMemoryError::ReadonlySharedMemoryExclusiveRequest)
1872    }
1873    // However, just access to the contents as a slice is doable
1874    fn with_contents<T, F: FnOnce(&[u8]) -> T>(&mut self, f: F) -> Result<T> {
1875        Ok(f(self.as_slice()))
1876    }
1877}
1878
1879impl<S: SharedMemory> PartialEq<S> for ReadonlySharedMemory {
1880    fn eq(&self, other: &S) -> bool {
1881        self.raw_ptr() == other.raw_ptr()
1882    }
1883}
1884
1885#[cfg(test)]
1886mod tests {
1887    #[cfg(not(miri))]
1888    use proptest::prelude::*;
1889
1890    #[cfg(not(miri))]
1891    use super::HostSharedMemory;
1892    use super::{ExclusiveSharedMemory, Result, SharedMemory};
1893    #[cfg(not(miri))]
1894    use crate::mem::shared_mem_tests::read_write_test_suite;
1895
1896    #[test]
1897    fn fill() {
1898        let mem_size: usize = page_size::get();
1899        let eshm = ExclusiveSharedMemory::new(mem_size).unwrap();
1900        let (mut hshm, _) = eshm.build();
1901
1902        hshm.fill(1, 0, 1024).unwrap();
1903        hshm.fill(2, 1024, 1024).unwrap();
1904        hshm.fill(3, 2048, 1024).unwrap();
1905        hshm.fill(4, 3072, 1024).unwrap();
1906
1907        let vec = hshm
1908            .with_exclusivity(|e| e.copy_all_to_vec().unwrap())
1909            .unwrap();
1910
1911        assert!(vec[0..1024].iter().all(|&x| x == 1));
1912        assert!(vec[1024..2048].iter().all(|&x| x == 2));
1913        assert!(vec[2048..3072].iter().all(|&x| x == 3));
1914        assert!(vec[3072..4096].iter().all(|&x| x == 4));
1915
1916        hshm.fill(5, 0, mem_size).unwrap();
1917
1918        let vec2 = hshm
1919            .with_exclusivity(|e| e.copy_all_to_vec().unwrap())
1920            .unwrap();
1921        assert!(vec2.iter().all(|&x| x == 5));
1922
1923        assert!(hshm.fill(0, 0, mem_size + 1).is_err());
1924        assert!(hshm.fill(0, mem_size, 1).is_err());
1925    }
1926
1927    /// Verify that `bounds_check!` rejects offset + size combinations that
1928    /// would overflow `usize`.
1929    #[test]
1930    fn bounds_check_overflow() {
1931        let mem_size: usize = page_size::get();
1932        let mut eshm = ExclusiveSharedMemory::new(mem_size).unwrap();
1933
1934        // ExclusiveSharedMemory methods
1935        assert!(eshm.read_i32(usize::MAX).is_err());
1936        assert!(eshm.write_i32(usize::MAX, 0).is_err());
1937        assert!(eshm.copy_from_slice(&[0u8; 1], usize::MAX).is_err());
1938
1939        // HostSharedMemory methods
1940        let (mut hshm, _) = eshm.build();
1941
1942        assert!(hshm.read::<u8>(usize::MAX).is_err());
1943        assert!(hshm.read::<u64>(usize::MAX - 3).is_err());
1944        assert!(hshm.write::<u8>(usize::MAX, 0).is_err());
1945        assert!(hshm.write::<u64>(usize::MAX - 3, 0).is_err());
1946
1947        let mut buf = [0u8; 1];
1948        assert!(hshm.copy_to_slice(&mut buf, usize::MAX).is_err());
1949        assert!(hshm.copy_from_slice(&[0u8; 1], usize::MAX).is_err());
1950
1951        assert!(hshm.fill(0, usize::MAX, 1).is_err());
1952        assert!(hshm.fill(0, 1, usize::MAX).is_err());
1953    }
1954
1955    #[test]
1956    fn copy_into_from() -> Result<()> {
1957        let mem_size: usize = page_size::get();
1958        let vec_len = 10;
1959        let eshm = ExclusiveSharedMemory::new(mem_size)?;
1960        let (hshm, _) = eshm.build();
1961        let vec = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
1962        // write the value to the memory at the beginning.
1963        hshm.copy_from_slice(&vec, 0)?;
1964
1965        let mut vec2 = vec![0; vec_len];
1966        // read the value back from the memory at the beginning.
1967        hshm.copy_to_slice(vec2.as_mut_slice(), 0)?;
1968        assert_eq!(vec, vec2);
1969
1970        let offset = mem_size - vec.len();
1971        // write the value to the memory at the end.
1972        hshm.copy_from_slice(&vec, offset)?;
1973
1974        let mut vec3 = vec![0; vec_len];
1975        // read the value back from the memory at the end.
1976        hshm.copy_to_slice(&mut vec3, offset)?;
1977        assert_eq!(vec, vec3);
1978
1979        let offset = mem_size / 2;
1980        // write the value to the memory at the middle.
1981        hshm.copy_from_slice(&vec, offset)?;
1982
1983        let mut vec4 = vec![0; vec_len];
1984        // read the value back from the memory at the middle.
1985        hshm.copy_to_slice(&mut vec4, offset)?;
1986        assert_eq!(vec, vec4);
1987
1988        // try and read a value from an offset that is beyond the end of the memory.
1989        let mut vec5 = vec![0; vec_len];
1990        assert!(hshm.copy_to_slice(&mut vec5, mem_size).is_err());
1991
1992        // try and write a value to an offset that is beyond the end of the memory.
1993        assert!(hshm.copy_from_slice(&vec5, mem_size).is_err());
1994
1995        // try and read a value from an offset that is too large.
1996        let mut vec6 = vec![0; vec_len];
1997        assert!(hshm.copy_to_slice(&mut vec6, mem_size * 2).is_err());
1998
1999        // try and write a value to an offset that is too large.
2000        assert!(hshm.copy_from_slice(&vec6, mem_size * 2).is_err());
2001
2002        // try and read a value that is too large.
2003        let mut vec7 = vec![0; mem_size * 2];
2004        assert!(hshm.copy_to_slice(&mut vec7, 0).is_err());
2005
2006        // try and write a value that is too large.
2007        assert!(hshm.copy_from_slice(&vec7, 0).is_err());
2008
2009        Ok(())
2010    }
2011
2012    // proptest uses file I/O (getcwd, open) which miri doesn't support
2013    #[cfg(not(miri))]
2014    proptest! {
2015        #[test]
2016        fn read_write_i32(val in -0x1000_i32..0x1000_i32) {
2017            read_write_test_suite(
2018                val,
2019                ExclusiveSharedMemory::new,
2020                Box::new(ExclusiveSharedMemory::read_i32),
2021                Box::new(ExclusiveSharedMemory::write_i32),
2022            )
2023            .unwrap();
2024            read_write_test_suite(
2025                val,
2026                |s| {
2027                    let e = ExclusiveSharedMemory::new(s)?;
2028                    let (h, _) = e.build();
2029                    Ok(h)
2030                },
2031                Box::new(HostSharedMemory::read::<i32>),
2032                Box::new(|h, o, v| h.write::<i32>(o, v)),
2033            )
2034            .unwrap();
2035        }
2036    }
2037
2038    #[test]
2039    fn alloc_fail() {
2040        let gm = ExclusiveSharedMemory::new(0);
2041        assert!(gm.is_err());
2042        let gm = ExclusiveSharedMemory::new(usize::MAX);
2043        assert!(gm.is_err());
2044    }
2045
2046    #[test]
2047    fn clone() {
2048        let eshm = ExclusiveSharedMemory::new(page_size::get()).unwrap();
2049        let (hshm1, _) = eshm.build();
2050        let hshm2 = hshm1.clone();
2051
2052        // after hshm1 is cloned, hshm1 and hshm2 should have identical
2053        // memory sizes and pointers.
2054        assert_eq!(hshm1.mem_size(), hshm2.mem_size());
2055        assert_eq!(hshm1.base_addr(), hshm2.base_addr());
2056
2057        // we should be able to copy a byte array into both hshm1 and hshm2,
2058        // and have both changes be reflected in all clones
2059        hshm1.copy_from_slice(b"a", 0).unwrap();
2060        hshm2.copy_from_slice(b"b", 1).unwrap();
2061
2062        // at this point, both hshm1 and hshm2 should have
2063        // offset 0 = 'a', offset 1 = 'b'
2064        for (raw_offset, expected) in &[(0, b'a'), (1, b'b')] {
2065            assert_eq!(hshm1.read::<u8>(*raw_offset).unwrap(), *expected);
2066            assert_eq!(hshm2.read::<u8>(*raw_offset).unwrap(), *expected);
2067        }
2068
2069        // after we drop hshm1, hshm2 should still exist, be valid,
2070        // and have all contents from before hshm1 was dropped
2071        drop(hshm1);
2072
2073        // at this point, hshm2 should still have offset 0 = 'a', offset 1 = 'b'
2074        for (raw_offset, expected) in &[(0, b'a'), (1, b'b')] {
2075            assert_eq!(hshm2.read::<u8>(*raw_offset).unwrap(), *expected);
2076        }
2077        hshm2.copy_from_slice(b"c", 2).unwrap();
2078        assert_eq!(hshm2.read::<u8>(2).unwrap(), b'c');
2079        drop(hshm2);
2080    }
2081
2082    #[test]
2083    fn copy_all_to_vec() {
2084        let mut data = vec![b'a', b'b', b'c'];
2085        data.resize(page_size::get(), 0);
2086        let mut eshm = ExclusiveSharedMemory::new(data.len()).unwrap();
2087        eshm.copy_from_slice(data.as_slice(), 0).unwrap();
2088        let ret_vec = eshm.copy_all_to_vec().unwrap();
2089        assert_eq!(data, ret_vec);
2090    }
2091
2092    /// Test that verifies memory is properly unmapped when all SharedMemory
2093    /// references are dropped.
2094    #[test]
2095    #[cfg(all(target_os = "linux", not(miri)))]
2096    fn test_drop() {
2097        use proc_maps::get_process_maps;
2098
2099        // Use a unique size that no other test uses to avoid false positives
2100        // from concurrent tests allocating at the same address.
2101        // The mprotect calls split the mapping into 3 regions (guard, usable, guard),
2102        // so we check for the usable region which has this exact size.
2103        //
2104        // NOTE: If this test fails intermittently, there may be a race condition
2105        // where another test allocates memory at the same address between our
2106        // drop and the mapping check. Ensure UNIQUE_SIZE is not used by any
2107        // other test in the codebase to avoid this.
2108        let unique_size: usize = page_size::get() * 17;
2109
2110        let pid = std::process::id();
2111
2112        let eshm = ExclusiveSharedMemory::new(unique_size).unwrap();
2113        let (hshm1, gshm) = eshm.build();
2114        let hshm2 = hshm1.clone();
2115
2116        // Use the usable memory region (not raw), since mprotect splits the mapping
2117        let base_ptr = hshm1.base_ptr() as usize;
2118        let mem_size = hshm1.mem_size();
2119
2120        // Helper to check if exact mapping exists (matching both address and size)
2121        let has_exact_mapping = |ptr: usize, size: usize| -> bool {
2122            get_process_maps(pid.try_into().unwrap())
2123                .unwrap()
2124                .iter()
2125                .any(|m| m.start() == ptr && m.size() == size)
2126        };
2127
2128        // Verify mapping exists before drop
2129        assert!(
2130            has_exact_mapping(base_ptr, mem_size),
2131            "shared memory mapping not found at {:#x} with size {}",
2132            base_ptr,
2133            mem_size
2134        );
2135
2136        // Drop all references
2137        drop(hshm1);
2138        drop(hshm2);
2139        drop(gshm);
2140
2141        // Verify exact mapping is gone
2142        assert!(
2143            !has_exact_mapping(base_ptr, mem_size),
2144            "shared memory mapping still exists at {:#x} with size {} after drop",
2145            base_ptr,
2146            mem_size
2147        );
2148    }
2149
2150    /// Tests for the optimized aligned memory operations.
2151    /// These tests verify that the u128 chunk optimization works correctly
2152    /// for various alignment scenarios and buffer sizes.
2153    mod alignment_tests {
2154        use super::*;
2155
2156        const CHUNK_SIZE: usize = size_of::<u128>();
2157
2158        /// Test copy operations with all possible starting alignment offsets (0-15)
2159        #[test]
2160        fn copy_with_various_alignments() {
2161            // Use a buffer large enough to test all alignment cases
2162            let mem_size: usize = page_size::get();
2163            let eshm = ExclusiveSharedMemory::new(mem_size).unwrap();
2164            let (hshm, _) = eshm.build();
2165
2166            // Test all 16 possible alignment offsets (0 through 15)
2167            for start_offset in 0..CHUNK_SIZE {
2168                let test_len = 64; // Enough to cover head, aligned chunks, and tail
2169                let test_data: Vec<u8> = (0..test_len).map(|i| (i + start_offset) as u8).collect();
2170
2171                // Write data at the given offset
2172                hshm.copy_from_slice(&test_data, start_offset).unwrap();
2173
2174                // Read it back
2175                let mut read_buf = vec![0u8; test_len];
2176                hshm.copy_to_slice(&mut read_buf, start_offset).unwrap();
2177
2178                assert_eq!(
2179                    test_data, read_buf,
2180                    "Mismatch at alignment offset {}",
2181                    start_offset
2182                );
2183            }
2184        }
2185
2186        /// Test copy operations with lengths smaller than chunk size (< 16 bytes)
2187        #[test]
2188        fn copy_small_lengths() {
2189            let mem_size: usize = page_size::get();
2190            let eshm = ExclusiveSharedMemory::new(mem_size).unwrap();
2191            let (hshm, _) = eshm.build();
2192
2193            for len in 0..CHUNK_SIZE {
2194                let test_data: Vec<u8> = (0..len).map(|i| i as u8).collect();
2195
2196                hshm.copy_from_slice(&test_data, 0).unwrap();
2197
2198                let mut read_buf = vec![0u8; len];
2199                hshm.copy_to_slice(&mut read_buf, 0).unwrap();
2200
2201                assert_eq!(test_data, read_buf, "Mismatch for length {}", len);
2202            }
2203        }
2204
2205        /// Test copy operations with lengths that don't align to chunk boundaries
2206        #[test]
2207        fn copy_non_aligned_lengths() {
2208            let mem_size: usize = page_size::get();
2209            let eshm = ExclusiveSharedMemory::new(mem_size).unwrap();
2210            let (hshm, _) = eshm.build();
2211
2212            // Test lengths like 17, 31, 33, 47, 63, 65, etc.
2213            let test_lengths = [17, 31, 33, 47, 63, 65, 100, 127, 129, 255, 257];
2214
2215            for &len in &test_lengths {
2216                let test_data: Vec<u8> = (0..len).map(|i| (i % 256) as u8).collect();
2217
2218                hshm.copy_from_slice(&test_data, 0).unwrap();
2219
2220                let mut read_buf = vec![0u8; len];
2221                hshm.copy_to_slice(&mut read_buf, 0).unwrap();
2222
2223                assert_eq!(test_data, read_buf, "Mismatch for length {}", len);
2224            }
2225        }
2226
2227        /// Test copy with exactly one chunk (16 bytes)
2228        #[test]
2229        fn copy_exact_chunk_size() {
2230            let mem_size: usize = page_size::get();
2231            let eshm = ExclusiveSharedMemory::new(mem_size).unwrap();
2232            let (hshm, _) = eshm.build();
2233
2234            let test_data: Vec<u8> = (0..CHUNK_SIZE).map(|i| i as u8).collect();
2235
2236            hshm.copy_from_slice(&test_data, 0).unwrap();
2237
2238            let mut read_buf = vec![0u8; CHUNK_SIZE];
2239            hshm.copy_to_slice(&mut read_buf, 0).unwrap();
2240
2241            assert_eq!(test_data, read_buf);
2242        }
2243
2244        /// Test fill with various alignment offsets
2245        #[test]
2246        fn fill_with_various_alignments() {
2247            let mem_size: usize = page_size::get();
2248            let eshm = ExclusiveSharedMemory::new(mem_size).unwrap();
2249            let (mut hshm, _) = eshm.build();
2250
2251            for start_offset in 0..CHUNK_SIZE {
2252                let fill_len = 64;
2253                let fill_value = (start_offset % 256) as u8;
2254
2255                // Clear memory first
2256                hshm.fill(0, 0, mem_size).unwrap();
2257
2258                // Fill at the given offset
2259                hshm.fill(fill_value, start_offset, fill_len).unwrap();
2260
2261                // Read it back and verify
2262                let mut read_buf = vec![0u8; fill_len];
2263                hshm.copy_to_slice(&mut read_buf, start_offset).unwrap();
2264
2265                assert!(
2266                    read_buf.iter().all(|&b| b == fill_value),
2267                    "Fill mismatch at alignment offset {}",
2268                    start_offset
2269                );
2270            }
2271        }
2272
2273        /// Test fill with lengths smaller than chunk size
2274        #[test]
2275        fn fill_small_lengths() {
2276            let mem_size: usize = page_size::get();
2277            let eshm = ExclusiveSharedMemory::new(mem_size).unwrap();
2278            let (mut hshm, _) = eshm.build();
2279
2280            for len in 0..CHUNK_SIZE {
2281                let fill_value = 0xAB;
2282
2283                hshm.fill(0, 0, mem_size).unwrap(); // Clear
2284                hshm.fill(fill_value, 0, len).unwrap();
2285
2286                let mut read_buf = vec![0u8; len];
2287                hshm.copy_to_slice(&mut read_buf, 0).unwrap();
2288
2289                assert!(
2290                    read_buf.iter().all(|&b| b == fill_value),
2291                    "Fill mismatch for length {}",
2292                    len
2293                );
2294            }
2295        }
2296
2297        /// Test fill with non-aligned lengths
2298        #[test]
2299        fn fill_non_aligned_lengths() {
2300            let mem_size: usize = page_size::get();
2301            let eshm = ExclusiveSharedMemory::new(mem_size).unwrap();
2302            let (mut hshm, _) = eshm.build();
2303
2304            let test_lengths = [17, 31, 33, 47, 63, 65, 100, 127, 129, 255, 257];
2305
2306            for &len in &test_lengths {
2307                let fill_value = 0xCD;
2308
2309                hshm.fill(0, 0, mem_size).unwrap(); // Clear
2310                hshm.fill(fill_value, 0, len).unwrap();
2311
2312                let mut read_buf = vec![0u8; len];
2313                hshm.copy_to_slice(&mut read_buf, 0).unwrap();
2314
2315                assert!(
2316                    read_buf.iter().all(|&b| b == fill_value),
2317                    "Fill mismatch for length {}",
2318                    len
2319                );
2320            }
2321        }
2322
2323        /// Test edge cases: length 0 and length 1
2324        #[test]
2325        fn copy_edge_cases() {
2326            let mem_size: usize = page_size::get();
2327            let eshm = ExclusiveSharedMemory::new(mem_size).unwrap();
2328            let (hshm, _) = eshm.build();
2329
2330            // Length 0
2331            let empty: Vec<u8> = vec![];
2332            hshm.copy_from_slice(&empty, 0).unwrap();
2333            let mut read_buf: Vec<u8> = vec![];
2334            hshm.copy_to_slice(&mut read_buf, 0).unwrap();
2335            assert!(read_buf.is_empty());
2336
2337            // Length 1
2338            let single = vec![0x42u8];
2339            hshm.copy_from_slice(&single, 0).unwrap();
2340            let mut read_buf = vec![0u8; 1];
2341            hshm.copy_to_slice(&mut read_buf, 0).unwrap();
2342            assert_eq!(single, read_buf);
2343        }
2344
2345        /// Test combined: unaligned start + non-aligned length
2346        #[test]
2347        fn copy_unaligned_start_and_length() {
2348            let mem_size: usize = page_size::get();
2349            let eshm = ExclusiveSharedMemory::new(mem_size).unwrap();
2350            let (hshm, _) = eshm.build();
2351
2352            // Start at offset 7 (unaligned), length 37 (not a multiple of 16)
2353            let start_offset = 7;
2354            let len = 37;
2355            let test_data: Vec<u8> = (0..len).map(|i| (i * 3) as u8).collect();
2356
2357            hshm.copy_from_slice(&test_data, start_offset).unwrap();
2358
2359            let mut read_buf = vec![0u8; len];
2360            hshm.copy_to_slice(&mut read_buf, start_offset).unwrap();
2361
2362            assert_eq!(test_data, read_buf);
2363        }
2364    }
2365
2366    /// Bounds checking for `try_pop_buffer_into` against corrupt guest data.
2367    mod try_pop_buffer_bounds {
2368        use super::*;
2369
2370        #[derive(Debug, PartialEq)]
2371        struct RawBytes(Vec<u8>);
2372
2373        impl TryFrom<&[u8]> for RawBytes {
2374            type Error = String;
2375            fn try_from(value: &[u8]) -> std::result::Result<Self, Self::Error> {
2376                Ok(RawBytes(value.to_vec()))
2377            }
2378        }
2379
2380        /// Create a buffer with stack pointer initialized to 8 (empty).
2381        fn make_buffer(mem_size: usize) -> super::super::HostSharedMemory {
2382            let eshm = ExclusiveSharedMemory::new(mem_size).unwrap();
2383            let (hshm, _) = eshm.build();
2384            hshm.write::<u64>(0, 8u64).unwrap();
2385            hshm
2386        }
2387
2388        #[test]
2389        fn normal_push_pop_roundtrip() {
2390            let mem_size = page_size::get();
2391            let mut hshm = make_buffer(mem_size);
2392
2393            // Size-prefixed flatbuffer-like payload: [size: u32 LE][payload]
2394            let payload = b"hello";
2395            let mut data = Vec::new();
2396            data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
2397            data.extend_from_slice(payload);
2398
2399            hshm.push_buffer(0, mem_size, &data).unwrap();
2400            let result: RawBytes = hshm.try_pop_buffer_into(0, mem_size).unwrap();
2401            assert_eq!(result.0, data);
2402        }
2403
2404        #[test]
2405        fn malicious_flatbuffer_size_prefix() {
2406            let mem_size = page_size::get();
2407            let mut hshm = make_buffer(mem_size);
2408
2409            let payload = b"small";
2410            let mut data = Vec::new();
2411            data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
2412            data.extend_from_slice(payload);
2413            hshm.push_buffer(0, mem_size, &data).unwrap();
2414
2415            // Corrupt size prefix at element start (offset 8) to near u32::MAX.
2416            hshm.write::<u32>(8, 0xFFFF_FFFBu32).unwrap(); // +4 = 0xFFFF_FFFF
2417
2418            let result: Result<RawBytes> = hshm.try_pop_buffer_into(0, mem_size);
2419            let err_msg = format!("{}", result.unwrap_err());
2420            assert!(
2421                err_msg.contains("Corrupt buffer size prefix: flatbuffer claims 4294967295 bytes but the element slot is only 9 bytes"),
2422                "Unexpected error message: {}",
2423                err_msg
2424            );
2425        }
2426
2427        #[test]
2428        fn malicious_element_offset_too_small() {
2429            let mem_size = page_size::get();
2430            let mut hshm = make_buffer(mem_size);
2431
2432            let payload = b"test";
2433            let mut data = Vec::new();
2434            data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
2435            data.extend_from_slice(payload);
2436            hshm.push_buffer(0, mem_size, &data).unwrap();
2437
2438            // Corrupt back-pointer (offset 16) to 0 (before valid range).
2439            hshm.write::<u64>(16, 0u64).unwrap();
2440
2441            let result: Result<RawBytes> = hshm.try_pop_buffer_into(0, mem_size);
2442            let err_msg = format!("{}", result.unwrap_err());
2443            assert!(
2444                err_msg.contains(
2445                    "Corrupt buffer back-pointer: element offset 0 is outside valid range [8, 8]"
2446                ),
2447                "Unexpected error message: {}",
2448                err_msg
2449            );
2450        }
2451
2452        #[test]
2453        fn malicious_element_offset_past_stack_pointer() {
2454            let mem_size = page_size::get();
2455            let mut hshm = make_buffer(mem_size);
2456
2457            let payload = b"test";
2458            let mut data = Vec::new();
2459            data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
2460            data.extend_from_slice(payload);
2461            hshm.push_buffer(0, mem_size, &data).unwrap();
2462
2463            // Corrupt back-pointer (offset 16) to 9999 (past stack pointer 24).
2464            hshm.write::<u64>(16, 9999u64).unwrap();
2465
2466            let result: Result<RawBytes> = hshm.try_pop_buffer_into(0, mem_size);
2467            let err_msg = format!("{}", result.unwrap_err());
2468            assert!(
2469                err_msg.contains(
2470                    "Corrupt buffer back-pointer: element offset 9999 is outside valid range [8, 8]"
2471                ),
2472                "Unexpected error message: {}",
2473                err_msg
2474            );
2475        }
2476
2477        #[test]
2478        fn malicious_flatbuffer_size_off_by_one() {
2479            let mem_size = page_size::get();
2480            let mut hshm = make_buffer(mem_size);
2481
2482            let payload = b"abcd";
2483            let mut data = Vec::new();
2484            data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
2485            data.extend_from_slice(payload);
2486            hshm.push_buffer(0, mem_size, &data).unwrap();
2487
2488            // Corrupt size prefix: claim 5 bytes (total 9), exceeding the 8-byte slot.
2489            hshm.write::<u32>(8, 5u32).unwrap(); // fb_buffer_size = 5 + 4 = 9
2490
2491            let result: Result<RawBytes> = hshm.try_pop_buffer_into(0, mem_size);
2492            let err_msg = format!("{}", result.unwrap_err());
2493            assert!(
2494                err_msg.contains("Corrupt buffer size prefix: flatbuffer claims 9 bytes but the element slot is only 8 bytes"),
2495                "Unexpected error message: {}",
2496                err_msg
2497            );
2498        }
2499
2500        /// Back-pointer just below stack_pointer causes underflow in
2501        /// `stack_pointer_rel - last_element_offset_rel - 8`.
2502        #[test]
2503        fn back_pointer_near_stack_pointer_underflow() {
2504            let mem_size = page_size::get();
2505            let mut hshm = make_buffer(mem_size);
2506
2507            let payload = b"test";
2508            let mut data = Vec::new();
2509            data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
2510            data.extend_from_slice(payload);
2511            hshm.push_buffer(0, mem_size, &data).unwrap();
2512
2513            // stack_pointer_rel = 24. Set back-pointer to 23 (> 24 - 16 = 8, so rejected).
2514            hshm.write::<u64>(16, 23u64).unwrap();
2515
2516            let result: Result<RawBytes> = hshm.try_pop_buffer_into(0, mem_size);
2517            let err_msg = format!("{}", result.unwrap_err());
2518            assert!(
2519                err_msg.contains(
2520                    "Corrupt buffer back-pointer: element offset 23 is outside valid range [8, 8]"
2521                ),
2522                "Unexpected error message: {}",
2523                err_msg
2524            );
2525        }
2526
2527        /// Size prefix of 0xFFFF_FFFD causes u32 overflow: 0xFFFF_FFFD + 4 wraps.
2528        #[test]
2529        fn size_prefix_u32_overflow() {
2530            let mem_size = page_size::get();
2531            let mut hshm = make_buffer(mem_size);
2532
2533            let payload = b"test";
2534            let mut data = Vec::new();
2535            data.extend_from_slice(&(payload.len() as u32).to_le_bytes());
2536            data.extend_from_slice(payload);
2537            hshm.push_buffer(0, mem_size, &data).unwrap();
2538
2539            // Write 0xFFFF_FFFD as size prefix: checked_add(4) returns None.
2540            hshm.write::<u32>(8, 0xFFFF_FFFDu32).unwrap();
2541
2542            let result: Result<RawBytes> = hshm.try_pop_buffer_into(0, mem_size);
2543            let err_msg = format!("{}", result.unwrap_err());
2544            assert!(
2545                err_msg.contains("Corrupt buffer size prefix: value 4294967293 overflows when adding 4-byte header"),
2546                "Unexpected error message: {}",
2547                err_msg
2548            );
2549        }
2550    }
2551
2552    #[cfg(target_os = "linux")]
2553    mod guard_page_crash_test {
2554        use crate::mem::shared_mem::{ExclusiveSharedMemory, SharedMemory};
2555
2556        const TEST_EXIT_CODE: u8 = 211; // an uncommon exit code, used for testing purposes
2557
2558        /// hook sigsegv to exit with status code, to make it testable, rather than have it exit from a signal
2559        /// NOTE: We CANNOT panic!() in the handler, and make the tests #[should_panic], because
2560        ///     the test harness process will crash anyway after the test passes
2561        fn setup_signal_handler() {
2562            unsafe {
2563                signal_hook_registry::register_signal_unchecked(libc::SIGSEGV, || {
2564                    std::process::exit(TEST_EXIT_CODE.into());
2565                })
2566                .unwrap();
2567            }
2568        }
2569
2570        #[test]
2571        #[ignore] // this test is ignored because it will crash the running process
2572        fn read() {
2573            setup_signal_handler();
2574
2575            let eshm = ExclusiveSharedMemory::new(page_size::get()).unwrap();
2576            let (hshm, _) = eshm.build();
2577            let guard_page_ptr = hshm.raw_ptr();
2578            unsafe { std::ptr::read_volatile(guard_page_ptr) };
2579        }
2580
2581        #[test]
2582        #[ignore] // this test is ignored because it will crash the running process
2583        fn write() {
2584            setup_signal_handler();
2585
2586            let eshm = ExclusiveSharedMemory::new(page_size::get()).unwrap();
2587            let (hshm, _) = eshm.build();
2588            let guard_page_ptr = hshm.raw_ptr();
2589            unsafe { std::ptr::write_volatile(guard_page_ptr, 0u8) };
2590        }
2591
2592        #[test]
2593        #[ignore] // this test is ignored because it will crash the running process
2594        fn exec() {
2595            setup_signal_handler();
2596
2597            let eshm = ExclusiveSharedMemory::new(page_size::get()).unwrap();
2598            let (hshm, _) = eshm.build();
2599            let guard_page_ptr = hshm.raw_ptr();
2600            let func: fn() = unsafe { std::mem::transmute(guard_page_ptr) };
2601            func();
2602        }
2603
2604        // provides a way for running the above tests in a separate process since they expect to crash
2605        #[test]
2606        #[cfg_attr(miri, ignore)] // miri can't spawn subprocesses
2607        fn guard_page_testing_shim() {
2608            let tests = vec!["read", "write", "exec"];
2609            for test in tests {
2610                let triple = std::env::var("TARGET_TRIPLE").ok();
2611                let target_args = if let Some(triple) = triple.filter(|t| !t.is_empty()) {
2612                    vec!["--target".to_string(), triple.to_string()]
2613                } else {
2614                    vec![]
2615                };
2616                let output = std::process::Command::new("cargo")
2617                    .args(["test", "-p", "hyperlight-host", "--lib"])
2618                    .args(target_args)
2619                    .args(["--", "--ignored", test])
2620                    .stdin(std::process::Stdio::null())
2621                    .output()
2622                    .expect("Unable to launch tests");
2623                let exit_code = output.status.code();
2624                if exit_code != Some(TEST_EXIT_CODE.into()) {
2625                    eprintln!("=== Guard Page test '{}' failed ===", test);
2626                    eprintln!("Exit code: {:?} (expected {})", exit_code, TEST_EXIT_CODE);
2627                    eprintln!("=== STDOUT ===");
2628                    eprintln!("{}", String::from_utf8_lossy(&output.stdout));
2629                    eprintln!("=== STDERR ===");
2630                    eprintln!("{}", String::from_utf8_lossy(&output.stderr));
2631                    panic!(
2632                        "Guard Page test failed: {} (exit code {:?}, expected {})",
2633                        test, exit_code, TEST_EXIT_CODE
2634                    );
2635                }
2636            }
2637        }
2638    }
2639
2640    #[cfg(not(miri))]
2641    mod from_file_tests {
2642        use std::io::Write;
2643
2644        use tempfile::NamedTempFile;
2645
2646        use crate::mem::shared_mem::{ReadonlySharedMemory, SharedMemory};
2647
2648        pub(super) fn make_temp_file(len: usize) -> NamedTempFile {
2649            let mut f = NamedTempFile::new().expect("create temp file");
2650            if len > 0 {
2651                let mut buf = vec![0u8; len];
2652                for (i, b) in buf.iter_mut().enumerate() {
2653                    *b = (i & 0xff) as u8;
2654                }
2655                f.write_all(&buf).expect("write temp file");
2656                f.flush().expect("flush temp file");
2657            }
2658            f
2659        }
2660
2661        #[test]
2662        fn from_file_success_single_page() {
2663            let tmp = make_temp_file(page_size::get());
2664            let mut rsm = ReadonlySharedMemory::from_file(tmp.as_file(), page_size::get())
2665                .expect("from_file should succeed");
2666            assert_eq!(rsm.mem_size(), page_size::get());
2667            rsm.with_contents(|slice| {
2668                for (i, b) in slice.iter().enumerate() {
2669                    assert_eq!(*b, (i & 0xff) as u8);
2670                }
2671            })
2672            .expect("with_contents should succeed");
2673        }
2674
2675        #[test]
2676        fn from_file_success_smaller_guest_mapped_size() {
2677            let tmp = make_temp_file(2 * page_size::get());
2678            let rsm = ReadonlySharedMemory::from_file(tmp.as_file(), page_size::get())
2679                .expect("from_file should succeed");
2680            assert_eq!(rsm.mem_size(), 2 * page_size::get());
2681        }
2682
2683        #[test]
2684        fn from_file_rejects_empty_file() {
2685            let tmp = make_temp_file(0);
2686            let err = ReadonlySharedMemory::from_file(tmp.as_file(), page_size::get())
2687                .expect_err("empty file should be rejected");
2688            assert!(format!("{}", err).contains("0x0 < 0x0"));
2689        }
2690
2691        #[test]
2692        fn from_file_rejects_unaligned_file_length() {
2693            let tmp = make_temp_file(page_size::get() + 1);
2694            let err = ReadonlySharedMemory::from_file(tmp.as_file(), page_size::get())
2695                .expect_err("unaligned file length should be rejected");
2696            assert!(format!("{}", err).contains(&format!(
2697                "0x{:x} % 0x{:x} = 0",
2698                page_size::get() + 1,
2699                page_size::get()
2700            )));
2701        }
2702
2703        #[test]
2704        fn from_file_rejects_zero_guest_mapped_size() {
2705            let tmp = make_temp_file(page_size::get());
2706            let err = ReadonlySharedMemory::from_file(tmp.as_file(), 0)
2707                .expect_err("zero guest_mapped_size should be rejected");
2708            assert!(format!("{}", err).contains("0x0 < 0x0"));
2709        }
2710
2711        #[test]
2712        fn from_file_rejects_unaligned_guest_mapped_size() {
2713            let tmp = make_temp_file(2 * page_size::get());
2714            let err = ReadonlySharedMemory::from_file(tmp.as_file(), page_size::get() + 1)
2715                .expect_err("unaligned guest_mapped_size should be rejected");
2716            assert!(format!("{}", err).contains(&format!(
2717                "0x{:x} % 0x{:x} = ",
2718                page_size::get() + 1,
2719                page_size::get()
2720            )));
2721        }
2722
2723        #[test]
2724        fn from_file_rejects_guest_mapped_size_exceeding_file() {
2725            let tmp = make_temp_file(page_size::get());
2726            let err = ReadonlySharedMemory::from_file(tmp.as_file(), 2 * page_size::get())
2727                .expect_err("guest_mapped_size > file length should be rejected");
2728            assert!(format!("{}", err).contains(&format!(
2729                "0x{:x} <= 0x{:x}",
2730                2 * page_size::get(),
2731                page_size::get()
2732            )));
2733        }
2734
2735        /// Tests in this submodule are `#[ignore]`'d because each one
2736        /// is expected to die from a memory access violation. They are
2737        /// not run by a plain `cargo test`. The `from_file_guard_page_shim`
2738        /// test in the parent module re-executes each one in a subprocess
2739        /// and asserts the trap occurred.
2740        mod guard_page_crash_tests {
2741            use super::make_temp_file;
2742            use crate::mem::shared_mem::{ReadonlySharedMemory, SharedMemory};
2743
2744            /// Loads from the byte immediately before the mapping.
2745            #[test]
2746            #[ignore]
2747            pub(super) fn leading_guard_page_traps() {
2748                let tmp = make_temp_file(page_size::get());
2749                let rsm = ReadonlySharedMemory::from_file(tmp.as_file(), page_size::get())
2750                    .expect("from_file should succeed");
2751                let guard_ptr = unsafe { rsm.base_ptr().sub(page_size::get()) };
2752                println!("reached_guard");
2753                let _ = unsafe { std::ptr::read_volatile(guard_ptr) };
2754                println!("survived_guard");
2755            }
2756
2757            /// Loads from the byte immediately after the mapping.
2758            #[test]
2759            #[ignore]
2760            pub(super) fn trailing_guard_page_traps() {
2761                let tmp = make_temp_file(page_size::get());
2762                let rsm = ReadonlySharedMemory::from_file(tmp.as_file(), page_size::get())
2763                    .expect("from_file should succeed");
2764                let guard_ptr = unsafe { rsm.base_ptr().add(rsm.mem_size()) };
2765                println!("reached_guard");
2766                let _ = unsafe { std::ptr::read_volatile(guard_ptr) };
2767                println!("survived_guard");
2768            }
2769        }
2770
2771        /// Spawn each ignored guard-page test as a subprocess and assert
2772        /// it terminated by an OS access violation. Re-executes the
2773        /// current test binary so no rebuild or `cargo` invocation is
2774        /// needed.
2775        #[test]
2776        #[cfg_attr(miri, ignore)] // miri can't spawn subprocesses
2777        fn from_file_guard_page_shim() {
2778            use guard_page_crash_tests::{leading_guard_page_traps, trailing_guard_page_traps};
2779            let ignored_test_paths = [
2780                test_path(leading_guard_page_traps),
2781                test_path(trailing_guard_page_traps),
2782            ];
2783
2784            let exe = std::env::current_exe().expect("current_exe");
2785            for path in &ignored_test_paths {
2786                run_guard_page_subprocess(&exe, path);
2787            }
2788        }
2789
2790        /// Derive a libtest filter path for a test function from its
2791        /// type name. Strips the leading crate-name segment that
2792        /// `type_name` includes but libtest does not.
2793        fn test_path<F: Fn()>(_: F) -> &'static str {
2794            let full = std::any::type_name::<F>();
2795            let (_, rest) = full
2796                .split_once("::")
2797                .expect("type_name of a function item is always qualified by the crate name");
2798            rest
2799        }
2800
2801        fn run_guard_page_subprocess(exe: &std::path::Path, ignored_test_path: &str) {
2802            let output = std::process::Command::new(exe)
2803                .args([
2804                    "--ignored",
2805                    "--nocapture",
2806                    "--exact",
2807                    "--test-threads=1",
2808                    ignored_test_path,
2809                ])
2810                .stdin(std::process::Stdio::null())
2811                .output()
2812                .expect("Unable to launch subprocess test");
2813
2814            let stdout = String::from_utf8_lossy(&output.stdout);
2815            let stderr = String::from_utf8_lossy(&output.stderr);
2816
2817            // libtest with no matching filter exits 0, so verify the
2818            // test actually ran via the "running 1 test" banner.
2819            let ran_test = stdout.contains("running 1 test");
2820            let reached = stdout.contains("reached_guard");
2821            let survived = stdout.contains("survived_guard");
2822            let by_access_violation = killed_by_access_violation(&output.status);
2823
2824            let ok = reached && !survived && by_access_violation && ran_test;
2825            if !ok {
2826                eprintln!("=== Guard page shim failed for {} ===", ignored_test_path);
2827                eprintln!(
2828                    "status={:?} ran_test={} reached={} survived={} by_access_violation={}",
2829                    output.status, ran_test, reached, survived, by_access_violation
2830                );
2831                eprintln!("=== STDOUT ===\n{}", stdout);
2832                eprintln!("=== STDERR ===\n{}", stderr);
2833                let hint = if !ran_test {
2834                    format!(
2835                        "\nHINT: ran_test=false (subprocess reported 'running 0 tests'). \
2836                         Most likely cause is a stale test path in the shim. Verify that \
2837                         `{}` still exists and matches the path passed via --exact above.",
2838                        ignored_test_path
2839                    )
2840                } else {
2841                    String::new()
2842                };
2843                panic!(
2844                    "Expected subprocess to run {}, print 'reached_guard', \
2845                     then die from a memory access fault. ran_test={}, reached={}, \
2846                     survived={}, by_access_violation={}, status={:?}{}",
2847                    ignored_test_path,
2848                    ran_test,
2849                    reached,
2850                    survived,
2851                    by_access_violation,
2852                    output.status,
2853                    hint
2854                );
2855            }
2856
2857            println!(
2858                "guard page trap confirmed for {}: subprocess terminated with {:?}",
2859                ignored_test_path, output.status
2860            );
2861        }
2862
2863        /// Returns true if `status` indicates the process died from a
2864        /// memory access fault (SIGBUS on macos, SIGSEGV on linux,
2865        /// STATUS_ACCESS_VIOLATION (or 0xDEAD) on Windows).
2866        fn killed_by_access_violation(status: &std::process::ExitStatus) -> bool {
2867            #[cfg(unix)]
2868            {
2869                use std::os::unix::process::ExitStatusExt;
2870                let expected_signal = if cfg!(target_os = "macos") {
2871                    libc::SIGBUS
2872                } else {
2873                    libc::SIGSEGV
2874                };
2875                status.signal() == Some(expected_signal)
2876            }
2877            #[cfg(windows)]
2878            {
2879                use windows::Win32::Foundation::STATUS_ACCESS_VIOLATION;
2880                // See https://github.com/hyperlight-dev/hyperlight/issues/1507
2881                status.code() == Some(STATUS_ACCESS_VIOLATION.0) || status.code() == Some(0xDEAD)
2882            }
2883        }
2884    }
2885}