Skip to content

Concurrent C-API type queries abort via unsynchronized OnceCell in &T-style read-only functions (e.g. wasm_memorytype_limits) #13751

Description

@sdjasj

Test Case

This is not triggered by a .wasm file; it is a host-embedding issue in the C API, so no Wasm module is needed. Below is a self-contained two-file Cargo project that links against the in-tree wasmtime-c-api and calls wasm_memorytype_limits concurrently from 32 threads on 10,000 shared wasm_memorytype_t objects. Copy the two files into a new directory inside the Wasmtime source tree (e.g. <wasmtime-repo>/repro/poc/), then replace the CRATES_CAPI placeholder with the relative path back to crates/c-api (one-liner included below), and cargo run.

Cargo.toml:

[package]
name = "oncecell-race-poc"
version = "0.1.0"
edition = "2021"

[workspace]

[dependencies]
wasmtime-c-api-impl = { path = "CRATES_CAPI" }

src/main.rs:

use std::sync::{Arc, Barrier};
use std::thread;
use wasmtime_c_api::{wasm_limits_t, wasm_memorytype_limits, wasm_memorytype_new};

#[derive(Copy, Clone)]
struct SharedPtr(usize);
unsafe impl Send for SharedPtr {}
unsafe impl Sync for SharedPtr {}

fn main() {
    let limits = wasm_limits_t { min: 1, max: u32::MAX };
    // 10_000 distinct wasm_memorytype_t objects, all shared across 32 threads.
    let ptrs: Vec<_> = (0..10_000)
        .map(|_| SharedPtr(Box::into_raw(wasm_memorytype_new(&limits)) as usize))
        .collect();
    let ptrs = Arc::new(ptrs);
    let start = Arc::new(Barrier::new(32));

    let mut threads = Vec::new();
    for _ in 0..32 {
        let start = start.clone();
        let ptrs = ptrs.clone();
        threads.push(thread::spawn(move || {
            for ptr in ptrs.iter().copied() {
                // First call on each object races across all 32 threads at once.
                start.wait();
                let mt = unsafe { &*(ptr.0 as *const _) };
                let limits = wasm_memorytype_limits(mt);
                assert_eq!(limits.min, 1);
                start.wait();
            }
        }));
    }
    for t in threads { t.join().unwrap(); }
}

Steps to Reproduce

  1. From the Wasmtime main branch (repo HEAD 08c456e887; the affected file was last touched by 7948e0ff62, "Expose custom page sizes in the C and C++ APIs (Expose custom page sizes in the C and C++ APIs #11890)"), create the two files above inside the repo (e.g. at <repo>/repro/poc/).
  2. Fix the dependency path, relative to that directory:
sed -i 's#CRATES_CAPI#../../crates/c-api#' Cargo.toml
  1. Build and run it (first run compiles the C API, ~a few minutes):
cargo run
  1. 32 threads concurrently make the first call to wasm_memorytype_limits on each shared wasm_memorytype_t.

Expected Results

Per the C API's documented thread-safety contract, the program should run to completion. Every wasm_memorytype_limits call returns the cached wasm_limits_t { min: 1, ... }, all assertions pass, and all threads join.

Actual Results

The process aborts. A std::cell::OnceCell::get_or_init reentrancy check trips during the concurrent first-initialization race, panicking with reentrant init; because the panic occurs inside an extern "C" function it cannot unwind, so the process aborts (exit status 141 / SIGABRT). Aborted output:

thread '<unnamed>' (707599) panicked at /rustc/.../library/core/src/cell/once.rs:300:66:
reentrant init
...
thread '<unnamed>' (707599) panicked at /rustc/.../library/core/src/panicking.rs:225:5:
panic in a function that cannot unwind
stack backtrace:
   ...
  19:     wasm_memorytype_limits
  20:     capi_oncecell_race_poc::main::{{closure}}
thread caused non-unwinding panic. aborting.

Versions and Environment

Wasmtime version or commit: Repo main HEAD 08c456e8870b0b85566f9e9808b7a2f044eaa265 (affected file crates/c-api/src/types/memory.rs, last modified by commit 7948e0ff62). PHPUnit/driver builds of the CLI report Wasmtime 47.0.0.

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/types/memory.rs:17 declares limits_cache: std::cell::OnceCell<wasm_limits_t> and wasm_memorytype_limits (:60) initializes it through mt.limits_cache.get_or_init(...) (:62). std::cell::OnceCell is a single-threaded type; this mutates shared state behind an immutable &wasm_memorytype_t argument.
  • This violates the documented C-API thread-safety contract. crates/c-api/include/wasmtime.h (Thread Safety section, ~lines 129-149) states: "functions which correspond to &T in Rust can be called concurrently with any other methods that take &T" and that "Functions which don't mutate anything, such as learning type information ... can be called concurrently." wasm_memorytype_limits(mt: &wasm_memorytype_t) is precisely such a read-only &T type-information query, so concurrent access is documented as safe; the OnceCell makes it a data race that aborts.
  • Because the mutation is hidden behind &T, the Rust type system's normal !Sync protection is bypassed — wasm_memorytype_t advertises shared-reference (concurrent) access while internally mutating a single-threaded cell.
  • Same class of bug exists in other C-API type-query caches that use std::cell::OnceCell behind &T, e.g. wasm_functype_t (params_cache/returns_cache), wasm_importtype_t (module_cache/...), and wasm_frame_t (func_name/module_name). An audit/reuse of the same fix is warranted across those paths.
  • Impact is denial of service: a multi-threaded host that concurrently queries C-API type metadata on a shared, attacker-influenceable type object can be made to abort. Default single-threaded usage is unaffected.
  • Suggested fixes: replace these caches with a thread-safe primitive such as std::sync::OnceLock (or once_cell::sync::OnceCell); or remove the small type-query caches and compute the value directly on each call; and apply the same change to the sibling caches noted above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIncorrect behavior in the current implementation that needs fixing

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions