https://gcc.gnu.org/bugzilla/show_bug.cgi?id=113337
Bug ID: 113337 Summary: Rethrown uncaught exceptions don't invoke std::terminate if SEH-based unwinding is used Product: gcc Version: 13.1.0 Status: UNCONFIRMED Severity: normal Priority: P3 Component: libgcc Assignee: unassigned at gcc dot gnu.org Reporter: matteo at mitalia dot net Target Milestone: --- Created attachment 57046 --> https://gcc.gnu.org/bugzilla/attachment.cgi?id=57046&action=edit Test program to reproduce the bug Sample code: ``` #include <exception> #include <stdio.h> #include <stdlib.h> #include <string.h> static void custom_terminate_handler() { fprintf(stderr, "custom_terminate_handler invoked\n"); std::exit(1); } int main(int argc, char *argv[]) { std::set_terminate(&custom_terminate_handler); if (argc < 2) return 1; const char *mode = argv[1]; fprintf(stderr, "%s\n", mode); if (strcmp(mode, "throw") == 0) { throw std::exception(); } else if (strcmp(mode, "rethrow") == 0) { try { throw std::exception(); } catch (...) { throw; } } else { return 1; } return 0; } ``` Compiling and running this on e.g. Linux, it prints "custom_terminate_handler invoked" both when invoked as `./a.out throw` and `./a.out rethrow`. If instead this is compiled with a 64 bit gcc+mingw64 build that uses SEH exceptions, it behaves correctly in the "throw" case, but in the rethrow case it crashes in std::abort (so, you get an "abnormal program termination"/the JIT debugger is invoked). Diving in the problem, I think I found the culprit: on rethrow, `__cxxabiv1::__cxa_rethrow` (libstdc++-v3/libsupc++/eh_throw.cc) invokes `_Unwind_Resume_or_Rethrow` (libgcc/unwind-seh.c), which goes like this: ``` _Unwind_Reason_Code _Unwind_Resume_or_Rethrow (struct _Unwind_Exception *exc) { if (exc->private_[0] == 0) _Unwind_RaiseException (exc); else _Unwind_ForcedUnwind_Phase2 (exc); abort (); } ``` The problem here is that unconditional abort(); I don't know exactly about the else branch, but I think that the "regular", first branch case should read `return _Unwind_RaiseException (exc);` as, if no handler is found, `_Unwind_RaiseException` does return to allow the runtime to call `std::terminate`, as per the relevant comment ``` /* The exception handler installed in crt0 will continue any GCC exception that reaches there (and isn't marked non-continuable). Returning allows the C++ runtime to call std::terminate. */ return _URC_END_OF_STACK; ``` Indeed, patching the binary on the fly to change the `std::abort` call to a return fixes the problem, as `__cxa_rethrow` does call `std::terminate` if `_Unwind_Resume_or_Rethrow` returns. Comparing with the code from libgcc/unwind.inc (used in the SjLj and Dwarf2 case, from what I gather) ``` /* Resume propagation of an FORCE_UNWIND exception, or to rethrow a normal exception that was handled. */ _Unwind_Reason_Code LIBGCC2_UNWIND_ATTRIBUTE _Unwind_Resume_or_Rethrow (struct _Unwind_Exception *exc) { struct _Unwind_Context this_context, cur_context; _Unwind_Reason_Code code; unsigned long frames; /* Choose between continuing to process _Unwind_RaiseException or _Unwind_ForcedUnwind. */ if (exc->private_1 == 0) return _Unwind_RaiseException (exc); uw_init_context (&this_context); cur_context = this_context; code = _Unwind_ForcedUnwind_Phase2 (exc, &cur_context, &frames); gcc_assert (code == _URC_INSTALL_CONTEXT); uw_install_context (&this_context, &cur_context, frames); } ``` I see that the `_Unwind_RaiseException` case is indeed implemented forwarding the error code back to the caller, while the `_Unwind_ForcedUnwind_Phase2` case terminates either in a failed `gcc_assert`, or in a `uw_install_context` (which is noreturn and boils down to a longjmp, at least in the sjlj case). So again, I'm not entirely sure about the `_UA_FORCE_UNWIND` case, but the "regular rethrown exception" branch should surely forward back the error code instead of crashing on `abort`.