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.

Activity

  1. added
    arch-wasmWebAssembly architecture
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on Jul 23, 2026
  2. dotnet-policy-service commented on Jul 23, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
    See info in area-owners.md if you want to be subscribed.

  3. dotnet-policy-service commented on Jul 23, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
    See info in area-owners.md if you want to be subscribed.

  4. self-assigned this
    on Jul 23, 2026
  5. removed
    untriagedNew issue has not been triaged by the area owner
    on Jul 23, 2026
  6. added a commit that references this issue on Jul 23, 2026
    304d1cc
  7. added a commit that references this issue on Jul 26, 2026
    d4817a8
  8. locked and limited conversation to collaborators on Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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