[WebAssembly] CFGStackify addNestedTryTable crashes / emits invalid br_table on a C++20 coroutine resume function under exnref EH
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
Compiling a C++20 coroutine that `co_await`s another coroutine (whose exception propagates
across the `co_await` into a `try`/`catch`) with the **standardized (exnref) Wasm exception
model** produces broken output on all optimization levels ≥ `-O1`:
- **`-O1` / `-O2`**: clang crashes in the `WebAssembly CFG Stackify` pass, in
`WebAssemblyCFGStackifyImpl::addNestedTryTable`, on the coroutine *resume* function
(`@_Z5outeri.resume`).
- **`-Oz`**: no crash, but the emitted module is **invalid wasm** — the coroutine resume
`br_table` has inconsistent target arity. `wasm-opt` and V8 both reject it.
The legacy Wasm EH path (`-sWASM_LEGACY_EXCEPTIONS=1`) and `-O0` are unaffected. This blocks
adopting `WASM_LEGACY_EXCEPTIONS=0` for any coroutine-using codebase that catches exceptions
around `co_await`.
## Reproducer (`repro.cpp`, self-contained, no third-party deps)
```cpp
#include
#include
#include
#include
template
struct Task {
struct promise_type {
T value{};
std::exception_ptr err{};
Task get_return_object() { return Task{std::coroutine_handle::from_promise(*this)}; }
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_value(T v) { value = v; }
void unhandled_exception() { err = std::current_exception(); }
};
std::coroutine_handle h;
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<> c) noexcept { c.resume(); }
T await_resume() {
if (h.promise().err) std::rethrow_exception(h.promise().err);
return h.promise().value;
}
};
Task inner(int x) {
if (x == 1) throw std::runtime_error("boom"); // surfaces at the co_await in outer()
co_return x * 2;
}
Task outer(int n) {
for (int attempt = 0; attempt < 2; ++attempt) {
try {
int r = co_await inner(n + attempt);
if (r > 0) {
co_return r;
}
} catch (const std::exception& e) {
printf("caught: %s\n", e.what());
} catch (...) {
printf("caught unknown\n");
}
}
co_return -1;
}
int main() {
auto t = outer(0);
printf("done %d\n", t.h.promise().value);
return 0;
}
```
## Steps
```bash
# invalid wasm (br_table arity):
em++ -std=c++20 -fwasm-exceptions -sWASM_LEGACY_EXCEPTIONS=0 -Oz repro.cpp -o repro.js
# clang crash:
em++ -std=c++20 -fwasm-exceptions -sWASM_LEGACY_EXCEPTIONS=0 -O1 repro.cpp -o repro.js
```
## Observed
`-Oz`:
```
[parse exception: popping from empty stack (at 0:1672)]
Fatal: error parsing wasm (try --debug for more info)
em++: error: '.../bin/wasm-opt ... --enable-exception-handling ... --enable-reference-types ...' failed (returned 1)
```
Validating the `wasm-ld` output directly in V8:
```
CompileError: WebAssembly.Module(): Compiling function #10 ("outer(int) (.resume)") failed:
br_table: label arity inconsistent with previous arity 0 @+1670
```
`-O1` / `-O2`:
```
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ ...
3. Running pass 'Function Pass Manager' ...
4. Running pass 'WebAssembly CFG Stackify' on function '@_Z5outeri.resume'
#5 (anonymous namespace)::WebAssemblyCFGStackifyImpl::addNestedTryTable(llvm::MachineInstr*, ...)
#6 (anonymous namespace)::WebAssemblyCFGStackifyImpl::placeMarkers(llvm::MachineFunction&)
#7 (anonymous namespace)::WebAssemblyCFGStackifyImpl::runOnMachineFunction(llvm::MachineFunction&)
```
## Condition matrix
| opt | exnref (`WASM_LEGACY_EXCEPTIONS=0`) | legacy (`=1`) |
|-----|-------------------------------------|----------------|
| -O0 | OK | OK |
| -O1 | **clang crash** (addNestedTryTable) | OK |
| -O2 | **clang crash** (addNestedTryTable) | OK |
| -Oz | **invalid `br_table` wasm** | OK |
## Expected
Valid wasm (as with legacy EH / `-O0`).
## Notes on the trigger
- Requires a coroutine that `co_await`s **another coroutine** whose exception crosses the
`co_await` into a `try`. A `co_await` of a plain (non-coroutine) awaiter, or removing the
`try`, does not reproduce.
- The failing function is the coroutine **resume** function; the bad `br_table` is the
coroutine resume dispatch, whose EH-cleanup edge is given an inconsistent arity when the
exnref `try_table` markers are placed.
## Environment
- Emscripten 6.0.7 (`emcc ... 6.0.7 (4483d70a78098ed5d860dff2dc21f3025b2da2ee)`)
- clang 24.0.0git — llvm-project rev `4cc02503f584aad493a1d0d35bb5afb710a5510b`
- Binaryen (`wasm-opt`) v132 (`version_129-327-g8d9412ef6`)
- Host: macOS arm64
Contributor guide
Assessment
This issue has not been assessed yet.