nub_host_kvm/sandbox/uninitialized.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::fmt::Debug;
18use std::option::Option;
19use std::path::Path;
20use std::sync::{Arc, Mutex};
21
22use tracing::{Span, instrument};
23use tracing_core::LevelFilter;
24
25use super::host_funcs::FunctionRegistry;
26use super::snapshot::Snapshot;
27use super::uninitialized_evolve::evolve_impl_multi_use;
28use crate::func::HostFn;
29use crate::func::host_functions::register_host_function;
30#[cfg(feature = "build-metadata")]
31use crate::log_build_details;
32use crate::mem::memory_region::{DEFAULT_GUEST_BLOB_MEM_FLAGS, MemoryRegionFlags};
33use crate::mem::mgr::SandboxMemoryManager;
34#[cfg(feature = "guest-counter")]
35use crate::mem::shared_mem::HostSharedMemory;
36use crate::mem::shared_mem::{ExclusiveSharedMemory, SharedMemory};
37use crate::sandbox::SandboxConfiguration;
38use crate::{MultiUseSandbox, Result, new_error};
39
40#[cfg(any(crashdump, gdb))]
41#[derive(Clone, Debug, Default)]
42pub(crate) struct SandboxRuntimeConfig {
43 #[cfg(crashdump)]
44 pub(crate) binary_path: Option<String>,
45 #[cfg(gdb)]
46 pub(crate) debug_info: Option<super::config::DebugInfo>,
47 #[cfg(crashdump)]
48 pub(crate) guest_core_dump: bool,
49 /// The original entry point address of the loaded guest binary
50 /// (load_addr + ELF entry offset). Used for AT_ENTRY in core dumps
51 /// so GDB can compute the correct load offset for PIE binaries.
52 ///
53 /// `None` until resolved from the snapshot's `NextAction::Initialise`
54 /// in `set_up_hypervisor_partition`.
55 #[cfg(crashdump)]
56 pub(crate) entry_point: Option<u64>,
57}
58
59/// A host-authoritative shared counter exposed to the guest via a `u64`
60/// in guest scratch memory.
61///
62/// Created via [`UninitializedSandbox::guest_counter()`]. The host owns
63/// the counter value and is the only writer: [`increment()`](Self::increment)
64/// and [`decrement()`](Self::decrement) update the cached value and write
65/// to shared memory via [`HostSharedMemory::write()`]. [`value()`](Self::value)
66/// returns the cached value — the host never reads back from guest memory,
67/// so a malicious guest cannot influence the host's view of the counter.
68///
69/// Thread safety is provided by an internal `Mutex`, so `increment()` and
70/// `decrement()` take `&self` rather than `&mut self`.
71///
72/// The counter holds an `Arc<Mutex<Option<HostSharedMemory>>>` that is
73/// shared with [`UninitializedSandbox`]. The `Option` is `None` until
74/// [`evolve()`](UninitializedSandbox::evolve) populates it, at which point
75/// the counter can issue volatile writes via the proper protocol.
76///
77/// Only one `GuestCounter` may be created per sandbox; a second call to
78/// [`UninitializedSandbox::guest_counter()`] returns an error.
79#[cfg(feature = "guest-counter")]
80pub struct GuestCounter {
81 inner: Mutex<GuestCounterInner>,
82}
83
84#[cfg(feature = "guest-counter")]
85struct GuestCounterInner {
86 deferred_hshm: Arc<Mutex<Option<HostSharedMemory>>>,
87 offset: usize,
88 value: u64,
89}
90
91#[cfg(feature = "guest-counter")]
92impl core::fmt::Debug for GuestCounter {
93 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
94 f.debug_struct("GuestCounter").finish_non_exhaustive()
95 }
96}
97
98#[cfg(feature = "guest-counter")]
99impl GuestCounter {
100 /// Increments the counter by one and writes it to guest memory.
101 pub fn increment(&self) -> Result<()> {
102 let mut inner = self.inner.lock().map_err(|e| new_error!("{e}"))?;
103 let shm = {
104 let guard = inner.deferred_hshm.lock().map_err(|e| new_error!("{e}"))?;
105 guard
106 .as_ref()
107 .ok_or_else(|| {
108 new_error!("GuestCounter cannot be used before shared memory is built")
109 })?
110 .clone()
111 };
112 let new_value = inner
113 .value
114 .checked_add(1)
115 .ok_or_else(|| new_error!("GuestCounter overflow"))?;
116 shm.write::<u64>(inner.offset, new_value)?;
117 inner.value = new_value;
118 Ok(())
119 }
120
121 /// Decrements the counter by one and writes it to guest memory.
122 pub fn decrement(&self) -> Result<()> {
123 let mut inner = self.inner.lock().map_err(|e| new_error!("{e}"))?;
124 let shm = {
125 let guard = inner.deferred_hshm.lock().map_err(|e| new_error!("{e}"))?;
126 guard
127 .as_ref()
128 .ok_or_else(|| {
129 new_error!("GuestCounter cannot be used before shared memory is built")
130 })?
131 .clone()
132 };
133 let new_value = inner
134 .value
135 .checked_sub(1)
136 .ok_or_else(|| new_error!("GuestCounter underflow"))?;
137 shm.write::<u64>(inner.offset, new_value)?;
138 inner.value = new_value;
139 Ok(())
140 }
141
142 /// Returns the current host-side value of the counter.
143 pub fn value(&self) -> Result<u64> {
144 let inner = self.inner.lock().map_err(|e| new_error!("{e}"))?;
145 Ok(inner.value)
146 }
147}
148
149/// A preliminary sandbox that represents allocated memory and registered host functions,
150/// but has not yet created the underlying virtual machine.
151///
152/// This struct holds the configuration and setup needed for a sandbox without actually
153/// creating the VM. It allows you to:
154/// - Set up memory layout and load guest binary data
155/// - Register host functions that will be available to the guest
156/// - Configure sandbox settings before VM creation
157///
158/// The virtual machine is not created until you call [`evolve`](Self::evolve) to transform
159/// this into an initialized [`MultiUseSandbox`].
160pub struct UninitializedSandbox {
161 /// Registered host functions
162 pub(crate) host_funcs: Arc<Mutex<FunctionRegistry>>,
163 /// The memory manager for the sandbox.
164 pub(crate) mgr: SandboxMemoryManager<ExclusiveSharedMemory>,
165 pub(crate) max_guest_log_level: Option<LevelFilter>,
166 pub(crate) config: SandboxConfiguration,
167 #[cfg(any(crashdump, gdb))]
168 pub(crate) rt_cfg: SandboxRuntimeConfig,
169 pub(crate) load_info: crate::mem::exe::LoadInfo,
170 // This is needed to convey the stack pointer between the snapshot
171 // and the HyperlightVm creation
172 pub(crate) stack_top_gva: u64,
173 /// Populated by [`evolve()`](Self::evolve) with a [`HostSharedMemory`]
174 /// view of scratch memory. Code that needs host-style volatile access
175 /// before `evolve()` (e.g. `GuestCounter`) can clone this `Arc` and
176 /// will see `Some` once `evolve()` completes.
177 #[cfg(feature = "guest-counter")]
178 pub(crate) deferred_hshm: Arc<Mutex<Option<HostSharedMemory>>>,
179 /// Set to `true` once a [`GuestCounter`] has been handed out via
180 /// [`guest_counter()`](Self::guest_counter). Prevents creating
181 /// multiple counters that would have divergent cached values.
182 #[cfg(feature = "guest-counter")]
183 counter_taken: std::sync::atomic::AtomicBool,
184}
185
186impl Debug for UninitializedSandbox {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 f.debug_struct("UninitializedSandbox")
189 .field("memory_layout", &self.mgr.layout)
190 .finish()
191 }
192}
193
194/// A `GuestBinary` is either a buffer or the file path to some data (e.g., a guest binary).
195#[derive(Debug)]
196pub enum GuestBinary<'a> {
197 /// A buffer containing the GuestBinary
198 Buffer(&'a [u8]),
199 /// A path to the GuestBinary
200 FilePath(String),
201}
202impl<'a> GuestBinary<'a> {
203 /// If the guest binary is identified by a file, canonicalise the path
204 ///
205 /// For [`GuestBinary::FilePath`], this resolves the path to its canonical
206 /// form. For [`GuestBinary::Buffer`], this method is a no-op.
207 /// TODO: Maybe we should make the GuestEnvironment or
208 /// GuestBinary constructors crate-private and turn this
209 /// into an invariant on one of those types.
210 pub fn canonicalize(&mut self) -> Result<()> {
211 if let GuestBinary::FilePath(p) = self {
212 let canon = Path::new(&p)
213 .canonicalize()
214 .map_err(|e| new_error!("GuestBinary not found: '{}': {}", p, e))?
215 .into_os_string()
216 .into_string()
217 .map_err(|e| new_error!("Error converting OsString to String: {:?}", e))?;
218 *self = GuestBinary::FilePath(canon)
219 }
220 Ok(())
221 }
222}
223
224/// A `GuestBlob` containing data and the permissions for its use.
225#[derive(Debug)]
226pub struct GuestBlob<'a> {
227 /// The data contained in the blob.
228 pub data: &'a [u8],
229 /// The permissions for the blob in memory.
230 /// By default, it's READ
231 pub permissions: MemoryRegionFlags,
232}
233
234impl<'a> From<&'a [u8]> for GuestBlob<'a> {
235 fn from(data: &'a [u8]) -> Self {
236 GuestBlob {
237 data,
238 permissions: DEFAULT_GUEST_BLOB_MEM_FLAGS,
239 }
240 }
241}
242
243/// Container for a guest binary and optional initialization data.
244///
245/// This struct combines a guest binary (either from a file or memory buffer) with
246/// optional data that will be available to the guest during execution.
247#[derive(Debug)]
248pub struct GuestEnvironment<'a, 'b> {
249 /// The guest binary, which can be a file path or a buffer.
250 pub guest_binary: GuestBinary<'a>,
251 /// An optional guest blob, which can be used to provide additional data to the guest.
252 pub init_data: Option<GuestBlob<'b>>,
253}
254
255impl<'a, 'b> GuestEnvironment<'a, 'b> {
256 /// Creates a new `GuestEnvironment` with the given guest binary and an optional guest blob.
257 pub fn new(guest_binary: GuestBinary<'a>, init_data: Option<&'b [u8]>) -> Self {
258 GuestEnvironment {
259 guest_binary,
260 init_data: init_data.map(GuestBlob::from),
261 }
262 }
263}
264
265impl<'a> From<GuestBinary<'a>> for GuestEnvironment<'a, '_> {
266 fn from(guest_binary: GuestBinary<'a>) -> Self {
267 GuestEnvironment {
268 guest_binary,
269 init_data: None,
270 }
271 }
272}
273
274impl UninitializedSandbox {
275 /// Creates a [`GuestCounter`] at a fixed offset in scratch memory.
276 ///
277 /// The counter lives at `SCRATCH_TOP_GUEST_COUNTER_OFFSET` bytes from
278 /// the top of scratch memory, so both host and guest can locate it
279 /// without an explicit GPA parameter.
280 ///
281 /// The returned counter holds an `Arc` clone of the sandbox's
282 /// `deferred_hshm`, so it will automatically gain access to the
283 /// [`HostSharedMemory`] once [`evolve()`](Self::evolve) completes.
284 ///
285 /// This method can only be called once; a second call returns an error
286 /// because multiple counters would have divergent cached values.
287 #[cfg(feature = "guest-counter")]
288 pub fn guest_counter(&mut self) -> Result<GuestCounter> {
289 use std::sync::atomic::Ordering;
290
291 use nub_host_common::layout::SCRATCH_TOP_GUEST_COUNTER_OFFSET;
292
293 if self.counter_taken.swap(true, Ordering::Relaxed) {
294 return Err(new_error!(
295 "GuestCounter has already been created for this sandbox"
296 ));
297 }
298
299 let scratch_size = self.mgr.scratch_mem.mem_size();
300 if (SCRATCH_TOP_GUEST_COUNTER_OFFSET as usize) > scratch_size {
301 return Err(new_error!(
302 "scratch memory too small for guest counter (size {:#x}, need offset {:#x})",
303 scratch_size,
304 SCRATCH_TOP_GUEST_COUNTER_OFFSET,
305 ));
306 }
307
308 let offset = scratch_size - SCRATCH_TOP_GUEST_COUNTER_OFFSET as usize;
309 let deferred_hshm = self.deferred_hshm.clone();
310
311 Ok(GuestCounter {
312 inner: Mutex::new(GuestCounterInner {
313 deferred_hshm,
314 offset,
315 value: 0,
316 }),
317 })
318 }
319
320 // Creates a new uninitialized sandbox from a pre-built snapshot.
321 // Note that since memory configuration is part of the snapshot the only configuration
322 // that can be changed (from the original snapshot) is the configuration defines the behaviour of
323 // `InterruptHandler` on Linux.
324 //
325 // This is ok for now as this is not a public function
326 fn from_snapshot(
327 snapshot: Arc<Snapshot>,
328 cfg: Option<SandboxConfiguration>,
329 #[cfg(crashdump)] binary_path: Option<String>,
330 ) -> Result<Self> {
331 #[cfg(feature = "build-metadata")]
332 log_build_details();
333
334 // hyperlight is only supported on Windows 11 and Windows Server 2022 and later
335 #[cfg(target_os = "windows")]
336 check_windows_version()?;
337
338 let sandbox_cfg = cfg.unwrap_or_default();
339
340 #[cfg(any(crashdump, gdb))]
341 let rt_cfg = {
342 #[cfg(crashdump)]
343 let guest_core_dump = sandbox_cfg.get_guest_core_dump();
344
345 #[cfg(gdb)]
346 let debug_info = sandbox_cfg.get_guest_debug_info();
347
348 SandboxRuntimeConfig {
349 #[cfg(crashdump)]
350 binary_path,
351 #[cfg(gdb)]
352 debug_info,
353 #[cfg(crashdump)]
354 guest_core_dump,
355 // entry_point is set later in set_up_hypervisor_partition
356 // once the entrypoint is resolved from the snapshot
357 #[cfg(crashdump)]
358 entry_point: None,
359 }
360 };
361
362 let mem_mgr_wrapper =
363 SandboxMemoryManager::<ExclusiveSharedMemory>::from_snapshot(snapshot.as_ref())?;
364
365 let host_funcs = Arc::new(Mutex::new(FunctionRegistry::default()));
366
367 let sandbox = Self {
368 host_funcs,
369 mgr: mem_mgr_wrapper,
370 max_guest_log_level: None,
371 config: sandbox_cfg,
372 #[cfg(any(crashdump, gdb))]
373 rt_cfg,
374 load_info: snapshot.load_info(),
375 stack_top_gva: snapshot.stack_top_gva(),
376 #[cfg(feature = "guest-counter")]
377 deferred_hshm: Arc::new(Mutex::new(None)),
378 #[cfg(feature = "guest-counter")]
379 counter_taken: std::sync::atomic::AtomicBool::new(false),
380 };
381
382 // Upstream registered a default "HostPrint" handler here.
383 // After the FB/SCALE → rkyv migration, host functions are
384 // fn_id-indexed and there is no host-print integration; if a
385 // future caller needs guest stdout it can register a handler
386 // explicitly via `register_host_function`.
387
388 crate::debug!("Sandbox created: {:#?}", sandbox);
389
390 Ok(sandbox)
391 }
392
393 /// Creates a new uninitialized sandbox for the given guest environment.
394 ///
395 /// The guest binary can be provided as either a file path or memory buffer.
396 /// An optional configuration can customize memory sizes and sandbox settings.
397 /// After creation, register host functions using [`register`](Self::register)
398 /// before calling [`evolve`](Self::evolve) to complete initialization and create the VM.
399 #[instrument(
400 err(Debug),
401 skip(env),
402 parent = Span::current()
403 )]
404 pub fn new<'a, 'b>(
405 env: impl Into<GuestEnvironment<'a, 'b>>,
406 cfg: Option<SandboxConfiguration>,
407 ) -> Result<Self> {
408 let cfg = cfg.unwrap_or_default();
409 let env = env.into();
410 #[cfg(crashdump)]
411 let binary_path = match &env.guest_binary {
412 GuestBinary::FilePath(path) => Some(path.clone()),
413 GuestBinary::Buffer(_) => None,
414 };
415 let snapshot = Snapshot::from_env(env, cfg)?;
416 Self::from_snapshot(
417 Arc::new(snapshot),
418 Some(cfg),
419 #[cfg(crashdump)]
420 binary_path,
421 )
422 }
423
424 /// Creates and initializes the virtual machine, transforming this into a ready-to-use sandbox.
425 ///
426 /// This method consumes the `UninitializedSandbox` and performs the final initialization
427 /// steps to create the underlying virtual machine. Once evolved, the resulting
428 /// [`MultiUseSandbox`] can execute guest code and handle function calls.
429 #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
430 pub fn evolve(self) -> Result<MultiUseSandbox> {
431 evolve_impl_multi_use(self)
432 }
433
434 /// Returns the total size of the sandbox shared memory region in bytes.
435 ///
436 /// This is useful for placing file mappings at guest physical addresses
437 /// that don't overlap the primary shared memory slot.
438 pub fn shared_mem_size(&self) -> usize {
439 self.mgr.shared_mem.mem_size()
440 }
441
442 /// Sets the maximum log level for guest code execution.
443 ///
444 /// If not set, the log level is determined by the `RUST_LOG` environment variable,
445 /// defaulting to `LevelFilter::Error` if unset.
446 pub fn set_max_guest_log_level(&mut self, log_level: LevelFilter) {
447 self.max_guest_log_level = Some(log_level);
448 }
449
450 /// Registers a host function under `fn_id` that the guest can
451 /// call via the `OutBAction::CallFunction` outb port. The
452 /// closure receives the raw `Request.payload` bytes from the
453 /// guest and returns the raw response payload bytes.
454 pub fn register(&mut self, fn_id: u32, host_func: HostFn) -> Result<()> {
455 register_host_function(self, fn_id, host_func)
456 }
457
458 /// Populate the deferred `HostSharedMemory` slot without running
459 /// the full `evolve()` pipeline. Used in tests where guest boot
460 /// is not available.
461 #[cfg(all(test, feature = "guest-counter"))]
462 fn simulate_build(&self) {
463 let hshm = self.mgr.scratch_mem.as_host_shared_memory();
464 #[allow(clippy::unwrap_used)]
465 {
466 *self.deferred_hshm.lock().unwrap() = Some(hshm);
467 }
468 }
469}
470// Check to see if the current version of Windows is supported
471// Hyperlight is only supported on Windows 11 and Windows Server 2022 and later
472#[cfg(target_os = "windows")]
473fn check_windows_version() -> Result<()> {
474 use windows_version::{OsVersion, is_server};
475 const WINDOWS_MAJOR: u32 = 10;
476 const WINDOWS_MINOR: u32 = 0;
477 const WINDOWS_PACK: u32 = 0;
478
479 // Windows Server 2022 has version numbers 10.0.20348 or greater
480 if is_server() {
481 if OsVersion::current() < OsVersion::new(WINDOWS_MAJOR, WINDOWS_MINOR, WINDOWS_PACK, 20348)
482 {
483 return Err(new_error!(
484 "Hyperlight Requires Windows Server 2022 or newer"
485 ));
486 }
487 } else if OsVersion::current()
488 < OsVersion::new(WINDOWS_MAJOR, WINDOWS_MINOR, WINDOWS_PACK, 22000)
489 {
490 return Err(new_error!("Hyperlight Requires Windows 11 or newer"));
491 }
492 Ok(())
493}