# How the QuickJS-WASM Plugin Sandbox Isolates Code in Instatic

> Discover how the QuickJS-WASM plugin sandbox in Instatic isolates code using a three-layer architecture for enhanced security and resource control. Learn about Bun.Worker, WebAssembly, and host-call bridging.

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

---

**The QuickJS-WASM plugin sandbox in Instatic uses a three-layer isolation architecture: Bun.Worker threads for crash containment, QuickJS running inside WebAssembly for security boundaries, and strict resource limits with a controlled host-call bridge to prevent unauthorized system access.**

Instatic's server-side plugin system is built on a sophisticated isolation model that prevents untrusted code from compromising the host server. According to the CoreBunch/Instatic source code, every plugin executes inside a **QuickJS-WASM sandbox** that combines multiple defensive layers. This article breaks down exactly how that isolation works, with direct references to the implementation files.

---

## Three-Layer Architecture Overview

The sandbox stack creates defense in depth through progressive isolation:

```

┌─ Bun host (main process)
│  ┌─ Bun.Worker (crash-isolation, CPU yielding)
│  │  ┌─ QuickJS-WASM context (security sandbox)
│  │  │  ┌─ Bootstrap (SDK façade, handler registries, polyfills)
│  │  │  └─ Plugin source (IIFE → globalThis.__plugin_exports)
│  │  └─ Host functions: __hostCall, __hostSleep, __log
│  └─ protocol / wire format
└─ host / API-call dispatch

```

Each layer addresses a specific threat model: process-level crashes, system-level access, and resource exhaustion.

---

## Layer 1: Bun.Worker Thread Isolation

In [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts), each plugin spawns in a separate **Bun.Worker**. This provides two critical protections:

- **Crash containment** — A plugin that segfaults or hits an unrecoverable error terminates only its own worker, not the main server process
- **Event-loop separation** — Timers and asynchronous operations run on the worker's isolated event loop, preventing the main process from being blocked by slow or malicious plugin code

The worker pattern ensures that **no single plugin can bring down the entire Instatic instance**.

---

## Layer 2: QuickJS-WASM Security Boundary

Inside each worker, the actual JavaScript execution happens in **QuickJS compiled to WebAssembly**. This is the core of the security model implemented in [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts):

- **No filesystem access** — QuickJS-WASM has no native bindings to the host filesystem
- **No network sockets** — Direct network I/O is impossible from within the WASM boundary
- **No Node/Bun globals** — Standard library functions like `process.exit()` or `Deno.readFile()` simply do not exist

The VM is created fresh for each plugin with a minimal, audited set of host functions injected explicitly:

```ts
// From server/plugins/quickjs/vm.ts — host function registration
const vm = await createPluginVm({
  pluginSource: compiledPluginIIFE,
  env: {
    pluginId: 'my-plugin',
    grantedPermissions: ['cms.routes.register'],
    hostCall: async (target, args) => {
      // Host implements permission checks here
      if (target === 'cms.routes.register') { /* ... */ }
      throw new Error('Unauthorized host call');
    },
    // Only explicit capabilities are forwarded
    log: (msg) => console.info('[my-plugin]', ...msg),
  },
});

```

---

## Layer 3: Bootstrap Environment and SDK Facade

Before any plugin code executes, the sandbox evaluates a **bootstrap bundle** from [`server/plugins/quickjs/bootstrap/generated/pluginBootstrap.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/bootstrap/generated/pluginBootstrap.ts). This serves as the controlled runtime environment:

- **`__plugin_handlers`** — Global registry where plugins register lifecycle hooks
- **Dispatcher functions** — `__runLifecycle`, `__runRoute`, and others that the host calls to invoke plugin code safely
- **Read-only metadata** — `__plugin_meta` exposes manifest data that plugins cannot modify
- **Mutable settings** — `__plugin_settings` provides user configuration with type-safe access

The bootstrap exposes only three host-capability polyfills:

| Polyfill | Purpose | Implementation Location |
|----------|---------|------------------------|
| `__hostCall` | Controlled API access to host | [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts) |
| `__hostSleep` | Timer functionality without native `setTimeout` | Bootstrap + worker tracking |
| `__log` | Structured logging to host | Injected host function |

Plugin source code is an **IIFE that assigns to `globalThis.__plugin_exports`**:

```ts
// Inside the sandbox (bootstrap handles registration)
globalThis.__plugin_exports = {
  async activate() {
    // Plugin lifecycle hook
  },
  async myRoute(req) {
    // Route handler
  }
};

```

The bootstrap discovers these exports via `__detectExportedHooks` and validates them before the host can invoke them.

---

## Strict Resource Limits in server/plugins/quickjs/limits.ts

The sandbox enforces **three hard caps** defined in [`server/plugins/quickjs/limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/limits.ts):

### Memory Limit

```ts
setMemoryLimit(64 * 1024 * 1024); // 64 MiB per VM

```

