From 0a3a5b987f9324be0865c9802e5a55828b0debe3 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 30 Sep 2026 14:40:44 -0500 Subject: [PATCH 1/3] JIT: make Wasm frames that call a finally unwindable genCallFinally emits a call to the finally funclet but did not mark the calling function as needing an unwindable frame. When the callfinally was the method's only call, the prolog never stored the function table index at $fp[0]. An exception thrown from the finally then unwound into a frame with a stale index, the runtime saw no R2R caller, and the exception was reported as unhandled instead of reaching the caller's catch. Fixes b51875 under browser-wasm R2R. Also affects returning finallys that are not cloned (for example in MinOpts). Fixes #134975 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegenwasm.cpp | 5 ++ .../Runtime_134975/Runtime_134975.cs | 83 +++++++++++++++++++ .../Runtime_134975/Runtime_134975.csproj | 8 ++ 3 files changed, 96 insertions(+) create mode 100644 src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.cs create mode 100644 src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.csproj diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 348c4db661aad8..6d5142fcd148e1 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -4128,6 +4128,11 @@ void CodeGen::genCallFinally(BasicBlock* block) assert((funcletIndex >= 1) && (funcletIndex < m_compiler->compFuncCount())); + // The finally can throw or trigger GC, so the calling frame must be walkable + // even if this is its only call. + // + ensureCurrentFuncIsUnwindable(); + EmitCallParams params; params.callType = EmitCallType::EC_INDIR_R; diff --git a/src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.cs b/src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.cs new file mode 100644 index 00000000000000..9585f117a65b73 --- /dev/null +++ b/src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// An exception thrown from a finally that is called on the normal path must +// propagate to the caller's catch. Under WebAssembly R2R, a method whose only +// call is the call to its finally did not set up an unwindable frame, so the +// stack walk lost the caller and the exception was reported as unhandled. + +using System; +using System.Runtime.CompilerServices; +using Xunit; + +public class Runtime_134975 +{ + [MethodImpl(MethodImplOptions.NoInlining)] + private static int NonReturningFinally() + { + try + { + goto Done; + } + finally + { + throw new Exception(); + } + Done: + return 0; + } + + // NoOptimization keeps the finally from being cloned onto the normal path. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + private static int ReturningFinally(bool doThrow) + { + int x = 0; + try + { + x = 1; + } + finally + { + if (doThrow) + { + throw new Exception(); + } + } + + return x; + } + + [Fact] + public static void TestNonReturningFinally() + { + bool caught = false; + try + { + NonReturningFinally(); + } + catch (Exception) + { + caught = true; + } + + Assert.True(caught); + } + + [Fact] + public static void TestReturningFinally() + { + Assert.Equal(1, ReturningFinally(false)); + + bool caught = false; + try + { + ReturningFinally(true); + } + catch (Exception) + { + caught = true; + } + + Assert.True(caught); + } +} diff --git a/src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.csproj b/src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.csproj new file mode 100644 index 00000000000000..de6d5e08882e86 --- /dev/null +++ b/src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.csproj @@ -0,0 +1,8 @@ + + + True + + + + + From c6102df710e6cfc61e9d9784c6274098aab23844 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 30 Sep 2026 15:50:36 -0500 Subject: [PATCH 2/3] Remove Runtime_134975 test b51875 and the JIT/Methodical tests already cover this under Wasm R2R. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Runtime_134975/Runtime_134975.cs | 83 ------------------- .../Runtime_134975/Runtime_134975.csproj | 8 -- 2 files changed, 91 deletions(-) delete mode 100644 src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.cs delete mode 100644 src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.csproj diff --git a/src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.cs b/src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.cs deleted file mode 100644 index 9585f117a65b73..00000000000000 --- a/src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// An exception thrown from a finally that is called on the normal path must -// propagate to the caller's catch. Under WebAssembly R2R, a method whose only -// call is the call to its finally did not set up an unwindable frame, so the -// stack walk lost the caller and the exception was reported as unhandled. - -using System; -using System.Runtime.CompilerServices; -using Xunit; - -public class Runtime_134975 -{ - [MethodImpl(MethodImplOptions.NoInlining)] - private static int NonReturningFinally() - { - try - { - goto Done; - } - finally - { - throw new Exception(); - } - Done: - return 0; - } - - // NoOptimization keeps the finally from being cloned onto the normal path. - [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] - private static int ReturningFinally(bool doThrow) - { - int x = 0; - try - { - x = 1; - } - finally - { - if (doThrow) - { - throw new Exception(); - } - } - - return x; - } - - [Fact] - public static void TestNonReturningFinally() - { - bool caught = false; - try - { - NonReturningFinally(); - } - catch (Exception) - { - caught = true; - } - - Assert.True(caught); - } - - [Fact] - public static void TestReturningFinally() - { - Assert.Equal(1, ReturningFinally(false)); - - bool caught = false; - try - { - ReturningFinally(true); - } - catch (Exception) - { - caught = true; - } - - Assert.True(caught); - } -} diff --git a/src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.csproj b/src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.csproj deleted file mode 100644 index de6d5e08882e86..00000000000000 --- a/src/tests/JIT/Regression_2/Runtime_134975/Runtime_134975.csproj +++ /dev/null @@ -1,8 +0,0 @@ - - - True - - - - - From 0019a476ac8874810839935405259ca3a9fa5f9c Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 30 Sep 2026 15:52:24 -0500 Subject: [PATCH 3/3] Remove comment Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegenwasm.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 6d5142fcd148e1..171b8a38e1b58e 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -4128,9 +4128,6 @@ void CodeGen::genCallFinally(BasicBlock* block) assert((funcletIndex >= 1) && (funcletIndex < m_compiler->compFuncCount())); - // The finally can throw or trigger GC, so the calling frame must be walkable - // even if this is its only call. - // ensureCurrentFuncIsUnwindable(); EmitCallParams params;