ri-sandbox
v1.0.1
Published
Deterministic WASM execution with resource limits, isolation, and snapshot/restore
Maintainers
Readme
ri-sandbox
Deterministic WASM execution with resource limits, isolation, and snapshot/restore.
Status
v1.0.0 — Production Ready
| Milestone | Status | |-----------|--------| | M1: Project Scaffolding | Complete | | M2: Core Types & Configuration | Complete | | M3: WASM Module Loading & Instantiation | Complete | | M4: Execution Engine & Host Function Bridge | Complete | | M5: Gas Metering & Resource Limits | Complete | | M6: Determinism Enforcement | Complete | | M7: Snapshot & Restore | Complete | | M8: Memory Pressure System | Complete | | M9: Integration Tests & Performance Validation | Complete |
See ROADMAP.md for the full development plan.
Overview
ri-sandbox is a standalone TypeScript library that provides:
- Deterministic execution — identical inputs produce identical outputs, always
- Resource limits — memory caps, gas metering, wall-clock timeouts
- Complete isolation — no host memory access, no ambient authority
- Host function injection — controlled WASM-to-host bridge
- Gas metering enforcement — computation budget per execution
- Determinism enforcement — seeded PRNG, injected time, import isolation
- Snapshot/restore — serialize and resume execution state
- Memory pressure monitoring — tiered alerts with actionable recommendations
Install
npm install ri-sandboxQuick Start
import { createWasmSandbox } from 'ri-sandbox';
// 1. Create a sandbox factory
const sandbox = createWasmSandbox();
// 2. Create an instance with resource limits
const instance = sandbox.create({
maxMemoryBytes: 1_048_576, // 1 MB
maxGas: 500_000,
maxExecutionMs: 30,
hostFunctions: {},
deterministicSeed: 42,
eventTimestamp: 1700000000000,
});
// instance.id → "sandbox-0", instance.status → "created"
// 3. Load a WASM module
const wasmBytes = new Uint8Array(/* ... your .wasm file ... */);
await sandbox.load(instance, wasmBytes);
// instance status is now "loaded"
// 4. Execute a WASM function
const result = sandbox.execute(instance, 'add', [3, 7]);
if (result.ok) {
console.log(result.value); // 10
console.log(result.metrics);
console.log(result.gasUsed); // gas consumed during execution
console.log(result.durationMs); // wall-clock time in ms
}
// 5. Check resource metrics
const metrics = sandbox.getMetrics(instance);
// metrics.memoryUsedBytes, metrics.memoryLimitBytes, ...
// 6. Clean up
sandbox.destroy(instance);
// instance status is now "destroyed"API
Resource Enforcement
The sandbox enforces three resource limits during execution:
| Resource | Config | Error Code | Enforcement |
|----------|--------|------------|-------------|
| Gas (computation) | maxGas | GAS_EXHAUSTED | Consumed at host function call boundaries |
| Memory | maxMemoryBytes | MEMORY_EXCEEDED | Checked after execution; WebAssembly.Memory maximum enforces hard limit |
| Timeout | maxExecutionMs | TIMEOUT | Checked at host function call boundaries via injectable timer |
const result = sandbox.execute(instance, 'expensive', payload);
if (!result.ok) {
switch (result.error.code) {
case 'GAS_EXHAUSTED':
console.log(`Gas: ${result.error.gasUsed}/${result.error.gasLimit}`);
break;
case 'TIMEOUT':
console.log(`Timeout: ${result.error.elapsedMs}ms > ${result.error.limitMs}ms`);
break;
case 'MEMORY_EXCEEDED':
console.log(`Memory: ${result.error.memoryUsed}/${result.error.memoryLimit} bytes`);
break;
}
}Determinism Enforcement
The sandbox eliminates all sources of non-determinism at load time and execution time:
| Mechanism | Module | Description |
|-----------|--------|-------------|
| Time injection | time-injection.ts | __get_time() host function returns config.eventTimestamp — never reads the system clock |
| Seeded PRNG | random-injection.ts | __get_random() host function returns values from a Mulberry32 PRNG seeded with config.deterministicSeed |
| Import isolation | isolation.ts | Rejects WASM modules that import WASI, undeclared functions, or non-env namespaces |
| Double-execution check | determinism-validator.ts | Runs a function twice with state capture/restore and compares results byte-for-byte |
The __get_time and __get_random host functions are automatically injected into every sandbox instance. WASM modules can import them from the env namespace:
(import "env" "__get_time" (func $get_time (result i32)))
(import "env" "__get_random" (func $get_random (result i32)))Import validation runs during sandbox.load() — modules with disallowed imports are rejected before instantiation with an INVALID_MODULE error.
Snapshot & Restore
Sandbox state (WASM linear memory, PRNG position, gas counter) can be serialized to a binary snapshot and restored later:
// Capture state
const snap = sandbox.snapshot(instance);
// Execute more actions...
sandbox.execute(instance, 'processEvent', payload);
// Rollback to captured state
sandbox.restore(instance, snap);
// Execution after restore is identical to execution after snapshot
const result = sandbox.execute(instance, 'processEvent', payload);Snapshot binary format: 5-byte header (WSNP magic + version), memory section (uint32 length + raw bytes), state section (uint32 length + JSON). Invalid or corrupted snapshots are rejected with SNAPSHOT_ERROR.
Memory Pressure
Monitor system-wide memory usage across sandbox instances and get actionable recommendations:
import { getMemoryPressure, advise } from 'ri-sandbox';
// Compute pressure level — caller provides the memory budget
const level = getMemoryPressure(instances, availableBytes);
// level: 'NORMAL' | 'WARNING' | 'PRESSURE' | 'CRITICAL' | 'OOM'
// Get actionable recommendation
const rec = advise(level, instances, foregroundInstanceId);
switch (rec.action) {
case 'none': break;
case 'log': console.warn(rec.message); break;
case 'suspend': rec.instanceIds.forEach(id => /* suspend */); break;
case 'emergency_save': rec.instanceIds.forEach(id => /* snapshot + suspend */); break;
}| Level | Threshold | Recommendation | |-------|-----------|----------------| | NORMAL | < 70% | No action | | WARNING | 70–85% | Log a warning | | PRESSURE | 85–95% | Suspend non-foreground instances | | CRITICAL | ≥ 95% | Emergency save all non-foreground | | OOM | ≥ 100% | Emergency save (should never happen) |
createWasmSandbox(): WasmSandbox
Factory function — creates a new WasmSandbox with its own instance registry.
import { createWasmSandbox } from 'ri-sandbox';
const sandbox = createWasmSandbox();WasmSandbox
The main sandbox interface — all 7 methods for WASM execution lifecycle.
interface WasmSandbox {
create(config: SandboxConfig): SandboxInstance;
load(instance: SandboxInstance, module: Uint8Array): Promise<void>;
execute(instance: SandboxInstance, action: string, payload: unknown): ExecutionResult;
destroy(instance: SandboxInstance): void;
snapshot(instance: SandboxInstance): Uint8Array;
restore(instance: SandboxInstance, snapshot: Uint8Array): void;
getMetrics(instance: SandboxInstance): ResourceMetrics;
}SandboxConfig
Configuration for creating a sandbox instance.
interface SandboxConfig {
readonly maxMemoryBytes: number; // Hard memory limit (default: 16,777,216 — 16 MB)
readonly maxGas: number; // Computation budget per execution (default: 1,000,000)
readonly maxExecutionMs: number; // Wall-clock timeout (default: 50ms)
readonly hostFunctions: HostFunctionMap; // Injected bridge functions (default: {})
readonly deterministicSeed: number; // PRNG seed (default: 0)
readonly eventTimestamp: number; // Injected "current time" (ms since epoch)
}Default constants:
| Constant | Value |
|----------|-------|
| DEFAULT_MAX_MEMORY_BYTES | 16,777,216 (16 MB) |
| DEFAULT_MAX_GAS | 1,000,000 |
| DEFAULT_MAX_EXECUTION_MS | 50 |
| DEFAULT_DETERMINISTIC_SEED | 0 |
SandboxInstance
An isolated WASM execution environment.
interface SandboxInstance {
readonly id: string;
readonly config: Readonly<SandboxConfig>;
readonly status: SandboxStatus;
readonly metrics: ResourceMetrics;
}
type SandboxStatus = 'created' | 'loaded' | 'running' | 'suspended' | 'destroyed';ExecutionResult
Discriminated union returned by execute().
// Success
{ ok: true; value: unknown; metrics: ResourceMetrics; gasUsed: number; durationMs: number }
// Failure
{ ok: false; error: SandboxError }ResourceMetrics
Current resource usage for a sandbox instance.
interface ResourceMetrics {
readonly memoryUsedBytes: number;
readonly memoryLimitBytes: number;
readonly gasUsed: number;
readonly gasLimit: number;
readonly executionMs: number;
readonly executionLimitMs: number;
}HostFunction & HostFunctionMap
Host functions injected from the caller into WASM.
interface HostFunction {
readonly name: string;
readonly params: readonly WasmValueType[];
readonly results: readonly WasmValueType[];
readonly handler: (...args: readonly number[]) => number | undefined;
}
type HostFunctionMap = Readonly<Record<string, HostFunction>>;
type WasmValueType = 'i32' | 'i64' | 'f32' | 'f64';SandboxError
Discriminated union with 8 error codes:
| Code | Fields |
|------|--------|
| GAS_EXHAUSTED | gasUsed, gasLimit |
| MEMORY_EXCEEDED | memoryUsed, memoryLimit |
| TIMEOUT | elapsedMs, limitMs |
| WASM_TRAP | trapKind, message |
| INVALID_MODULE | reason |
| HOST_FUNCTION_ERROR | functionName, message |
| INSTANCE_DESTROYED | instanceId |
| SNAPSHOT_ERROR | reason |
Error factory functions: gasExhausted(), memoryExceeded(), timeout(), wasmTrap(), invalidModule(), hostFunctionError(), instanceDestroyed(), snapshotError().
MemoryPressureLevel
System-wide memory pressure level (percentage of available memory).
type MemoryPressureLevel = 'NORMAL' | 'WARNING' | 'PRESSURE' | 'CRITICAL' | 'OOM';| Level | Threshold | |-------|-----------| | NORMAL | < 70% | | WARNING | 70–85% | | PRESSURE | 85–95% | | CRITICAL | ≥ 95% | | OOM | ≥ 100% |
Result<T, E>
Generic discriminated union for fallible operations.
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };Development
npm install # Install dependencies
npm run typecheck # Type check
npm run test # Run tests
npm run test:watch # Run tests in watch mode
npm run test:coverage # Run tests with coverage
npm run lint # Lint
npm run format # Format
npm run build # BuildDeterminism Guarantees
| Non-Determinism Source | Prevention |
|------------------------|------------|
| System clock | Injected eventTimestamp |
| Random numbers | Deterministic PRNG with injected seed |
| Memory layout | WASM linear memory is deterministic |
| Floating point | WASM IEEE 754 compliance |
| Thread scheduling | Single-threaded (no concurrency) |
| GC | WASM has manual memory (no GC) |
| Browser differences | WASM spec guarantees identical behavior |
License
MIT
Architecture
The sandbox is structured as a pipeline:
Caller
│
├─ create(config) ─────→ Instance Factory → WebAssembly.Memory + PRNG
├─ load(instance, wasm) ─→ Module Loader → Import Validator → Instantiator
├─ execute(instance, ..) → Executor → Gas Meter + Timeout + Memory Check
├─ snapshot(instance) ──→ Serializer → Binary (WSNP header + memory + state)
├─ restore(instance, ..) → Deserializer → Memory + PRNG + gas restoration
└─ destroy(instance) ───→ Resource cleanupKey internal modules:
| Module | Path | Responsibility |
|--------|------|----------------|
| Instance Factory | src/loader/instance-factory.ts | Creates sandbox instances with WASM memory and PRNG |
| Module Loader | src/loader/module-loader.ts | Validates and compiles WASM bytes |
| Import Validator | src/determinism/isolation.ts | Rejects WASI, undeclared imports, non-env namespaces |
| Instantiator | src/loader/instantiator.ts | Wires host functions, memory, time, and random injection |
| Executor | src/execution/executor.ts | Executes WASM functions with resource enforcement |
| Gas Meter | src/resources/gas-meter.ts | Tracks computation budget at host function boundaries |
| Timeout Checker | src/resources/timeout.ts | Wall-clock timeout with injectable timer |
| Memory Limiter | src/resources/memory-limiter.ts | Post-execution memory usage check |
| Serializer | src/snapshot/serializer.ts | Binary snapshot creation (WSNP format) |
| Deserializer | src/snapshot/deserializer.ts | Binary snapshot restoration with full validation |
| Pressure | src/pressure/memory-pressure.ts | System-wide memory pressure computation |
| Advisor | src/pressure/pressure-advisor.ts | Actionable pressure recommendations |
Performance Characteristics
| Operation | Target | Measured | |-----------|--------|----------| | Create sandbox instance | < 5ms | < 1ms | | Load WASM module | < 50ms | < 5ms | | Execute simple function (add) | < 1ms | < 0.1ms | | Execute fibonacci(20) with gas metering | < 50ms | < 5ms | | Snapshot | < 10ms | < 1ms | | Restore | < 10ms | < 1ms | | Create + load + execute end-to-end | < 100ms | < 10ms | | 10 concurrent instances | all functional | < 10ms total |
Test WASM Modules
The library includes hand-crafted WASM binaries in src/loader/__tests__/wasm-fixtures.ts for testing:
| Module | Exports | Purpose |
|--------|---------|----------|
| addWasmModule() | add(i32, i32): i32 | Pure computation |
| counterWasmModule() | increment(): void, get(): i32 | Stateful execution |
| fibonacciWasmModule() | fib(i32): i32 | Gas metering (calls __get_time each iteration) |
| memoryHogWasmModule() | allocate(i32): void, getMemSize(): i32 | Memory growth testing |
| infiniteLoopWasmModule() | loop(): void | Gas/timeout testing (never returns) |
| hostCallWasmModule() | callDouble(i32): i32 | Single host function call |
| multiHostCallWasmModule() | callBoth(i32): i32 | Multiple host function calls |
| randomImportWasmModule() | getRandom(): i32 | Deterministic PRNG testing |
| timeImportWasmModule() | getTime(): i32 | Deterministic time testing |
| memoryImportWasmModule() | getMemSize(): i32 | Memory import testing |
All modules are hand-crafted byte arrays — no WAT toolchain required.
Contributing
- Clone the repo and install dependencies:
git clone https://github.com/Robust-infrastructure/ri-sandbox.git
cd ri-sandbox
npm install- Run the development workflow:
npm run typecheck # Must pass with zero errors
npm run lint # Must pass with zero warnings
npm run test # All tests must pass
npm run test:coverage # Coverage ≥ 90% lines, 85% branches, 90% functions
npm run build # Must produce clean ESM + CJS + types- Follow the commit conventions in
.github/instructions/git-workflow.instructions.md. - Development follows the ROADMAP — complete milestones in order.