Exceeding this throws `OutOfMemory` inside the VM, caught and reported by the host.

### Stack Limit

```ts
setMaxStackSize(1024 * 1024); // 1 MiB

```

Prevents infinite recursion from exhausting the WASM stack and crashing the worker.

### Wall-Clock Deadline

From [`server/plugins/quickjs/eval.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/eval.ts), every evaluation uses `withSyncDeadline`:

```ts
const DEFAULT_EVAL_TIMEOUT_MS = 5000; // 5 seconds

// All plugin code entry points are wrapped
await withSyncDeadline(vm, () => {
  return vm.runRoute('myRoute', context);
}, DEFAULT_EVAL_TIMEOUT_MS);

```

If a plugin loops indefinitely, the VM is **aborted and the job logged**.

---

## The Host-Call Bridge: Controlled Escape Hatch

The only way plugin code reaches the host is through **`__hostCall`**, implemented in [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts). This synchronous function creates a carefully controlled async pattern:

1. Plugin calls `__hostCall(target, args)` synchronously
2. VM-side creates a `Promise` and returns its handle immediately
3. Host receives the call, executes the requested operation
4. Host resolves the promise from outside
5. `runtime.executePendingJobs()` drains the micro-task queue

This design **prevents plugins from calling arbitrary host APIs** — the host implementation validates every `target` against the plugin's `grantedPermissions` before executing.

---

## Timer Isolation and Cleanup

Timer functionality uses `__hostSleep` with full tracking in [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts):

- All `setTimeout`/`setInterval` polyfills are tracked in `pendingTimers`
- On `dispose()`, timers are cleared before VM teardown
- Prevents "fire-into-dead-context" crashes where a timer callback runs after the VM is destroyed

## Complete Cleanup on Plugin Unload

The `dispose()` method in [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts) implements deterministic cleanup:

```ts
vm.dispose(); // When plugin is removed or reloaded

```

This sequence:
1. Marks VM as disposed to short-circuit pending host calls
2. Clears `pendingTimers` and disposes `Deferred` promises
3. Releases all host-function and dispatcher handles
4. Finally disposes the QuickJS context via `context.dispose()`

---

## Summary

- **Bun.Worker** provides process-level crash isolation and event-loop separation
- **QuickJS-WASM** eliminates direct system access through the WebAssembly security boundary
- **Bootstrap bundle** in [`server/plugins/quickjs/bootstrap/generated/pluginBootstrap.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/bootstrap/generated/pluginBootstrap.ts) creates a controlled runtime with explicit capability grants
- **Resource limits** in [`server/plugins/quickjs/limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/limits.ts) cap memory (64 MiB), stack (1 MiB), and execution time (5s default)
- **`__hostCall` bridge** is the sole escape hatch, with synchronous invocation and async resolution preventing arbitrary API access
- **Comprehensive cleanup** in `dispose()` eliminates resource leaks and timer-related crashes

Together, these mechanisms ensure that **QuickJS-WASM plugin sandbox isolation in Instatic** is robust against crashes, resource exhaustion, and security escapes.

---

## Frequently Asked Questions

### How does Instatic prevent a plugin from crashing the main server?

Instatic spawns each plugin in a **Bun.Worker thread** according to [`server/plugins/quickjs/vm.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/vm.ts). Worker threads run in separate OS processes, so a segmentation fault, infinite loop, or memory exhaustion in one plugin terminates only that worker. The main Bun host process continues running and can detect the worker failure through its standard messaging protocol.

### Can plugins access the filesystem or network directly?

**No.** Plugins run inside **QuickJS-WASM**, which has no bindings to the host filesystem, network sockets, or standard I/O. The WebAssembly sandbox provides a memory-safe execution environment with no direct system calls. Any file or network operations must go through `__hostCall`, where the host implementation enforces permission checks against the plugin's `grantedPermissions` array.

### What happens when a plugin exceeds its memory or time limits?

The sandbox enforces **hard caps** defined in [`server/plugins/quickjs/limits.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/limits.ts). Memory over-allocation triggers an `OutOfMemory` error inside the VM. Stack overflow from deep recursion hits the 1 MiB stack limit. Execution time exceeding the default 5-second deadline (customizable via `DEFAULT_EVAL_TIMEOUT_MS`) causes `withSyncDeadline` in [`server/plugins/quickjs/eval.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/plugins/quickjs/eval.ts) to abort the VM and log the timeout. In all cases, the worker can be terminated without affecting other plugins or the host.

### How does the host communicate with a plugin running in a synchronous VM?

Instatic uses a **synchronous-initiated, async-resolved pattern**. When plugin code calls `__hostCall`, the VM immediately returns a promise handle and yields. The host processes the request asynchronously, then resolves the promise from outside the VM. The `runtime.executePendingJobs()` call drains the micro-task queue to deliver the result. This design lets plugins write natural async/await code while the host maintains full control over when and how operations execute.