From 486de5b47bf6f7d2f74cc81f2e5bcf402829eaba Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 24 Sep 2026 09:24:29 -0700 Subject: [PATCH] Fix TypeLoadException in .NET 11 Preview 7 with DotNext threading (#134460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes a `TypeLoadException` ("does not have an implementation") thrown when loading a concrete type whose base hierarchy contains an **abstract** covariant `Task` → `Task` override (the DotNext.Threading failure reported in the issue). ### Root cause For a covariant `Task` → `Task` virtual override, `MethodTableBuilder::EnumerateClassMethods` synthesizes a third MethodDesc — a *return-dropping thunk* — whose void-returning signature MethodImpl-overrides the base's void-returning async variant. All three MethodDescs share the same metadata token, and `MethodDesc::GetAttrs()` reads attributes straight from metadata. When the covariant override is declared `abstract`, the synthesized thunk therefore also reports `mdAbstract`, even though `EmitReturnDroppingThunk` always emits a body (CALLVIRT to the `T`-returning async variant + POP + RET). A concrete derived type's own `Task` override matches the abstract declaration exactly, so no MethodImpl and no additional variant is created for it — nothing ever implements the inherited "abstract" thunk slot, and `VerifyVirtualMethodsImplemented` fails the type load. ### Fix `MethodDesc::GetAttrs()` now masks off `mdAbstract` for return-dropping thunks. Patching the single choke point keeps the VM, JIT (`CORINFO_FLG_ABSTRACT`), `MayHaveNativeCode()`/`MayHavePrecode()` and default-interface-method resolution consistent. Async variants are filtered out of reflection, so the adjusted attributes are not observable from managed code. - [x] Analyze the issue and identify the root cause - [x] Baseline build (`./build.sh clr+libs -lc release -rc checked`) + Core_Root layout - [x] Reproduce the failure with a standalone repro (abstract covariant override fails, non-abstract passes) - [x] Minimal VM fix in `MethodDesc::GetAttrs()` - [x] Add regression test to `src/tests/async/covariant-return/covariant-returns.cs` (abstract covariant method override + abstract covariant property override, the `get_Task` shape from the issue) - [x] Verify new tests fail without the fix (both throw the reported `TypeLoadException`) - [x] Verify new tests pass with the fix and no async tests regress (144 passed / 0 failed) Not verified: only linux-x64 Checked was built/tested; no test trees outside `src/tests/async` were run; `parallel_validation` produced no signal (code-review binary missing in the environment, CodeQL skipped both `cpp` and `csharp` for database size). The NativeAOT/crossgen2 path models this differently (`CompilerTypeSystemContext.Async.cs` wraps the resolved implementation in a `ReturnDroppingAsyncThunk`) and appears unaffected, but was reasoned about rather than tested; interface/DIM variants of this shape are likewise untested. Resolves dotnet/runtime#132971 > [!NOTE] > This pull request description was generated by GitHub Copilot. - Fixes #132971 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: VSadov <8218165+VSadov@users.noreply.github.com> --- src/coreclr/vm/method.cpp | 10 +++ src/coreclr/vm/methodtablebuilder.h | 16 +++- .../covariant-return/covariant-returns.cs | 78 +++++++++++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) diff --git a/src/coreclr/vm/method.cpp b/src/coreclr/vm/method.cpp index 4a8295f653883b..ff1531a65c4eca 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -1547,6 +1547,16 @@ DWORD MethodDesc::GetAttrs() const _ASSERTE(!"If this ever fires, then this method should return HRESULT"); return 0; } + + if (IsReturnDroppingThunk()) + { + // A return-dropping thunk is synthesized by the runtime and always has an implementation - + // it calls the ordinary async variant virtually and drops the result. + // The metadata method that the thunk is derived from may be abstract (i.e. when the covariant + // override that needs the thunk is abstract), but the thunk itself never is. + dwAttributes &= ~mdAbstract; + } + return dwAttributes; } diff --git a/src/coreclr/vm/methodtablebuilder.h b/src/coreclr/vm/methodtablebuilder.h index 937e1dd11caa01..eba629739a0c3a 100644 --- a/src/coreclr/vm/methodtablebuilder.h +++ b/src/coreclr/vm/methodtablebuilder.h @@ -1090,7 +1090,21 @@ class MethodTableBuilder // Returns the metadata declaration attributes for this method. DWORD GetDeclAttrs() const - { LIMITED_METHOD_CONTRACT; return m_dwDeclAttrs; } + { + LIMITED_METHOD_CONTRACT; + + DWORD dwDeclAttrs = m_dwDeclAttrs; + if (hasAsyncFlags(m_asyncMethodFlags, AsyncMethodFlags::ReturnDroppingThunk)) + { + // A return-dropping thunk is synthesized by the runtime and always has an implementation - + // it calls the ordinary async variant virtually and drops the result. + // The metadata method that the thunk is derived from may be abstract (i.e. when the covariant + // override that needs the thunk is abstract), but the thunk itself never is. + dwDeclAttrs &= ~mdAbstract; + } + + return dwDeclAttrs; + } //----------------------------------------------------------------------------------------- // Returns the metadata implementation attributes for this method. diff --git a/src/tests/async/covariant-return/covariant-returns.cs b/src/tests/async/covariant-return/covariant-returns.cs index f94d1e1678a1a4..2886240229d600 100644 --- a/src/tests/async/covariant-return/covariant-returns.cs +++ b/src/tests/async/covariant-return/covariant-returns.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Xunit; @@ -294,3 +295,80 @@ public static void TestAsyncInterfaceGenericMethod() } } } + +namespace AbstractCovariantReturn +{ + // A covariant Task -> Task override may be abstract. + // The runtime still has to provide an async variant that matches the void-returning + // async variant of the base, otherwise concrete derived types cannot be loaded. + public class Program + { + internal static string Trace; + + [Fact] + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "This test intentionally exercises Assembly.GetTypes().")] + public static void TestAssemblyGetTypes() + { + _ = typeof(Program).Assembly.GetTypes(); + } + + [Fact] + public static void TestAbstractCovariantOverride() + { + Trace = null; + Base b = new Derived(); + CallBase(b).GetAwaiter().GetResult(); + Assert.Equal("Derived.M1;", Trace); + + Trace = null; + Assert.Equal(42, CallMid(new Derived()).GetAwaiter().GetResult()); + Assert.Equal("Derived.M1;", Trace); + } + + [Fact] + public static void TestAbstractCovariantOverrideProperty() + { + Base b = new Derived(); + Assert.Equal(42, CallBaseProperty(b).GetAwaiter().GetResult()); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task CallBase(Base b) => await b.M1(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task CallMid(Mid m) => await m.M1(); + + [MethodImpl(MethodImplOptions.NoInlining)] + private static async Task CallBaseProperty(Base b) + { + await b.Task; + return await ((Mid)b).Task; + } + + public abstract class Base + { + public abstract Task M1(); + + public abstract Task Task { get; } + } + + public abstract class Mid : Base + { + public abstract override Task M1(); + + public abstract override Task Task { get; } + } + + public sealed class Derived : Mid + { + public override Task M1() + { + Trace += "Derived.M1;"; + return System.Threading.Tasks.Task.FromResult(42); + } + + public override Task Task => System.Threading.Tasks.Task.FromResult(42); + } + } +}