Одиночный null pointer dereference внутри C-библиотеки может уронить всё приложение. Если эта библиотека парсит пользовательский ввод, декомпрессирует изображения или обрабатывает сетевые протоколы, вы находитесь в одном malformed packet от крэша. Контейнеры решают эту проблему, но запускать Docker ради одной зависимости — всё равно что нанимать охранника для комнатного растения. Нужна изоляция без оверхеда.
WebAssembly плюс WASI — самый практичный ответ. Скомпилируйте библиотеку в WASM, запустите её внутри sandboxed runtime, и host-процесс переживёт даже segfault guest. Память проверяется на bounds. Доступ к файловой системе осуществляется через capabilities. А sandbox — это library call, не system call.
Что на самом деле означает sandboxing C-библиотеки
В C нет memory safety. Buffer overflow в зависимости может повредить stack, захватить выполнение или уронить host. Традиционные митигации вроде ASAN находят баги во время runtime, но добавляют оверхед и не изолируют сбой от остальной части процесса. seccomp и Landlock могут ограничивать syscalls, но они только для Linux, требуют root для некоторых операций и не останавливают memory corruption внутри разрешённых syscalls.
WebAssembly идёт другим путём. Он компилируется в виртуальный instruction set с явными memory bounds, структурированным control flow и отсутствием undefined behavior для out-of-bounds доступа. WASM-runtime вроде Wasmtime или WAMR валидирует bytecode перед выполнением. Если guest трогает память, которая ему не принадлежит, runtime выбрасывает trap. Host продолжает работать.
Это не эмуляция. Современные WASM-runtimes компилируют в нативный machine code через Cranelift или LLVM. Проигрыш в производительности обычно составляет от 1,1x до 2x для compute-bound кода. Для I/O-bound библиотек разница часто теряется в шуме.
Как WASI превращает доступ к файловой системе в capability
WASI — это WebAssembly System Interface. Он даёт sandboxed модулям API в духе POSIX, но каждый ресурс — это capability. Нет глобальной файловой системы. Если вы хотите, чтобы guest читал ./data/input.txt, вы явно preopen этот каталог и передаёте file descriptor.
Вот минимальная C-библиотека, которая читает файл и возвращает 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;
}
Скомпилируйте её в WebAssembly с помощью 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
Получившийся .wasm-файл — это sandboxed модуль. Он не может открыть ни один файл, если runtime явно не предоставит доступ. Он не может аллоцировать память за пределами своей linear memory region. Он не может выполнять произвольные shell-команды, потому что WASI не определяет execve.
Запуск sandboxed библиотеки из Rust
Wasmtime — production-ready WASM-runtime, написанный на Rust с биндингами для C и Python. Вот host-программа, которая загружает скомпилированную C-библиотеку, даёт ей read-only доступ к ./data/ и вызывает 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(())
}
Guest-код на C выполняется внутри собственной 32-битной linear memory. Вызов memory.write копирует строку пути в это sandboxed address space. Если checksum_file попытается прочитать за пределы своей памяти, Wasmtime выбросит trap. Если попытается открыть /etc/passwd, WASI вернёт ENOENT, потому что этот path не был preopened.
Где модель памяти реально защищает вас
WASM-модули используют единую непрерывную linear memory, обычно начиная с 64KB и растущую по требованию. Каждый доступ к памяти проверяется runtime на bounds. Buffer overflow в C-библиотеке не может повредить host-stack или heap. Нельзя перепрыгнуть на произвольный код. Можно трогать только память внутри собственной выделенной области.
Это слабее полной изоляции процессов. Guest и host делят один OS-процесс. CPU-level side-channel-атака или speculative execution баг теоретически могут утечь данные через границу. Но для threat model «у этой C-библиотеки есть баг и она может упасть» WASM-изоляция подлинна и практична.
Поведение trap-on-fault — ключевое преимущество. Если guest разыменовывает null pointer или переполняет буфер, Wasmtime ловит это и возвращает ошибку. Ваш host-процесс не падает с segfault. Ваш веб-сервер не перезапускается. Вы логируете ошибку и продолжаете.
Компромиссы, которые замедлят вас
WASM — не прозрачный. C-код, использующий threads, signals, setjmp/longjmp или raw mmap, не скомпилируется под WASI без переписывания. WASI SDK поддерживает pthreads через опциональный флаг, но это всё ещё экспериментально и требует, чтобы host-runtime его активировала.
FFI-boundary ручная. Вы не можете просто слинковать .wasm-файл в C- или Rust-проект и вызывать его функции как обычную библиотеку. Вы маршаллите аргументы в linear memory, вызываете функцию по индексу и читаете результаты обратно. Тулинг вроде wit-bindgen генерирует glue code из определений интерфейсов, но вы всё равно платите complexity tax.
Производительность варьируется. Integer-heavy код компилируется в WASM с near-native скоростью. Код, который часто делает host calls, интенсивно аллоцирует или зависит от SIMD, увидит большие slowdowns. Wasmtime поддерживает SIMD proposals, но не каждый target поддерживает их равномерно.
Наконец, WASI всё ещё эволюционирует. WASI Preview 1 стабилен и широко поддержан. Preview 2 вводит component model interfaces, которые чище, но ещё не универсальны. Если вы ship сегодня — оставайтесь на Preview 1.
Альтернативы, о которых стоит знать
Если WebAssembly кажется слишком сложным, есть более лёгкие варианты с другими компромиссами.
seccomp-bpf позволяет вносить syscalls в whitelist для Linux-процесса. Быстро и kernel-enforced. Но только для Linux, не предотвращает memory corruption внутри разрешённых syscalls, и написать корректный seccomp-filter — задача печально известная своей ошибкоёмкостью.
Landlock — более новая возможность Linux, ограничивающая доступ к файловой системе без привилегий. Проще seccomp и хорошо решает проблему «может ли эта библиотека читать мои SSH-ключи». Но всё ещё только для Linux и не sandboxит память.
Отдельные процессы через fork или worker pool дают настоящую OS-изоляцию. Segfault в child убивает только child. Минус — overhead сериализации и сложность управления процессами. Для high-throughput сценариев process-per-call не масштабируется.
WebAssembly находится посередине. Более сильная изоляция, чем seccomp, более портабельный, чем Landlock, легче отдельных процессов. Правильный выбор зависит от того, является ли ваш bottleneck latency syscalls, memory safety или сложность deployment.
Практическая отправная точка
Вам не нужно переписывать приложение. Выберите одну C-зависимость, обрабатывающую untrusted input, скомпилируйте её в WASM и оберните за небольшим 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
Синтаксис --dir=./input::/input монтирует host-каталог ./input в /input внутри guest. Guest не видит ничего другого в вашей файловой системе. Если в parser.c есть buffer overflow, вы получите trap-сообщение, а не core dump.
Для host-интеграции используйте Wasmtime Embedding API на Rust, C или Python. Начните с одной exported function. Добавьте wit-bindgen, когда hand-written marshaling станет недостаточным. Профилируйте overhead, прежде чем закреплять WASM на вашем hot path.
FAQ
Можно ли таким же образом sandbox C++-библиотеку?
Да. WASI SDK включает clang++ и поддерживает большую часть C++17. Exceptions и RTTI работают, хотя thread-local storage имеет некоторые ограничения. Компилируйте с --target=wasm32-wasi и теми же флагами.
А что с библиотеками, которым нужна сеть?
WASI Preview 1 не включает sockets. Некоторые runtimes предлагают nonstandard extensions, или вы можете проксировать сетевые запросы через host через imported functions. Preview 2 добавляет sockets, но поддержка ещё раскатывается по runtimes.
Каков размер скомпилированного WASM-бинарника?
Обычно от 2x до 5x размера stripped native binary для того же кода на C. Wasmtime может компилировать и кешировать нативный код при первом запуске, так что startup overhead — это one-time cost на версию бинарника.
Заменяет ли это контейнеры?
Нет. WASM — это sandbox на уровне процесса. Контейнеры — это изоляция на уровне ОС. Используйте WASM для изоляции untrusted кода внутри вашего процесса. Используйте контейнеры для изоляции процесса от остальной системы. Они stack, и обслуживают разные threat models.
Может ли guest сбежать через уязвимость WASI?
Attack surface — это WASM-runtime и WASI-implementation, а не сам guest-код. Wasmtime написан на Rust, что устраняет множество memory safety багов в runtime. Спецификация WebAssembly также проходит formal verification. Ни один sandbox не идеален, но WASM-runtimes имеют лучший security track record, чем большинство нативных библиотек.
Segfault на C должен быть contained failure, а не production outage. Контейнеры — правильный ответ, когда нужна полная OS-изоляция. Когда нужно лишь не дать одной библиотеке захватить ваш процесс, WebAssembly быстрее деплоить, проще reason about и подлинно эффективен.