]> git.kaiwu.me - njs.git/commitdiff
QuickJS: fix njs.on() replacing the exit hook during its call
authorDmitry Volyntsev <xeioex@nginx.com>
Thu, 12 Nov 2026 01:34:31 +0000 (17:34 -0800)
committerDmitry Volyntsev <xeioexception@gmail.com>
Thu, 12 Nov 2026 22:29:31 +0000 (14:29 -0800)
Previously, the exit hook was released after the call while the caller
kept no reference to it.  If the hook replaced or reset itself with
njs.on(), the function was freed while it was still running, which
resulted in use-after-free.

Now the hook is detached from the njs object before the call, and the
call owns the reference.

The issue was introduced in 04f6dfb9 (0.9.2).

src/qjs.c
test/js/njs_on_exit_replace.t.js [new file with mode: 0644]

index 5220486b227e4feb2a1fd125925c9b64c4464adb..2ab6ed0cb433627a5dd7f3198be557ee84e3c0a9 100644 (file)
--- a/src/qjs.c
+++ b/src/qjs.c
@@ -355,7 +355,7 @@ qjs_chb_rt_free(void *rt, void *p)
 JSValue
 qjs_call_exit_hook(JSContext *ctx)
 {
-    JSValue    global, obj, ret;
+    JSValue    global, obj, hook, ret;
     qjs_njs_t  *njs;
 
     global = JS_GetGlobalObject(ctx);
@@ -367,12 +367,13 @@ qjs_call_exit_hook(JSContext *ctx)
 
     njs = JS_GetOpaque(obj, QJS_CORE_CLASS_ID_NJS);
     if (njs != NULL && JS_IsFunction(ctx, njs->hooks[QJS_NJS_HOOK_EXIT])) {
-        ret = JS_Call(ctx, njs->hooks[QJS_NJS_HOOK_EXIT], JS_UNDEFINED,
-                      0, NULL);
-
-        JS_FreeValue(ctx, njs->hooks[QJS_NJS_HOOK_EXIT]);
+        hook = njs->hooks[QJS_NJS_HOOK_EXIT];
         njs->hooks[QJS_NJS_HOOK_EXIT] = JS_UNDEFINED;
 
+        ret = JS_Call(ctx, hook, JS_UNDEFINED, 0, NULL);
+
+        JS_FreeValue(ctx, hook);
+
         if (JS_IsException(ret)) {
             JS_FreeValue(ctx, obj);
             JS_FreeValue(ctx, global);
diff --git a/test/js/njs_on_exit_replace.t.js b/test/js/njs_on_exit_replace.t.js
new file mode 100644 (file)
index 0000000..f005523
--- /dev/null
@@ -0,0 +1,24 @@
+/*---
+includes: []
+flags: []
+---*/
+
+if (typeof njs == 'object' && typeof njs.on == 'function') {
+    (function() {
+        let o = {v: 'x'};
+
+        njs.on('exit', function() {
+            njs.on('exit', null);
+
+            let a = [];
+
+            for (let i = 0; i < 100; i++) {
+                a.push({i});
+            }
+
+            if (o.v !== 'x') {
+                throw Error('closure is lost');
+            }
+        });
+    })();
+}