]> git.kaiwu.me - njs.git/commitdiff
QuickJS: fix compatibility with quickjs-ng 0.16.0 and later
authorDmitry Volyntsev <xeioex@nginx.com>
Mon, 17 Aug 2026 16:33:34 +0000 (09:33 -0700)
committerDmitry Volyntsev <xeioexception@gmail.com>
Thu, 20 Aug 2026 17:31:10 +0000 (10:31 -0700)
Two separate problems prevented njs from working with the recent
quickjs-ng releases.

JS_NewArrayBuffer() has grown a "max_len" argument and its free callback
has become a realloc one.  The calls are wrapped now, the callback has
to implement the whole realloc contract, because transfer() reallocates
the data even for a buffer which is not resizable.

The class ids used by njs started at 64, while the engine reserves the
ids below JS_CLASS_INIT_COUNT, which has reached 67.  JS_NewClass()
failed with a collision and the context could not be created.  The ids
are moved above the engine range and the remaining unchecked
JS_NewClass() calls now report the failure.

This fixes #1113 issue on Github.

auto/quickjs
external/njs_shell.c
nginx/ngx_http_js_module.c
nginx/ngx_qjs_fetch.c
src/qjs.c
src/qjs.h
src/qjs_buffer.c

index 630f9870083d2914a365d39aae935e53bb568c9c..785ee7832e3871e297630bb3c8991b4ad4f4123d 100644 (file)
@@ -187,6 +187,33 @@ if [ $NJS_TRY_QUICKJS = YES ]; then
 
         . auto/feature
 
+        njs_feature="QuickJS JS_NewArrayBuffer() with max_len"
+        njs_feature_name=NJS_HAVE_QUICKJS_ARRAY_BUFFER_MAX_LEN
+        njs_feature_run=no
+        njs_feature_test="#include <quickjs_compat.h>
+
+                          static void *
+                          realloc_func(JSRuntime *rt, void *opaque, void *ptr,
+                                       size_t size)
+                          {
+                              return NULL;
+                          }
+
+                          int main() {
+                              JSRuntime *rt;
+                              JSContext *ctx;
+
+                              rt = JS_NewRuntime();
+                              ctx = JS_NewContext(rt);
+                              (void) JS_NewArrayBuffer(ctx, NULL, 0, 0,
+                                                       realloc_func, NULL, 0);
+                              JS_FreeContext(ctx);
+                              JS_FreeRuntime(rt);
+                              return 0;
+                         }"
+
+        . auto/feature
+
         njs_feature="QuickJS JS_NewError() attaches stack"
         njs_feature_run=value
         njs_feature_name=NJS_HAVE_QUICKJS_NEW_ERROR_STACK
index b65b98aae73c4f215746faf87b33fe3e5a3236f6..1b9582a5410fcd6e1b642f400db28896dfdd92bb 100644 (file)
@@ -2081,9 +2081,8 @@ njs_qjs_agent(void *arg)
                 pthread_cond_signal(&console->agent_cond);
                 pthread_mutex_unlock(&console->agent_mutex);
 
