Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Prerelease] - Unreleased

### Changed
- **BREAKING CHANGE:** Removed `SandboxBuilder::with_function_definition_size`. Host function definitions are now pushed to the guest at runtime load time instead of using a separate memory region. (#388)

## [v0.12.0] - 2025-12

### Added
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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ readme = "README.md"

[workspace.dependencies]
hyperlight-host = { version = "0.12.0", default-features = false, features = ["executable_heap", "init-paging"] }
hyperlight-common = { version = "0.12.0", default-features = false }
1 change: 1 addition & 0 deletions src/component_sample/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ world = "example"

[package.metadata.component.target.dependencies]

[workspace] # indicate that this crate is not part of any workspace
1 change: 1 addition & 0 deletions src/hyperlight_wasm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ test = true

[dependencies]
hyperlight-host = { workspace = true }
hyperlight-common = { workspace = true }
libc = { version = "0.2.182" }
once_cell = "1.21.3"
tracing = "0.1.44"
Expand Down
23 changes: 23 additions & 0 deletions src/hyperlight_wasm/src/sandbox/loaded_wasm_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,29 @@ mod tests {
assert_eq!(r, 0);
}

#[test]
fn test_load_module_fails_with_missing_host_function() {
// HostFunction.aot imports "HostFuncWithBufferAndLength" from "env".
// Loading it without registering that host function should fail
// at instantiation time (linker.instantiate) because the import
// cannot be satisfied.
let proto_wasm_sandbox = SandboxBuilder::new().build().unwrap();

let wasm_sandbox = proto_wasm_sandbox.load_runtime().unwrap();

let result: std::result::Result<LoadedWasmSandbox, _> = {
let mod_path = get_wasm_module_path("HostFunction.aot").unwrap();
wasm_sandbox.load_module(mod_path)
};

let err = result.unwrap_err();
let err_msg = format!("{:?}", err);
assert!(
err_msg.contains("HostFuncWithBufferAndLength"),
"Error should mention the missing host function, got: {err_msg}"
);
}

fn call_funcs(
mut loaded_wasm_sandbox: LoadedWasmSandbox,
iterations: i32,
Expand Down
47 changes: 45 additions & 2 deletions src/hyperlight_wasm/src/sandbox/proto_wasm_sandbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

use hyperlight_common::flatbuffer_wrappers::function_types::{ParameterType, ReturnType};
use hyperlight_common::flatbuffer_wrappers::host_function_definition::HostFunctionDefinition;
use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails;
use hyperlight_host::func::{HostFunction, ParameterTuple, Registerable, SupportedReturnType};
use hyperlight_host::sandbox::config::SandboxConfiguration;
use hyperlight_host::{GuestBinary, Result, UninitializedSandbox, new_error};
Expand All @@ -31,6 +34,8 @@ use crate::build_info::BuildInfo;
/// With that `WasmSandbox` you can load a Wasm module through the `load_module` method and get a `LoadedWasmSandbox` which can then execute functions defined in the Wasm module.
pub struct ProtoWasmSandbox {
pub(super) inner: Option<UninitializedSandbox>,
/// Tracks registered host function definitions for pushing to the guest at load time
host_function_definitions: Vec<HostFunctionDefinition>,
}

impl Registerable for ProtoWasmSandbox {
Expand All @@ -39,6 +44,13 @@ impl Registerable for ProtoWasmSandbox {
name: &str,
hf: impl Into<HostFunction<Output, Args>>,
) -> Result<()> {
// Track the host function definition for pushing to guest at load time
self.host_function_definitions.push(HostFunctionDefinition {
function_name: name.to_string(),
parameter_types: Some(Args::TYPE.to_vec()),
return_type: Output::TYPE,
});
Comment on lines +47 to +52
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

host_function_definitions is built by blindly pushing entries, but you also pre-seed it with a HostPrint definition in new(). If a caller later registers a host function named HostPrint via register(...)/register_host_function(...), the list will contain duplicates and the guest init_wasm_runtime will attempt to linker.func_new the same name twice, which should error and prevent the runtime from loading. Consider enforcing uniqueness by name (e.g., replace existing definition instead of pushing, or special-case HostPrint).

Suggested change
// Track the host function definition for pushing to guest at load time
self.host_function_definitions.push(HostFunctionDefinition {
function_name: name.to_string(),
parameter_types: Some(Args::TYPE.to_vec()),
return_type: Output::TYPE,
});
// Track the host function definition for pushing to guest at load time,
// ensuring uniqueness by function name.
if let Some(existing) = self
.host_function_definitions
.iter_mut()
.find(|def| def.function_name == name)
{
existing.parameter_types = Some(Args::TYPE.to_vec());
existing.return_type = Output::TYPE;
} else {
self.host_function_definitions
.push(HostFunctionDefinition {
function_name: name.to_string(),
parameter_types: Some(Args::TYPE.to_vec()),
return_type: Output::TYPE,
});
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did think about this, but wasn't sure what the best thing to do was. Anyone have any thoughts? This feels like it could lead to some strange bugs as well


self.inner
.as_mut()
.ok_or(new_error!("inner sandbox was none"))
Expand All @@ -65,7 +77,18 @@ impl ProtoWasmSandbox {
let inner = UninitializedSandbox::new(guest_binary, cfg)?;
metrics::gauge!(METRIC_ACTIVE_PROTO_WASM_SANDBOXES).increment(1);
metrics::counter!(METRIC_TOTAL_PROTO_WASM_SANDBOXES).increment(1);
Ok(Self { inner: Some(inner) })

// HostPrint is always registered by UninitializedSandbox, so include it by default
let host_function_definitions = vec![HostFunctionDefinition {
function_name: "HostPrint".to_string(),
parameter_types: Some(vec![ParameterType::String]),
return_type: ReturnType::Int,
}];

Ok(Self {
inner: Some(inner),
host_function_definitions,
})
}

/// Load the Wasm runtime into the sandbox and return a `WasmSandbox`
Expand All @@ -75,12 +98,22 @@ impl ProtoWasmSandbox {
/// The returned `WasmSandbox` can be then be cached and used to load a different Wasm module.
///
pub fn load_runtime(mut self) -> Result<WasmSandbox> {
// Serialize host function definitions to push to the guest during InitWasmRuntime
let host_function_definitions = HostFunctionDetails {
host_functions: Some(std::mem::take(&mut self.host_function_definitions)),
};

let host_function_definitions_bytes: Vec<u8> = (&host_function_definitions)
.try_into()
.map_err(|e| new_error!("Failed to serialize host function details: {:?}", e))?;

let mut sandbox = match self.inner.take() {
Some(s) => s.evolve()?,
None => return Err(new_error!("No inner sandbox found.")),
};

let res: i32 = sandbox.call("InitWasmRuntime", ())?;
// Pass host function definitions to the guest as a parameter
let res: i32 = sandbox.call("InitWasmRuntime", (host_function_definitions_bytes,))?;
if res != 0 {
return Err(new_error!(
"InitWasmRuntime Failed with error code {:?}",
Expand All @@ -99,6 +132,13 @@ impl ProtoWasmSandbox {
name: impl AsRef<str>,
host_func: impl Into<HostFunction<Output, Args>>,
) -> Result<()> {
// Track the host function definition for pushing to guest at load time
self.host_function_definitions.push(HostFunctionDefinition {
function_name: name.as_ref().to_string(),
parameter_types: Some(Args::TYPE.to_vec()),
return_type: Output::TYPE,
});
Comment on lines +135 to +140
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The logic to append a HostFunctionDefinition is duplicated in both Registerable::register_host_function and the inherent ProtoWasmSandbox::register method. This duplication makes it easy for the two paths to drift (e.g., one adds de-dup or normalization and the other doesn’t). Consider extracting a small helper (or having one method delegate to the other) so the tracking behavior stays consistent.

Copilot uses AI. Check for mistakes.

self.inner
.as_mut()
.ok_or(new_error!("inner sandbox was none"))?
Expand All @@ -111,6 +151,9 @@ impl ProtoWasmSandbox {
&mut self,
print_func: impl Into<HostFunction<i32, (String,)>>,
) -> Result<()> {
// HostPrint definition is already tracked from new() since
// UninitializedSandbox always registers a default HostPrint.
// This method only replaces the implementation, not the definition.
self.inner
.as_mut()
.ok_or(new_error!("inner sandbox was none"))?
Expand Down
8 changes: 0 additions & 8 deletions src/hyperlight_wasm/src/sandbox/sandbox_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,6 @@ impl SandboxBuilder {
self
}

/// Set the size of the memory buffer that is made available
/// for serialising host function definitions the minimum value
/// is MIN_FUNCTION_DEFINITION_SIZE
pub fn with_function_definition_size(mut self, size: usize) -> Self {
self.config.set_host_function_definition_size(size);
self
}

/// Build the ProtoWasmSandbox
pub fn build(self) -> Result<ProtoWasmSandbox> {
if !is_hypervisor_present() {
Expand Down
2 changes: 2 additions & 0 deletions src/hyperlight_wasm_macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ syn = { version = "2.0.117" }
itertools = { version = "0.14.0" }
prettyplease = { version = "0.2.37" }
hyperlight-component-util = { version = "0.12.0" }

[workspace] # indicate that this crate is not part of any workspace
2 changes: 2 additions & 0 deletions src/rust_wasm_samples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ opt-level = 'z'
strip = true


[workspace] # indicate that this crate is not part of any workspace

[dependencies]

2 changes: 1 addition & 1 deletion src/wasm_runtime/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ pub extern "C" fn hyperlight_main() {

register_function(GuestFunctionDefinition::new(
"InitWasmRuntime".to_string(),
vec![],
vec![ParameterType::VecBytes],
ReturnType::Int,
init_wasm_runtime as usize,
));
Expand Down
4 changes: 0 additions & 4 deletions src/wasm_runtime/src/hostfuncs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ pub(crate) type HostFunctionDefinition =
pub(crate) type HostFunctionDetails =
hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails;

pub(crate) fn get_host_function_details() -> HostFunctionDetails {
hyperlight_guest_bin::host_comm::get_host_function_details()
}

pub(crate) fn hostfunc_type(d: &HostFunctionDefinition, e: &Engine) -> Result<FuncType> {
let mut params = Vec::new();
let mut last_was_vec = false;
Expand Down
33 changes: 28 additions & 5 deletions src/wasm_runtime/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ pub fn guest_dispatch_function(function_call: FunctionCall) -> Result<Vec<u8>> {
}

#[instrument(skip_all, level = "Info")]
fn init_wasm_runtime() -> Result<Vec<u8>> {
fn init_wasm_runtime(function_call: &FunctionCall) -> Result<Vec<u8>> {
let mut config = Config::new();
config.with_custom_code_memory(Some(alloc::sync::Arc::new(platform::WasmtimeCodeMemory {})));
#[cfg(gdb)]
Expand All @@ -112,9 +112,32 @@ fn init_wasm_runtime() -> Result<Vec<u8>> {
let mut linker = Linker::new(&engine);
wasip1::register_handlers(&mut linker)?;

let hostfuncs = hostfuncs::get_host_function_details()
.host_functions
.unwrap_or_default();
// Parse host function details pushed by the host as a parameter
let params = function_call.parameters.as_ref().ok_or_else(|| {
HyperlightGuestError::new(
ErrorCode::GuestFunctionParameterTypeMismatch,
"InitWasmRuntime: missing parameters".to_string(),
)
})?;

let bytes = match params.first() {
Some(ParameterValue::VecBytes(ref b)) => b,
_ => {
return Err(HyperlightGuestError::new(
ErrorCode::GuestFunctionParameterTypeMismatch,
"InitWasmRuntime: first parameter must be VecBytes".to_string(),
))
}
Comment on lines +125 to +130
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When parameters is present but empty, this returns "InitWasmRuntime: first parameter must be VecBytes", which is a bit misleading (the real issue is that no parameters were provided). Consider explicitly handling the empty-vec case and returning a clearer error (e.g., "expected 1 parameter") to make debugging host/guest ABI mismatches easier.

Suggested change
_ => {
return Err(HyperlightGuestError::new(
ErrorCode::GuestFunctionParameterTypeMismatch,
"InitWasmRuntime: first parameter must be VecBytes".to_string(),
))
}
Some(_) => {
return Err(HyperlightGuestError::new(
ErrorCode::GuestFunctionParameterTypeMismatch,
"InitWasmRuntime: first parameter must be VecBytes".to_string(),
))
}
None => {
return Err(HyperlightGuestError::new(
ErrorCode::GuestFunctionParameterTypeMismatch,
"InitWasmRuntime: expected 1 parameter".to_string(),
))
}

Copilot uses AI. Check for mistakes.
};

let hfd: hostfuncs::HostFunctionDetails = bytes.as_slice().try_into().map_err(|e| {
HyperlightGuestError::new(
ErrorCode::GuestError,
alloc::format!("Failed to parse host function details: {:?}", e),
)
})?;
let hostfuncs = hfd.host_functions.unwrap_or_default();

for hostfunc in hostfuncs.iter() {
let captured = hostfunc.clone();
linker.func_new(
Expand Down Expand Up @@ -210,7 +233,7 @@ pub extern "C" fn hyperlight_main() {

register_function(GuestFunctionDefinition::new(
"InitWasmRuntime".to_string(),
vec![],
vec![ParameterType::VecBytes],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not strictly related to your PR, but can we use the #[guest_func] macro on init_wasm_runtime instead of manually calling register_function?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe teh same applies to all usages of register_function

ReturnType::Int,
init_wasm_runtime as usize,
));
Expand Down
Loading