From: Anil Dongare <[email protected]> Upstream Repository: https://github.com/nodejs/node.git
Bug Details: https://nvd.nist.gov/vuln/detail/CVE-2025-59466 Type: Security Fix CVE: CVE-2025-59466 Score: 7.5 Patch: https://github.com/nodejs/node/commit/ddadc31f09af Signed-off-by: Anil Dongare <[email protected]> --- .../nodejs/nodejs/CVE-2025-59466.patch | 508 ++++++++++++++++++ .../recipes-devtools/nodejs/nodejs_20.18.2.bb | 1 + 2 files changed, 509 insertions(+) create mode 100644 meta-oe/recipes-devtools/nodejs/nodejs/CVE-2025-59466.patch diff --git a/meta-oe/recipes-devtools/nodejs/nodejs/CVE-2025-59466.patch b/meta-oe/recipes-devtools/nodejs/nodejs/CVE-2025-59466.patch new file mode 100644 index 0000000000..6121432697 --- /dev/null +++ b/meta-oe/recipes-devtools/nodejs/nodejs/CVE-2025-59466.patch @@ -0,0 +1,508 @@ +From c5cad9d8ec0bccc8136903823c079da7cc6b1930 Mon Sep 17 00:00:00 2001 +From: Matteo Collina <[email protected]> +Date: Tue, 9 Dec 2025 23:50:18 +0100 +Subject: [PATCH 3/6] src: rethrow stack overflow exceptions in async_hooks + +When a stack overflow exception occurs during async_hooks callbacks +(which use TryCatchScope::kFatal), detect the specific "Maximum call +stack size exceeded" RangeError and re-throw it instead of immediately +calling FatalException. This allows user code to catch the exception +with try-catch blocks instead of requiring uncaughtException handlers. + +The implementation adds IsStackOverflowError() helper to detect stack +overflow RangeErrors and re-throws them in TryCatchScope destructor +instead of calling FatalException. + +This fixes the issue where async_hooks would cause stack overflow +exceptions to exit with code 7 (kExceptionInFatalExceptionHandler) +instead of being catchable. + +Fixes: https://github.com/nodejs/node/issues/37989 +Ref: https://hackerone.com/reports/3456295 +PR-URL: https://github.com/nodejs-private/node-private/pull/773 +Refs: https://hackerone.com/reports/3456295 +Reviewed-By: Robert Nagy <[email protected]> +Reviewed-By: Paolo Insogna <[email protected]> +Reviewed-By: Marco Ippolito <[email protected]> +Reviewed-By: Rafael Gonzaga <[email protected]> +Reviewed-By: Anna Henningsen <[email protected]> +CVE-ID: CVE-2025-59466 + +CVE: CVE-2025-59466 +Upstream-Status: Backport [https://github.com/nodejs/node/commit/ddadc31f09af] + +(cherry picked from commit ddadc31f09afdbab7545c86cc0f17d137beb8048) +Signed-off-by: Anil Dongare <[email protected]> +--- + src/async_wrap.cc | 9 ++- + src/debug_utils.cc | 3 +- + src/node_errors.cc | 71 ++++++++++++++-- + src/node_errors.h | 2 +- + src/node_report.cc | 3 +- + ...async-hooks-stack-overflow-nested-async.js | 80 +++++++++++++++++++ + ...st-async-hooks-stack-overflow-try-catch.js | 47 +++++++++++ + .../test-async-hooks-stack-overflow.js | 47 +++++++++++ + ...andler-stack-overflow-on-stack-overflow.js | 29 +++++++ + ...caught-exception-handler-stack-overflow.js | 29 +++++++ + 10 files changed, 306 insertions(+), 14 deletions(-) + create mode 100644 test/parallel/test-async-hooks-stack-overflow-nested-async.js + create mode 100644 test/parallel/test-async-hooks-stack-overflow-try-catch.js + create mode 100644 test/parallel/test-async-hooks-stack-overflow.js + create mode 100644 test/parallel/test-uncaught-exception-handler-stack-overflow-on-stack-overflow.js + create mode 100644 test/parallel/test-uncaught-exception-handler-stack-overflow.js + +diff --git a/src/async_wrap.cc b/src/async_wrap.cc +index 65829a31a36..2b6fa142385 100644 +--- a/src/async_wrap.cc ++++ b/src/async_wrap.cc +@@ -67,7 +67,8 @@ static const char* const provider_names[] = { + void AsyncWrap::DestroyAsyncIdsCallback(Environment* env) { + Local<Function> fn = env->async_hooks_destroy_function(); + +- TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal); ++ TryCatchScope try_catch(env, ++ TryCatchScope::CatchMode::kFatalRethrowStackOverflow); + + do { + std::vector<double> destroy_async_id_list; +@@ -96,7 +97,8 @@ void Emit(Environment* env, double async_id, AsyncHooks::Fields type, + + HandleScope handle_scope(env->isolate()); + Local<Value> async_id_value = Number::New(env->isolate(), async_id); +- TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal); ++ TryCatchScope try_catch(env, ++ TryCatchScope::CatchMode::kFatalRethrowStackOverflow); + USE(fn->Call(env->context(), Undefined(env->isolate()), 1, &async_id_value)); + } + +@@ -668,7 +670,8 @@ void AsyncWrap::EmitAsyncInit(Environment* env, + object, + }; + +- TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal); ++ TryCatchScope try_catch(env, ++ TryCatchScope::CatchMode::kFatalRethrowStackOverflow); + USE(init_fn->Call(env->context(), object, arraysize(argv), argv)); + } + +diff --git a/src/debug_utils.cc b/src/debug_utils.cc +index 9d36082db67..7d85ab72615 100644 +--- a/src/debug_utils.cc ++++ b/src/debug_utils.cc +@@ -333,7 +333,8 @@ void DumpJavaScriptBacktrace(FILE* fp) { + } + + Local<StackTrace> stack; +- if (!GetCurrentStackTrace(isolate).ToLocal(&stack)) { ++ if (!GetCurrentStackTrace(isolate).ToLocal(&stack) || ++ stack->GetFrameCount() == 0) { + return; + } + +diff --git a/src/node_errors.cc b/src/node_errors.cc +index 69e474257b0..f984fd4c3d9 100644 +--- a/src/node_errors.cc ++++ b/src/node_errors.cc +@@ -188,7 +188,7 @@ static std::string GetErrorSource(Isolate* isolate, + } + + static std::atomic<bool> is_in_oom{false}; +-static std::atomic<bool> is_retrieving_js_stacktrace{false}; ++static thread_local std::atomic<bool> is_retrieving_js_stacktrace{false}; + MaybeLocal<StackTrace> GetCurrentStackTrace(Isolate* isolate, int frame_count) { + if (isolate == nullptr) { + return MaybeLocal<StackTrace>(); +@@ -216,9 +216,6 @@ MaybeLocal<StackTrace> GetCurrentStackTrace(Isolate* isolate, int frame_count) { + StackTrace::CurrentStackTrace(isolate, frame_count, options); + + is_retrieving_js_stacktrace.store(false); +- if (stack->GetFrameCount() == 0) { +- return MaybeLocal<StackTrace>(); +- } + + return scope.Escape(stack); + } +@@ -293,7 +290,8 @@ void PrintStackTrace(Isolate* isolate, + + void PrintCurrentStackTrace(Isolate* isolate, StackTracePrefix prefix) { + Local<StackTrace> stack; +- if (GetCurrentStackTrace(isolate).ToLocal(&stack)) { ++ if (GetCurrentStackTrace(isolate).ToLocal(&stack) && ++ stack->GetFrameCount() > 0) { + PrintStackTrace(isolate, stack, prefix); + } + } +@@ -664,13 +662,52 @@ v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings( + }; + } + ++// Check if an exception is a stack overflow error (RangeError with ++// "Maximum call stack size exceeded" message). This is used to handle ++// stack overflow specially in TryCatchScope - instead of immediately ++// exiting, we can use the red zone to re-throw to user code. ++static bool IsStackOverflowError(Isolate* isolate, Local<Value> exception) { ++ if (!exception->IsNativeError()) return false; ++ ++ Local<Object> err_obj = exception.As<Object>(); ++ Local<String> constructor_name = err_obj->GetConstructorName(); ++ ++ // Must be a RangeError ++ Utf8Value name(isolate, constructor_name); ++ if (name.ToStringView() != "RangeError") return false; ++ ++ // Check for the specific stack overflow message ++ Local<Context> context = isolate->GetCurrentContext(); ++ Local<Value> message_val; ++ if (!err_obj->Get(context, String::NewFromUtf8Literal(isolate, "message")) ++ .ToLocal(&message_val)) { ++ return false; ++ } ++ ++ if (!message_val->IsString()) return false; ++ ++ Utf8Value message(isolate, message_val.As<String>()); ++ return message.ToStringView() == "Maximum call stack size exceeded"; ++} ++ + namespace errors { + + TryCatchScope::~TryCatchScope() { +- if (HasCaught() && !HasTerminated() && mode_ == CatchMode::kFatal) { ++ if (HasCaught() && !HasTerminated() && mode_ != CatchMode::kNormal) { + HandleScope scope(env_->isolate()); + Local<v8::Value> exception = Exception(); + Local<v8::Message> message = Message(); ++ ++ // Special handling for stack overflow errors in async_hooks: instead of ++ // immediately exiting, re-throw the exception. This allows the exception ++ // to propagate to user code's try-catch blocks. ++ if (mode_ == CatchMode::kFatalRethrowStackOverflow && ++ IsStackOverflowError(env_->isolate(), exception)) { ++ ReThrow(); ++ Reset(); ++ return; ++ } ++ + EnhanceFatalException enhance = CanContinue() ? + EnhanceFatalException::kEnhance : EnhanceFatalException::kDontEnhance; + if (message.IsEmpty()) +@@ -1221,8 +1258,26 @@ void TriggerUncaughtException(Isolate* isolate, + if (env->can_call_into_js()) { + // We do not expect the global uncaught exception itself to throw any more + // exceptions. If it does, exit the current Node.js instance. +- errors::TryCatchScope try_catch(env, +- errors::TryCatchScope::CatchMode::kFatal); ++ // Special case: if the original error was a stack overflow and calling ++ // _fatalException causes another stack overflow, rethrow it to allow ++ // user code's try-catch blocks to potentially catch it. ++ auto is_stack_overflow = [&] { ++ return IsStackOverflowError(env->isolate(), error); ++ }; ++ // Without a JS stack, rethrowing may or may not do anything. ++ // TODO(addaleax): In V8, expose a way to check whether there is a JS stack ++ // or TryCatch that would capture the rethrown exception. ++ auto has_js_stack = [&] { ++ HandleScope handle_scope(env->isolate()); ++ Local<StackTrace> stack; ++ return GetCurrentStackTrace(env->isolate(), 1).ToLocal(&stack) && ++ stack->GetFrameCount() > 0; ++ }; ++ errors::TryCatchScope::CatchMode mode = ++ is_stack_overflow() && has_js_stack() ++ ? errors::TryCatchScope::CatchMode::kFatalRethrowStackOverflow ++ : errors::TryCatchScope::CatchMode::kFatal; ++ errors::TryCatchScope try_catch(env, mode); + // Explicitly disable verbose exception reporting - + // if process._fatalException() throws an error, we don't want it to + // trigger the per-isolate message listener which will call this +diff --git a/src/node_errors.h b/src/node_errors.h +index ac07b96b5ca..0b60e521df1 100644 +--- a/src/node_errors.h ++++ b/src/node_errors.h +@@ -265,7 +265,7 @@ namespace errors { + + class TryCatchScope : public v8::TryCatch { + public: +- enum class CatchMode { kNormal, kFatal }; ++ enum class CatchMode { kNormal, kFatal, kFatalRethrowStackOverflow }; + + explicit TryCatchScope(Environment* env, CatchMode mode = CatchMode::kNormal) + : v8::TryCatch(env->isolate()), env_(env), mode_(mode) {} +diff --git a/src/node_report.cc b/src/node_report.cc +index 5368d8eef2f..f7ec2c6ed0e 100644 +--- a/src/node_report.cc ++++ b/src/node_report.cc +@@ -463,7 +463,8 @@ static void PrintJavaScriptStack(JSONWriter* writer, + const char* trigger) { + HandleScope scope(isolate); + Local<v8::StackTrace> stack; +- if (!GetCurrentStackTrace(isolate, MAX_FRAME_COUNT).ToLocal(&stack)) { ++ if (!GetCurrentStackTrace(isolate, MAX_FRAME_COUNT).ToLocal(&stack) || ++ stack->GetFrameCount() == 0) { + PrintEmptyJavaScriptStack(writer); + return; + } +diff --git a/test/parallel/test-async-hooks-stack-overflow-nested-async.js b/test/parallel/test-async-hooks-stack-overflow-nested-async.js +new file mode 100644 +index 00000000000..779f8d75ae2 +--- /dev/null ++++ b/test/parallel/test-async-hooks-stack-overflow-nested-async.js +@@ -0,0 +1,80 @@ ++'use strict'; ++ ++// This test verifies that stack overflow during deeply nested async operations ++// with async_hooks enabled can be caught by try-catch. This simulates real-world ++// scenarios like processing deeply nested JSON structures where each level ++// creates async operations (e.g., database calls, API requests). ++ ++require('../common'); ++const assert = require('assert'); ++const { spawnSync } = require('child_process'); ++ ++if (process.argv[2] === 'child') { ++ const { createHook } = require('async_hooks'); ++ ++ // Enable async_hooks with all callbacks (simulates APM tools) ++ createHook({ ++ init() {}, ++ before() {}, ++ after() {}, ++ destroy() {}, ++ promiseResolve() {}, ++ }).enable(); ++ ++ // Simulate an async operation (like a database call or API request) ++ async function fetchThing(id) { ++ return { id, data: `data-${id}` }; ++ } ++ ++ // Recursively process deeply nested data structure ++ // This will cause stack overflow when the nesting is deep enough ++ function processData(data, depth = 0) { ++ if (Array.isArray(data)) { ++ for (const item of data) { ++ // Create a promise to trigger async_hooks init callback ++ fetchThing(depth); ++ processData(item, depth + 1); ++ } ++ } ++ } ++ ++ // Create deeply nested array structure iteratively (to avoid stack overflow ++ // during creation) ++ function createNestedArray(depth) { ++ let result = 'leaf'; ++ for (let i = 0; i < depth; i++) { ++ result = [result]; ++ } ++ return result; ++ } ++ ++ // Create a very deep nesting that will cause stack overflow during processing ++ const deeplyNested = createNestedArray(50000); ++ ++ try { ++ processData(deeplyNested); ++ // Should not complete successfully - the nesting is too deep ++ console.log('UNEXPECTED: Processing completed without error'); ++ process.exit(1); ++ } catch (err) { ++ assert.strictEqual(err.name, 'RangeError'); ++ assert.match(err.message, /Maximum call stack size exceeded/); ++ console.log('SUCCESS: try-catch caught the stack overflow in nested async'); ++ process.exit(0); ++ } ++} else { ++ // Parent process - spawn the child and check exit code ++ const result = spawnSync( ++ process.execPath, ++ [__filename, 'child'], ++ { encoding: 'utf8', timeout: 30000 } ++ ); ++ ++ // Should exit successfully (try-catch worked) ++ assert.strictEqual(result.status, 0, ++ `Expected exit code 0, got ${result.status}.\n` + ++ `stdout: ${result.stdout}\n` + ++ `stderr: ${result.stderr}`); ++ // Verify the error was handled by try-catch ++ assert.match(result.stdout, /SUCCESS: try-catch caught the stack overflow/); ++} +diff --git a/test/parallel/test-async-hooks-stack-overflow-try-catch.js b/test/parallel/test-async-hooks-stack-overflow-try-catch.js +new file mode 100644 +index 00000000000..43338905e78 +--- /dev/null ++++ b/test/parallel/test-async-hooks-stack-overflow-try-catch.js +@@ -0,0 +1,47 @@ ++'use strict'; ++ ++// This test verifies that when a stack overflow occurs with async_hooks ++// enabled, the exception can be caught by try-catch blocks in user code. ++ ++require('../common'); ++const assert = require('assert'); ++const { spawnSync } = require('child_process'); ++ ++if (process.argv[2] === 'child') { ++ const { createHook } = require('async_hooks'); ++ ++ createHook({ init() {} }).enable(); ++ ++ function recursive(depth = 0) { ++ // Create a promise to trigger async_hooks init callback ++ new Promise(() => {}); ++ return recursive(depth + 1); ++ } ++ ++ try { ++ recursive(); ++ // Should not reach here ++ process.exit(1); ++ } catch (err) { ++ assert.strictEqual(err.name, 'RangeError'); ++ assert.match(err.message, /Maximum call stack size exceeded/); ++ console.log('SUCCESS: try-catch caught the stack overflow'); ++ process.exit(0); ++ } ++ ++ // Should not reach here ++ process.exit(2); ++} else { ++ // Parent process - spawn the child and check exit code ++ const result = spawnSync( ++ process.execPath, ++ [__filename, 'child'], ++ { encoding: 'utf8', timeout: 30000 } ++ ); ++ ++ assert.strictEqual(result.status, 0, ++ `Expected exit code 0 (try-catch worked), got ${result.status}.\n` + ++ `stdout: ${result.stdout}\n` + ++ `stderr: ${result.stderr}`); ++ assert.match(result.stdout, /SUCCESS: try-catch caught the stack overflow/); ++} +diff --git a/test/parallel/test-async-hooks-stack-overflow.js b/test/parallel/test-async-hooks-stack-overflow.js +new file mode 100644 +index 00000000000..aff41969dbd +--- /dev/null ++++ b/test/parallel/test-async-hooks-stack-overflow.js +@@ -0,0 +1,47 @@ ++'use strict'; ++ ++// This test verifies that when a stack overflow occurs with async_hooks ++// enabled, the uncaughtException handler is still called instead of the ++// process crashing with exit code 7. ++ ++const common = require('../common'); ++const assert = require('assert'); ++const { spawnSync } = require('child_process'); ++ ++if (process.argv[2] === 'child') { ++ const { createHook } = require('async_hooks'); ++ ++ let handlerCalled = false; ++ ++ function recursive() { ++ // Create a promise to trigger async_hooks init callback ++ new Promise(() => {}); ++ return recursive(); ++ } ++ ++ createHook({ init() {} }).enable(); ++ ++ process.on('uncaughtException', common.mustCall((err) => { ++ assert.strictEqual(err.name, 'RangeError'); ++ assert.match(err.message, /Maximum call stack size exceeded/); ++ // Ensure handler is only called once ++ assert.strictEqual(handlerCalled, false); ++ handlerCalled = true; ++ })); ++ ++ setImmediate(recursive); ++} else { ++ // Parent process - spawn the child and check exit code ++ const result = spawnSync( ++ process.execPath, ++ [__filename, 'child'], ++ { encoding: 'utf8', timeout: 30000 } ++ ); ++ ++ // Should exit with code 0 (handler was called and handled the exception) ++ // Previously would exit with code 7 (kExceptionInFatalExceptionHandler) ++ assert.strictEqual(result.status, 0, ++ `Expected exit code 0, got ${result.status}.\n` + ++ `stdout: ${result.stdout}\n` + ++ `stderr: ${result.stderr}`); ++} +diff --git a/test/parallel/test-uncaught-exception-handler-stack-overflow-on-stack-overflow.js b/test/parallel/test-uncaught-exception-handler-stack-overflow-on-stack-overflow.js +new file mode 100644 +index 00000000000..1923b7f24d9 +--- /dev/null ++++ b/test/parallel/test-uncaught-exception-handler-stack-overflow-on-stack-overflow.js +@@ -0,0 +1,29 @@ ++'use strict'; ++ ++// This test verifies that when the uncaughtException handler itself causes ++// a stack overflow, the process exits with a non-zero exit code. ++// This is important to ensure we don't silently swallow errors. ++ ++require('../common'); ++const assert = require('assert'); ++const { spawnSync } = require('child_process'); ++ ++if (process.argv[2] === 'child') { ++ function f() { f(); } ++ process.on('uncaughtException', f); ++ f(); ++} else { ++ // Parent process - spawn the child and check exit code ++ const result = spawnSync( ++ process.execPath, ++ [__filename, 'child'], ++ { encoding: 'utf8', timeout: 30000 } ++ ); ++ ++ // Should exit with non-zero exit code since the uncaughtException handler ++ // itself caused a stack overflow. ++ assert.notStrictEqual(result.status, 0, ++ `Expected non-zero exit code, got ${result.status}.\n` + ++ `stdout: ${result.stdout}\n` + ++ `stderr: ${result.stderr}`); ++} +diff --git a/test/parallel/test-uncaught-exception-handler-stack-overflow.js b/test/parallel/test-uncaught-exception-handler-stack-overflow.js +new file mode 100644 +index 00000000000..050cd0923ee +--- /dev/null ++++ b/test/parallel/test-uncaught-exception-handler-stack-overflow.js +@@ -0,0 +1,29 @@ ++'use strict'; ++ ++// This test verifies that when the uncaughtException handler itself causes ++// a stack overflow, the process exits with a non-zero exit code. ++// This is important to ensure we don't silently swallow errors. ++ ++require('../common'); ++const assert = require('assert'); ++const { spawnSync } = require('child_process'); ++ ++if (process.argv[2] === 'child') { ++ function f() { f(); } ++ process.on('uncaughtException', f); ++ throw new Error('X'); ++} else { ++ // Parent process - spawn the child and check exit code ++ const result = spawnSync( ++ process.execPath, ++ [__filename, 'child'], ++ { encoding: 'utf8', timeout: 30000 } ++ ); ++ ++ // Should exit with non-zero exit code since the uncaughtException handler ++ // itself caused a stack overflow. ++ assert.notStrictEqual(result.status, 0, ++ `Expected non-zero exit code, got ${result.status}.\n` + ++ `stdout: ${result.stdout}\n` + ++ `stderr: ${result.stderr}`); ++} +-- +2.43.7 diff --git a/meta-oe/recipes-devtools/nodejs/nodejs_20.18.2.bb b/meta-oe/recipes-devtools/nodejs/nodejs_20.18.2.bb index 7c97c7282c..779c70dbd0 100644 --- a/meta-oe/recipes-devtools/nodejs/nodejs_20.18.2.bb +++ b/meta-oe/recipes-devtools/nodejs/nodejs_20.18.2.bb @@ -31,6 +31,7 @@ SRC_URI = "http://nodejs.org/dist/v${PV}/node-v${PV}.tar.xz \ file://run-ptest \ file://CVE-2025-55132.patch \ file://CVE-2025-55130.patch \ + file://CVE-2025-59466.patch \ " SRC_URI:append:class-target = " \ file://0001-Using-native-binaries.patch \ -- 2.44.1
-=-=-=-=-=-=-=-=-=-=-=- Links: You receive all messages sent to this group. View/Reply Online (#124350): https://lists.openembedded.org/g/openembedded-devel/message/124350 Mute This Topic: https://lists.openembedded.org/mt/117772147/21656 Group Owner: [email protected] Unsubscribe: https://lists.openembedded.org/g/openembedded-devel/unsub [[email protected]] -=-=-=-=-=-=-=-=-=-=-=-
