fix(node): prevent logAndExitProcess from recursing on a broken pipe - #24338
fix(node): prevent logAndExitProcess from recursing on a broken pipe#24338jkubo wants to merge 1 commit into
Conversation
logAndExitProcess writes the error to the console before shutting down. When stderr is a closed pipe that write raises EPIPE, which surfaces as another uncaught exception and re-enters the handler. calledFatalError is already set by then, so onuncaughtexception routes straight back into logAndExitProcess and the cycle repeats, allocating an Error with a captured stack and a pending client.close() on every pass until V8 aborts with a heap OOM. Guard the function itself rather than the calledFatalError branch, so every caller is covered. Fixes getsentry#24337 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want reviews to match your repository better? Bugbot Learning can learn team-specific rules from PR activity. A team admin can enable Learning in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit e3f8993. Configure here.
| global.process.exit(1); | ||
| return; | ||
| } | ||
| isShuttingDown = true; |
There was a problem hiding this comment.
Fix PR missing regression test
Medium Severity
This fix PR does not include a unit, integration, or E2E test for the logAndExitProcess broken-pipe recursion. I flagged this because it was mentioned in the review rules file, which requires a regression test on fix PRs. A test that re-enters the handler after a failed stderr write would lock in the isShuttingDown guard.
Triggered by project rule: PR Review Guidelines for Cursor Bot
Reviewed by Cursor Bugbot for commit e3f8993. Configure here.


Fixes #24337.
logAndExitProcesswrites the error to the console before shutting down. When stderr is a closed pipe that write raisesEPIPE, which surfaces as another uncaught exception and re-enters the handler.calledFatalErroris already set by then, soonuncaughtexception.tsroutes straight back intologAndExitProcess:and the cycle repeats.
caughtFirstErrorandcaughtSecondErrorguard the entry paths; nothing guards this one. Each pass allocates a freshErrorwith a captured stack and another pendingclient.close()that cannot resolve, so the heap climbs until V8 aborts withFATAL ERROR: ... JavaScript heap out of memory.I guarded the function rather than the
calledFatalErrorbranch, sincelogAndExitProcessis what performs the failing write and this covers every caller.Reproduction
The DSN needs to be unreachable rather than invalid, so
client.close()cannot resolve. The heap cap only makes it fail in about a second; at default heap it takes roughly a minute and reads as a hang.@sentry/node10.74.0, stockAlso reproduces on 7.120.4, so this is not a recent regression.
Why it is worth more than a crash
On Linux with
systemd-coredumpenabled the abort writes a core dump per occurrence — ~300MB in the case I hit — and a core is a verbatim copy of process memory, so it contains whatever account data and token material the process was holding. CI and agent runners are the common shape, since they capture combined output and close the pipe on timeout. An interactive TTY never producesEPIPE, so this stays invisible in normal use.What I have not done
develop.isShuttingDownis module-level state, so it persists across aclose()/ re-init()cycle in the same process. That seemed acceptable for a path that only runs on fatal shutdown, but if you would rather it lived on the client I am happy to move it.Happy to add a regression test if you tell me where it belongs —
packages/node/test/has no existing coverage for this handler that I could find.Note
This is a contribution from an AI agent: Claude Code, Claude Opus 5.