Skip to content

Uncaught C++ exceptions in pthread workers on Chromium 149 when using JS exceptions #27084

@tsakirist

Description

@tsakirist

We have a heavily multithreaded application that is built with JS exception support and after the Chromium update to 149 that was pushed recently, it started surfacing uncaught exceptions from pthread workers.

  • Chromium 149 (Chrome, Edge) -> broken
  • Chromium 148 -> works
  • Firefox 151 -> works

We started getting:

Uncaught CppException:
    at ___cxa_throw

Version of emscripten/emsdk:

emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 4.0.19 (08e2de1031913e4ba7963b1c56f35f036a7d4d56)
clang version 22.0.0git (https:/github.com/llvm/llvm-project 12f392cff10fcc70b4ec4f01ab386922742e9136)
Target: wasm32-unknown-emscripten
Thread model: posix

Have also tested with latest 6.0.0

Full build command and output with -v appended for the MRE:
em++ -v -std=c++20 -pthread -sSTACK_SIZE=1MB -sINITIAL_MEMORY=8MB -sALLOW_MEMORY_GROWTH=1 -sPTHREAD_POOL_SIZE_STRICT=0 -sPTHREADS_DEBUG -sDISABLE_EXCEPTION_CATCHING=0 main.cpp -o main.html

Verbose output of build command
"D:/emsdk/upstream/bin\clang++.exe" -target wasm32-unknown-emscripten -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-cxx-exceptions -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr '--sysroot=D:\emsdk\upstream\emscripten\cache\sysroot' -D__EMSCRIPTEN_SHARED_MEMORY__=1 -DEMSCRIPTEN -Xclang '-iwithsysroot/include\fakesdl' -Xclang '-iwithsysroot/include\compat' -v -std=c++20 -pthread -c main.cpp -o 'C:\Users\TmpUser~1\AppData\Local\Temp\1\emscripten_temp_o_9e2v1x\main.o'
clang version 22.0.0git (https:/github.com/llvm/llvm-project 12f392cff10fcc70b4ec4f01ab386922742e9136)
Target: wasm32-unknown-emscripten
Thread model: posix
InstalledDir: D:\emsdk\upstream\bin
 (in-process)
 "D:\\emsdk\\upstream\\bin\\clang++.exe" -cc1 -triple wasm32-unknown-emscripten -emit-obj -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name main.cpp -mrelocation-model static -mframe-pointer=none -ffp-contract=on -fno-rounding-math -mconstructor-aliases -target-feature +atomics -target-feature +bulk-memory -target-feature +mutable-globals -target-feature +sign-ext -target-cpu generic -fvisibility=hidden -debugger-tuning=gdb "-fdebug-compilation-dir=D:\\Sources\\emscripten-exception-mre\\minimal" -v "-fcoverage-compilation-dir=D:\\Sources\\emscripten-exception-mre\\minimal" -resource-dir "D:\\emsdk\\upstream\\lib\\clang\\22" -D __EMSCRIPTEN_SHARED_MEMORY__=1 -D EMSCRIPTEN -isysroot "D:\\emsdk\\upstream\\emscripten\\cache\\sysroot" -internal-isystem "D:\\emsdk\\upstream\\emscripten\\cache\\sysroot/include/wasm32-emscripten/c++/v1" -internal-isystem "D:\\emsdk\\upstream\\emscripten\\cache\\sysroot/include/c++/v1" -internal-isystem "D:\\emsdk\\upstream\\lib\\clang\\22\\include" -internal-isystem "D:\\emsdk\\upstream\\emscripten\\cache\\sysroot/include/wasm32-emscripten" -internal-isystem "D:\\emsdk\\upstream\\emscripten\\cache\\sysroot/include" -std=c++20 -fdeprecated-macro -ferror-limit 19 -fmessage-length=269 -pthread -fgnuc-version=4.2.1 -fno-implicit-modules -fskip-odr-check-in-gmf -fcxx-exceptions -fexceptions -fcolor-diagnostics "-iwithsysroot/include\\fakesdl" "-iwithsysroot/include\\compat" -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-cxx-exceptions -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr -o "C:\\Users\\TmpUser~1\\AppData\\Local\\Temp\\1\\emscripten_temp_o_9e2v1x\\main.o" -x c++ main.cpp
clang -cc1 version 22.0.0git based upon LLVM 22.0.0git default target x86_64-pc-windows-msvc
ignoring nonexistent directory "D:\emsdk\upstream\emscripten\cache\sysroot/include/wasm32-emscripten/c++/v1"
ignoring nonexistent directory "D:\emsdk\upstream\emscripten\cache\sysroot/include/wasm32-emscripten"
#include "..." search starts here:
#include <...> search starts here:
 D:\emsdk\upstream\emscripten\cache\sysroot/include\fakesdl
 D:\emsdk\upstream\emscripten\cache\sysroot/include\compat
 D:\emsdk\upstream\emscripten\cache\sysroot/include/c++/v1
 D:\emsdk\upstream\lib\clang\22\include
 D:\emsdk\upstream\emscripten\cache\sysroot/include
