From e4c33200ac3692c78affe45405f71e9f82a10f1d Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Wed, 12 Aug 2026 12:14:18 -0700 Subject: [PATCH 1/2] Diagnostics: report the message of an uncaught C++ exception 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: 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 --- Apps/Playground/Shared/Diagnostics.cpp | 62 +++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/Apps/Playground/Shared/Diagnostics.cpp b/Apps/Playground/Shared/Diagnostics.cpp index 5b16c5aa51..29c102767d 100644 --- a/Apps/Playground/Shared/Diagnostics.cpp +++ b/Apps/Playground/Shared/Diagnostics.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #if defined(_MSC_VER) @@ -40,6 +41,31 @@ namespace bool s_ansiEnabled{false}; + // Recover the message of the exception that is currently propagating, if + // any. Returns an empty string when no exception is in flight. Valid inside + // a terminate handler and inside the SIGABRT handler that terminate() ends + // up calling, because the exception stays current until the handler returns. + std::string DescribeCurrentException() + { + if (std::current_exception() == nullptr) + { + return {}; + } + + try + { + std::rethrow_exception(std::current_exception()); + } + catch (const std::exception& e) + { + return std::string{"uncaught std::exception: "} + e.what(); + } + catch (...) + { + return "uncaught non-std exception."; + } + } + #if defined(_MSC_VER) void __cdecl OnInvalidParameter( const wchar_t* expression, @@ -71,7 +97,13 @@ namespace void OnSignalAbort(int /*signal*/) { - Diagnostics::DumpFailure("ABORT", nullptr, 0, 1, "SIGABRT raised."); + // abort() is where std::terminate() ends up, among other paths. On MSVC + // std::set_terminate() is per-thread, so a terminate on a worker thread + // never reaches OnTerminate below and lands here instead; recover the + // exception message either way. + const std::string detail = DescribeCurrentException(); + Diagnostics::DumpFailure("ABORT", nullptr, 0, 1, "SIGABRT raised.%s%s", + detail.empty() ? "" : "\n", detail.c_str()); if (::IsDebuggerPresent()) { bx::debugBreak(); @@ -105,7 +137,9 @@ namespace #else void OnSignalAbort(int /*signal*/) { - Diagnostics::DumpFailure("ABORT", nullptr, 0, 1, "SIGABRT raised."); + 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); @@ -162,6 +196,27 @@ namespace Diagnostics::PrintFinishLine(); std::_Exit(3); } + + void OnTerminate() + { + // An uncaught C++ exception otherwise reaches abort() with nothing but + // "SIGABRT raised.", which says nothing about what actually went wrong. + // + // This only covers the thread that installed it on Windows: the standard + // says the terminate handler is global, but the Microsoft CRT keeps it + // per-thread. OnSignalAbort() above repeats the same reporting so + // worker-thread terminations stay diagnosable there. + std::string detail = DescribeCurrentException(); + if (detail.empty()) + { + detail = "terminate called without an active exception."; + } + + Diagnostics::DumpFailure("TERMINATE", nullptr, 0, 1, "%s", detail.c_str()); + Diagnostics::SetExitCode(3); + Diagnostics::PrintFinishLine(); + std::_Exit(3); + } } namespace Diagnostics @@ -180,6 +235,9 @@ namespace Diagnostics // (stderr-visible) instead of bx's OutputDebugString-only default. bx::setAssertHandler(&OnBxAssert); + // Report the message of an uncaught exception before abort() swallows it. + std::set_terminate(&OnTerminate); + #if defined(_MSC_VER) // Route assert() to stderr instead of UCRT's modal dialog. Covers the // direct assert() codepath; _CrtSetReportMode below covers _CRT_*. From c98e161d791843ad4dfef31f9953373a3cb8f382 Mon Sep 17 00:00:00 2001 From: Branimir Karadzic Date: Thu, 13 Aug 2026 11:02:43 -0700 Subject: [PATCH 2/2] Diagnostics: keep the POSIX SIGABRT handler allocation-free 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 --- Apps/Playground/Shared/Diagnostics.cpp | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Apps/Playground/Shared/Diagnostics.cpp b/Apps/Playground/Shared/Diagnostics.cpp index 29c102767d..e8ee4a1bbb 100644 --- a/Apps/Playground/Shared/Diagnostics.cpp +++ b/Apps/Playground/Shared/Diagnostics.cpp @@ -43,8 +43,10 @@ namespace // Recover the message of the exception that is currently propagating, if // any. Returns an empty string when no exception is in flight. Valid inside - // a terminate handler and inside the SIGABRT handler that terminate() ends - // up calling, because the exception stays current until the handler returns. + // a terminate handler, and inside the MSVC SIGABRT handler that terminate() + // ends up calling, because the exception stays current until the handler + // returns. Allocates, so it must not be called from a signal handler that + // can fire asynchronously (see OnSignalAbort on non-MSVC). std::string DescribeCurrentException() { if (std::current_exception() == nullptr) @@ -137,9 +139,20 @@ namespace #else 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()); + // Deliberately does not call DescribeCurrentException(). Outside the + // Microsoft CRT std::set_terminate() is global, and OnTerminate() below + // ends in std::_Exit(), so an uncaught exception is fully reported there + // and never reaches abort(). Everything that does land here -- a direct + // abort(), a libc assertion, raise(SIGABRT), kill -ABRT -- has no C++ + // exception in flight, so recovering one would return an empty string + // anyway. + // + // That matters because this is a real signal handler: std::current_exception() + // and std::string allocate, and abort() is frequently raised from inside + // the allocator (heap corruption, a glibc malloc assertion). Allocating + // here would deadlock against the allocator's own lock in exactly the + // cases where the diagnostic is most needed. + Diagnostics::DumpFailure("ABORT", nullptr, 0, 1, "SIGABRT raised."); Diagnostics::SetExitCode(3); Diagnostics::PrintFinishLine(); std::_Exit(3);