[wasm] Fix R2R codegen trap on async try/return/catch normal-completion branch - #131287
Conversation
An async `try { ...; return; } catch {}` MoveNext compiled to WebAssembly
R2R (crossgen) produced a valid module that trapped at runtime
(RuntimeError: unreachable) on the normal-completion path.
In Compiler::fgWasmControlFlow(), a non-contiguous forward branch that
exits a try/catch region started its enclosing Block at the innermost
try's begin. When that branch also escapes an outer catch-try sharing the
same end cursor, the Block ended up nested inside the outer try, so the
`br` resolved to depth 0 and landed on the outer try's trailing
validation `unreachable` (emitted after its `end`) instead of the
continuation.
Walk the enclosing try regions outward and start 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
is unchanged there.
Adds a JitBlue regression test that forces the browser leg through
crossgen wasm R2R via AlwaysUseCrossGen2.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 964b4f25-1c41-44d6-81fb-a6a75ee7d3e4
Fills in the placeholder Runtime_NNNNN name now that the tracking issue (#131285) is filed: renames the folder, the .cs/.csproj, and the test class. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 964b4f25-1c41-44d6-81fb-a6a75ee7d3e4
|
Azure Pipelines: Successfully started running 6 pipeline(s). 10 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
Tightens the explanatory comment on the outermost-escaped-catch-try Block placement to match the terser JIT comment style. No code change. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 964b4f25-1c41-44d6-81fb-a6a75ee7d3e4
There was a problem hiding this comment.
Pull request overview
This PR adjusts wasm control-flow interval construction in fgWasmControlFlow() so forward branches that exit multiple enclosing try/catch regions create their enclosing Block interval starting at the outermost escaped catch-try’s try begin, avoiding mis-targeted branches that can land on the post-end validation unreachable and trap at runtime in wasm R2R async EH shapes.
Changes:
- Update wasm interval selection logic to walk outward across enclosing EH regions and choose a safer
Blockstart for non-contiguous forward branches that exit catch-try regions. - Add a wasm R2R-focused regression test that forces CrossGen2 on the browser leg to ensure the problematic codegen path is actually exercised.
- Introduce a dedicated JitBlue regression test project for the repro.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/coreclr/jit/fgwasm.cpp | Walks enclosing EH regions to pick the outermost escaped catch-try begin as the Block interval start for affected forward branches. |
| src/tests/JIT/Regression/JitBlue/Runtime_131285/Runtime_131285.csproj | Forces CrossGen2 only on TargetOS=browser to ensure wasm R2R coverage for the regression. |
| src/tests/JIT/Regression/JitBlue/Runtime_131285/Runtime_131285.cs | Adds an async try/return/catch repro driven synchronously to validate normal-completion behavior under wasm R2R. |
|
Azure Pipelines: Successfully started running 6 pipeline(s). 10 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
AndyAyersMS
left a comment
There was a problem hiding this comment.
There's probably a more elegant way to do this, but this fix seems reasonable enough.
|
| <!-- The bug is wasm R2R (crossgen) codegen. On non-browser legs this runs as | ||
| ordinary JIT/interp and passes trivially (expected). On the browser leg, | ||
| AlwaysUseCrossGen2 forces crossgen (exports RunCrossGen2=1) so the test is | ||
| actually compiled to wasm R2R and run under node -- otherwise a plain |
There was a problem hiding this comment.
| actually compiled to wasm R2R and run under node -- otherwise a plain | |
| actually compiled to wasm R2R and run under node; otherwise a plain |
Fix formatting issue in project file comments. #131287 (comment)
…on branch (dotnet#131287) Fixes dotnet#131285 ## Problem On WebAssembly R2R (crossgen), an `async` method with a `try { ...; return; } catch { }` where the `try` completes normally by branching out early (a `return` after an `await`) generates a **valid** wasm module that **traps at runtime** with `RuntimeError: unreachable` on the normal-completion path. This is distinct from dotnet#131279 (dropped terminal `end` → *invalid* module) and is not a reopening of dotnet#129335 — here the module is well-formed and validates; it simply traps at runtime. ## Root cause In `Compiler::fgWasmControlFlow()`, a non-contiguous forward branch that exits a try/catch region started its enclosing wasm `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. The normal-completion branch then resolves to depth 0 and lands on the outer try's trailing validation `unreachable` (emitted right after its `end` by `genEmitStartBlock`) instead of the continuation. ## Fix Walk the enclosing try regions outward and start the `Block` at the **outermost** escaped catch-try's begin, so the `Block` encloses those trys' ends and the branch resolves to the continuation (`br 0` → `br 1`). For a single-level `try/catch` the outermost escaped region is also the innermost, so behavior there is unchanged. ## Test Adds `src/tests/JIT/Regression/JitBlue/Runtime_131285`, a sync `[Fact]` that drives an `async Task` `MoveNext` through the normal-completion `return` path. The csproj sets `<AlwaysUseCrossGen2 Condition="'$(TargetOS)' == 'browser'">true</AlwaysUseCrossGen2>` so the browser leg is actually compiled to wasm R2R and run under node — without it a plain browser leg runs the test as ordinary JIT and silently false-passes. ## Validation - Runtime A/B on the exact CLRTest node command: unfixed JIT traps (`exit=1`), fixed JIT passes (`exit=100`). - Composite-mode crossgen A/B: unfixed emits trapping `br 0`, fixed emits `br 1`. - 194-assembly sweep: 0 new invalid modules, 185 byte-identical; single-level try/catch provably unaffected. > [!NOTE] > This pull request was generated with the assistance of GitHub Copilot. --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 964b4f25-1c41-44d6-81fb-a6a75ee7d3e4
Fix formatting issue in project file comments. dotnet#131287 (comment)
Fixes #131285
Problem
On WebAssembly R2R (crossgen), an
asyncmethod with atry { ...; return; } catch { }where thetrycompletes normally by branching out early (areturnafter anawait) generates a valid wasm module that traps at runtime withRuntimeError: unreachableon the normal-completion path.This is distinct from #131279 (dropped terminal
end→ invalid module) and is not a reopening of #129335 — here the module is well-formed and validates; it simply traps at runtime.Root cause
In
Compiler::fgWasmControlFlow(), a non-contiguous forward branch that exits a try/catch region started its enclosing wasmBlockat 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 pairedExnRefWrapperis stretched past the try-end to cover the resumption dispatcher — theBlockends up nested inside that outer try. The normal-completion branch then resolves to depth 0 and lands on the outer try's trailing validationunreachable(emitted right after itsendbygenEmitStartBlock) instead of the continuation.Fix
Walk the enclosing try regions outward and start the
Blockat the outermost escaped catch-try's begin, so theBlockencloses those trys' ends and the branch resolves to the continuation (br 0→br 1). For a single-leveltry/catchthe outermost escaped region is also the innermost, so behavior there is unchanged.Test
Adds
src/tests/JIT/Regression/JitBlue/Runtime_131285, a sync[Fact]that drives anasync TaskMoveNextthrough the normal-completionreturnpath. The csproj sets<AlwaysUseCrossGen2 Condition="'$(TargetOS)' == 'browser'">true</AlwaysUseCrossGen2>so the browser leg is actually compiled to wasm R2R and run under node — without it a plain browser leg runs the test as ordinary JIT and silently false-passes.Validation
exit=1), fixed JIT passes (exit=100).br 0, fixed emitsbr 1.Note
This pull request was generated with the assistance of GitHub Copilot.