Security Implications of Wasm Shared-Everything Threads

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