Wasmer WebAssembly Runtime Security

Wasmer WebAssembly Runtime Security

Problem

Wasmer (wasmer.io, github.com/wasmerio/wasmer) is an open source WebAssembly runtime that exposes a universal API for embedding WASM execution inside Go, Python, Ruby, PHP, Java, and a growing list of other host languages. A Python service can load a Wasmer store, compile a WASM module, and call exports with the same semantics as a Rust embedder — the language SDK is a thin wrapper around the same core runtime. Wasmer also operates the WAPM package manager and Wasmer Edge, a WASM-based edge runtime. That breadth of reach means a runtime-level sandbox bug propagates across every language SDK simultaneously.

Unlike Wasmtime (maintained by the Bytecode Alliance) or WasmEdge, Wasmer ships three distinct compiler backends with meaningfully different security profiles. The Cranelift backend is shared with Wasmtime and benefits from the Bytecode Alliance’s security review process. The LLVM backend compiles WASM to native code via LLVM’s IR and optimisation passes, trading compilation speed for peak execution performance. The Singlepass backend generates machine code in a single forward pass — no optimisation, minimal IR, fast compilation — which makes it attractive for cold-start-sensitive workloads like serverless and plugin sandboxing. Operators who choose Wasmer often do so precisely because this backend flexibility exists. The security implications of that choice are rarely documented.

The central risk of running Wasmer in a security-sensitive context is not one that most operational runbooks cover: Wasmer has no public CVE disclosure process and no GHSA security advisories as of May 2026. There is no SECURITY.md file in the wasmerio/wasmer GitHub repository, no designated security contact email, and no published vulnerability database entries for the project. When a sandbox-relevant bug is found and fixed — a bounds-check regression in a compiler backend, a memory access miscalculation in the VM layer, a WASI privilege escalation — the fix lands as a regular commit with a message like fix: Singlepass codegen for memory access with large offsets or fix: bounds check regression in Cranelift backend. The fix is real; the notification to operators is absent.

Compare this to Wasmtime, which published 12 security advisories in April 2026 alone, each with a CVE identifier, a severity rating, affected version ranges, and recommended mitigations. Operators running Wasmtime-based infrastructure receive GitHub Security Advisory notifications through standard dependency scanning tooling. Operators running Wasmer receive nothing. If a cargo audit or pip-audit run is the primary signal for “does my runtime have a known sandbox bug”, Wasmer will remain silent even when a security-relevant commit has landed upstream. This is not a theoretical gap: the Wasmer commit log shows multiple fixes to lib/compiler-singlepass/src/codegen_x86_64.rs, lib/vm/src/trap/, and lib/wasi/src/ that touch memory bounds enforcement and code generation correctness, and none of them carry a CVE or advisory.

The Singlepass backend’s correctness risk deserves specific attention. Singlepass is designed for contexts where compilation speed matters more than execution speed: serverless cold start, plugin sandboxing, interactive REPL environments. Those are also exactly the contexts where untrusted WASM input is most likely. The simplified single-pass code generation approach reduces compiler complexity, but it also reduces the code paths that receive deep security review. Specific WASM instruction patterns — i64.load with large constant offsets combined with certain address arithmetic sequences, or specific patterns of memory.grow interleaved with bounds-sensitive loads — have produced incorrect bounds-checking code in Wasmer point releases. These were corrected in subsequent patch releases with no advisory, no CVE, and no communication beyond the commit message and changelog entry for operators paying close enough attention.

The LLVM backend carries a different risk profile. LLVM’s optimisation passes are extraordinarily complex and are not written with WASM sandbox preservation as a first-order goal. An optimisation pass that performs value range analysis and eliminates a branch it concludes is unreachable could in principle eliminate a WASM bounds check that the guest memory model requires. This class of bug — where a correct optimisation from the perspective of a native program is incorrect from the perspective of a sandboxed guest — has appeared in other JIT compilers applied to sandboxed execution contexts. LLVM’s attack surface for this class of miscompilation is orders of magnitude larger than Cranelift’s, and LLVM’s maintainers are not focused on WASM sandbox correctness as a security property.

