Skip to main content

nub_host_kvm/
guest_cache_reader.rs

1//! Host-side read-only view of the guest's heap-resident cap
2//! directory.
3//!
4//! After Commit 2, the guest kernel is linked into the per-process
5//! `GUEST_VA` reservation at a canonical low-half VA. The host
6//! process can mmap-shadow the kernel image at the same VA, so any
7//! kernel-mode pointer (e.g. the address of the
8//! `nub_arch_x86::state_cache::CACHE` `CacheDirectory<FixedState>`)
9//! is directly dereferenceable from host code.
10//!
11//! `GuestCacheReader` wraps the directory VA published by the
12//! guest in its `BootInfo` block (`MultiUseSandbox::boot_info`
13//! later — for now this module just exposes the type for
14//! Commit 4's wiring) and exposes a `get(hash) -> Option<&Cap>`
15//! helper. The directory is a `CacheDirectory<FixedState>` on
16//! both sides — both host and guest see the same
17//! `Box<CacheEntry>` cells through the same `FixedState` seed, so
18//! bucket assignments match and the host's view of the table is
19//! byte-identical to the guest's.
20//!
21//! ## Safety
22//!
23//! - The construction is `unsafe`: the caller must promise the
24//!   `directory_va` is correct (came from a verified
25//!   `BootInfo::magic` + matching `directory_type_id`).
26//! - The reader holds no lock on its own. To read consistently, the
27//!   caller must ensure no concurrent guest-mode mutation is in
28//!   flight (V0: the host only reads when no guest call is
29//!   executing — Hyperlight serialises host/guest exclusively).
30//! - Returned `&Cap` borrows live until the next time the host hands
31//!   control back to the guest. After that the directory's contents
32//!   may change and any retained pointer is stale.
33
34use core::ptr::NonNull;
35use std::sync::Arc;
36
37use foldhash::fast::FixedState;
38use javm_cap::cache::CacheDirectory;
39use javm_cap::{Cap, CapHash};
40use nub_arch_x86_abi::BootInfo;
41
42/// The directory's concrete type. Must match
43/// `nub_arch_x86::state_cache::CACHE`'s inner type exactly —
44/// `CacheDirectory`'s layout depends on its hasher parameter, so
45/// any divergence would silently produce nonsense reads.
46type GuestDirectory = CacheDirectory<FixedState>;
47
48/// Read-only view of the guest's heap-resident cap directory.
49pub struct GuestCacheReader {
50    /// Pointer to the guest's CacheDirectory living at `directory_va`.
51    /// The pointer is valid only while the sandbox is alive and the
52    /// guest's kernel-mode VA mapping is still in place.
53    directory: NonNull<GuestDirectory>,
54}
55
56// SAFETY: `directory` is a raw pointer into the host's mapping of the
57// guest's kernel-half VA range. The reader holds it for read-only
58// access; we never observe a write through this pointer from another
59// thread while we're reading because the owning `MultiUseSandbox`
60// itself isn't shared across threads simultaneously (the workspace
61// keeps Hyperlight sandboxes inside `Mutex<Nub>`). Marking the type
62// `Send + Sync` lets a `MultiUseSandbox` containing one satisfy the
63// `Sync` bound demanded by `static OnceLock<Mutex<Nub>>`.
64unsafe impl Send for GuestCacheReader {}
65unsafe impl Sync for GuestCacheReader {}
66
67impl GuestCacheReader {
68    /// Construct a reader from a `BootInfo` block.
69    ///
70    /// # Safety
71    ///
72    /// - `boot_info.magic` must equal [`BootInfo::MAGIC`].
73    /// - `boot_info.directory_va` must point at a `GuestDirectory`
74    ///   value living in the same address space (= the host's
75    ///   process), allocated through the same `FixedState` seed as
76    ///   the guest's `DIRECTORY_HASHER_SEED`.
77    /// - The reader must not outlive the sandbox that owns the
78    ///   directory.
79    pub unsafe fn new(boot_info: &BootInfo) -> Result<Self, GuestCacheReaderError> {
80        if boot_info.magic != BootInfo::MAGIC {
81            return Err(GuestCacheReaderError::BadMagic);
82        }
83        if boot_info.directory_va == 0 {
84            return Err(GuestCacheReaderError::UninitialisedDirectoryVa);
85        }
86        let ptr = boot_info.directory_va as usize as *mut GuestDirectory;
87        let nn = NonNull::new(ptr).ok_or(GuestCacheReaderError::NullPointer)?;
88        Ok(Self { directory: nn })
89    }
90
91    /// Number of blob entries in the guest's directory.
92    ///
93    /// # Safety
94    ///
95    /// Implicit: see the type's safety section. We declare this
96    /// `pub` (not `unsafe`) on the strength of the `new` contract
97    /// — once you have a `GuestCacheReader`, every read assumes
98    /// the directory is quiescent.
99    pub fn len(&self) -> usize {
100        // SAFETY: `directory` is `NonNull<GuestDirectory>`; the
101        // construction contract requires it points at a valid
102        // directory in the host's address space.
103        unsafe { self.directory.as_ref().blob_count() }
104    }
105
106    /// `true` iff the directory holds no blob entries.
107    pub fn is_empty(&self) -> bool {
108        self.len() == 0
109    }
110
111    /// Look up a cap by content hash. Returns an Arc::clone of the
112    /// guest's `Arc<Cap>` if the blob is published; `None` otherwise.
113    ///
114    /// The returned `Arc` keeps the guest's Cap data alive across the
115    /// lookup boundary — even if the guest later overwrites or removes
116    /// the entry, the caller's Arc stays valid (the data is freed only
117    /// when the last Arc clone drops).
118    pub fn get(&self, hash: &CapHash) -> Option<Arc<Cap>> {
119        // SAFETY: directory is valid (see construction contract);
120        // `CacheDirectory::get_blob` clones the stored Arc internally.
121        let dir: &GuestDirectory = unsafe { self.directory.as_ref() };
122        dir.get_blob(hash)
123    }
124
125    /// Whether a hash is present, without dereferencing the value.
126    pub fn contains(&self, hash: &CapHash) -> bool {
127        let dir: &GuestDirectory = unsafe { self.directory.as_ref() };
128        dir.contains_blob(hash)
129    }
130}
131
132/// Failures from [`GuestCacheReader::new`].
133#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
134pub enum GuestCacheReaderError {
135    /// The `BootInfo` magic field didn't match
136    /// `BootInfo::MAGIC`.
137    #[error("boot info magic mismatch")]
138    BadMagic,
139    /// The directory VA in `BootInfo` was zero — the guest hasn't
140    /// run `init_directory_va` yet. Call any RPC that triggers the
141    /// init hook (e.g. `nub_get_boot_info`) and retry.
142    #[error("boot info directory_va is zero (guest hasn't initialised)")]
143    UninitialisedDirectoryVa,
144    /// The directory VA was non-zero but, after the
145    /// `directory_va -> *mut GuestDirectory` cast, resulted in a
146    /// null pointer. Shouldn't be observable in practice; covers
147    /// the cast hazard for completeness.
148    #[error("directory_va decoded to a null pointer")]
149    NullPointer,
150}