Skip to content

Commit 3b2330b

Browse files
committed
Support loading a wasm module/component via direct mapping
This adds support for directly mapping a host buffer containing a wasm module/component into the guest, enabling the use of mmap() on the host to share a single module/component across multiple sandboxes. Signed-off-by: Lucy Menon <[email protected]>
1 parent 917a1da commit 3b2330b

File tree

6 files changed

+289
-21
lines changed

6 files changed

+289
-21
lines changed

flake.lock

Lines changed: 44 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
{
2+
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
3+
inputs.nixpkgs-mozilla.url = "github:mozilla/nixpkgs-mozilla/master";
4+
outputs = { self, nixpkgs, nixpkgs-mozilla, ... } @ inputs:
5+
let token = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IktRMnRBY3JFN2xCYVZWR0JtYzVGb2JnZEpvNCIsImtpZCI6IktRMnRBY3JFN2xCYVZWR0JtYzVGb2JnZEpvNCJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuY29yZS53aW5kb3dzLm5ldC8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC83MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcvIiwiaWF0IjoxNzIyODc3MzM2LCJuYmYiOjE3MjI4NzczMzYsImV4cCI6MTcyMjg4MjAyOCwiX2NsYWltX25hbWVzIjp7Imdyb3VwcyI6InNyYzEifSwiX2NsYWltX3NvdXJjZXMiOnsic3JjMSI6eyJlbmRwb2ludCI6Imh0dHBzOi8vZ3JhcGgud2luZG93cy5uZXQvNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3L3VzZXJzLzFkNDgzMWMxLWVhYTgtNGY2OS1hZTVjLTE4MTQwMjBlZTJlYy9nZXRNZW1iZXJPYmplY3RzIn19LCJhY3IiOiIxIiwiYWlvIjoiQVZRQXEvOFhBQUFBK3V0TC9hdXdsU1ppQXp1T1pjdk14aERWay9lTlJaR0tiSFNOTjZXcS9VRTlRZGJTOEdmeWFadDUyM2dsT3FOZVdDckVkQ2E1RFV5c2lHcTdJK1ZMTzJoYW9UVzJ6MFhzVm1VTEMwK1RHUGs9IiwiYW1yIjpbInB3ZCIsInJzYSIsIm1mYSJdLCJhcHBpZCI6IjA0YjA3Nzk1LThkZGItNDYxYS1iYmVlLTAyZjllMWJmN2I0NiIsImFwcGlkYWNyIjoiMCIsImNhcG9saWRzX2xhdGViaW5kIjpbIjI5Mzk5Y2Y5LTliNmItNDIwNS1iNWIzLTEzYTEzNGU5YjIzMyJdLCJkZXZpY2VpZCI6IjVmZThlYWYzLTYzYWUtNGY3Ny1hMDkxLTI4MzcxZTRlZmRlMyIsImZhbWlseV9uYW1lIjoiTWVub24iLCJnaXZlbl9uYW1lIjoiTHVjeSIsImlkdHlwIjoidXNlciIsImlwYWRkciI6IjkyLjQwLjE3MC4yMjAiLCJuYW1lIjoiTHVjeSBNZW5vbiIsIm9pZCI6IjFkNDgzMWMxLWVhYTgtNGY2OS1hZTVjLTE4MTQwMjBlZTJlYyIsIm9ucHJlbV9zaWQiOiJTLTEtNS0yMS0yMTI3NTIxMTg0LTE2MDQwMTI5MjAtMTg4NzkyNzUyNy03ODMzODE4MyIsInB1aWQiOiIxMDAzMjAwM0FBNkI0MTNFIiwicmgiOiIwLkFSb0F2NGo1Y3ZHR3IwR1JxeTE4MEJIYlIwWklmM2tBdXRkUHVrUGF3ZmoyTUJNYUFIUS4iLCJzY3AiOiJ1c2VyX2ltcGVyc29uYXRpb24iLCJzdWIiOiJ0bmo3a0hTb1ZIM3V6eGVsYkVfNnRFSEVoaDRBaWVtTGZ4aThDNld1NzBvIiwidGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3IiwidW5pcXVlX25hbWUiOiJtZW5vbmx1Y3lAbWljcm9zb2Z0LmNvbSIsInVwbiI6Im1lbm9ubHVjeUBtaWNyb3NvZnQuY29tIiwidXRpIjoiaFRXM0lFVmtCRXVaWFRTSnoxNW1BQSIsInZlciI6IjEuMCIsIndpZHMiOlsiYjc5ZmJmNGQtM2VmOS00Njg5LTgxNDMtNzZiMTk0ZTg1NTA5Il0sInhtc19jYWUiOiIxIiwieG1zX2NjIjpbIkNQMSJdLCJ4bXNfZmlsdGVyX2luZGV4IjpbIjI2Il0sInhtc19pZHJlbCI6IjI2IDEiLCJ4bXNfcmQiOiIwLjQyTGxZQlJpbEFJQSIsInhtc19zc20iOiIxIiwieG1zX3RjZHQiOjEyODkyNDE1NDd9.t5d56DG8TDlkLYJPIVHFcftn4PbrPOhzwIQoyJRqe4s1VVgscqhNQH4PYqPjizkHy76lZM0cI6a3lrRfVbr1wXBDk82jLyxQ_AXR0yMN4RmrrTRK5M5p5sKHtXq85rTohHl2RSeu7VsWNNBVMsy1Efh6UN095zFZvzntbn-N2JXjloprrz5JcHb1hasT9-o_tsebIM2jefbj7ggpuiGRcbYM3i5mgLVGd19gYzlpAq7YyiE-SvHtLu3cSSms4KwDeoi3QpZiV_5ok1dsZxaonGplaqXeE_pRM92JCvk1paahCKNmrOjZPcJ4hXUlH4q7OY2Hm6MY4GKcTG_xXXoWaQ"; in
6+
{
7+
devShells.x86_64-linux.default = with import nixpkgs { system = "x86_64-linux"; overlays = [ (import (nixpkgs-mozilla + "/rust-overlay.nix")) ]; }; let
8+
stable = rustChannelOf {
9+
date = "2025-02-20";
10+
channel = "stable";
11+
sha256 = "sha256-AJ6LX/Q/Er9kS15bn9iflkUwcgYqRQxiOIL2ToVAXaU=";
12+
};
13+
nightly = rustChannelOf {
14+
date = "2025-06-04";
15+
channel = "nightly";
16+
sha256 = "sha256-eFuFA5spScrde7b7lSV5QAND1m0+Ds6gbVODfDE3scg=";
17+
};
18+
rust_stable = stable.rust.override {
19+
targets = [ "x86_64-unknown-linux-gnu" "x86_64-pc-windows-msvc" "x86_64-unknown-none" "wasm32-wasip1" "wasm32-unknown-unknown" ];
20+
};
21+
rust_nightly = nightly.rust.override {
22+
targets = [ "x86_64-unknown-linux-gnu" "x86_64-pc-windows-msvc" "x86_64-unknown-none" "wasm32-wasip1" "wasm32-unknown-unknown" ];
23+
};
24+
rust-platform = makeRustPlatform {
25+
cargo = rust_stable;
26+
rustc = rust_stable;
27+
};
28+
in ((rust-platform.buildRustPackage.override { stdenv = clangStdenv; }) rec {
29+
pname = "hyperlight";
30+
version = "0.0.0";
31+
src = lib.cleanSource ./.;
32+
#cargoHash = lib.fakeHash;
33+
#cargoHash = "sha256-Z4dkYH1BBSUpqiHETkIi+i13FD1q5CyB0NuGHUxhLm0=";
34+
cargoHash = "sha256-CsHcz91S5NHdoHgQVLFizyd5rUXMlMDHEN67JrQ6Xls=";
35+
nativeBuildInputs = [
36+
azure-cli
37+
just
38+
dotnet-sdk_6
39+
clang
40+
llvmPackages_18.llvm
41+
gh
42+
lld
43+
valgrind
44+
pkg-config
45+
ffmpeg
46+
mkvtoolnix
47+
wasm-tools
48+
nodejs # todo - just for now
49+
cargo-component
50+
wasm-tools
51+
];
52+
buildInputs = [
53+
pango
54+
cairo
55+
openssl
56+
];
57+
auditable = false;
58+
depsExtraArgs = {
59+
CARGO_REGISTRIES_HYPERLIGHT_REDIST_TOKEN = token;
60+
CARGO_REGISTRIES_HYPERLIGHT_PACKAGES_TOKEN = token;
61+
};
62+
KVM_SHOULD_BE_PRESENT = "true";
63+
LIBCLANG_PATH = "${pkgs.llvmPackages_18.libclang.lib}/lib";
64+
RUST_NIGHTLY = "${rust_nightly}";
65+
shellHook = ''
66+
rustc_nightly() {
67+
(export PATH="${rust_nightly}/bin/:$PATH"; ${rust_nightly}/bin/rustc "$@")
68+
}
69+
rustc_stable() {
70+
(export PATH="${rust_stable}/bin/:$PATH"; ${rust_stable}/bin/rustc "$@")
71+
}
72+
cargo_nightly() {
73+
(export PATH="${rust_nightly}/bin/:$PATH"; ${rust_nightly}/bin/cargo "$@")
74+
}
75+
cargo_stable() {
76+
(export PATH="${rust_stable}/bin/:$PATH"; ${rust_stable}/bin/cargo "$@")
77+
}
78+
'';
79+
}).overrideAttrs(oA: {
80+
hardeningDisable = [ "all" ];
81+
});
82+
};
83+
}

src/hyperlight_wasm/src/sandbox/wasm_sandbox.rs

Lines changed: 69 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
use std::path::Path;
1818

1919
use hyperlight_host::func::call_ctx::MultiUseGuestCallContext;
20+
use hyperlight_host::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType};
2021
use hyperlight_host::sandbox::Callable;
2122
use hyperlight_host::sandbox_state::sandbox::{EvolvableSandbox, Sandbox};
2223
use hyperlight_host::sandbox_state::transition::MultiUseContextCallback;
@@ -42,6 +43,7 @@ pub struct WasmSandbox {
4243

4344
impl Sandbox for WasmSandbox {}
4445

46+
const MAPPED_BINARY_VA: u64 = 0x1_0000_0000u64;
4547
impl WasmSandbox {
4648
/// Create a new WasmSandBox from a `MultiUseSandbox`.
4749
/// This function should be used to create a new `WasmSandbox` from a ProtoWasmSandbox.
@@ -59,34 +61,87 @@ impl WasmSandbox {
5961
/// Before you can call guest functions in the sandbox, you must call
6062
/// this function and use the returned value to call guest functions.
6163
pub fn load_module(self, file: impl AsRef<Path>) -> Result<LoadedWasmSandbox> {
62-
let wasm_bytes = std::fs::read(file)?;
63-
self.load_module_inner(wasm_bytes)
64+
let func = Box::new(move |call_ctx: &mut MultiUseGuestCallContext| {
65+
if let Ok(len) = call_ctx.map_file_cow(file.as_ref(), MAPPED_BINARY_VA) {
66+
call_ctx.call("LoadWasmModulePhys", (MAPPED_BINARY_VA, len))
67+
} else {
68+
let wasm_bytes = std::fs::read(file)?;
69+
Self::load_module_from_buffer_transition_func(wasm_bytes)(call_ctx)
70+
}
71+
});
72+
self.load_module_inner(func)
6473
}
6574

66-
/// Load a Wasm module from a buffer of bytes into the sandbox and return a `LoadedWasmSandbox`
67-
/// able to execute code in the loaded Wasm Module.
75+
/// Load a Wasm module that is currently present in a buffer in
76+
/// host memory, by mapping the host memory directly into the
77+
/// sandbox.
6878
///
69-
/// Before you can call guest functions in the sandbox, you must call
70-
/// this function and use the returned value to call guest functions.
71-
pub fn load_module_from_buffer(self, buffer: &[u8]) -> Result<LoadedWasmSandbox> {
72-
self.load_module_inner(buffer.to_vec())
79+
/// Depending on the host platform, there are likely alignment
80+
/// requirements of at least one page for base and len
81+
///
82+
/// # Safety
83+
/// It is the caller's responsibility to ensure that the host side
84+
/// of the region remains intact and is not written to until the
85+
/// produced LoadedWasmSandbox is discarded or devolved.
86+
pub unsafe fn load_module_by_mapping(
87+
self,
88+
base: *mut libc::c_void,
89+
len: usize,
90+
) -> Result<LoadedWasmSandbox> {
91+
let func = Box::new(move |call_ctx: &mut MultiUseGuestCallContext| {
92+
let guest_base: usize = MAPPED_BINARY_VA as usize;
93+
let rgn = MemoryRegion {
94+
host_region: base as usize..base.wrapping_add(len) as usize,
95+
guest_region: guest_base..guest_base + len,
96+
flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE,
97+
region_type: MemoryRegionType::Heap,
98+
};
99+
if let Ok(()) = unsafe { call_ctx.map_region(&rgn) } {
100+
call_ctx.call("LoadWasmModulePhys", (MAPPED_BINARY_VA, len as u64))
101+
} else {
102+
let wasm_bytes =
103+
unsafe { std::slice::from_raw_parts(base as *const u8, len).to_vec() };
104+
Self::load_module_from_buffer_transition_func(wasm_bytes)(call_ctx)
105+
}
106+
});
107+
self.load_module_inner(func)
73108
}
74109

75-
fn load_module_inner(mut self, wasm_bytes: Vec<u8>) -> Result<LoadedWasmSandbox> {
76-
let func = Box::new(move |call_ctx: &mut MultiUseGuestCallContext| {
77-
let len = wasm_bytes.len() as i32;
78-
let res: i32 = call_ctx.call("LoadWasmModule", (wasm_bytes, len))?;
110+
// todo: take a slice rather than a vec (requires somewhat
111+
// refactoring the flatbuffers stuff maybe)
112+
fn load_module_from_buffer_transition_func(
113+
buffer: Vec<u8>,
114+
) -> impl FnOnce(&mut MultiUseGuestCallContext) -> Result<()> {
115+
move |call_ctx: &mut MultiUseGuestCallContext| {
116+
let len = buffer.len() as i32;
117+
let res: i32 = call_ctx.call("LoadWasmModule", (buffer, len))?;
79118
if res != 0 {
80119
return Err(new_error!(
81120
"LoadWasmModule Failed with error code {:?}",
82121
res
83122
));
84123
}
85124
Ok(())
86-
});
125+
}
126+
}
87127

88-
let transition_func = MultiUseContextCallback::from(func);
128+
/// Load a Wasm module from a buffer of bytes into the sandbox and return a `LoadedWasmSandbox`
129+
/// able to execute code in the loaded Wasm Module.
130+
///
131+
/// Before you can call guest functions in the sandbox, you must call
132+
/// this function and use the returned value to call guest functions.
133+
pub fn load_module_from_buffer(self, buffer: &[u8]) -> Result<LoadedWasmSandbox> {
134+
// TODO: get rid of this clone
135+
let func = Self::load_module_from_buffer_transition_func(buffer.to_vec());
89136

137+
self.load_module_inner(func)
138+
}
139+
140+
fn load_module_inner<F: FnOnce(&mut MultiUseGuestCallContext) -> Result<()>>(
141+
mut self,
142+
func: F,
143+
) -> Result<LoadedWasmSandbox> {
144+
let transition_func = MultiUseContextCallback::from(func);
90145
match self.inner.take() {
91146
Some(sbox) => {
92147
let new_sbox: MultiUseSandbox = sbox.evolve(transition_func)?;

src/wasm_runtime/src/component.rs

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ fn init_wasm_runtime(_function_call: &FunctionCall) -> Result<Vec<u8>> {
4747
Ok(get_flatbuffer_result::<i32>(0))
4848
}
4949

50+
fn load_component_common(engine: &Engine, component: Component) -> Result<()> {
51+
let mut store = Store::new(engine, ());
52+
let instance = (*CUR_LINKER.lock())
53+
.as_ref()
54+
.unwrap()
55+
.instantiate(&mut store, &component)?;
56+
*CUR_STORE.lock() = Some(store);
57+
*CUR_INSTANCE.lock() = Some(instance);
58+
Ok(())
59+
}
60+
5061
fn load_wasm_module(function_call: &FunctionCall) -> Result<Vec<u8>> {
5162
if let (
5263
ParameterValue::VecBytes(ref wasm_bytes),
@@ -58,13 +69,7 @@ fn load_wasm_module(function_call: &FunctionCall) -> Result<Vec<u8>> {
5869
&*CUR_ENGINE.lock(),
5970
) {
6071
let component = unsafe { Component::deserialize(engine, wasm_bytes)? };
61-
let mut store = Store::new(engine, ());
62-
let instance = (*CUR_LINKER.lock())
63-
.as_ref()
64-
.unwrap()
65-
.instantiate(&mut store, &component)?;
66-
*CUR_STORE.lock() = Some(store);
67-
*CUR_INSTANCE.lock() = Some(instance);
72+
load_component_common(engine, component)?;
6873
Ok(get_flatbuffer_result::<i32>(0))
6974
} else {
7075
Err(HyperlightGuestError::new(
@@ -74,6 +79,24 @@ fn load_wasm_module(function_call: &FunctionCall) -> Result<Vec<u8>> {
7479
}
7580
}
7681

82+
fn load_wasm_module_phys(function_call: &FunctionCall) -> Result<Vec<u8>> {
83+
if let (ParameterValue::ULong(ref phys), ParameterValue::ULong(ref len), Some(ref engine)) = (
84+
&function_call.parameters.as_ref().unwrap()[0],
85+
&function_call.parameters.as_ref().unwrap()[1],
86+
&*CUR_ENGINE.lock(),
87+
) {
88+
let component =
89+
unsafe { Component::deserialize_raw(engine, platform::map_buffer(*phys, *len))? };
90+
load_component_common(engine, component)?;
91+
Ok(get_flatbuffer_result::<()>(()))
92+
} else {
93+
Err(HyperlightGuestError::new(
94+
ErrorCode::GuestFunctionParameterTypeMismatch,
95+
"Invalid parameters passed to LoadWasmModulePhys".to_string(),
96+
))
97+
}
98+
}
99+
77100
#[no_mangle]
78101
pub extern "C" fn hyperlight_main() {
79102
platform::register_page_fault_handler();
@@ -83,6 +106,7 @@ pub extern "C" fn hyperlight_main() {
83106
config.memory_guard_size(0);
84107
config.memory_reservation_for_growth(0);
85108
config.guard_before_linear_memory(false);
109+
config.with_custom_code_memory(Some(alloc::sync::Arc::new(platform::WasmtimeCodeMemory {})));
86110
let engine = Engine::new(&config).unwrap();
87111
let linker = Linker::new(&engine);
88112
*CUR_ENGINE.lock() = Some(engine);
@@ -102,6 +126,12 @@ pub extern "C" fn hyperlight_main() {
102126
ReturnType::Int,
103127
load_wasm_module as usize,
104128
));
129+
register_function(GuestFunctionDefinition::new(
130+
"LoadWasmModulePhys".to_string(),
131+
vec![ParameterType::ULong, ParameterType::ULong],
132+
ReturnType::Void,
133+
load_wasm_module_phys as usize,
134+
));
105135
}
106136

107137
#[no_mangle]

src/wasm_runtime/src/module.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ fn init_wasm_runtime() -> Result<Vec<u8>> {
9393
config.memory_guard_size(0);
9494
config.memory_reservation_for_growth(0);
9595
config.guard_before_linear_memory(false);
96+
config.with_custom_code_memory(Some(alloc::sync::Arc::new(platform::WasmtimeCodeMemory {})));
9697
let engine = Engine::new(&config)?;
9798
let mut linker = Linker::new(&engine);
9899
wasip1::register_handlers(&mut linker)?;
@@ -138,6 +139,23 @@ fn load_wasm_module(function_call: &FunctionCall) -> Result<Vec<u8>> {
138139
}
139140
}
140141

142+
fn load_wasm_module_phys(function_call: &FunctionCall) -> Result<Vec<u8>> {
143+
if let (ParameterValue::ULong(ref phys), ParameterValue::ULong(ref len), Some(ref engine)) = (
144+
&function_call.parameters.as_ref().unwrap()[0],
145+
&function_call.parameters.as_ref().unwrap()[1],
146+
&*CUR_ENGINE.lock(),
147+
) {
148+
let module = unsafe { Module::deserialize_raw(engine, platform::map_buffer(*phys, *len))? };
149+
*CUR_MODULE.lock() = Some(module);
150+
Ok(get_flatbuffer_result::<()>(()))
151+
} else {
152+
Err(HyperlightGuestError::new(
153+
ErrorCode::GuestFunctionParameterTypeMismatch,
154+
"Invalid parameters passed to LoadWasmModulePhys".to_string(),
155+
))
156+
}
157+
}
158+
141159
#[no_mangle]
142160
#[allow(clippy::fn_to_numeric_cast)] // GuestFunctionDefinition expects a function pointer as i64
143161
pub extern "C" fn hyperlight_main() {
@@ -163,4 +181,10 @@ pub extern "C" fn hyperlight_main() {
163181
ReturnType::Int,
164182
load_wasm_module as usize,
165183
));
184+
register_function(GuestFunctionDefinition::new(
185+
"LoadWasmModulePhys".to_string(),
186+
vec![ParameterType::ULong, ParameterType::ULong],
187+
ReturnType::Void,
188+
load_wasm_module_phys as usize,
189+
));
166190
}

0 commit comments

Comments
 (0)