Ein einzelner Null-Pointer-Dereference innerhalb einer C-Bibliothek kann Ihre gesamte Anwendung zum Absturz bringen. Wenn diese Bibliothek Benutzereingaben parst, Bilder dekomprimiert oder network protocols verarbeitet, sind Sie ein malformed packet von einem Crash entfernt. Container lösen das Problem, aber Docker für eine einzelne dependency zu starten, ist wie ein Türsteher für eine Zimmerpflanze. Sie brauchen Isolation ohne den Overhead.

WebAssembly plus WASI ist die praktischste Antwort. Kompilieren Sie die Bibliothek zu WASM, führen Sie sie in einer sandboxed Runtime aus, und der Host-Prozess überlebt selbst dann, wenn der Guest segfaultet. Memory ist bounds-checked. Dateisystemzugriff ist capability-based. Und die Sandbox ist ein Library Call, kein System Call.

Was das Sandboxing einer C-Bibliothek tatsächlich bedeutet

C hat keinen Memory Safety. Ein Buffer Overflow in einer dependency kann Ihren Stack korrumpieren, die Ausführung hijacken oder den Host zum Absturz bringen. Traditionelle Mitigations wie ASAN finden Bugs zur Runtime, aber sie addieren Overhead und isolieren den Fehler nicht vom Rest des Prozesses. seccomp und Landlock können Syscalls einschränken, aber sie sind Linux-only, erfordern für manche Operationen root, und stoppen keine Memory Corruption innerhalb der erlaubten Syscalls.

WebAssembly verfolgt einen anderen Ansatz. Es kompiliert zu einem virtuellen Instruction Set mit expliziten Memory Bounds, strukturiertem Control Flow und keinem Undefined Behavior für Out-of-Bounds-Zugriffe. Eine WASM-Runtime wie Wasmtime oder WAMR validiert den Bytecode vor der Ausführung. Wenn der Guest Memory berührt, das ihm nicht gehört, trapt die Runtime. Der Host läuft weiter.

Das ist keine Emulation. Moderne WASM-Runtimes kompilieren zu nativem Machine Code via Cranelift oder LLVM. Der Performance-Hit liegt üblicherweise zwischen 1,1x und 2x für compute-bounden Code. Für I/O-bound Bibliotheken geht der Unterschied oft im Noise verloren.

Wie WASI Dateisystemzugriff in eine Capability verwandelt

WASI ist das WebAssembly System Interface. Es gibt sandboxed Modulen eine POSIX-like API, aber jede Resource ist eine Capability. Es gibt kein globales Dateisystem. Wenn Sie wollen, dass der Guest ./data/input.txt liest, preopen Sie explizit dieses Verzeichnis und übergeben den File Descriptor.

Hier ist eine minimale C-Bibliothek, die eine Datei liest und eine Checksum zurückgibt:

// 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;
}

Kompilieren Sie sie zu WebAssembly mit dem 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

Die resultierende .wasm-Datei ist ein sandboxed Modul. Es kann keine Datei öffnen, es sei denn, die Runtime gewährt explizit Zugriff. Es kann kein Memory außerhalb seiner linear memory region allozieren. Es kann keine arbiträren Shell-Kommandos ausführen, weil WASI execve nicht definiert.

Ausführung der sandboxed Bibliothek aus Rust

Wasmtime ist eine production-ready WASM-Runtime, geschrieben in Rust mit C- und Python-Bindings. Hier ist ein Host-Programm, das die kompilierte C-Bibliothek lädt, ihr read-only-Zugriff auf ./data/ gewährt und checksum_file aufruft:

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(())
}

Der Guest-C-Code läuft in seiner eigenen 32-bit linear memory. Der memory.write-Aufruf kopiert den Path-String in diesen sandboxed Address Space. Wenn checksum_file versucht, über das Ende seines Memory hinaus zu lesen, raised Wasmtime einen Trap. Wenn es versucht, /etc/passwd zu öffnen, gibt WASI ENOENT zurück, weil dieser Path nie preopened wurde.

Wo das Memory-Modell Sie tatsächlich schützt

WASM-Module nutzen einen einzelnen zusammenhängenden linear memory, typischerweise ab 64KB und wachsend on demand. Jeder Memory-Zugriff wird von der Runtime bounds-checked. Ein Buffer Overflow in der C-Bibliothek kann den Host-Stack oder Heap nicht korrumpieren. Es kann nicht zu arbitrary code springen. Es kann nur Memory in seiner eigenen allozierten Region berühren.

Das ist schwächer als volle Prozess-Isolation. Guest und Host teilen denselben OS-Prozess. Ein CPU-level Side-Channel-Angriff oder ein speculative execution Bug könnte theoretisch Daten über die Boundary leaken. Aber für das Threat Model „diese C-Bibliothek hat einen Bug und könnte crashen“ ist WASM-Isolation echt und praktisch.

Das Trap-on-Fault-Verhalten ist der Key Selling Point. Wenn der Guest einen Null-Pointer dereferenziert oder einen Buffer überläuft, fängt Wasmtime den Fehler ab und gibt einen Error zurück. Ihr Host-Prozess segfaultet nicht. Ihr Webserver startet nicht neu. Sie loggen den Error und machen weiter.

Die Trade-offs, die Sie bremsen werden

WASM ist nicht transparent. C-Code, der Threads, Signals, setjmp/longjmp oder raw mmap nutzt, wird nicht ohne Rewriting zu WASI kompilieren. Das WASI SDK unterstützt pthreads via einem optionalen Flag, aber es ist noch experimentell und erfordert, dass die Host-Runtime es aktiviert.