End of search list.
 "D:/emsdk/upstream/bin\clang.exe" --version
em++: warning: -pthread + ALLOW_MEMORY_GROWTH may run non-wasm code slowly, see https://github.com/WebAssembly/design/issues/1271 [-Wpthreads-mem-growth]
 "D:/emsdk/upstream/bin\wasm-ld.exe" -o main.wasm 'C:\Users\TmpUser~1\AppData\Local\Temp\1\emscripten_temp_o_9e2v1x\main.o' '-LD:\emsdk\upstream\emscripten\cache\sysroot\lib\wasm32-emscripten' '-LD:\emsdk\upstream\emscripten\src\lib' 'D:\emsdk\upstream\emscripten\cache\sysroot\lib\wasm32-emscripten\crtbegin.o' -lGL-mt-getprocaddr -lal -lhtml5 -lstubs-debug -lnoexit -lc-mt-debug -ldlmalloc-mt-debug -lcompiler_rt-mt -lc++-debug-mt -lc++abi-debug-mt -lsockets-mt -mllvm -combiner-global-alias-analysis=false -mllvm -enable-emscripten-cxx-exceptions -mllvm -enable-emscripten-sjlj -mllvm -disable-lsr 'C:\Users\TmpUser~1\AppData\Local\Temp\1\tmp53ofs8_9libemscripten_js_symbols.so' --import-memory --shared-memory --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=_emscripten_thread_free_data --export=_emscripten_thread_crashed --export=__cxa_can_catch --export=__cxa_increment_exception_refcount --export=__cxa_decrement_exception_refcount --export=setThrew --export=__cxa_free_exception --export=__wasm_call_ctors --export=_emscripten_tls_init --export=_emscripten_thread_init --export=_emscripten_stack_restore --export=emscripten_stack_set_limits --export=_emscripten_thread_exit --export=__get_exception_message --export=free --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=1048576 --max-memory=2147483648 --initial-memory=8388608 --no-entry --stack-first --table-base=1
 "D:/emsdk/upstream/bin\llvm-objcopy.exe" main.wasm main.wasm '--remove-section=.debug*' --remove-section=producers --remove-section=name
 "D:/emsdk/node/22.16.0_64bit/bin/node.exe" 'D:\emsdk\upstream\emscripten\tools\compiler.mjs' -
 "D:/emsdk/node/22.16.0_64bit/bin/node.exe" 'D:\emsdk\upstream\emscripten\tools\acorn-optimizer.mjs' 'C:\Users\TmpUser~1\AppData\Local\Temp\1\emscripten_temp_o_9e2v1x\main.js' growableHeap --closure-friendly -o 'C:\Users\TmpUser~1\AppData\Local\Temp\1\emscripten_temp_o_9e2v1x\main.jso1.js'
 "D:/emsdk/node/22.16.0_64bit/bin/node.exe" 'D:\emsdk\upstream\emscripten\tools\preprocessor.mjs' - shell.html

MRE
The MRE uses INITIAL_MEMORY=8MB and STACK_SIZE=1MB so that spawning 8 threads forces a memory growth.

Running the MRE produces Uncaught RuntimeError: memory access out of bounds, while our main application surfaces Uncaught CppException. But I think these are likely symptoms of the same underlying issue.

#include <iostream>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>

#include <emscripten.h>

void setupIOThreadPool(unsigned int numThreads) {
  std::vector<std::thread> threads;
  for (unsigned int i = 0; i < numThreads; ++i) {
    threads.emplace_back([]() {
      while (true) {
        // Simulate some work
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
      }
    });
  }

  for (auto &t : threads) {
    t.detach();
  }
}

void testThrowCatchInThreads(unsigned int numThreads) {
  std::vector<std::thread> threads;
  threads.reserve(numThreads);
  for (unsigned int i = 0; i < numThreads; ++i) {
    threads.emplace_back([i]() {
      try {
        throw std::logic_error("Logic error Thread-" + std::to_string(i));
      } catch (std::exception &e) {
        std::cout << "Thread-" << i << " caught exception: " << e.what()
                  << "\n";
      }
    });
  }

  for (auto &t : threads) {
    t.detach();
  }
}

int main() {
  const auto numOfPoolThreads{4};
  const auto numOfTestThreads{4};

  setupIOThreadPool(numOfPoolThreads);
  testThrowCatchInThreads(numOfTestThreads);

  emscripten_exit_with_live_runtime();
}

Increasing INITIAL_MEMORY to avoid growth in the MRE works around the issue, but a memory growth can happen at any point during runtime, so in theory this can resurface again.

Switching from JS exceptions to Wasm exceptions -fwasm-exceptions does not seem to reproduce the issue.

Is this a defect of JS based exceptions or Chromium regression? Or are we doing something really wrong?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions