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::atomic::{AtomicBool, Ordering};
21use std::sync::{Arc, Mutex};
22
23use tracing::{Span, instrument};
24use tracing_core::LevelFilter;
25
26use super::host_funcs::FunctionRegistry;
27use super::snapshot::Snapshot;
28use super::uninitialized_evolve::evolve_impl_multi_use;
29use crate::func::HostFn;
30use crate::func::host_functions::register_host_function;
31use crate::mem::memory_region::{DEFAULT_GUEST_BLOB_MEM_FLAGS, MemoryRegionFlags};
32use crate::mem::mgr::SandboxMemoryManager;
33use crate::mem::shared_mem::{ExclusiveSharedMemory, SharedMemory};
34use crate::sandbox::SandboxConfiguration;
35use crate::{HyperlightError, MultiUseSandbox, Result, new_error};
36
37/// One-shot latch enforcing the at-most-one-sandbox-per-process
38/// substrate limit. Set by the FIRST construction attempt and never
39/// cleared — not on sandbox drop, not on a failed construction:
40/// [`nub_host_common::layout::reserve_guest_va_range`] reserves the
41/// process-wide guest-VA window exactly once (its `OnceLock` makes a
42/// second reservation a silent no-op), and every sandbox
43/// `MAP_FIXED`-overlays its kernel-shadow at the single fixed VA
44/// inside that window (`FixedVaMapping`, whose `Drop` munmaps it).
45/// A second sandbox — concurrent or sequential — would therefore
46/// silently corrupt the first one's live guest memory instead of
47/// failing, so we refuse loudly here, before any resource is
48/// acquired. (Even create → drop → create is unsafe: the drop
49/// munmaps the kernel-shadow hole out of the reservation, so an
50/// unrelated allocation can land there and be clobbered by the next
51/// MAP_FIXED — see the historical corruption note in
52/// `javm-bench/examples/smoke.rs`.)
53static SANDBOX_CREATED: AtomicBool = AtomicBool::new(false);
54
55/// A preliminary sandbox that represents allocated memory and registered host functions,
56/// but has not yet created the underlying virtual machine.
57///
58/// This struct holds the configuration and setup needed for a sandbox without actually
59/// creating the VM. It allows you to:
60/// - Set up memory layout and load guest binary data
61/// - Register host functions that will be available to the guest
62/// - Configure sandbox settings before VM creation
63///
64/// The virtual machine is not created until you call [`evolve`](Self::evolve) to transform
65/// this into an initialized [`MultiUseSandbox`].
66pub struct UninitializedSandbox {
67    /// Registered host functions
68    pub(crate) host_funcs: Arc<Mutex<FunctionRegistry>>,
69    /// The memory manager for the sandbox.
70    pub(crate) mgr: SandboxMemoryManager<ExclusiveSharedMemory>,
71    pub(crate) max_guest_log_level: Option<LevelFilter>,
72    pub(crate) config: SandboxConfiguration,
73    pub(crate) load_info: crate::mem::exe::LoadInfo,
74    // This is needed to convey the stack pointer between the snapshot
75    // and the HyperlightVm creation
76    pub(crate) stack_top_gva: u64,
77}
78
79impl Debug for UninitializedSandbox {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        f.debug_struct("UninitializedSandbox")
82            .field("memory_layout", &self.mgr.layout)
83            .finish()
84    }
85}
86
87/// A `GuestBinary` is either a buffer or the file path to some data (e.g., a guest binary).
88#[derive(Debug)]
89pub enum GuestBinary<'a> {
90    /// A buffer containing the GuestBinary
91    Buffer(&'a [u8]),
92    /// A path to the GuestBinary
93    FilePath(String),
94}
95impl<'a> GuestBinary<'a> {
96    /// If the guest binary is identified by a file, canonicalise the path
97    ///
98    /// For [`GuestBinary::FilePath`], this resolves the path to its canonical
99    /// form. For [`GuestBinary::Buffer`], this method is a no-op.
100    /// TODO: Maybe we should make the GuestEnvironment or
101    ///       GuestBinary constructors crate-private and turn this
102    ///       into an invariant on one of those types.
103    pub fn canonicalize(&mut self) -> Result<()> {
104        if let GuestBinary::FilePath(p) = self {
105            let canon = Path::new(&p)
106                .canonicalize()
107                .map_err(|e| new_error!("GuestBinary not found: '{}': {}", p, e))?
108                .into_os_string()
109                .into_string()
110                .map_err(|e| new_error!("Error converting OsString to String: {:?}", e))?;
111            *self = GuestBinary::FilePath(canon)
112        }
113        Ok(())
114    }
115}
116
117/// A `GuestBlob` containing data and the permissions for its use.
118#[derive(Debug)]
119pub struct GuestBlob<'a> {
120    /// The data contained in the blob.
121    pub data: &'a [u8],
122    /// The permissions for the blob in memory.
123    /// By default, it's READ
124    pub permissions: MemoryRegionFlags,
125}
126
127impl<'a> From<&'a [u8]> for GuestBlob<'a> {
128    fn from(data: &'a [u8]) -> Self {
129        GuestBlob {
130            data,
131            permissions: DEFAULT_GUEST_BLOB_MEM_FLAGS,
132        }
133    }
134}
135
136/// Container for a guest binary and optional initialization data.
137///
138/// This struct combines a guest binary (either from a file or memory buffer) with
139/// optional data that will be available to the guest during execution.
140#[derive(Debug)]
141pub struct GuestEnvironment<'a, 'b> {
142    /// The guest binary, which can be a file path or a buffer.
143    pub guest_binary: GuestBinary<'a>,
144    /// An optional guest blob, which can be used to provide additional data to the guest.
145    pub init_data: Option<GuestBlob<'b>>,
146}
147
148impl<'a, 'b> GuestEnvironment<'a, 'b> {
149    /// Creates a new `GuestEnvironment` with the given guest binary and an optional guest blob.
150    pub fn new(guest_binary: GuestBinary<'a>, init_data: Option<&'b [u8]>) -> Self {
151        GuestEnvironment {
152            guest_binary,
153            init_data: init_data.map(GuestBlob::from),
154        }
155    }
156}
157
158impl<'a> From<GuestBinary<'a>> for GuestEnvironment<'a, '_> {
159    fn from(guest_binary: GuestBinary<'a>) -> Self {
160        GuestEnvironment {
161            guest_binary,
162            init_data: None,
163        }
164    }
165}
166
167impl UninitializedSandbox {
168    // Creates a new uninitialized sandbox from a pre-built snapshot.
169    // Note that since memory configuration is part of the snapshot the only configuration
170    // that can be changed (from the original snapshot) is that defines the behaviour of
171    // `InterruptHandle` on Linux.
172    //
173    // This is ok for now as this is not a public function
174    fn from_snapshot(snapshot: Arc<Snapshot>, cfg: Option<SandboxConfiguration>) -> Result<Self> {
175        let sandbox_cfg = cfg.unwrap_or_default();
176
177        let mem_mgr_wrapper =
178            SandboxMemoryManager::<ExclusiveSharedMemory>::from_snapshot(snapshot.as_ref())?;
179
180        let host_funcs = Arc::new(Mutex::new(FunctionRegistry::default()));
181
182        let sandbox = Self {
183            host_funcs,
184            mgr: mem_mgr_wrapper,
185            max_guest_log_level: None,
186            config: sandbox_cfg,
187            load_info: snapshot.load_info(),
188            stack_top_gva: snapshot.stack_top_gva(),
189        };
190
191        // Upstream registered a default "HostPrint" handler here.
192        // After the FB/SCALE → rkyv migration, host functions are
193        // fn_id-indexed and there is no host-print integration; if a
194        // future caller needs guest stdout it can register a handler
195        // explicitly via `register_host_function`.
196
197        crate::debug!("Sandbox created:  {:#?}", sandbox);
198
199        Ok(sandbox)
200    }
201
202    /// Creates a new uninitialized sandbox for the given guest environment.
203    ///
204    /// The guest binary can be provided as either a file path or memory buffer.
205    /// An optional configuration can customize memory sizes and sandbox settings.
206    /// After creation, register host functions using [`register`](Self::register)
207    /// before calling [`evolve`](Self::evolve) to complete initialization and create the VM.
208    #[instrument(
209        err(Debug),
210        skip(env),
211        parent = Span::current()
212    )]
213    pub fn new<'a, 'b>(
214        env: impl Into<GuestEnvironment<'a, 'b>>,
215        cfg: Option<SandboxConfiguration>,
216    ) -> Result<Self> {
217        // Claim the process-wide sandbox slot BEFORE acquiring any
218        // resource, so a rejected second construction leaves no
219        // partial state behind. See `SANDBOX_CREATED`.
220        if SANDBOX_CREATED.swap(true, Ordering::SeqCst) {
221            return Err(HyperlightError::SandboxAlreadyCreated());
222        }
223        let cfg = cfg.unwrap_or_default();
224        let env = env.into();
225        let snapshot = Snapshot::from_env(env, cfg)?;
226        Self::from_snapshot(Arc::new(snapshot), Some(cfg))
227    }
228
229    /// Creates and initializes the virtual machine, transforming this into a ready-to-use sandbox.
230    ///
231    /// This method consumes the `UninitializedSandbox` and performs the final initialization
232    /// steps to create the underlying virtual machine. Once evolved, the resulting
233    /// [`MultiUseSandbox`] can execute guest code and handle function calls.
234    #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")]
235    pub fn evolve(self) -> Result<MultiUseSandbox> {
236        evolve_impl_multi_use(self)
237    }
238
239    /// Returns the total size of the sandbox shared memory region in bytes.
240    ///
241    /// This is useful for placing file mappings at guest physical addresses
242    /// that don't overlap the primary shared memory slot.
243    pub fn shared_mem_size(&self) -> usize {
244        self.mgr.shared_mem.mem_size()
245    }
246
247    /// Sets the maximum log level for guest code execution.
248    ///
249    /// If not set, the log level is determined by the `RUST_LOG` environment variable,
250    /// defaulting to `LevelFilter::Error` if unset.
251    pub fn set_max_guest_log_level(&mut self, log_level: LevelFilter) {
252        self.max_guest_log_level = Some(log_level);
253    }
254
255    /// Registers a host function under `fn_id` that the guest can
256    /// call via the `OutBAction::CallFunction` outb port. The
257    /// closure receives the raw `Request.payload` bytes from the
258    /// guest and returns the raw response payload bytes.
259    pub fn register(&mut self, fn_id: u32, host_func: HostFn) -> Result<()> {
260        register_host_function(self, fn_id, host_func)
261    }
262}