From 82697e083439d3f7d662a03c85184ba0a5696161 Mon Sep 17 00:00:00 2001 From: Dmitry Volyntsev Date: Thu, 24 Sep 2026 22:07:14 -0700 Subject: [PATCH] Shell: fix unhandled rejection report in QuickJS Previously, njs_engine_qjs_unhandled_rejection() kept a pointer to the rejected promise list while converting the first rejection reason to a string. The conversion may call user JavaScript code, which can reject more promises and cause the list to be reallocated. After that, the old pointer was used together with the new item count to free the promise and reason values, which caused out-of-bounds reads and invalid frees. The fix is to take a reference to the first reason and free the list before conversion, so that rejections created during conversion are tracked in a new list. The test/js/promise_rejection_tracker_recursive.t.js test is no longer skipped for the QuickJS engine. --- auto/make | 3 +- external/njs_shell.c | 66 +++++++++++++++++++++++++++----------------- test/shell_test.exp | 9 ++++++ 3 files changed, 50 insertions(+), 28 deletions(-) diff --git a/auto/make b/auto/make index 113ccbf8..1c62b3df 100644 --- a/auto/make +++ b/auto/make @@ -372,8 +372,7 @@ if [ $NJS_HAVE_QUICKJS = YES ]; then js_test: js_test_njs js_test_quickjs js_test_quickjs: njs - NJS_SKIP_LIST="test/js/promise_rejection_tracker_recursive.t.js \\ -test/js/async_exception_in_await.t.js" \\ + NJS_SKIP_LIST="test/js/async_exception_in_await.t.js" \\ test/js_test --binary='$NJS_BUILD_DIR/njs -n QuickJS -m' END diff --git a/external/njs_shell.c b/external/njs_shell.c index 276a2964..126ca9db 100644 --- a/external/njs_shell.c +++ b/external/njs_shell.c @@ -2787,19 +2787,35 @@ done: } -static njs_int_t -njs_engine_qjs_destroy(njs_engine_t *engine) +static void +njs_qjs_rejected_promises_free(JSContext *ctx, njs_arr_t *promises) { uint32_t i; - njs_ev_t *ev; - njs_int_t ret; - JSContext *cx; - njs_queue_t *events; - njs_console_t *console; - njs_262agent_t *agent; - njs_queue_link_t *link; njs_rejected_promise_t *rejected_promise; + rejected_promise = promises->start; + + for (i = 0; i < promises->items; i++) { + JS_FreeValue(ctx, rejected_promise[i].u.qjs.promise); + JS_FreeValue(ctx, rejected_promise[i].u.qjs.message); + } + + njs_arr_destroy(promises); +} + + +static njs_int_t +njs_engine_qjs_destroy(njs_engine_t *engine) +{ + njs_ev_t *ev; + njs_int_t ret; + JSContext *cx; + njs_arr_t *promises; + njs_queue_t *events; + njs_console_t *console; + njs_262agent_t *agent; + njs_queue_link_t *link; + (void) qjs_call_exit_hook(engine->u.qjs.ctx); for ( ;; ) { @@ -2811,13 +2827,11 @@ njs_engine_qjs_destroy(njs_engine_t *engine) console = JS_GetRuntimeOpaque(engine->u.qjs.rt); - if (console->rejected_promises != NULL) { - rejected_promise = console->rejected_promises->start; + promises = console->rejected_promises; + console->rejected_promises = NULL; - for (i = 0; i < console->rejected_promises->items; i++) { - JS_FreeValue(engine->u.qjs.ctx, rejected_promise[i].u.qjs.promise); - JS_FreeValue(engine->u.qjs.ctx, rejected_promise[i].u.qjs.message); - } + if (promises != NULL) { + njs_qjs_rejected_promises_free(engine->u.qjs.ctx, promises); } events = &console->posted_events; @@ -2907,9 +2921,10 @@ static njs_int_t njs_engine_qjs_unhandled_rejection(njs_engine_t *engine) { size_t len; - uint32_t i; + JSValue reason; JSContext *ctx; const char *str; + njs_arr_t *promises; njs_console_t *console; njs_rejected_promise_t *rejected_promise; @@ -2922,9 +2937,16 @@ njs_engine_qjs_unhandled_rejection(njs_engine_t *engine) return 0; } - rejected_promise = console->rejected_promises->start; + promises = console->rejected_promises; + rejected_promise = promises->start; + reason = JS_DupValue(ctx, rejected_promise->u.qjs.message); + + console->rejected_promises = NULL; + njs_qjs_rejected_promises_free(ctx, promises); + + str = JS_ToCStringLen(ctx, &len, reason); + JS_FreeValue(ctx, reason); - str = JS_ToCStringLen(ctx, &len, rejected_promise->u.qjs.message); if (njs_slow_path(str == NULL)) { return -1; } @@ -2932,14 +2954,6 @@ njs_engine_qjs_unhandled_rejection(njs_engine_t *engine) JS_ThrowTypeError(ctx, "unhandled promise rejection: %.*s", (int) len, str); JS_FreeCString(ctx, str); - for (i = 0; i < console->rejected_promises->items; i++) { - JS_FreeValue(ctx, rejected_promise[i].u.qjs.promise); - JS_FreeValue(ctx, rejected_promise[i].u.qjs.message); - } - - njs_arr_destroy(console->rejected_promises); - console->rejected_promises = NULL; - return 1; } diff --git a/test/shell_test.exp b/test/shell_test.exp index f394070b..b56e1be4 100644 --- a/test/shell_test.exp +++ b/test/shell_test.exp @@ -408,6 +408,15 @@ njs_test { ">> " "unhandled promise rejection"} } +njs_test { + {"var later\r\n" "undefined\r\n>> "} + {"var message = {toString() { later.catch(() => {}); return 1 }}\r\n" + "undefined\r\n>> "} + {"Promise.reject(message); later = Promise.reject(2); undefined\r\n" + "unhandled promise rejection: 1\r\n*>> "} + {"42\r\n" "42\r\n>> " "unhandled promise rejection"} +} + # CLI OPTIONS # help -- 2.47.3