How to Fix: Calling wasi:cli/run#run function twice in the same component instance returns “`unreachable` instruction executed” error
Why calling wasi:cli/run#run twice on the same component instance crashes with "unreachable instruction executed"
This failure is not a random Wasm trap. It happens because a component implementing WASI CLI is typically designed as a single-shot command invocation. After the first call to wasi:cli/run#run, the guest program may consume process-like state, trigger cleanup paths, or reach an internal terminal state that is not valid for re-entry. Calling it again on the same component instance reuses state that was never meant to be reused, and the runtime surfaces that invalid re-entry as an unreachable trap.
What Is Happening
In the reproduction repo linked in the issue, the component builds successfully, the first invocation of wasi:cli/run#run works, and the second invocation on that same instantiated component fails with "unreachable instruction executed". That behavior strongly indicates a lifecycle mismatch between the host and the guest: the host treats run() as reusable, while the guest treats it as a one-time program entrypoint.
Conceptually, wasi:cli/run models the execution of a command-line program. A CLI program is usually instantiated, run once, and then discarded. Re-entering that same command instance is different from calling a normal pure function twice.
Understanding the Root Cause
The core issue is that WASI CLI components are process-like, not service-like. The run export represents execution of the command entrypoint, not an idempotent API method. After the first call, several things may have happened inside the component:
- Global or mutable state may have been consumed or finalized.
- Constructors/destructors generated by the toolchain may assume a single execution lifecycle.
- WASI resources such as stdio handles, environment-backed state, or runtime bookkeeping may no longer be in a valid state for another invocation.
- The guest code may explicitly or implicitly reach a path that should never be executed twice, resulting in a WebAssembly unreachable trap.
This is especially important with components compiled from Rust using cargo component. The resulting component often behaves like a command binary wrapped in the component model. That means the correct host pattern is:
- Create a new Store and host state as needed.
- Instantiate the component.
- Call wasi:cli/run#run once.
- Drop that instance.
- Instantiate a fresh instance for the next run.
If you need repeated calls against the same instance, the exported interface should be designed as a custom world or service API, not as wasi:cli/run.
Step-by-Step Solution
The fix is to instantiate a fresh component instance for every call to run(). Do not cache and reuse the same instantiated CLI component.
1. Keep the compiled component, but do not reuse the instance
You can reuse the compiled Component object for efficiency, but each execution should create a new Store and a new instance.
2. Build the guest component
cd guest
cargo component build --release
3. Host pattern that causes the bug
This is the problematic shape: instantiate once, call run() twice.
let component = Component::from_file(&engine, "guest/target/wasm32-wasip1/release/guest.wasm")?;
let mut store = Store::new(&engine, host_state);
let linker = build_linker(&engine)?;
let instance = linker.instantiate(&mut store, &component)?;
let run = instance.get_typed_func::<(), ()>(&mut store, "wasi:cli/run#run")?;
run.call(&mut store, ())?;
run.call(&mut store, ())?; // traps: unreachable instruction executed
4. Correct host pattern: instantiate per invocation
let component = Component::from_file(&engine, "guest/target/wasm32-wasip1/release/guest.wasm")?;
let linker = build_linker(&engine)?;
for _ in 0..2 {
let mut store = Store::new(&engine, host_state_for_run());
let instance = linker.instantiate(&mut store, &component)?;
let run = instance.get_typed_func::<(), ()>(&mut store, "wasi:cli/run#run")?;
run.call(&mut store, ())?;
}
5. If you are using generated bindings, recreate bindings per run
for _ in 0..2 {
let mut store = Store::new(&engine, host_state_for_run());
let bindings = Command::instantiate(&mut store, &component, &linker)?;
bindings.wasi_cli_run().call_run(&mut store)?;
}
6. If you actually need multiple calls, redesign the component API
If your goal is not "run a CLI twice" but "call guest logic multiple times," expose your own world with methods such as init, process, and shutdown. That gives you a reusable service-style interface instead of a single-use command entrypoint.
package example:runner;
world reusable-runner {
export init: func();
export process: func(input: string) -> string;
export shutdown: func();
}
That model is the better fit for long-lived instances.
7. Verify the fix
After updating the host to instantiate per call, run the same workflow twice. You should see both invocations complete without the unreachable trap.
Common Edge Cases
- Reusing the same Store: Even if you instantiate a new component, reusing host state carelessly can preserve invalid state between runs. Fresh per-run state is safer for CLI semantics.
- Toolchain path confusion: Depending on your build target and setup, the output artifact path may differ. Confirm the component file you load is the one produced by cargo component build –release.
- Assuming all exports are reentrant: A normal exported function in a custom component may be safely callable multiple times, but wasi:cli/run#run should not be treated that way.
- Captured resources: If your host injects files, streams, or custom resources into the component, make sure they are recreated or rewound for each execution.
- Hidden guest-side globals: Rust static state, once-cell patterns, and lazy initialization can make second-run behavior even more fragile when the instance is reused improperly.
- Misreading the trap: The unreachable message is a low-level symptom. The real bug is usually lifecycle misuse, not a random branch failure in your application code.
FAQ
Can I ever call a component export twice on the same instance?
Yes, but it depends on the interface contract. A custom exported function may be reusable. wasi:cli/run#run, however, represents a CLI execution entrypoint and should generally be treated as single-use per instance.
Why does the runtime show unreachable instruction executed instead of a clearer error?
Because the invalid second invocation often collapses into a WebAssembly trap generated by guest code or compiler-inserted runtime paths. The runtime reports the low-level trap it sees, not necessarily the higher-level lifecycle mistake.
What is the best architecture if I need repeatable guest calls?
Use a service-oriented component interface instead of wasi:cli/run. Define your own world with reusable exported functions and keep run() only for true command-style execution.
For the original repro, the practical fix is simple: do not call wasi:cli/run#run twice on the same component instance. Create a fresh instance for each run, or redesign the component as a reusable service if repeated calls are required.