Skip to content

Commit 24cf33f

Browse files
committed
[EH] Make std::terminate() work with EH
`noexcept` function shouldn't throw, so `noexcept` function code generation is to `invoke` every function call in those functions and in case they throw, call `std::terminate`. This codegen comes from clang and native platforms do this too. So in wasm, they become something like ```wasm try function body catch_all call std::terminate end ``` `std::terminate` calls `std::__terminate`. Both of `std::terminate` and `std::__terminate` are `noexcept` now. So that means their code is structured like that, which sounds like self-calling, but normally no function calls in those functions should ever throw, so that's fine. But in our case, `abort` ends up throwing, which is a problem. The function body of `__terminate` eventually calls JS `abort`, and ends up here: https://github.com/emscripten-core/emscripten/blob/970998b2670a9bcf39d31e2b01db571089955add/src/preamble.js#L605-L623 This ends up throwing a JS exception. This is basically just a foreign exception from the wasm perspective, and is caught by `catch_all`, and calls `std::terminate` again. And the whole process continues until the call stack is exhausted. What emscripten-core#9730 tried to do was throwing a trap, because Wasm `catch`/`catch_all` don't catch traps. Traps become `RuntimeError`s after they hit a JS frame. To be consistent, we decided `catch`/`catch_all` shouldn't catch them after they become `RuntimeError`s. That's the reason emscripten-core#9730 changed the code to throw not just any random thing but `RuntimeError`. But somehow we decided that we make that trap distinction not based on `RuntimeError` class but some hidden field (WebAssembly/exception-handling#89 (comment)). This PR removes `noexcept` from `std::terminate` and `std::__terminate`'s signatures so that the cleanup that contains `catch_all` is not generated for those two functions. So now the JS exception thrown by `abort()` will unwind the stack, which is different from native, but that can be considered OK because I don't think users expect `abort` to preserve the stack intact? Fixes emscripten-core#16407.
1 parent c78fddf commit 24cf33f

File tree

4 files changed

+39
-1
lines changed

4 files changed

+39
-1
lines changed

system/lib/libcxx/include/exception

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ unexpected_handler get_unexpected() noexcept;
4545
typedef void (*terminate_handler)();
4646
terminate_handler set_terminate(terminate_handler f ) noexcept;
4747
terminate_handler get_terminate() noexcept;
48+
#ifdef __USING_WASM_EXCEPTIONS__
49+
[[noreturn]] void terminate();
50+
#else
4851
[[noreturn]] void terminate() noexcept;
52+
#endif
4953
5054
bool uncaught_exception() noexcept;
5155
int uncaught_exceptions() noexcept; // C++17
@@ -128,7 +132,11 @@ _LIBCPP_NORETURN _LIBCPP_FUNC_VIS void unexpected();
128132
typedef void (*terminate_handler)();
129133
_LIBCPP_FUNC_VIS terminate_handler set_terminate(terminate_handler) _NOEXCEPT;
130134
_LIBCPP_FUNC_VIS terminate_handler get_terminate() _NOEXCEPT;
135+
#ifdef __USING_WASM_EXCEPTIONS__
136+
_LIBCPP_NORETURN _LIBCPP_FUNC_VIS void terminate();
137+
#else
131138
_LIBCPP_NORETURN _LIBCPP_FUNC_VIS void terminate() _NOEXCEPT;
139+
#endif
132140

133141
_LIBCPP_FUNC_VIS bool uncaught_exception() _NOEXCEPT;
134142
_LIBCPP_FUNC_VIS _LIBCPP_AVAILABILITY_UNCAUGHT_EXCEPTIONS int uncaught_exceptions() _NOEXCEPT;

system/lib/libcxxabi/src/cxa_handlers.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,13 @@ get_terminate() noexcept
5050
}
5151

5252
void
53+
#ifdef __USING_WASM_EXCEPTIONS__
54+
// In Wasm EH, a JS exception thrown by abort() is caught by 'noexcept' cleanup
55+
// from which std::__terminate is called again, causing an infinite loop
56+
__terminate(terminate_handler func)
57+
#else
5358
__terminate(terminate_handler func) noexcept
59+
#endif
5460
{
5561
#ifndef _LIBCXXABI_NO_EXCEPTIONS
5662
try
@@ -71,7 +77,13 @@ __terminate(terminate_handler func) noexcept
7177

7278
__attribute__((noreturn))
7379
void
80+
#ifdef __USING_WASM_EXCEPTIONS__
81+
// In Wasm EH, a JS exception thrown by abort() is caught by 'noexcept' cleanup
82+
// from which std::terminate is called again, causing an infinite loop
83+
terminate()
84+
#else
7485
terminate() noexcept
86+
#endif
7587
{
7688
#if !defined(_LIBCXXABI_NO_EXCEPTIONS) && !defined(__USING_EMSCRIPTEN_EXCEPTIONS__)
7789
// If there might be an uncaught exception

system/lib/libcxxabi/src/cxa_handlers.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ __unexpected(unexpected_handler func);
2525

2626
_LIBCXXABI_HIDDEN _LIBCXXABI_NORETURN
2727
void
28-
__terminate(terminate_handler func) noexcept;
28+
__terminate(terminate_handler func);
2929

3030
} // std
3131

tests/test_core.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1639,6 +1639,24 @@ class Polymorphic {virtual void member(){}};
16391639
}
16401640
''', 'exception caught: std::bad_typeid')
16411641

1642+
@with_both_eh_sjlj
1643+
def test_std_terminate(self):
1644+
# std::terminate eventually calls abort(), which is implemented with
1645+
# throwing a JS exception, which used to cause an infinite loop that
1646+
# exhausted the call stack. The reason is std::terminate is marked
1647+
# 'noexcept' in the upstream LLVM, which generates cleanuppads that call
1648+
# std::terminate in case of an unexpected second exception happens while
1649+
# aborting, and our abort() was considered as that second exception. We
1650+
# removed 'noexcept' from std::terminate signature when Wasm EH is enabled
1651+
# to avoid this issue.
1652+
err = self.do_run(r'''
1653+
#include <exception>
1654+
int main() {
1655+
std::terminate();
1656+
}
1657+
''', assert_returncode=NON_ZERO)
1658+
self.assertNotContained('Maximum call stack size exceeded', err)
1659+
16421660
def test_iostream_ctors(self):
16431661
# iostream stuff must be globally constructed before user global
16441662
# constructors, so iostream works in global constructors

0 commit comments

Comments
 (0)