Um único null pointer dereference dentro de uma biblioteca em C pode derrubar toda a sua aplicação. Se essa biblioteca faz parse de input do usuário, descomprime imagens ou lida com protocols de rede, você está a um malformed packet de um crash. Containers resolvem isso, mas subir Docker para uma única dependência é como contratar um segurança para uma planta de interior. Você precisa de isolamento sem o overhead.

WebAssembly mais WASI é a resposta mais prática. Compile a biblioteca para WASM, execute-a dentro de uma runtime sandboxed, e o processo host sobrevive mesmo quando o guest segfaulta. A memória tem bounds checking. O acesso ao filesystem é capability-based. E o sandbox é uma library call, não uma system call.

O que significa realmente fazer sandbox de uma biblioteca em C

C não tem memory safety. Um buffer overflow em uma dependência pode corromper sua stack, sequestrar a execução ou derrubar o host. Mitigações tradicionais como ASAN pegam bugs em runtime, mas adicionam overhead e não isolam a falha do resto do processo. seccomp e Landlock podem restringir syscalls, mas são apenas para Linux, exigem root para algumas operações, e não impedem memory corruption dentro das syscalls permitidas.

WebAssembly toma uma abordagem diferente. Ele compila para um instruction set virtual com memory bounds explícitas, control flow estruturado e nenhum undefined behavior para acessos out-of-bounds. Uma runtime de WASM como Wasmtime ou WAMR valida o bytecode antes da execução. Se o guest toca memória que não lhe pertence, a runtime dispara um trap. O host continua rodando.

Isso não é emulação. Runtimes modernas de WASM compilam para código de máquina nativo via Cranelift ou LLVM. O hit de performance geralmente fica entre 1,1x e 2x para código compute-bound. Para bibliotecas I/O-bound, a diferença geralmente se perde no ruído.

Como WASI transforma o acesso ao filesystem em uma capability

WASI é o WebAssembly System Interface. Ele dá a modules sandboxed uma API tipo POSIX, mas cada recurso é uma capability. Não há filesystem global. Se você quer que o guest leia ./data/input.txt, você preabre explicitamente esse diretório e passa o file descriptor.

Aqui está uma biblioteca mínima em C que lê um arquivo e retorna um 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-a para WebAssembly com o 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

O arquivo .wasm resultante é um module sandboxed. Ele não pode abrir nenhum arquivo a menos que a runtime conceda acesso explicitamente. Ele não pode alocar memória fora de sua linear memory region. Ele não pode executar comandos de shell arbitrários porque WASI não define execve.

Executar a biblioteca sandboxed a partir de Rust

Wasmtime é uma runtime de WASM production-ready, escrita em Rust com bindings para C e Python. Aqui está um programa host que carrega a biblioteca compilada em C, concede acesso read-only a ./data/, e chama 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(())
}

O código C do guest roda dentro de sua própria linear memory de 32 bits. A chamada memory.write copia a string de path para aquele address space sandboxed. Se checksum_file tentar ler além do final de sua memória, Wasmtime levanta um trap. Se tentar abrir /etc/passwd, WASI retorna ENOENT porque esse path nunca foi preopened.

Onde o modelo de memória realmente te protege

modules WASM usam uma única linear memory contígua, tipicamente começando em 64KB e crescendo on demand. Cada acesso à memória é bounds-checked pela runtime. Um buffer overflow na biblioteca C não pode corromper a stack ou heap do host. Não pode pular para código arbitrário. Só pode tocar memória dentro de sua própria região alocada.

Isso é mais fraco que o isolamento completo de processo. O guest e o host compartilham o mesmo processo do SO. Um ataque de side-channel a nível de CPU ou um bug de speculative execution poderia teoricamente vazar dados através da fronteira. Mas para o threat model de “esta biblioteca em C tem um bug e pode crashar”, o isolamento de WASM é genuíno e prático.

O comportamento trap-on-fault é o key selling point. Se o guest dereferencia um null pointer ou transborda um buffer, Wasmtime o intercepta e retorna um erro. Seu processo host não segfaulta. Seu servidor web não reinicia. Você loga o erro e segue.

Os trade-offs que vão te atrasar

WASM não é transparente. Código C que usa threads, sinais, setjmp/longjmp, ou mmap raw não vai compilar para WASI sem reescrita. O WASI SDK suporta pthreads via uma flag opcional, mas ainda é experimental e requer que a runtime host o habilite.

A fronteira FFI é manual. Você não pode simplesmente linkar um arquivo .wasm em um projeto C ou Rust e chamar suas funções como uma biblioteca normal. Você marshalla argumentos para a linear memory, chama a função por index, e lê os resultados de volta. Tooling como wit-bindgen gera o glue code a partir de definições de interface, mas você ainda paga a complexity tax.

Performance varia. Código intensivo em inteiros compila para WASM com near-native speed. Código que faz host calls frequentes, aloca muito, ou depende de SIMD verá slowdowns maiores. Wasmtime suporta propostas de SIMD, mas nem todo target as suporta uniformemente.

Finalmente, WASI ainda está evoluindo. WASI Preview 1 é estável e amplamente suportado. Preview 2 introduz interfaces de component model que são mais limpas mas ainda não universais. Se você faz ship hoje, fique no Preview 1.

Alternativas que valem a pena conhecer

Se WebAssembly parece com muita maquinaria, existem opções mais leves com diferentes trade-offs.

seccomp-bpf permite que você faça uma whitelist de syscalls para um processo Linux. É rápido e kernel-enforced. Mas é apenas para Linux, não previne memory corruption dentro de syscalls permitidas, e escrever um filtro seccomp correto é notoriamente propenso a erros.

Landlock é um recurso Linux mais novo que restringe o acesso ao filesystem sem exigir privilégios. É mais simples que seccomp e lida bem com o problema de “esta biblioteca pode ler minhas chaves SSH?”. Mas ainda é apenas para Linux e não faz sandbox de memória.

Processos separados com fork ou um pool de workers te dão true OS isolation. Um segfault no child mata apenas o child. A desvantagem é o overhead de serialização e a complexidade de gerenciamento de processos. Para cenários de alto throughput, process-per-call não escala.

WebAssembly fica no meio. Isolamento mais forte que seccomp, mais portável que Landlock, mais leve que processos separados. A escolha certa depende de se seu bottleneck é latência de syscalls, memory safety ou complexidade de deployment.

Um ponto de partida prático

Você não precisa reescrever sua aplicação. Escolha uma dependência em C que lida com input não confiável, compile-a para WASM, e envolva-a atrás de um pequeno shim host.

# 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

A sintaxe --dir=./input::/input monta o diretório ./input do host em /input dentro do guest. O guest não pode ver mais nada no seu filesystem. Se parser.c contém um buffer overflow, você recebe uma mensagem de trap, não um core dump.

Para a integração com o host, use a Wasmtime Embedding API em Rust, C ou Python. Comece com uma única exported function. Adicione wit-bindgen uma vez que o marshaling manual se torne limitado. Profile o overhead antes de se comprometer com WASM no seu hot path.

FAQ

Posso fazer sandbox de uma biblioteca C++ da mesma maneira?

Sim. O WASI SDK inclui clang++ e suporta a maior parte de C++17. Exceções e RTTI funcionam, embora o thread-local storage tenha algumas limitações. Compile com --target=wasm32-wasi e as mesmas flags.

E bibliotecas que precisam de rede?

WASI Preview 1 não inclui sockets. Algumas runtimes oferecem extensões não padrão, ou você pode proxyar requisições de rede através do host via imported functions. Preview 2 adiciona sockets, mas o suporte ainda está sendo implantado entre as runtimes.

Qual é o tamanho do binário WASM compilado?

Tipicamente 2x a 5x o tamanho de um binário nativo stripped para o mesmo código em C. Wasmtime pode compilar e cachear o código nativo na primeira execução, então o startup overhead é um custo one-time por versão do binário.

Isso substitui containers?

Não. WASM é um sandbox a nível de processo. Containers são isolamento a nível de SO. Use WASM para isolar código não confiável dentro do seu processo. Use containers para isolar o processo do resto do sistema. Eles se empilham, e servem a diferentes threat models.

O guest pode escapar via uma vulnerabilidade de WASI?

A superfície de ataque é a runtime de WASM e a implementação de WASI, não o código do guest em si. Wasmtime é escrito em Rust, o que elimina muitos bugs de memory safety na runtime. A especificação WebAssembly também passa por formal verification. Nenhum sandbox é perfeito, mas as runtimes de WASM têm um melhor security track record que a maioria das bibliotecas nativas.

Um segfault em C deveria ser uma falha contida, não um outage de produção. Containers são a resposta certa quando você precisa de isolamento completo do SO. Quando você só precisa impedir que uma biblioteca tome conta do seu processo, WebAssembly é mais rápido de deployar, mais simples de raciocinar, e genuinamente eficaz.