Satu null pointer dereference di dalam library C bisa membuat seluruh aplikasi Anda crash. Jika library tersebut mem-parse input pengguna, melakukan dekompresi gambar, atau menangani protocol jaringan, Anda hanya berjarak satu malformed packet dari kecelakaan. Container memang menyelesaikan masalah ini, tapi menjalankan Docker hanya untuk satu dependency itu seperti menyewa penjaga untuk tanaman hias. Anda butuh isolasi tanpa overhead yang besar.

WebAssembly ditambah WASI adalah jawaban paling praktis. Kompilasi library ke WASM, jalankan di dalam runtime yang tersandbox, dan proses host akan tetap bertahan bahkan ketika guest mengalami segfault. Memori dilakukan bounds checking. Akses filesystem bersifat capability-based. Dan sandbox ini adalah library call, bukan system call.

Apa yang sebenarnya dimaksud dengan sandboxing library C

C tidak memiliki memory safety. Sebuah buffer overflow di dalam dependency bisa merusak stack, membajak eksekusi, atau membuat host crash. Mitigasi tradisional seperti ASAN menemukan bug saat runtime, tapi menambah overhead dan tidak mengisolasi kegagalan dari sisa proses. seccomp dan Landlock bisa membatasi syscall, tapi hanya untuk Linux, memerlukan root untuk beberapa operasi, dan tidak menghentikan memory corruption di dalam syscall yang diizinkan.

WebAssembly mengambil pendekatan berbeda. Ia dikompilasi ke instruction set virtual dengan memory bounds eksplisit, control flow terstruktur, dan tidak ada undefined behavior untuk akses out-of-bounds. Runtime WASM seperti Wasmtime atau WAMR memvalidasi bytecode sebelum eksekusi. Jika guest menyentuh memori yang bukan miliknya, runtime akan melakukan trap. Host tetap berjalan.

Ini bukan emulasi. Runtime WASM modern mengompilasi ke kode mesin native melalui Cranelift atau LLVM. Penalti performa biasanya berada di antara 1,1x hingga 2x untuk kode compute-bound. Untuk library I/O-bound, perbedaannya seringkali hilang dalam derau.

Bagaimana WASI mengubah akses filesystem menjadi capability

WASI adalah WebAssembly System Interface. Ia memberikan API mirip POSIX ke module yang tersandbox, tapi setiap resource adalah sebuah capability. Tidak ada filesystem global. Jika Anda ingin guest membaca ./data/input.txt, Anda harus secara eksplisit melakukan preopen direktori tersebut dan melewatkan file descriptor-nya.

Berikut adalah library C minimal yang membaca file dan mengembalikan 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;
}

Kompilasi ke WebAssembly dengan 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

File .wasm yang dihasilkan adalah module yang tersandbox. Ia tidak bisa membuka file apa pun kecuali runtime secara eksplisit memberikan akses. Ia tidak bisa mengalokasikan memori di luar linear memory region miliknya. Ia tidak bisa menjalankan perintah shell sewenang-wenang karena WASI tidak mendefinisikan execve.

Menjalankan library tersandbox dari Rust

Wasmtime adalah runtime WASM production-ready, ditulis dalam Rust dengan binding untuk C dan Python. Berikut adalah program host yang memuat library C yang telah dikompilasi, memberinya akses read-only ke ./data/, dan memanggil 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(())
}

Kode C guest berjalan di dalam linear memory 32-bit miliknya sendiri. Panggilan memory.write menyalin string path ke address space yang tersandbox tersebut. Jika checksum_file mencoba membaca melewati akhir memorinya, Wasmtime akan melakukan trap. Jika ia mencoba membuka /etc/passwd, WASI akan mengembalikan ENOENT karena path tersebut tidak pernah di-preopen.

Di mana model memori sebenarnya melindungi Anda

Module WASM menggunakan satu linear memory yang berkelanjutan, biasanya dimulai dari 64KB dan tumbuh sesuai permintaan. Setiap akses memori dilakukan bounds checking oleh runtime. Sebuah buffer overflow di library C tidak bisa merusak stack atau heap host. Ia tidak bisa melompat ke kode sewenang-wenang. Ia hanya bisa menyentuh memori di dalam region yang dialokasikan untuknya sendiri.

Ini lebih lemah dibanding isolasi proses penuh. Guest dan host berbagi proses OS yang sama. Serangan side-channel tingkat CPU atau bug speculative execution secara teoretis bisa menyebabkan kebocoran data melintasi batas. Tapi untuk threat model “library C ini memiliki bug dan mungkin crash”, isolasi WASM adalah nyata dan praktis.

Perilaku trap-on-fault adalah selling point utamanya. Jika guest melakukan dereference null pointer atau buffer overflow, Wasmtime menangkapnya dan mengembalikan error. Proses host Anda tidak mengalami segfault. Server web Anda tidak restart. Anda mencatat error dan melanjutkan.

Trade-off yang akan memperlambat Anda

WASM tidak transparan. Kode C yang menggunakan thread, signal, setjmp/longjmp, atau mmap raw tidak akan dikompilasi ke WASI tanpa penulisan ulang. WASI SDK mendukung pthreads melalui flag opsional, tapi masih bersifat eksperimental dan memerlukan runtime host untuk mengaktifkannya.

