Skip to content

Repository files navigation

Vpod

What is a vpod ?

A vpod is a lightweight, portable sandbox that gives an untrusted process an instant Linux environment. It uses a RISC‑V architecture and runs entirely inside WebAssembly.

  • Fast startup : Boot in under a second.
  • Portable : Runs anywhere without any setup required.
  • Isolated : All execution state stays inside the WASM sandboxes.

How it works

A vpod runs a complete RISC‑V system (RV64GC, single vCPU) compiled to WebAssembly. Inside it boots a real Linux kernel with a real userspace, so shells, tools and daemons all behave like they would on actual hardware.

Snapshots. Instead of booting Linux from scratch, a vpod restores a snapshot: a saved machine state (CPU registers, RAM, filesystem) captured right after boot. Restoring one takes well under a second. Suspend works the same way in reverse, only dirty memory pages are written back to disk, so you can pause a sandbox and resume it later, even from another process.

Ahead-of-time translation. Pure instruction-by-instruction emulation is slow, and WebAssembly rules out a runtime JIT. So at snapshot build time, the hottest guest code paths are translated from RISC‑V into native code that gets compiled into the WASM module itself. At runtime the emulator dispatches into these translated blocks when the guest code matches, and falls back to the interpreter when it doesn't. This is worth roughly 5x on CPU-bound work, with zero effect on isolation: translated code goes through the same MMU and memory checks as interpreted code.

The WASI boundary. The WASM component talks to the host exclusively through WASI 0.2. The guest never sees host file descriptors, sockets, or memory: filesystem access goes through explicitly mounted directories, and networking goes through a user-mode network stack inside the component that only ever asks the host for plain outbound sockets. Everything else (guest kernel, processes, memory) lives inside the WASM linear memory and dies with it.

RV64GC Specification

G (General-purpose extensions)

  • I : Base 64-bit integer instruction set.
  • M : Hardware multiply and divide, useful for hashing and cryptography.
  • A : Atomic operations for thread-safe programs.
  • F/D : Single and double-precision floating-point, suited for scientific computing and ML inference.

C (Compressed instructions) Reduces code size by 30%, improving instruction fetch speed and memory efficiency. This matters when running a full Linux userspace inside our memory-constrained WASM environment.

Note

The V (vector) extension is not implemented. RVV instructions would execute as emulated RISC-V; there is no SIMD passthrough to the host CPU. Adding V would increase emulation overhead without any performance benefit for vectorized workloads.

Getting started

TypeScript SDK

npm install @capsule-run/vpod
import { Sandbox } from "@capsule-run/vpod";

const sandbox = await Sandbox.create();

// State is preserved across calls
await sandbox.commands.run("export API_KEY=secret");
const key = await sandbox.commands.run("echo $API_KEY");
console.log(key.stdout); // secret

// Python REPL — variables persist
await sandbox.code.run("data = [1, 2, 3]");
const total = await sandbox.code.run("print(sum(data))");
console.log(total.text); // 6

await sandbox.close();

The same package runs in a browser tab, where the snapshot is cached in origin-private storage rather than on disk.

Important

The first call to Sandbox.create() downloads the default snapshot (alpine) and caches it locally if not already present.

Python SDK

pip install vpod
from vpod import Sandbox

# Run a command
sandbox = Sandbox.create()
result = sandbox.commands.run("whoami")
print(result.stdout)  # root
sandbox.close()

# Persistent session — state preserved across calls
with Sandbox.create() as sandbox:
    sandbox.commands.run("export API_KEY=secret")
    result = sandbox.commands.run("echo $API_KEY")
    print(result.stdout)  # secret

# Python REPL — variables persist
with Sandbox.create() as sandbox:
    sandbox.code.run("import requests")
    sandbox.code.run("data = [1, 2, 3]")
    result = sandbox.code.run("print(sum(data))")
    print(result.text)  # 6

CLI

curl -fsSL https://install.vpod.sh | sh
Or install via PowerShell (windows)
irm https://install.vpod.sh | iex
# Pull a snapshot
vpod pull alpine:latest

# Start an interactive shell
vpod

Documentation

Visit Vpod documentation.

