node/test/parallel/test-async-hooks-stack-overflow-nested-async.js
Matteo Collina 73747597e0
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 <ronagy@icloud.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
Reviewed-By: Anna Henningsen <anna@addaleax.net>
CVE-ID: CVE-2025-59466
2026-01-13 17:02:27 +01:00

80 lines
2.6 KiB
JavaScript

'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/);
}