From 1abb0b42025e66424a973fadda15d41855f012de Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 17:27:23 +0000 Subject: [PATCH 1/4] Initial plan From e46462c4541fef1796e43a3d09888df32a8a1ee9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:03:55 +0000 Subject: [PATCH 2/4] Add runtime async tests for catching non-Exception throws Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com> --- .../NoWrapThrowers.cs | 46 +++++++ .../NoWrapThrowers.csproj | 6 + .../NonExceptionThrower.il | 24 ++++ .../NonExceptionThrower.ilproj | 10 ++ .../runtime-wrapped-exception.cs | 125 ++++++++++++++++++ .../runtime-wrapped-exception.csproj | 7 + 6 files changed, 218 insertions(+) create mode 100644 src/tests/async/runtime-wrapped-exception/NoWrapThrowers.cs create mode 100644 src/tests/async/runtime-wrapped-exception/NoWrapThrowers.csproj create mode 100644 src/tests/async/runtime-wrapped-exception/NonExceptionThrower.il create mode 100644 src/tests/async/runtime-wrapped-exception/NonExceptionThrower.ilproj create mode 100644 src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.cs create mode 100644 src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.csproj diff --git a/src/tests/async/runtime-wrapped-exception/NoWrapThrowers.cs b/src/tests/async/runtime-wrapped-exception/NoWrapThrowers.cs new file mode 100644 index 00000000000000..97c9c8631b2622 --- /dev/null +++ b/src/tests/async/runtime-wrapped-exception/NoWrapThrowers.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +// This assembly opts OUT of wrapping non-Exception throws, in contrast to the +// C# default (WrapNonExceptionThrows = true) used by the main test assembly and +// by CoreLib. The async methods below are otherwise identical to the ones in the +// main test assembly; keeping them in a separate assembly is the only way to +// exercise the WrapNonExceptionThrows = false configuration, since the setting is +// assembly-scoped and decided by the frame that catches the exception. +[assembly: RuntimeCompatibility(WrapNonExceptionThrows = false)] + +public static class NoWrapThrowers +{ + // Runtime async (async2) throwing a non-Exception after suspending. + public static async Task ThrowAfterYieldAsync2() + { + await Task.Yield(); + NonExceptionThrower.ThrowNonException(); + } + + // Compiler state machine (async1) throwing a non-Exception after suspending. + [RuntimeAsyncMethodGeneration(false)] + public static async Task ThrowAfterYieldAsync1() + { + await Task.Yield(); + NonExceptionThrower.ThrowNonException(); + } + + // Runtime async (async2) throwing a non-Exception before suspending. + public static async Task ThrowBeforeYieldAsync2() + { + NonExceptionThrower.ThrowNonException(); + await Task.Yield(); + } + + // Compiler state machine (async1) throwing a non-Exception before suspending. + [RuntimeAsyncMethodGeneration(false)] + public static async Task ThrowBeforeYieldAsync1() + { + NonExceptionThrower.ThrowNonException(); + await Task.Yield(); + } +} diff --git a/src/tests/async/runtime-wrapped-exception/NoWrapThrowers.csproj b/src/tests/async/runtime-wrapped-exception/NoWrapThrowers.csproj new file mode 100644 index 00000000000000..6981b089475cc1 --- /dev/null +++ b/src/tests/async/runtime-wrapped-exception/NoWrapThrowers.csproj @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/tests/async/runtime-wrapped-exception/NonExceptionThrower.il b/src/tests/async/runtime-wrapped-exception/NonExceptionThrower.il new file mode 100644 index 00000000000000..3a5cfd42de722c --- /dev/null +++ b/src/tests/async/runtime-wrapped-exception/NonExceptionThrower.il @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Helper that throws an object which does not derive from System.Exception. +// This is only expressible in IL, so it lives in its own IL assembly. The thrown +// object (a string) is what a catch handler in a WrapNonExceptionThrows=true +// assembly observes as RuntimeWrappedException.WrappedException. + +.assembly extern System.Runtime { .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A) .ver 4:0:0:0 } + +.assembly NonExceptionThrower +{ +} + +.class public auto ansi abstract sealed beforefieldinit NonExceptionThrower + extends [System.Runtime]System.Object +{ + .method public hidebysig static void ThrowNonException() cil managed noinlining + { + .maxstack 8 + ldstr "A non-Exception object thrown from IL" + throw + } +} diff --git a/src/tests/async/runtime-wrapped-exception/NonExceptionThrower.ilproj b/src/tests/async/runtime-wrapped-exception/NonExceptionThrower.ilproj new file mode 100644 index 00000000000000..da615aeb006351 --- /dev/null +++ b/src/tests/async/runtime-wrapped-exception/NonExceptionThrower.ilproj @@ -0,0 +1,10 @@ + + + Library + + + + + + + diff --git a/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.cs b/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.cs new file mode 100644 index 00000000000000..34287ca08a9317 --- /dev/null +++ b/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.cs @@ -0,0 +1,125 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; +using Xunit; + +// Regression tests for https://github.com/dotnet/runtime/issues/123194. +// +// IL allows throwing an object that does not derive from System.Exception. When +// such an object propagates into a catch handler, it is (or is not) wrapped in a +// RuntimeWrappedException depending on the RuntimeCompatibilityAttribute +// WrapNonExceptionThrows setting of the assembly that owns the catch handler. +// +// The goal of runtime async (async2) is to match the traditional compiler +// generated state machine (async1) behavior. These tests exercise both async +// forms (via [RuntimeAsyncMethodGeneration]) throwing a non-Exception object, +// once from an assembly with WrapNonExceptionThrows = true (this assembly, which +// matches the C# default and CoreLib) and once from an assembly with +// WrapNonExceptionThrows = false (NoWrapThrowers), for throws both before and +// after a suspension point. +public class RuntimeAsyncNonExceptionThrows +{ + private const string ThrownObject = "A non-Exception object thrown from IL"; + + // WrapNonExceptionThrows = true (this assembly): the non-Exception is observed + // as a RuntimeWrappedException by the awaiting caller for both async forms. + + [Fact] + public static void CatchAfterYield_Async2() + => AssertNonExceptionWrapped(ThrowAfterYieldAsync2); + + [Fact] + public static void CatchAfterYield_Async1() + => AssertNonExceptionWrapped(ThrowAfterYieldAsync1); + + [Fact] + public static void CatchBeforeYield_Async2() + => AssertNonExceptionWrapped(ThrowBeforeYieldAsync2); + + [Fact] + public static void CatchBeforeYield_Async1() + => AssertNonExceptionWrapped(ThrowBeforeYieldAsync1); + + // WrapNonExceptionThrows = false (NoWrapThrowers assembly). + + [Fact] + public static void CatchAfterYield_NoWrap_Async2() + => AssertNonExceptionWrapped(NoWrapThrowers.ThrowAfterYieldAsync2); + + // async2 and async1 behave differently for a non-Exception thrown after a + // suspension point in a WrapNonExceptionThrows = false assembly: + // * async2 faults the returned Task with a RuntimeWrappedException, which the + // caller observes just like the wrap=true case above. + // * async1 lets the raw non-Exception escape the resumed state machine onto + // the thread pool where it becomes an unhandled exception and crashes the + // process, so it can never be observed by the caller. + // Enable this test once async1 and async2 agree (see the tracking issue). + [ActiveIssue("https://github.com/dotnet/runtime/issues/123194")] + [Fact] + public static void CatchAfterYield_NoWrap_Async1() + => AssertNonExceptionWrapped(NoWrapThrowers.ThrowAfterYieldAsync1); + + [Fact] + public static void CatchBeforeYield_NoWrap_Async2() + => AssertNonExceptionWrapped(NoWrapThrowers.ThrowBeforeYieldAsync2); + + [Fact] + public static void CatchBeforeYield_NoWrap_Async1() + => AssertNonExceptionWrapped(NoWrapThrowers.ThrowBeforeYieldAsync1); + + private static void AssertNonExceptionWrapped(Func thrower) + { + object wrapped = ObserveNonException(thrower).GetAwaiter().GetResult(); + Assert.Equal(ThrownObject, wrapped); + } + + // Awaits the throwing method and returns the wrapped non-Exception object. + // The catch handler lives in this WrapNonExceptionThrows = true assembly, so a + // propagating non-Exception is caught as a RuntimeWrappedException. Returns null + // if nothing was thrown so the assertion above fails with a clear diff. + private static async Task ObserveNonException(Func thrower) + { + try + { + await thrower(); + return null; + } + catch (RuntimeWrappedException e) + { + return e.WrappedException; + } + } + + // Runtime async (async2) throwing a non-Exception after suspending. + public static async Task ThrowAfterYieldAsync2() + { + await Task.Yield(); + NonExceptionThrower.ThrowNonException(); + } + + // Compiler state machine (async1) throwing a non-Exception after suspending. + [RuntimeAsyncMethodGeneration(false)] + public static async Task ThrowAfterYieldAsync1() + { + await Task.Yield(); + NonExceptionThrower.ThrowNonException(); + } + + // Runtime async (async2) throwing a non-Exception before suspending. + public static async Task ThrowBeforeYieldAsync2() + { + NonExceptionThrower.ThrowNonException(); + await Task.Yield(); + } + + // Compiler state machine (async1) throwing a non-Exception before suspending. + [RuntimeAsyncMethodGeneration(false)] + public static async Task ThrowBeforeYieldAsync1() + { + NonExceptionThrower.ThrowNonException(); + await Task.Yield(); + } +} diff --git a/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.csproj b/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.csproj new file mode 100644 index 00000000000000..3849e3f6c3ccf7 --- /dev/null +++ b/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.csproj @@ -0,0 +1,7 @@ + + + + + + + From 188608e914cc68ff01be19b0fcd2d5efbb7745e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Jul 2026 18:14:28 +0000 Subject: [PATCH 3/4] Apply remaining changes Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com> --- reflect_ai.csx | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 reflect_ai.csx diff --git a/reflect_ai.csx b/reflect_ai.csx new file mode 100644 index 00000000000000..e69de29bb2d1d6 From 362e9c2143e4fc206749a4da766383915a86f920 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 11 Jul 2026 01:41:44 +0000 Subject: [PATCH 4/4] Block async1 divergent case with a comment instead of ActiveIssue Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com> --- .../runtime-wrapped-exception.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.cs b/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.cs index 34287ca08a9317..1de34df3001157 100644 --- a/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.cs +++ b/src/tests/async/runtime-wrapped-exception/runtime-wrapped-exception.cs @@ -56,11 +56,11 @@ public static void CatchAfterYield_NoWrap_Async2() // * async1 lets the raw non-Exception escape the resumed state machine onto // the thread pool where it becomes an unhandled exception and crashes the // process, so it can never be observed by the caller. - // Enable this test once async1 and async2 agree (see the tracking issue). - [ActiveIssue("https://github.com/dotnet/runtime/issues/123194")] - [Fact] - public static void CatchAfterYield_NoWrap_Async1() - => AssertNonExceptionWrapped(NoWrapThrowers.ThrowAfterYieldAsync1); + // async1 behavior here is terminal to the process, thus we will not test it: + // + // [Fact] + // public static void CatchAfterYield_NoWrap_Async1() + // => AssertNonExceptionWrapped(NoWrapThrowers.ThrowAfterYieldAsync1); [Fact] public static void CatchBeforeYield_NoWrap_Async2()