Limitations

  • Emulation overhead: There is no hardware virtualization inside WebAssembly, so all guest code is emulated. The overhead depends entirely on the workload: I/O-bound and network-bound work runs close to native speed, while heavy CPU-bound work runs noticeably slower even with AOT translation. If your workload is mostly "run a tool, read a file, call an API", you won't notice.
  • No GPU access: CUDA, Metal, and hardware ML accelerators are not available. Support may be added in the future with wasi-nn.

Contributing

Contributions are welcome, from bug reports to new device support. Open an issue to discuss anything substantial before building it.

Prerequisites

  • Rust (latest stable) with the wasm32-wasip2 target: rustup target add wasm32-wasip2
  • Python 3.10+ for the Python SDK
  • Node 20+ for the TypeScript SDK
  • Zig (0.16) and bsdtar, only needed if you build snapshots yourself

Development setup

# One-time: generate the AOT stub (a fresh clone has no translated blocks)
./scripts/aot-stub.sh

# Build the WASM component (library + CLI). Copies both tiers into sdks/python/vpod/
./scripts/build-wasm.sh

# Install the host CLI
cargo install --path crates/vpod

# Install the Python SDK in dev mode
pip install -e "sdks/python[dev]"

# Build the TypeScript SDK. Picks the component up from the Python SDK directory
cd sdks/typescript && npm install && npm run build

npm run build defaults to --tier aot; CI pins --tier base. The build refuses a component older than the newest file under crates/, so rerun ./scripts/build-wasm.sh after touching the emulator. An emulator change only surfaces through the guest, so a stale component compiles and passes almost everything.

Running tests

CI runs these on every PR, so run them before pushing:

cargo fmt --all -- --check                        # formatting
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all                                  # Rust tests

# Python SDK integration tests (needs the WASM library in place)
cp target/wasm32-wasip2/release/vpod_wasi_lib.wasm sdks/python/vpod/
pytest sdks/python/tests/ -v -m integration

# TypeScript SDK (from sdks/typescript)
npm run typecheck
npm test                  # unit
npm run test:all          # unit + integration, needs a local snapshot
npm run test:perf         # guest-time regressions, exact constants

The TypeScript tests import the built dist/, not src/, so build before running them. They look for a snapshot in the shared cache directory; VPOD_TEST_SNAPSHOT=/path/to/x.snap points them somewhere else.

To exercise the browser end to end, npm run dev serves the page with COOP/COEP on and node dev/run-network.mjs --browser chrome drives it headless.

Using a locally built snapshot

The SDKs pull from registry.vpod.sh by default. To run one you built yourself, hand it over directly instead of a registry name:

// TypeScript: a file on disk (Node), or bytes (anywhere)
await Sandbox.create({ snapshot: { path: "./dist/alpine-3.23.0-256mb.snap" } });
await Sandbox.create({ snapshot: { bytes, name: "alpine-3.23.0-256mb.snap" } });
# Python: VPOD_SNAPSHOT=/path/to/x.snap

Keep the RAM size in the file name either way, because the emulator reads it from there.

Building snapshots

The project uses pre-built Alpine snapshots from registry.vpod.sh, so you normally don't need this. To build one locally:

./scripts/build-default-snapshot.sh   # dist/alpine-3.23.0-256mb.snap
./scripts/build-data-snapshot.sh      # 512 MB variant with numpy/pandas/scipy

Important

To use a locally built snapshot, uncomment the lines in resolve_snapshot() in crates/vpod/src/main.rs.

Snapshot builds can also run the AOT pass (scripts/aot-snapshot.sh <snapshot>), which traces a representative workload, translates the hot blocks, and rebuilds the emulator with them baked in. It takes a while; the stub from aot-stub.sh is fine for everyday development, everything works the same, just slower.

Pull requests

  • Keep PRs focused: one change per PR.
  • fmt, clippy and the test suite must pass (CI enforces all three).
  • If you touch the emulator's execution or memory paths, say how you validated correctness (the test suite at minimum; for subtle changes a boot plus a real workload in the guest is a good sanity check).

License

This project is licensed under the Apache License 2.0. See the LICENSE file for details.

About

Lightweight, secure linux sandboxes for untrusted processes.

Topics

Resources

Stars

65 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages