Stream: git-wasmtime

Topic: wasmtime / issue #13785 C API reports success for a null ...


view this post on Zulip Wasmtime GitHub notifications bot (Jul 01 2026 at 05:49):

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 return NULL on 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 the exnref C 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

  1. From the Wasmtime source tree on main (verified at HEAD 2753ee7393), save the script above to a file (e.g. repro.sh) in the repo root and run bash repro.sh.
  2. It runs the embedded program twice: once plainly to observe the success-on-null behavior, and once with --abort to reuse the "successful" tag.
  3. Note that wasmtime_exnref_tag(null), wasmtime_exnref_field(null, 0), and wasmtime_context_set_exception(null) all return NULL.
  4. In the --abort run, the host then calls wasmtime_tag_type(ctx, &tag) with the tag that the successful wasmtime_exnref_tag left untouched.

Expected Results

Per their documentation, wasmtime_exnref_tag() / wasmtime_exnref_field() return NULL only on success and fill the output on success. A null exnref is a valid WebAssembly value (and constructable via the public wasmtime_exnref_set_null()), so the API contract implies these queries should either succeed with a meaningful output or return a wasmtime_error_t. Likewise wasmtime_context_set_exception() is documented to return the trap the host callback must return.

Expected: the helpers reject a null exnref (return a wasmtime_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 null exnref and leave the output pointer untouched (the poison pattern 0xa5 / 0xb6 is unchanged). wasmtime_context_set_exception(null) returns no trap and leaves no pending exception. Reusing the untouched tag with wasmtime_tag_type() panics in store-id validation (object used with the wrong store); because the panic occurs inside an extern "C" function it cannot unwind, so the host process aborts with SIGABRT (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: 134

Versions and Environment

Wasmtime version or commit: main HEAD 2753ee739335187dc4459b4dd4453c41fc5516ac (affected file crates/c-api/src/exnref.rs, last modified by commit 9c3a793afc, "Use MaybeUninit.write instead of initialize<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

view this post on Zulip Wasmtime GitHub notifications bot (Jul 01 2026 at 05:49):

sdjasj added the bug label to Issue #13785.

view this post on Zulip Wasmtime GitHub notifications bot (Jul 01 2026 at 18:22):

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 return NULL on 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 the exnref C 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

  1. From the Wasmtime source tree on main (verified at HEAD 2753ee7393), save the script above to a file (e.g. repro.sh) in the repo root and run bash repro.sh.
  2. It runs the embedded program twice: once plainly to observe the success-on-null behavior, and once with --abort to reuse the "successful" tag.
  3. Note that wasmtime_exnref_tag(null), wasmtime_exnref_field(null, 0), and wasmtime_context_set_exception(null) all return NULL.
  4. In the --abort run, the host then calls wasmtime_tag_type(ctx, &tag) with the tag that the successful wasmtime_exnref_tag left untouched.

Expected Results

Per their documentation, wasmtime_exnref_tag() / wasmtime_exnref_field() return NULL only on success and fill the output on success. A null exnref is a valid WebAssembly value (and constructable via the public wasmtime_exnref_set_null()), so the API contract implies these queries should either succeed with a meaningful output or return a wasmtime_error_t. Likewise wasmtime_context_set_exception() is documented to return the trap the host callback must return.

Expected: the helpers reject a null exnref (return a wasmtime_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 null exnref and leave the output pointer untouched (the poison pattern 0xa5 / 0xb6 is unchanged). wasmtime_context_set_exception(null) returns no trap and leaves no pending exception. Reusing the untouched tag with wasmtime_tag_type() panics in store-id validation (object used with the wrong store); because the panic occurs inside an extern "C" function it cannot unwind, so the host process aborts with SIGABRT (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: 134

Versions and Environment

Wasmtime version or commit: main HEAD 2753ee739335187dc4459b4dd4453c41fc5516ac (affected file crates/c-api/src/exnref.rs, last modified by commit 9c3a793afc, "Use MaybeUninit.write instead of initialize<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


Last updated: Jul 29 2026 at 05:03 UTC