Preventing Sensitive Data Exposure via WebAssembly Coredumps in Production
Problem
WebAssembly coredumps are a debugging feature that captures the complete state of a Wasm instance at the point of a trap: the operand stack, the call stack, all local variable values, and the full contents of linear memory. This is intentionally comprehensive — the goal is to give developers enough information to diagnose hard-to-reproduce crashes without a live debugger attached.
The security problem is that “full contents of linear memory” means exactly that. The Wasm linear memory model uses a single flat address space for all application data. When a coredump is generated, that address space includes: in-flight TLS session keys, decrypted secrets that were loaded from environment variables or secret stores, cryptographic private keys loaded for signing operations, database passwords, API tokens, personally identifiable information being processed at the time of the trap, and any other sensitive data the Wasm module was handling in memory at crash time.
The Wasm coredump specification (part of the WebAssembly tooling/debugging proposals) has been gaining runtime support rapidly. Wasmtime added coredump support in version 12.0 (2023) and it is enabled by default in some configurations. WasmEdge, Wasmer, and several edge runtimes have followed. Developer tooling generates coredumps from browser-side Wasm traps as well, where crash reports sent to analytics services may contain the same memory contents.
The concern is not hypothetical. Coredumps in traditional POSIX systems have caused secret leaks for decades: a core dump of a process handling payment card data, written to a world-readable location or forwarded to a crash reporting service, is a direct path to data exfiltration. Wasm coredumps reproduce this problem in a new context where:
- The feature is newer and less understood — security policies around core dump handling that teams have for Linux processes haven’t been ported to Wasm deployment playbooks.
- Wasm is increasingly used for sensitive workloads — MCP servers, AI inference, payment processing, authentication, and enterprise middleware, all run as Wasm modules in edge and cloud-native environments.
- Coredumps may be forwarded to external services automatically — crash reporting integrations in runtimes or build tools can send coredumps to Sentry, Datadog, or similar, where they are processed on infrastructure outside the organization’s control.
- WASI surfaces new paths for coredumps to reach unintended locations — a WASI-enabled module can write its own coredump to a file via
wasi_snapshot_preview1::path_open, potentially in a shared filesystem path.
The risk is compounded by multi-tenant Wasm deployments (Cloudflare Workers, Fastly Compute, WasmCloud) where linear memory from one tenant’s crashing module must not be accessible to the hosting platform’s logging or other tenants.
Target systems: Wasmtime ≥12.0 with coredump feature enabled; WasmEdge ≥0.13; Wasmer ≥4.0; any Wasm deployment running in edge functions, MCP servers, or enterprise middleware that processes sensitive data; browser environments where crash reporters are configured.
Threat Model
Adversary 1 — Crash reporting service with insufficient data handling. Access level: the crash reporting service (Sentry, Bugsnag, internal logging) receives coredump attachments. Objective: extract API keys, database credentials, or PII from the linear memory snapshot included in the crash report. No active attacker required — this is a passive data leak through a legitimate logging channel.
Adversary 2 — Attacker-induced crash. Access level: ability to send requests to a Wasm-based service (HTTP endpoint, MCP server). Objective: craft inputs that cause a predictable trap (out-of-bounds access, division by zero, explicit unreachable in error handling), trigger coredump generation, and arrange for the coredump file to be written to a path where the attacker can read it (e.g., a shared filesystem or a publicly accessible blob store).
Adversary 3 — Multi-tenant isolation breach. Access level: a tenant running code on a shared Wasm hosting platform. Objective: cause a crash in a way that causes the platform to log memory contents in shared logging infrastructure, cross-contaminating one tenant’s secrets into another tenant’s log stream.
Adversary 4 — Build tooling exfiltration. Access level: control over a build or CI process. Objective: configure the Wasm runtime in the CI environment to generate coredumps to a path that is captured by artifact upload steps, causing secrets to be stored in the artifact store.
Without hardening: a single Wasm trap during the handling of a sensitive request writes the entire linear memory snapshot to disk or to a crash report. With hardening: coredump generation is disabled in production, scoped to non-sensitive workloads, or processed through a scrubbing pipeline that removes sensitive regions before the coredump is stored.
Configuration / Implementation
Step 1 — Audit current coredump configuration in your runtimes
# Wasmtime: check if coredump is enabled
wasmtime --help | grep coredump
# --coredump-on-trap=PATH Enables coredump generation on trap
# Check if any running processes use coredump
ps aux | grep wasmtime | grep coredump
# WasmEdge: check configuration
wasmedge --help | grep core
# Check for coredump files in common locations
find /tmp /var/tmp /var/log -name "*.coredump" -o -name "wasm.core" 2>/dev/null
Step 2 — Disable coredumps in production Wasmtime deployments
Wasmtime requires an explicit flag to enable coredump generation. Verify it is absent from production:
# Production — DO NOT include --coredump-on-trap
wasmtime run --module your-module.wasm
# Development/staging — enable to an access-controlled path only
wasmtime run --coredump-on-trap=/var/coredumps/dev/wasm.core \
--module your-module.wasm
For deployments using the Wasmtime embedding API (Rust):
use wasmtime::Config;
fn production_engine_config() -> Config {
let mut config = Config::new();
// Coredump is disabled by default — do not call .coredump_on_trap(true)
// Explicitly document the intention:
// config.coredump_on_trap(false); // Default; not required but documents intent
config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Disable); // No memory in backtraces
config
}
fn development_engine_config() -> Config {
let mut config = Config::new();
// Only enable in controlled development environments
// config.coredump_on_trap(true);
config
}
For the Wasmtime C API:
// Production: coredump disabled by default
wasm_config_t *config = wasm_config_new();
// No wasmtime_config_coredump_on_trap_set() call
// Development only:
// wasmtime_config_coredump_on_trap_set(config, true);
Step 3 — Harden file permissions if coredumps are needed in staging
If coredumps are needed in non-production environments for debugging:
# Create a coredump directory accessible only to the service user
install -d -m 0700 -o wasmservice -g wasmservice /var/coredumps/staging
# Run the Wasm runtime as a dedicated service user
useradd --system --no-create-home --shell /bin/false wasmservice
# Systemd service unit
cat > /etc/systemd/system/wasm-service.service <<'EOF'
[Unit]
Description=Wasm Service (Staging)
[Service]
User=wasmservice
Group=wasmservice
ExecStart=/usr/local/bin/wasmtime run \
--coredump-on-trap=/var/coredumps/staging/wasm.core \
/opt/wasm/your-module.wasm
# Prevent coredump from leaking to systemd journal
LimitCORE=0
# Prevent coredump from being written outside the designated path
ReadWritePaths=/var/coredumps/staging
PrivateTmp=true
ProtectSystem=strict
EOF
systemctl daemon-reload
systemctl start wasm-service
The LimitCORE=0 in the service unit prevents the OS-level coredump mechanism from firing even if the Wasm runtime process itself crashes (distinct from Wasm-level coredumps).
Step 4 — Implement a coredump scrubbing pipeline for debugging workflows
When coredumps are required for debugging and may contain sensitive data, implement a scrubbing step before the coredump is stored long-term or forwarded:
#!/usr/bin/env python3
"""Scrub sensitive data patterns from Wasm coredump binary files.
Wasm coredumps are binary files following the coredump custom section format.
Linear memory contents are embedded as raw bytes. This script zeroes known
sensitive memory patterns.
"""
import re
import struct
import sys
from pathlib import Path
# Patterns to scrub from binary memory contents
SENSITIVE_PATTERNS = [
# AWS access key format
rb'AKIA[A-Z0-9]{16}',
# Generic API key-like patterns (high-entropy 32+ char alphanumeric)
rb'(?:api[_-]?key|apikey|bearer|token)[=: ]+[A-Za-z0-9_\-]{32,}',
# OpenAI key format
rb'sk-[A-Za-z0-9]{32,}',
# Private key headers
rb'-----BEGIN [A-Z ]+PRIVATE KEY-----',
# Database connection strings
rb'(?:postgres|mysql|mongodb)://[^@]+:[^@]+@',
]
def scrub_coredump(input_path: Path, output_path: Path) -> int:
"""Scrub sensitive patterns from coredump. Returns count of scrubbed regions."""
data = bytearray(input_path.read_bytes())
scrub_count = 0
for pattern in SENSITIVE_PATTERNS:
for match in re.finditer(pattern, data, re.IGNORECASE):
start, end = match.span()
# Replace with zeros (preserves file structure; just nulls the value)
data[start:end] = b'\x00' * (end - start)
scrub_count += 1
print(f"Scrubbed pattern at offset 0x{start:08x}–0x{end:08x}: {pattern[:30]}")
output_path.write_bytes(data)
return scrub_count
if __name__ == "__main__":
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <input.coredump> <output.coredump>")
sys.exit(1)
count = scrub_coredump(Path(sys.argv[1]), Path(sys.argv[2]))
print(f"Scrubbing complete: {count} sensitive regions zeroed")
Integrate into the coredump handling pipeline:
#!/bin/bash
# /usr/local/bin/handle-wasm-coredump.sh
# Called by the Wasm runtime or a file watcher when a coredump appears
COREDUMP_FILE=$1
STAGING_DIR=/var/coredumps/staging
ARCHIVE_DIR=/var/coredumps/archive
SCRUBBER=/usr/local/bin/scrub-wasm-coredump.py
if [[ -z "$COREDUMP_FILE" ]]; then
echo "Usage: $0 <coredump-file>"
exit 1
fi
BASENAME=$(basename "$COREDUMP_FILE")
SCRUBBED="$STAGING_DIR/scrubbed-$BASENAME"
# Scrub the coredump
python3 "$SCRUBBER" "$COREDUMP_FILE" "$SCRUBBED"
# Archive the scrubbed version with 90-day retention
install -m 0600 "$SCRUBBED" "$ARCHIVE_DIR/$BASENAME-$(date +%Y%m%d).coredump.scrubbed"
# Delete the original (unscrubbed) coredump immediately
shred -u "$COREDUMP_FILE"
rm -f "$SCRUBBED"
echo "Coredump processed: original deleted, scrubbed archive stored"
Step 5 — Configure browser-side coredump policies
For browser-based Wasm deployments where crash reporters are in use:
// Disable coredump attachment in crash reporter configuration
// Sentry example:
import * as Sentry from "@sentry/browser";
Sentry.init({
dsn: "https://...",
// Custom beforeSend to strip Wasm-related memory attachments
beforeSend(event, hint) {
// Remove any attachments that might contain memory contents
if (event.attachments) {
event.attachments = event.attachments.filter(attachment =>
!attachment.filename?.includes('.coredump') &&
!attachment.filename?.includes('wasm.core')
);
}
// Strip large breadcrumb data that might contain memory snapshots
if (event.breadcrumbs?.values) {
event.breadcrumbs.values = event.breadcrumbs.values.map(crumb => ({
...crumb,
data: undefined // Remove data payloads from breadcrumbs
}));
}
return event;
}
});
Step 6 — Apply memory isolation for sensitive operations
For Wasm modules that handle secrets, use a separate module instance that is destroyed immediately after the sensitive operation, minimizing the window during which a trap could expose the secret in a coredump:
use wasmtime::{Engine, Linker, Module, Store};
fn execute_with_secret_isolation(
engine: &Engine,
module: &Module,
secret_operation: impl FnOnce(&mut Store<()>) -> anyhow::Result<()>
) -> anyhow::Result<()> {
// Create a fresh store for each sensitive operation
// If this store traps, coredump contains only this operation's memory
let mut isolated_store = Store::new(engine, ());
// Execute the sensitive operation
let result = secret_operation(&mut isolated_store);
// Explicitly drop the store to clear memory immediately after use
// This minimizes the window where memory is accessible
drop(isolated_store);
result
}
Expected Behaviour
| Signal | Before hardening | After hardening |
|---|---|---|
| Wasm trap in production | Coredump file written to /tmp/wasm.core with full memory |
No coredump generated; trap results in logged error only |
find /tmp -name "*.coredump" in production |
Returns coredump files | Returns nothing |
| Coredump in staging | Full memory; may contain secrets | Scrubbing pipeline runs; original deleted; scrubbed archive stored |
| Crash reporter receives event | May include Wasm memory attachment | beforeSend filter removes coredump attachments |
| Sensitive operation Wasm instance | Shared with other operations | Isolated store; dropped immediately after operation |
Verification:
# Confirm no coredump flag in production process
ps aux | grep wasmtime | grep -v grep | grep coredump
# Should return nothing
# Confirm coredump directory is properly restricted in staging
ls -la /var/coredumps/staging/
# Should show mode 0700 and owner wasmservice
# Trigger a test trap and verify no coredump is written
wasmtime run --module test-trap.wasm 2>&1
# Expected: error output, no file written to /tmp
ls /tmp/*.coredump 2>/dev/null; echo "exit: $?"
# exit: 1 (no file found)
Trade-offs
| Aspect | Benefit | Cost | Mitigation |
|---|---|---|---|
| Disabled coredumps in production | Eliminates memory disclosure risk | Debugging production crashes becomes much harder | Use structured error logging with safe (non-memory) context; enable coredumps on identical staging environment with same input data |
| Scrubbing pipeline | Enables coredump use in staging without full secret exposure | Regex patterns can miss novel secret formats; file structure must be preserved | Supplement with allow-listing: scrub everything except known-safe memory regions (code sections, public strings) |
| Per-operation store isolation | Limits coredump blast radius to single operation | Overhead of store creation per operation (~microseconds) | Profile; acceptable for most workloads; skip for hot loops with non-sensitive data |
shred -u on original coredumps |
Reduces residual data on disk | Not effective on SSDs with wear leveling or copy-on-write filesystems | Use dm-crypt encrypted volumes for coredump directories; treat scrubbed archive as the only retained artifact |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| New runtime version re-enables coredumps by default | Production module starts writing coredumps on trap | Regular audit of find /var /tmp -name "*.coredump" |
Pin runtime version in deployments; include coredump check in post-deploy verification |
| Scrubbing pipeline fails — original not deleted | Unscrubbed coredump remains in staging | Monitor for *.coredump files not processed within 5 minutes |
Alert on coredump file age > 10 minutes; escalate to manual scrub + delete |
| Sensitive data in Wasm global variables (not linear memory) | Scrubber covers linear memory but globals are in a different section of the coredump binary | Coredump section analysis shows populated global section | Extend scrubber to parse Wasm coredump format and zero global value sections |
| CI/CD artifact upload captures coredump from test run | Coredump with test credentials uploaded to artifact store | Audit artifact store for *.coredump files; check artifact upload lists |
Add .coredump to artifact upload exclusion list; rotate any credentials that appeared in test input |
Related Articles
- Wasm Linear Memory Safety — understanding the linear memory model that makes coredumps comprehensive (and dangerous)
- Wasm Edge Secrets Management — keeping secrets out of Wasm linear memory in the first place
- Wasm Debug Symbol Security — controlling what debugging information is included in production Wasm binaries
- Wasm Multi-Tenancy — isolation boundaries that prevent one tenant’s coredump memory from contaminating another’s
- Wasmtime Production Hardening — comprehensive Wasmtime configuration for production deployments including memory limits and trap handling