-                args[0] = JS_NewArrayBuffer(ctx, agent->broadcast_sab_buf,
-                                            agent->broadcast_sab_size,
-                                            NULL, NULL, 1);
+                args[0] = qjs_new_external_array_buffer(ctx,
+                    agent->broadcast_sab_buf, agent->broadcast_sab_size, 1);
                 args[1] = JS_NewInt32(ctx, agent->broadcast_val);
 
                 ret_val = JS_Call(ctx, agent->broadcast_func, JS_UNDEFINED,
index d3da585f8f9f27aca7e345298bcfafdf57bc918a..e5c2b655d0d5646a1728dd25f223ecff8467c06b 100644 (file)
@@ -6681,8 +6681,8 @@ ngx_http_qjs_body_to_value(JSContext *cx, ngx_http_js_ctx_t *ctx,
 
     switch (type) {
     case NGX_JS_BODY_ARRAY_BUFFER:
-        return JS_NewArrayBuffer(cx, ctx->body_read_data,
-                                 ctx->body_read_len, NULL, NULL, 0);
+        return qjs_new_external_array_buffer(cx, ctx->body_read_data,
+                                             ctx->body_read_len, 0);
 
     case NGX_JS_BODY_JSON:
         if (ctx->body_read_nul) {
index 7adcda4f6ecc56c7b25e93ae59addec4431fa27c..5a8e98d785de61b12f5234bfd9df2fa29a69a71e 100644 (file)
@@ -2002,12 +2002,11 @@ ngx_qjs_ext_fetch_request_body(JSContext *cx, JSValueConst this_val,
     switch (magic) {
     case NGX_JS_BODY_ARRAY_BUFFER:
         /*
-         * no free_func for JS_NewArrayBuffer()
-         * because request->body is allocated from e->pool
+         * request->body is allocated from e->pool
          * and will be freed when context is freed.
          */
-        result = JS_NewArrayBuffer(cx, request->body.data, request->body.len,
-                                   NULL, NULL, 0);
+        result = qjs_new_external_array_buffer(cx, request->body.data,
+                                               request->body.len, 0);
         if (JS_IsException(result)) {
             return JS_ThrowOutOfMemory(cx);
         }
@@ -2331,12 +2330,11 @@ ngx_qjs_ext_fetch_response_body(JSContext *cx, JSValueConst this_val,
         }
 
         /*
-         * no free_func for JS_NewArrayBuffer()
-         * because string.start is allocated from e->pool
+         * string.start is allocated from e->pool
          * and will be freed when context is freed.
          */
-        result = JS_NewArrayBuffer(cx, string.start, string.length, NULL, NULL,
-                                   0);
+        result = qjs_new_external_array_buffer(cx, string.start,
+                                               string.length, 0);
         if (JS_IsException(result)) {
             return JS_ThrowOutOfMemory(cx);
         }
index c8ed8eafbcdd837de30bab2726e34a19aef11a35..90a464d6e5c9eb663b9c688123f6fbb0e76244d5 100644 (file)
--- a/src/qjs.c
+++ b/src/qjs.c
@@ -1252,17 +1252,61 @@ qjs_typed_array_data(JSContext *ctx, JSValueConst value, njs_str_t *data)
 }
 
 
+#ifdef NJS_HAVE_QUICKJS_ARRAY_BUFFER_MAX_LEN
+
+#define qjs_array_buffer_create(cx, src, len, free, shared)                    \
+    JS_NewArrayBuffer(cx, src, len, 0, free, NULL, shared)
+
+
+/*
+ * ArrayBuffer.prototype.transfer() reallocates the data even when the
+ * buffer is not resizable, so the whole realloc contract has to be
+ * implemented here.
+ */
+
+static void *
+qjs_array_buffer_free(JSRuntime *rt, void *opaque, void *ptr, size_t size)
+{
+    if (size == 0) {
+        js_free_rt(rt, ptr);
+        return NULL;
+    }
+
+    return js_realloc_rt(rt, ptr, size);
+}
+
+#else
+
+#define qjs_array_buffer_create(cx, src, len, free, shared)                    \
+    JS_NewArrayBuffer(cx, src, len, free, NULL, shared)
+
+
 static void
-js_array_buffer_free(JSRuntime *rt, void *opaque, void *ptr)
+qjs_array_buffer_free(JSRuntime *rt, void *opaque, void *ptr)
 {
     js_free_rt(rt, ptr);
 }
 
+#endif
+
 
 JSValue
 qjs_new_array_buffer(JSContext *cx, uint8_t *src, size_t len)
 {
-    return JS_NewArrayBuffer(cx, src, len, js_array_buffer_free, NULL, 0);
+    return qjs_array_buffer_create(cx, src, len, qjs_array_buffer_free, 0);
+}
+
+
+/*
+ * The memory is not managed by the engine, the caller is responsible for
+ * keeping it alive while the buffer is reachable.
+ */
+
+JSValue
+qjs_new_external_array_buffer(JSContext *cx, uint8_t *src, size_t len,
+    int is_shared)
+{
+    return qjs_array_buffer_create(cx, src, len, NULL, is_shared);
 }
 
 
index 365361520b64f0d3fd70f5641d0b4d3582ac5387..cc45643fd4c551c743f5572fc949abab4203e8dc 100644 (file)
--- a/src/qjs.h
+++ b/src/qjs.h
 #include <quickjs_compat.h>
 
 
+/*
+ * The engine reserves class ids below JS_CLASS_INIT_COUNT for its own
+ * classes.  The value is internal and grows between releases, so the ids
+ * below start far enough above it.
+ */
+#define QJS_CORE_CLASS_ID_OFFSET   128
+
 enum {
-    QJS_CORE_CLASS_ID_BUFFER = 64,
+    QJS_CORE_CLASS_ID_BUFFER = QJS_CORE_CLASS_ID_OFFSET,
     QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR,
     QJS_CORE_CLASS_ID_TEXT_DECODER,
     QJS_CORE_CLASS_ID_TEXT_ENCODER,
@@ -55,6 +62,8 @@ JSValue qjs_call_exit_hook(JSContext *ctx);
 
 JSValue qjs_new_uint8_array(JSContext *ctx, int argc, JSValueConst *argv);
 JSValue qjs_new_array_buffer(JSContext *cx, uint8_t *src, size_t len);
+JSValue qjs_new_external_array_buffer(JSContext *cx, uint8_t *src, size_t len,
+    int is_shared);
 JSValue qjs_buffer_alloc(JSContext *ctx, size_t size);
 JSValue qjs_buffer_create(JSContext *ctx, u_char *start, size_t size);
 JSValue qjs_buffer_chb_alloc(JSContext *ctx, njs_chb_t *chain);
index b081d1e7457d29a7a8f4be7d48cb657fe5430aab..b61251353f167d234b6a22e29b312001714f4081 100644 (file)
@@ -2666,8 +2666,11 @@ qjs_buffer_builtin_init(JSContext *ctx)
     JSValue    global_obj, buffer, proto, ctor, ta, ta_proto, symbol, species;
     JSClassID  u8_ta_class_id;
 
-    JS_NewClass(JS_GetRuntime(ctx), QJS_CORE_CLASS_ID_BUFFER,
-                &qjs_buffer_class);
+    if (JS_NewClass(JS_GetRuntime(ctx), QJS_CORE_CLASS_ID_BUFFER,
+                    &qjs_buffer_class) < 0)
+    {
+        return -1;
+    }
 
     global_obj = JS_GetGlobalObject(ctx);
 
@@ -2683,8 +2686,12 @@ qjs_buffer_builtin_init(JSContext *ctx)
      * We use JS_SetClassProto()/JS_GetClassProto() as a key-value store
      * for fast value query by class ID without querying the global object.
      */
-    JS_NewClass(JS_GetRuntime(ctx), QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR,
-                &qjs_uint8_array_ctor_class);
+    if (JS_NewClass(JS_GetRuntime(ctx), QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR,
+                    &qjs_uint8_array_ctor_class) < 0)
+    {
+        return -1;
+    }
+
     JS_SetClassProto(ctx, QJS_CORE_CLASS_ID_UINT8_ARRAY_CTOR,
                      JS_DupValue(ctx, ctor));
 #endif