Skip to content

[wasm] R2R codegen: async try/return/catch MoveNext traps at runtime (RuntimeError: unreachable) on normal-completion branch #131285

Description

@lewing

Description

On WebAssembly R2R (crossgen), an async method whose try block completes normally by branching out early (a return after an await) inside a try { ... } catch { } generates a valid wasm module that traps at runtime with RuntimeError: unreachable on the normal-completion path.

This is a codegen defect in the CoreCLR wasm JIT control-flow lowering (Compiler::fgWasmControlFlow() in src/coreclr/jit/fgwasm.cpp). The normal-completion branch of the state-machine MoveNext is bound to a wasm Block that ends inside the exception-ref wrapper funclet, so the branch resolves to depth 0 and lands on that region's trailing validation unreachable (emitted right after its end by genEmitStartBlock) instead of the continuation.

Note: this is a separate bug from #131279 (dropped terminal end producing an invalid module) and is not a reopening of the (closed) #129335. Here the module is well-formed and validates; it simply traps at runtime.

Reproduction Steps

Compile the following to wasm R2R (crossgen, TargetOS=browser) and run under node:

using System;
using System.Threading.Tasks;

class Program
{
    static void Main() => RunAsync().GetAwaiter().GetResult();

    static async Task RunAsync()
    {
        try
        {
            await Task.CompletedTask;
            return;
        }
        catch (Exception ex)
        {
            GC.KeepAlive(ex);
        }
    }
}

A synchronously-completing await (Task.CompletedTask) is deliberate: the trap is pure codegen and does not require a real suspend/resume, and a completed await avoids blocking the single wasm thread. The defect reproduces only under crossgen wasm R2R; all other targets pass trivially (ordinary JIT/interp).

Expected behavior

RunAsync completes normally and the process exits cleanly.

Actual behavior

The wasm module validates but traps at runtime:

RuntimeError: unreachable
  at RunAsync MoveNext (wasm-function[2])

Disassembly shows the normal-completion branch emitted as a depth-0 branch (br 0) that lands on the enclosing try's trailing unreachable. The fix flips it to br 1, landing on the continuation.

Regression?

wasm R2R codegen is new/in-development; not a regression from a shipping release.

Known Workarounds

None at the source level.

Configuration

  • CoreCLR wasm R2R (crossgen2, CrossGen2OutputFormat=wasm), TargetOS=browser.
  • Reproduced on osx.arm64 host building the browser-wasm R2R leg; runtime trap observed under node --experimental-wasm-exnref.

Other information

Root cause and fix: in fgWasmControlFlow(), a non-contiguous forward branch that exits a try/catch region started its enclosing Block at the innermost try's begin. When the branch also escapes an outer catch-try that shares the same end cursor (the wasm catch-resumption / resume-after-catch shape, where the paired ExnRefWrapper is stretched past the try-end to cover the resumption dispatcher), the Block ends up nested inside that outer try, so the branch resolves to depth 0 → the outer try's trailing unreachable.

The fix walks the enclosing try regions outward and starts the Block at the outermost escaped catch-try's begin, so the Block encloses those trys' ends and the branch resolves to the continuation. For a single-level try/catch the outermost escaped region is also the innermost, so behavior there is unchanged (validated: 194-assembly sweep, 0 new invalid, single-level provably unaffected).

Fix + regression test staged on branch lewing-bug-c-wasm-catch-resume-fix (touches only src/coreclr/jit/fgwasm.cpp and a new src/tests/JIT/Regression/JitBlue/ test that forces the browser leg through crossgen wasm R2R via AlwaysUseCrossGen2).

Note

This issue was generated with the assistance of GitHub Copilot.

Metadata

Metadata

Assignees

Labels

arch-wasmWebAssembly architecturearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions