From 51c174eaa78f197c745c360bb5481037cbe698f9 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Fri, 19 Dec 2025 09:37:12 +0000 Subject: [PATCH 1/6] [EH] Fix inconsistency of in/decrementing refcounts This fixes inconsistency of incrementing/decrementing refcounts between Wasm EH and Emscripten EH. Previously, in Emscripten EH, we incremented the refcount in `__cxa_begin_catch`, while Wasm EH incremented it in `__cxa_throw`. This PR moves the incrementing call from `__cxa_begin_catch` to `__cxa_throw` as well. This also increments the refcount in `__cxa_rethrow`. These incrementing calls are guarded with `+#if !DISABLE_EXCEPTION_CATCHING`, because without that, `std::terminate` will run: https://github.com/emscripten-core/emscripten/blob/d1251798144df813c52934768964a1223504c440/system/lib/libcxxabi/src/cxa_noexception.cpp#L25-L35 Fixes #17115. --- src/lib/libexceptions.js | 22 +++++++++++++++++----- test/test_core.py | 14 ++------------ 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/lib/libexceptions.js b/src/lib/libexceptions.js index 6d55a107eee4d..216fc4cb4aadb 100644 --- a/src/lib/libexceptions.js +++ b/src/lib/libexceptions.js @@ -82,7 +82,11 @@ var LibraryExceptions = { // Here, we throw an exception after recording a couple of values that we need to remember // We also remember that it was the last exception thrown as we need to know that later. - __cxa_throw__deps: ['$ExceptionInfo', '$exceptionLast', '$uncaughtExceptionCount'], + __cxa_throw__deps: ['$ExceptionInfo', '$exceptionLast', '$uncaughtExceptionCount' +#if !DISABLE_EXCEPTION_CATCHING + , '__cxa_increment_exception_refcount' +#endif + ], __cxa_throw: (ptr, type, destructor) => { #if EXCEPTION_DEBUG dbg('__cxa_throw: ' + [ptrToString(ptr), type, ptrToString(destructor)]); @@ -90,6 +94,9 @@ var LibraryExceptions = { var info = new ExceptionInfo(ptr); // Initialize ExceptionInfo content after it was allocated in __cxa_allocate_exception. info.init(type, destructor); +#if !DISABLE_EXCEPTION_CATCHING + ___cxa_increment_exception_refcount(ptr); +#endif {{{ storeException('exceptionLast', 'ptr') }}} uncaughtExceptionCount++; {{{ makeThrow('exceptionLast') }}} @@ -98,7 +105,11 @@ var LibraryExceptions = { // This exception will be caught twice, but while begin_catch runs twice, // we early-exit from end_catch when the exception has been rethrown, so // pop that here from the caught exceptions. - __cxa_rethrow__deps: ['$exceptionCaught', '$exceptionLast', '$uncaughtExceptionCount'], + __cxa_rethrow__deps: ['$exceptionCaught', '$exceptionLast', '$uncaughtExceptionCount' +#if !DISABLE_EXCEPTION_CATCHING + , '__cxa_increment_exception_refcount' +#endif + ], __cxa_rethrow: () => { var info = exceptionCaught.pop(); if (!info) { @@ -112,6 +123,9 @@ var LibraryExceptions = { info.set_caught(false); uncaughtExceptionCount++; } +#if !DISABLE_EXCEPTION_CATCHING + ___cxa_increment_exception_refcount(ptr); +#endif #if EXCEPTION_DEBUG dbg('__cxa_rethrow, popped ' + [ptrToString(ptr), exceptionLast, 'stack', exceptionCaught]); @@ -122,8 +136,7 @@ var LibraryExceptions = { llvm_eh_typeid_for: (type) => type, - __cxa_begin_catch__deps: ['$exceptionCaught', '__cxa_increment_exception_refcount', - '__cxa_get_exception_ptr', + __cxa_begin_catch__deps: ['$exceptionCaught', '__cxa_get_exception_ptr', '$uncaughtExceptionCount'], __cxa_begin_catch: (ptr) => { var info = new ExceptionInfo(ptr); @@ -136,7 +149,6 @@ var LibraryExceptions = { #if EXCEPTION_DEBUG dbg('__cxa_begin_catch ' + [ptrToString(ptr), 'stack', exceptionCaught]); #endif - ___cxa_increment_exception_refcount(ptr); return ___cxa_get_exception_ptr(ptr); }, diff --git a/test/test_core.py b/test/test_core.py index 72586881b214c..d776b2a31337e 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -1551,18 +1551,8 @@ class myexception : public exception { } catch(p) { // Because we are catching and handling the exception in JS, the normal // exception catching C++ code doesn't kick in, so we need to make sure we free - // the exception, if necessary. By incrementing and decrementing the refcount - // we trigger the free'ing of the exception if its refcount was zero. -#ifdef __USING_EMSCRIPTEN_EXCEPTION__ - // FIXME Currently Wasm EH and Emscripten EH increases - // refcounts in different places. Wasm EH sets the refcount to - // 1 when throwing, and decrease it in __cxa_end_catch. - // Emscripten EH sets the refcount to 0 when throwing, and - // increase it in __cxa_begin_catch, and decrease it in - // __cxa_end_catch. Fix this inconsistency later. - // https://github.com/emscripten-core/emscripten/issues/17115 - incrementExceptionRefcount(p); -#endif + // the exception, if necessary. By decrementing the refcount we trigger the + // free'ing of the exception. out(getExceptionMessage(p).toString()); decrementExceptionRefcount(p); } From 9fee4820d5db61f05d58eda465c86dc0011c749b Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Mon, 22 Dec 2025 10:04:02 +0000 Subject: [PATCH 2/6] Remove mentions of the inconsistency --- site/source/docs/porting/exceptions.rst | 10 ++++------ src/settings.js | 4 +--- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/site/source/docs/porting/exceptions.rst b/site/source/docs/porting/exceptions.rst index 4dffc6fe76264..638353eebab41 100644 --- a/site/source/docs/porting/exceptions.rst +++ b/site/source/docs/porting/exceptions.rst @@ -155,12 +155,10 @@ leaked. .. note:: If you catch a Wasm exception and do not rethrow it, you need to free the storage associated with the exception in JS using - ``decrementExceptionRefcount`` method because the exception - catching code in Wasm does not have a chance to free it. But currently due to - an implementation issue that Wasm EH and Emscripten (JS-based) EH, you need - to call incrementExceptionRefcount additionally in case of Emscripten EH. See - https://github.com/emscripten-core/emscripten/issues/17115 for details and a - code example. + ``decrementExceptionRefcount`` method because the exception catching code in + Wasm does not have a chance to free it. See + ``test_EXPORT_EXCEPTION_HANDLING_HELPERS`` in test/test_core.py for an + example usage. .. todo:: Fix the above-mentinoed `inconsistency `_ between Wasm diff --git a/src/settings.js b/src/settings.js index 1ba3b7856b8da..1dd3251efa165 100644 --- a/src/settings.js +++ b/src/settings.js @@ -761,9 +761,7 @@ var DISABLE_EXCEPTION_THROWING = false; // Setting this option also adds refcount increasing and decreasing functions // ('incrementExceptionRefcount' and 'decrementExceptionRefcount') in the JS // library because if you catch an exception from JS, you may need to manipulate -// the refcount manually not to leak memory. What you need to do is different -// depending on the kind of EH you use -// (https://github.com/emscripten-core/emscripten/issues/17115). +// the refcount manually not to leak memory. // // See test_EXPORT_EXCEPTION_HANDLING_HELPERS in test/test_core.py for an // example usage. From 2dd06bdec731ea142de5340a82ec5daaec384f8c Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Mon, 22 Dec 2025 10:08:47 +0000 Subject: [PATCH 3/6] Run tools/maint/update_settings_docs.py --- site/source/docs/tools_reference/settings_reference.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/site/source/docs/tools_reference/settings_reference.rst b/site/source/docs/tools_reference/settings_reference.rst index 77871dc3d80af..76ac6dab2fb7b 100644 --- a/site/source/docs/tools_reference/settings_reference.rst +++ b/site/source/docs/tools_reference/settings_reference.rst @@ -1127,9 +1127,7 @@ the JS library: Setting this option also adds refcount increasing and decreasing functions ('incrementExceptionRefcount' and 'decrementExceptionRefcount') in the JS library because if you catch an exception from JS, you may need to manipulate -the refcount manually not to leak memory. What you need to do is different -depending on the kind of EH you use -(https://github.com/emscripten-core/emscripten/issues/17115). +the refcount manually not to leak memory. See test_EXPORT_EXCEPTION_HANDLING_HELPERS in test/test_core.py for an example usage. From c56eade9d6ba64ce29a3bd19873d3b0eb207135c Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Mon, 22 Dec 2025 10:17:59 +0000 Subject: [PATCH 4/6] Remove todo --- site/source/docs/porting/exceptions.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/site/source/docs/porting/exceptions.rst b/site/source/docs/porting/exceptions.rst index 638353eebab41..27e97ee6dad8d 100644 --- a/site/source/docs/porting/exceptions.rst +++ b/site/source/docs/porting/exceptions.rst @@ -160,10 +160,6 @@ leaked. ``test_EXPORT_EXCEPTION_HANDLING_HELPERS`` in test/test_core.py for an example usage. -.. todo:: Fix the above-mentinoed `inconsistency - `_ between Wasm - EH and Emscripten EH, on the reference counting. - Using Exceptions and setjmp-longjmp Together ============================================ From 7ac3b79d5f6748b33bf5dc0d4d750f863ea89247 Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Tue, 23 Dec 2025 00:30:55 +0000 Subject: [PATCH 5/6] ChangeLog --- ChangeLog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index f3a4fc16e1488..78521ed8c915b 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -20,6 +20,10 @@ See docs/process.md for more on how version tagging works. 4.0.23 (in development) ----------------------- +- The inconsistency of incrementing / decrementing refcounts between Wasm EH and + Emscripten EH has been fixed. See `test_EXPORT_EXCEPTION_HANDLING_HELPERS` in + `test_core.py` to see the usage. (#25988) + 4.0.22 - 12/18/25 ----------------- From fa4f62c4346344ceade9a5b27c0f8f833546e51e Mon Sep 17 00:00:00 2001 From: Heejin Ahn Date: Tue, 23 Dec 2025 22:39:11 +0000 Subject: [PATCH 6/6] Remove a newline in ChangeLog --- ChangeLog.md | 1 - 1 file changed, 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 02b7ac40b6fb7..21cadb5441db6 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -23,7 +23,6 @@ See docs/process.md for more on how version tagging works. - The inconsistency of incrementing / decrementing refcounts between Wasm EH and Emscripten EH has been fixed. See `test_EXPORT_EXCEPTION_HANDLING_HELPERS` in `test_core.py` to see the usage. (#25988) - - The `select()` and `poll()` system calls can now block under certain circumstances. Specifically, if they are called from a background thread and file descriptors include pipes. (#25523, #25990)