https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82081
Bug ID: 82081 Summary: Tail call optimisation of noexcept function leads to exception allowed through Product: gcc Version: 7.1.1 Status: UNCONFIRMED Severity: normal Priority: P3 Component: c++ Assignee: unassigned at gcc dot gnu.org Reporter: thiago at kde dot org Target Milestone: --- When a noexcept function gets optimised with tail-call, the frame disappears so the unwinder cannot know that the function was noexcept and thus std::terminate() should be called. Code: $ cat throw.cpp void noexcept_function() noexcept; bool false_condition = false; void will_throw() { throw 1; } void wrapper() { noexcept_function(); if (false_condition) throw 42; } $ cat main.cpp #include <iostream> void will_throw(); // throws int void wrapper(); extern bool false_condition; void noexcept_function() noexcept { will_throw(); } int main() { try { wrapper(); } catch (int v) { std::cout << "Caught " << v; return v; } return 0; } By bouncing around translation units, we prevent inlining. The compiler cannot know that wrapper() calls noexcept_function(), which calls will_throw(). In debug mode, the program behaves as expected $ g++ -O0 -g throw.cpp main.cpp $ ./a.out terminate called after throwing an instance of 'int' [1] 46552 abort (core dumped) ./a.out (gdb) bt #0 0x00007f9df0ce1a90 in raise () from /lib64/libc.so.6 #1 0x00007f9df0ce30f6 in abort () from /lib64/libc.so.6 #2 0x00007f9df1615235 in __gnu_cxx::__verbose_terminate_handler() () from /usr/lib64/libstdc++.so.6 #3 0x00007f9df1613026 in ?? () from /usr/lib64/libstdc++.so.6 #4 0x00007f9df1611fe9 in ?? () from /usr/lib64/libstdc++.so.6 #5 0x00007f9df1612958 in __gxx_personality_v0 () from /usr/lib64/libstdc++.so.6 #6 0x00007f9df10633a3 in ?? () from /lib64/libgcc_s.so.1 #7 0x00007f9df10638b0 in _Unwind_RaiseException () from /lib64/libgcc_s.so.1 #8 0x00007f9df16132a6 in __cxa_throw () from /usr/lib64/libstdc++.so.6 #9 0x00000000004009ed in will_throw () at throw.cpp:6 #10 0x0000000000400a2f in noexcept_function () at main.cpp:7 #11 0x00000000004009f6 in wrapper () at throw.cpp:11 #12 0x0000000000400a40 in main () at main.cpp:12 However, when optimised, we see that the exception thrown from will_throw() does pass through and is caught by main(): $ g++ -O2 -g throw.cpp main.cpp $ ./a.out Caught 1 (gdb) disass noexcept_function Dump of assembler code for function noexcept_function(): 0x0000000000400b10 <+0>: jmpq 0x400aa0 <will_throw()> I see two possible paths to solving this. 1) forbid tail-call optimisation of a noexcept(false) call in a noexcept function, so that there is a frame in place for the unwinder to find. That is, the noexcept_function should be: sub %rsp, 8 call will_throw() retq (GCC generates this under some conditions, like placing all functions in the same TU but using -fno-inline) 2) wrap the call point of the noexcept function (in this case, wrapper()) with an EH table that enforces that no exceptions should come out of it. The first solution implies a performance penalty due to optimisation that could not be used. If you choose to implement this, please try to disable this correction under -fno-exceptions. The second solution allows the runtime performance at the expense of expanding EH tables around every noexcept function. Neither solution completely solves the problem for mixed-age code in different libraries: solution 1 solves the problem if the callee is recompiled but lets the problem still happen if only the caller is recompiled. Solution 2 is the dual converse: if the caller is recompiled, the problem is solved, but the problem still happens if only the callee is recompiled.