kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 01dc12b56bc1ff4d3630b4330ebd89d87e81a2dd
parent 7edb4aefe48ae7c856034c528ec4ff1e0801520b
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Fri, 12 Jun 2026 12:17:39 -0700

fix(wasm): proc_exit unwinds the guest instead of trapping

WASI proc_exit is _Noreturn, so toolchains emit an 'unreachable' right
after the call. The host shim only recorded the exit code and returned
into the guest, which then hit that 'unreachable' and trapped the process
(SIGTRAP) with the exit code lost. The recorded-code pickup only worked
for hand-written modules that fall through after proc_exit, which is why
the existing test missed it; every real wasi-libc program (_start calls
__wasi_proc_exit(main(...)) then unreachable) trapped.

Add kit_wasm_instance_run_entry: a setjmp landing pad around init+entry
on the native JIT path, with proc_exit longjmping out when armed. The
interpreter path leaves the pad disarmed and recovers the exit code from
the resulting trap instead (longjmp cannot cross interpreter frames).

Regression: driver-wasm wasm-wasi-proc-exit-unreachable (proc_exit
followed by unreachable, modeling real toolchain output).

Diffstat:
Mdriver/lib/wasm_run.c | 11++++++++---
Minclude/kit/wasm.h | 16++++++++++++++++
Msrc/api/wasm_host.c | 39+++++++++++++++++++++++++++++++++++++++
Mtest/driver-wasm/run.sh | 22++++++++++++++++++++++
4 files changed, 85 insertions(+), 3 deletions(-)

diff --git a/driver/lib/wasm_run.c b/driver/lib/wasm_run.c @@ -952,9 +952,10 @@ int driver_wasm_run_call_entry(const DriverWasmRunOptions* opts, } init_u.p = init_sym; entry_u.p = entry; - init_u.fn(inst); - *rc_out = entry_u.fn(inst); - (void)kit_wasm_instance_exit_code(inst, rc_out); + /* Run init + entry under the proc_exit unwind guard so a WASI guest that + * calls proc_exit unwinds cleanly with its exit code instead of returning + * into the trailing `unreachable` and trapping the process. */ + (void)kit_wasm_instance_run_entry(inst, init_u.fn, entry_u.fn, rc_out); kit_wasm_instance_free(inst); wasm_build_release(&build); return 1; @@ -993,6 +994,10 @@ int driver_wasm_run_call_entry_interp(const DriverWasmRunOptions* opts, if (s == KIT_INTERP_DONE) { *rc_out = (int)ret; (void)kit_wasm_instance_exit_code(inst, rc_out); + } else if (kit_wasm_instance_exit_code(inst, rc_out)) { + /* proc_exit fired, then the trailing `unreachable` trapped the interpreter; + * the recorded exit code is the real result, not an execution failure. + * (The native JIT path unwinds via kit_wasm_instance_run_entry instead.) */ } else { driver_errf(tool, "interp: could not execute wasm entry %.*s", KIT_SLICE_ARG(kit_slice_cstr(entry_name))); diff --git a/include/kit/wasm.h b/include/kit/wasm.h @@ -277,6 +277,22 @@ KIT_API KitStatus kit_wasm_host_bind_imports(KitWasmHost*, KitCompiler*, /* Nonzero when a WASI proc_exit import was called. */ KIT_API int kit_wasm_instance_exit_code(KitWasmInstance*, int* code_out); +/* Run the JIT-lowered module's init + entry under a proc_exit unwind guard. + * + * A WASI guest exits by calling the `proc_exit` import, which is `_Noreturn`: + * toolchains emit an `unreachable` immediately after the call. Without a guard, + * the import returns into the guest and that trailing `unreachable` traps the + * process. This wrapper establishes a setjmp landing pad so proc_exit unwinds + * here instead, writing its code to *rc_out. On normal entry return, *rc_out is + * the entry's value, overridden by a recorded exit code if proc_exit was + * called. `init` may be NULL. Native (JIT) entry/init only; the interpreter + * path must not be run through here (longjmp cannot cross interpreter frames). + * Returns KIT_OK. */ +typedef void (*KitWasmInitFn)(KitWasmInstance*); +typedef int (*KitWasmEntryFn)(KitWasmInstance*); +KIT_API KitStatus kit_wasm_instance_run_entry(KitWasmInstance*, KitWasmInitFn, + KitWasmEntryFn, int* rc_out); + /* Resolve every declared import in the module loaded into `jit` against * the supplied static table and/or resolver, and write the resolved * function pointers into the per-import slots of `inst`. diff --git a/src/api/wasm_host.c b/src/api/wasm_host.c @@ -1,4 +1,5 @@ #include <kit/wasm.h> +#include <setjmp.h> #include <stddef.h> #include <stdint.h> #include <string.h> @@ -38,6 +39,11 @@ typedef struct KitWasmRuntimeInstance { uint32_t nmemories; int exit_called; int exit_code; + /* proc_exit unwind landing pad. Valid only while running under + * kit_wasm_instance_run_entry (the native JIT path); zero otherwise so the + * interpreter path falls through to its own trap-then-exit-code handling. */ + jmp_buf exit_jmp; + int exit_jmp_valid; KitWasmOpenFd fds[KIT_WASM_HOST_MAX_FDS]; uint8_t instance[]; } KitWasmRuntimeInstance; @@ -263,6 +269,15 @@ static void wasi_proc_exit(KitWasmInstance* inst, int32_t code) { KitWasmRuntimeInstance* w = wasm_wrap_from_instance(inst); w->exit_called = 1; w->exit_code = code; + /* proc_exit is _Noreturn: toolchains emit `unreachable` right after the call. + * Under the native JIT path a setjmp landing pad is armed, so unwind there + * instead of returning into the guest (which would hit that `unreachable` and + * trap). The interpreter path leaves exit_jmp_valid clear and recovers the + * exit code from the resulting trap instead. */ + if (w->exit_jmp_valid) { + w->exit_jmp_valid = 0; + longjmp(w->exit_jmp, 1); + } } static int32_t wasi_fd_write(KitWasmInstance* inst, int32_t fd_i, @@ -1075,3 +1090,27 @@ KIT_API int kit_wasm_instance_exit_code(KitWasmInstance* inst, int* code_out) { if (code_out) *code_out = w->exit_code; return 1; } + +KIT_API KitStatus kit_wasm_instance_run_entry(KitWasmInstance* inst, + KitWasmInitFn init, + KitWasmEntryFn entry, + int* rc_out) { + KitWasmRuntimeInstance* w; + int rc = 0; + if (!inst || !entry) return KIT_INVALID; + w = wasm_wrap_from_instance(inst); + if (setjmp(w->exit_jmp) == 0) { + /* First return from setjmp: arm the pad and run the guest. A proc_exit call + * longjmps back with a nonzero value, landing in the else branch. */ + w->exit_jmp_valid = 1; + if (init) init(inst); + rc = entry(inst); + w->exit_jmp_valid = 0; + if (w->exit_called) rc = w->exit_code; + } else { + /* Unwound out of the guest via proc_exit; exit_code is authoritative. */ + rc = w->exit_code; + } + if (rc_out) *rc_out = rc; + return KIT_OK; +} diff --git a/test/driver-wasm/run.sh b/test/driver-wasm/run.sh @@ -60,6 +60,18 @@ cat > "$work/wasi_exit.wat" <<'WAT' i32.const 0)) WAT +# proc_exit is _Noreturn: real toolchains (wasi-libc) emit `unreachable` right +# after the call. proc_exit must unwind out of the guest rather than return into +# that `unreachable` and trap the process. (Regression: previously SIGTRAPped.) +cat > "$work/wasi_exit_unreachable.wat" <<'WAT' +(module + (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (param i32))) + (func (export "test_main") (result i32) + i32.const 9 + call $proc_exit + unreachable)) +WAT + cat > "$work/wasi_stdout.wat" <<'WAT' (module (import "wasi_snapshot_preview1" "fd_write" @@ -401,6 +413,16 @@ else not_ok "wasm-wasi-proc-exit" "$work/wasm-wasi-proc-exit.err" fi +"$KIT" run --wasm-wasi -e test_main "$work/wasi_exit_unreachable.wat" \ + > "$work/wasm-wasi-proc-exit-unreachable.out" \ + 2> "$work/wasm-wasi-proc-exit-unreachable.err" +if [ "$?" -eq 9 ]; then + ok "wasm-wasi-proc-exit-unreachable" +else + not_ok "wasm-wasi-proc-exit-unreachable" \ + "$work/wasm-wasi-proc-exit-unreachable.err" +fi + "$KIT" run --wasm-wasi --wasm-stdio=inherit -e test_main \ "$work/wasi_stdout.wat" > "$work/wasm-wasi-stdout.out" \ 2> "$work/wasm-wasi-stdout.err"