Die FFI-Boundary ist manuell. Sie können nicht einfach eine .wasm-Datei in ein C- oder Rust-Projekt linken und ihre Funktionen wie eine normale Bibliothek aufrufen. Sie marshallen Argumente in linear memory, rufen die Funktion per Index auf und lesen die Results zurück. Tooling wie wit-bindgen generiert den Glue Code aus Interface-Definitions, aber Sie zahlen trotzdem die Complexity Tax.

Performance variiert. Integer-heavy Code kompiliert zu WASM mit near-native Speed. Code, der häufige Host Calls macht, stark alloziert oder auf SIMD angewiesen ist, wird größere Slowdowns sehen. Wasmtime unterstützt SIMD-Proposals, aber nicht jedes Target unterstützt sie gleichmäßig.

Schließlich entwickelt sich WASI noch weiter. WASI Preview 1 ist stabil und weit verbreitet. Preview 2 führt Component Model Interfaces ein, die cleaner sind, aber noch nicht universal. Wenn Sie heute shipen, bleiben Sie bei Preview 1.

Alternativen, die man kennen sollte

Wenn WebAssembly sich wie zu viel Machinery anfühlt, gibt es leichtere Optionen mit anderen Trade-offs.

seccomp-bpf lässt Sie Syscalls für einen Linux-Prozess whitelisten. Es ist schnell und kernel-enforced. Aber es ist Linux-only, verhindert keine Memory Corruption innerhalb erlaubter Syscalls, und das Schreiben eines korrekten seccomp-Filters ist notorisch fehleranfällig.

Landlock ist ein neueres Linux-Feature, das Dateisystemzugriff ohne Privilegien einschränkt. Es ist einfacher als seccomp und behandelt das „kann diese Bibliothek meine SSH-Keys lesen“-Problem gut. Aber es ist immer noch Linux-only und sandboxed kein Memory.

Separate Prozesse mit fork oder einem Worker Pool geben Ihnen true OS Isolation. Ein Segfault im Child killt nur das Child. Der Nachteil ist Serialization Overhead und Prozess-Management-Komplexität. Für High-Throughput-Szenarien skaliert process-per-call nicht.

WebAssembly sitzt in der Mitte. Stärkere Isolation als seccomp, portabler als Landlock, leichter als separate Prozesse. Die richtige Wahl hängt davon ab, ob Ihr Bottleneck Syscall-Latenz, Memory Safety oder Deployment-Komplexität ist.

Ein praktischer Startpunkt

Sie müssen Ihre Anwendung nicht umschreiben. Picken Sie eine C-dependency, die untrusted input verarbeitet, kompilieren Sie sie zu WASM, und wrappen Sie sie hinter einem kleinen 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

Die --dir=./input::/input-Syntax mountet das Host-./input-Verzeichnis unter /input im Guest. Der Guest kann nichts anderes auf Ihrem Dateisystem sehen. Wenn parser.c einen Buffer Overflow enthält, bekommen Sie eine Trap-Message, kein Core Dump.

Für Host-Integration nutzen Sie die Wasmtime Embedding API in Rust, C oder Python. Starten Sie mit einer einzelnen exported function. Fügen Sie wit-bindgen hinzu, sobald hand-written marshaling zu klein wird. Profilen Sie den Overhead, bevor Sie sich für WASM auf Ihrem Hot Path festlegen.

FAQ

Kann ich eine C++-Bibliothek auf dieselbe Weise sandboxen?

Ja. Das WASI SDK enthält clang++ und unterstützt den Großteil von C++17. Exceptions und RTTI funktionieren, auch wenn thread-local storage einige Limitationen hat. Kompilieren Sie mit --target=wasm32-wasi und denselben Flags.

Was ist mit Bibliotheken, die das Netzwerk brauchen?

WASI Preview 1 enthält keine Sockets. Einige Runtimes bieten nonstandard extensions, oder Sie können Netzwerk-Requests durch den Host via imported functions proxyen. Preview 2 fügt Sockets hinzu, aber Support rollt noch über die Runtimes aus.

Wie groß ist das kompilierte WASM-Binary?

Typischerweise 2x bis 5x die Größe eines stripped native binary für denselben C-Code. Wasmtime kann den native code beim ersten Run kompilieren und cachen, sodass Startup Overhead ein one-time cost pro Binary-Version ist.

Ersetzt das Container?

Nein. WASM ist ein Prozess-level Sandbox. Container sind OS-level Isolation. Nutzen Sie WASM, um untrusted code in Ihrem Prozess zu isolieren. Nutzen Sie Container, um den Prozess vom Rest des Systems zu isolieren. Sie stacken, und sie bedienen verschiedene Threat Models.

Kann der Guest via einer WASI-Vulnerability entkommen?

Die Attack Surface ist die WASM-Runtime und die WASI-Implementation, nicht der Guest-Code selbst. Wasmtime ist in Rust geschrieben, was viele Memory Safety Bugs in der Runtime eliminiert. Die WebAssembly-Spec unterliegt auch formal verification. Keine Sandbox ist perfekt, aber WASM-Runtimes haben eine bessere Security Track Record als die meisten native Bibliotheken.

Ein Segfault in C sollte ein contained failure sein, kein Production Outage. Container sind die richtige Antwort, wenn Sie volle OS-Isolation brauchen. Wenn Sie nur verhindern müssen, dass eine Bibliothek Ihren Prozess übernimmt, ist WebAssembly schneller zu deployen, einfacher zu reason about, und genuin effektiv.