emscripten-core / emscripten-core/emscripten
Missing JS library deps with `DISABLE_EXCEPTION_CATCHING=1`: closure reports `_setThrew`, `setTempRet0`, `ExceptionInfo` undeclared
- Dominant language
- C++
- Stars
- 27.6k
- Forks
- 3.6k
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 105
Description
### Version
Reproduced with emcc 5.0.7 (263db4cffa6f9fc2ec514a70abac81362ea41849). All three gates below are unchanged in 6.0.9 (latest) and on `main`.
### Setup
Link an object that has exception landing pads against the default `DISABLE_EXCEPTION_CATCHING=1`. In the repros below `-fexceptions` is the shortest way to produce such an object; it belongs on the **compile** command only, since at link time it flips the setting and hides everything.
No user flag is needed in practice: our own build hits the `setTempRet0` failure with no exception-related flag anywhere, from a retained member of emscripten's own sysroot `libc++.a`/`libc++abi.a` — `iostream.o`, `locale.o`, `stdlib_new_delete.o` and the `filesystem` objects all import `__cxa_find_matching_catch_*`, and whether it bites depends on which functions survive dead-stripping. The configuration is supported by design: `$findMatchingCatch` carries a dedicated `#if DISABLE_EXCEPTION_CATCHING` stub whose only purpose is to service those imports, and emcc emits no warning.
`--closure=1` is what makes this fatal. Without closure the link succeeds and the output JS carries the undefined references, but in everything I tried they are unreachable at runtime (with catching disabled the program halts at the throw, and the SjLj path supplies `setThrew` itself) — so as far as I can tell this is broken output caught by closure, not a silent miscompile.
#### Repro 1 — no `throw`, no `try`/`catch` (a cleanup landing pad is enough)
```cpp
// a.cpp
#include
int main() {
std::string a = std::to_string(42);
std::string b = a + a;
return b.size();
}
```
```
emcc -fexceptions -c a.cpp -O2 -o a.o
emcc a.o -O2 --closure=1 -o a.js
```
```
building:ERROR: .../a.jso1.js:501:2: ERROR - [JSC_UNDEFINED_VARIABLE] variable setTempRet0 is undeclared
building:ERROR: .../a.jso1.js:676:4: ERROR - [JSC_UNDEFINED_VARIABLE] variable _setThrew is undeclared
```
#### Repro 2 — a `catch` clause, still no `throw` in user code
```cpp
// b.cpp
#include
int main() {
try {
return std::stoi("1");
} catch (...) {
return 2;
}
}
```
→ `variable ExceptionInfo is undeclared` and `variable setTempRet0 is undeclared`.
### Three independent missing deps
**1. The `invoke_*` wrappers' supporting symbols are gated on exception catching**, in two places in [`tools/link.py`](https://github.com/emscripten-core/emscripten/blob/beac36cdff5c6b392b9beddb123fd94195a20697/tools/link.py#L1480-L1482):
```python
if settings.SUPPORT_LONGJMP == 'emscripten' or not settings.DISABLE_EXCEPTION_CATCHING:
# make_invoke depends on stackSave and stackRestore
settings.DEFAULT_LIBRARY_FUNCS_TO_INCLUDE += ['$stackSave', '$stackRestore']
```
and [L1704-1708](https://github.com/emscripten-core/emscripten/blob/beac36cdff5c6b392b9beddb123fd94195a20697/tools/link.py#L1704-L1708):
```python
# Emscripten exception handling can generate invoke calls, and they call
# setThrew(). We cannot handle this using deps_info as the invokes are not
# emitted because of library function usage, but by codegen itself.
if not settings.DISABLE_EXCEPTION_CATCHING:
settings.REQUIRED_EXPORTS += ['setThrew']
```
The second block already does exactly the right thing, and its comment states the reason this cannot go through `deps_info`: the invokes come from **codegen**, not from library function usage. But codegen emits them based on the landing pads in the input objects, which does not depend on `DISABLE_EXCEPTION_CATCHING` at all — so the condition guarding it is the wrong predicate, and with catching disabled `setThrew` is never exported even though `make_invoke` still emits `_setThrew(1, 0)`. That is the `_setThrew is undeclared` in repro 1. (With a `catch` clause in the link it is masked, because `__cxa_end_catch__deps` lists `'setThrew'`.)
This is not a new bug but the same one as #14078 (2021): `_setThrew is undeclared` under `--closure` with `-fexceptions`. The `setThrew` export block above *is* its fix — PR #14153 "Auto-export setThrew for emscripten exceptions" (`0e05ab9d`, "Fixes #14078") added the export and the comment quoted above, appending them to an `if not settings.DISABLE_EXCEPTION_CATCHING:` block that already existed for `___cxa_is_pointer_type` / `___cxa_can_catch`. So the predicate was inherited from the catch-side exports rather than chosen for the invokes — which is precisely the problem, since the invokes do not depend on catching state. The fix was right; the condition it landed under was not.
The reason that went unnoticed for five years is its regression test, [`test_exceptions_with_closure_and_without_catching`](https://github.com/emscripten-core/emscripten/blob/beac36cdff5c6b392b9beddb123fd94195a20697/test/test_other.py#L11826), which builds in a single command:
```python
self.run_process([EMXX, 'src.cpp', '-fexceptions', '--closure=1'])
```
so `-fexceptions` reaches the **link**, sets `DISABLE_EXCEPTION_CATCHING=0`, and satisfies the very gate the test is named after — despite its comment saying it checks that `setThrew` is defined "even without catching any exceptions". Splitting it into a compile step carrying `-fexceptions` and a link step without it is exactly repro 1 above, and would be the natural regression test for this report.
**2. `$setTempRet0` (regression in 5.0.5).** [`src/lib/libexceptions.js`](https://github.com/emscripten-core/emscripten/blob/beac36cdff5c6b392b9beddb123fd94195a20697/src/lib/libexceptions.js#L249-L252):
```js
#if !DISABLE_EXCEPTION_CATCHING
$findMatchingCatch__deps: ['$exceptionLast', '$ExceptionInfo', '__cxa_can_catch', '$setTempRet0'],
#endif
$findMatchingCatch: (args) => {
#if DISABLE_EXCEPTION_CATCHING
setTempRet0(0);
```
The dep is declared only when catching is **enabled**, but it is the catching-**disabled** stub that calls `setTempRet0`. 5.0.4 is fine; the `#if` was added in 5.0.5 by `5967d73d18` ("[EmscriptenEH] Always use custom JS class for C/C++ exceptions", #26523).
**3. `$ExceptionInfo` is missing from `__cxa_begin_catch__deps`** (`libexceptions.js`, long-standing — 4.0.19 has it too). `__cxa_begin_catch` does `new ExceptionInfo(ptr)`. With catching enabled the class is pulled in by `$findMatchingCatch`/`__cxa_throw`, which masks it; with catching disabled, repro 2 links `__cxa_begin_catch` without it.
### Suggested fixes
In `link.py`, both gates (the `if` on L1480 and the one on L1707) should key on whether `invoke_*` imports can appear in the wasm, not on catching state.
In `src/lib/libexceptions.js`:
```diff
--- a/src/lib/libexceptions.js
+++ b/src/lib/libexceptions.js
-#if !DISABLE_EXCEPTION_CATCHING
- $findMatchingCatch__deps: ['$exceptionLast', '$ExceptionInfo', '__cxa_can_catch', '$setTempRet0'],
-#endif
+ $findMatchingCatch__deps: ['$setTempRet0',
+#if !DISABLE_EXCEPTION_CATCHING
+ '$exceptionLast', '$ExceptionInfo', '__cxa_can_catch',
+#endif
+ ],
__cxa_begin_catch__deps: ['$exceptionCaught', '__cxa_get_exception_ptr',
- '$uncaughtExceptionCount'],
+ '$uncaughtExceptionCount', '$ExceptionInfo'],
```
I verified locally on 5.0.7 that with these three changes both repros (and a `throw`-based variant) link under `--closure=1` and run.
### Workaround
`-sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='$setTempRet0'` covers the `setTempRet0` one. The `$ExceptionInfo` and `setThrew` ones are masked as soon as anything else in the link throws (`__cxa_throw__deps` pulls `$ExceptionInfo`) or catches (`__cxa_end_catch__deps` pulls `setThrew`), which is presumably why they have gone unnoticed.
Contributor guide
Assessment
This issue has not been assessed yet.