From 955d7e402e9d08d7842f6924b2093def97ec0891 Mon Sep 17 00:00:00 2001 From: Temuulen Undrakhbayar Date: Tue, 7 Jul 2026 00:08:45 +0800 Subject: [PATCH 1/2] src/node_errors: add re-entrancy guard to prevent infinite recursion in TriggerUncaughtException Add a thread_local re-entrancy guard to TriggerUncaughtException() that detects when the JS-level exception handler (process._fatalException) itself triggers another exception through the inspector protocol. This prevents the infinite loop: TriggerUncaughtException -> InspectorConsoleCall -> TriggerUncaughtException -> InspectorConsoleCall -> ... When re-entrancy is detected, the function prints a diagnostic with the formatted exception to stderr using FormatCaughtException (which avoids the inspector path entirely), then aborts the process. Fixes: https://github.com/nodejs/node/issues/64326 --- src/node_errors.cc | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/node_errors.cc b/src/node_errors.cc index 63db97f6a56db0..3a636b5ac3fd43 100644 --- a/src/node_errors.cc +++ b/src/node_errors.cc @@ -191,6 +191,7 @@ static std::string GetErrorSource(Isolate* isolate, static std::atomic is_in_oom{false}; static thread_local std::atomic is_retrieving_js_stacktrace{false}; +static thread_local bool is_in_uncaught_exception = false; MaybeLocal GetCurrentStackTrace(Isolate* isolate, int frame_count) { if (isolate == nullptr) { return MaybeLocal(); @@ -1278,6 +1279,26 @@ void TriggerUncaughtException(Isolate* isolate, CHECK(isolate->InContext()); Local context = isolate->GetCurrentContext(); Environment* env = Environment::GetCurrent(context); + // Re-entrancy guard: prevent infinite recursion when the JS-level + // exception handler (process._fatalException) itself triggers another + // exception through the inspector protocol, causing V8's pending message + // reporting to call TriggerUncaughtException reentrantly. + if (is_in_uncaught_exception) { + PrintToStderrAndFlush( + "FATAL ERROR: Re-entrant uncaught exception detected.\n" + "The exception handler threw an error while processing a\n" + "previous uncaught exception, likely due to an inspector\n" + "protocol issue. Aborting to prevent infinite recursion.\n" + + FormatCaughtException(isolate, context, error, message)); + ABORT(); + } + // RAII guard to ensure the flag is cleared when TriggerUncaughtException + // returns normally. Note: ABORT() and env->Exit() do not run this + // destructor, which is intentional — the process is terminating. + struct UncaughtExceptionGuard { + UncaughtExceptionGuard() { is_in_uncaught_exception = true; } + ~UncaughtExceptionGuard() { is_in_uncaught_exception = false; } + } uncaught_exception_guard; if (env == nullptr) { // This means that the exception happens before Environment is assigned // to the context e.g. when there is a SyntaxError in a per-context From b9c03aed9487cf252c662e6d50b0e66db71da6de Mon Sep 17 00:00:00 2001 From: Temuulen Undrakhbayar Date: Tue, 7 Jul 2026 14:56:52 +0800 Subject: [PATCH 2/2] src/node_errors: document re-entrancy guard --- src/node_errors.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/node_errors.cc b/src/node_errors.cc index 3a636b5ac3fd43..394b8eb36a5d0a 100644 --- a/src/node_errors.cc +++ b/src/node_errors.cc @@ -191,6 +191,8 @@ static std::string GetErrorSource(Isolate* isolate, static std::atomic is_in_oom{false}; static thread_local std::atomic is_retrieving_js_stacktrace{false}; +// This is thread-local because it only guards re-entrancy within the current +// thread's uncaught-exception path; no cross-thread synchronization is needed. static thread_local bool is_in_uncaught_exception = false; MaybeLocal GetCurrentStackTrace(Isolate* isolate, int frame_count) { if (isolate == nullptr) {