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]]
-=-=-=-=-=-=-=-=-=-=-=-

  • ... Anil Dongare -X (adongare - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org
    • ... Anil Dongare -X (adongare - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org
    • ... Anil Dongare -X (adongare - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org
    • ... Anil Dongare -X (adongare - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org
    • ... Anil Dongare -X (adongare - E INFOCHIPS PRIVATE LIMITED at Cisco) via lists.openembedded.org

Reply via email to