Skip to content

Investigate quickjs-emscripten 0.32 upgrade: HostRef-based function binding requires wrapper changes #71

@bnimit

Description

@bnimit

Background

Upstream quickjs-emscripten 0.32.0 (released 2026-02-16) reworked function binding in newFunction to use a new abstraction called HostRef. From the upstream changelog:

Re-works function binding in newFunction to use a different proxying strategy based on a new abstraction, HostRef. HostRef allows tracking references of host values from guest handles. Once all references to the HostRef object are disposed (either in the host, or GC'd in the guest), the host value will be dereferenced and become garbage collectable. Functions passed to newFunction are interned in the runtime's HostRefMap until dereferenced.

New API surface introduced upstream: newHostRef, toHostRef, unwrapHostRef, QuickJSContext.newConstructorFunction.

Why this matters for this wrapper

Naively bumping the quickjs-emscripten devDependency from 0.31.0 to 0.32.0 causes the following test to fail:

FAIL  src/index.test.ts > memory management > memory limits work with marshaling

RuntimeError: Aborted(Assertion failed: list_empty(&rt->gc_obj_list), at: ../../vendor/quickjs/quickjs.c,2036,JS_FreeRuntime).

The remaining 136 tests pass. The specific failure path:

  1. The test sets a 200 KB memory limit on the runtime
  2. The marshaling layer exposes host functions to the sandbox via newFunction — under 0.32 these get interned in the runtime's HostRefMap
  3. The QuickJS sandbox runs a loop that exceeds the memory limit; QuickJS aborts the operation mid-way
  4. arena.dispose() cleans up the handles it tracks, but does not clean up the orphaned HostRefMap entries left by the aborted operation
  5. ctx.dispose() → JS_FreeRuntime asserts the GC object list is empty — but it isn't, because the HostRefMap entries are still alive in the runtime
  6. The WASM module aborts at the C-level assertion

The underlying issue is broader than the specific memory-limit case: any abort path (memory limit, stack overflow, interrupt handler, etc.) that interrupts marshaling will likely leave HostRefs orphaned and trigger the same assertion on disposal.

Scope of work to upgrade

  • Read the new HostRef API surface in detail (newHostRef, toHostRef, unwrapHostRef, newConstructorFunction) and document lifecycle guarantees
  • Audit every newFunction call site in src/ (notably src/marshal/function.ts, src/marshal/object.ts, anywhere we expose host callables)
  • Decide whether to migrate from newFunction to the new newHostRef primitive (cleaner, more direct control over lifetime) or stay with newFunction and add explicit HostRefMap cleanup hooks
  • Update Arena.dispose() (and VMMap.dispose() if relevant) to handle HostRef cleanup on abort paths
  • Add regression tests for additional abort scenarios beyond the memory-limit case (stack overflow via setMaxStackSize, interrupt handler aborting an operation, etc.) so we know the fix is general
  • Bump quickjs-emscripten 0.31.0 → 0.32.0 in package.json and verify all 137 tests pass
  • Update peerDependency range if appropriate (currently "*", but worth considering pinning to >=0.32 once stable)

Estimate

Half a day to a full day depending on how deep the marshaling integration goes. Worth pairing with someone who has prior context on this wrapper.

Why this is being tracked separately

The secure-release-workflow PR (#70) is unrelated to this work and shouldn't be held up by it. Renovate will continue surfacing the 0.32.0 update on its normal cadence; this issue exists so the work doesn't get forgotten when someone gets to a "yes, finally" moment on the Renovate PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions