Skip to content

only-cliches/quickjs-worker

Repository files navigation

QuickJS-VM

npm version License

A QuickJS runtime wrapper for Node.js that allows you to run untrusted ES6 code in a secure, sandboxed environment. Doesn't use WASM, compiles to a native module with Rust.

Installation of the Rust Programming Language is required to use this module.

Features include:

  • Safely evaluate untrusted Javascript (supports most of ES2023).
  • Worker-like API and eval interface.
  • Asynchronous and synchronous code execution.
  • Runtime resource limits (memory, CPU time, stack size) to reduce attack surface.
  • Argument passing into the sandbox environment.
  • Message passing to/from the sandbox via event handlers.
  • Bytecode exporting and loading for more advanced usage.
  • Global injection of primitives, JSON objects, arrays, and sync functions.

Table of Contents

Installation

npm install quickjs-vm

Quick Start

import { QJSWorker } from "quickjs-vm";

const runtime = QJSWorker({
  console: console,                // Forward QuickJS logs to Node console
  maxEvalMs: 1500,                 // Limit execution time for each eval
  maxMemoryBytes: 256 * 1_000_000, // Limit runtime memory
  maxStackSizeBytes: 10 * 1_000_000, // Limit stack size
  globals: {
    a_boolean: true,
    addTwo: (a, b) => a + b,
  },
});

(async () => {
  const [result] = await runtime.eval("addTwo(2, 3)");
  console.log(result); // 5

  await runtime.close();
})();

Features

  • Secure Sandbox: Each instance runs in its own thread and memory space, with optional time and memory limits.
  • Simple API: A worker-like API for asynchronous operations and an evalSync for synchronous ones.
  • Global Injection: Inject primitives, JSON objects, arrays, and sync functions into the QuickJS environment.
  • Message Passing: Use on("message", ...) and postMessage(...) to communicate between Node.js and QuickJS.
  • Promises & Async: Automatically resolves native JavaScript promises returned from QuickJS code.
  • Bytecode Support: Export and load QuickJS bytecode for advanced or optimized usage scenarios.
  • Resource Control: Configure maximum evaluation time, memory usage, and stack size to protect against runaway scripts.

Usage Examples

Below is an example usage showing various features of quickjs-vm:

import { QJSWorker } from "quickjs-vm";

const runtime = QJSWorker({
  console: {
    log: (...args) => {
      // capture console.log commands from the runtime
    },
  },
  // Alternatively, just provide Node console to pass them through
  // console: console,
  maxEvalMs: 1500, // limit execution time of each eval call
  maxMemoryBytes: 256 * 1000 * 1000, // limit runtime memory
  maxStackSizeBytes: 10 * 1000 * 1000, // limit stack size
  maxInterrupt: 10000, // prevent while(true) or similar lockups
  // inject globals into the runtime
  // supports primitives, JSON, arrays, and sync functions
  globals: {
    a_boolean: true,
    addTwo: (a, b) => a + b,
    json_value: {
      state: "Texas",
    },
    array_value: [
      { nested: "object" },
      { foo: "bar" },
    ],
    // Handle require() calls
    require: (moduleName) => {
      /* ... */
    },
  },
});

(async () => {
  const [evalResult, evalStats] = await runtime.eval("2 + 2");
  console.assert(evalResult == 4);

  const [evalResultSync] = runtime.evalSync("2 + 3");
  console.assert(evalResultSync == 5);

  const [addTwoResult] = await runtime.eval("addTwo(2, 3)");
  console.assert(addTwoResult == 5);

  // args can be passed directly
  const [addTwoResultWithArgs] = await runtime.eval(
    "addTwo",    // function name to call in the sandbox
    "script.js", // optional script name
    6,           // arg1
    6            // arg2
  );
  console.assert(addTwoResultWithArgs == 12);

  // return nested properties
  const [jsonValue] = await runtime.eval("json_value.state");
  console.assert(jsonValue == "Texas");

  // send a message to node from quickjs
  let receivedMessage;
  runtime.on("message", (msg) => {
    receivedMessage = msg;
  });
  await runtime.eval(`postMessage({hello: "from QuickJS"})`);
  console.assert(receivedMessage.hello == "from QuickJS");

  // send a message to quickjs from node
  await runtime.eval(`
    let messageFromNode;
    on('message', (msg) => { messageFromNode = msg; })
  `);
  await runtime.postMessage({ hello: "from Node" });
  const [message] = await runtime.eval("messageFromNode");
  console.assert(message.hello == "from Node");

  // Promises are resolved before returning their internal value.
  const [promise] = await runtime.eval("Promise.resolve({hello: 'world'})");
  console.assert(promise.hello == "world");

  // even nested promises are resolved
  const [nestedPromise] = await runtime.eval(
    "Promise.resolve(Promise.resolve({hello: 'world'}))"
  );
  console.assert(nestedPromise.hello == "world");

  // get memory stats
  const memStatus = await runtime.memory();
  // shows number of bytes currently being used
  console.log(memStats.memory_used_size); 

  // force garbage collector to run
  await runtime.gc();

  // bytecode, provides a Uint8Array
  const byteCode = await runtime.getByteCode(
    `const test = (a, b) => Promise.resolve(a+b)`
  );

  const runtime2 = QJSWorker();
  await runtime2.loadByteCode(byteCode);
  const [byteCodeFnResult] = await runtime2.eval("test(1, 2)");
  console.assert(byteCodeFnResult == 3);

  // make sure to close your runtimes or the node process will hang
  await runtime.close();
  await runtime2.close();
})();

Basic Eval

const [evalResult, evalStats] = await runtime.eval("2 + 2");
console.assert(evalResult === 4);
console.log(evalStats);

Arguments and Return Values

const [result] = await runtime.eval(
  "addTwo",    // function name to call in the sandbox
  "script.js", // optional script name
  6,           // arg1
  6            // arg2
);
console.assert(result === 12);

Asynchronous Operations

const [promiseResult] = await runtime.eval("Promise.resolve({ hello: 'world' })");
console.assert(promiseResult.hello === "world");

Message Passing

From QuickJS to Node:

runtime.on("message", (msg) => {
  console.log("Message from QuickJS:", msg);
});

await runtime.eval(`postMessage({ hello: "from QuickJS" })`);

And from Node to QuickJS:

await runtime.eval(`
  let messageFromNode;
  on('message', (msg) => { messageFromNode = msg; });
`);

await runtime.postMessage({ hello: "from Node" });
const [msg] = await runtime.eval("messageFromNode");
console.assert(msg.hello === "from Node");

Promises & Nested Promises

const [nestedPromise] = await runtime.eval(
  "Promise.resolve(Promise.resolve({ hello: 'world' }))"
);
console.assert(nestedPromise.hello === "world");

Memory Management and Garbage Collection

// Retrieve memory usage stats
const memStats = await runtime.memory();
// shows number of bytes currently being used
console.log(memStats.memory_used_size); 

// Force garbage collection
await runtime.gc();

Bytecode Export and Import

const byteCode = await runtime.getByteCode(`const test = (a, b) => a + b;`);
const runtime2 = QJSWorker();
await runtime2.loadByteCode(byteCode);

const [byteCodeFnResult] = await runtime2.eval("test(1, 2)");
console.assert(byteCodeFnResult === 3);

await runtime2.close();

API Reference

QJSWorker(options)

Creates a new QuickJS runtime instance in its own thread/memory space.

Options (all optional):

  • console (object)
    An object with methods like log, warn, error. Defaults to a no-op if not provided.

  • maxEvalMs (number)
    Maximum evaluation time in milliseconds per call to eval.

  • maxMemoryBytes (number)
    Maximum memory usage for the runtime (approximate).

  • maxStackSizeBytes (number)
    Maximum stack size for the runtime (approximate).

  • maxInterrupt (number)
    Interrupt count limit to break out of infinite loops.

  • globals (object)
    Key-value pairs to inject into the global scope of QuickJS. Supports primitives, arrays, JSON objects, and sync functions.

globals: {
  a_boolean: true,
  addTwo: (a, b) => a + b,
  json_value: { state: "Texas" },
  require: (moduleName) => { /* custom require logic */ },
}

Runtime Methods

  • eval(code: string, fileName?: string, ...args: any[]): Promise<[any, EvalStats?]>
    Evaluate JavaScript code asynchronously. Returns a Promise that resolves with [result, evalStats].

  • evalSync(code: string, fileName?: string, ...args: any[]): [any, EvalStats?]
    Synchronously evaluate JavaScript code. Blocks the Node.js event loop until completion.

  • on(eventName: string, callback: (msg: any) => void): void
    Subscribe to a named event from the QuickJS environment (e.g., "message").

  • postMessage(message: any): Promise<void>
    Send a message to the QuickJS environment, triggering any on('message') listeners inside the sandbox.

  • memory(): Promise<MemoryStats>
    Retrieve memory usage statistics from the QuickJS runtime.

  • gc(): Promise<void>
    Trigger garbage collection in the QuickJS runtime.

  • getByteCode(code: string): Promise<Uint8Array>
    Compile code into QuickJS bytecode and return it.

  • loadByteCode(byteCode: Uint8Array): Promise<void>
    Load previously exported QuickJS bytecode into the runtime.\

  • close(): Promise<void>
    Destroy the QuickJS runtime instance and free all resources.

Security Considerations

  • While QuickJS itself is designed to be relatively secure, no sandbox is 100% guaranteed to be invulnerable.
  • Use resource limits (maxEvalMs, maxMemoryBytes, maxStackSizeBytes, etc.) to mitigate denial-of-service attacks.
  • Network, file system, or other Node.js APIs are not automatically exposed, unless explicitly injected in globals.
  • Always be cautious when executing unknown or untrusted code, even within a sandbox.

Contributing

Contributions are welcome! Here’s how you can help:

  • Fork this repository.
  • Create a feature branch for your patch, e.g. feature/my-new-feature.
  • Write tests for your changes.
  • Submit a Pull Request detailing your updates.

For major changes, please open an issue first to discuss your proposal.

License

This project is licensed under the MIT License. See the LICENSE file for more details.

Thanks for using QuickJS-VM! If you find it helpful, consider giving it a star on GitHub.

About

Run QuickJS as a worker from NodeJS

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published