From: Dmitry Volyntsev Date: Fri, 14 Aug 2026 02:10:28 +0000 (-0700) Subject: QuickJS: make Fetch finalization pool-independent X-Git-Tag: 1.0.1~11 X-Git-Url: https://git.kaiwu.me/%7B@url%7D?a=commitdiff_plain;h=72d3150a4d99522c242888a578d61b161a70ad97;p=njs.git QuickJS: make Fetch finalization pool-independent QuickJS may defer finalization of a Fetch object until after its native backing pools have been destroyed. Context reuse is not required: a local JavaScript cycle is sufficient. function content(r) { const response = new Response("body"); response.self = response; r.return(200); } With context reuse disabled, the cycle is collected while JS_FreeContext() destroys the per-request context. At that point the engine pool has already been destroyed. The Response finalizer accessed the pool-backed native structure and attempted to destroy its response chain, causing a use-after-free. Request finalization had the same lifetime dependency through its cached Headers value. The fix introduces a QuickJS-runtime-owned wrapper for Fetch Request and Response objects. It owns the cached Headers value and keeps only a non-owning pointer to the pool-backed data. Finalizers now release only runtime-owned state, while response chains continue to be reclaimed in bulk with the engine pool. --- diff --git a/nginx/ngx_qjs_fetch.c b/nginx/ngx_qjs_fetch.c index 2c67f630..7adcda4f 100644 --- a/nginx/ngx_qjs_fetch.c +++ b/nginx/ngx_qjs_fetch.c @@ -28,6 +28,12 @@ typedef struct { } ngx_qjs_fetch_t; +typedef struct { + void *data; + JSValue header_value; +} ngx_qjs_fetch_object_t; + + static ngx_int_t ngx_qjs_method_process(JSContext *cx, ngx_js_request_t *request); static ngx_int_t ngx_qjs_headers_inherit(JSContext *cx, @@ -89,6 +95,8 @@ static JSValue ngx_qjs_ext_fetch_request_field(JSContext *cx, JSValueConst this_val, int magic); static JSValue ngx_qjs_ext_fetch_request_mode(JSContext *cx, JSValueConst this_val); +static void ngx_qjs_fetch_request_mark(JSRuntime *rt, JSValueConst val, + JS_MarkFunc *mark_func); static void ngx_qjs_fetch_request_finalizer(JSRuntime *rt, JSValue val); static JSValue ngx_qjs_fetch_response_ctor(JSContext *cx, @@ -111,6 +119,8 @@ static JSValue ngx_qjs_ext_fetch_response_redirected(JSContext *cx, JSValueConst this_val); static JSValue ngx_qjs_ext_fetch_response_field(JSContext *cx, JSValueConst this_val, int magic); +static void ngx_qjs_fetch_response_mark(JSRuntime *rt, JSValueConst val, + JS_MarkFunc *mark_func); static void ngx_qjs_fetch_response_finalizer(JSRuntime *rt, JSValue val); static JSValue ngx_qjs_fetch_flag(JSContext *cx, const ngx_qjs_entry_t *entries, @@ -183,12 +193,14 @@ static const JSClassDef ngx_qjs_fetch_headers_class = { static const JSClassDef ngx_qjs_fetch_request_class = { "Request", .finalizer = ngx_qjs_fetch_request_finalizer, + .gc_mark = ngx_qjs_fetch_request_mark, }; static const JSClassDef ngx_qjs_fetch_response_class = { "Response", .finalizer = ngx_qjs_fetch_response_finalizer, + .gc_mark = ngx_qjs_fetch_response_mark, }; @@ -477,6 +489,37 @@ ngx_qjs_fetch_headers_ctor(JSContext *cx, JSValueConst new_target, int argc, } +static ngx_int_t +ngx_qjs_fetch_object_set(JSContext *cx, JSValueConst value, void *data) +{ + ngx_qjs_fetch_object_t *object; + + object = js_malloc(cx, sizeof(ngx_qjs_fetch_object_t)); + if (object == NULL) { + return NGX_ERROR; + } + + object->data = data; + object->header_value = JS_UNDEFINED; + + JS_SetOpaque(value, object); + + return NGX_OK; +} + + +static void * +ngx_qjs_fetch_object_data(JSContext *cx, JSValueConst value, + JSClassID class_id) +{ + ngx_qjs_fetch_object_t *object; + + object = JS_GetOpaque2(cx, value, class_id); + + return (object != NULL) ? object->data : NULL; +} + + static JSValue ngx_qjs_fetch_request_ctor(JSContext *cx, JSValueConst new_target, int argc, JSValueConst *argv) @@ -511,7 +554,10 @@ ngx_qjs_fetch_request_ctor(JSContext *cx, JSValueConst new_target, int argc, return JS_EXCEPTION; } - JS_SetOpaque(obj, request); + if (ngx_qjs_fetch_object_set(cx, obj, request) != NGX_OK) { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } return obj; } @@ -521,10 +567,11 @@ static ngx_int_t ngx_qjs_request_ctor(JSContext *cx, ngx_js_request_t *request, ngx_url_t *u, int argc, JSValueConst *argv) { - JSValue input, init, value; - ngx_int_t rc; - ngx_pool_t *pool; - ngx_js_request_t *orig; + JSValue input, init, value; + ngx_int_t rc; + ngx_pool_t *pool; + ngx_js_request_t *orig; + ngx_qjs_fetch_object_t *object; input = argv[0]; if (JS_IsUndefined(input)) { @@ -549,8 +596,6 @@ ngx_qjs_request_ctor(JSContext *cx, ngx_js_request_t *request, request->body.data = NULL; request->body.len = 0; request->headers.guard = GUARD_REQUEST; - ngx_qjs_arg(request->header_value) = JS_UNDEFINED; - pool = ngx_qjs_external_pool(cx, JS_GetContextOpaque(cx)); rc = ngx_list_init(&request->headers.header_list, pool, 4, @@ -571,13 +616,15 @@ ngx_qjs_request_ctor(JSContext *cx, ngx_js_request_t *request, } } else { - orig = JS_GetOpaque(input, NGX_QJS_CLASS_ID_FETCH_REQUEST); - if (orig == NULL) { + object = JS_GetOpaque(input, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (object == NULL) { JS_ThrowTypeError(cx, "input is not string or a Request object"); return NGX_ERROR; } + orig = object->data; + request->url = orig->url; request->method = orig->method; request->body = orig->body; @@ -781,8 +828,6 @@ ngx_qjs_fetch_response_ctor(JSContext *cx, JSValueConst new_target, int argc, response->code = 200; response->headers.guard = GUARD_RESPONSE; - ngx_qjs_arg(response->header_value) = JS_UNDEFINED; - ret = ngx_list_init(&response->headers.header_list, pool, 4, sizeof(ngx_js_tb_elt_t)); if (ret != NGX_OK) { @@ -926,7 +971,10 @@ string: return JS_EXCEPTION; } - JS_SetOpaque(obj, response); + if (ngx_qjs_fetch_object_set(cx, obj, response) != NGX_OK) { + JS_FreeValue(cx, obj); + return JS_EXCEPTION; + } return obj; } @@ -1194,8 +1242,6 @@ ngx_qjs_fetch_alloc(JSContext *cx, ngx_pool_t *pool, ngx_log_t *log, http->keepalive = (conf->fetch_keepalive > 0 && !ngx_js_conf_dynamic_proxy(conf)); - ngx_qjs_arg(http->response.header_value) = JS_UNDEFINED; - http->append_headers = ngx_qjs_fetch_append_headers; http->ready_handler = ngx_qjs_fetch_process_done; http->error_handler = ngx_qjs_fetch_error; @@ -1354,7 +1400,15 @@ ngx_qjs_fetch_process_done(ngx_js_http_t *http) return; } - JS_SetOpaque(fetch->response_value, &http->response); + if (ngx_qjs_fetch_object_set(fetch->cx, fetch->response_value, + &http->response) + != NGX_OK) + { + JS_FreeValue(fetch->cx, fetch->response_value); + fetch->response_value = JS_UNDEFINED; + ngx_qjs_fetch_error(http, "fetch response creation failed"); + return; + } ngx_qjs_fetch_done(fetch, fetch->response_value, NGX_OK); } @@ -1933,7 +1987,8 @@ ngx_qjs_ext_fetch_request_body(JSContext *cx, JSValueConst this_val, JSValue result; ngx_js_request_t *request; - request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + request = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_REQUEST); if (request == NULL) { return JS_UNDEFINED; } @@ -1998,7 +2053,8 @@ ngx_qjs_ext_fetch_request_body_used(JSContext *cx, JSValueConst this_val) { ngx_js_request_t *request; - request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + request = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_REQUEST); if (request == NULL) { return JS_UNDEFINED; } @@ -2012,7 +2068,8 @@ ngx_qjs_ext_fetch_request_cache(JSContext *cx, JSValueConst this_val) { ngx_js_request_t *request; - request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + request = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_REQUEST); if (request == NULL) { return JS_UNDEFINED; } @@ -2027,7 +2084,8 @@ ngx_qjs_ext_fetch_request_credentials(JSContext *cx, JSValueConst this_val) { ngx_js_request_t *request; - request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + request = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_REQUEST); if (request == NULL) { return JS_UNDEFINED; } @@ -2040,15 +2098,17 @@ ngx_qjs_ext_fetch_request_credentials(JSContext *cx, JSValueConst this_val) static JSValue ngx_qjs_ext_fetch_request_headers(JSContext *cx, JSValueConst this_val) { - JSValue header; - ngx_js_request_t *request; + JSValue header; + ngx_js_request_t *request; + ngx_qjs_fetch_object_t *object; - request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); - if (request == NULL) { + object = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (object == NULL) { return JS_UNDEFINED; } - header = ngx_qjs_arg(request->header_value); + request = object->data; + header = object->header_value; if (JS_IsUndefined(header)) { header = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_FETCH_HEADERS); @@ -2058,7 +2118,7 @@ ngx_qjs_ext_fetch_request_headers(JSContext *cx, JSValueConst this_val) JS_SetOpaque(header, &request->headers); - ngx_qjs_arg(request->header_value) = header; + object->header_value = header; } return JS_DupValue(cx, header); @@ -2071,7 +2131,8 @@ ngx_qjs_ext_fetch_request_field(JSContext *cx, JSValueConst this_val, int magic) ngx_str_t *field; ngx_js_request_t *request; - request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + request = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_REQUEST); if (request == NULL) { return JS_UNDEFINED; } @@ -2087,7 +2148,8 @@ ngx_qjs_ext_fetch_request_mode(JSContext *cx, JSValueConst this_val) { ngx_js_request_t *request; - request = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + request = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_REQUEST); if (request == NULL) { return JS_UNDEFINED; } @@ -2096,14 +2158,31 @@ ngx_qjs_ext_fetch_request_mode(JSContext *cx, JSValueConst this_val) } +static void +ngx_qjs_fetch_request_mark(JSRuntime *rt, JSValueConst val, + JS_MarkFunc *mark_func) +{ + ngx_qjs_fetch_object_t *object; + + object = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (object != NULL) { + JS_MarkValue(rt, object->header_value, mark_func); + } +} + + static void ngx_qjs_fetch_request_finalizer(JSRuntime *rt, JSValue val) { - ngx_js_request_t *request; + ngx_qjs_fetch_object_t *object; - request = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + object = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_REQUEST); + if (object == NULL) { + return; + } - JS_FreeValueRT(rt, ngx_qjs_arg(request->header_value)); + JS_FreeValueRT(rt, object->header_value); + js_free_rt(rt, object); } @@ -2112,7 +2191,8 @@ ngx_qjs_ext_fetch_response_status(JSContext *cx, JSValueConst this_val) { ngx_js_response_t *response; - response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + response = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_RESPONSE); if (response == NULL) { return JS_UNDEFINED; } @@ -2126,7 +2206,8 @@ ngx_qjs_ext_fetch_response_status_text(JSContext *cx, JSValueConst this_val) { ngx_js_response_t *response; - response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + response = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_RESPONSE); if (response == NULL) { return JS_UNDEFINED; } @@ -2141,7 +2222,8 @@ ngx_qjs_ext_fetch_response_ok(JSContext *cx, JSValueConst this_val) { ngx_js_response_t *response; - response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + response = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_RESPONSE); if (response == NULL) { return JS_UNDEFINED; } @@ -2155,7 +2237,8 @@ ngx_qjs_ext_fetch_response_body_used(JSContext *cx, JSValueConst this_val) { ngx_js_response_t *response; - response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + response = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_RESPONSE); if (response == NULL) { return JS_UNDEFINED; } @@ -2167,15 +2250,17 @@ ngx_qjs_ext_fetch_response_body_used(JSContext *cx, JSValueConst this_val) static JSValue ngx_qjs_ext_fetch_response_headers(JSContext *cx, JSValueConst this_val) { - JSValue header; - ngx_js_response_t *response; + JSValue header; + ngx_js_response_t *response; + ngx_qjs_fetch_object_t *object; - response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); - if (response == NULL) { + object = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (object == NULL) { return JS_UNDEFINED; } - header = ngx_qjs_arg(response->header_value); + response = object->data; + header = object->header_value; if (JS_IsUndefined(header)) { header = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_FETCH_HEADERS); @@ -2185,7 +2270,7 @@ ngx_qjs_ext_fetch_response_headers(JSContext *cx, JSValueConst this_val) JS_SetOpaque(header, &response->headers); - ngx_qjs_arg(response->header_value) = header; + object->header_value = header; } return JS_DupValue(cx, header); @@ -2197,7 +2282,8 @@ ngx_qjs_ext_fetch_response_type(JSContext *cx, JSValueConst this_val) { ngx_js_response_t *response; - response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + response = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_RESPONSE); if (response == NULL) { return JS_UNDEFINED; } @@ -2215,7 +2301,8 @@ ngx_qjs_ext_fetch_response_body(JSContext *cx, JSValueConst this_val, njs_str_t string; ngx_js_response_t *response; - response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + response = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_RESPONSE); if (response == NULL) { return JS_UNDEFINED; } @@ -2281,7 +2368,8 @@ ngx_qjs_ext_fetch_response_redirected(JSContext *cx, JSValueConst this_val) { ngx_js_response_t *response; - response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + response = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_RESPONSE); if (response == NULL) { return JS_UNDEFINED; } @@ -2296,7 +2384,8 @@ ngx_qjs_ext_fetch_response_field(JSContext *cx, JSValueConst this_val, int magic ngx_str_t *field; ngx_js_response_t *response; - response = JS_GetOpaque2(cx, this_val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + response = ngx_qjs_fetch_object_data(cx, this_val, + NGX_QJS_CLASS_ID_FETCH_RESPONSE); if (response == NULL) { return JS_UNDEFINED; } @@ -2307,15 +2396,31 @@ ngx_qjs_ext_fetch_response_field(JSContext *cx, JSValueConst this_val, int magic } +static void +ngx_qjs_fetch_response_mark(JSRuntime *rt, JSValueConst val, + JS_MarkFunc *mark_func) +{ + ngx_qjs_fetch_object_t *object; + + object = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (object != NULL) { + JS_MarkValue(rt, object->header_value, mark_func); + } +} + + static void ngx_qjs_fetch_response_finalizer(JSRuntime *rt, JSValue val) { - ngx_js_response_t *response; + ngx_qjs_fetch_object_t *object; - response = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + object = JS_GetOpaque(val, NGX_QJS_CLASS_ID_FETCH_RESPONSE); + if (object == NULL) { + return; + } - JS_FreeValueRT(rt, ngx_qjs_arg(response->header_value)); - njs_chb_destroy(&response->chain); + JS_FreeValueRT(rt, object->header_value); + js_free_rt(rt, object); } diff --git a/nginx/t/js_fetch_object_lifetime.t b/nginx/t/js_fetch_object_lifetime.t new file mode 100644 index 00000000..c1d4fb22 --- /dev/null +++ b/nginx/t/js_fetch_object_lifetime.t @@ -0,0 +1,158 @@ +#!/usr/bin/perl + +# (C) Dmitry Volyntsev +# (C) F5, Inc. + +# Tests for QuickJS Fetch object finalization after pool destruction. + +############################################################################### + +use warnings; +use strict; + +use Test::More; + +BEGIN { use FindBin; chdir($FindBin::Bin); } + +use lib 'lib'; +use Test::Nginx; + +############################################################################### + +select STDERR; $| = 1; +select STDOUT; $| = 1; + +my $t = Test::Nginx->new()->has(qw/http rewrite/) + ->write_file_expand('nginx.conf', <<'EOF'); + +%%TEST_GLOBALS%% + +daemon off; +worker_processes 1; + +events { +} + +http { + %%TEST_GLOBALS_HTTP%% + + js_engine qjs; + js_import test.js; + + server { + listen 127.0.0.1:8080; + server_name localhost; + + location /reuse { + js_context_reuse 1; + js_content test.reuse; + } + + location /no_reuse { + js_context_reuse 0; + js_content test.no_reuse; + } + + location /target { + add_header X-Test preserved; + return 200 body; + } + } +} + +EOF + +my $p0 = port(8080); + +$t->write_file('test.js', <try_run('no QuickJS support')->plan(11); + +############################################################################### + +like(http_get('/reuse?op=request'), qr/request$/s, 'retain Request'); +like(http_get('/reuse?op=release'), qr/200 OK/, 'release Request'); + +like(http_get('/reuse?op=response'), qr/response$/s, 'retain Response'); +like(http_get('/reuse?op=release'), qr/200 OK/, 'release Response'); + +like(http_get('/reuse?op=fetch'), qr/preserved$/s, 'retain fetch Response'); +like(http_get('/reuse?op=release'), qr/200 OK/, 'release fetch Response'); + +like(http_get('/no_reuse?type=request'), qr/200 OK/, 'Request cycle'); +like(http_get('/no_reuse?type=response'), qr/200 OK/, 'Response cycle'); +like(http_get('/no_reuse?type=fetch'), qr/200 OK/, 'fetch Response cycle'); +like(http_get('/no_reuse?type=request_headers'), qr/200 OK/, + 'Request Headers cycle'); +like(http_get('/no_reuse?type=response_headers'), qr/200 OK/, + 'Response Headers cycle'); + +###############################################################################