From 674b5ee0769ab799dcdd3588698672a3db7fbd2a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 00:25:35 +0000 Subject: [PATCH 1/7] Initial plan From 86c98da98e3ef6cb028e8102cbc43e9d9b322ff0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 00:30:51 +0000 Subject: [PATCH 2/7] Add fix to skip async variant methods in covariant return type validation Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com> --- src/coreclr/vm/class.cpp | 5 ++ .../UnitTest/AsyncCovariantReturn.cs | 49 +++++++++++++++++++ .../UnitTest/AsyncCovariantReturn.csproj | 11 +++++ 3 files changed, 65 insertions(+) create mode 100644 src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs create mode 100644 src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.csproj diff --git a/src/coreclr/vm/class.cpp b/src/coreclr/vm/class.cpp index dbc9b2514eb188..20125ede2f959b 100644 --- a/src/coreclr/vm/class.cpp +++ b/src/coreclr/vm/class.cpp @@ -1374,6 +1374,11 @@ void ClassLoader::ValidateMethodsWithCovariantReturnTypes(MethodTable* pMT) if (!pMD->RequiresCovariantReturnTypeChecking() && !pParentMD->RequiresCovariantReturnTypeChecking()) continue; + // Skip validation for async variant methods, as they have different signatures by design + // to support the async calling convention + if (pMD->IsAsyncVariantMethod() || pParentMD->IsAsyncVariantMethod()) + continue; + // Locate the MethodTable defining the pParentMD. MethodTable* pDefinitionParentMT = pParentMT; while (pDefinitionParentMT->GetCanonicalMethodTable() != pParentMD->GetMethodTable()) diff --git a/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs b/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs new file mode 100644 index 00000000000000..6da308d3ca04f2 --- /dev/null +++ b/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs @@ -0,0 +1,49 @@ +// 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.Threading.Tasks; +using Xunit; + +namespace AsyncCovariantReturnTest; + +public static class Program +{ + [Fact] + public static int TestEntryPoint() + { + // This test validates that async methods with covariant-like return type signatures + // do not trigger TypeLoadException during type loading. + // The issue is that async methods generate variant methods with different signatures, + // and the covariant return type validation was incorrectly treating these as errors. + + try + { + var derived = new Derived(); + return 100; + } + catch (TypeLoadException) + { + Console.WriteLine("FAIL: TypeLoadException thrown when loading type with async override"); + return 101; + } + } +} + +public abstract class Base +{ + public abstract Task HandleAsync(); +} + +public class Derived : Base +{ + // This method appears to have a covariant return type (Task instead of Task), + // but due to the async transformation, this should be valid. + // The async variant will have a different signature that the covariant return type + // validator should skip. + public override async Task HandleAsync() + { + await Task.Yield(); + return string.Empty; + } +} diff --git a/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.csproj b/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.csproj new file mode 100644 index 00000000000000..00dd1ee50adbfc --- /dev/null +++ b/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.csproj @@ -0,0 +1,11 @@ + + + true + + + + + + + + From f90f25bf247f5016e04ecaaa3e6b90caa07be3aa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 00:58:21 +0000 Subject: [PATCH 3/7] Update test comments to clarify async variant behavior Co-authored-by: elinor-fung <47805090+elinor-fung@users.noreply.github.com> --- .../UnitTest/AsyncCovariantReturn.cs | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs b/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs index 6da308d3ca04f2..a6cc7c09aef398 100644 --- a/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs +++ b/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs @@ -14,17 +14,27 @@ public static int TestEntryPoint() { // This test validates that async methods with covariant-like return type signatures // do not trigger TypeLoadException during type loading. - // The issue is that async methods generate variant methods with different signatures, - // and the covariant return type validation was incorrectly treating these as errors. + // + // Background: The C# compiler allows overriding a Task method with an async Task method + // because the async keyword changes the method's semantics. The compiler generates + // async variant methods with different signatures to support the async calling convention. + // These variant methods have the unwrapped return type (T instead of Task). + // + // The issue was that the runtime's covariant return type validator was incorrectly + // treating these synthetic async variant methods as invalid overrides, causing a + // TypeLoadException even though the code is valid and compiles successfully. + // + // This test ensures that such valid code can be loaded without throwing. try { var derived = new Derived(); return 100; } - catch (TypeLoadException) + catch (TypeLoadException ex) { Console.WriteLine("FAIL: TypeLoadException thrown when loading type with async override"); + Console.WriteLine($"Exception: {ex.Message}"); return 101; } } @@ -37,10 +47,11 @@ public abstract class Base public class Derived : Base { - // This method appears to have a covariant return type (Task instead of Task), - // but due to the async transformation, this should be valid. - // The async variant will have a different signature that the covariant return type - // validator should skip. + // This override is valid C# code that compiles successfully. + // The C# compiler allows overriding Task with async Task because the async keyword + // changes how the method is compiled. However, the runtime generates async variant methods + // with different signatures (returning T instead of Task) to support the async calling + // convention. The covariant return type validator should skip these synthetic methods. public override async Task HandleAsync() { await Task.Yield(); From 7be6836e7f008b4df120c8823e9d7043bb6639bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 01:22:49 +0000 Subject: [PATCH 4/7] Move async variant check earlier and optimize per feedback - Only check pMD (derived class method) for async variant - Move check before resolving pParentMD to save unnecessary work - AsyncVariantMethods should only override AsyncVariantMethods Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- src/coreclr/vm/class.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/coreclr/vm/class.cpp b/src/coreclr/vm/class.cpp index 20125ede2f959b..c532eae900b5b4 100644 --- a/src/coreclr/vm/class.cpp +++ b/src/coreclr/vm/class.cpp @@ -1366,6 +1366,12 @@ void ClassLoader::ValidateMethodsWithCovariantReturnTypes(MethodTable* pMT) continue; } MethodDesc* pMD = pMT->GetMethodDescForSlot(i); + + // Skip validation for async variant methods, as they have different signatures by design + // to support the async calling convention + if (pMD->IsAsyncVariantMethod()) + continue; + MethodDesc* pParentMD = pParentMT->GetMethodDescForSlot(i); if (pMD == pParentMD) @@ -1374,11 +1380,6 @@ void ClassLoader::ValidateMethodsWithCovariantReturnTypes(MethodTable* pMT) if (!pMD->RequiresCovariantReturnTypeChecking() && !pParentMD->RequiresCovariantReturnTypeChecking()) continue; - // Skip validation for async variant methods, as they have different signatures by design - // to support the async calling convention - if (pMD->IsAsyncVariantMethod() || pParentMD->IsAsyncVariantMethod()) - continue; - // Locate the MethodTable defining the pParentMD. MethodTable* pDefinitionParentMT = pParentMT; while (pDefinitionParentMT->GetCanonicalMethodTable() != pParentMD->GetMethodTable()) From 89130b38c39093de1e2ae52d467a22c42050ef7b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:56:00 +0000 Subject: [PATCH 5/7] Remove try/catch block from test per feedback Let test harness handle exceptions rather than catching them explicitly. Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../UnitTest/AsyncCovariantReturn.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs b/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs index a6cc7c09aef398..2a586d28544286 100644 --- a/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs +++ b/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs @@ -26,17 +26,8 @@ public static int TestEntryPoint() // // This test ensures that such valid code can be loaded without throwing. - try - { - var derived = new Derived(); - return 100; - } - catch (TypeLoadException ex) - { - Console.WriteLine("FAIL: TypeLoadException thrown when loading type with async override"); - Console.WriteLine($"Exception: {ex.Message}"); - return 101; - } + var derived = new Derived(); + return 100; } } From e719797cc17312f5e05f890179ae64126c28d543 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:58:04 +0000 Subject: [PATCH 6/7] Convert test to void return per analyzer feedback Change TestEntryPoint from int to void since it unconditionally returns 100. Test only validates type loading succeeds, no conditional logic needed. Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../CovariantReturns/UnitTest/AsyncCovariantReturn.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs b/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs index 2a586d28544286..d844a18eac1860 100644 --- a/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs +++ b/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs @@ -10,7 +10,7 @@ namespace AsyncCovariantReturnTest; public static class Program { [Fact] - public static int TestEntryPoint() + public static void TestEntryPoint() { // This test validates that async methods with covariant-like return type signatures // do not trigger TypeLoadException during type loading. @@ -27,7 +27,6 @@ public static int TestEntryPoint() // This test ensures that such valid code can be loaded without throwing. var derived = new Derived(); - return 100; } } From ab7f774013d2508aeee4c09dee46da8b98a68d67 Mon Sep 17 00:00:00 2001 From: Jan Kotas Date: Fri, 6 Feb 2026 11:00:01 -0800 Subject: [PATCH 7/7] Update src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs --- .../CovariantReturns/UnitTest/AsyncCovariantReturn.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs b/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs index d844a18eac1860..c26e1183f5a100 100644 --- a/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs +++ b/src/tests/Loader/classloader/MethodImpl/CovariantReturns/UnitTest/AsyncCovariantReturn.cs @@ -26,7 +26,7 @@ public static void TestEntryPoint() // // This test ensures that such valid code can be loaded without throwing. - var derived = new Derived(); + GC.KeepAlive(new Derived()); } }