How QuickJS-WASM Plugin Sandbox Isolation Works in Instatic
Instatic enforces complete isolation between server-side plugins by running each plugin inside its own QuickJS-WASM sandbox, ensuring no access to the host Node/Bun runtime, file system, or network without explicit permission grants.
The Instatic platform (CoreBunch/Instatic) implements a defense-in-depth architecture where untrusted JavaScript plugin code executes inside a WebAssembly-based sandbox. This QuickJS-WASM plugin sandbox isolation guarantees that malicious or buggy code cannot compromise the host server or interfere with other plugins, while still permitting controlled access to specific capabilities through a strictly permissioned SDK.
Bun Worker Per Plugin
Each plugin spawns in its own Bun.Worker thread, completely separating its execution context from the main server process and other plugins. The worker never uses import() to load the plugin source directly; instead, it receives the pre-compiled bundle and evaluates it inside the QuickJS virtual machine.
This design ensures that a crash or infinite loop in one plugin terminates only that specific worker without affecting the host runtime or neighboring sandboxes. The worker implementation lives in server/plugins/pluginWorker.ts, which handles the lifecycle from boot to disposal.
QuickJS-WASM VM Factory
The sandbox relies on a fresh QuickJS context created for every plugin entry-point via the factory function createQuickJsVm() in server/plugins/quickjs/vm.ts. This instantiation produces an isolated JavaScript environment with its own global object, heap, and execution stack, explicitly preventing any leakage of Node.js or Bun globals like process, require, or fs.
During creation, the VM is hardened with concrete resource boundaries:
- Heap limit: 64 MiB via
setMemoryLimit - Stack limit: 1 MiB via
setMaxStackSize - Execution deadline: 5 seconds (5000 ms) wall-clock time
These constraints prevent runaway memory allocations and deep recursion from exhausting server resources.
Strict Permission Model
Privileged operations are gated through a declarative permission system enforced in server/plugins/quickjs/limits.ts. The plugin manifest declares requested capabilities (e.g., network.outbound, content.read), while the host validates granted permissions before exposing any SDK surface to the VM.
The host constructs a sandboxed API object using buildSandboxedApi() based solely on the grantedPermissions array, then injects it into the VM as a global named api. When plugin code invokes api.fetch() or similar methods, the call marshals across the worker boundary, where assertPermission() validates the action against the granted set before execution.
{
"id": "secure-plugin",
"manifestVersion": "1.0",
"entrypoints": {
"server": "dist/server.js"
},
"permissions": ["content.read", "network.outbound"],
"grantedPermissions": ["content.read"]
}
Execution Flow and RPC Isolation
The complete lifecycle from activation to runtime follows a hardened path that maintains QuickJS-WASM plugin sandbox isolation at every stage:
- Activation:
server/plugins/runtime.tsreads the plugin manifest and spawns a dedicatedBun.Worker. - Worker Boot:
server/plugins/pluginWorker.tsimports the VM factory and callscreateQuickJsVm()with the 64 MiB heap, 1 MiB stack, and 5-second timeout configuration. - SDK Injection: The host builds the restricted API object and calls
vm.setGlobal('api', sandboxApi), exposing only permitted capabilities. - Evaluation: The compiled entry point executes inside the VM under the enforced limits.
- RPC Bridging: Plugin interactions with the host occur through the injected
apiobject, with all calls crossing the worker boundary and undergoing permission validation inlimits.ts. - Cleanup: On disable or crash, the VM is destroyed and its memory freed, preventing leaks in the Emscripten-backed runtime.
Resource Exhaustion Protection
Beyond memory isolation, the sandbox enforces temporal limits to prevent denial-of-service via long-running computations. The DEFAULT_EVAL_TIMEOUT_MS constant sets a hard 5-second wall-clock deadline on plugin execution. When exceeded, QuickJS throws an interrupted exception, which the host records as a timeout status and handles gracefully without crashing the worker.
try {
await pluginVm.runTask(task);
} catch (err) {
if (err.message === 'interrupted') {
recordPluginTimeout(pluginId);
throw new Error('Plugin execution timed out');
}
throw err;
}
Summary
- Worker Isolation: Each plugin runs in a dedicated
Bun.Workerthat hosts a separate QuickJS-WASM VM, preventing cross-plugin contamination. - Resource Constraints: Hard limits of 64 MiB heap, 1 MiB stack, and 5-second execution timeouts enforce deterministic resource usage.
- Permission Gates: All host capabilities require explicit grants in the plugin manifest, enforced at runtime by
assertPermission()inserver/plugins/quickjs/limits.ts. - Zero Host Exposure: The VM instantiation in
server/plugins/quickjs/vm.tsexcludes native modules and globals, presenting only the injectedapisurface. - Clean Termination: Explicit disposal of the VM on plugin shutdown prevents memory leaks in the WebAssembly runtime.
Frequently Asked Questions
How does the QuickJS-WASM sandbox prevent plugins from accessing the file system?
The sandbox removes all native host APIs from the JavaScript environment. The VM created in server/plugins/quickjs/vm.ts has no access to require, process, or Bun-specific globals. File system access is only possible if the plugin manifest includes grantedPermissions containing the specific file-system flag, which causes the host to expose a restricted api.files method that validates paths against an allowlist.
What happens when a plugin exceeds the 5-second execution limit?
When plugin execution exceeds the DEFAULT_EVAL_TIMEOUT_MS of 5000 milliseconds, the QuickJS engine throws an interrupted exception. This error propagates to the host in server/plugins/pluginWorker.ts, which records a timeout status for that plugin and terminates the current task without crashing the worker or affecting other sandboxes.
How are permissions granted to an Instatic plugin?
Permissions are granted through the grantedPermissions array in the plugin manifest JSON. During initialization, server/plugins/pluginWorker.ts passes these grants to buildSandboxedApi(), which constructs a tailored SDK containing only the approved capabilities. The assertPermission() function in server/plugins/quickjs/limits.ts validates every host-bound RPC call against this set before execution.
Can a plugin crash affect other plugins or the host server?
No. Because each plugin executes inside its own Bun.Worker with a dedicated QuickJS-WASM VM instance, segmentation faults, infinite loops, or unhandled exceptions remain confined to that specific worker. The host process and other plugin workers continue operating normally, as the sandbox design guarantees complete process isolation at both the worker and VM levels.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →