Files
2026-05-27 23:47:53 +01:00

3.2 KiB

VmEngine

(no submodules)

Purpose

VmEngine is the default sandbox engine. It uses node:vm directly: caller code is compiled with new vm.Script(...) and executed via script.runInContext(ctx, { timeout }). The context is created with codeGeneration: { strings: false, wasm: false } so eval and the Function constructor are syntactically unavailable inside the sandbox even if the code-transformer's regex filter fails. Context variables and registered modules are injected as own properties of the context object; the host's intrinsics (Object, Array, etc.) are reachable through the shared realm.

VmEngine is NOT a security boundary against adversarial code. node:vm's context-sharing model means a determined attacker with code-injection can escape via this.constructor.constructor("...") or prototype-chain tricks. For adversarial isolation use worker-engine (OS-thread isolation) or spawn-engine (process isolation).

Features

  • Real node:vm execution — synchronous, deterministic.
  • Frozen intrinsics flag (codeGeneration: { strings: false, wasm: false }).
  • Captured console (log/warn/error) with output-size cap.
  • Module + context-variable injection per execution.
  • V8 heap statistics via v8.getHeapStatistics().
  • OTel recordExecution + recordDuration emission per call.
  • executeSync companion for pool callers that prefer synchronous APIs.

Usage: Basic

import { VmEngine } from "@suaveplan/sandbox/vm-engine";

const engine = new VmEngine({
    timeout: 5000,
    maxOutputSize: 1024,
    memoryLimit: 50 * 1024 * 1024,
});
const r = await engine.execute("1 + 1");
console.log(r.result); // 2
engine.destroy();

Usage: Advanced

Inject a host bridge as a module so user code can interact with the controlled-vocabulary API rather than the host's intrinsics:

import { VmEngine } from "@suaveplan/sandbox/vm-engine";

const engine = new VmEngine({
    timeout: 5000,
    maxOutputSize: 1024,
    memoryLimit: 50 * 1024 * 1024,
    modules: [
        {
            name: "host",
            exports: {
                fetchProduct: (id: string) => loadProduct(id),
            },
        },
    ],
});
const r = await engine.execute('host.fetchProduct("sku-42")');

API Reference

  • new VmEngine(config: SandboxEngineConfig).
  • execute(code, context?, options?): Promise<SandboxResult>.
  • executeSync(code, context?, options?): SandboxResult.
  • registerModule(mod): void.
  • unregisterModule(name): boolean.
  • getHeapStatistics(): { usedHeapSize, heapSizeLimit } | null.
  • reset(): void.
  • destroy(): void.

Implementation Notes

The compiled script wraps the user code in (${code}) so a bare expression (e.g. 1 + 1) returns its value rather than throwing a syntax error from a missing semicolon. An IIFE (function(){...}()) compiles to (function(){...}()) which is also valid. The wrapper is intentionally lightweight; callers who want statement-level execution should use executeFunction on the facade or wrap their code in a function themselves. The breakOnSigint: true flag is set so a SIGINT delivered to the host terminates the running script rather than killing the entire process.