Skip to content

fix: honor options.contextPointer in QuickJSAsyncRuntime.newContext - #272

Open
xfy2412 wants to merge 1 commit into
justjake:mainfrom
xfy2412:fix-asyncify-newcontext-context-pointer
Open

xfy2412 wants to merge 1 commit into
justjake:mainfrom
xfy2412:fix-asyncify-newcontext-context-pointer

Conversation

@xfy2412

@xfy2412 xfy2412 commented Sep 20, 2026

Copy link
Copy Markdown

QuickJSAsyncRuntime.newContext silently drops options.contextPointer, while the base
QuickJSRuntime.newContext honors it:

// runtime.ts (base)
const ctx = new Lifetime(options.contextPointer || this.ffi.QTS_NewContext(this.rt.value, intrinsics), ...)
// runtime-asyncify.ts (before this PR)
const ctx = new Lifetime(this.ffi.QTS_NewContext(this.rt.value, intrinsics), ...)  // contextPointer ignored

Three call sites pass that option in order to wrap a context the engine already handed us —
executePendingJobs() and the module loader / normalizer callbacks in runtime.ts, all shaped like:

const context = this.contextMap.get(ptr) ?? this.newContext({ contextPointer: ptr })

On asyncify variants the override therefore creates a brand new JSContext instead of wrapping
the existing one:

  • the caller gets the wrong realm (e.g. a module loader runs against a fresh, empty context), and
  • nothing owns the new context, so it is never disposed. It survives until JS_FreeRuntime, which
    asserts list_empty(&rt->gc_obj_list) — an uncatchable WASM abort(), i.e. untrusted JS can
    kill the host process.

Relation to #269 and #248

While root-causing #269 (that same assertion, reachable from a plain promise job) we found that
executePendingJobs can reach this fallback with a garbage pointer: the out-parameter view is
created before QTS_ExecutePendingJob, and if the job grows the WASM memory the view is
detached, so typedArray[0] reads undefined rather than 0. The ctxPtr === 0 guard misses
undefined, contextMap.get(undefined) misses as well — and this override turns that miss into a
leaked context.

#248 fixes the detach half of that chain. This PR fixes the other half, which remains reachable
through the module-loader paths even with #248 applied.

For reference, the full chain reproduces deterministically (160k+ objects in a single promise job →
Aborted(Assertion failed: list_empty(&rt->gc_obj_list), at: quickjs.c, JS_FreeRuntime)); with both
halves fixed, 160k/200k/500k all tear down cleanly. I can share the reproducer if useful.

Tests

No test added: my environment can't run the suite (it needs a full pnpm install, which fails here
on an unrelated peer-dependency conflict). Happy to add one along the lines of "newContext({ contextPointer: ptr })
returns a context whose ctx.value === ptr on an asyncify runtime" — just tell me which file you'd
prefer it in.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant