| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 26be208 commit 0578e3e
10 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -68,7 +68,8 @@ static const char* const provider_names[] = { | |||
| 68 | 68 | void AsyncWrap::DestroyAsyncIdsCallback(Environment* env) { | |
| 69 | 69 | Local<Function> fn = env->async_hooks_destroy_function(); | |
| 70 | 70 | ||
| 71 | - TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal); | ||
| 71 | + TryCatchScope try_catch(env, | ||
| 72 | + TryCatchScope::CatchMode::kFatalRethrowStackOverflow); | ||
| 72 | 73 | ||
| 73 | 74 | do { | |
| 74 | 75 | std::vector<double> destroy_async_id_list; | |
@@ -97,7 +98,8 @@ void Emit(Environment* env, double async_id, AsyncHooks::Fields type, | |||
| 97 | 98 | ||
| 98 | 99 | HandleScope handle_scope(env->isolate()); | |
| 99 | 100 | Local<Value> async_id_value = Number::New(env->isolate(), async_id); | |
| 100 | - TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal); | ||
| 101 | + TryCatchScope try_catch(env, | ||
| 102 | + TryCatchScope::CatchMode::kFatalRethrowStackOverflow); | ||
| 101 | 103 | USE(fn->Call(env->context(), Undefined(env->isolate()), 1, &async_id_value)); | |
| 102 | 104 | } | |
| 103 | 105 | ||
@@ -646,7 +648,8 @@ void AsyncWrap::EmitAsyncInit(Environment* env, | |||
| 646 | 648 | object, | |
| 647 | 649 | }; | |
| 648 | 650 | ||
| 649 | - TryCatchScope try_catch(env, TryCatchScope::CatchMode::kFatal); | ||
| 651 | + TryCatchScope try_catch(env, | ||
| 652 | + TryCatchScope::CatchMode::kFatalRethrowStackOverflow); | ||
| 650 | 653 | USE(init_fn->Call(env->context(), object, arraysize(argv), argv)); | |
| 651 | 654 | } | |
| 652 | 655 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -333,7 +333,8 @@ void DumpJavaScriptBacktrace(FILE* fp) { | |||
| 333 | 333 | } | |
| 334 | 334 | ||
| 335 | 335 | Local<StackTrace> stack; | |
| 336 | - if (!GetCurrentStackTrace(isolate).ToLocal(&stack)) { | ||
| 336 | + if (!GetCurrentStackTrace(isolate).ToLocal(&stack) || | ||
| 337 | + stack->GetFrameCount() == 0) { | ||
| 337 | 338 | return; | |
| 338 | 339 | } | |
| 339 | 340 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -194,7 +194,7 @@ static std::string GetErrorSource(Isolate* isolate, | |||
| 194 | 194 | } | |
| 195 | 195 | ||
| 196 | 196 | static std::atomic<bool> is_in_oom{false}; | |
| 197 | - static std::atomic<bool> is_retrieving_js_stacktrace{false}; | ||
| 197 | + static thread_local std::atomic<bool> is_retrieving_js_stacktrace{false}; | ||
| 198 | 198 | MaybeLocal<StackTrace> GetCurrentStackTrace(Isolate* isolate, int frame_count) { | |
| 199 | 199 | if (isolate == nullptr) { | |
| 200 | 200 | return MaybeLocal<StackTrace>(); | |
@@ -222,9 +222,6 @@ MaybeLocal<StackTrace> GetCurrentStackTrace(Isolate* isolate, int frame_count) { | |||
| 222 | 222 | StackTrace::CurrentStackTrace(isolate, frame_count, options); | |
| 223 | 223 | ||
| 224 | 224 | is_retrieving_js_stacktrace.store(false); | |
| 225 | - if (stack->GetFrameCount() == 0) { | ||
| 226 | - return MaybeLocal<StackTrace>(); | ||
| 227 | - } | ||
| 228 | 225 | ||
| 229 | 226 | return scope.Escape(stack); | |
| 230 | 227 | } | |
@@ -299,7 +296,8 @@ void PrintStackTrace(Isolate* isolate, | |||
| 299 | 296 | ||
| 300 | 297 | void PrintCurrentStackTrace(Isolate* isolate, StackTracePrefix prefix) { | |
| 301 | 298 | Local<StackTrace> stack; | |
| 302 | - if (GetCurrentStackTrace(isolate).ToLocal(&stack)) { | ||
| 299 | + if (GetCurrentStackTrace(isolate).ToLocal(&stack) && | ||
| 300 | + stack->GetFrameCount() > 0) { | ||
| 303 | 301 | PrintStackTrace(isolate, stack, prefix); | |
| 304 | 302 | } | |
| 305 | 303 | } | |
@@ -671,13 +669,52 @@ v8::ModifyCodeGenerationFromStringsResult ModifyCodeGenerationFromStrings( | |||
| 671 | 669 | }; | |
| 672 | 670 | } | |
| 673 | 671 | ||
| 672 | + // Check if an exception is a stack overflow error (RangeError with | ||
| 673 | + // "Maximum call stack size exceeded" message). This is used to handle | ||
| 674 | + // stack overflow specially in TryCatchScope - instead of immediately | ||
| 675 | + // exiting, we can use the red zone to re-throw to user code. | ||
| 676 | + static bool IsStackOverflowError(Isolate* isolate, Local<Value> exception) { | ||
| 677 | + if (!exception->IsNativeError()) return false; | ||
| 678 | + | ||
| 679 | + Local<Object> err_obj = exception.As<Object>(); | ||
| 680 | + Local<String> constructor_name = err_obj->GetConstructorName(); | ||
| 681 | + | ||
| 682 | + // Must be a RangeError | ||
| 683 | + Utf8Value name(isolate, constructor_name); | ||
| 684 | + if (name.ToStringView() != "RangeError") return false; | ||
| 685 | + | ||
| 686 | + // Check for the specific stack overflow message | ||
| 687 | + Local<Context> context = isolate->GetCurrentContext(); | ||
| 688 | + Local<Value> message_val; | ||
| 689 | + if (!err_obj->Get(context, String::NewFromUtf8Literal(isolate, "message")) | ||
| 690 | + .ToLocal(&message_val)) { | ||
| 691 | + return false; | ||
| 692 | + } | ||
| 693 | + | ||
| 694 | + if (!message_val->IsString()) return false; | ||
| 695 | + | ||
| 696 | + Utf8Value message(isolate, message_val.As<String>()); | ||
| 697 | + return message.ToStringView() == "Maximum call stack size exceeded"; | ||
| 698 | + } | ||
| 699 | + | ||
| 674 | 700 | namespace errors { | |
| 675 | 701 | ||
| 676 | 702 | TryCatchScope::~TryCatchScope() { | |
| 677 | - if (HasCaught() && !HasTerminated() && mode_ == CatchMode::kFatal) { | ||
| 703 | + if (HasCaught() && !HasTerminated() && mode_ != CatchMode::kNormal) { | ||
| 678 | 704 | HandleScope scope(env_->isolate()); | |
| 679 | 705 | Local<v8::Value> exception = Exception(); | |
| 680 | 706 | Local<v8::Message> message = Message(); | |
| 707 | + | ||
| 708 | + // Special handling for stack overflow errors in async_hooks: instead of | ||
| 709 | + // immediately exiting, re-throw the exception. This allows the exception | ||
| 710 | + // to propagate to user code's try-catch blocks. | ||
| 711 | + if (mode_ == CatchMode::kFatalRethrowStackOverflow && | ||
| 712 | + IsStackOverflowError(env_->isolate(), exception)) { | ||
| 713 | + ReThrow(); | ||
| 714 | + Reset(); | ||
| 715 | + return; | ||
| 716 | + } | ||
| 717 | + | ||
| 681 | 718 | EnhanceFatalException enhance = CanContinue() ? | |
| 682 | 719 | EnhanceFatalException::kEnhance : EnhanceFatalException::kDontEnhance; | |
| 683 | 720 | if (message.IsEmpty()) | |
@@ -1277,8 +1314,26 @@ void TriggerUncaughtException(Isolate* isolate, | |||
| 1277 | 1314 | if (env->can_call_into_js()) { | |
| 1278 | 1315 | // We do not expect the global uncaught exception itself to throw any more | |
| 1279 | 1316 | // exceptions. If it does, exit the current Node.js instance. | |
| 1280 | - errors::TryCatchScope try_catch(env, | ||
| 1281 | - errors::TryCatchScope::CatchMode::kFatal); | ||
| 1317 | + // Special case: if the original error was a stack overflow and calling | ||
| 1318 | + // _fatalException causes another stack overflow, rethrow it to allow | ||
| 1319 | + // user code's try-catch blocks to potentially catch it. | ||
| 1320 | + auto is_stack_overflow = [&] { | ||
| 1321 | + return IsStackOverflowError(env->isolate(), error); | ||
| 1322 | + }; | ||
| 1323 | + // Without a JS stack, rethrowing may or may not do anything. | ||
| 1324 | + // TODO(addaleax): In V8, expose a way to check whether there is a JS stack | ||
| 1325 | + // or TryCatch that would capture the rethrown exception. | ||
| 1326 | + auto has_js_stack = [&] { | ||
| 1327 | + HandleScope handle_scope(env->isolate()); | ||
| 1328 | + Local<StackTrace> stack; | ||
| 1329 | + return GetCurrentStackTrace(env->isolate(), 1).ToLocal(&stack) && | ||
| 1330 | + stack->GetFrameCount() > 0; | ||
| 1331 | + }; | ||
| 1332 | + errors::TryCatchScope::CatchMode mode = | ||
| 1333 | + is_stack_overflow() && has_js_stack() | ||
| 1334 | + ? errors::TryCatchScope::CatchMode::kFatalRethrowStackOverflow | ||
| 1335 | + : errors::TryCatchScope::CatchMode::kFatal; | ||
| 1336 | + errors::TryCatchScope try_catch(env, mode); | ||
| 1282 | 1337 | // Explicitly disable verbose exception reporting - | |
| 1283 | 1338 | // if process._fatalException() throws an error, we don't want it to | |
| 1284 | 1339 | // trigger the per-isolate message listener which will call this | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -328,7 +328,7 @@ namespace errors { | |||
| 328 | 328 | ||
| 329 | 329 | class TryCatchScope : public v8::TryCatch { | |
| 330 | 330 | public: | |
| 331 | - enum class CatchMode { kNormal, kFatal }; | ||
| 331 | + enum class CatchMode { kNormal, kFatal, kFatalRethrowStackOverflow }; | ||
| 332 | 332 | ||
| 333 | 333 | explicit TryCatchScope(Environment* env, CatchMode mode = CatchMode::kNormal) | |
| 334 | 334 | : v8::TryCatch(env->isolate()), env_(env), mode_(mode) {} | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -474,7 +474,8 @@ static void PrintJavaScriptStack(JSONWriter* writer, | |||
| 474 | 474 | std::string_view trigger) { | |
| 475 | 475 | HandleScope scope(isolate); | |
| 476 | 476 | Local<v8::StackTrace> stack; | |
| 477 | - if (!GetCurrentStackTrace(isolate, MAX_FRAME_COUNT).ToLocal(&stack)) { | ||
| 477 | + if (!GetCurrentStackTrace(isolate, MAX_FRAME_COUNT).ToLocal(&stack) || | ||
| 478 | + stack->GetFrameCount() == 0) { | ||
| 478 | 479 | PrintEmptyJavaScriptStack(writer); | |
| 479 | 480 | return; | |
| 480 | 481 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,80 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + | ||
| 3 | + // This test verifies that stack overflow during deeply nested async operations | ||
| 4 | + // with async_hooks enabled can be caught by try-catch. This simulates real-world | ||
| 5 | + // scenarios like processing deeply nested JSON structures where each level | ||
| 6 | + // creates async operations (e.g., database calls, API requests). | ||
| 7 | + | ||
| 8 | + require('../common'); | ||
| 9 | + const assert = require('assert'); | ||
| 10 | + const { spawnSync } = require('child_process'); | ||
| 11 | + | ||
| 12 | + if (process.argv[2] === 'child') { | ||
| 13 | + const { createHook } = require('async_hooks'); | ||
| 14 | + | ||
| 15 | + // Enable async_hooks with all callbacks (simulates APM tools) | ||
| 16 | + createHook({ | ||
| 17 | + init() {}, | ||
| 18 | + before() {}, | ||
| 19 | + after() {}, | ||
| 20 | + destroy() {}, | ||
| 21 | + promiseResolve() {}, | ||
| 22 | + }).enable(); | ||
| 23 | + | ||
| 24 | + // Simulate an async operation (like a database call or API request) | ||
| 25 | + async function fetchThing(id) { | ||
| 26 | + return { id, data: `data-${id}` }; | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + // Recursively process deeply nested data structure | ||
| 30 | + // This will cause stack overflow when the nesting is deep enough | ||
| 31 | + function processData(data, depth = 0) { | ||
| 32 | + if (Array.isArray(data)) { | ||
| 33 | + for (const item of data) { | ||
| 34 | + // Create a promise to trigger async_hooks init callback | ||
| 35 | + fetchThing(depth); | ||
| 36 | + processData(item, depth + 1); | ||
| 37 | + } | ||
| 38 | + } | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + // Create deeply nested array structure iteratively (to avoid stack overflow | ||
| 42 | + // during creation) | ||
| 43 | + function createNestedArray(depth) { | ||
| 44 | + let result = 'leaf'; | ||
| 45 | + for (let i = 0; i < depth; i++) { | ||
| 46 | + result = [result]; | ||
| 47 | + } | ||
| 48 | + return result; | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + // Create a very deep nesting that will cause stack overflow during processing | ||
| 52 | + const deeplyNested = createNestedArray(50000); | ||
| 53 | + | ||
| 54 | + try { | ||
| 55 | + processData(deeplyNested); | ||
| 56 | + // Should not complete successfully - the nesting is too deep | ||
| 57 | + console.log('UNEXPECTED: Processing completed without error'); | ||
| 58 | + process.exit(1); | ||
| 59 | + } catch (err) { | ||
| 60 | + assert.strictEqual(err.name, 'RangeError'); | ||
| 61 | + assert.match(err.message, /Maximum call stack size exceeded/); | ||
| 62 | + console.log('SUCCESS: try-catch caught the stack overflow in nested async'); | ||
| 63 | + process.exit(0); | ||
| 64 | + } | ||
| 65 | + } else { | ||
| 66 | + // Parent process - spawn the child and check exit code | ||
| 67 | + const result = spawnSync( | ||
| 68 | + process.execPath, | ||
| 69 | + [__filename, 'child'], | ||
| 70 | + { encoding: 'utf8', timeout: 30000 } | ||
| 71 | + ); | ||
| 72 | + | ||
| 73 | + // Should exit successfully (try-catch worked) | ||
| 74 | + assert.strictEqual(result.status, 0, | ||
| 75 | + `Expected exit code 0, got ${result.status}.\n` + | ||
| 76 | + `stdout: ${result.stdout}\n` + | ||
| 77 | + `stderr: ${result.stderr}`); | ||
| 78 | + // Verify the error was handled by try-catch | ||
| 79 | + assert.match(result.stdout, /SUCCESS: try-catch caught the stack overflow/); | ||
| 80 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,47 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + | ||
| 3 | + // This test verifies that when a stack overflow occurs with async_hooks | ||
| 4 | + // enabled, the exception can be caught by try-catch blocks in user code. | ||
| 5 | + | ||
| 6 | + require('../common'); | ||
| 7 | + const assert = require('assert'); | ||
| 8 | + const { spawnSync } = require('child_process'); | ||
| 9 | + | ||
| 10 | + if (process.argv[2] === 'child') { | ||
| 11 | + const { createHook } = require('async_hooks'); | ||
| 12 | + | ||
| 13 | + createHook({ init() {} }).enable(); | ||
| 14 | + | ||
| 15 | + function recursive(depth = 0) { | ||
| 16 | + // Create a promise to trigger async_hooks init callback | ||
| 17 | + new Promise(() => {}); | ||
| 18 | + return recursive(depth + 1); | ||
| 19 | + } | ||
| 20 | + | ||
| 21 | + try { | ||
| 22 | + recursive(); | ||
| 23 | + // Should not reach here | ||
| 24 | + process.exit(1); | ||
| 25 | + } catch (err) { | ||
| 26 | + assert.strictEqual(err.name, 'RangeError'); | ||
| 27 | + assert.match(err.message, /Maximum call stack size exceeded/); | ||
| 28 | + console.log('SUCCESS: try-catch caught the stack overflow'); | ||
| 29 | + process.exit(0); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + // Should not reach here | ||
| 33 | + process.exit(2); | ||
| 34 | + } else { | ||
| 35 | + // Parent process - spawn the child and check exit code | ||
| 36 | + const result = spawnSync( | ||
| 37 | + process.execPath, | ||
| 38 | + [__filename, 'child'], | ||
| 39 | + { encoding: 'utf8', timeout: 30000 } | ||
| 40 | + ); | ||
| 41 | + | ||
| 42 | + assert.strictEqual(result.status, 0, | ||
| 43 | + `Expected exit code 0 (try-catch worked), got ${result.status}.\n` + | ||
| 44 | + `stdout: ${result.stdout}\n` + | ||
| 45 | + `stderr: ${result.stderr}`); | ||
| 46 | + assert.match(result.stdout, /SUCCESS: try-catch caught the stack overflow/); | ||
| 47 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,47 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + | ||
| 3 | + // This test verifies that when a stack overflow occurs with async_hooks | ||
| 4 | + // enabled, the uncaughtException handler is still called instead of the | ||
| 5 | + // process crashing with exit code 7. | ||
| 6 | + | ||
| 7 | + const common = require('../common'); | ||
| 8 | + const assert = require('assert'); | ||
| 9 | + const { spawnSync } = require('child_process'); | ||
| 10 | + | ||
| 11 | + if (process.argv[2] === 'child') { | ||
| 12 | + const { createHook } = require('async_hooks'); | ||
| 13 | + | ||
| 14 | + let handlerCalled = false; | ||
| 15 | + | ||
| 16 | + function recursive() { | ||
| 17 | + // Create a promise to trigger async_hooks init callback | ||
| 18 | + new Promise(() => {}); | ||
| 19 | + return recursive(); | ||
| 20 | + } | ||
| 21 | + | ||
| 22 | + createHook({ init() {} }).enable(); | ||
| 23 | + | ||
| 24 | + process.on('uncaughtException', common.mustCall((err) => { | ||
| 25 | + assert.strictEqual(err.name, 'RangeError'); | ||
| 26 | + assert.match(err.message, /Maximum call stack size exceeded/); | ||
| 27 | + // Ensure handler is only called once | ||
| 28 | + assert.strictEqual(handlerCalled, false); | ||
| 29 | + handlerCalled = true; | ||
| 30 | + })); | ||
| 31 | + | ||
| 32 | + setImmediate(recursive); | ||
| 33 | + } else { | ||
| 34 | + // Parent process - spawn the child and check exit code | ||
| 35 | + const result = spawnSync( | ||
| 36 | + process.execPath, | ||
| 37 | + [__filename, 'child'], | ||
| 38 | + { encoding: 'utf8', timeout: 30000 } | ||
| 39 | + ); | ||
| 40 | + | ||
| 41 | + // Should exit with code 0 (handler was called and handled the exception) | ||
| 42 | + // Previously would exit with code 7 (kExceptionInFatalExceptionHandler) | ||
| 43 | + assert.strictEqual(result.status, 0, | ||
| 44 | + `Expected exit code 0, got ${result.status}.\n` + | ||
| 45 | + `stdout: ${result.stdout}\n` + | ||
| 46 | + `stderr: ${result.stderr}`); | ||
| 47 | + } | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,29 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + | ||
| 3 | + // This test verifies that when the uncaughtException handler itself causes | ||
| 4 | + // a stack overflow, the process exits with a non-zero exit code. | ||
| 5 | + // This is important to ensure we don't silently swallow errors. | ||
| 6 | + | ||
| 7 | + require('../common'); | ||
| 8 | + const assert = require('assert'); | ||
| 9 | + const { spawnSync } = require('child_process'); | ||
| 10 | + | ||
| 11 | + if (process.argv[2] === 'child') { | ||
| 12 | + function f() { f(); } | ||
| 13 | + process.on('uncaughtException', f); | ||
| 14 | + f(); | ||
| 15 | + } else { | ||
| 16 | + // Parent process - spawn the child and check exit code | ||
| 17 | + const result = spawnSync( | ||
| 18 | + process.execPath, | ||
| 19 | + [__filename, 'child'], | ||
| 20 | + { encoding: 'utf8', timeout: 30000 } | ||
| 21 | + ); | ||
| 22 | + | ||
| 23 | + // Should exit with non-zero exit code since the uncaughtException handler | ||
| 24 | + // itself caused a stack overflow. | ||
| 25 | + assert.notStrictEqual(result.status, 0, | ||
| 26 | + `Expected non-zero exit code, got ${result.status}.\n` + | ||
| 27 | + `stdout: ${result.stdout}\n` + | ||
| 28 | + `stderr: ${result.stderr}`); | ||
| 29 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments