Skip to content

Commit 0e1cfc1

Browse files
committed
[guest] improve WASM runtime memory management
- Reuse wasmtime Store and Instance across guest function calls instead of creating new one per call. - Establish memory contract between host and guest. - Guest functions takes ownership of input parameters - Guest transfer ownership of return values - Host functions parameters are borrowed from guest - Host function return values are owned by guest and guest must free them. - Component: Add post_return calls for proper WASM function cleanup - Fix ABI mismatch in parameter of guest_dispatch_function Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent 66b47e7 commit 0e1cfc1

File tree

5 files changed

+160
-37
lines changed

5 files changed

+160
-37
lines changed

src/hyperlight_wasm_macro/src/wasmguest.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,15 +212,17 @@ fn emit_wasm_function_call(
212212
let rwt = match result {
213213
None => {
214214
quote! {
215-
instance.get_typed_func::<(#(#pwts,)*), ()>(&mut *store, func_idx)?
216-
.call(&mut *store, (#(#pus,)*))?;
215+
let func = instance.get_typed_func::<(#(#pwts,)*), ()>(&mut *store, func_idx)?;
216+
func.call(&mut *store, (#(#pus,)*))?;
217+
func.post_return(&mut *store)?;
217218
}
218219
}
219220
_ => {
220221
let r = rtypes::emit_func_result(s, result);
221222
quote! {
222-
let #ret = instance.get_typed_func::<(#(#pwts,)*), ((#r,))>(&mut *store, func_idx)?
223-
.call(&mut *store, (#(#pus,)*))?.0;
223+
let func = instance.get_typed_func::<(#(#pwts,)*), ((#r,))>(&mut *store, func_idx)?;
224+
let #ret = func.call(&mut *store, (#(#pus,)*))?.0;
225+
func.post_return(&mut *store)?;
224226
}
225227
}
226228
};

src/wasm_runtime/src/hostfuncs.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,9 @@ pub(crate) fn hostfunc_type(d: &HostFunctionDefinition, e: &Engine) -> Result<Fu
7979
Ok(FuncType::new(e, params, results))
8080
}
8181

82-
pub(crate) fn call(
82+
pub(crate) fn call<T>(
8383
d: &HostFunctionDefinition,
84-
mut c: Caller<'_, ()>,
84+
mut c: Caller<'_, T>,
8585
ps: &[Val],
8686
rs: &mut [Val],
8787
) -> Result<()> {

src/wasm_runtime/src/marshal.rs

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,34 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17+
//! Parameter and return value marshalling for WASM guest function calls.
18+
//!
19+
//! # Memory Management Contract
20+
//!
21+
//! This module implements a clear memory ownership model for both guest function calls and host function calls:
22+
//!
23+
//! ## Guest Function Parameters (Host → Guest)
24+
//! - When calling guest functions with String or VecBytes parameters, the host allocates memory
25+
//! in the guest's memory space and passes pointers to the guest.
26+
//! - **The guest owns these allocations and must free them** when no longer needed using the
27+
//! `free` function exported from the guest module.
28+
//!
29+
//! ## Guest Function Return Values (Guest → Host)
30+
//! - When guest functions return String or VecBytes values, the guest allocates memory in its
31+
//! own memory space and returns pointers to the host.
32+
//! - **The host takes ownership of these allocations and will free them** on the next VM entry
33+
//! to prevent memory leaks.
34+
//!
35+
//! ## Host Function Parameters (Guest → Host)
36+
//! - When guest code calls host functions with String or VecBytes parameters, the guest passes
37+
//! pointers to data in its own memory space.
38+
//! - **The guest retains ownership** of these allocations and remains responsible for freeing them.
39+
//!
40+
//! ## Host Function Return Values (Host → Guest)
41+
//! - When host functions return String or VecBytes values to the guest, the host allocates memory
42+
//! in the guest's memory space and returns pointers.
43+
//! - **The guest owns these allocations and must free them** when no longer needed.
44+
1745
extern crate alloc;
1846

1947
use alloc::ffi::CString;
@@ -29,6 +57,28 @@ use hyperlight_common::flatbuffer_wrappers::util::get_flatbuffer_result;
2957
use hyperlight_guest::error::{HyperlightGuestError, Result};
3058
use wasmtime::{AsContextMut, Extern, Val};
3159

60+
use spin::Mutex;
61+
62+
// Global tracking for return value allocations that need to be freed on next VM entry
63+
static RETURN_VALUE_ALLOCATIONS: Mutex<Vec<i32>> = Mutex::new(Vec::new());
64+
65+
/// Track a return value allocation that should be freed on the next VM entry
66+
fn track_return_value_allocation(addr: i32) {
67+
RETURN_VALUE_ALLOCATIONS.lock().push(addr);
68+
}
69+
70+
/// Free all tracked return value allocations from previous VM calls
71+
pub fn free_return_value_allocations<C: AsContextMut>(
72+
ctx: &mut C,
73+
get_export: &impl Fn(&mut C, &str) -> Option<Extern>,
74+
) -> Result<()> {
75+
let mut allocations = RETURN_VALUE_ALLOCATIONS.lock();
76+
for addr in allocations.drain(..) {
77+
free(ctx, get_export, addr)?;
78+
}
79+
Ok(())
80+
}
81+
3282
fn malloc<C: AsContextMut>(
3383
ctx: &mut C,
3484
get_export: &impl Fn(&mut C, &str) -> Option<Extern>,
@@ -46,6 +96,21 @@ fn malloc<C: AsContextMut>(
4696
Ok(addr)
4797
}
4898

99+
fn free<C: AsContextMut>(
100+
ctx: &mut C,
101+
get_export: &impl Fn(&mut C, &str) -> Option<Extern>,
102+
addr: i32,
103+
) -> Result<()> {
104+
let free = get_export(&mut *ctx, "free")
105+
.and_then(Extern::into_func)
106+
.ok_or(HyperlightGuestError::new(
107+
ErrorCode::GuestError,
108+
"free function not exported".to_string(),
109+
))?;
110+
free.typed::<i32, ()>(&mut *ctx)?.call(&mut *ctx, addr)?;
111+
Ok(())
112+
}
113+
49114
fn write<C: AsContextMut>(
50115
ctx: &mut C,
51116
get_export: &impl Fn(&mut C, &str) -> Option<Extern>,
@@ -126,6 +191,11 @@ fn read_cstr<C: AsContextMut>(
126191
})
127192
}
128193

194+
/// Convert a hyperlight parameter value to a wasmtime value.
195+
///
196+
/// For String and VecBytes parameter types, this allocates memory in the guest's memory space
197+
/// and returns a pointer. The guest function is responsible for freeing this memory when it is no
198+
/// longer needed using the `free` function exported from the guest module.
129199
pub fn hl_param_to_val<C: AsContextMut>(
130200
mut ctx: C,
131201
get_export: impl Fn(&mut C, &str) -> Option<Extern>,
@@ -155,6 +225,11 @@ pub fn hl_param_to_val<C: AsContextMut>(
155225
}
156226
}
157227

228+
/// Convert guest function return values to hyperlight return value.
229+
///
230+
/// For String and VecBytes return types, the guest has allocated memory in its own memory space
231+
/// and returned pointers. The host takes ownership of these allocations and tracks them for
232+
/// automatic cleanup on the next VM entry to prevent memory leaks.
158233
pub fn val_to_hl_result<C: AsContextMut>(
159234
mut ctx: C,
160235
get_export: impl Fn(&mut C, &str) -> Option<Extern>,
@@ -172,15 +247,21 @@ pub fn val_to_hl_result<C: AsContextMut>(
172247
/* todo: get_flatbuffer_result_from_bool is missing */
173248
(ReturnType::Float, Val::F32(f)) => Ok(get_flatbuffer_result::<f32>(f32::from_bits(f))),
174249
(ReturnType::Double, Val::F64(f)) => Ok(get_flatbuffer_result::<f64>(f64::from_bits(f))),
175-
(ReturnType::String, Val::I32(p)) => Ok(get_flatbuffer_result::<&str>(
176-
read_cstr(&mut ctx, &get_export, p)?.to_str().map_err(|e| {
177-
HyperlightGuestError::new(
178-
ErrorCode::GuestError,
179-
format!("non-UTF-8 c string in guest function return: {}", e),
180-
)
181-
})?,
182-
)),
250+
(ReturnType::String, Val::I32(p)) => {
251+
// Track this allocation so it can be freed on next VM entry
252+
track_return_value_allocation(p);
253+
Ok(get_flatbuffer_result::<&str>(
254+
read_cstr(&mut ctx, &get_export, p)?.to_str().map_err(|e| {
255+
HyperlightGuestError::new(
256+
ErrorCode::GuestError,
257+
format!("non-UTF-8 c string in guest function return: {}", e),
258+
)
259+
})?,
260+
))
261+
}
183262
(ReturnType::VecBytes, Val::I32(ret)) => {
263+
// Track this allocation so it can be freed on next VM entry
264+
track_return_value_allocation(ret);
184265
let mut size_bytes = [0; 4];
185266
read(&mut ctx, &get_export, ret, &mut size_bytes)?;
186267
let size = i32::from_le_bytes(size_bytes);
@@ -198,6 +279,11 @@ pub fn val_to_hl_result<C: AsContextMut>(
198279
}
199280
}
200281

282+
/// Convert guest-provided WASM values to hyperlight parameters for host function calls.
283+
///
284+
/// For String and VecBytes parameter types, the guest passes pointers to data in its own
285+
/// memory space. The guest retains ownership of these allocations and remains responsible
286+
/// for freeing them. This function only reads the data without taking ownership.
201287
pub fn val_to_hl_param<'a, C: AsContextMut>(
202288
ctx: &mut C,
203289
get_export: impl Fn(&mut C, &str) -> Option<Extern>,
@@ -248,6 +334,11 @@ pub fn val_to_hl_param<'a, C: AsContextMut>(
248334
}
249335
}
250336

337+
/// Convert a hyperlight return value to a wasmtime value for host function returns.
338+
///
339+
/// For String and VecBytes return types, this allocates memory in the guest's memory space
340+
/// and returns a pointer. The guest owns these allocations and must free them when no longer needed
341+
/// using the `free` function exported from the guest module.
251342
pub fn hl_return_to_val<C: AsContextMut>(
252343
ctx: &mut C,
253344
get_export: impl Fn(&mut C, &str) -> Option<Extern>,

src/wasm_runtime/src/module.rs

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ limitations under the License.
1717
use alloc::string::ToString;
1818
use alloc::vec::Vec;
1919
use alloc::{format, vec};
20-
use core::ops::Deref;
20+
use core::ops::{Deref, DerefMut};
2121

2222
use hyperlight_common::flatbuffer_wrappers::function_call::FunctionCall;
2323
use hyperlight_common::flatbuffer_wrappers::function_types::{
@@ -34,57 +34,65 @@ use wasmtime::{Config, Engine, Linker, Module, Store, Val};
3434

3535
use crate::{hostfuncs, marshal, platform, wasip1};
3636

37+
// Set by transition to WasmSandbox (by init_wasm_runtime)
3738
static CUR_ENGINE: Mutex<Option<Engine>> = Mutex::new(None);
3839
static CUR_LINKER: Mutex<Option<Linker<()>>> = Mutex::new(None);
40+
// Set by transition to LoadedWasmSandbox (by load_wasm_module/load_wasm_module_phys)
3941
static CUR_MODULE: Mutex<Option<Module>> = Mutex::new(None);
42+
static CUR_STORE: Mutex<Option<Store<()>>> = Mutex::new(None);
43+
static CUR_INSTANCE: Mutex<Option<wasmtime::Instance>> = Mutex::new(None);
4044

4145
#[no_mangle]
42-
pub fn guest_dispatch_function(function_call: &FunctionCall) -> Result<Vec<u8>> {
43-
let engine = CUR_ENGINE.lock();
44-
let engine = engine.deref().as_ref().ok_or(HyperlightGuestError::new(
46+
pub fn guest_dispatch_function(function_call: FunctionCall) -> Result<Vec<u8>> {
47+
let mut store = CUR_STORE.lock();
48+
let store = store.deref_mut().as_mut().ok_or(HyperlightGuestError::new(
4549
ErrorCode::GuestError,
46-
"Wasm runtime is not initialized".to_string(),
50+
"No wasm store available".to_string(),
4751
))?;
48-
let linker = CUR_LINKER.lock();
49-
let linker = linker.deref().as_ref().ok_or(HyperlightGuestError::new(
52+
let instance = CUR_INSTANCE.lock();
53+
let instance = instance.deref().as_ref().ok_or(HyperlightGuestError::new(
5054
ErrorCode::GuestError,
51-
"impossible: wasm runtime has no valid linker".to_string(),
55+
"No wasm instance available".to_string(),
5256
))?;
53-
let module = CUR_MODULE.lock();
54-
let module = module.deref().as_ref().ok_or(HyperlightGuestError::new(
55-
ErrorCode::GuestError,
56-
"No wasm module loaded".to_string(),
57-
))?;
58-
let mut store = Store::new(engine, ());
59-
let instance = linker.instantiate(&mut store, module)?;
57+
58+
// Free any return value allocations from the previous VM call
59+
// This implements the memory model where hyperlight owns return values
60+
// and frees them on the next VM entry
61+
marshal::free_return_value_allocations(&mut *store, &|ctx, name| {
62+
instance.get_export(ctx, name)
63+
})?;
64+
6065
let func = instance
61-
.get_func(&mut store, &function_call.function_name)
66+
.get_func(&mut *store, &function_call.function_name)
6267
.ok_or(HyperlightGuestError::new(
6368
ErrorCode::GuestError,
6469
"Function not found".to_string(),
6570
))?;
71+
6672
let mut w_params = vec![];
6773
for f_param in (function_call.parameters)
6874
.as_ref()
6975
.unwrap_or(&vec![])
7076
.iter()
7177
{
7278
w_params.push(marshal::hl_param_to_val(
73-
&mut store,
79+
&mut *store,
7480
|ctx, name| instance.get_export(ctx, name),
7581
f_param,
7682
)?);
7783
}
7884
let is_void = ReturnType::Void == function_call.expected_return_type;
7985
let n_results = if is_void { 0 } else { 1 };
8086
let mut results = vec![Val::I32(0); n_results];
81-
func.call(&mut store, &w_params, &mut results)?;
82-
marshal::val_to_hl_result(
83-
&mut store,
87+
func.call(&mut *store, &w_params, &mut results)?;
88+
let result = marshal::val_to_hl_result(
89+
&mut *store,
8490
|ctx, name| instance.get_export(ctx, name),
8591
function_call.expected_return_type,
8692
&results,
87-
)
93+
);
94+
95+
result
8896
}
8997

9098
fn init_wasm_runtime() -> Result<Vec<u8>> {
@@ -124,8 +132,19 @@ fn load_wasm_module(function_call: &FunctionCall) -> Result<Vec<u8>> {
124132
&function_call.parameters.as_ref().unwrap()[1],
125133
&*CUR_ENGINE.lock(),
126134
) {
135+
let linker = CUR_LINKER.lock();
136+
let linker = linker.deref().as_ref().ok_or(HyperlightGuestError::new(
137+
ErrorCode::GuestError,
138+
"impossible: wasm runtime has no valid linker".to_string(),
139+
))?;
140+
127141
let module = unsafe { Module::deserialize(engine, wasm_bytes)? };
142+
let mut store = Store::new(engine, ());
143+
let instance = linker.instantiate(&mut store, &module)?;
144+
128145
*CUR_MODULE.lock() = Some(module);
146+
*CUR_STORE.lock() = Some(store);
147+
*CUR_INSTANCE.lock() = Some(instance);
129148
Ok(get_flatbuffer_result::<i32>(0))
130149
} else {
131150
Err(HyperlightGuestError::new(
@@ -141,8 +160,19 @@ fn load_wasm_module_phys(function_call: &FunctionCall) -> Result<Vec<u8>> {
141160
&function_call.parameters.as_ref().unwrap()[1],
142161
&*CUR_ENGINE.lock(),
143162
) {
163+
let linker = CUR_LINKER.lock();
164+
let linker = linker.deref().as_ref().ok_or(HyperlightGuestError::new(
165+
ErrorCode::GuestError,
166+
"impossible: wasm runtime has no valid linker".to_string(),
167+
))?;
168+
144169
let module = unsafe { Module::deserialize_raw(engine, platform::map_buffer(*phys, *len))? };
170+
let mut store = Store::new(engine, ());
171+
let instance = linker.instantiate(&mut store, &module)?;
172+
145173
*CUR_MODULE.lock() = Some(module);
174+
*CUR_STORE.lock() = Some(store);
175+
*CUR_INSTANCE.lock() = Some(instance);
146176
Ok(get_flatbuffer_result::<()>(()))
147177
} else {
148178
Err(HyperlightGuestError::new(

src/wasm_runtime/src/platform.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ pub(crate) fn register_page_fault_handler() {
5959
// See AMD64 Architecture Programmer's Manual, Volume 2
6060
// §8.2 Vectors, p. 245
6161
// Table 8-1: Interrupt Vector Source and Cause
62-
handler::handlers[14].store(page_fault_handler as usize as u64, Ordering::Release);
62+
handler::HANDLERS[14].store(page_fault_handler as usize as u64, Ordering::Release);
6363
}
6464

6565
// Wasmtime Embedding Interface
@@ -155,7 +155,7 @@ pub extern "C" fn wasmtime_init_traps(handler: wasmtime_trap_handler_t) -> i32 {
155155
// See AMD64 Architecture Programmer's Manual, Volume 2
156156
// §8.2 Vectors, p. 245
157157
// Table 8-1: Interrupt Vector Source and Cause
158-
handler::handlers[6].store(wasmtime_trap_handler as usize as u64, Ordering::Release);
158+
handler::HANDLERS[6].store(wasmtime_trap_handler as usize as u64, Ordering::Release);
159159
// TODO: Add handlers for any other traps that wasmtime needs,
160160
// probably including at least some floating-point
161161
// exceptions

0 commit comments

Comments
 (0)