Conversation
The crash reporter is not async-signal-safe: doReport() fopen()s the crash file, which allocates. When the fatal signal was raised from inside the allocator itself -- e.g. glibc abort() on detected heap corruption, which holds the arena mutex -- that allocation deadlocks on the mutex the aborting thread already holds. The process then wedges forever in the signal handler (main thread in futex_wait, an empty 0-byte crash.dmp, a stuck stderr), so a heap bug produces no diagnostics at all and, in CI, only a pipeline timeout. Make the handler self-terminating: doReport() arms an alarm() watchdog with a dedicated async-signal-safe SIGALRM handler (write(2) + _exit) before touching the allocator, and disarms it when the report completes. SIGALRM is otherwise routed to the crash handler, so the watchdog handler is installed for the duration and the previous disposition restored afterwards (keeping the SIGUSR1 diagnostic-dump path unchanged). A reentrancy guard turns a crash inside the reporter into an immediate abort instead of unbounded recursion. This does not make reportStackState() itself async-signal-safe (a larger change); it guarantees the process always terminates and, in the common case where the report path does not deadlock, the full dump is still produced. Verified: a standalone reproduction using the VM's exact handler flags (SA_NODEFER|SA_SIGINFO, empty sa_mask) with a handler that blocks forever is terminated by the alarm watchdog after the timeout; the patched VM boots and evaluates normally. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dszsb5ALkx1ytH8atZPvEV
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
When a fatal signal is raised from inside the allocator — most importantly
glibc abort()on detected heap corruption, which raisesSIGABRTwhile still holding the malloc arena mutex — the VM's crash reporter deadlocks and the process hangs forever.doReport()(src/unix/debugUnix.c) is not async-signal-safe: itfopen()s the crash-dump file, which callsmalloc(). From the signal handler, thatmalloc()blocks on the arena mutex the aborting thread already holds → deadlock. The result on this VM is the worst possible outcome for debugging: the main thread sits infutex_wait,crash.dmpis 0 bytes,stderrproduces nothing, and in CI the job only dies on a pipeline timeout. Every heap-corruption bug in the VM is thereby rendered undiagnosable.(Found while debugging a real heap overflow on aarch64: each occurrence wedged with an empty
crash.dmpand had to bekill -9'd.)Fix
Make the fatal-signal report self-terminating.
doReport()now arms analarm()watchdog with a dedicated, async-signal-safeSIGALRMhandler (write(2)+_exit) before it touches the allocator, and disarms it once the report completes:SIGALRMis otherwise routed to the crash handler (seeinstallErrorHandlers), so the watchdog handler is installed for the duration and the previous disposition is restored afterwards — theSIGUSR1diagnostic-dump path is unchanged.abort()rather than unbounded recursion.This intentionally does not rewrite
reportStackState()to be fully async-signal-safe (a much larger change). It guarantees the process always terminates, and in the common case — a fatal signal that does not originate in the allocator — the full crash dump is still produced exactly as before.Verification
SA_NODEFER | SA_SIGINFO, emptysa_mask) with a fatal handler that blocks forever (pause()loop, standing in for themallocdeadlock) is terminated by the alarm watchdog after the timeout, exiting via the watchdog handler — instead of hanging.3+4→7); the change adds code only around the crash path and leaves normal execution untouched.Scope note
The 30-second timeout is a safety bound, not a normal-path cost (a healthy report finishes in well under a second and cancels the alarm). Only a genuinely deadlocked/hung report hits it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Dszsb5ALkx1ytH8atZPvEV