Skip to content

JavaScriptTask has no step budget or timeout — and this window typed the step() that would give it one #872

Description

@sroussey

What

packages/javascript/src/task/JavaScriptTask.ts:37 is still a bare

const myInterpreter = new Interpreter(`${inputVariablesString} ${code}`);
myInterpreter.run();

run() executes to completion with no instruction budget, no deadline and no AbortSignal check, so while (true) {} in a javascript_code port wedges the thread it runs on — the main thread in the browser build, a worker in node. Six consecutive review cycles (first recorded 2026-08-03; grade held at B− since).

Why now

Two in-repo remedies now exist, both built for neighbouring problems and neither applied here:

  1. step() is typed in this repo as of 7fea774. packages/javascript/src/task/interpreter.d.ts:103 declares step(): boolean"Executes one step. Returns false when there are no more instructions." The vendored interpreter always had it; until this commit it was an implicit any under noImplicitAny, which was the standing excuse. The whole fix is a bounded loop:
const deadline = performance.now() + budgetMs;
let steps = 0;
while (myInterpreter.step()) {
  if (++steps % 10_000 === 0) {
    if (signal?.aborted) throw new TaskAbortedError(...);
    if (performance.now() > deadline) throw new TaskInvalidInputError(...);
  }
}

~10 lines in a 175-line file, with no dependency on the host runtime.

  1. packages/tasks/src/util/BoundedRegex.server.ts:37-64 already runs untrusted work under an enforced node:vm timeout and converts an overrun into a TaskInvalidInputError rather than a hang, with the measurements in its docstring. That is the server-side shape if a vm is preferred over the step loop.

The centralised-limits precedent for the constant is packages/util/src/limits.ts (DEFAULT_LIMITS / SECURITY_LIMITS), which already carries the file/regex budgets packages/tasks added.

Why it matters

JavaScriptTask is registered by registerCommonTasks() in every entrypoint, so it is resolvable by type name from graph JSON the host did not author — the same threat model that drove 2cebe68e9 to take the three filesystem tasks out of the ambient registry. A JavaScriptTask node is the one remaining ambient task that accepts attacker-authored code, and it has no stopping condition but the process.

Acceptance

  • JavaScriptTask refuses a program that exceeds a configurable step budget or wall-clock deadline, with a typed error rather than a hang.
  • The budget is honoured on both entrypoints (browser and node), which the step() loop gets for free and a vm does not.
  • A co-located test that runs while(true){} and asserts the task fails within the budget. packages/javascript currently has no tsconfig.test.json and no co-located tests, so this is also the cheapest place to start one.

References

  • packages/javascript/src/task/JavaScriptTask.ts:22-48
  • packages/javascript/src/task/interpreter.d.ts:101-103 (added 7fea774)
  • packages/tasks/src/util/BoundedRegex.server.ts:18-64

Found during the 2026-08-31 review of packages/; standing open since 2026-08-03.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions