DEV Community

Cover image for Safely Running AI-Generated Code in Node.js: `vm` vs `worker_threads` vs V8 Isolates
Mindinu Ariyawansha
Mindinu Ariyawansha

Posted on

Safely Running AI-Generated Code in Node.js: `vm` vs `worker_threads` vs V8 Isolates

With the rise of autonomous AI agents and code-interpreting LLM features, Node.js backends are increasingly required to execute code generated on the fly.

However, running arbitrary user or AI code inside a Node.js process is dangerous. A single malicious or hallucinated script can read environment variables (process.env), spawn OS processes (child_process), freeze the event loop with infinite loops, or crash your entire server by exhausting heap memory.

In this post, we’ll analyze the three primary methods for executing isolated code in Node.js, why built-in modules fail at security, and how to implement true V8 heap sandboxing.


1. The vm Module: Great for Context, Terrible for Security

Node.js ships with a built-in vm module that allows compiling and running code within a synthetic context.

import vm from 'node:vm';

const context = { x: 2 };
vm.createContext(context);

// Looks sandboxed, but IS NOT SECURE!
vm.runInContext('x += 40;', context);
Enter fullscreen mode Exit fullscreen mode

The Security Trap

The Node.js documentation explicitly states: "The vm module is not a security mechanism. Do not use it to run untrusted code."

Because the context objects inside vm still inherit from the host process's standard prototypes, untrusted code can escape the sandbox via constructor chain navigation:

// Exploiting Node.js vm to execute host OS commands
const code = `
  this.constructor.constructor('return process')().mainModule.require('child_process').execSync('whoami');
`;
vm.runInContext(code, context); // Executed on your host machine!
Enter fullscreen mode Exit fullscreen mode

2. worker_threads: Good for Offloading, Weak for Isolation

Node.js worker_threads move execution off the main event loop onto a separate thread.

import { Worker } from 'node:worker_threads';

const worker = new Worker('./worker-script.js', {
  workerData: { code: 'console.log("Hello from worker");' }
});
Enter fullscreen mode Exit fullscreen mode

Pros & Cons

  • Pros: Non-blocking. If the guest script runs an infinite loop, the main event loop remains responsive. You can also destroy the thread via worker.terminate().
  • Cons: High memory footprint (each worker instantiates an entire Node.js runtime state). More importantly, workers still share access to Node's internal APIs (fs, net, process), meaning malicious code can still read disk files or make outgoing requests unless restricted by external containers like Docker.

3. V8 Isolates (isolated-vm): True Heap & Execution Sandboxing

To safely execute untrusted code without running heavy Docker containers, you need V8 Isolates.

An isolate is a distinct instance of the V8 engine with its own heap, garbage collector, and execution state. V8 isolates allow running multiple execution contexts inside a single Node.js process with zero cross-leakage and strict physical memory ceilings.

Using libraries like isolated-vm, you can instantiate pure V8 isolates directly in Node.js:

import ivm from 'isolated-vm';

async function runUntrustedCode(codeString: string) {
  // 1. Create a new V8 isolate with a hard 64MB memory cap
  const isolate = new ivm.Isolate({ memoryLimit: 64 });

  // 2. Create an execution context inside the isolate
  const context = await isolate.createContext();
  const global = context.global;

  // 3. Set standard global primitives
  await global.set('global', global.derefInto());

  // 4. Compile and run the code with a strict 1-second timeout
  const script = await isolate.compileScript(codeString);

  try {
    const result = await script.run(context, { timeout: 1000 });
    return result;
  } catch (err) {
    console.error('Execution terminated or timed out:', err);
  } finally {
    // 5. Clean up V8 heap resources immediately
    isolate.dispose();
  }
}
Enter fullscreen mode Exit fullscreen mode

Why V8 Isolates Win for AI Runtimes:

  1. Zero Host Leaks: Prototype chains inside the isolate do not lead back to the host Node.js process or require functions.
  2. Hard Memory Ceilings: Exceeding memoryLimit immediately terminates the isolate without crashing the parent process.
  3. Sub-Millisecond Startup: Spawning a V8 isolate takes under $1\text{ms}$, compared to $100\text{ms}+$ for OS containers or heavy VMs.

Technical Comparison Matrix

Feature vm Module worker_threads V8 Isolates (isolated-vm) Docker Containers
Security Isolation ❌ None ⚠️ Partial 🟢 High 🟢 Complete
Startup Overhead $< 1\text{ms}$ $\sim 20\text{ms}$–$50\text{ms}$ $< 1\text{ms}$ $500\text{ms}$–$2000\text{ms}$
Memory Isolation Shared Separate Thread Hard Ceiling Per Isolate Full OS Container
Host Process Risk Arbitrary Code Execution Shared System Resources Isolated V8 Heap Completely Sandboxed
Best Use Case Trusted Internal Plugins CPU-bound background math Untrusted AI / User Scripts Multi-tenant untrusted apps

Best Practices for AI Code Execution

If your application evaluates LLM-generated code or executes dynamic tool calls:

  1. Never pass Node system modules into the execution scope. Strip fs, child_process, and net entirely.
  2. Enforce hard execution timeouts. Always wrap code execution in a deadline timer (e.g., $1000\text{ms}$) to prevent event loop locking via while(true).
  3. Limit memory allocations. Cap isolate memory at 32MB–128MB depending on the workload.
  4. Sanitize return payloads. Pass return values through Data Loss Prevention (DLP) scanners or schema validators before returning them to your LLM's context window.

Summary

  • Avoid node:vm for untrusted inputs—it is easily escaped.
  • Use worker_threads for heavy concurrency, but not as a security barrier.
  • Adopt V8 Isolates when you need fast, local, sub-millisecond sandboxing for untrusted dynamic scripts.

Top comments (0)