]> git.kaiwu.me - njs.git/commitdiff
Modules: initialize the JS context of a completed subrequest
authorDmitry Volyntsev <xeioex@nginx.com>
Wed, 11 Nov 2026 01:46:20 +0000 (17:46 -0800)
committerDmitry Volyntsev <xeioexception@gmail.com>
Thu, 12 Nov 2026 22:29:31 +0000 (14:29 -0800)
When a subrequest completed, its JS context was allocated by the
subrequest completion handler without ngx_js_ctx_init(), so the log and
the waiting events tree were not initialized.  If the subrequest had no
JS context of its own, a reply received by the parent could then use
it:

- readRequest*() on the reply inserted a caller event into the
  uninitialized tree;
- reading a js_set variable through reply.variables created an engine
  for the uninitialized context.

Both crashed with both engines.  The waiting events tree is missing
since 9de30135 (0.8.3), the log since 9b674412 (0.8.6).

Now the context is initialized as in ngx_http_js_init_vm().  Also,
deferred body reads are allowed only for the executing request, because
the body read state of a reply belongs to the subrequest.

Changed behaviour:
- readRequestText() and the other readRequest*() methods called on a
  subrequest reply throw TypeError.  They used to resolve with the body
  of the parent request, or to crash when the body had to be read.

nginx/ngx_http_js_module.c
nginx/t/js_http_runtime_isolation.t

index f85b0b657264eb82a73abc3e729edf30debbf4d1..ad9649ff4beda56891c8634cbec9a8dec883a8b7 100644 (file)
@@ -3950,6 +3950,11 @@ ngx_http_js_ext_read_request_body(njs_vm_t *vm, njs_value_t *args,
         return NJS_ERROR;
     }
 
+    if (r != njs_vm_external_ptr(vm)) {
+        njs_vm_type_error(vm, "cannot read body of another request");
+        return NJS_ERROR;
+    }
+
     ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
 
     event = njs_mp_zalloc(njs_vm_memory_pool(vm),
@@ -4061,6 +4066,11 @@ ngx_http_js_ext_read_request_form(njs_vm_t *vm, njs_value_t *args,
         return NJS_ERROR;
     }
 
+    if (r != njs_vm_external_ptr(vm)) {
+        njs_vm_type_error(vm, "cannot read body of another request");
+        return NJS_ERROR;
+    }
+
     if (ngx_http_js_request_form_max_keys(vm, njs_arg(args, nargs, 1),
                                           &max_keys)
         != NJS_OK)
@@ -5066,6 +5076,8 @@ ngx_http_js_subrequest_done(ngx_http_request_t *r, void *data, ngx_int_t rc)
             return NGX_ERROR;
         }
 
+        ngx_js_ctx_init((ngx_js_ctx_t *) ctx, r->connection->log);
+
         ngx_http_set_ctx(r, ctx, ngx_http_js_module);
     }
 
@@ -6788,6 +6800,11 @@ ngx_http_qjs_ext_read_request_body(JSContext *cx, JSValueConst this_val,
     }
 
     r = req->opaque.external;
+
+    if (r != JS_GetContextOpaque(cx)) {
+        return JS_ThrowTypeError(cx, "cannot read body of another request");
+    }
+
     ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
 
     if (r->request_body) {
@@ -6943,6 +6960,11 @@ ngx_http_qjs_ext_read_request_form(JSContext *cx, JSValueConst this_val,
     }
 
     r = req->opaque.external;
+
+    if (r != JS_GetContextOpaque(cx)) {
+        return JS_ThrowTypeError(cx, "cannot read body of another request");
+    }
+
     ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
 
     if (r->request_body) {
@@ -7668,6 +7690,8 @@ ngx_http_qjs_subrequest_done(ngx_http_request_t *r, void *data, ngx_int_t rc)
             return NGX_ERROR;
         }
 
+        ngx_js_ctx_init((ngx_js_ctx_t *) sctx, r->connection->log);
+
         ngx_http_set_ctx(r, sctx, ngx_http_js_module);
 
         ngx_qjs_arg(sctx->response_body) = JS_UNDEFINED;
index a8b979aa89fde27ed763831f07519a30a56296ad..e85fbd5051ae6a71cbcb0ec53c6dda48dd2279ae 100644 (file)
@@ -37,6 +37,8 @@ http {
 
     js_import test.js;
     js_set $retval test.variable;
+    js_set $reply_uri test.reply_uri;
+    js_var $read_result;
 
     server {
         listen       127.0.0.1:8080;
@@ -69,6 +71,19 @@ http {
         location /variable {
             return 200 $retval;
         }
+
+        location /access_read {
+            js_access test.access_read;
+            js_content test.read_result;
+        }
+
+        location /content_read {
+            js_content test.content_read;
+        }
+
+        location /reply_variable {
+            js_content test.reply_variable;
+        }
     }
 }
 
@@ -139,10 +154,45 @@ $t->write_file('test.js', <<'EOF');
         r.setReturnValue('own result');
     }
 
-    export default {content, child, variable};
+    function read_reply(reply) {
+        return ['readRequestText', 'readRequestForm'].map(method => {
+            try {
+                reply[method]();
+                return 'accepted';
+            } catch (e) {
+                return e instanceof TypeError ? 'rejected' : e.name;
+            }
+        }).join(',');
+    }
+
+    async function access_read(r) {
+        const reply = await r.subrequest('/native');
+        r.variables.read_result = read_reply(reply);
+    }
+
+    function read_result(r) {
+        r.return(200, r.variables.read_result);
+    }
+
+    async function content_read(r) {
+        const reply = await r.subrequest('/native');
+        r.return(200, read_reply(reply));
+    }
+
+    function reply_uri(r) {
+        return r.uri;
+    }
+
+    async function reply_variable(r) {
+        const reply = await r.subrequest('/native');
+        r.return(200, reply.variables.reply_uri);
+    }
+
+    export default {content, child, variable, access_read, read_result,
+                    content_read, reply_uri, reply_variable};
 EOF
 
-$t->try_run('no njs available')->plan(19);
+$t->try_run('no njs available')->plan(22);
 
 for my $scope ('zero', 'reuse') {
     for my $mode ('parent', 'shape', 'args', 'parent_set') {
@@ -162,5 +212,10 @@ for my $scope ('zero', 'reuse') {
 }
 
 like(http_get('/variable'), qr/own result$/m, 'own async js_set result');
+like(http_get('/access_read'), qr/rejected,rejected$/m,
+    'reply body read in access handler');
+like(http_get('/content_read'), qr/rejected,rejected$/m,
+    'reply body read in content handler');
+like(http_get('/reply_variable'), qr/\/native$/m, 'reply js_set variable');
 
 ###############################################################################