nub_host_kvm/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};
21use std::ptr::null_mut;
22use std::sync::{Arc, RwLock};
23
24use nub_host_common::mem::PAGE_SIZE_USIZE;
25use tracing::{Span, instrument};
26
27use super::memory_region::{
28 HostGuestMemoryRegion, MemoryRegion, MemoryRegionFlags, MemoryRegionKind, MemoryRegionType,
29};
30use crate::{HyperlightError, Result, log_then_return, new_error};
31
32/// Makes sure that the given `offset` and `size` are within the bounds of the memory with size `mem_size`.
33macro_rules! bounds_check {
34 ($offset:expr, $size:expr, $mem_size:expr) => {
35 if $offset.checked_add($size).is_none_or(|end| end > $mem_size) {
36 return Err(new_error!(
37 "Cannot read value from offset {} with size {} in memory of size {}",
38 $offset,
39 $size,
40 $mem_size
41 ));
42 }
43 };
44}
45
46/// generates a reader function for the given type
47macro_rules! generate_reader {
48 ($fname:ident, $ty:ty) => {
49 /// Read a value of type `$ty` from the memory at the given offset.
50 #[allow(dead_code)]
51 #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
52 pub(crate) fn $fname(&self, offset: usize) -> Result<$ty> {
53 let data = self.as_slice();
54 bounds_check!(offset, std::mem::size_of::<$ty>(), data.len());
55 Ok(<$ty>::from_le_bytes(
56 data[offset..offset + std::mem::size_of::<$ty>()].try_into()?,
57 ))
58 }
59 };
60}
61
62/// generates a writer function for the given type
63macro_rules! generate_writer {
64 ($fname:ident, $ty:ty) => {
65 /// Write a value of type `$ty` to the memory at the given offset.
66 #[allow(dead_code)]
67 pub(crate) fn $fname(&mut self, offset: usize, value: $ty) -> Result<()> {
68 let data = self.as_mut_slice();
69 bounds_check!(offset, std::mem::size_of::<$ty>(), data.len());
70 data[offset..offset + std::mem::size_of::<$ty>()].copy_from_slice(&value.to_le_bytes());
71 Ok(())
72 }
73 };
74}
75
76/// A representation of a host mapping of a shared memory region,
77/// which will be released when this structure is Drop'd. This is not
78/// individually Clone (since it holds ownership of the mapping), or
79/// Send or Sync, since it doesn't ensure any particular synchronization.
80#[derive(Debug)]
81pub struct HostMapping {
82 ptr: *mut u8,
83 size: usize,
84}
85
86impl Drop for HostMapping {
87 fn drop(&mut self) {
88 use libc::munmap;
89
90 unsafe {
91 munmap(self.ptr as *mut c_void, self.size);
92 }
93 }
94}
95
96/// These three structures represent various phases of the lifecycle of
97/// a memory buffer that is shared with the guest. An
98/// ExclusiveSharedMemory is used for certain operations that
99/// unrestrictedly write to the shared memory, including setting it up
100/// and taking snapshots.
101#[derive(Debug)]
102pub struct ExclusiveSharedMemory {
103 region: Arc<HostMapping>,
104}
105unsafe impl Send for ExclusiveSharedMemory {}
106
107/// A GuestSharedMemory is used to represent
108/// the reference to all-of-memory that is taken by the virtual cpu.
109/// Because of the memory model limitations that affect
110/// HostSharedMemory, it is likely fairly important (to ensure that
111/// our UB remains limited to interaction with an external compilation
112/// unit that likely can't be discovered by the compiler) that _rust_
113/// users do not perform racy accesses to the guest communication
114/// buffers that are also accessed by HostSharedMemory.
115#[derive(Debug)]
116pub struct GuestSharedMemory {
117 region: Arc<HostMapping>,
118 /// The lock that indicates this shared memory is being used by non-Rust code
119 ///
120 /// This lock _must_ be held whenever the guest is executing,
121 /// because it prevents the host from converting its
122 /// HostSharedMemory to an ExclusiveSharedMemory. Since the guest
123 /// may arbitrarily mutate the shared memory, only synchronized
124 /// accesses from Rust should be allowed!
125 ///
126 /// We cannot enforce this in the type system, because the memory
127 /// is mapped in to the VM at VM creation time.
128 pub lock: Arc<RwLock<()>>,
129}
130// SAFETY: `GuestSharedMemory` is the KVM-facing handle for a mapping whose
131// lifetime is owned by `Arc<HostMapping>`. The wrapper exposes no direct Rust
132// slice access except through `SharedMemory::with_exclusivity`, which takes the
133// shared RwLock's write side and therefore excludes host-side volatile access
134// while exclusive remapping/snapshotting is in progress. Sharing this handle
135// between host worker threads only shares the registered mapping/lifetime
136// handle; synchronized host reads/writes still go through `HostSharedMemory`.
137unsafe impl Send for GuestSharedMemory {}
138unsafe impl Sync for GuestSharedMemory {}
139
140/// A HostSharedMemory allows synchronized accesses to guest
141/// communication buffers, allowing it to be used concurrently with a
142/// GuestSharedMemory.
143///
144/// # Concurrency model
145///
146/// Given future requirements for asynchronous I/O with a minimum
147/// amount of copying (e.g. WASIp3 streams), we would like it to be
148/// possible to safely access these buffers concurrently with the
149/// guest, ensuring that (1) data is read appropriately if the guest
150/// is well-behaved; and (2) the host's behaviour is defined
151/// regardless of whether or not the guest is well-behaved.
152///
153/// The ideal (future) flow for a guest->host message is something like
154/// - Guest writes (unordered) bytes describing a work item into a buffer
155/// - Guest reveals buffer via a release-store of a pointer into an
156/// MMIO ring-buffer
157/// - Host acquire-loads the buffer pointer from the "MMIO" ring
158/// buffer
159/// - Host (unordered) reads the bytes from the buffer
160/// - Host performs validation of those bytes and uses them
161///
162/// Unfortunately, there appears to be no way to do this with defined
163/// behaviour in present Rust (see
164/// e.g. <https://github.com/rust-lang/unsafe-code-guidelines/issues/152>).
165/// Rust does not yet have its own defined memory model, but in the
166/// interim, it is widely treated as inheriting the current C/C++
167/// memory models. The most immediate problem is that regardless of
168/// anything else, under those memory models \[1, p. 17-18; 2, p. 88\],
169///
170/// > The execution of a program contains a _data race_ if it
171/// > contains two [C++23: "potentially concurrent"] conflicting
172/// > actions [C23: "in different threads"], at least one of which
173/// > is not atomic, and neither happens before the other [C++23: ",
174/// > except for the special case for signal handlers described
175/// > below"]. Any such data race results in undefined behavior.
176///
177/// Consequently, if a misbehaving guest fails to correctly
178/// synchronize its stores with the host, the host's innocent loads
179/// will trigger undefined behaviour for the entire program, including
180/// the host. Note that this also applies if the guest makes an
181/// unsynchronized read of a location that the host is writing!
182///
183/// Despite Rust's de jure inheritance of the C memory model at the
184/// present time, the compiler in many cases de facto adheres to LLVM
185/// semantics, so it is worthwhile to consider what LLVM does in this
186/// case as well. According to the the LangRef \[3\] memory model,
187/// loads which are involved in a race that includes at least one
188/// non-atomic access (whether the load or a store) return `undef`,
189/// making them roughly equivalent to reading uninitialized
190/// memory. While this is much better, it is still bad.
191///
192/// Considering a different direction, recent C++ papers have seemed
193/// to lean towards using `volatile` for similar use cases. For
194/// example, in P1152R0 \[4\], JF Bastien notes that
195///
196/// > We’ve shown that volatile is purposely defined to denote
197/// > external modifications. This happens for:
198/// > - Shared memory with untrusted code, where volatile is the
199/// > right way to avoid time-of-check time-of-use (ToCToU)
200/// > races which lead to security bugs such as \[PWN2OWN\] and
201/// > \[XENXSA155\].
202///
203/// Unfortunately, although this paper was adopted for C++20 (and,
204/// sadly, mostly un-adopted for C++23, although that does not concern
205/// us), the paper did not actually redefine volatile accesses or data
206/// races to prevent volatile accesses from racing with other accesses
207/// and causing undefined behaviour. P1382R1 \[5\] would have amended
208/// the wording of the data race definition to specifically exclude
209/// volatile, but, unfortunately, despite receiving a
210/// generally-positive reception at its first WG21 meeting more than
211/// five years ago, it has not progressed.
212///
213/// Separately from the data race issue, there is also a concern that
214/// according to the various memory models in use, there may be ways
215/// in which the guest can semantically obtain uninitialized memory
216/// and write it into the shared buffer, which may also result in
217/// undefined behaviour on reads. The degree to which this is a
218/// concern is unclear, however, since it is unclear to what degree
219/// the Rust abstract machine's conception of uninitialized memory
220/// applies to the sandbox. Returning briefly to the LLVM level,
221/// rather than the Rust level, this, combined with the fact that
222/// racing loads in LLVM return `undef`, as discussed above, we would
223/// ideally `llvm.freeze` the result of any load out of the sandbox.
224///
225/// It would furthermore be ideal if we could run the flatbuffers
226/// parsing code directly on the guest memory, in order to avoid
227/// unnecessary copies. That is unfortunately probably not viable at
228/// the present time: because the generated flatbuffers parsing code
229/// doesn't use atomic or volatile accesses, it is likely to introduce
230/// double-read vulnerabilities.
231///
232/// In short, none of the Rust-level operations available to us do the
233/// right thing, at the Rust spec level or the LLVM spec level. Our
234/// major remaining options are therefore:
235/// - Choose one of the options that is available to us, and accept
236/// that we are doing something unsound according to the spec, but
237/// hope that no reasonable compiler could possibly notice.
238/// - Use inline assembly per architecture, for which we would only
239/// need to worry about the _architecture_'s memory model (which
240/// is far less demanding).
241///
242/// The leading candidate for the first option would seem to be to
243/// simply use volatile accesses; there seems to be wide agreement
244/// that this _should_ be a valid use case for them (even if it isn't
245/// now), and projects like Linux and rust-vmm already use C11
246/// `volatile` for this purpose. It is also worth noting that because
247/// we still do need to synchronize with the guest when it _is_ being
248/// well-behaved, we would ideally use volatile acquire loads and
249/// volatile release stores for interacting with the stack pointer in
250/// the guest in this case. Unfortunately, while those operations are
251/// defined in LLVM, they are not presently exposed to Rust. While
252/// atomic fences that are not associated with memory accesses
253/// ([`std::sync::atomic::fence`]) might at first glance seem to help with
254/// this problem, they unfortunately do not \[6\]:
255///
256/// > A fence ‘A’ which has (at least) Release ordering semantics,
257/// > synchronizes with a fence ‘B’ with (at least) Acquire
258/// > semantics, if and only if there exist operations X and Y,
259/// > both operating on some atomic object ‘M’ such that A is
260/// > sequenced before X, Y is sequenced before B and Y observes
261/// > the change to M. This provides a happens-before dependence
262/// > between A and B.
263///
264/// Note that the X and Y must be to an _atomic_ object.
265///
266/// We consequently assume that there has been a strong architectural
267/// fence on a vmenter/vmexit between data being read and written.
268/// This is unsafe (not guaranteed in the type system)!
269///
270/// \[1\] N3047 C23 Working Draft. <https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3047.pdf>
271/// \[2\] N4950 C++23 Working Draft. <https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/n4950.pdf>
272/// \[3\] LLVM Language Reference Manual, Memory Model for Concurrent Operations. <https://llvm.org/docs/LangRef.html#memmodel>
273/// \[4\] P1152R0: Deprecating `volatile`. JF Bastien. <https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1152r0.html>
274/// \[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>
275/// \[6\] Documentation for std::sync::atomic::fence. <https://doc.rust-lang.org/std/sync/atomic/fn.fence.html>
276#[derive(Clone, Debug)]
277pub struct HostSharedMemory {
278 region: Arc<HostMapping>,
279 lock: Arc<RwLock<()>>,
280}
281unsafe impl Send for HostSharedMemory {}
282
283impl ExclusiveSharedMemory {
284 /// Create a new region of shared memory with the given minimum
285 /// size in bytes. The region will be surrounded by guard pages.
286 ///
287 /// Return `Err` if shared memory could not be allocated.
288 #[instrument(skip_all, parent = Span::current(), level= "Trace")]
289 pub fn new(min_size_bytes: usize) -> Result<Self> {
290 use libc::{
291 MAP_ANONYMOUS, MAP_FAILED, MAP_PRIVATE, PROT_READ, PROT_WRITE, c_int, mmap, off_t,
292 size_t,
293 };
294 #[cfg(not(miri))]
295 use libc::{MAP_NORESERVE, PROT_NONE, mprotect};
296
297 if min_size_bytes == 0 {
298 return Err(new_error!("Cannot create shared memory with size 0"));
299 }
300
301 let total_size = min_size_bytes
302 .checked_add(2 * PAGE_SIZE_USIZE) // guard page around the memory
303 .ok_or_else(|| new_error!("Memory required for sandbox exceeded usize::MAX"))?;
304
305 if total_size % PAGE_SIZE_USIZE != 0 {
306 return Err(new_error!(
307 "shared memory must be a multiple of {}",
308 PAGE_SIZE_USIZE
309 ));
310 }
311
312 // usize and isize are guaranteed to be the same size, and
313 // isize::MAX should be positive, so this cast should be safe.
314 if total_size > isize::MAX as usize {
315 return Err(HyperlightError::MemoryRequestTooBig(
316 total_size,
317 isize::MAX as usize,
318 ));
319 }
320
321 // allocate the memory
322 #[cfg(not(miri))]
323 let flags = MAP_ANONYMOUS | MAP_PRIVATE | MAP_NORESERVE;
324 #[cfg(miri)]
325 let flags = MAP_ANONYMOUS | MAP_PRIVATE;
326
327 let addr = unsafe {
328 mmap(
329 null_mut(),
330 total_size as size_t,
331 PROT_READ | PROT_WRITE,
332 flags,
333 -1 as c_int,
334 0 as off_t,
335 )
336 };
337 if addr == MAP_FAILED {
338 log_then_return!(HyperlightError::MmapFailed(
339 Error::last_os_error().raw_os_error()
340 ));
341 }
342
343 // protect the guard pages
344 #[cfg(not(miri))]
345 {
346 let res = unsafe { mprotect(addr, PAGE_SIZE_USIZE, PROT_NONE) };
347 if res != 0 {
348 return Err(HyperlightError::MprotectFailed(
349 Error::last_os_error().raw_os_error(),
350 ));
351 }
352 let res = unsafe {
353 mprotect(
354 (addr as *const u8).add(total_size - PAGE_SIZE_USIZE) as *mut c_void,
355 PAGE_SIZE_USIZE,
356 PROT_NONE,
357 )
358 };
359 if res != 0 {
360 return Err(HyperlightError::MprotectFailed(
361 Error::last_os_error().raw_os_error(),
362 ));
363 }
364 }
365
366 Ok(Self {
367 // HostMapping is only non-Send/Sync because raw pointers
368 // are not ("as a lint", as the Rust docs say). We don't
369 // want to mark HostMapping Send/Sync immediately, because
370 // that could socially imply that it's "safe" to use
371 // unsafe accesses from multiple threads at once. Instead, we
372 // directly impl Send and Sync on this type. Since this
373 // type does have Send and Sync manually impl'd, the Arc
374 // is not pointless as the lint suggests.
375 #[allow(clippy::arc_with_non_send_sync)]
376 region: Arc::new(HostMapping {
377 ptr: addr as *mut u8,
378 size: total_size,
379 }),
380 })
381 }
382
383 /// Internal helper method to get the backing memory as a mutable slice.
384 ///
385 /// # Safety
386 /// As per std::slice::from_raw_parts_mut:
387 /// - self.base_addr() must be valid for both reads and writes for
388 /// self.mem_size() * mem::size_of::<u8>() many bytes, and it
389 /// must be properly aligned.
390 ///
391 /// The rules on validity are still somewhat unspecified, but we
392 /// assume that the result of our calls to mmap/CreateFileMappings may
393 /// be considered a single "allocated object". The use of
394 /// non-atomic accesses is alright from a Safe Rust standpoint,
395 /// because SharedMemoryBuilder is not Sync.
396 /// - self.base_addr() must point to self.mem_size() consecutive
397 /// properly initialized values of type u8
398 ///
399 /// Again, the exact provenance restrictions on what is
400 /// considered to be initialized values are unclear, but we make
401 /// sure to use mmap(MAP_ANONYMOUS) and
402 /// CreateFileMapping(SEC_COMMIT), so the pages in question are
403 /// zero-initialized, which we hope counts for u8.
404 /// - The memory referenced by the returned slice must not be
405 /// accessed through any other pointer (not derived from the
406 /// return value) for the duration of the lifetime 'a. Both read
407 /// and write accesses are forbidden.
408 ///
409 /// Accesses from Safe Rust necessarily follow this rule,
410 /// because the returned slice's lifetime is the same as that of
411 /// a mutable borrow of self.
412 /// - The total size self.mem_size() * mem::size_of::<u8>() of the
413 /// slice must be no larger than isize::MAX, and adding that
414 /// size to data must not "wrap around" the address space. See
415 /// the safety documentation of pointer::offset.
416 ///
417 /// This is ensured by a check in ::new()
418 pub(super) fn as_mut_slice(&mut self) -> &mut [u8] {
419 unsafe { std::slice::from_raw_parts_mut(self.base_ptr(), self.mem_size()) }
420 }
421
422 /// Internal helper method to get the backing memory as a slice.
423 ///
424 /// # Safety
425 /// See the discussion on as_mut_slice, with the third point
426 /// replaced by:
427 /// - The memory referenced by the returned slice must not be
428 /// mutated for the duration of lifetime 'a, except inside an
429 /// UnsafeCell.
430 ///
431 /// Host accesses from Safe Rust necessarily follow this rule,
432 /// because the returned slice's lifetime is the same as that of
433 /// a borrow of self, preventing mutations via other methods.
434 #[instrument(skip_all, parent = Span::current(), level= "Trace")]
435 pub fn as_slice<'a>(&'a self) -> &'a [u8] {
436 unsafe { std::slice::from_raw_parts(self.base_ptr(), self.mem_size()) }
437 }
438
439 /// Copies all bytes from `src` to `self` starting at offset
440 #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
441 pub fn copy_from_slice(&mut self, src: &[u8], offset: usize) -> Result<()> {
442 let data = self.as_mut_slice();
443 bounds_check!(offset, src.len(), data.len());
444 data[offset..offset + src.len()].copy_from_slice(src);
445 Ok(())
446 }
447
448 generate_reader!(read_u8, u8);
449 generate_reader!(read_i8, i8);
450 generate_reader!(read_u16, u16);
451 generate_reader!(read_i16, i16);
452 generate_reader!(read_u32, u32);
453 generate_reader!(read_i32, i32);
454 generate_reader!(read_u64, u64);
455 generate_reader!(read_i64, i64);
456 generate_reader!(read_usize, usize);
457 generate_reader!(read_isize, isize);
458
459 generate_writer!(write_u8, u8);
460 generate_writer!(write_i8, i8);
461 generate_writer!(write_u16, u16);
462 generate_writer!(write_i16, i16);
463 generate_writer!(write_u32, u32);
464 generate_writer!(write_i32, i32);
465 generate_writer!(write_u64, u64);
466 generate_writer!(write_i64, i64);
467 generate_writer!(write_usize, usize);
468 generate_writer!(write_isize, isize);
469
470 /// Convert the ExclusiveSharedMemory, which may be freely
471 /// modified, into a GuestSharedMemory, which may be somewhat
472 /// freely modified (mostly by the guest), and a HostSharedMemory,
473 /// which may only make certain kinds of accesses that do not race
474 /// in the presence of malicious code inside the guest mutating
475 /// the GuestSharedMemory.
476 pub fn build(self) -> (HostSharedMemory, GuestSharedMemory) {
477 let lock = Arc::new(RwLock::new(()));
478 let hshm = HostSharedMemory {
479 region: self.region.clone(),
480 lock: lock.clone(),
481 };
482 (
483 hshm,
484 GuestSharedMemory {
485 region: self.region.clone(),
486 lock,
487 },
488 )
489 }
490}
491
492fn mapping_at(
493 s: &impl SharedMemory,
494 gpa: u64,
495 size: usize,
496 region_type: MemoryRegionType,
497 flags: MemoryRegionFlags,
498) -> MemoryRegion {
499 let guest_base = gpa as usize;
500
501 MemoryRegion {
502 guest_region: guest_base..(guest_base + size),
503 host_region: s.host_region_base()
504 ..<HostGuestMemoryRegion as MemoryRegionKind>::add(s.host_region_base(), size),
505 region_type,
506 flags,
507 }
508}
509
510impl GuestSharedMemory {
511 /// Create a [`super::memory_region::MemoryRegion`] structure
512 /// suitable for mapping this region into a VM
513 pub(crate) fn mapping_at(
514 &self,
515 guest_base: u64,
516 region_type: MemoryRegionType,
517 ) -> MemoryRegion {
518 let flags = match region_type {
519 MemoryRegionType::Scratch => {
520 MemoryRegionFlags::READ | MemoryRegionFlags::WRITE | MemoryRegionFlags::EXECUTE
521 }
522 #[allow(clippy::panic)]
523 // This will not ever actually panic: the only places this
524 // is called are HyperlightVm::update_snapshot_mapping and
525 // HyperlightVm::update_scratch_mapping. The latter
526 // statically uses the Scratch region type, and the former
527 // does not use this at all — the snapshot scratch mapping is
528 // a ReadonlySharedMemory, not a GuestSharedMemory.
529 _ => panic!(
530 "GuestSharedMemory::mapping_at should only be used for Scratch or Snapshot regions"
531 ),
532 };
533 mapping_at(self, guest_base, self.mem_size(), region_type, flags)
534 }
535}
536
537/// A trait that abstracts over the particular kind of SharedMemory,
538/// used when invoking operations from Rust that absolutely must have
539/// exclusive control over the shared memory for correctness +
540/// performance, like snapshotting.
541pub trait SharedMemory {
542 /// Return a readonly reference to the host mapping backing this SharedMemory
543 fn region(&self) -> &HostMapping;
544
545 /// Return the base address of the host mapping of this
546 /// region. Following the general Rust philosophy, this does not
547 /// need to be marked as `unsafe` because doing anything with this
548 /// pointer itself requires `unsafe`.
549 fn base_addr(&self) -> usize {
550 self.region().ptr as usize + PAGE_SIZE_USIZE
551 }
552
553 /// Return the base address of the host mapping of this region as
554 /// a pointer. Following the general Rust philosophy, this does
555 /// not need to be marked as `unsafe` because doing anything with
556 /// this pointer itself requires `unsafe`.
557 fn base_ptr(&self) -> *mut u8 {
558 self.region().ptr.wrapping_add(PAGE_SIZE_USIZE)
559 }
560
561 /// Return the length of usable memory contained in `self`.
562 /// The returned size does not include the size of the surrounding
563 /// guard pages.
564 fn mem_size(&self) -> usize {
565 self.region().size - 2 * PAGE_SIZE_USIZE
566 }
567
568 /// Return the raw base address of the host mapping, including the
569 /// guard pages.
570 fn raw_ptr(&self) -> *mut u8 {
571 self.region().ptr
572 }
573
574 /// Return the raw size of the host mapping, including the guard
575 /// pages.
576 fn raw_mem_size(&self) -> usize {
577 self.region().size
578 }
579
580 /// Extract a base address that can be mapped into a VM for this
581 /// SharedMemory.
582 ///
583 /// Returns the host base address as a raw `usize` pointer.
584 fn host_region_base(&self) -> <HostGuestMemoryRegion as MemoryRegionKind>::HostBaseType {
585 self.base_addr()
586 }
587
588 /// Return the end address of the host region (base + usable size).
589 fn host_region_end(&self) -> <HostGuestMemoryRegion as MemoryRegionKind>::HostBaseType {
590 <HostGuestMemoryRegion as MemoryRegionKind>::add(self.host_region_base(), self.mem_size())
591 }
592
593 /// Run some code with exclusive access to the SharedMemory
594 /// underlying this. If the SharedMemory is not an
595 /// ExclusiveSharedMemory, any concurrent accesses to the relevant
596 /// HostSharedMemory/GuestSharedMemory may make this fail, or be
597 /// made to fail by this, and should be avoided.
598 fn with_exclusivity<T, F: FnOnce(&mut ExclusiveSharedMemory) -> T>(
599 &mut self,
600 f: F,
601 ) -> Result<T>;
602
603 /// Run some code that is allowed to access the contents of the
604 /// SharedMemory as if it is a normal slice. By default, this is
605 /// implemented via [`SharedMemory::with_exclusivity`], which is
606 /// the correct implementation for a memory that can be mutated,
607 /// but a [`ReadonlySharedMemory`], can support this.
608 fn with_contents<T, F: FnOnce(&[u8]) -> T>(&mut self, f: F) -> Result<T> {
609 self.with_exclusivity(|m| f(m.as_slice()))
610 }
611
612 /// Zero a shared memory region
613 fn zero(&mut self) -> Result<()> {
614 self.with_exclusivity(|e| {
615 #[allow(unused_mut)] // unused on some platforms, although not others
616 let mut do_copy = true;
617 // TODO: Compare & add heuristic thresholds: mmap, MADV_DONTNEED, MADV_REMOVE, MADV_FREE (?)
618 #[cfg(feature = "kvm")]
619 unsafe {
620 let ret = libc::madvise(
621 e.region.ptr as *mut libc::c_void,
622 e.region.size,
623 libc::MADV_DONTNEED,
624 );
625 if ret == 0 {
626 do_copy = false;
627 }
628 }
629 if do_copy {
630 e.as_mut_slice().fill(0);
631 }
632 })
633 }
634}
635
636impl SharedMemory for ExclusiveSharedMemory {
637 fn region(&self) -> &HostMapping {
638 &self.region
639 }
640 fn with_exclusivity<T, F: FnOnce(&mut ExclusiveSharedMemory) -> T>(
641 &mut self,
642 f: F,
643 ) -> Result<T> {
644 Ok(f(self))
645 }
646}
647
648impl SharedMemory for GuestSharedMemory {
649 fn region(&self) -> &HostMapping {
650 &self.region
651 }
652 fn with_exclusivity<T, F: FnOnce(&mut ExclusiveSharedMemory) -> T>(
653 &mut self,
654 f: F,
655 ) -> Result<T> {
656 let guard = self
657 .lock
658 .try_write()
659 .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?;
660 let mut excl = ExclusiveSharedMemory {
661 region: self.region.clone(),
662 };
663 let ret = f(&mut excl);
664 drop(excl);
665 drop(guard);
666 Ok(ret)
667 }
668}
669
670/// An unsafe marker trait for types for which all bit patterns are valid.
671/// This is required in order for it to be safe to read a value of a particular
672/// type out of the sandbox from the HostSharedMemory.
673///
674/// # Safety
675/// This must only be implemented for types for which all bit patterns
676/// are valid. It requires that any (non-undef/poison) value of the
677/// correct size can be transmuted to the type.
678pub unsafe trait AllValid {}
679unsafe impl AllValid for u8 {}
680unsafe impl AllValid for u16 {}
681unsafe impl AllValid for u32 {}
682unsafe impl AllValid for u64 {}
683unsafe impl AllValid for i8 {}
684unsafe impl AllValid for i16 {}
685unsafe impl AllValid for i32 {}
686unsafe impl AllValid for i64 {}
687unsafe impl AllValid for [u8; 16] {}
688
689impl HostSharedMemory {
690 /// Read a value of type T, whose representation is the same
691 /// between the sandbox and the host, and which has no invalid bit
692 /// patterns
693 pub fn read<T: AllValid>(&self, offset: usize) -> Result<T> {
694 bounds_check!(offset, std::mem::size_of::<T>(), self.mem_size());
695 unsafe {
696 let mut ret: core::mem::MaybeUninit<T> = core::mem::MaybeUninit::uninit();
697 {
698 let slice: &mut [u8] = core::slice::from_raw_parts_mut(
699 ret.as_mut_ptr() as *mut u8,
700 std::mem::size_of::<T>(),
701 );
702 self.copy_to_slice(slice, offset)?;
703 }
704 Ok(ret.assume_init())
705 }
706 }
707
708 /// Write a value of type T, whose representation is the same
709 /// between the sandbox and the host, and which has no invalid bit
710 /// patterns
711 pub fn write<T: AllValid>(&self, offset: usize, data: T) -> Result<()> {
712 bounds_check!(offset, std::mem::size_of::<T>(), self.mem_size());
713 unsafe {
714 let slice: &[u8] = core::slice::from_raw_parts(
715 core::ptr::addr_of!(data) as *const u8,
716 std::mem::size_of::<T>(),
717 );
718 self.copy_from_slice(slice, offset)?;
719 }
720 Ok(())
721 }
722
723 /// Copy the contents of the slice into the sandbox at the
724 /// specified offset
725 pub fn copy_to_slice(&self, slice: &mut [u8], offset: usize) -> Result<()> {
726 bounds_check!(offset, slice.len(), self.mem_size());
727 let base = self.base_ptr().wrapping_add(offset);
728 let guard = self
729 .lock
730 .try_read()
731 .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?;
732
733 const CHUNK: usize = size_of::<u128>();
734 let len = slice.len();
735 let mut i = 0;
736
737 // Handle unaligned head bytes until we reach u128 alignment.
738 // Note: align_offset can return usize::MAX if alignment is impossible.
739 // In that case, head_len = len via .min(), so we fall back to byte-by-byte
740 // operations for the entire slice.
741 let align_offset = base.align_offset(align_of::<u128>());
742 let head_len = align_offset.min(len);
743 while i < head_len {
744 unsafe {
745 slice[i] = base.add(i).read_volatile();
746 }
747 i += 1;
748 }
749
750 // Read aligned u128 chunks
751 // SAFETY: After processing head_len bytes, base.add(i) is u128-aligned.
752 // We use write_unaligned for the destination since the slice may not be u128-aligned.
753 let dst = slice.as_mut_ptr();
754 while i + CHUNK <= len {
755 unsafe {
756 let value = (base.add(i) as *const u128).read_volatile();
757 std::ptr::write_unaligned(dst.add(i) as *mut u128, value);
758 }
759 i += CHUNK;
760 }
761
762 // Handle remaining tail bytes
763 while i < len {
764 unsafe {
765 slice[i] = base.add(i).read_volatile();
766 }
767 i += 1;
768 }
769
770 drop(guard);
771 Ok(())
772 }
773
774 /// Copy the contents of the sandbox at the specified offset into
775 /// the slice
776 pub fn copy_from_slice(&self, slice: &[u8], offset: usize) -> Result<()> {
777 bounds_check!(offset, slice.len(), self.mem_size());
778 let base = self.base_ptr().wrapping_add(offset);
779 let guard = self
780 .lock
781 .try_read()
782 .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?;
783
784 const CHUNK: usize = size_of::<u128>();
785 let len = slice.len();
786 let mut i = 0;
787
788 // Handle unaligned head bytes until we reach u128 alignment.
789 // Note: align_offset can return usize::MAX if alignment is impossible.
790 // In that case, head_len = len via .min(), so we fall back to byte-by-byte
791 // operations for the entire slice.
792 let align_offset = base.align_offset(align_of::<u128>());
793 let head_len = align_offset.min(len);
794 while i < head_len {
795 unsafe {
796 base.add(i).write_volatile(slice[i]);
797 }
798 i += 1;
799 }
800
801 // Write aligned u128 chunks
802 // SAFETY: After processing head_len bytes, base.add(i) is u128-aligned.
803 // We use read_unaligned for the source since the slice may not be u128-aligned.
804 let src = slice.as_ptr();
805 while i + CHUNK <= len {
806 unsafe {
807 let value = std::ptr::read_unaligned(src.add(i) as *const u128);
808 (base.add(i) as *mut u128).write_volatile(value);
809 }
810 i += CHUNK;
811 }
812
813 // Handle remaining tail bytes
814 while i < len {
815 unsafe {
816 base.add(i).write_volatile(slice[i]);
817 }
818 i += 1;
819 }
820
821 drop(guard);
822 Ok(())
823 }
824
825 /// Fill the memory in the range `[offset, offset + len)` with `value`
826 #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
827 pub fn fill(&mut self, value: u8, offset: usize, len: usize) -> Result<()> {
828 bounds_check!(offset, len, self.mem_size());
829 let base = self.base_ptr().wrapping_add(offset);
830 let guard = self
831 .lock
832 .try_read()
833 .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?;
834
835 const CHUNK: usize = size_of::<u128>();
836 let value_u128 = u128::from_ne_bytes([value; CHUNK]);
837 let mut i = 0;
838
839 // Handle unaligned head bytes until we reach u128 alignment.
840 // Note: align_offset can return usize::MAX if alignment is impossible.
841 // In that case, head_len = len via .min(), so we fall back to byte-by-byte
842 // operations for the entire slice.
843 let align_offset = base.align_offset(align_of::<u128>());
844 let head_len = align_offset.min(len);
845 while i < head_len {
846 unsafe {
847 base.add(i).write_volatile(value);
848 }
849 i += 1;
850 }
851
852 // Write aligned u128 chunks
853 // SAFETY: After processing head_len bytes, base.add(i) is u128-aligned
854 while i + CHUNK <= len {
855 unsafe {
856 (base.add(i) as *mut u128).write_volatile(value_u128);
857 }
858 i += CHUNK;
859 }
860
861 // Handle remaining tail bytes
862 while i < len {
863 unsafe {
864 base.add(i).write_volatile(value);
865 }
866 i += 1;
867 }
868
869 drop(guard);
870 Ok(())
871 }
872
873 /// Pushes the given data onto shared memory to the buffer at the given offset.
874 /// NOTE! buffer_start_offset must point to the beginning of the buffer
875 #[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
876 pub fn push_buffer(
877 &mut self,
878 buffer_start_offset: usize,
879 buffer_size: usize,
880 data: &[u8],
881 ) -> Result<()> {
882 let stack_pointer_rel = self.read::<u64>(buffer_start_offset)? as usize;
883 let buffer_size_u64: u64 = buffer_size.try_into()?;
884
885 if stack_pointer_rel > buffer_size || stack_pointer_rel < 8 {
886 return Err(new_error!(
887 "Unable to push data to buffer: Stack pointer is out of bounds. Stack pointer: {}, Buffer size: {}",
888 stack_pointer_rel,
889 buffer_size_u64
890 ));
891 }
892
893 let size_required = data.len() + 8;
894 let size_available = buffer_size - stack_pointer_rel;
895
896 if size_required > size_available {
897 return Err(new_error!(
898 "Not enough space in buffer to push data. Required: {}, Available: {}",
899 size_required,
900 size_available
901 ));
902 }
903
904 // get absolute
905 let stack_pointer_abs = stack_pointer_rel + buffer_start_offset;
906
907 // write the actual data to the top of stack
908 self.copy_from_slice(data, stack_pointer_abs)?;
909
910 // write the offset to the newly written data, to the top of stack.
911 // this is used when popping the stack, to know how far back to jump
912 self.write::<u64>(stack_pointer_abs + data.len(), stack_pointer_rel as u64)?;
913
914 // update stack pointer to point to the next free address
915 self.write::<u64>(
916 buffer_start_offset,
917 (stack_pointer_rel + data.len() + 8) as u64,
918 )?;
919 Ok(())
920 }
921
922 /// Pop the top element of the ring as raw bytes. Unlike
923 /// [`Self::try_pop_buffer_into`], this doesn't peek at the element's
924 /// contents — the element size is recovered from the trailing
925 /// back-pointer that [`Self::push_buffer`] wrote.
926 pub fn try_pop_buffer_raw(
927 &mut self,
928 buffer_start_offset: usize,
929 buffer_size: usize,
930 ) -> Result<Vec<u8>> {
931 let stack_pointer_rel = self.read::<u64>(buffer_start_offset)? as usize;
932
933 if stack_pointer_rel > buffer_size || stack_pointer_rel < 16 {
934 return Err(new_error!(
935 "try_pop_buffer_raw: stack pointer {} out of bounds (size {})",
936 stack_pointer_rel,
937 buffer_size
938 ));
939 }
940
941 let back_ptr_abs = stack_pointer_rel + buffer_start_offset - 8;
942 let element_offset_rel = self.read::<u64>(back_ptr_abs)? as usize;
943
944 if element_offset_rel < 8 || element_offset_rel > stack_pointer_rel.saturating_sub(8) {
945 return Err(new_error!(
946 "try_pop_buffer_raw: back-pointer {} outside [8, {}]",
947 element_offset_rel,
948 stack_pointer_rel.saturating_sub(8)
949 ));
950 }
951
952 let element_size = stack_pointer_rel - element_offset_rel - 8;
953 let element_abs = element_offset_rel + buffer_start_offset;
954 let mut out = vec![0u8; element_size];
955 self.copy_to_slice(&mut out, element_abs)?;
956
957 // Pop: rewind stack pointer.
958 self.write::<u64>(buffer_start_offset, element_offset_rel as u64)?;
959 // Zero out the popped slot + its back-pointer.
960 self.fill(0, element_abs, stack_pointer_rel - element_offset_rel)?;
961
962 Ok(out)
963 }
964
965 /// Pops the given given buffer into a `T` and returns it.
966 /// NOTE! the data must be a size-prefixed flatbuffer, and
967 /// buffer_start_offset must point to the beginning of the buffer
968 pub fn try_pop_buffer_into<T>(
969 &mut self,
970 buffer_start_offset: usize,
971 buffer_size: usize,
972 ) -> Result<T>
973 where
974 T: for<'b> TryFrom<&'b [u8]>,
975 {
976 // get the stackpointer
977 let stack_pointer_rel = self.read::<u64>(buffer_start_offset)? as usize;
978
979 if stack_pointer_rel > buffer_size || stack_pointer_rel < 16 {
980 return Err(new_error!(
981 "Unable to pop data from buffer: Stack pointer is out of bounds. Stack pointer: {}, Buffer size: {}",
982 stack_pointer_rel,
983 buffer_size
984 ));
985 }
986
987 // make it absolute
988 let last_element_offset_abs = stack_pointer_rel + buffer_start_offset;
989
990 // go back 8 bytes to get offset to element on top of stack
991 let last_element_offset_rel: usize =
992 self.read::<u64>(last_element_offset_abs - 8)? as usize;
993
994 // Validate element offset (guest-writable): must be in [8, stack_pointer_rel - 16]
995 // to leave room for the 8-byte back-pointer plus at least 8 bytes of element data
996 // (the minimum for a size-prefixed flatbuffer: 4-byte prefix + 4-byte root offset).
997 if last_element_offset_rel > stack_pointer_rel.saturating_sub(16)
998 || last_element_offset_rel < 8
999 {
1000 return Err(new_error!(
1001 "Corrupt buffer back-pointer: element offset {} is outside valid range [8, {}].",
1002 last_element_offset_rel,
1003 stack_pointer_rel.saturating_sub(16),
1004 ));
1005 }
1006
1007 // make it absolute
1008 let last_element_offset_abs = last_element_offset_rel + buffer_start_offset;
1009
1010 // Max bytes the element can span (excluding the 8-byte back-pointer).
1011 let max_element_size = stack_pointer_rel - last_element_offset_rel - 8;
1012
1013 // Get the size of the flatbuffer buffer from memory
1014 let fb_buffer_size = {
1015 let raw_prefix = self.read::<u32>(last_element_offset_abs)?;
1016 // flatbuffer byte arrays are prefixed by 4 bytes indicating
1017 // the remaining size; add 4 for the prefix itself.
1018 let total = raw_prefix.checked_add(4).ok_or_else(|| {
1019 new_error!(
1020 "Corrupt buffer size prefix: value {} overflows when adding 4-byte header.",
1021 raw_prefix
1022 )
1023 })?;
1024 usize::try_from(total)
1025 }?;
1026
1027 if fb_buffer_size > max_element_size {
1028 return Err(new_error!(
1029 "Corrupt buffer size prefix: flatbuffer claims {} bytes but the element slot is only {} bytes.",
1030 fb_buffer_size,
1031 max_element_size
1032 ));
1033 }
1034
1035 let mut result_buffer = vec![0; fb_buffer_size];
1036
1037 self.copy_to_slice(&mut result_buffer, last_element_offset_abs)?;
1038 let to_return = T::try_from(result_buffer.as_slice()).map_err(|_e| {
1039 new_error!(
1040 "pop_buffer_into: failed to convert buffer to {}",
1041 type_name::<T>()
1042 )
1043 })?;
1044
1045 // update the stack pointer to point to the element we just popped off since that is now free
1046 self.write::<u64>(buffer_start_offset, last_element_offset_rel as u64)?;
1047
1048 // zero out the memory we just popped off
1049 let num_bytes_to_zero = stack_pointer_rel - last_element_offset_rel;
1050 self.fill(0, last_element_offset_abs, num_bytes_to_zero)?;
1051
1052 Ok(to_return)
1053 }
1054}
1055
1056impl SharedMemory for HostSharedMemory {
1057 fn region(&self) -> &HostMapping {
1058 &self.region
1059 }
1060 fn with_exclusivity<T, F: FnOnce(&mut ExclusiveSharedMemory) -> T>(
1061 &mut self,
1062 f: F,
1063 ) -> Result<T> {
1064 let guard = self
1065 .lock
1066 .try_write()
1067 .map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?;
1068 let mut excl = ExclusiveSharedMemory {
1069 region: self.region.clone(),
1070 };
1071 let ret = f(&mut excl);
1072 drop(excl);
1073 drop(guard);
1074 Ok(ret)
1075 }
1076}
1077
1078/// A ReadonlySharedMemory is a different kind of shared memory,
1079/// separate from the exclusive/host/guest lifecycle, used to
1080/// represent read-only mappings of snapshot pages into the guest
1081/// efficiently.
1082#[derive(Clone, Debug)]
1083pub struct ReadonlySharedMemory {
1084 region: Arc<HostMapping>,
1085 /// If `Some`, only this many bytes are mapped into guest PA space
1086 /// by `mapping_at`. If `None`, the full `mem_size()` is mapped.
1087 guest_mapped_size: Option<usize>,
1088}
1089// Safety: HostMapping is only non-Send/Sync (causing
1090// ReadonlySharedMemory to not be automatically Send/Sync) because raw
1091// pointers are not ("as a lint", as the Rust docs say). We don't want
1092// to mark HostMapping Send/Sync immediately, because that could
1093// socially imply that it's "safe" to use unsafe accesses from
1094// multiple threads at once in more cases, including ones that don't
1095// actually ensure immutability/synchronisation. Since
1096// ReadonlySharedMemory can only be accessed by reading, and reading
1097// concurrently from multiple threads is not racy,
1098// ReadonlySharedMemory can be Send and Sync.
1099unsafe impl Send for ReadonlySharedMemory {}
1100unsafe impl Sync for ReadonlySharedMemory {}
1101
1102impl ReadonlySharedMemory {
1103 pub(crate) fn from_bytes(contents: &[u8]) -> Result<Self> {
1104 let mut anon = ExclusiveSharedMemory::new(contents.len())?;
1105 anon.copy_from_slice(contents, 0)?;
1106 Ok(ReadonlySharedMemory {
1107 region: anon.region,
1108 guest_mapped_size: None,
1109 })
1110 }
1111
1112 /// The number of bytes that should be mapped into guest PA space.
1113 /// Returns `guest_mapped_size` if set, otherwise `mem_size()`.
1114 pub(crate) fn guest_mapped_size(&self) -> usize {
1115 self.guest_mapped_size.unwrap_or_else(|| self.mem_size())
1116 }
1117
1118 pub(crate) fn as_slice(&self) -> &[u8] {
1119 unsafe { std::slice::from_raw_parts(self.base_ptr(), self.mem_size()) }
1120 }
1121
1122 pub(crate) fn build(self) -> (Self, Self) {
1123 (self.clone(), self)
1124 }
1125
1126 pub(crate) fn mapping_at(
1127 &self,
1128 guest_base: u64,
1129 region_type: MemoryRegionType,
1130 ) -> MemoryRegion {
1131 #[allow(clippy::panic)]
1132 // This will not ever actually panic: the only place this is
1133 // called is HyperlightVm::update_snapshot_mapping, which
1134 // always calls it with the Snapshot region type.
1135 if region_type != MemoryRegionType::Snapshot {
1136 panic!("ReadonlySharedMemory::mapping_at should only be used for Snapshot regions");
1137 }
1138 // Register snapshot mem RWX at the KVM level. Upstream marked
1139 // this RX-only and relied on guest-PT CoW for write semantics,
1140 // which trapped first writes and resolved them to scratch frames
1141 // — driving a slow leak via prim_alloc. The underlying mmap is
1142 // already PROT_READ|PROT_WRITE; `ReadonlySharedMemory` is a
1143 // host-side Rust-API artifact, not a KVM-level constraint.
1144 mapping_at(
1145 self,
1146 guest_base,
1147 self.guest_mapped_size(),
1148 region_type,
1149 MemoryRegionFlags::READ | MemoryRegionFlags::WRITE | MemoryRegionFlags::EXECUTE,
1150 )
1151 }
1152}
1153
1154impl SharedMemory for ReadonlySharedMemory {
1155 fn region(&self) -> &HostMapping {
1156 &self.region
1157 }
1158 // There's no way to get exclusive (and therefore writable) access
1159 // to a ReadonlySharedMemory.
1160 fn with_exclusivity<T, F: FnOnce(&mut ExclusiveSharedMemory) -> T>(
1161 &mut self,
1162 _: F,
1163 ) -> Result<T> {
1164 Err(new_error!(
1165 "Cannot take exclusive access to a ReadonlySharedMemory"
1166 ))
1167 }
1168 // However, just access to the contents as a slice is doable
1169 fn with_contents<T, F: FnOnce(&[u8]) -> T>(&mut self, f: F) -> Result<T> {
1170 Ok(f(self.as_slice()))
1171 }
1172}
1173
1174impl<S: SharedMemory> PartialEq<S> for ReadonlySharedMemory {
1175 fn eq(&self, other: &S) -> bool {
1176 self.raw_ptr() == other.raw_ptr()
1177 }
1178}