Skip to content

interface.c fails to build against quickjs-ng 0.16.0 (JS_NewArrayBuffer gained max_len + realloc callback) #268

Description

@AbyssinianGuy

c/interface.c doesn't compile against quickjs-ng 0.16.0, because JS_NewArrayBuffer gained a parameter and its free callback became a realloc callback. Filing this with a working patch in case it's useful — happy to open a PR if you'd like it in that form.

The break

main (7b7af98) vendors quickjs-ng v0.12.1. Dropping in the published quickjs-amalgam.zip for v0.16.0 fails at two call sites:

c/interface.c:706:85:  error: too few arguments to function call, expected 7, have 6
c/interface.c:1555:92: error: too few arguments to function call, expected 7, have 6

The signature changed to support ES2024 resizable ArrayBuffers:

// <= 0.15.1
JS_NewArrayBuffer(ctx, buf, len,           free_func, opaque, is_shared)
typedef void  JSFreeArrayBufferDataFunc   (JSRuntime*, void *opaque, void *ptr);

// 0.16.0
JS_NewArrayBuffer(ctx, buf, len, max_len, realloc_func, opaque, is_shared)
typedef void *JSReallocArrayBufferDataFunc(JSRuntime*, void *opaque, void *ptr, size_t size);

Growing a buffer requires reallocation, so freeing became a special case of realloc — the engine calls abuf->realloc_func(rt, abuf->opaque, abuf->data, 0) on collection.

0.16.0 is the first affected release. I checked the published amalgams for v0.13.0 and v0.15.1 and both still carry the 6-argument form; the upstream commit ("Support resizable externally managed ArrayBuffers") is dated 2026-07-21, between the 0.15.1 and 0.16.0 releases.

Two things that make the port less mechanical than it looks

The two call sites use different allocators. QTS_NewArrayBuffer gets a host malloc buffer, so it needs free/realloc. QTS_bjson_encode gets a buffer from JS_WriteObject, i.e. quickjs's own allocator, so it needs js_realloc_rt — which conveniently already frees at size 0, making that shim identical to the engine's internal js_array_buffer_realloc. Using the wrong one is heap corruption rather than a compile error.

Passing NULL for realloc_func compiles and leaks. It's the smallest change that builds, but the header defines NULL as "the memory must not be managed by quickjs at all", so nothing frees the buffer.

I used max_len = 0 at both sites, which keeps the buffers fixed-length and preserves the pre-0.16 behaviour exactly.

Patch

-void qts_free_buffer(JSRuntime *unused_rt, void *unused_opaque, void *ptr) { free(ptr); }
+void *qts_realloc_buffer(JSRuntime *unused_rt, void *unused_opaque, void *ptr, size_t size) {
+  if (size == 0) {
+    free(ptr);
+    return NULL;
+  }
+  return realloc(ptr, size);
+}

 JSValue *QTS_NewArrayBuffer(JSContext *ctx, JSVoid *buffer, size_t length) {
   return jsvalue_to_heap(
-      JS_NewArrayBuffer(ctx, (uint8_t *)buffer, length, qts_free_buffer, NULL, false));
+      JS_NewArrayBuffer(ctx, (uint8_t *)buffer, length, 0, qts_realloc_buffer, NULL, false));
 }
-static void qts_free_bjson_buffer(JSRuntime *rt, void *opaque, void *ptr) {
-  js_free_rt(rt, ptr);
+static void *qts_realloc_bjson_buffer(JSRuntime *rt, void *opaque, void *ptr, size_t size) {
+  return js_realloc_rt(rt, ptr, size);
 }
...
-  JSValue array = JS_NewArrayBuffer(ctx, buffer, length, qts_free_bjson_buffer, NULL, false);
+  JSValue array = JS_NewArrayBuffer(ctx, buffer, length, 0, qts_realloc_bjson_buffer, NULL, false);

Since this only changes the callback plumbing, it should be conditional on the vendored version if you want to keep building against older quickjs-ng.

One other note for anyone attempting the same upgrade: replacing the amalgam also drops vendor/quickjs-ng-patches/0001-bellard-module-detection.patch, which surfaces later as undefined symbol: QTS_DetectModule at link time. It re-applies cleanly to 0.16.0 with an offset.

Verification

Built the quickjs-ng-wasmfile-release-sync variant from 0.16.0 with the above (Docker emsdk, unmodified Makefile). The resulting wasm reports 0.16.0, evalCode works, the interrupt handler still terminates while(true), setMemoryLimit is still enforced, and a 41-call async workload returns byte-identical output about 10% faster than the 0.12.1 build.

Why I care about the upgrade specifically

quickjs-ng has published ten security advisories, all of which postdate 0.12.1 — including a critical RCE in js_array_toReversed fixed in 0.15.0, and eight more fixed in 0.16.0. Anyone consuming the published @jitl/quickjs-ng-* variants is getting an engine from before all of them, and since the npm packages are the only distribution channel there's no way to pick up the fixes downstream.

Entirely understand this is your call and your time — thanks for maintaining this, it's a genuinely useful project.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions