Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions src/coreclr/jit/fgwasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1458,11 +1458,36 @@ PhaseStatus Compiler::fgWasmControlFlow()
continue;
}

// Non-contiguous, non-subsumed forward branch. Start the Block at the try
// header when crossing a try-catch exit so it encloses the wrapper.
// Non-contiguous, non-subsumed forward branch. If it exits enclosing
// try/catch regions, start the Block at the outermost escaped catch-try
// so the Block encloses those trys' ends. Each Try emits a trailing
// validation `unreachable` after its `end`; starting only at the innermost
// try would nest the Block inside an outer try sharing this target as its
// end cursor, so the branch would land on that `unreachable` instead of the
// continuation. Single-level try/catch is unchanged (outermost == innermost).
//
BasicBlock* const blockStart = isCrossingTryCatchExit ? blockTryDsc->ebdTryBeg : block;
WasmInterval* const branch = WasmInterval::NewBlock(this, blockStart, initialLayout[succNum]);
BasicBlock* blockStart = block;
for (EHblkDsc* tryDsc = ehGetBlockTryDsc(block); tryDsc != nullptr;)
{
// Once succ is contained in an enclosing try, all outer trys contain it
// too, so the branch does not exit them.
//
if (bbInTryRegions(ehGetIndex(tryDsc), succ))
{
break;
}

if (tryDsc->HasCatchHandler())
{
blockStart = tryDsc->ebdTryBeg;
}

tryDsc = (tryDsc->ebdEnclosingTryIndex == EHblkDsc::NO_ENCLOSING_INDEX)
? nullptr
: ehGetDsc(tryDsc->ebdEnclosingTryIndex);
}

WasmInterval* const branch = WasmInterval::NewBlock(this, blockStart, initialLayout[succNum]);
fgWasmIntervals->push_back(branch);

// Remember an interval end here
Expand Down
50 changes: 50 additions & 0 deletions src/tests/JIT/Regression/JitBlue/Runtime_131285/Runtime_131285.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

// Regression test for a WebAssembly R2R (crossgen) codegen bug in async
// exception-handling lowering. When an async method's try block completes
// normally by branching out early (a `return` after an `await`), the
// normal-completion branch was bound to a Block that ended at the try's own
// end cursor -- nested inside the exception-ref wrapper funclet -- so the
// branch landed on the wrapper's trailing validation `unreachable` and
// trapped (RuntimeError: unreachable) instead of resuming.
//
// The defect is in codegen, independent of whether the await actually
// suspends: `await Task.CompletedTask` still generates the state-machine
// normal-completion branch that traps. A completed await is used deliberately
// so the synchronous .GetAwaiter().GetResult() does not block the single wasm
// thread (blocking on incomplete work traps elsewhere in corelib on wasm).
//
// Reproduces only under crossgen wasm R2R (TargetOS=browser), where the
// unfixed JIT aborts the module (exit != 100 => test fails). Passes trivially
// on all other targets.

using System;
using System.Threading.Tasks;
using Xunit;

public class Runtime_131285
{
[Fact]
public static void TestEntryPoint()
{
// Sync [Fact]: the merged runner invokes this via a bare call, so drive
// the async method to completion synchronously here. RunAsync's MoveNext
// takes the normal-completion `return` path -- the edge that trapped
// under the unfixed wasm R2R JIT.
RunAsync().GetAwaiter().GetResult();
}

private static async Task RunAsync()
{
try
{
await Task.CompletedTask;
return;
}
catch (Exception ex)
{
GC.KeepAlive(ex);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Optimize>True</Optimize>
<!-- 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
actually compiled to wasm R2R and run under node -- otherwise a plain
actually compiled to wasm R2R and run under node; otherwise a plain

browser leg runs it as normal JIT and silently false-passes without ever
exercising the fix. CrossGen2OutputFormat=wasm is set automatically for
TargetOS=browser (src/tests/Directory.Build.props). -->
<AlwaysUseCrossGen2 Condition="'$(TargetOS)' == 'browser'">true</AlwaysUseCrossGen2>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildProjectName).cs" />
</ItemGroup>
</Project>
Loading