Skip to content

fix(runtime): safe-marshal hook ctx so circular host handles don't crash the sandbox (#2674)#2676

Merged
os-zhuang merged 1 commit into
mainfrom
fix/2674-safe-json-marshal
Jul 7, 2026
Merged

fix(runtime): safe-marshal hook ctx so circular host handles don't crash the sandbox (#2674)#2676
os-zhuang merged 1 commit into
mainfrom
fix/2674-safe-json-marshal

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Problem

When a sandboxed hook executes, QuickJSScriptRunner.installCtx marshals the hook ctx into the QuickJS sandbox via setGlobalJson / setObjectJson, each of which calls a bare JSON.stringify(v ?? null). If the context (or anything reachable from it) holds a live setTimeout/setInterval handle, the timer object links back on itself (Timeout._idlePrev -> TimersList._idleNext -> …) and JSON.stringify throws:

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Timeout'
    |     property '_idlePrev' -> object with constructor 'TimersList'
    --- property '_idleNext' closes the circle

The throw happens before the existing evalCode error guard, so it escapes as an opaque circular-reference crash that takes the whole hook down (#2674).

Fix

Introduce a shared safeJsonStringify helper and route all three host→VM marshalling paths through it — setGlobalJson, setObjectJson, and jsonToHandle. It uses a JSON.stringify replacer that:

  • tracks ancestors in a WeakSet and drops circular back-edges (the timer case), and
  • coerces BigInt to a string (plain JSON.stringify throws on BigInt).

Only JSON-safe leaves are meant to cross the sandbox boundary anyway, so anything unserialisable is stripped rather than fatal, and the hook body still runs.

Tests

Added two regression tests to quickjs-runner.test.ts:

  • ctx.input containing a live circular setInterval handle marshals without crashing and the body reads the sibling fields.
  • a BigInt in ctx.input is coerced to a string instead of throwing.

pnpm vitest run src/sandbox/quickjs-runner.test.ts → 26 passed. turbo run build --filter=@objectstack/runtime (incl. strict DTS) green.

Closes #2674.

🤖 Generated with Claude Code

…ash the sandbox (#2674)

`QuickJSScriptRunner.installCtx` serialised the hook context into the
sandbox with a bare `JSON.stringify`. A live `setTimeout`/`setInterval`
handle reachable from `ctx` links back on itself
(`Timeout._idlePrev -> TimersList._idleNext -> …`), so `JSON.stringify`
threw `TypeError: Converting circular structure to JSON` and took the
whole hook down.

Route all three host→VM marshalling paths (`setGlobalJson`,
`setObjectJson`, `jsonToHandle`) through a shared `safeJsonStringify`
that drops circular back-edges via a path `WeakSet` and coerces `BigInt`
to a string. Only JSON-safe leaves cross the boundary — unserialisable
host objects are stripped rather than fatal — and the body still runs.

Closes #2674.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 7, 2026 1:45pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/s labels Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime.

15 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/permissions/authentication.mdx (via @objectstack/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectos/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectos/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/releases/implementation-status.mdx (via @objectstack/runtime)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit b5a87eb into main Jul 7, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the fix/2674-safe-json-marshal branch July 7, 2026 14:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

QuickJSScriptRunner.installCtx throws when context contains a Timeout with circular refs

1 participant