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