Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Prerelease] - Unreleased

### Added

* Add `Snapshot::save`, `Snapshot::load` and `Snapshot::checked_load` to save snapshots to disk and load them in another process.
* Add `SandboxBuilder::from_snapshot` to create a sandbox from a snapshot.
* Add `WasmSandbox::snapshot` and `WasmSandbox::restore`.

### Changed

* Change `LoadedWasmSandbox::snapshot`, `LoadedWasmSandbox::restore` and `WasmSandbox::load_from_snapshot` to use `Snapshot<LoadedWasmSandbox>` instead of `Arc<Snapshot>`.
* Make `unload_module` return to the snapshot's runtime state after `LoadedWasmSandbox::restore` or `WasmSandbox::load_from_snapshot`.
* Count sandboxes created from snapshots in `sandbox_loads_total`.

## [v0.15.0] - 2026-08

### Changed
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ examples-components target=default-target features="": (build-rust-component-exa

run-examples-components target=default-target features="":
{{ wit-world }} cargo run {{ if features =="" {''} else {"--no-default-features -F kvm -F " + features } }} --profile={{ if target == "debug" {"dev"} else { target } }} --example component_example
{{ wit-world }} cargo test -p hyperlight-wasm {{ if features =="" {''} else {"--no-default-features -F kvm -F " + features } }} --profile={{ if target == "debug" {"dev"} else { target } }} --lib snapshot::tests::component
{{ wit-world-c }} cargo run {{ if features =="" {''} else {"--no-default-features -F kvm -F " + features } }} --profile={{ if target == "debug" {"dev"} else { target } }} --example c-component
{{ wit-world }} {{ wit-world-name-greeter }} cargo run {{ if features =="" {''} else {"--no-default-features -F kvm -F " + features } }} --profile={{ if target == "debug" {"dev"} else { target } }} --example component_greeter_example

Expand Down
2 changes: 1 addition & 1 deletion docs/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The following metrics are provided and are enabled by default:
* `proto_wasm_sandboxes_total` - A counter indicating the total number of proto wasm sandboxes created during the lifetime of the process
* `wasm_sandboxes_total` - A counter indicating the total number of wasm sandboxes created during the lifetime of the process
* `loaded_wasm_sandboxes_total` - A counter indicating the total number of loaded wasm sandboxes created during the lifetime of the process
* `sandbox_loads_total` - A counter indicating how many times a wasm sandbox has been loaded into a loaded wasm sandbox during the lifetime of the process
* `sandbox_loads_total` - A counter indicating how many loaded wasm sandboxes have been created by loading a module or restoring a snapshot during the lifetime of the process
* `sandbox_unloads_total` - A counter indicating how many times a loaded wasm sandbox has been unloaded into a wasm sandbox during the lifetime of the process


Expand Down
3 changes: 2 additions & 1 deletion src/hyperlight_wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ cfg-if = "1"
metrics = "0.24.5"
env_logger = "0.11.10"
hyperlight-wasm-runtime.workspace = true
serde = { version = "1.0", features = ["derive"] }

[target.'cfg(windows)'.dependencies]
windows = { version = "0.62", features = ["Win32_System_Threading"] }
Expand All @@ -91,10 +92,10 @@ tracing = "0.1.44"
tracing-subscriber = {version = "0.3.23", features = ["std", "env-filter"]}
tracing-opentelemetry = "0.33.0"
uuid = { version = "1.23.3", features = ["v4"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
semver = "1.0.28"
bytes = { version = "1", default-features = false }
tempfile = "3"

[build-dependencies]
cfg_aliases = "0.2.1"
Expand Down
32 changes: 27 additions & 5 deletions src/hyperlight_wasm/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,9 @@ fn find_target_dir() -> PathBuf {
target_dir.to_path_buf()
}

fn build_wasm_runtime() -> PathBuf {
/// Build the guest runtime and return its binary path and component interface hash.
/// The hash is `Some` when `WIT_WORLD` selects component mode and `None` for core modules.
fn build_wasm_runtime() -> (PathBuf, Option<String>) {
let profile = env::var_os("PROFILE").unwrap();

// Get the current target directory.
Expand All @@ -127,6 +129,19 @@ fn build_wasm_runtime() -> PathBuf {
println!("cargo::rerun-if-changed={}", runtime_dir.display());
println!("cargo::rerun-if-env-changed=WIT_WORLD");
println!("cargo::rerun-if-env-changed=WIT_WORLD_NAME");
let component_interface = env::var_os("WIT_WORLD").map(|path| {
let path = runtime_dir.join(path);
println!("cargo::rerun-if-changed={}", path.display());
let bytes = fs::read(path).expect("could not read WIT_WORLD");
let mut hash = blake3::Hasher::new();
hash.update(blake3::hash(&bytes).as_bytes());
// One file can hold several worlds. Unset selects the last one.
if let Ok(world) = env::var("WIT_WORLD_NAME") {
hash.update(b"\x01");
hash.update(world.as_bytes());
}
hash.finalize().to_string()
});
// the PROFILE env var unfortunately only gives us 1 bit of "dev or release"
let cargo_profile = if profile == "debug" { "dev" } else { "release" };

Expand Down Expand Up @@ -184,7 +199,7 @@ fn build_wasm_runtime() -> PathBuf {
);
}

path
(path, component_interface)
} else {
panic!(
"could not find hyperlight-wasm-runtime after building it (expected {:?})",
Expand All @@ -194,16 +209,23 @@ fn build_wasm_runtime() -> PathBuf {
}

fn main() -> Result<()> {
let built_runtime = build_wasm_runtime();
let (built_runtime, component_interface) = build_wasm_runtime();

println!("cargo::rustc-check-cfg=cfg(component)");
if component_interface.is_some() {
println!("cargo::rustc-cfg=component");
}

let out_dir = env::var_os("OUT_DIR").unwrap();
let wasm_runtime_resource = Path::new(&out_dir).join("hyperlight-wasm-runtime");
// Copy to OUT_DIR to preserve this runtime when other feature builds overwrite the shared binary.
fs::copy(&built_runtime, &wasm_runtime_resource)?;
let dest_path = Path::new(&out_dir).join("wasm_runtime_resource.rs");
let contents = format!(
"pub (super) static WASM_RUNTIME: [u8; include_bytes!({name:?}).len()] = *include_bytes!({name:?});",
name = wasm_runtime_resource.as_os_str()
"pub (super) static WASM_RUNTIME: [u8; include_bytes!({name:?}).len()] = *include_bytes!({name:?});
pub (super) const WASM_RUNTIME_COMPONENT_INTERFACE: Option<&str> = {component_interface:?};",
name = wasm_runtime_resource.as_os_str(),
component_interface = component_interface.as_deref(),
);

fs::write(dest_path, contents).unwrap();
Expand Down
8 changes: 6 additions & 2 deletions src/hyperlight_wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ limitations under the License.
#![deny(dead_code, missing_docs, unused_mut)]
//! This crate provides a Hyperlight implementation for WebAssembly (Wasm) guest code.

// Generated component bindings in tests use `alloc` paths.
#[cfg(all(test, component))]
extern crate alloc;

/// provides details about the build
pub mod build_info;
mod sandbox;
Expand Down Expand Up @@ -57,13 +61,13 @@ pub use hyperlight_host::is_hypervisor_present;
pub use hyperlight_host::new_error;
/// The lifecycle state of a sandbox.
pub use hyperlight_host::sandbox::SandboxStatus;
/// A snapshot of the memory of a sandbox at a given point in time.
pub use hyperlight_host::sandbox::snapshot::Snapshot;
/// OCI Image Layout reference types used by [`Snapshot::save`] /
/// [`Snapshot::load`] to identify snapshots inside an OCI Image Layout
/// directory. Re-exported here so downstream crates do not need a
/// direct dependency on `hyperlight-host`.
pub use hyperlight_host::sandbox::snapshot::{OciDigest, OciReference, OciTag};
pub use sandbox::sandbox_builder::{NewSandbox, SnapshotSource};
pub use sandbox::snapshot::{Snapshot, SnapshotTarget};

/// Get the build information for this version of hyperlight-wasm
pub fn get_build_info() -> BuildInfo {
Expand Down
36 changes: 25 additions & 11 deletions src/hyperlight_wasm/src/sandbox/loaded_wasm_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ use std::sync::Arc;

use hyperlight_host::func::{ParameterTuple, SupportedReturnType};
use hyperlight_host::hypervisor::InterruptHandle;
use hyperlight_host::sandbox::snapshot::Snapshot;
use hyperlight_host::sandbox::{Callable, SandboxStatus};
use hyperlight_host::{Result, Sandbox, log_then_return, new_error};

use super::metrics::METRIC_TOTAL_LOADED_WASM_SANDBOXES;
use super::snapshot::Snapshot;
use super::wasm_sandbox::WasmSandbox;
use crate::sandbox::metrics::{METRIC_ACTIVE_LOADED_WASM_SANDBOXES, METRIC_SANDBOX_UNLOADS};
use crate::sandbox::metrics::{
METRIC_ACTIVE_LOADED_WASM_SANDBOXES, METRIC_SANDBOX_LOADS, METRIC_SANDBOX_UNLOADS,
};

/// A sandbox that has both a Wasm engine and an arbitrary Wasm module
/// loaded into memory.
Expand All @@ -42,7 +44,7 @@ pub struct LoadedWasmSandbox {
// because of this we cannot implement drop without making inner an Option (alternatively we could make Sandbox Copy but that would introduce other issues)
inner: Option<Sandbox>,
// The state the sandbox was in before loading a wasm module. Used for transitioning back to a `WasmSandbox` (unloading the wasm module).
runtime_snapshot: Option<Arc<Snapshot>>,
runtime_snapshot: Option<Snapshot<WasmSandbox>>,
}

impl LoadedWasmSandbox {
Expand Down Expand Up @@ -86,15 +88,22 @@ impl LoadedWasmSandbox {
/// Returns `Err(HyperlightError::PoisonedSandbox)` if the sandbox is in a
/// poisoned state. Use [`restore()`](Self::restore) with a previously
/// taken snapshot to recover before taking a new snapshot.
pub fn snapshot(&mut self) -> Result<Arc<Snapshot>> {
pub fn snapshot(&mut self) -> Result<Snapshot<LoadedWasmSandbox>> {
let runtime = self
.runtime_snapshot
.as_ref()
.ok_or_else(|| new_error!("No runtime snapshot"))?;
match &mut self.inner {
Some(inner) => inner.snapshot(),
Some(inner) => Ok(Snapshot::loaded(inner.snapshot()?, runtime)),
None => log_then_return!("No inner Sandbox to snapshot"),
}
}

/// Restore the state of the sandbox to the state captured in the given snapshot.
///
/// A later [`unload_module()`](Self::unload_module) returns to the runtime
/// state from before the given snapshot's module was loaded.
///
/// This method clears the poisoned state if the sandbox was poisoned, making
/// it usable again for guest function calls.
///
Expand All @@ -105,9 +114,14 @@ impl LoadedWasmSandbox {
/// 1. Clear the poisoned state
/// 2. Reset memory to the snapshot state
/// 3. Allow subsequent [`call_guest_function()`](Self::call_guest_function) calls to succeed
pub fn restore(&mut self, snapshot: Arc<Snapshot>) -> Result<()> {
pub fn restore(&mut self, snapshot: Snapshot<LoadedWasmSandbox>) -> Result<()> {
let runtime = snapshot.runtime_snapshot();
match &mut self.inner {
Some(inner) => inner.restore(snapshot),
Some(inner) => {
inner.restore(snapshot.inner())?;
self.runtime_snapshot = Some(runtime);
Ok(())
}
None => log_then_return!("No inner Sandbox to restore"),
}
}
Expand All @@ -131,15 +145,15 @@ impl LoadedWasmSandbox {
.take()
.ok_or_else(|| new_error!("No snapshot of the WasmSandbox to unload"))?;

WasmSandbox::new_from_loaded(sandbox, snapshot).inspect(|_| {
metrics::counter!(METRIC_SANDBOX_UNLOADS).increment(1);
})
metrics::counter!(METRIC_SANDBOX_UNLOADS).increment(1);
Ok(WasmSandbox::dirty(sandbox, snapshot))
}

pub(super) fn new(
inner: Sandbox,
runtime_snapshot: Arc<Snapshot>,
runtime_snapshot: Snapshot<WasmSandbox>,
) -> Result<LoadedWasmSandbox> {
metrics::counter!(METRIC_SANDBOX_LOADS).increment(1);
metrics::gauge!(METRIC_ACTIVE_LOADED_WASM_SANDBOXES).increment(1);
metrics::counter!(METRIC_TOTAL_LOADED_WASM_SANDBOXES).increment(1);
Ok(LoadedWasmSandbox {
Expand Down
1 change: 1 addition & 0 deletions src/hyperlight_wasm/src/sandbox/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub(crate) mod loaded_wasm_sandbox;
pub(crate) mod metrics;
/// A builder for a WasmSandbox.
pub(crate) mod sandbox_builder;
pub(crate) mod snapshot;
/// A Wasm Sandbox that can load a module.
pub(crate) mod wasm_sandbox;

Expand Down
Loading
Loading