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}