Batas FFI bersifat manual. Anda tidak bisa sekadar men-link file .wasm ke proyek C atau Rust dan memanggil fungsinya seperti library normal. Anda melakukan marshalling argumen ke linear memory, memanggil fungsi berdasarkan index, dan membaca hasilnya kembali. Tooling seperti wit-bindgen menghasilkan glue code dari definisi antarmuka, tapi Anda tetap membayar complexity tax.

Performa bervariasi. Kode yang banyak menggunakan integer dikompilasi ke WASM dengan kecepatan mendekati native. Kode yang sering melakukan host call, banyak mengalokasikan, atau mengandalkan SIMD akan mengalami penurunan performa yang lebih besar. Wasmtime mendukung proposal SIMD, tapi tidak semua target mendukungnya secara seragam.

Terakhir, WASI masih terus berkembang. WASI Preview 1 stabil dan banyak didukung. Preview 2 memperkenalkan antarmuka component model yang lebih bersih tapi belum universal. Jika Anda melakukan ship hari ini, tetaplah di Preview 1.

Alternatif yang patut diketahui

Jika WebAssembly terasa seperti terlalu banyak machinery, ada opsi yang lebih ringan dengan trade-off berbeda.

seccomp-bpf memungkinkan Anda membuat whitelist syscall untuk proses Linux. Ini cepat dan kernel-enforced. Tapi hanya untuk Linux, tidak mencegah memory corruption di dalam syscall yang diizinkan, dan menulis filter seccomp yang benar terkenal rentan terhadap kesalahan.

Landlock adalah fitur Linux yang lebih baru yang membatasi akses filesystem tanpa memerlukan hak istimewa. Ini lebih sederhana daripada seccomp dan menangani masalah “apakah library ini bisa membaca SSH key saya” dengan baik. Tapi tetap hanya untuk Linux dan tidak melakukan sandbox terhadap memori.

Proses terpisah dengan fork atau pool worker memberikan true OS isolation. Segfault di child hanya membunuh child. Kelemahannya adalah overhead serialization dan kompleksitas manajemen proses. Untuk skenario high-throughput, process-per-call tidak scalable.

WebAssembly berada di tengah-tengah. Isolasi lebih kuat daripada seccomp, lebih portabel daripada Landlock, lebih ringan daripada proses terpisah. Pilihan yang tepat bergantung pada apakah bottleneck Anda adalah latensi syscall, memory safety, atau kompleksitas deployment.

Titik awal yang praktis

Anda tidak perlu menulis ulang aplikasi Anda. Pilih satu dependency C yang menangani input yang tidak terpercaya, kompilasi ke WASM, dan bungkus di balik host shim kecil.

# 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

Sintaks --dir=./input::/input memasang direktori ./input host di dalam /input guest. Guest tidak bisa melihat apa pun di filesystem Anda. Jika parser.c mengandung buffer overflow, Anda mendapatkan pesan trap, bukan core dump.

Untuk integrasi host, gunakan Wasmtime Embedding API dalam Rust, C, atau Python. Mulai dari satu exported function. Tambahkan wit-bindgen setelah marshalling manual menjadi tidak memadai. Profil overhead sebelum berkomitmen pada WASM untuk hot path Anda.

FAQ

Bisakah saya melakukan sandbox pada library C++ dengan cara yang sama?

Ya. WASI SDK menyertakan clang++ dan mendukung sebagian besar C++17. Exception dan RTTI berfungsi, meskipun thread-local storage memiliki beberapa keterbatasan. Kompilasi dengan --target=wasm32-wasi dan flag yang sama.

Bagaimana dengan library yang membutuhkan jaringan?

WASI Preview 1 tidak menyertakan socket. Beberapa runtime menawarkan extension non-standar, atau Anda bisa mem-proxy request jaringan melalui host via imported functions. Preview 2 menambahkan socket, tapi dukungannya masih dalam proses roll-out di berbagai runtime.

Seberapa besar ukuran binary WASM yang dikompilasi?

Biasanya 2x hingga 5x ukuran binary native stripped untuk kode C yang sama. Wasmtime dapat mengompilasi dan menyimpan cache kode native pada saat pertama kali dijalankan, sehingga startup overhead adalah biaya one-time per versi binary.

Apakah ini menggantikan container?

Tidak. WASM adalah sandbox tingkat proses. Container adalah isolasi tingkat OS. Gunakan WASM untuk mengisolasi kode yang tidak terpercaya di dalam proses Anda. Gunakan container untuk mengisolasi proses dari seluruh sistem. Keduanya bisa di-stack, dan melayani threat model yang berbeda.

Bisakah guest melarikan diri melalui kerentanan WASI?

Attack surface-nya adalah runtime WASM dan implementasi WASI, bukan kode guest itu sendiri. Wasmtime ditulis dalam Rust, yang menghilangkan banyak bug memory safety di dalam runtime. Spesifikasi WebAssembly juga menjalani formal verification. Tidak ada sandbox yang sempurna, tapi runtime WASM memiliki security track record yang lebih baik daripada kebanyakan library native.

Sebuah segfault dalam C seharusnya adalah kegagalan yang terkontrol, bukan outage produksi. Container adalah jawaban yang tepat ketika Anda membutuhkan isolasi OS penuh. Ketika Anda hanya perlu mencegah satu library menguasai proses Anda, WebAssembly lebih cepat untuk dideploy, lebih mudah untuk di-reason about, dan benar-benar efektif.