Skip to content

[runtime-async] Calling interface Task<T> methods on structs crash the runtime when invoked in an ALC #115667

Open
@jakobbotsch

Description

@jakobbotsch

The following program compiled as a Roslyn async state machine (i.e. not new-style) crashes the runtime when FEATURE_RUNTIME_ASYNC is enabled.

// Generated by Fuzzlyn v3.1 on 2025-05-16 13:28:12
// Run on X64 Windows
// Seed: 6767841392543623596-async,vectort,vector128,vector256,x86aes,x86avx,x86avx2,x86bmi1,x86bmi1x64,x86bmi2,x86bmi2x64,x86fma,x86lzcnt,x86lzcntx64,x86pclmulqdq,x86pclmulqdqv256,x86popcnt,x86popcntx64,x86sse,x86ssex64,x86sse2,x86sse2x64,x86sse3,x86sse41,x86sse41x64,x86sse42,x86sse42x64,x86ssse3,x86x86base
// Reduced from 333.1 KiB to 1.3 KiB in 00:23:17
// Exits with error:
// Fatal error.
// 0xC0000005
//
using System.Threading.Tasks;

public interface I0
{
    Task<bool> M8();
}

public struct S1 : I0
{
    public async Task<bool> M8()
    {
        return false;
    }
}

public class Program
{
    public static void Main()
    {
        CollectibleALC alc = new CollectibleALC();
        System.Reflection.Assembly asm = alc.LoadFromAssemblyPath(System.Reflection.Assembly.GetExecutingAssembly().Location);
        System.Reflection.MethodInfo mi = asm.GetType(typeof(Program).FullName).GetMethod(nameof(MainInner));
        System.Type runtimeTy = asm.GetType(typeof(Runtime).FullName);
        mi.Invoke(null, new object[] { System.Activator.CreateInstance(runtimeTy) });
    }

    public static void MainInner(IRuntime rt)
    {
        bool vr1 = new S1().M8().GetAwaiter().GetResult();
    }
}

public interface IRuntime
{
    void WriteLine<T>(string site, T value);
}

public class Runtime : IRuntime
{
    public void WriteLine<T>(string site, T value) => System.Console.WriteLine(value);
}

public class CollectibleALC : System.Runtime.Loader.AssemblyLoadContext
{
    public CollectibleALC() : base(true)
    {
    }
}

It seems when calling the prestub for S1.M8 we somehow end up with the code address 0x1. The JIT itself seems to succeed, but we fail here which ends up returning pOtherCode == 0x1:

// Aside from rejit, performing a SetNativeCodeInterlocked at this point
// generally ensures that there is only one winning version of the native
// code. This also avoid races with profiler overriding ngened code (see
// matching SetNativeCodeInterlocked done after
// JITCachedFunctionSearchStarted)
if (!pConfig->SetNativeCode(pCode, &pOtherCode))
{
#ifdef HAVE_GCCOVER
// When GCStress is enabled, this thread should always win the publishing race
// since we're under a lock.
_ASSERTE(!GCStress<cfg_instr_jit>::IsEnabled() || !"GC Cover native code publish failed");
#endif
// Another thread beat us to publishing its copy of the JITted code.
return pOtherCode;
}

cc @VSadov

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions