Skip to content

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

Description

@stevenwdv

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:

#include <emscripten/bind.h>
#include <emscripten/val.h>
#include <iostream>
#include <stdexcept>
#include <string>

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<val>("resolve");
  throw MyError();
}

void throwSync() {
  throw MyError();
}

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

And test.mjs:

#!/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); }
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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions