emscripten-core / emscripten-core/emscripten

C++ exception in promise is freed prematurely, and destructor does not run

Open
#27,705 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
C++
Stars
27.6k
Forks
3.6k
Avg merge
1d 1h
Merged PRs (30d)
105

Description

With Emscripten 6.0.9, a C++ exception escaping an asynchronous `val` Promise is freed before it reaches a JS caller. This happens in the `val::promise` logic in the `~val` destructor of the error it rejects with (obtained via `_emval_from_current_cxa_exception`).

Moreover, even though the memory is freed, the destructor of the C++ exception never gets run.

Given `mwe.cpp`:
```cpp
#include
#include
#include
#include
#include

using emscripten::val;

class MyError : public std::exception {
volatile std::uint32_t check{0xdeadbeef};
public:
MyError() { std::clog << "MyError" << std::endl; }
~MyError() { std::clog << "~MyError" << std::endl; check = 0; }
const char *what() const noexcept override {
return check == 0xdeadbeef ? "MyError (alive)" : "MyError (USE-AFTER-FREE!)";
}
};

/// Coroutine that rejects its Promise with a C++ exception
val rejectAsync() {
// Ensure a real suspension happens first, so the exception is thrown from a resumed coroutine
co_await val::global("Promise").call("resolve");
throw MyError();
}

void throwSync() {
throw MyError();
}

EMSCRIPTEN_BINDINGS(mwe) {
emscripten::function("rejectAsync", &rejectAsync);
emscripten::function("throwSync", &throwSync);
}
```

And `test.mjs`:
```js
#!/usr/bin/env node
import initModule from './mwe.mjs';
const Module = await initModule();

function describe(label, ex) {
console.log(`${label}: caught`, ex?.constructor?.name);
try {
console.log(`${label}: getExceptionMessage =`, Module.getExceptionMessage(ex));
Module.decrementExceptionRefcount(ex);
console.log(`${label}: decrementExceptionRefcount OK`);
} catch (e) {
console.log(`${label}: FAILED:`, e);
}
}

try { Module.throwSync(); } catch (ex) { describe('sync', ex); }

try { await Module.rejectAsync(); } catch (ex) { describe('async', ex); }
```

```shell
em++ mwe.cpp -o mwe.mjs -std=c++20 -lembind -fwasm-exceptions -sEXPORTED_RUNTIME_METHODS=getExceptionMessage,decrementExceptionRefcount -v
```

Compile log

```
/home/swdv/emsdk/upstream/bin/clang --version
shared:INFO: (Emscripten: Running sanity checks)
"/home/swdv/emsdk/upstream/bin/clang++" -target wasm32-unknown-emscripten -mllvm -combiner-global-alias-analysis=false -mllvm -wasm-enable-sjlj -mllvm -wasm-use-legacy-eh -mllvm -disable-lsr --sysroot=/home/swdv/emsdk/upstream/emscripten/cache/sysroot -Xclang -iwithsysroot/include/fakesdl -Xclang -iwithsysroot/include/compat -std=c++20 -fwasm-exceptions -v -c mwe.cpp -o /tmp/emscripten_temp_p7hbp8i3/mwe.o
clang version 24.0.0git (https:/github.com/llvm/llvm-project 510126255f89d693717c1ce7105b593f995f07c1)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: /home/swdv/emsdk/upstream/bin
(in-process)
"/home/swdv/emsdk/upstream/bin/clang-24" -cc1 -triple wasm32-unknown-emscripten -emit-obj -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name mwe.cpp -mrelocation-model static -mframe-pointer=none -ffp-contract=on -fno-rounding-math -mconstructor-aliases -target-feature +exception-handling -target-feature +multivalue -target-feature +reference-types -exception-model=wasm -mllvm -wasm-enable-eh -target-cpu generic -fvisibility=hidden -debugger-tuning=gdb -fdebug-compilation-dir=/tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe -v -fcoverage-compilation-dir=/tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe -resource-dir /home/swdv/emsdk/upstream/lib/clang/24 -isysroot /home/swdv/emsdk/upstream/emscripten/cache/sysroot -internal-isystem /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten/eh/c++/v1 -internal-isystem /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten/c++/v1 -internal-isystem /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/c++/v1 -internal-isystem /home/swdv/emsdk/upstream/lib/clang/24/include -internal-isystem /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten -internal-isystem /home/swdv/emsdk/upstream/emscripten/cache/sysroot/include -std=c++20 -fdeprecated-macro -ferror-limit 19 -fmessage-length=382 -fgnuc-version=4.2.1 -fno-implicit-modules -fskip-odr-check-in-gmf -fcxx-exceptions -fexceptions -exception-model=wasm -iwithsysroot/include/fakesdl -iwithsysroot/include/compat -mllvm -combiner-global-alias-analysis=false -mllvm -wasm-enable-sjlj -mllvm -wasm-use-legacy-eh -mllvm -disable-lsr -o /tmp/emscripten_temp_p7hbp8i3/mwe.o -x c++ mwe.cpp
clang -cc1 version 24.0.0git based upon LLVM 24.0.0git default target x86_64-unknown-linux-gnu
ignoring nonexistent directory "/home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten/eh/c++/v1"
ignoring nonexistent directory "/home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten/c++/v1"
ignoring nonexistent directory "/home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/wasm32-emscripten"
#include "..." search starts here:
#include <...> search starts here:
/home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/fakesdl
/home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/compat
/home/swdv/emsdk/upstream/emscripten/cache/sysroot/include/c++/v1
/home/swdv/emsdk/upstream/lib/clang/24/include
/home/swdv/emsdk/upstream/emscripten/cache/sysroot/include
End of search list.
/home/swdv/emsdk/node/24.19.0_64bit/bin/node /home/swdv/emsdk/upstream/emscripten/tools/compiler.mjs - --symbols-only
/home/swdv/emsdk/upstream/bin/wasm-ld -o mwe.wasm /tmp/tmp72zg_dtnlibemscripten_js_symbols.so -Bstatic --strip-debug --export=emscripten_stack_get_end --export=emscripten_stack_get_free --export=emscripten_stack_get_base --export=emscripten_stack_get_current --export=emscripten_stack_init --export=_emscripten_stack_alloc --export=__getTypeName --export=__trap --export=__wasm_call_ctors --export=__get_exception_message --export=free --export=_emscripten_stack_restore --export=__thrown_object_from_unwind_exception --export=__cpp_exception --export=__cxa_decrement_exception_refcount --export-if-defined=__start_em_asm --export-if-defined=__stop_em_asm --export-if-defined=__start_em_lib_deps --export-if-defined=__stop_em_lib_deps --export-if-defined=__start_em_js --export-if-defined=__stop_em_js --export-if-defined=main --export-if-defined=__main_argc_argv --export-if-defined=fflush --export-table -z stack-size=65536 --no-growable-memory --initial-heap=16777216 --no-entry --stack-first --table-base=1 /tmp/emscripten_temp_p7hbp8i3/mwe.o -lembind-rtti -L/home/swdv/emsdk/upstream/emscripten/cache/sysroot/lib/wasm32-emscripten -L/home/swdv/emsdk/upstream/emscripten/src/lib -lGL-getprocaddr -lal -lhtml5 -lstubs-debug -lnoexit -lc-debug -ldlmalloc-debug -lclang_rt.builtins-legacysjlj -lc++-debug-legacyexcept -lc++abi-debug-legacyexcept -lunwind-legacyexcept -lsockets -mllvm -combiner-global-alias-analysis=false -mllvm -wasm-enable-sjlj -mllvm -wasm-use-legacy-eh -mllvm -disable-lsr -mllvm -wasm-enable-eh -mllvm -exception-model=wasm
/home/swdv/emsdk/upstream/bin/llvm-objcopy mwe.wasm mwe.wasm '--remove-section=llvm.func_attr.annotate.*' --remove-section=producers --remove-section=name '--remove-section=.debug*'
/home/swdv/emsdk/node/24.19.0_64bit/bin/node /home/swdv/emsdk/upstream/emscripten/tools/compiler.mjs -
/home/swdv/emsdk/node/24.19.0_64bit/bin/node /home/swdv/emsdk/upstream/emscripten/tools/preprocessor.mjs - modularize.js --expand-macros
```

The `rejectAsync` case does not behave as expected:

```
$ ./test.mjs
MyError
sync: caught Exception
sync: getExceptionMessage = [ 'MyError', 'MyError (alive)' ]
~MyError
sync: decrementExceptionRefcount OK
MyError
async: caught Exception
async: FAILED: RuntimeError: memory access out of bounds
at wasm://wasm/0011e712:wasm-function[1692]:0x3fbc6
at file:///tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe/mwe.mjs:564:12
at getExceptionMessageCommon (file:///tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe/mwe.mjs:881:7)
at Object.getExceptionMessage (file:///tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe/mwe.mjs:896:14)
at describe (file:///tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe/test.mjs:9:59)
at file:///tmp/claude-1000/-home-swdv-pep-core/3161dffd-f72e-47c9-a01e-f3118c6f3059/scratchpad/mwe/test.mjs:19:50
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.