Target systems: Wasmer 4.x (all compiler backends — Cranelift, LLVM, Singlepass), applications embedding the wasmer crate or Wasmer language SDKs (Python wasmer, Ruby wasmer, Go wasmer-go, PHP wasmer-php, Java wasmer-java), Wasmer Edge deployments, and any platform using wasmer run to execute WAPM packages from the wasmer.io package registry.

Threat Model

  1. Malicious WASM module exploiting a Singlepass codegen bug. An attacker submits a crafted WASM module to a Wasmer-based plugin sandbox that uses the Singlepass backend — a typical configuration for a SaaS product offering customer-authored plugins where compilation latency is a product constraint. The module contains a specific i64.load instruction with a large constant offset combined with address arithmetic that triggers a bounds-checking regression present in the installed Wasmer version. The Singlepass backend generates machine code that omits the required bounds check for that specific pattern. The guest module reads from host process memory beyond its linear memory region, potentially reaching in-process secrets: API keys, session tokens, or other tenants’ data.

  2. LLVM backend miscompilation leading to OOB access. The LLVM backend is selected for a compute-intensive WASM workload where execution throughput matters. An LLVM optimisation pass performs value range analysis on the WASM module’s memory access pattern and determines — incorrectly, based on a flaw in the pass’s handling of WASM’s unsigned 32-bit address space — that a bounds check is redundant and eliminates it. A malicious WASM module crafted to hit the affected code path accesses host memory beyond its sandbox. The bug is present in an LLVM version that Wasmer links against; there is no advisory and no CVE to alert the operator.

  3. No-CVE-process attacker: patch diffing for exploit development. An attacker monitors github.com/wasmerio/wasmer/commits/main and watches for commits that touch lib/compiler-singlepass/src/, lib/vm/src/, and lib/wasi/src/. When a commit with a message like fix: bounds check in codegen_x86_64 appears, the attacker diffs the change, identifies the specific instruction pattern that was previously mis-handled, and crafts a WASM module that triggers the pre-fix code path. The attacker then targets production deployments of Wasmer-based SaaS products — plugin execution platforms, edge function hosts, WASM sandbox services — that are running the unfixed version. Because there is no CVE and no advisory, those deployments have no mechanism to know they are vulnerable other than having independently tracked the same commit.

  4. WAPM package supply chain attack. Wasmer’s package registry (wasmer.io/packages) hosts WASM packages that can be executed directly with wasmer run <package>. A malicious actor publishes or compromises a WAPM package that contains a WASM module crafted to exploit a known Wasmer sandbox bug. Host systems running wasmer run to execute community WAPM packages — developer workstations, CI pipelines, automation scripts — are exposed to sandbox escape on the vulnerable Wasmer version.

Blast radius. A sandbox escape in any of these scenarios runs with the full privileges of the Wasmer host process. In an in-process embedding — the common case for language SDKs — a successful escape has access to the entire address space of the host application: all in-memory secrets, file descriptors, network connections, and any other data the host process holds. Singlepass bugs are particularly high-impact in multi-tenant plugin platforms because the compilation speed advantage that made Singlepass attractive is also present in the attack scenario: many guest modules are being compiled and executed, increasing the probability that a crafted module reaches the vulnerable code path. OS-level isolation must be in place before a Singlepass or LLVM backend bug becomes exploitable — without it, the WASM sandbox is the only enforcement layer, and a codegen bug defeats it entirely.

Configuration / Implementation

Choosing the right compiler backend for security-sensitive contexts

The Cranelift backend is the correct choice for any Wasmer deployment executing untrusted WASM. It is the most security-reviewed of the three backends, shares its codebase with Wasmtime, and is actively maintained by the Bytecode Alliance with security as an explicit goal. The Singlepass and LLVM backends should not be used for multi-tenant or untrusted WASM execution unless the compilation speed requirement is a hard constraint and additional OS-level isolation (see below) is in place.

Selecting Cranelift in Rust:

use wasmer::{Config, Engine, Module, Store};
use wasmer_compiler_cranelift::Cranelift;

fn build_secure_store() -> Store {
    let mut config = Config::new();
    // Cranelift is the default, but be explicit for auditability.
    let engine = Engine::new(&config, Cranelift::new());
    Store::new(engine)
}

Selecting Cranelift in Python using the wasmer SDK:

import wasmer
import wasmer_compiler_cranelift

