A single null pointer dereference inside a C library can bring down your entire application. If that library parses user input, decompresses images, or handles network protocols, you are one malformed packet away from a crash. Containers solve this, but spinning up Docker for one dependency is like hiring a bouncer for a houseplant. You need isolation without the overhead.

WebAssembly plus WASI is the most practical answer. Compile the library to WASM, run it inside a sandboxed runtime, and the host process survives even when the guest segfaults. Memory is bounds-checked. Filesystem access is capability-based. And the sandbox is a library call, not a system call.

What sandboxing a C library actually means

C has no memory safety. A buffer overflow in a dependency can corrupt your stack, hijack execution, or crash the host. Traditional mitigations like ASAN catch bugs at runtime, but they add overhead and do not isolate the failure from the rest of the process. seccomp and Landlock can restrict syscalls, but they are Linux-only, require root for some operations, and do not stop memory corruption inside the allowed syscalls.

WebAssembly takes a different approach. It compiles to a virtual instruction set with explicit memory bounds, structured control flow, and no undefined behavior for out-of-bounds access. A WASM runtime like Wasmtime or WAMR validates the bytecode before execution. If the guest touches memory it does not own, the runtime traps. The host keeps running.

This is not emulation. Modern WASM runtimes compile to native machine code via Cranelift or LLVM. The performance hit is usually between 1.1x and 2x for compute-bound code. For I/O-bound libraries, the difference is often lost in the noise.

How WASI turns filesystem access into a capability

WASI is the WebAssembly System Interface. It gives sandboxed modules a POSIX-like API, but every resource is a capability. There is no global filesystem. If you want the guest to read ./data/input.txt, you explicitly preopen that directory and pass the file descriptor in.

Here is a minimal C library that reads a file and returns a checksum:

// libchecksum.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>

uint32_t checksum_file(const char *path) {
    FILE *f = fopen(path, "rb");
    if (!f) return 0;

    uint32_t sum = 0;
    int c;
    while ((c = fgetc(f)) != EOF) {
        sum += (uint32_t)c;
    }
    fclose(f);
    return sum;
}

Compile it to WebAssembly with WASI SDK:

# Install wasi-sdk from https://github.com/WebAssembly/wasi-sdk
/opt/wasi-sdk/bin/clang \
  --target=wasm32-wasi \
  -Wl,--export=checksum_file \
  -o libchecksum.wasm libchecksum.c

The resulting .wasm file is a sandboxed module. It cannot open any file unless the runtime explicitly grants access. It cannot allocate memory outside its linear memory region. It cannot execute arbitrary shell commands because WASI does not define execve.

Running the sandboxed library from Rust

Wasmtime is a production-ready WASM runtime written in Rust with C and Python bindings. Here is a host program that loads the compiled C library, grants it read-only access to ./data/, and calls checksum_file:

use wasmtime::{Engine, Linker, Module, Store};
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView};

fn main() -> anyhow::Result<()> {
    let engine = Engine::default();
    let module = Module::from_file(&engine, "libchecksum.wasm")?;

    // Build a WASI context that preopens ./data as /data inside the guest
    let mut builder = WasiCtxBuilder::new();
    builder.preopen_dir("./data", "/data", wasmtime_wasi::DirPerms::READ_ONLY, wasmtime_wasi::FilePerms::READ_ONLY)?;
    let wasi = builder.build();

    let mut linker = Linker::new(&engine);
    wasmtime_wasi::add_to_linker_sync(&mut linker)?;

    let mut store = Store::new(&engine, wasi);
    let instance = linker.instantiate(&mut store, &module)?;

    // Call the exported function with a guest-allocated string
    let checksum_fn = instance.get_typed_func::<(i32, i32), i32>(&mut store, "checksum_file")?;

    // Write the path string into guest memory
    let memory = instance.get_memory(&mut store, "memory").unwrap();
    let path = b"/data/input.txt\0";
    let ptr = 0;
    memory.write(&mut store, ptr, path)?;

    let result = checksum_fn.call(&mut store, (ptr as i32, path.len() as i32))?;
    println!("Checksum: {}", result);

    Ok(())
}

The guest C code runs inside its own 32-bit linear memory. The memory.write call copies the path string into that sandboxed address space. If checksum_file tries to read past the end of its memory, Wasmtime raises a trap. If it tries to open /etc/passwd, WASI returns ENOENT because that path was never preopened.

Where the memory model actually protects you

WASM modules use a single contiguous linear memory, typically starting at 64KB and growing on demand. Every memory access is bounds-checked by the runtime. A buffer overflow in the C library cannot corrupt the host stack or heap. It cannot jump to arbitrary code. It can only touch memory inside its own allocated region.

This is weaker than full process isolation. The guest and host share the same OS process. A CPU-level side-channel attack or a speculative execution bug could theoretically leak data across the boundary. But for the threat model of “this C library has a bug and might crash,” WASM isolation is genuine and practical.

The trap-on-fault behavior is the key selling point. If the guest dereferences a null pointer or overflows a buffer, Wasmtime catches it and returns an error. Your host process does not segfault. Your web server does not restart. You log the error and move on.

The trade-offs that will slow you down

WASM is not transparent. C code that uses threads, signals, setjmp/longjmp, or raw mmap will not compile to WASI without rewriting. The WASI SDK supports pthreads via an optional flag, but it is still experimental and requires the host runtime to enable it.

The FFI boundary is manual. You cannot simply link a .wasm file into a C or Rust project and call its functions like a normal library. You marshal arguments into linear memory, call the function by index, and read the results back. Tooling like wit-bindgen generates the glue code from interface definitions, but you still pay the complexity tax.

Performance varies. Integer-heavy code compiles to WASM with near-native speed. Code that makes frequent host calls, allocates heavily, or relies on SIMD will see larger slowdowns. Wasmtime supports SIMD proposals, but not every target supports them uniformly.

Finally, WASI is still evolving. WASI Preview 1 is stable and widely supported. Preview 2 introduces component model interfaces that are cleaner but not yet universal. If you ship today, stick to Preview 1.

Alternatives that are worth knowing about

If WebAssembly feels like too much machinery, there are lighter options with different trade-offs.

seccomp-bpf lets you whitelist syscalls for a Linux process. It is fast and kernel-enforced. But it is Linux-only, does not prevent memory corruption inside allowed syscalls, and writing a correct seccomp filter is notoriously error-prone.

Landlock is a newer Linux feature that restricts filesystem access without requiring privileges. It is simpler than seccomp and handles the “can this library read my SSH keys” problem well. But it is still Linux-only and does not sandbox memory.

Separate processes with fork or a worker pool give you true OS isolation. A segfault in the child kills only the child. The downside is serialization overhead and process management complexity. For high-throughput scenarios, process-per-call does not scale.

WebAssembly sits in the middle. Stronger isolation than seccomp, more portable than Landlock, lighter than separate processes. The right choice depends on whether your bottleneck is syscall latency, memory safety, or deployment complexity.

A practical starting point

You do not need to rewrite your application. Pick one C dependency that handles untrusted input, compile it to WASM, and wrap it behind a small host shim.

# 1. Install wasi-sdk (or use the prebuilt release)
curl -LO https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-24/wasi-sdk-24.0-x86_64-linux.tar.gz
tar xzf wasi-sdk-24.0-x86_64-linux.tar.gz

# 2. Compile your C library
./wasi-sdk-24.0/bin/clang --target=wasm32-wasi \
  -O2 -Wl,--export-all -o parser.wasm parser.c

# 3. Run it with wasmtime
wasmtime run --dir=./input::/input parser.wasm /input/untrusted.dat

The --dir=./input::/input syntax mounts the host’s ./input directory at /input inside the guest. The guest cannot see anything else on your filesystem. If parser.c contains a buffer overflow, you get a trap message, not a core dump.

For host integration, use the Wasmtime embedding API in Rust, C, or Python. Start with a single exported function. Add wit-bindgen once you outgrow hand-written marshaling. Profile the overhead before committing to WASM for your hot path.

FAQ

Can I sandbox a C++ library the same way?

Yes. WASI SDK includes clang++ and supports most of C++17. Exceptions and RTTI work, though thread-local storage has some limitations. Compile with --target=wasm32-wasi and the same flags.

What about libraries that need the network?

WASI Preview 1 does not include sockets. Some runtimes offer nonstandard extensions, or you can proxy network requests through the host via imported functions. Preview 2 adds sockets, but support is still rolling out across runtimes.

How large is the compiled WASM binary?

Typically 2x to 5x the size of a stripped native binary for the same C code. Wasmtime can compile and cache the native code on first run, so startup overhead is a one-time cost per binary version.

Does this replace containers?

No. WASM is a process-level sandbox. Containers are OS-level isolation. Use WASM to isolate untrusted code inside your process. Use containers to isolate the process from the rest of the system. They stack, and they serve different threat models.

Can the guest escape via a WASI vulnerability?

The attack surface is the WASM runtime and the WASI implementation, not the guest code itself. Wasmtime is written in Rust, which eliminates many memory safety bugs in the runtime. The WebAssembly spec also undergoes formal verification. No sandbox is perfect, but WASM runtimes have a better security track record than most native libraries.

A segfault in C should be a contained failure, not a production outage. Containers are the right answer when you need full OS isolation. When you just need to stop one library from owning your process, WebAssembly is faster to deploy, easier to reason about, and genuinely effective.