How QuickJS-WASM Plugin Sandbox Isolation Works in Instatic
Instatic enforces complete plugin isolation by running each server-side plugin inside a dedicated Bun Worker that hosts a QuickJS-WASM virtual machine with no access to the host runtime, file system, or network unless explicitly granted.
The QuickJS-WASM plugin sandbox isolation in CoreBunch/Instatic creates a secure execution environment where untrusted code operates under strict resource constraints and permission boundaries. Every plugin runs in its own process boundary and JavaScript context, ensuring that crashes, infinite loops, or malicious code cannot compromise the host server or other plugins. This architecture combines WebAssembly-based JavaScript execution with a capability-based security model to provide deterministic, safe server-side extensibility.
Bun Worker Per-Plugin Architecture
Isolation begins at the process level. Instatic spawns a separate Bun.Worker for each plugin instance, ensuring that memory corruption or crashes remain contained within that single worker.
In server/plugins/pluginWorker.ts, the worker never uses import() to load plugin source code directly. Instead, it receives a pre-compiled bundle and evaluates it exclusively within the QuickJS VM. This design guarantees that a fatal error in one plugin cannot trigger a server-wide crash or affect other active plugins.
Fresh QuickJS Context Creation
Each plugin activation instantiates a pristine JavaScript environment. The createQuickJsVm() function in server/plugins/quickjs/vm.ts generates a new QuickJS context with its own global object, heap, and execution stack.
This VM factory applies hard resource limits during initialization:
- Memory limit: 64 MiB heap via
setMemoryLimit() - Stack limit: 1 MiB maximum stack size via
setMaxStackSize() - Execution timeout: 5-second wall-clock deadline (
DEFAULT_EVAL_TIMEOUT_MS)
When plugin code exceeds these boundaries, the VM throws an interrupted exception, which the host logs and handles as a timeout or resource exhaustion event.
Strict Permission Model
The sandbox implements a capability-based security system where privileged APIs are inaccessible by default. Plugin manifests declare requested permissions, but the host validates granted permissions before exposing any SDK surface.
Permission enforcement lives in server/plugins/quickjs/limits.ts. The assertPermission() function checks whether a required capability exists in the plugin's grantedPermissions set before executing sensitive operations like network requests or file system access:
// server/plugins/quickjs/limits.ts
export function assertPermission(
granted: Set<string>,
required: string,
context: string
) {
if (!granted.has(required)) {
throw new Error(
`[${context}] Permission "${required}" not granted to plugin`
);
}
}
Only permissions explicitly listed in the manifest's grantedPermissions array become available through the injected api global object.
Absence of Host API Leakage
QuickJS provides no built-in require, process, or Bun-specific globals. The VM is instantiated with a minimal import object that excludes all native Node.js modules.
The host constructs a sandboxed API object via buildSandboxedApi() and injects it as the sole global variable named api. This thin SDK proxy marshals calls across the worker boundary, ensuring plugin code can never escape the sandbox by accessing native modules or the underlying runtime.
Execution Flow and Lifecycle
The complete isolation mechanism follows a strict activation sequence:
- Activation:
server/plugins/runtime.tsloads the plugin manifest and spawns a dedicatedBun.Worker - Worker boot: The worker initializes in
server/plugins/pluginWorker.tsand callscreateQuickJsVm()to establish the QuickJS context - SDK injection: The host builds a permission-scoped API object based on
grantedPermissionsand sets it as the globalapivariable - Evaluation: The compiled entry point (e.g.,
dist/server.js) executes inside the VM under memory, stack, and time constraints - RPC handling: When plugin code invokes
api.*methods, the host validates permissions inlimits.tsbefore executing the requested operation - Cleanup: Upon plugin disable or worker crash, the VM is explicitly disposed to free emscripten-backed memory
Implementation Examples
Secure Plugin Manifest
Plugins declare capabilities explicitly, while operators control the actual grant:
{
"id": "my-cool-plugin",
"manifestVersion": "1.0",
"entrypoints": {
"server": "dist/server.js"
},
"permissions": ["content.read", "network.outbound"],
"grantedPermissions": ["content.read"]
}
Sandbox Initialization
The worker creates a constrained VM environment:
// server/plugins/pluginWorker.ts
import { createQuickJsVm } from './quickjs/vm';
import { buildSandboxedApi } from '../core/plugin-sdk/apiFactory';
const vm = createQuickJsVm({
memoryLimit: 64 * 1024 * 1024, // 64 MiB
stackSize: 1 * 1024 * 1024, // 1 MiB
evalTimeoutMs: 5000,
});
const sandboxApi = buildSandboxedApi(pluginManifest.grantedPermissions);
vm.setGlobal('api', sandboxApi);
vm.evalFile(bundlePath);
Runtime Permission Enforcement
SDK methods validate capabilities before executing:
// Inside the SDK implementation
export async function fetchExternal(url: string) {
assertPermission(
grantedPermissions,
'network.outbound',
'api.network.fetch'
);
return fetch(url);
}
Timeout Handling
The host catches execution deadline violations:
try {
await pluginVm.runTask(task);
} catch (err) {
if (err.message === 'interrupted') {
recordPluginTimeout(pluginId);
throw new Error('Plugin execution timed out');
}
throw err;
}
Summary
- Process isolation: Each plugin runs in a dedicated
Bun.Workerthat never imports host code directly - VM sandboxing: Fresh QuickJS contexts in
server/plugins/quickjs/vm.tsprovide separate heaps and stacks with 64 MiB memory and 1 MiB stack limits - Capability security: The permission model in
server/plugins/quickjs/limits.tsgrants access only to explicitly approved APIs - Resource constraints: 5-second execution timeouts prevent infinite loops, while explicit VM disposal prevents memory leaks
- Zero host exposure: Plugins access only the injected
apiglobal, with no access torequire,process, or native modules
Frequently Asked Questions
How does Instatic prevent plugins from accessing the file system?
Instatic removes all file system capabilities from the QuickJS runtime by default. The VM instantiation in server/plugins/quickjs/vm.ts provides no native file system bindings, and the buildSandboxedApi() function only injects file-related methods if the grantedPermissions set contains specific storage flags. Without these permissions, any attempt to access files results in a runtime error when assertPermission() fails in server/plugins/quickjs/limits.ts.
What happens when a plugin exceeds the 5-second execution limit?
When plugin code runs longer than the DEFAULT_EVAL_TIMEOUT_MS value (5 seconds), the QuickJS VM interrupts execution and throws an interrupted exception. The worker catches this error, records a timeout status for that plugin instance, and terminates the task. This prevents runaway code from blocking server resources while maintaining isolation between the failed plugin and other active plugins.
Can plugins share memory or communicate with each other directly?
No, plugins cannot share memory or establish direct communication channels. Each plugin operates in its own Bun.Worker process with a separate QuickJS heap. There are no shared global objects or memory spaces between plugin instances. All inter-plugin communication must flow through the host-mediated api object, which enforces permission checks on every operation.
Why does Instatic use QuickJS-WASM instead of Node.js VM modules?
QuickJS-WASM provides deterministic resource limits and complete isolation from the host runtime that Node.js VM modules cannot guarantee. By compiling QuickJS to WebAssembly and running it inside a Bun Worker, Instatic ensures that plugins have zero access to native Node.js APIs unless explicitly proxied through the permission-scoped SDK. This eliminates entire classes of escape vulnerabilities while maintaining deterministic memory and execution boundaries.
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 →