# Explicitly select Cranelift for untrusted module execution.
store = wasmer.Store(wasmer_compiler_cranelift.Cranelift())

with open("untrusted_module.wasm", "rb") as f:
    wasm_bytes = f.read()

module = wasmer.Module(store, wasm_bytes)

Avoid the LLVM backend in contexts where the WASM source is untrusted and compilation throughput is not a constraint — the LLVM optimisation surface is too large for the security benefit (peak native performance) to outweigh the risk in a sandboxing context. Avoid Singlepass for untrusted input unless OS-level process isolation (described below) is enforced; if Singlepass is required for cold-start latency reasons, treat the WASM sandbox as advisory rather than security-enforcing and rely on the OS isolation boundary.

Checking the Wasmer version and applying updates

Check the installed Wasmer CLI version:

wasmer --version

Update the Wasmer CLI:

# Using the Wasmer self-updater:
wasmer self-update

# Or re-running the installer to get the latest release:
curl https://get.wasmer.io -sSfL | sh

Update the wasmer Rust crate in a Cargo project:

cargo update wasmer

Update the Python SDK:

pip install wasmer --upgrade
pip install wasmer_compiler_cranelift --upgrade

Update the Node.js SDK:

npm update @wasmerio/wasmer-js

After updating, check the Wasmer CHANGELOG for commits that touch compiler backends, VM memory handling, or WASI implementation since the previously installed version. The CHANGELOG is the primary (and often only) signal for security-relevant changes.

Memory limit enforcement

Wasmer allows explicit memory caps via MemoryType. For untrusted guest modules, always set a maximum page count. One WASM page is 64 KiB; 16 pages is 1 MiB.

use wasmer::{MemoryType, Pages, Store};

fn constrained_memory_type() -> MemoryType {
    // Minimum 1 page (64 KiB), maximum 16 pages (1 MiB).
    // The `false` argument means shared memory is disabled.
    MemoryType::new(1, Some(16), false)
}

To limit CPU consumption, use Wasmer’s metering middleware. Metering counts WASM instructions and traps the module when a fuel budget is exhausted:

use std::sync::Arc;
use wasmer::Store;
use wasmer_middlewares::Metering;
use wasmer_middlewares::metering::get_remaining_points;

fn cost_function(operator: &wasmer::wasmparser::Operator) -> u64 {
    // Assign uniform cost of 1 per instruction.
    // Adjust specific opcodes (e.g., memory operations) to higher values.
    match operator {
        wasmer::wasmparser::Operator::MemoryGrow { .. } => 1000,
        _ => 1,
    }
}

fn build_metered_engine() -> wasmer::Engine {
    use wasmer_compiler_cranelift::Cranelift;
    let metering = Arc::new(Metering::new(10_000_000, cost_function));
    let mut compiler = Cranelift::new();
    compiler.push_middleware(metering);
    wasmer::Engine::new(&wasmer::Config::new(), compiler)
}

These limits reduce the blast radius of a sandbox escape: even if a bounds-check bug allows an OOB read, the module cannot exhaust unbounded host memory before the fuel limit terminates execution.

Monitoring Wasmer commits for sandbox-relevant fixes

Because there is no CVE process or advisory feed, commit monitoring is the only reliable way to detect security-relevant changes in Wasmer. The following script queries the GitHub API for recent commits with messages matching patterns associated with security-relevant fixes:

#!/usr/bin/env bash
# wasmer-security-commits.sh
# Run on a schedule (e.g., daily via cron or CI) to detect sandbox-relevant fixes.

REPO="wasmerio/wasmer"

echo "=== Recent security-relevant commits to ${REPO} ==="
gh api "repos/${REPO}/commits?per_page=100" \
  --jq '.[] | select(
    .commit.message | test(
      "fix.*bound|fix.*memory|fix.*sandbox|fix.*codegen|fix.*miscompil|oob|security|overflow|escape";
      "i"
    )
  ) | {sha: .sha[0:8], msg: (.commit.message | split("\n")[0])}'

echo ""
echo "=== Commits touching compiler-singlepass ==="
gh api "repos/${REPO}/commits?per_page=50&path=lib/compiler-singlepass/src/codegen_x86_64.rs" \
  --jq '.[] | {sha: .sha[0:8], msg: (.commit.message | split("\n")[0]), date: .commit.committer.date}'

echo ""
echo "=== Commits touching VM trap handling ==="
gh api "repos/${REPO}/commits?per_page=50&path=lib/vm/src/trap" \
  --jq '.[] | {sha: .sha[0:8], msg: (.commit.message | split("\n")[0]), date: .commit.committer.date}'

echo ""
echo "=== Commits touching WASI implementation ==="
gh api "repos/${REPO}/commits?per_page=50&path=lib/wasi/src" \
  --jq '.[] | {sha: .sha[0:8], msg: (.commit.message | split("\n")[0]), date: .commit.committer.date}'

Run this script daily in CI and page on any new results. The paths to watch are:

  • lib/compiler-singlepass/src/ — Singlepass code generation, including codegen_x86_64.rs
  • lib/compiler-llvm/src/ — LLVM backend code generation
  • lib/compiler-cranelift/src/ — Cranelift backend
  • lib/vm/src/trap/ — VM-level trap and bounds handling
  • lib/wasi/src/ — WASI syscall implementation

Enable GitHub Watch notifications on the wasmerio/wasmer repository (Releases only is insufficient; watch All Activity or use the API polling approach above) so that commits to these paths generate a notification before they are included in a packaged release.

Isolation at the process level

Because Wasmer’s sandbox may contain unannounced correctness bugs, defence in depth requires OS-level isolation around Wasmer guest execution. Do not rely on the WASM sandbox alone for untrusted input.

Process isolation in Python:

import subprocess
import json
import sys

def run_wasm_isolated(wasm_path: str, input_data: bytes, timeout_seconds: float = 5.0) -> bytes:
    """
    Execute a WASM module in a separate subprocess so that a sandbox escape
    does not compromise the parent process.
    """
    result = subprocess.run(
        [sys.executable, "-c", f"""
import wasmer
import wasmer_compiler_cranelift
import sys

store = wasmer.Store(wasmer_compiler_cranelift.Cranelift())
with open({repr(wasm_path)}, "rb") as f:
    module = wasmer.Module(store, f.read())
instance = wasmer.Instance(module)
# Call the module's entry point; adapt to your module's exports.
run = instance.exports.run
output = run(len(sys.stdin.buffer.read()))
sys.stdout.buffer.write(str(output).encode())
"""],
        input=input_data,
        capture_output=True,
        timeout=timeout_seconds,
    )
    if result.returncode != 0:
        raise RuntimeError(f"WASM execution failed: {result.stderr.decode()}")
    return result.stdout

Seccomp profile for the Wasmer host process (Kubernetes):

apiVersion: v1
kind: Pod
metadata:
  name: wasmer-plugin-host
spec:
  securityContext:
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: plugin-runner
    image: your-registry/wasmer-host:4.x
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      runAsNonRoot: true
      runAsUser: 10001
      capabilities:
        drop:
          - ALL

RuntimeDefault seccomp blocks the most dangerous syscalls (ptrace, process_vm_readv, kexec_load, and others) that a sandbox escape would need to escalate privileges or read other processes’ memory. For maximum restriction, define a custom seccomp allowlist that permits only the syscalls Wasmer’s host process actually requires (read, write, mmap, munmap, mprotect, futex, clock_gettime, and the JIT-related mmap/mprotect calls).

Comparing Wasmer vs Wasmtime for your use case

If multi-tenant security is the primary requirement, evaluate whether Wasmtime is a better fit before investing further in Wasmer hardening. The security posture differences are significant:

  • Wasmtime has a published security advisory process, a CVE assignment pipeline, and an active Bytecode Alliance security team. Wasmer has none of these as of May 2026.
  • Both runtimes ship a Cranelift backend. Wasmtime’s Cranelift and Wasmer’s Cranelift share code but are maintained by different teams at different update cadences. For security-reviewed Cranelift specifically, Wasmtime’s version receives more direct security attention.
  • Wasmtime does not offer a Singlepass or LLVM backend — it is Cranelift-only for production use. If the Wasmer-specific backends are not a requirement, switching to Wasmtime eliminates the Singlepass and LLVM attack surface entirely.
  • Migration from Wasmer to Wasmtime requires API-level changes (different crate, different Store/Engine/Linker API surface, different WASI linker setup) and re-validation of all embedded WASM modules against the Wasmtime runtime. For a Rust embedder, this is a multi-day effort. For Python/Go/other SDK embedders, the available Wasmtime SDK for the target language needs evaluation for API parity.

