Skip to main content

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}