Diagnostics: report the message of an uncaught C++ exception - #1834
Conversation
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Improve crash diagnostics in the Playground by producing actionable symbolized callstacks (MSVC/Windows) and including messages for uncaught C++ exceptions.
Changes:
- Add MSVC-only dbghelp-based symbolization to print
module+RVA, symbol names, and file:line when available. - Add terminate + SIGABRT reporting to include the message of an uncaught exception in crash output.
Suppressed comments (1)
Apps/Playground/Shared/Diagnostics.cpp:227
- On non-MSVC platforms this is a POSIX signal handler; calling into the C++ runtime (
std::current_exception,std::rethrow_exception,std::stringallocations) from a signal handler is not async-signal-safe and can deadlock or crash (especially if the abort happens while the allocator/runtime holds internal locks). Since you already install a terminate handler (which is global on typical non-MSVC libcs), consider keepingOnSignalAbortminimal on non-MSVC (just print the fixed message / exit), and do exception description only inOnTerminate. If you need a message in SIGABRT too, capture it earlier into a preallocated thread-local buffer inOnTerminateand only print that buffer in the signal handler.
void OnSignalAbort(int /*signal*/)
{
const std::string detail = DescribeCurrentException();
Diagnostics::DumpFailure("ABORT", nullptr, 0, 1, "SIGABRT raised.%s%s",
detail.empty() ? "" : "\n", detail.c_str());
Diagnostics::SetExitCode(3);
Diagnostics::PrintFinishLine();
std::_Exit(3);
}
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
An uncaught exception currently produces "SIGABRT raised." and nothing else,
so a crash report never says what actually failed. The exception that is
still propagating is now described:
--- BN: ABORT ---
SIGABRT raised.
uncaught std::exception: <what()>
The reporting is done from both the terminate handler and the SIGABRT
handler on purpose. The standard says std::set_terminate() is global, but
the Microsoft CRT keeps the terminate handler per-thread, so a terminate on
a worker thread never reaches a handler installed on the main thread and
lands in the SIGABRT handler instead. Handling both keeps worker-thread
failures diagnosable on Windows.
Verified on Windows with an exception escaping a worker thread.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09
24eabd6 to
e4c3320
Compare
Copilot flagged that OnSignalAbort is a real signal handler on non-MSVC, and DescribeCurrentException() is not async-signal-safe: std::current_exception() and std::string both allocate. abort() is frequently raised from inside the allocator itself (heap corruption, a glibc malloc assertion), so allocating in the handler can deadlock against the allocator's own lock in exactly the cases where the crash report matters most. The call was also redundant there. Outside the Microsoft CRT std::set_terminate() is global rather than per-thread, and OnTerminate() ends in std::_Exit(), so an uncaught exception is reported and the process is gone before abort() is ever reached. Everything that does land in the POSIX handler -- a direct abort(), a libc assertion, raise(SIGABRT), kill -ABRT -- has no C++ exception in flight, so the call returned an empty string anyway. The MSVC branch keeps the call, because there std::set_terminate() is per-thread and a worker-thread terminate genuinely bypasses OnTerminate() and lands here with the exception still current. Verified by compiling the POSIX branch standalone (MSVC never builds it) at /W4 /WX: the emitted message is identical with and without an exception in flight, and OnTerminate() still recovers "uncaught std::exception: ...". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 88569c10-a7ff-4373-9a58-afa9c68b8c09
|
Good catch — fixed in c98e161. You're right on both counts, and the call was redundant on that platform as well as unsafe. Outside the Microsoft CRT That makes the risk you describe strictly one-sided: So the POSIX branch now prints the fixed message and nothing else. I kept the call in the MSVC branch, where it's both safe and necessary: there Since MSVC never compiles the branch I changed, I verified it by pulling it into a standalone TU and building at Identical output with and without an exception in flight (so no behavioural regression on the only path that reaches it), and Worth being explicit about the remaining limitation: |
An uncaught C++ exception currently produces this, and nothing more:
The report never says what actually failed. The exception that is still propagating is now described:
Why it is reported from two handlers
The reporting is done from both the terminate handler and the SIGABRT handler on purpose.
The standard says
std::set_terminate()is global, but the Microsoft CRT keeps the terminate handler per-thread. A terminate on a worker thread therefore never reaches a handler installed on the main thread — it goes straight toabort()and lands in the SIGABRT handler. Handling both keeps worker-thread failures diagnosable on Windows, and still uses the terminate handler on platforms where it is genuinely global.I found this the hard way: the first version of this PR only installed a terminate handler, and it silently never fired.
What changed since the first revision
This PR originally also carried an ad-hoc dbghelp symbolizer for callstacks. That has been dropped — the unresolved frames were a bug in bx's own resolver, not something Babylon Native should work around locally. Two bx bugs are responsible:
DbgHelpSymbolResolve::resolve()requiresSymFromAddrandSymGetLineFromAddrto both succeed. Modules that ship public symbols only (every system DLL) have a perfectly good function name but no line info, so the name was thrown away and the frame printed as<Unknown?>.write(WriterI*, const void*, ...)instring.cppcasts throughuint32_t, so every pointer printed via bx's%ploses its top 32 bits on a 64-bit build. Callstack PCs came out as0x115cc3f6instead of0x00007ff9115cc3f6, which cannot be fed back into a debugger.With both fixed in bx, the same crash goes from 15 of 23 frames unresolved to all 23 resolved, with full-width addresses, and no Babylon Native change is needed:
So this PR is now just the uncaught-exception message.
Scope
Cross-platform; no new dependencies. Verified on Windows by throwing from a worker thread and confirming the message appears and the exit code stays 3.