Skip to main content

hyperlight_host/mem/
shared_mem.rs

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