# How QuickJS-WASM Plugin Sandbox Isolation Works in Instatic

> Discover how Instatic's QuickJS-WASM plugin sandbox isolation ensures total security. Each plugin runs in a dedicated Bun Worker VM, isolated from host access.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-28

---

**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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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:

```typescript
// 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:

1. **Activation**: [`server/plugins/runtime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/runtime.ts) loads the plugin manifest and spawns a dedicated `Bun.Worker`
2. **Worker boot**: The worker initializes in [`server/plugins/pluginWorker.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/pluginWorker.ts) and calls `createQuickJsVm()` to establish the QuickJS context
3. **SDK injection**: The host builds a permission-scoped API object based on `grantedPermissions` and sets it as the global `api` variable
4. **Evaluation**: The compiled entry point (e.g., [`dist/server.js`](https://github.com/CoreBunch/Instatic/blob/main/dist/server.js)) executes inside the VM under memory, stack, and time constraints
5. **RPC handling**: When plugin code invokes `api.*` methods, the host validates permissions in [`limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/limits.ts) before executing the requested operation
6. **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:

```json
{
  "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:

```typescript
// 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:

```typescript
// 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:

```typescript
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.Worker` that never imports host code directly
- **VM sandboxing**: Fresh QuickJS contexts in [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts) provide separate heaps and stacks with 64 MiB memory and 1 MiB stack limits
- **Capability security**: The permission model in [`server/plugins/quickjs/limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/limits.ts) grants 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 `api` global, with no access to `require`, `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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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.