How to Fix: handle_instantiate doesn’t set trap pointer on success
When wasmtime_instance_new succeeds but leaves trap untouched, your caller can read garbage and mis-handle success as failure.
This issue appears when native code expects out-parameters to be deterministic on every call path, but the success path in handle_instantiate does not explicitly clear the trap pointer. If the caller passes an uninitialized wasm_trap_t* trap; and instantiation succeeds, the function may return success while trap still contains whatever value happened to be in memory before the call.
Table of Contents
The Problem
Consider the reported usage:
wasm_trap_t* trap;
auto err = wasmtime_instance_new(context, module, &import, 1, &instance, &trap);
If wasmtime_instance_new succeeds, many C and C++ callers reasonably expect:
err == nullptrtrap == nullptrinstanceto be initialized
The bug is that the success path can leave trap unchanged unless a trap actually occurred. That creates an API contract problem: successful instantiation does not fully initialize all output parameters.
In practice, this can lead to:
- false-positive trap handling in caller code
- undefined behavior when stale pointers are dereferenced
- non-deterministic test failures depending on stack state
- hard-to-reproduce crashes in FFI consumers
Understanding the Root Cause
The root cause is simple but important: the trap out-parameter is conditionally written instead of unconditionally initialized.
At the API boundary, wasmtime_instance_new ultimately relies on an internal instantiation handler such as handle_instantiate. On the failure or trap path, code typically assigns a trap object into the caller-provided location. But on the success path, if no explicit assignment like *trap = nullptr; happens first, the caller receives an indeterminate value.
This is especially dangerous in C-style APIs because:
- callers often declare output pointers without zero-initializing them
- FFI consumers assume null means “no result” or “no error”
- success and error are frequently represented through multiple outputs simultaneously
Technically, this is not just a caller mistake. A robust native API should ensure every documented out-parameter is initialized on all code paths where the pointer argument itself is non-null.
The intended semantic model is usually:
err != nullptr: API-level error occurredtrap != nullptr: WebAssembly trap occurred during instantiation or start- both null: success
That model breaks if the implementation leaves trap uninitialized on success.
Step-by-Step Solution
The fix is to clear the trap output before any instantiation logic runs, or at minimum guarantee it is set to nullptr on every successful return path.
1. Initialize the trap out-parameter at function entry
If you maintain the runtime or are patching the affected code, update the implementation so the out-pointer is normalized immediately.
wasmtime_error_t* wasmtime_instance_new(..., wasm_trap_t** trap) {
if (trap != nullptr) {
*trap = nullptr;
}
// existing instantiation logic...
}
If the actual bug lives in handle_instantiate, the same principle applies there:
static wasmtime_error_t* handle_instantiate(..., wasm_trap_t** trap) {
if (trap != nullptr) {
*trap = nullptr;
}
// perform instantiation
// if a trap occurs:
// *trap = created_trap;
// return nullptr;
// if an error occurs:
// return error;
// on success:
// return nullptr;
}
This is the safest pattern because it guarantees a known state before branching.
2. Preserve the semantic distinction between error and trap
Do not collapse traps into generic errors unless the API contract explicitly requires that. A correct implementation should still distinguish these cases:
if (api_error) {
return error;
}
if (runtime_trap) {
if (trap != nullptr) {
*trap = runtime_trap;
}
return nullptr;
}
if (trap != nullptr) {
*trap = nullptr;
}
return nullptr;
Even if you initialize at the top, writing the success semantics clearly makes maintenance easier.
3. Add a regression test for the success path
The issue exists because trap handling was likely tested only when a trap occurred. Add a regression test that proves success explicitly nulls the out-pointer.
TEST(case_trap_pointer_is_cleared_on_success) {
wasm_trap_t* trap = reinterpret_cast<wasm_trap_t*>(0x1);
wasmtime_instance_t instance;
wasmtime_error_t* err = wasmtime_instance_new(
context,
module,
imports,
num_imports,
&instance,
&trap
);
ASSERT_EQ(err, nullptr);
ASSERT_EQ(trap, nullptr);
}
Using a non-null sentinel value is important. If the test initializes trap to nullptr, it may pass even when the implementation is still broken.
4. Harden caller code as a defensive workaround
Even after patching the library, callers should still initialize output pointers defensively:
wasm_trap_t* trap = nullptr;
wasmtime_instance_t instance;
wasmtime_error_t* err = wasmtime_instance_new(
context,
module,
&import,
1,
&instance,
&trap
);
if (err != nullptr) {
// handle API error
} else if (trap != nullptr) {
// handle wasm trap
} else {
// success
}
This does not replace the library fix, but it reduces exposure for downstream users.
5. Document the API contract explicitly
If you are contributing the fix upstream, update the API documentation or code comments to state that:
- if
trapis provided, it will be set tonullptron success - if a trap occurs, it will point to a valid
wasm_trap_t - callers must still release trap resources according to the runtime’s ownership rules
That documentation matters for C, C++, and language bindings built on top of the native API.
Example patch pattern
static wasmtime_error_t* handle_instantiate(
wasmtime_context_t* context,
wasmtime_module_t* module,
const wasmtime_extern_t* imports,
size_t nimports,
wasmtime_instance_t* instance,
wasm_trap_t** trap
) {
if (trap != nullptr) {
*trap = nullptr;
}
// existing validation and instantiation code
if (/* trapped during start or instantiation */) {
if (trap != nullptr) {
*trap = produced_trap;
}
return nullptr;
}
if (/* ordinary API error */) {
return produced_error;
}
*instance = produced_instance;
return nullptr;
}
The key idea is not the exact syntax. The key is enforcing deterministic output initialization.
Common Edge Cases
1. Caller passes an uninitialized local pointer
This is the exact reproducer. Without the fix, the stack value may look non-null and trigger bogus trap handling.
2. Caller passes nullptr for the trap out-parameter
The implementation must guard all assignments with if (trap != nullptr). Initializing the output is correct only when the output location exists.
3. Instantiation fails with an API error before trap-producing logic runs
Even in this case, it is best for trap to remain explicitly null if the caller supplied it. That avoids ambiguous mixed-state outputs.
4. Start function traps after instance creation begins
Some runtimes distinguish module instantiation from execution of the module’s start function. If the start function traps, the API should still reliably return the trap through trap and not through stale memory.
5. Bindings in Rust, Python, or other languages
FFI layers often assume native functions obey strict nullability rules. A non-deterministic trap out-parameter can break wrapper logic, produce incorrect exceptions, or leak resources due to confused ownership tracking.
6. Tests that only assert on err
A test suite may report success because err == nullptr while missing that trap is dirty. This bug survives when regression coverage checks only one output channel.
FAQ
Should callers still initialize trap to nullptr themselves?
Yes. Caller-side initialization is a good defensive habit, especially in C/C++. But the library should still set the trap out-parameter deterministically on success. Both sides can and should be safe.
Why return nullptr for both success and trap cases?
Because many Wasmtime-style APIs separate host/API errors from WebAssembly traps. An API error is returned as wasmtime_error_t*, while a trap is returned via wasm_trap_t**. That separation is useful, but only if both outputs are initialized consistently.
What is the minimal upstream fix for this issue?
The minimal safe fix is to add if (trap != nullptr) *trap = nullptr; at the beginning of the instantiation handler or wrapper function, before any early returns or trap-producing branches.
In short, the bug is not that trapping fails. The bug is that success does not clear the trap channel. Fix that by initializing the trap out-parameter on entry, keep error and trap semantics separate, and add a regression test using a non-null sentinel pointer so the issue never comes back.