If multi-language SDK support or the LLVM backend’s peak performance is a hard requirement, Wasmer remains the only option — but then the commit monitoring, Cranelift selection, and process isolation controls described above are not optional.

Expected Behaviour

Signal Wasmer with no monitoring or isolation Cranelift backend + process isolation + commit monitoring
Singlepass bounds-check bypass via crafted WASM Silent OOB memory read in host process; no log, no trap, no alert Attack surface eliminated: Singlepass not used; process isolation contains any residual escape
LLVM miscompilation causing OOB Silent OOB read or write; no trap; potential data leakage or memory corruption LLVM backend not used for untrusted input; Cranelift used instead; process isolation limits impact
WAPM package sandbox escape Arbitrary code execution in host process; full access to secrets and file descriptors Subprocess isolation limits escape to child process; seccomp blocks privilege escalation syscalls
Security-relevant fix committed to Wasmer upstream No notification received; vulnerable version runs indefinitely in production Commit monitoring script alerts within 24 hours; version pinned; update scheduled
Host memory read via OOB guest access Guest reads host process memory; secrets, tokens, and other tenant data exposed Escape contained to subprocess; parent process memory not accessible; alert generated

Trade-offs

Aspect Benefit Cost Mitigation
Cranelift over Singlepass Most security-reviewed backend; shared with Wasmtime; known-good security history Compilation is slower than Singlepass; cold-start latency increases in serverless/plugin contexts Pre-compile modules with wasmer compile to .wasmu artifacts and cache; use AOT loading to avoid JIT cost at instantiation
Process isolation (subprocess per WASM execution) Sandbox escape cannot reach parent process address space; secrets in host are protected Subprocess fork/exec overhead adds latency per invocation; IPC serialisation cost for input/output data Pre-warm a pool of subprocess workers; use shared memory or Unix sockets for IPC to reduce serialisation overhead
Commit monitoring for sandbox-relevant fixes Only mechanism for detecting Wasmer security fixes before a CVE exists High commit volume; regex matching produces false positives; alert fatigue risk Tune regex to path-scoped queries; add human review step before paging; track commits per-path rather than all commits
Migration from Wasmer to Wasmtime Gains a mature CVE process, published advisories, and Bytecode Alliance security review API surface changes require re-coding all embedder logic; all WASM modules must be re-validated against Wasmtime; testing burden is significant Scope migration to new workloads first; run Wasmer and Wasmtime in parallel during transition; validate with the existing WASM module test suite before cutting over

Failure Modes

Failure Symptom Detection Recovery
Cranelift backend unavailable for a specific WASM feature (e.g., WASM exception handling proposal not yet supported) Module compilation returns CompileError at startup; the module cannot be loaded with the Cranelift backend Compilation error logged at startup; module fails to instantiate Check Wasmer’s proposal support matrix for the Cranelift backend; either upgrade Wasmer to a version with support, switch to the LLVM backend with added OS isolation, or recompile the WASM module without the unsupported proposal
Process isolation overhead breaks latency SLA Per-request latency increases by 50–200 ms due to subprocess fork/exec and IPC overhead; timeout SLAs breached under load Latency percentile dashboards (p99, p999) spike; timeout error rate increases Implement a subprocess worker pool pre-forked at startup; use Unix domain sockets for low-latency IPC; profile IPC serialisation and switch to zero-copy where possible
Commit monitoring script produces excessive false positives Alerting channel receives tens of notifications per day; engineers stop reviewing; real security-relevant commits are missed Alert volume metrics; team feedback; manual review rate drops to near zero Refine the regex to require commit messages touching specific file paths; add a secondary filter requiring both message keyword match and path match; introduce a human triage step before paging
wasmer self-update or dependency update breaks existing WASM module compatibility Modules that compiled and ran correctly on the previous Wasmer version fail to compile, trap on instantiation, or produce incorrect results after the update Integration test suite failures immediately post-update; production error rate spike if update was applied without staging validation Pin Wasmer version in Cargo.lock and language SDK requirements files; validate all WASM modules against the candidate version in a staging environment before promoting to production; maintain a rollback path to the previous version