From 35f61c9dcecb20fc91e9a111928df3dc0beb714c Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Thu, 29 Sep 2022 15:09:31 -0700 Subject: [PATCH 01/11] [EH] Support stack traces for Wasm exceptions This embeds stack traces into `WebAssembly.Exception` objects when `ASSERTION` is set. To do this, we now have a separate debug version of libc++abi, whose `__cxa_throw` doesn't call libunwind's `_Unwind_RaiseException`, which uses Wasm's `throw` instruction directly to throw exceptions, but rather calls out to a helper JS function that creates and throws `WebAssembly.Exception`. That JS function uses the optional `stack` property of `WebAssembly.Exception` constructor to attach stack traces to objects. Without `ASSERTION` set, when an exception is thrown and uncaught, we only see something like ``` exiting due to exception: [object WebAssembly.Exception] ``` And this is what we've seen so far as well. With this patch, when `ASSERTION` is set, we see stack traces like ``` exiting due to exception: [object WebAssembly.Exception],Error at ___throwCppWebAssemblyException (test.js:1139:13) at __cxa_throw (wasm://wasm/009a7c9a:wasm-function[1551]:0x24367) at bar() (wasm://wasm/009a7c9a:wasm-function[12]:0xf53) at foo() (wasm://wasm/009a7c9a:wasm-function[19]:0x154e) at __original_main (wasm://wasm/009a7c9a:wasm-function[20]:0x15a6) at main (wasm://wasm/009a7c9a:wasm-function[56]:0x25be) at test.js:833:22 at callMain (test.js:4567:15) at doRun (test.js:4621:23) at run (test.js:4636:5) ``` Fixes #17466. --- embuilder.py | 1 + emcc.py | 5 +++ src/library_exceptions.js | 11 ++++-- system/lib/libcxxabi/src/cxa_exception.cpp | 15 ++++++++ test/test_other.py | 41 ++++++++++++++++++++++ tools/system_libs.py | 3 +- 6 files changed, 72 insertions(+), 4 deletions(-) diff --git a/embuilder.py b/embuilder.py index 1aab1d1316023..e6e4601818d8b 100755 --- a/embuilder.py +++ b/embuilder.py @@ -35,6 +35,7 @@ 'libc_optz-debug', 'libc++abi', 'libc++abi-except', + 'libc++abi-debug-except', 'libc++abi-noexcept', 'libc++', 'libc++-except', diff --git a/emcc.py b/emcc.py index c3f64cb0e78b6..28a271b33017a 100755 --- a/emcc.py +++ b/emcc.py @@ -2652,6 +2652,11 @@ def get_full_import_name(name): if settings.WASM_EXCEPTIONS: settings.REQUIRED_EXPORTS += ['__trap'] + # When ASSERTIONS is set, we include stack traces in Wasm exception objects + # using the JS API, which needs this C++ tag exported. + if settings.ASSERTIONS and settings.WASM_EXCEPTIONS: + settings.EXPORTED_FUNCTIONS += ['___cpp_exception'] + # Make `getExceptionMessage` and other necessary functions available for use. if settings.EXPORT_EXCEPTION_HANDLING_HELPERS: # We also export refcount increasing and decreasing functions because if you diff --git a/src/library_exceptions.js b/src/library_exceptions.js index 00f24fd98c1b7..8b2af1d4008d5 100644 --- a/src/library_exceptions.js +++ b/src/library_exceptions.js @@ -417,11 +417,18 @@ var LibraryExceptions = { return Module['asm']['__cpp_exception']; }, - // Given an WebAssembly.Exception object, returns the actual user-thrown - // C++ object address in the Wasm memory. + // Throw a WebAssembly.Exception object with the C++ tag. If traceStack is + // true, includes the stack traces within the exception object. // WebAssembly.Exception is a JS object representing a Wasm exception, // provided by Wasm JS API: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Exception + __throwCppWebAssemblyException__deps: ['$getCppExceptionTag'], + __throwCppWebAssemblyException: function(ex, traceStack) { + throw new WebAssembly.Exception(getCppExceptionTag(), [ex], {traceStack: traceStack}); + }, + + // Given an WebAssembly.Exception object, returns the actual user-thrown + // C++ object address in the Wasm memory. $getCppExceptionThrownObjectFromWebAssemblyException__deps: ['$getCppExceptionTag', '__thrown_object_from_unwind_exception'], $getCppExceptionThrownObjectFromWebAssemblyException: function(ex) { // In Wasm EH, the value extracted from WebAssembly.Exception is a pointer diff --git a/system/lib/libcxxabi/src/cxa_exception.cpp b/system/lib/libcxxabi/src/cxa_exception.cpp index a92997b9031f9..b0065614c1af2 100644 --- a/system/lib/libcxxabi/src/cxa_exception.cpp +++ b/system/lib/libcxxabi/src/cxa_exception.cpp @@ -253,6 +253,13 @@ handler, _Unwind_RaiseException may return. In that case, __cxa_throw will call terminate, assuming that there was no handler for the exception. */ + +#if defined(__USING_WASM_EXCEPTIONS__) && !defined(NDEBUG) +extern "C" { +void __throwCppWebAssemblyException(_Unwind_Exception*, bool); +} // extern "C" +#endif + void #ifdef __USING_WASM_EXCEPTIONS__ // In wasm, destructors return their argument @@ -280,6 +287,14 @@ __cxa_throw(void *thrown_object, std::type_info *tinfo, void (*dest)(void *)) { #ifdef __USING_SJLJ_EXCEPTIONS__ _Unwind_SjLj_RaiseException(&exception_header->unwindHeader); +#elif __USING_WASM_EXCEPTIONS__ +#ifdef NDEBUG + _Unwind_RaiseException(&exception_header->unwindHeader); +#else + // In debug mode, call a JS library function to use WebAssembly.Exception JS + // API, which enables us to include stack traces + __throwCppWebAssemblyException(&exception_header->unwindHeader, true); +#endif #else _Unwind_RaiseException(&exception_header->unwindHeader); #endif diff --git a/test/test_other.py b/test/test_other.py index 7eba6aaeca641..577682c2a0a66 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -7904,6 +7904,47 @@ def test_wasm_nope(self): out = self.run_js('a.out.js', assert_returncode=NON_ZERO) self.assertContained('no native wasm support detected', out) + @requires_v8 + def test_wasm_exceptions_stack_trace(self): + src = r''' + void bar() { + throw 3; + } + void foo() { + bar(); + } + int main() { + foo(); + return 0; + } + ''' + emcc_args = ['-g', '-fwasm-exceptions'] + self.v8_args.append('--experimental-wasm-eh') + + # Stack trace example for this example code: + # exiting due to exception: [object WebAssembly.Exception],Error + # at ___throwCppWebAssemblyException (test.js:1139:13) + # at __cxa_throw (wasm://wasm/009a7c9a:wasm-function[1551]:0x24367) + # at bar() (wasm://wasm/009a7c9a:wasm-function[12]:0xf53) + # at foo() (wasm://wasm/009a7c9a:wasm-function[19]:0x154e) + # at __original_main (wasm://wasm/009a7c9a:wasm-function[20]:0x15a6) + # at main (wasm://wasm/009a7c9a:wasm-function[56]:0x25be) + # at test.js:833:22 + # at callMain (test.js:4567:15) + # at doRun (test.js:4621:23) + # at run (test.js:4636:5) + stack_trace_checks = ['at __cxa_throw', 'at bar', 'at foo', 'at main'] + + # We attach stack traces to exception objects only when ASSERTIONS is set + self.set_setting('ASSERTIONS') + self.do_run(src, emcc_args=emcc_args, assert_all=True, + assert_returncode=NON_ZERO, expected_output=stack_trace_checks) + + self.set_setting('ASSERTIONS', 0) + err = self.do_run(src, emcc_args=emcc_args, assert_returncode=NON_ZERO) + for check in stack_trace_checks: + self.assertNotContained(check, err) + @requires_node def test_jsrun(self): print(config.NODE_JS) diff --git a/tools/system_libs.py b/tools/system_libs.py index c9f80bb28811c..52a885b50f1cb 100644 --- a/tools/system_libs.py +++ b/tools/system_libs.py @@ -1350,7 +1350,7 @@ def can_use(self): return super().can_use() and settings.SHARED_MEMORY -class libcxxabi(NoExceptLibrary, MTLibrary): +class libcxxabi(NoExceptLibrary, MTLibrary, DebugLibrary): name = 'libc++abi' cflags = [ '-Oz', @@ -1363,7 +1363,6 @@ class libcxxabi(NoExceptLibrary, MTLibrary): def get_cflags(self): cflags = super().get_cflags() - cflags.append('-DNDEBUG') if not self.is_mt and not self.is_ww: cflags.append('-D_LIBCXXABI_HAS_NO_THREADS') if self.eh_mode == Exceptions.NONE: From 1b16d8000bea61b158790460d964fa90bf452dd7 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Tue, 4 Oct 2022 00:35:31 -0700 Subject: [PATCH 02/11] Remove JS function name from stack trace --- src/library_exceptions.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/library_exceptions.js b/src/library_exceptions.js index 8b2af1d4008d5..5e4e0a78440f1 100644 --- a/src/library_exceptions.js +++ b/src/library_exceptions.js @@ -424,7 +424,20 @@ var LibraryExceptions = { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Exception __throwCppWebAssemblyException__deps: ['$getCppExceptionTag'], __throwCppWebAssemblyException: function(ex, traceStack) { - throw new WebAssembly.Exception(getCppExceptionTag(), [ex], {traceStack: traceStack}); + var e = new WebAssembly.Exception(getCppExceptionTag(), [ex], {traceStack: traceStack}); + // The generated stack trace will be in the form of: + // + // Error + // at ___throwCppWebAssemblyException (test.js:1139:13) + // at __cxa_throw (wasm://wasm/009a7c9a:wasm-function[1551]:0x24367) + // ... + // + // Remove this JS function name, which is in the second line, from the stack + // trace. + arr = e.stack.split('\n'); + arr.splice(1,1); + e.stack = arr.join('\n'); + throw e; }, // Given an WebAssembly.Exception object, returns the actual user-thrown From 0cfb81daa9701e59470c6ccdb907d54c387b5fb6 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Tue, 4 Oct 2022 00:36:41 -0700 Subject: [PATCH 03/11] Fix comment too --- test/test_other.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/test_other.py b/test/test_other.py index 577682c2a0a66..fc5f968c29b97 100644 --- a/test/test_other.py +++ b/test/test_other.py @@ -7923,7 +7923,6 @@ def test_wasm_exceptions_stack_trace(self): # Stack trace example for this example code: # exiting due to exception: [object WebAssembly.Exception],Error - # at ___throwCppWebAssemblyException (test.js:1139:13) # at __cxa_throw (wasm://wasm/009a7c9a:wasm-function[1551]:0x24367) # at bar() (wasm://wasm/009a7c9a:wasm-function[12]:0xf53) # at foo() (wasm://wasm/009a7c9a:wasm-function[19]:0x154e) From a4fab86087d92d033757b0fe8de0c26a6ce3b2b0 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Tue, 4 Oct 2022 11:20:26 -0700 Subject: [PATCH 04/11] Address comments --- src/library_exceptions.js | 21 ++++++++++++--------- system/lib/libcxxabi/src/cxa_exception.cpp | 4 ++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/library_exceptions.js b/src/library_exceptions.js index 5e4e0a78440f1..8ad3be20668bd 100644 --- a/src/library_exceptions.js +++ b/src/library_exceptions.js @@ -417,28 +417,31 @@ var LibraryExceptions = { return Module['asm']['__cpp_exception']; }, - // Throw a WebAssembly.Exception object with the C++ tag. If traceStack is - // true, includes the stack traces within the exception object. - // WebAssembly.Exception is a JS object representing a Wasm exception, - // provided by Wasm JS API: +#if ASSERTIONS + // Throw a WebAssembly.Exception object with the C++ tag with a stack trace + // embedded. WebAssembly.Exception is a JS object representing a Wasm + // exception, provided by Wasm JS API: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Exception - __throwCppWebAssemblyException__deps: ['$getCppExceptionTag'], - __throwCppWebAssemblyException: function(ex, traceStack) { - var e = new WebAssembly.Exception(getCppExceptionTag(), [ex], {traceStack: traceStack}); + // In release builds, this function is not needed and the native + // _Unwind_RaiseException in libunwind is used instead. + __throw_exception_with_stack_trace__deps: ['$getCppExceptionTag'], + __throw_exception_with_stack_trace: function(ex) { + var e = new WebAssembly.Exception(getCppExceptionTag(), [ex], {traceStack: true}); // The generated stack trace will be in the form of: // // Error - // at ___throwCppWebAssemblyException (test.js:1139:13) + // at ___throw_exception_with_stack_trace(test.js:1139:13) // at __cxa_throw (wasm://wasm/009a7c9a:wasm-function[1551]:0x24367) // ... // // Remove this JS function name, which is in the second line, from the stack // trace. - arr = e.stack.split('\n'); + var arr = e.stack.split('\n'); arr.splice(1,1); e.stack = arr.join('\n'); throw e; }, +#endif // Given an WebAssembly.Exception object, returns the actual user-thrown // C++ object address in the Wasm memory. diff --git a/system/lib/libcxxabi/src/cxa_exception.cpp b/system/lib/libcxxabi/src/cxa_exception.cpp index b0065614c1af2..10ecf3bacc2b4 100644 --- a/system/lib/libcxxabi/src/cxa_exception.cpp +++ b/system/lib/libcxxabi/src/cxa_exception.cpp @@ -256,7 +256,7 @@ exception. #if defined(__USING_WASM_EXCEPTIONS__) && !defined(NDEBUG) extern "C" { -void __throwCppWebAssemblyException(_Unwind_Exception*, bool); +void __throw_exception_with_stack_trace(_Unwind_Exception*, bool); } // extern "C" #endif @@ -293,7 +293,7 @@ __cxa_throw(void *thrown_object, std::type_info *tinfo, void (*dest)(void *)) { #else // In debug mode, call a JS library function to use WebAssembly.Exception JS // API, which enables us to include stack traces - __throwCppWebAssemblyException(&exception_header->unwindHeader, true); + __throw_exception_with_stack_trace(&exception_heade->unwindHeader, true); #endif #else _Unwind_RaiseException(&exception_header->unwindHeader); From 842ef0f0210f41e297fa0b4710544eb4f2535ba8 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Tue, 4 Oct 2022 12:04:08 -0700 Subject: [PATCH 05/11] Typo fix --- system/lib/libcxxabi/src/cxa_exception.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/lib/libcxxabi/src/cxa_exception.cpp b/system/lib/libcxxabi/src/cxa_exception.cpp index 10ecf3bacc2b4..bbb6cd8ddbe4c 100644 --- a/system/lib/libcxxabi/src/cxa_exception.cpp +++ b/system/lib/libcxxabi/src/cxa_exception.cpp @@ -293,7 +293,7 @@ __cxa_throw(void *thrown_object, std::type_info *tinfo, void (*dest)(void *)) { #else // In debug mode, call a JS library function to use WebAssembly.Exception JS // API, which enables us to include stack traces - __throw_exception_with_stack_trace(&exception_heade->unwindHeader, true); + __throw_exception_with_stack_trace(&exception_header->unwindHeader, true); #endif #else _Unwind_RaiseException(&exception_header->unwindHeader); From 6449f7d04d1e38418dd7ae4cc573aa069e554755 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Wed, 5 Oct 2022 16:17:43 -0700 Subject: [PATCH 06/11] Add libc++abi-debug-noexcept to MINIMAL_TASKS --- embuilder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/embuilder.py b/embuilder.py index e6e4601818d8b..7b1ad1389eac8 100755 --- a/embuilder.py +++ b/embuilder.py @@ -35,8 +35,9 @@ 'libc_optz-debug', 'libc++abi', 'libc++abi-except', - 'libc++abi-debug-except', 'libc++abi-noexcept', + 'libc++abi-debug-except', + 'libc++abi-debug-noexcept', 'libc++', 'libc++-except', 'libc++-noexcept', From 2c165e12ce112c56618d49e0ddf61f3a7b3f5745 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Wed, 5 Oct 2022 16:59:04 -0700 Subject: [PATCH 07/11] Add libc++abi-debug to MINIMAL_TASKS --- embuilder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/embuilder.py b/embuilder.py index 7b1ad1389eac8..467f046049a9e 100755 --- a/embuilder.py +++ b/embuilder.py @@ -36,6 +36,7 @@ 'libc++abi', 'libc++abi-except', 'libc++abi-noexcept', + 'libc++abi-debug', 'libc++abi-debug-except', 'libc++abi-debug-noexcept', 'libc++', From 007130ad4521a5fa5df19f0fa1c652f0d31c4b5e Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Wed, 5 Oct 2022 17:34:38 -0700 Subject: [PATCH 08/11] ChangeLog --- ChangeLog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index ba1444a863979..a80a6ec0ee331 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,8 @@ See docs/process.md for more on how version tagging works. 3.1.24 (in development) ----------------------- +- In Wasm exception mode (`-fwasm-exceptions`), when `ASSERTIONS` is enabled, + uncaught exceptions will display stack traces. (#17979) 3.1.23 - 09/23/22 ----------------- From 34e346d5cfb79c8f78ac19fb3624bbb10f59a4bb Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Wed, 5 Oct 2022 18:37:45 -0700 Subject: [PATCH 09/11] Fix dynamic linking --- src/library_exceptions.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/library_exceptions.js b/src/library_exceptions.js index 20d9940f9467b..511030bab2791 100644 --- a/src/library_exceptions.js +++ b/src/library_exceptions.js @@ -414,7 +414,14 @@ var LibraryExceptions = { #endif #if WASM_EXCEPTIONS $getCppExceptionTag: function() { + // In static linking, tags are defined within the wasm module and are + // exported, whereas in dynamic linking, tags are defined in library.js in + // JS code and wasm modules import them. +#if RELOCATABLE + return ___cpp_exception; // defined in library.js +#else return Module['asm']['__cpp_exception']; +#endif }, #if ASSERTIONS From eb8ad2afeacb4e032632a535b98821305ddbf1ff Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Wed, 5 Oct 2022 20:11:08 -0700 Subject: [PATCH 10/11] Add libc++abi-debug-mt-noexcept to MINIMAL_PIC_TASKS --- embuilder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/embuilder.py b/embuilder.py index 467f046049a9e..86d6e9f11da2e 100755 --- a/embuilder.py +++ b/embuilder.py @@ -79,6 +79,7 @@ 'libc_optz-mt-debug', 'libc++abi-mt', 'libc++abi-mt-noexcept', + 'libc++abi-debug-mt-noexcept', 'libc++-mt', 'libc++-mt-noexcept', 'libdlmalloc-mt', From f9654eabc9c1d412221c9a344570e78fea9f6700 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Wed, 5 Oct 2022 21:26:55 -0700 Subject: [PATCH 11/11] Add libc++abi-debug-mt to MINIMAL_PIC_TASKS --- embuilder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/embuilder.py b/embuilder.py index 86d6e9f11da2e..51eb7fbfdf10e 100755 --- a/embuilder.py +++ b/embuilder.py @@ -79,6 +79,7 @@ 'libc_optz-mt-debug', 'libc++abi-mt', 'libc++abi-mt-noexcept', + 'libc++abi-debug-mt', 'libc++abi-debug-mt-noexcept', 'libc++-mt', 'libc++-mt-noexcept',