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
6 changes: 6 additions & 0 deletions src/coreclr/vm/class.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
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.

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

namespace AsyncCovariantReturnTest;

public static class Program
{
[Fact]
public static void TestEntryPoint()
{
// This test validates that async methods with covariant-like return type signatures
// do not trigger TypeLoadException during type loading.
//
// Background: The C# compiler allows overriding a Task method with an async Task<T> 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<T>).
Comment thread
agocke marked this conversation as resolved.
//
// 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.

Comment thread
agocke marked this conversation as resolved.
GC.KeepAlive(new Derived());
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.

Do we have a plan to add actual test coverage for covariant returns? The test here inherits whatever is the global runtime-async setting, but I believe it might be useful to have real coverage under tests/async that tests: actually calling the method, pairs of "base is runtime-async, override is not" and inverted.

This test has a minimal value because it surgically only checks type load and nothing else. We don't get much bang for our RequiresProcessIsolation buck.

}
}

public abstract class Base
{
public abstract Task HandleAsync();
}

public class Derived : Base
{
// This override is valid C# code that compiles successfully.
// The C# compiler allows overriding Task with async Task<T> 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<T>) to support the async calling
Comment thread
agocke marked this conversation as resolved.
// convention. The covariant return type validator should skip these synthetic methods.
public override async Task<string> HandleAsync()
{
await Task.Yield();
return string.Empty;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RequiresProcessIsolation>true</RequiresProcessIsolation>
</PropertyGroup>
Comment on lines +2 to +4
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.

Why does this require process isolation?

<ItemGroup>
<Compile Include="$(MSBuildProjectName).cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="$(TestLibraryProjectPath)" />
</ItemGroup>
</Project>
Loading