[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)
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.