sdjasj opened issue #13785:
Test Case
This is a host-embedding C API issue; no Wasm module is needed. The reproducer builds a null
exnref, calls read-only exception-helpers that are documented to fill an output on success and returnNULLon success, and observes that they return success (NULL) while leaving the output buffer untouched. Reusing the "successful" output with another public C API call then aborts the host.Copy the self-contained script below into a file and run it from the Wasmtime repository root. It builds the C API (default features include
gc, required for theexnrefC API surface), picks the GC-enabled generated header set, compiles an embedded C program, and runs it:#!/usr/bin/env bash set -euo pipefail ROOT="$(pwd)" # run from the wasmtime repo root WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT cargo build -p wasmtime-c-api # default features include `gc` # Select a GC-ENABLED generated header set (exnref.h decls are under #ifdef WASMTIME_FEATURE_GC). HEADER_DIR="" while IFS= read -r exn_h; do conf="$(dirname "$exn_h")/conf.h" if [ -f "$conf" ] && grep -Eq '^#define[[:space:]]+WASMTIME_FEATURE_GC([[:space:]]|$)' "$conf"; then HEADER_DIR="$(dirname "$(dirname "$exn_h")")"; break fi done < <(find "$ROOT/target/debug/build" -path '*/out/include/wasmtime/exnref.h' -print) [ -n "$HEADER_DIR" ] || { echo "no GC-enabled header set found under target/debug/build" >&2; exit 1; } cat >"$WORK/poc.c" <<'C_EOF' #include <stdbool.h> #include <stddef.h> #include <stdio.h> #include <string.h> #include <wasmtime.h> static bool bytes_are(const void *p, size_t n, unsigned char v) { for (size_t i = 0; i < n; i++) if (((const unsigned char *)p)[i] != v) return false; return true; } int main(int argc, char **argv) { bool abort_after = argc > 1 && !strcmp(argv[1], "--abort"); wasm_engine_t *E = wasm_engine_new(); wasmtime_store_t *S = wasmtime_store_new(E, NULL, NULL); wasmtime_context_t *ctx = wasmtime_store_context(S); wasmtime_exnref_t exn; wasmtime_exnref_set_null(&exn); // a valid null exnref wasmtime_tag_t tag; memset(&tag, 0xa5, sizeof(tag)); // poison the output wasmtime_error_t *err = wasmtime_exnref_tag(ctx, &exn, &tag); printf("wasmtime_exnref_tag(null) returned %s\n", err ? "an error" : "NULL/success"); printf("tag output unchanged after success: %s\n", bytes_are(&tag, sizeof(tag), 0xa5) ? "yes" : "no"); if (err) { wasmtime_error_delete(err); return 1; } wasmtime_val_t val; memset(&val, 0xb6, sizeof(val)); err = wasmtime_exnref_field(ctx, &exn, 0, &val); printf("wasmtime_exnref_field(null, 0) returned %s\n", err ? "an error" : "NULL/success"); printf("field output unchanged after success: %s\n", bytes_are(&val, sizeof(val), 0xb6) ? "yes" : "no"); if (err) { wasmtime_error_delete(err); return 1; } wasm_trap_t *trap = wasmtime_context_set_exception(ctx, &exn); printf("wasmtime_context_set_exception(null) returned %s\n", trap ? "a trap" : "NULL/no trap"); printf("store has pending exception: %s\n", wasmtime_context_has_exception(ctx) ? "yes" : "no"); if (trap) wasm_trap_delete(trap); if (abort_after) { puts("using the supposedly initialized tag after success..."); fflush(stdout); wasm_tagtype_t *ty = wasmtime_tag_type(ctx, &tag); // reuses the "successful" tag printf("unexpectedly obtained tag type: %p\n", (void *)ty); if (ty) wasm_tagtype_delete(ty); } wasmtime_store_delete(S); wasm_engine_delete(E); return 0; } C_EOF cc -O0 -g -I "$HEADER_DIR" "$WORK/poc.c" -L "$ROOT/target/debug" -lwasmtime \ -Wl,-rpath,"$ROOT/target/debug" -lpthread -ldl -lm -o "$WORK/poc" echo "[1/2] observing success-on-null"; "$WORK/poc" echo echo "[2/2] reusing the successful tag" set +e; "$WORK/poc" --abort >"$WORK/abort.out" 2>&1; status=$?; set -e cat "$WORK/abort.out"; echo "abort exit status: $status" [ "$status" -eq 134 ] && grep -qF "object used with the wrong store" "$WORK/abort.out" \ && echo "PoC verified: null exnref reported as success, then host aborted." || { echo "PoC not verified" >&2; exit 1; }Steps to Reproduce
- From the Wasmtime source tree on
main(verified at HEAD2753ee7393), save the script above to a file (e.g.repro.sh) in the repo root and runbash repro.sh.- It runs the embedded program twice: once plainly to observe the success-on-null behavior, and once with
--abortto reuse the "successful" tag.- Note that
wasmtime_exnref_tag(null),wasmtime_exnref_field(null, 0), andwasmtime_context_set_exception(null)all returnNULL.- In the
--abortrun, the host then callswasmtime_tag_type(ctx, &tag)with the tag that the successfulwasmtime_exnref_tagleft untouched.Expected Results
Per their documentation,
wasmtime_exnref_tag()/wasmtime_exnref_field()returnNULLonly on success and fill the output on success. A nullexnrefis a valid WebAssembly value (and constructable via the publicwasmtime_exnref_set_null()), so the API contract implies these queries should either succeed with a meaningful output or return awasmtime_error_t. Likewisewasmtime_context_set_exception()is documented to return the trap the host callback must return.Expected: the helpers reject a null
exnref(return awasmtime_error_t/ trap) instead of reporting success with an untouched output, so the host never reuses stale/uninitialized data and the process does not abort.Actual Results
All three helpers report success (
NULL) for a nullexnrefand leave the output pointer untouched (the poison pattern0xa5/0xb6is unchanged).wasmtime_context_set_exception(null)returns no trap and leaves no pending exception. Reusing the untouchedtagwithwasmtime_tag_type()panics in store-id validation (object used with the wrong store); because the panic occurs inside anextern "C"function it cannot unwind, so the host process aborts withSIGABRT(exit 134).Observed output:
wasmtime_exnref_tag(null) returned NULL/success tag output unchanged after success: yes wasmtime_exnref_field(null, 0) returned NULL/success field output unchanged after success: yes wasmtime_context_set_exception(null) returned NULL/no trap store has pending exception: no using the supposedly initialized tag after success... thread '<unnamed>' panicked at crates/wasmtime/src/runtime/store/data.rs:213:5: object used with the wrong store ... 19: wasmtime_tag_type at crates/c-api/src/tag.rs:22:1 20: main at .../poc.c:62:26 thread caused non-unwinding panic. aborting. abort exit status: 134Versions and Environment
Wasmtime version or commit:
mainHEAD2753ee739335187dc4459b4dd4453c41fc5516ac(affected filecrates/c-api/src/exnref.rs, last modified by commit9c3a793afc, "UseMaybeUninit.writeinstead ofinitialize<T>(#13638)").Operating system: Linux 5.15.0-139-generic (Ubuntu 20.04-based)
Architecture: x86_64
Rust: rustc 1.96.0 (ac68faa20 2026-05-25)
Extra Info
- Root cause:
crates/c-api/src/exnref.rsusesexn.as_wasmtime()?inwasmtime_exnref_tag(:50),wasmtime_exnref_field(:78), andwasmtime_context_set_exception(:90). For a nullexnref,as_wasmtime()returnsNone, the?propagatesNone, andNoneis the C-API success value forOption<Box<wasmtime_error_t>>/Option<Box<wasm_trap_t>>. So null maps to success while thetag_ret/val_retout-params are never written.wasmtime_exnref_field_count(:62-65) handles the sameNonecase explicitly by returning 0, showing that the author was aware null needs handling; the three query helpers above instead leak theNone-as-success semantics.- The abort sink is
crates/c-api/src/tag.rs:22(wasmtime_tag_type); the panic site iscrates/wasmtime/src/runtime/store/data.rs:213(object used with the wrong store), reached because the reusedtagwas never initialized to a valid store-owned tag.- Impact is a guest-triggerable host-process denial of service in embedders that query tag/fields of an
exnrefobtained from untrusted Wasm/plugin/script input (a nullexnrefis a valid value and constructable via the publicwasmtime_exnref_set_null). The bug bypasses normal error handling: the API reports success, so a well-written embedder has no error to check and reuses the stale output.- Note: the
exnrefC API is gated behindWASMTIME_FEATURE_GC(declared under#ifdef WASMTIME_FEATURE_GCin the generatedexnref.h), so this requires a GC-enabled Wasmtime C API build (the default features includegc).- Suggested fix: explicitly reject a null
exnrefinwasmtime_exnref_tag,wasmtime_exnref_field, andwasmtime_context_set_exception(return awasmtime_error_t/ trap) instead of mapping null to the C success value; do not leave the out-params unwritten on a reported-success path.wasmtime_exnref_type()is documented to returnNULLfor null and can keep that behavior.
sdjasj added the bug label to Issue #13785.
alexcrichton closed issue #13785:
Test Case
This is a host-embedding C API issue; no Wasm module is needed. The reproducer builds a null
exnref, calls read-only exception-helpers that are documented to fill an output on success and returnNULLon success, and observes that they return success (NULL) while leaving the output buffer untouched. Reusing the "successful" output with another public C API call then aborts the host.Copy the self-contained script below into a file and run it from the Wasmtime repository root. It builds the C API (default features include
gc, required for theexnrefC API surface), picks the GC-enabled generated header set, compiles an embedded C program, and runs it:#!/usr/bin/env bash set -euo pipefail ROOT="$(pwd)" # run from the wasmtime repo root WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT cargo build -p wasmtime-c-api # default features include `gc` # Select a GC-ENABLED generated header set (exnref.h decls are under #ifdef WASMTIME_FEATURE_GC). HEADER_DIR="" while IFS= read -r exn_h; do conf="$(dirname "$exn_h")/conf.h" if [ -f "$conf" ] && grep -Eq '^#define[[:space:]]+WASMTIME_FEATURE_GC([[:space:]]|$)' "$conf"; then HEADER_DIR="$(dirname "$(dirname "$exn_h")")"; break fi done < <(find "$ROOT/target/debug/build" -path '*/out/include/wasmtime/exnref.h' -print) [ -n "$HEADER_DIR" ] || { echo "no GC-enabled header set found under target/debug/build" >&2; exit 1; } cat >"$WORK/poc.c" <<'C_EOF' #include <stdbool.h> #include <stddef.h> #include <stdio.h> #include <string.h> #include <wasmtime.h> static bool bytes_are(const void *p, size_t n, unsigned char v) { for (size_t i = 0; i < n; i++) if (((const unsigned char *)p)[i] != v) return false; return true; } int main(int argc, char **argv) { bool abort_after = argc > 1 && !strcmp(argv[1], "--abort"); wasm_engine_t *E = wasm_engine_new(); wasmtime_store_t *S = wasmtime_store_new(E, NULL, NULL); wasmtime_context_t *ctx = wasmtime_store_context(S); wasmtime_exnref_t exn; wasmtime_exnref_set_null(&exn); // a valid null exnref wasmtime_tag_t tag; memset(&tag, 0xa5, sizeof(tag)); // poison the output wasmtime_error_t *err = wasmtime_exnref_tag(ctx, &exn, &tag); printf("wasmtime_exnref_tag(null) returned %s\n", err ? "an error" : "NULL/success"); printf("tag output unchanged after success: %s\n", bytes_are(&tag, sizeof(tag), 0xa5) ? "yes" : "no"); if (err) { wasmtime_error_delete(err); return 1; } wasmtime_val_t val; memset(&val, 0xb6, sizeof(val)); err = wasmtime_exnref_field(ctx, &exn, 0, &val); printf("wasmtime_exnref_field(null, 0) returned %s\n", err ? "an error" : "NULL/success"); printf("field output unchanged after success: %s\n", bytes_are(&val, sizeof(val), 0xb6) ? "yes" : "no"); if (err) { wasmtime_error_delete(err); return 1; } wasm_trap_t *trap = wasmtime_context_set_exception(ctx, &exn); printf("wasmtime_context_set_exception(null) returned %s\n", trap ? "a trap" : "NULL/no trap"); printf("store has pending exception: %s\n", wasmtime_context_has_exception(ctx) ? "yes" : "no"); if (trap) wasm_trap_delete(trap); if (abort_after) { puts("using the supposedly initialized tag after success..."); fflush(stdout); wasm_tagtype_t *ty = wasmtime_tag_type(ctx, &tag); // reuses the "successful" tag printf("unexpectedly obtained tag type: %p\n", (void *)ty); if (ty) wasm_tagtype_delete(ty); } wasmtime_store_delete(S); wasm_engine_delete(E); return 0; } C_EOF cc -O0 -g -I "$HEADER_DIR" "$WORK/poc.c" -L "$ROOT/target/debug" -lwasmtime \ -Wl,-rpath,"$ROOT/target/debug" -lpthread -ldl -lm -o "$WORK/poc" echo "[1/2] observing success-on-null"; "$WORK/poc" echo echo "[2/2] reusing the successful tag" set +e; "$WORK/poc" --abort >"$WORK/abort.out" 2>&1; status=$?; set -e cat "$WORK/abort.out"; echo "abort exit status: $status" [ "$status" -eq 134 ] && grep -qF "object used with the wrong store" "$WORK/abort.out" \ && echo "PoC verified: null exnref reported as success, then host aborted." || { echo "PoC not verified" >&2; exit 1; }Steps to Reproduce
- From the Wasmtime source tree on
main(verified at HEAD2753ee7393), save the script above to a file (e.g.repro.sh) in the repo root and runbash repro.sh.- It runs the embedded program twice: once plainly to observe the success-on-null behavior, and once with
--abortto reuse the "successful" tag.- Note that
wasmtime_exnref_tag(null),wasmtime_exnref_field(null, 0), andwasmtime_context_set_exception(null)all returnNULL.- In the
--abortrun, the host then callswasmtime_tag_type(ctx, &tag)with the tag that the successfulwasmtime_exnref_tagleft untouched.Expected Results
Per their documentation,
wasmtime_exnref_tag()/wasmtime_exnref_field()returnNULLonly on success and fill the output on success. A nullexnrefis a valid WebAssembly value (and constructable via the publicwasmtime_exnref_set_null()), so the API contract implies these queries should either succeed with a meaningful output or return awasmtime_error_t. Likewisewasmtime_context_set_exception()is documented to return the trap the host callback must return.Expected: the helpers reject a null
exnref(return awasmtime_error_t/ trap) instead of reporting success with an untouched output, so the host never reuses stale/uninitialized data and the process does not abort.Actual Results
All three helpers report success (
NULL) for a nullexnrefand leave the output pointer untouched (the poison pattern0xa5/0xb6is unchanged).wasmtime_context_set_exception(null)returns no trap and leaves no pending exception. Reusing the untouchedtagwithwasmtime_tag_type()panics in store-id validation (object used with the wrong store); because the panic occurs inside anextern "C"function it cannot unwind, so the host process aborts withSIGABRT(exit 134).Observed output:
wasmtime_exnref_tag(null) returned NULL/success tag output unchanged after success: yes wasmtime_exnref_field(null, 0) returned NULL/success field output unchanged after success: yes wasmtime_context_set_exception(null) returned NULL/no trap store has pending exception: no using the supposedly initialized tag after success... thread '<unnamed>' panicked at crates/wasmtime/src/runtime/store/data.rs:213:5: object used with the wrong store ... 19: wasmtime_tag_type at crates/c-api/src/tag.rs:22:1 20: main at .../poc.c:62:26 thread caused non-unwinding panic. aborting. abort exit status: 134Versions and Environment
Wasmtime version or commit:
mainHEAD2753ee739335187dc4459b4dd4453c41fc5516ac(affected filecrates/c-api/src/exnref.rs, last modified by commit9c3a793afc, "UseMaybeUninit.writeinstead ofinitialize<T>(#13638)").Operating system: Linux 5.15.0-139-generic (Ubuntu 20.04-based)
Architecture: x86_64
Rust: rustc 1.96.0 (ac68faa20 2026-05-25)
Extra Info
- Root cause:
crates/c-api/src/exnref.rsusesexn.as_wasmtime()?inwasmtime_exnref_tag(:50),wasmtime_exnref_field(:78), andwasmtime_context_set_exception(:90). For a nullexnref,as_wasmtime()returnsNone, the?propagatesNone, andNoneis the C-API success value forOption<Box<wasmtime_error_t>>/Option<Box<wasm_trap_t>>. So null maps to success while thetag_ret/val_retout-params are never written.wasmtime_exnref_field_count(:62-65) handles the sameNonecase explicitly by returning 0, showing that the author was aware null needs handling; the three query helpers above instead leak theNone-as-success semantics.- The abort sink is
crates/c-api/src/tag.rs:22(wasmtime_tag_type); the panic site iscrates/wasmtime/src/runtime/store/data.rs:213(object used with the wrong store), reached because the reusedtagwas never initialized to a valid store-owned tag.- Impact is a guest-triggerable host-process denial of service in embedders that query tag/fields of an
exnrefobtained from untrusted Wasm/plugin/script input (a nullexnrefis a valid value and constructable via the publicwasmtime_exnref_set_null). The bug bypasses normal error handling: the API reports success, so a well-written embedder has no error to check and reuses the stale output.- Note: the
exnrefC API is gated behindWASMTIME_FEATURE_GC(declared under#ifdef WASMTIME_FEATURE_GCin the generatedexnref.h), so this requires a GC-enabled Wasmtime C API build (the default features includegc).- Suggested fix: explicitly reject a null
exnrefinwasmtime_exnref_tag,wasmtime_exnref_field, andwasmtime_context_set_exception(return awasmtime_error_t/ trap) instead of mapping null to the C success value; do not leave the out-params unwritten on a reported-success path.wasmtime_exnref_type()is documented to returnNULLfor null and can keep that behavior.
Last updated: Jul 29 2026 at 05:03 UTC