Security Implications of Wasm Shared-Everything Threads
Problem
Wasm’s existing threads proposal (Phase 4, standardised) allows multiple threads to share a single SharedArrayBuffer-backed linear memory. This is a limited form of shared memory: threads can read and write to the same linear memory region, but they cannot share GC-managed objects (WasmGC reference types), function references, or externrefs. Each thread has its own value stack and local variables, and the only shared state is the linear memory array.
The shared-everything threads proposal (advancing through the standards process in 2025–2026) removes this restriction. It allows Wasm threads to share GC-managed heap objects across thread boundaries — passing references to GC objects between threads, accessing the same object from multiple threads simultaneously, and creating shared mutable data structures on the GC heap.
This is a significant capability addition for performance-sensitive workloads: shared data structures eliminate expensive serialisation when passing data between threads, and shared GC objects enable patterns like shared immutable configuration, work-stealing queues, and publisher-subscriber systems within a Wasm module.
The security implications are significant and underappreciated in current discussions:
Existing Wasm isolation assumptions break. The security model for Wasm sandboxing — particularly for multi-tenant environments and plugin systems — has relied on the fact that GC heap objects are per-instance and non-shareable. A Wasm instance cannot access another instance’s GC objects. Shared-everything threads potentially change this: if two threads from different “tenants” share a module instance, they share the GC heap. The question of whether separate instances can share objects is still being resolved in the proposal, but deployment configurations that put multiple workloads into a single Wasm module are affected.
Data races are now possible on GC objects. Wasm’s existing shared linear memory is used with atomics, which provide ordering guarantees. GC objects under shared-everything threads can potentially be accessed from multiple threads without synchronisation, creating data races on GC object fields. A data race on a reference field — where one thread reads a reference while another is updating it — can produce a reference to an unexpected object, potentially an attacker-controlled one if the race is deterministically exploitable.
Reference confusion attacks. A data race on a GC object’s type discriminant or reference field could allow a thread to read a reference with an incorrect type — effectively bypassing Wasm’s type-checked memory model. This class of vulnerability is similar to type confusion in C++, enabled by unsynchronised concurrent access to type-discriminant fields.
Lock complexity expands attack surface. Shared-everything threads require applications to implement their own locking for shared GC objects. Lock implementation errors — deadlocks, lock ordering violations, double-free equivalents — are notoriously difficult to audit and test. In a Wasm plugin environment where the plugin author is untrusted, a plugin that deliberately creates lock contention can affect other plugins sharing the runtime.
Host function call semantics change. With shared-everything threads, a Wasm module can call host functions from multiple threads simultaneously. Host functions that were written assuming single-threaded calling semantics become unsafe. State maintained in a host function’s “current execution context” can be corrupted by concurrent calls.
The shared-everything threads proposal is still advancing and not yet broadly implemented in production runtimes. The security implications need to be understood before deployment to ensure that sandboxing, multi-tenancy, and plugin isolation assumptions are re-evaluated.
Target systems: Wasm runtimes enabling shared-everything threads (Wasmtime, V8 when the proposal is standardised); multi-tenant Wasm deployments; plugin systems where untrusted Wasm modules share runtime instances.
Threat Model
Adversary 1 — Data race on GC object type field. Access level: code running in a Wasm thread with shared GC heap access. Objective: race the type discriminant field of a shared GC object to read it with the wrong type, causing a type confusion that allows access to memory that should not be reachable from the attacker’s context.
Adversary 2 — Plugin-induced lock contention DoS. Access level: an untrusted plugin running in a shared Wasm module instance with other plugins. Objective: acquire a shared lock that other plugins need, causing them to block indefinitely — a denial of service that affects all plugins sharing the runtime.
Adversary 3 — Host function concurrency bug exploitation. Access level: a Wasm module that calls a host function from multiple threads. Objective: exploit a host function that was not written to handle concurrent calls — corrupting shared host state, triggering a use-after-free in host code, or causing the host function to operate on data intended for a different module.
Adversary 4 — Shared reference escape. Access level: two threads sharing a GC object that contains a capability reference (e.g., a file handle, a network socket wrapped in a GC object). Objective: one thread passes the capability to an untrusted second thread that was not supposed to have access to it, by racing the reference field update.
Configuration / Implementation
Step 1 — Audit whether you are affected by the proposal
# Check if your Wasmtime version has shared-everything threads enabled
wasmtime --version
# Check if the proposal is in the enabled feature set
wasmtime explore --help 2>&1 | grep -i "shared"
# If "shared-everything-threads" appears, the feature exists
# Check your Wasm modules for thread use
wasm-objdump -h module.wasm | grep -i "thread"
wasm-objdump -d module.wasm | grep -i "atomic\|shared"
Step 2 — Disable shared-everything threads unless explicitly needed
Until you have audited the security implications for your deployment, disable the feature:
use wasmtime::Config;
fn secure_engine_config() -> Config {
let mut config = Config::new();
// Enable standard threads if needed
config.wasm_threads(true);
// Explicitly disable shared-everything threads until security implications
// are understood for your deployment
// (Feature name may vary as the proposal matures)
// config.wasm_shared_everything_threads(false); // When API is available
// Key principle: if you don't need shared-everything threads, don't enable them
config
}
For Wasmtime CLI:
# Run without enabling the shared-everything threads proposal
wasmtime run \
--wasm-features=-shared-everything-threads \
module.wasm
Step 3 — Audit host functions for thread safety
All host functions exposed to Wasm modules with shared-everything threads must be thread-safe:
// host_functions.rs — thread-safe host function implementation
use std::sync::{Arc, Mutex};
use wasmtime::{Caller, Linker};
// UNSAFE for shared-everything threads: mutable state in host function
struct UnsafeFileManager {
current_file: Option<std::fs::File>, // Not thread-safe
}
impl UnsafeFileManager {
fn read_current(&mut self, buf: &mut [u8]) -> usize {
// If called from multiple Wasm threads simultaneously, data race
if let Some(ref mut file) = self.current_file {
file.read(buf).unwrap_or(0)
} else { 0 }
}
}
// SAFE for shared-everything threads: proper synchronisation
struct SafeFileManager {
files: Arc<Mutex<std::collections::HashMap<u32, std::fs::File>>>,
}
impl SafeFileManager {
fn read_file(&self, file_id: u32, buf: &mut [u8]) -> usize {
let mut files = self.files.lock().unwrap();
if let Some(file) = files.get_mut(&file_id) {
file.read(buf).unwrap_or(0)
} else { 0 }
}
}
// When registering host functions, verify thread safety:
fn register_thread_safe_functions<T: Send + Sync + 'static>(
linker: &mut Linker<T>,
) -> anyhow::Result<()> {
linker.func_wrap("env", "read_file",
|mut caller: Caller<'_, T>, file_id: u32, buf_ptr: u32, len: u32| -> u32 {
// Implementation must be safe for concurrent calls
// from multiple Wasm threads
0u32
}
)?;
Ok(())
}
Step 4 — Enforce synchronisation discipline in shared Wasm modules
For Wasm modules that use shared-everything threads, audit all accesses to shared GC objects for proper synchronisation:
;; shared-objects.wat — demonstrating safe vs. unsafe patterns
(module
;; Shared GC object type
(type $SharedCounter (struct (field $value (mut i32))))
;; UNSAFE: Unprotected concurrent increment
;; Data race: two threads may read the same value and both write + 1
(func $unsafe_increment (param $counter (ref $SharedCounter))
(struct.set $SharedCounter $value
(local.get $counter)
(i32.add
(struct.get $SharedCounter $value (local.get $counter))
(i32.const 1)
)
)
;; Thread 1 and Thread 2 both read 5, both write 6 = lost update
)
;; SAFE: Use atomic operations for shared mutable integer state
;; Note: WasmGC struct fields are not directly atomically accessible yet
;; Safe pattern: store mutable state in shared linear memory, not GC objects
(memory $shared_mem (shared 1))
(func $safe_increment (param $offset i32)
(drop
(i32.atomic.rmw.add ;; Atomic add — no data race
(local.get $offset)
(i32.const 1)
)
)
)
)
Design principle for shared-everything threads: place mutable state that requires concurrent access in shared linear memory (where atomics work), not in GC objects (where atomics are not yet available for struct fields). Use GC objects for immutable shared data only.
Step 5 — Isolation boundary re-evaluation for multi-tenant deployments
For deployments where Wasm instances are used as isolation boundaries:
// isolation_audit.rs
// Audit whether your multi-tenant setup is affected by shared-everything threads
struct TenantWasmRuntime {
// CURRENT: each tenant has its own Engine and Store
// This is still isolated even with shared-everything threads enabled
// because separate Stores cannot share GC objects between them
store: wasmtime::Store<TenantContext>,
instance: wasmtime::Instance,
}
// KEY QUESTION: Are multiple tenants ever instantiated in the SAME module?
// If so, they may share GC heap under shared-everything threads.
// SAFE isolation pattern: separate Engine per tenant
fn create_isolated_tenant(engine: &wasmtime::Engine) -> TenantWasmRuntime {
// Each tenant gets its own Store — GC heaps are never shared between Stores
let store = wasmtime::Store::new(engine, TenantContext::new());
// ... instantiate module
todo!()
}
// POTENTIALLY UNSAFE: if multiple tenants share a Store
// This should never be done regardless of shared-everything threads
Step 6 — Monitor for suspicious concurrent access patterns
# Falco rule — alert on Wasm threads spawning unexpected concurrent host calls
# (Applicable when your runtime emits relevant events)
- rule: Wasm Concurrent Host Function Anomaly
desc: Wasm module making unusual number of concurrent host function calls
condition: >
wasm.host_calls_concurrent > 50 and
not wasm.module_name in (known-threaded-modules)
output: >
Wasm module making high-concurrency host calls
(module=%wasm.module_name concurrent_calls=%wasm.host_calls_concurrent)
priority: WARNING
Expected Behaviour
| Scenario | Without controls | With controls |
|---|---|---|
| Shared-everything threads in production Wasm | Enabled by default when available | Explicitly disabled unless audited and approved |
| Host function called from multiple Wasm threads | Data race in host function state | Host functions audited for thread safety; mutexes on shared state |
| Multi-tenant Wasm shares a module instance | Potential GC object sharing | Separate Stores per tenant; GC heaps never shared |
| Mutable state in GC objects | Potential data race | Mutable state in linear memory using atomics; GC objects immutable |
| New Wasm runtime version enables shared-everything | Behavior change not detected | Runtime configuration pinning; feature flag audit on upgrade |
Trade-offs
| Aspect | Benefit | Cost | Mitigation |
|---|---|---|---|
| Disabling shared-everything threads | Prevents data race vulnerabilities | Cannot use the performance features of the proposal | Re-enable selectively for trusted, audited modules after thread safety review |
| Separate Store per tenant | Maintains GC heap isolation | Higher memory overhead per tenant | Accept the overhead; GC heap sharing between tenants is not an acceptable risk |
| Mutable state in linear memory | Atomics available; no GC data races | Less ergonomic than GC objects for shared state | Establish a coding pattern; write a wrapper library |
| Host function Mutex locking | Thread safety | Lock contention; potential deadlock | Use lock-free data structures where possible; timeouts on all locks |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Runtime upgrade silently enables shared-everything threads | Existing modules that worked correctly now have potential data races | Pin runtime version; test after upgrade; review changelog | Disable the feature flag; audit code for thread safety before re-enabling |
| Host function deadlock under concurrent Wasm threads | Wasm module hangs; host thread stuck | Timeout monitoring on host function calls; deadlock detector in lock manager | Implement timeouts on all Mutex locks; restart the affected Wasm instance |
| Mutable GC object accessed from multiple threads | Incorrect results; potential security issue | Stress testing with concurrent threads; race condition detectors (ThreadSanitizer in host code) | Refactor mutable state into shared linear memory with atomics |
| Isolation audit missed a shared module instance | Two tenants share GC heap | Security audit of instantiation code | Add an invariant check: each tenant instantiation creates a new Store |
Related Articles
- Wasm Threads and Shared Memory — the existing linear memory threads proposal that shared-everything extends
- WasmGC Security Implications — security implications of the GC types that shared-everything threads can now share across threads
- Wasm Multi-Tenancy — isolation boundaries for multi-tenant Wasm deployments that shared-everything threads may affect
- Wasmtime Epoch Interruption Security — terminating runaway Wasm threads that may be involved in concurrency-based attacks
- Wasm Multi-Memory Isolation — using separate memories for isolation within a module, complementary to per-thread GC isolation