sdjasj opened issue #13751:
Test Case
This is not triggered by a
.wasmfile; 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-treewasmtime-c-apiand callswasm_memorytype_limitsconcurrently from 32 threads on 10,000 sharedwasm_memorytype_tobjects. Copy the two files into a new directory inside the Wasmtime source tree (e.g.<wasmtime-repo>/repro/poc/), then replace theCRATES_CAPIplaceholder with the relative path back tocrates/c-api(one-liner included below), andcargo 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
- From the Wasmtime
mainbranch (repo HEAD08c456e887; the affected file was last touched by7948e0ff62, "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/).- Fix the dependency path, relative to that directory:
sed -i 's#CRATES_CAPI#../../crates/c-api#' Cargo.toml
- Build and run it (first run compiles the C API, ~a few minutes):
cargo run
- 32 threads concurrently make the first call to
wasm_memorytype_limitson each sharedwasm_memorytype_t.Expected Results
Per the C API's documented thread-safety contract, the program should run to completion. Every
wasm_memorytype_limitscall returns the cachedwasm_limits_t { min: 1, ... }, all assertions pass, and all threads join.Actual Results
The process aborts. A
std::cell::OnceCell::get_or_initreentrancy check trips during the concurrent first-initialization race, panicking withreentrant init; because the panic occurs inside anextern "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
mainHEAD08c456e8870b0b85566f9e9808b7a2f044eaa265(affected filecrates/c-api/src/types/memory.rs, last modified by commit7948e0ff62). PHPUnit/driver builds of the CLI reportWasmtime 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:17declareslimits_cache: std::cell::OnceCell<wasm_limits_t>andwasm_memorytype_limits(:60) initializes it throughmt.limits_cache.get_or_init(...)(:62).std::cell::OnceCellis a single-threaded type; this mutates shared state behind an immutable&wasm_memorytype_targument.- 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&Tin 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&Ttype-information query, so concurrent access is documented as safe; theOnceCellmakes it a data race that aborts.- Because the mutation is hidden behind
&T, the Rust type system's normal!Syncprotection is bypassed —wasm_memorytype_tadvertises 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::OnceCellbehind&T, e.g.wasm_functype_t(params_cache/returns_cache),wasm_importtype_t(module_cache/...), andwasm_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(oronce_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.
sdjasj added the bug label to Issue #13751.
alexcrichton closed issue #13751:
Test Case
This is not triggered by a
.wasmfile; 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-treewasmtime-c-apiand callswasm_memorytype_limitsconcurrently from 32 threads on 10,000 sharedwasm_memorytype_tobjects. Copy the two files into a new directory inside the Wasmtime source tree (e.g.<wasmtime-repo>/repro/poc/), then replace theCRATES_CAPIplaceholder with the relative path back tocrates/c-api(one-liner included below), andcargo 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
- From the Wasmtime
mainbranch (repo HEAD08c456e887; the affected file was last touched by7948e0ff62, "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/).- Fix the dependency path, relative to that directory:
sed -i 's#CRATES_CAPI#../../crates/c-api#' Cargo.toml
- Build and run it (first run compiles the C API, ~a few minutes):
cargo run
- 32 threads concurrently make the first call to
wasm_memorytype_limitson each sharedwasm_memorytype_t.Expected Results
Per the C API's documented thread-safety contract, the program should run to completion. Every
wasm_memorytype_limitscall returns the cachedwasm_limits_t { min: 1, ... }, all assertions pass, and all threads join.Actual Results
The process aborts. A
std::cell::OnceCell::get_or_initreentrancy check trips during the concurrent first-initialization race, panicking withreentrant init; because the panic occurs inside anextern "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
mainHEAD08c456e8870b0b85566f9e9808b7a2f044eaa265(affected filecrates/c-api/src/types/memory.rs, last modified by commit7948e0ff62). PHPUnit/driver builds of the CLI reportWasmtime 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:17declareslimits_cache: std::cell::OnceCell<wasm_limits_t>andwasm_memorytype_limits(:60) initializes it throughmt.limits_cache.get_or_init(...)(:62).std::cell::OnceCellis a single-threaded type; this mutates shared state behind an immutable&wasm_memorytype_targument.- 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&Tin 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&Ttype-information query, so concurrent access is documented as safe; theOnceCellmakes it a data race that aborts.- Because the mutation is hidden behind
&T, the Rust type system's normal!Syncprotection is bypassed —wasm_memorytype_tadvertises 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::OnceCellbehind&T, e.g.wasm_functype_t(params_cache/returns_cache),wasm_importtype_t(module_cache/...), andwasm_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(oronce_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.
Last updated: Jul 29 2026 at 05:03 UTC