From 7dbe9174431f66dc2895b59f92acab6fb2bee231 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Wed, 25 Mar 2026 16:16:15 +0100 Subject: [PATCH 1/9] [wasm] Fix interpreter crash with MethodImpl .override on PortableEntryPoints Resolve MethodImpl overrides in getCallInfo for direct calls when FEATURE_PORTABLE_ENTRYPOINTS is enabled. On non-WASM, this resolution happens in getFunctionEntryPoint via MapMethodDeclToMethodImpl, but that function is not available with portable entry points. Adding the resolution to getCallInfo ensures the interpreter compiler receives the correct target MethodDesc at compile time. This fixes a crash where a non-virtual call to a MethodImpl-overridden method (e.g. call instance MyBar::DoBar() with .override pointing to DoBarOverride) would target the wrong method, leading to uninitialized interpreter code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/jitinterface.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index f1af7efa20c416..2cb3fbdb79054a 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -5372,6 +5372,20 @@ void CEEInfo::getCallInfo( #endif // STUB_DISPATCH_PORTABLE } +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + // On portable entry points, getFunctionEntryPoint is not available to resolve MethodImpl + // overrides. Do the resolution here so the interpreter compiler gets the right target + // method for non-virtual calls to MethodImpl-overridden methods. + if (directCall) + { + MethodDesc* pResolvedMD = MethodTable::MapMethodDeclToMethodImpl(pTargetMD); + if (pResolvedMD != pTargetMD) + { + pTargetMD = pResolvedMD; + } + } +#endif // FEATURE_PORTABLE_ENTRYPOINTS + pResult->hMethod = CORINFO_METHOD_HANDLE(pTargetMD); pResult->accessAllowed = CORINFO_ACCESS_ALLOWED; From 25941d01616c3e8db2254c3c2dc3ccc423e31fc4 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Wed, 8 Apr 2026 14:26:09 +0200 Subject: [PATCH 2/9] [wasm] Fix interpreter crash with .override on PortableEntryPoints Fix crash when a .override directive replaces a virtual method's vtable slot on WASM (PortableEntryPoints). The .override directive causes the overriding method's entry point to be placed in the overridden method's vtable slot. This makes SetNativeCode CAS fail for the overridden method (the slot no longer belongs to it), so SetInterpreterCode is never reached and GetInterpreterCode returns NULL, leading to a crash. Two fixes: - jitinterface.cpp: Resolve the .override at compile time in getCallInfo so the interpreter compiler targets the overriding method directly for non-virtual calls. - interpexec.cpp: In PrepareInterpreterCode, when GetInterpreterCode returns NULL and the vtable slot points to a different method due to .override, follow the redirect to prepare and cache the overriding method's interpreter code. This handles runtime paths (delegates, reflection) where the target is not known at compile time. Add delegate test coverage to self_override5.il using Delegate.CreateDelegate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/interpexec.cpp | 24 ++++++++ src/coreclr/vm/jitinterface.cpp | 5 +- .../MethodImpl/Desktop/self_override5.il | 61 +++++++++++++++++++ 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index 3f4c2279c4834d..4d35fc4e51393c 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -1159,6 +1159,30 @@ static InterpByteCodeStart* PrepareInterpreterCode(MethodDesc* targetMethod, Int } } InterpByteCodeStart* targetIp = targetMethod->GetInterpreterCode(); + +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + // Handle the case where .override replaces a virtual method's vtable slot. + // The targetMethod->GetInterpreterCode() above fails and we need to use the overriding + // method's interpreter code instead. + if (targetIp == NULL && !targetMethod->IsInterpreterCodePoisoned() + && targetMethod->IsVtableSlot()) + { + PCODE entryPoint = targetMethod->GetMethodEntryPointIfExists(); + if (entryPoint != (PCODE)NULL) + { + MethodDesc* pSlotMD = PortableEntryPoint::GetMethodDesc(entryPoint); + if (pSlotMD != NULL && pSlotMD != targetMethod + && pSlotMD->IsMethodImpl()) + { + targetIp = PrepareInterpreterCode(pSlotMD, pFrame, pInterpreterFrame, ip); + if (targetIp != NULL) + { + targetMethod->SetInterpreterCode(targetIp); + } + } + } + } +#endif // FEATURE_PORTABLE_ENTRYPOINTS if (targetIp == NULL) { // The prestub wasn't able to setup an interpreter code, so it will never be able to. diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 2cb3fbdb79054a..f02cddfc21599f 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -5373,9 +5373,8 @@ void CEEInfo::getCallInfo( } #ifdef FEATURE_PORTABLE_ENTRYPOINTS - // On portable entry points, getFunctionEntryPoint is not available to resolve MethodImpl - // overrides. Do the resolution here so the interpreter compiler gets the right target - // method for non-virtual calls to MethodImpl-overridden methods. + // Resolve the MethodImpl override here so that we call the overriding method directly, + // avoiding a DoPrestub failure when trying to set a non-overridden entry point in the slot. if (directCall) { MethodDesc* pResolvedMD = MethodTable::MapMethodDeclToMethodImpl(pTargetMD); diff --git a/src/tests/Loader/classloader/MethodImpl/Desktop/self_override5.il b/src/tests/Loader/classloader/MethodImpl/Desktop/self_override5.il index 6a4f305a4e34dc..157eca1c829e4a 100644 --- a/src/tests/Loader/classloader/MethodImpl/Desktop/self_override5.il +++ b/src/tests/Loader/classloader/MethodImpl/Desktop/self_override5.il @@ -124,6 +124,30 @@ L0: L1: + // Test delegate path: create a delegate to DoBar on a MyBar instance. + // Since DoBarOverride .overrides DoBar, invoking the delegate should + // execute MyBar::DoBarOverride and return 5. + ldc.i4.5 + newobj instance void MyBar::.ctor() + call bool CMain::TestDelegateDoBar(int32, class MyBar) + brtrue.s L2 + ldc.i4.0 + stloc.0 + +L2: + + // Test delegate path: create a delegate to DoBar on a MyFoo instance. + // Virtual dispatch through the delegate should execute + // MyFoo::DoBarOverride and return 6. + ldc.i4.6 + newobj instance void MyFoo::.ctor() + call bool CMain::TestDelegateDoBar(int32, class MyBar) + brtrue.s L3 + ldc.i4.0 + stloc.0 + +L3: + // return a status IL_0034: ldloc.0 IL_0035: brtrue.s IL_003b @@ -144,6 +168,43 @@ L1: IL_0041: ret } // end of method CMain::Main + // Helper method: creates a Func delegate targeting MyBar::DoBar on the + // given object, invokes it, and checks the result against the expected value. + .method public hidebysig static bool TestDelegateDoBar(int32 expected, class MyBar obj) cil managed + { + .maxstack 8 + .locals init (class [mscorlib]System.Func`1 del, + int32 result) + + // Func del = (Func)Delegate.CreateDelegate(typeof(Func), obj, "DoBar"); + ldtoken class [mscorlib]System.Func`1 + call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) + ldarg.1 + ldstr "DoBar" + call class [mscorlib]System.Delegate [mscorlib]System.Delegate::CreateDelegate(class [mscorlib]System.Type, object, string) + castclass class [mscorlib]System.Func`1 + stloc.0 + + // int result = del(); + ldloc.0 + callvirt instance !0 class [mscorlib]System.Func`1::Invoke() + stloc.1 + + // if (result == expected) return true; + ldloc.1 + ldarg.0 + beq.s DELEGATE_PASS + + ldstr "FAIL: delegate to DoBar returned wrong value" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.0 + ret + +DELEGATE_PASS: + ldc.i4.1 + ret + } // end of method CMain::TestDelegateDoBar + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed { From cf1c49abea6f5324cb80548a203139782758ab4b Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Wed, 8 Apr 2026 14:45:19 +0200 Subject: [PATCH 3/9] Update comment: use .override instead of MethodImpl Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/jitinterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index f02cddfc21599f..ebc2ef1af7b2a9 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -5373,7 +5373,7 @@ void CEEInfo::getCallInfo( } #ifdef FEATURE_PORTABLE_ENTRYPOINTS - // Resolve the MethodImpl override here so that we call the overriding method directly, + // Resolve the .override here so that we call the overriding method directly, // avoiding a DoPrestub failure when trying to set a non-overridden entry point in the slot. if (directCall) { From 80a782eeae2591c414209d99afe6e112c8ff7f09 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Wed, 8 Apr 2026 20:02:55 +0200 Subject: [PATCH 4/9] Replace compile-time .override resolution with ShouldCallPrestub fix Drop the MapMethodDeclToMethodImpl resolution in getCallInfo (Option D) which ran on every direct call (32,405 times per Loader suite, 1 hit). Instead, fix ShouldCallPrestub to check the method's own PortableEntryPoint when a .override directive has remapped its vtable slot. This is cheaper (IsVtableSlot flag check) and fixes the root cause for all callers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/jitinterface.cpp | 13 ------------- src/coreclr/vm/method.cpp | 9 +++++++++ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index ebc2ef1af7b2a9..f1af7efa20c416 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -5372,19 +5372,6 @@ void CEEInfo::getCallInfo( #endif // STUB_DISPATCH_PORTABLE } -#ifdef FEATURE_PORTABLE_ENTRYPOINTS - // Resolve the .override here so that we call the overriding method directly, - // avoiding a DoPrestub failure when trying to set a non-overridden entry point in the slot. - if (directCall) - { - MethodDesc* pResolvedMD = MethodTable::MapMethodDeclToMethodImpl(pTargetMD); - if (pResolvedMD != pTargetMD) - { - pTargetMD = pResolvedMD; - } - } -#endif // FEATURE_PORTABLE_ENTRYPOINTS - pResult->hMethod = CORINFO_METHOD_HANDLE(pTargetMD); pResult->accessAllowed = CORINFO_ACCESS_ALLOWED; diff --git a/src/coreclr/vm/method.cpp b/src/coreclr/vm/method.cpp index 32d9a717280647..0e58a8021f6a8f 100644 --- a/src/coreclr/vm/method.cpp +++ b/src/coreclr/vm/method.cpp @@ -2485,6 +2485,15 @@ BOOL MethodDesc::ShouldCallPrestub() #ifdef FEATURE_PORTABLE_ENTRYPOINTS methodEntryPoint = GetStableEntryPoint(); + // When the .override directive remaps this method's vtable slot, the entry point + // may be a different method's PortableEntryPoint (the overriding method). In that case, check this + // method's own PortableEntryPoint instead. + if (IsVtableSlot()) + { + PCODE ownEntryPoint = GetPortableEntryPointIfExists(); + if (ownEntryPoint != (PCODE)NULL && ownEntryPoint != methodEntryPoint) + methodEntryPoint = ownEntryPoint; + } return methodEntryPoint == (PCODE)NULL || (!PortableEntryPoint::HasInterpreterData(methodEntryPoint) && !PortableEntryPoint::HasNativeEntryPoint(methodEntryPoint)); From 4cc14e63d9e7bf6ca31f638b6195150c91430f4a Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Wed, 22 Apr 2026 17:10:26 +0200 Subject: [PATCH 5/9] Add PrepareMethod + .override regression test Test RuntimeHelpers.PrepareMethod on a method whose vtable slot has been remapped by a .override directive (scenario from jkotas review comment). Verifies that PrepareMethod correctly prepares the overriding method's body, and that subsequent virtual, non-virtual, and direct calls all execute the override. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Desktop/self_override_preparemethod.il | 170 ++++++++++++++++++ .../self_override_preparemethod.ilproj | 13 ++ 2 files changed, 183 insertions(+) create mode 100644 src/tests/Loader/classloader/MethodImpl/Desktop/self_override_preparemethod.il create mode 100644 src/tests/Loader/classloader/MethodImpl/self_override_preparemethod.ilproj diff --git a/src/tests/Loader/classloader/MethodImpl/Desktop/self_override_preparemethod.il b/src/tests/Loader/classloader/MethodImpl/Desktop/self_override_preparemethod.il new file mode 100644 index 00000000000000..a5519ba6677746 --- /dev/null +++ b/src/tests/Loader/classloader/MethodImpl/Desktop/self_override_preparemethod.il @@ -0,0 +1,170 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Regression test for RuntimeHelpers.PrepareMethod with .override (MethodImpl). +// +// MyClass::B explicitly overrides MyClass::A via .override directive. +// The test calls RuntimeHelpers.PrepareMethod on A's MethodHandle, which must +// also correctly handle B's body (since B replaces A's slot). On WASM with +// PortableEntryPoints, this could crash if the override is not accounted for +// during method preparation. +// +// After PrepareMethod, we verify that virtual and non-virtual calls to A +// both execute B's body (as expected with .override), and that calling B +// directly also works. + +.assembly extern mscorlib{} +.assembly extern xunit.core {} +.assembly self_override_preparemethod{} + +.class public MyClass extends [mscorlib]System.Object +{ + .method public virtual newslot instance int32 A() + { + .maxstack 8 + ldstr "In MyClass::A" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.4 + ret + } + + .method public virtual newslot instance int32 B() + { + .override MyClass::A + .maxstack 8 + ldstr "In MyClass::B" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.5 + ret + } + + .method public hidebysig specialname rtspecialname + instance void .ctor() + { + .maxstack 8 + ldarg.0 + call instance void [mscorlib]System.Object::.ctor() + ret + } +} + +.class public beforefieldinit CMain extends [mscorlib]System.Object +{ + .method public hidebysig static int32 Main() cil managed + { + .custom instance void [xunit.core]Xunit.FactAttribute::.ctor() = ( + 01 00 00 00 + ) + .entrypoint + .maxstack 4 + + .locals init ( + bool V_0, + class MyClass V_1, + int32 V_2, + valuetype [mscorlib]System.RuntimeMethodHandle V_3 + ) + + ldc.i4.1 + stloc.0 + + // Create an instance of MyClass + newobj instance void MyClass::.ctor() + stloc.1 + + // Get typeof(MyClass).GetMethod("A").MethodHandle + ldtoken MyClass + call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) + ldstr "A" + callvirt instance class [mscorlib]System.Reflection.MethodInfo [mscorlib]System.Type::GetMethod(string) + callvirt instance valuetype [mscorlib]System.RuntimeMethodHandle [mscorlib]System.Reflection.MethodBase::get_MethodHandle() + stloc.3 + + // Call RuntimeHelpers.PrepareMethod(h) - this is the key operation under test. + // On WASM/PortableEntryPoints, this must not crash even though B overrides A. + ldloc.3 + call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::PrepareMethod(valuetype [mscorlib]System.RuntimeMethodHandle) + + // Test 1: Virtual call to A() should execute B's body (returns 5) + // because B has .override on A, replacing A's vtable slot. + ldc.i4.5 + ldloc.1 + callvirt instance int32 MyClass::A() + beq.s TEST1_PASS + + ldstr "FAIL: Virtual call to A() did not execute B's body" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.0 + stloc.0 + br.s TEST2 + + TEST1_PASS: + ldstr "PASS: Virtual call to A() correctly executed B's body" + call void [mscorlib]System.Console::WriteLine(string) + + TEST2: + // Test 2: Non-virtual call to A() should also execute B's body (returns 5) + // because .override replaces the method body at the slot level. + ldc.i4.5 + ldloc.1 + call instance int32 MyClass::A() + beq.s TEST2_PASS + + ldstr "FAIL: Non-virtual call to A() did not execute B's body" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.0 + stloc.0 + br.s TEST3 + + TEST2_PASS: + ldstr "PASS: Non-virtual call to A() correctly executed B's body" + call void [mscorlib]System.Console::WriteLine(string) + + TEST3: + // Test 3: Direct virtual call to B() should execute B's body (returns 5) + ldc.i4.5 + ldloc.1 + callvirt instance int32 MyClass::B() + beq.s TEST3_PASS + + ldstr "FAIL: Call to B() did not return expected value" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.0 + stloc.0 + br.s DONE + + TEST3_PASS: + ldstr "PASS: Call to B() returned expected value" + call void [mscorlib]System.Console::WriteLine(string) + + DONE: + // Return 100 for PASS, 101 for FAIL + ldloc.0 + brtrue.s PASS + + ldc.i4.s 101 + stloc.2 + ldstr "FAIL" + call void [mscorlib]System.Console::WriteLine(string) + br.s EXIT + + PASS: + ldc.i4.s 100 + stloc.2 + ldstr "PASS" + call void [mscorlib]System.Console::WriteLine(string) + + EXIT: + ldloc.2 + ret + } // end of method CMain::Main + + .method public hidebysig specialname rtspecialname + instance void .ctor() cil managed + { + .maxstack 8 + ldarg.0 + call instance void [mscorlib]System.Object::.ctor() + ret + } +} // end of class CMain diff --git a/src/tests/Loader/classloader/MethodImpl/self_override_preparemethod.ilproj b/src/tests/Loader/classloader/MethodImpl/self_override_preparemethod.ilproj new file mode 100644 index 00000000000000..2082409ea4431f --- /dev/null +++ b/src/tests/Loader/classloader/MethodImpl/self_override_preparemethod.ilproj @@ -0,0 +1,13 @@ + + + 1 + + true + + + pdbonly + + + + + From 09127984e8d69aee1a625cda43416c9a99b4e1a6 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Wed, 22 Apr 2026 17:36:26 +0200 Subject: [PATCH 6/9] Resolve .override upfront in PrepareInterpreterCode Replace the post-DoPrestub retry with an upfront MapMethodDeclToMethodImpl call, so we compile the correct (overriding) method body on the first attempt. Cache the result on the original MethodDesc so callers that check IsInterpreterCodeInitialized don't re-resolve. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/interpexec.cpp | 48 ++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index e704d1f38133ba..b92bee87d7255d 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -1148,6 +1148,21 @@ static InterpByteCodeStart* PrepareInterpreterCode(MethodDesc* targetMethod, Int // small subset of frames high. pFrame->ip = ip; pInterpreterFrame->SetTopInterpMethodContextFrame(pFrame); + +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + // Resolve .override before compilation: if a MethodImpl has remapped + // targetMethod's vtable slot, switch to the overriding method so we + // compile the correct body. Cache the result on the original MethodDesc + // so callers that check IsInterpreterCodeInitialized don't re-resolve. + MethodDesc* pOriginalMethod = targetMethod; + if (targetMethod->IsVtableSlot()) + { + MethodDesc* pResolved = MethodTable::MapMethodDeclToMethodImpl(targetMethod); + if (pResolved != targetMethod) + targetMethod = pResolved; + } +#endif // FEATURE_PORTABLE_ENTRYPOINTS + { GCX_PREEMP(); if (targetMethod->ShouldCallPrestub()) @@ -1160,34 +1175,21 @@ static InterpByteCodeStart* PrepareInterpreterCode(MethodDesc* targetMethod, Int } InterpByteCodeStart* targetIp = targetMethod->GetInterpreterCode(); -#ifdef FEATURE_PORTABLE_ENTRYPOINTS - // Handle the case where .override replaces a virtual method's vtable slot. - // The targetMethod->GetInterpreterCode() above fails and we need to use the overriding - // method's interpreter code instead. - if (targetIp == NULL && !targetMethod->IsInterpreterCodePoisoned() - && targetMethod->IsVtableSlot()) - { - PCODE entryPoint = targetMethod->GetMethodEntryPointIfExists(); - if (entryPoint != (PCODE)NULL) - { - MethodDesc* pSlotMD = PortableEntryPoint::GetMethodDesc(entryPoint); - if (pSlotMD != NULL && pSlotMD != targetMethod - && pSlotMD->IsMethodImpl()) - { - targetIp = PrepareInterpreterCode(pSlotMD, pFrame, pInterpreterFrame, ip); - if (targetIp != NULL) - { - targetMethod->SetInterpreterCode(targetIp); - } - } - } - } -#endif // FEATURE_PORTABLE_ENTRYPOINTS if (targetIp == NULL) { // The prestub wasn't able to setup an interpreter code, so it will never be able to. targetMethod->PoisonInterpreterCode(); +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + if (pOriginalMethod != targetMethod) + pOriginalMethod->PoisonInterpreterCode(); +#endif } +#ifdef FEATURE_PORTABLE_ENTRYPOINTS + else if (pOriginalMethod != targetMethod) + { + pOriginalMethod->SetInterpreterCode(targetIp); + } +#endif return targetIp; } From 6e228e5b3bd2167c542a6c8b01e4a2d9bf9e4f54 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Mon, 27 Apr 2026 16:40:34 +0200 Subject: [PATCH 7/9] Delete self_override_preparemethod test Not testing anything interesting per review feedback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Desktop/self_override_preparemethod.il | 170 ------------------ .../self_override_preparemethod.ilproj | 13 -- 2 files changed, 183 deletions(-) delete mode 100644 src/tests/Loader/classloader/MethodImpl/Desktop/self_override_preparemethod.il delete mode 100644 src/tests/Loader/classloader/MethodImpl/self_override_preparemethod.ilproj diff --git a/src/tests/Loader/classloader/MethodImpl/Desktop/self_override_preparemethod.il b/src/tests/Loader/classloader/MethodImpl/Desktop/self_override_preparemethod.il deleted file mode 100644 index a5519ba6677746..00000000000000 --- a/src/tests/Loader/classloader/MethodImpl/Desktop/self_override_preparemethod.il +++ /dev/null @@ -1,170 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -// Regression test for RuntimeHelpers.PrepareMethod with .override (MethodImpl). -// -// MyClass::B explicitly overrides MyClass::A via .override directive. -// The test calls RuntimeHelpers.PrepareMethod on A's MethodHandle, which must -// also correctly handle B's body (since B replaces A's slot). On WASM with -// PortableEntryPoints, this could crash if the override is not accounted for -// during method preparation. -// -// After PrepareMethod, we verify that virtual and non-virtual calls to A -// both execute B's body (as expected with .override), and that calling B -// directly also works. - -.assembly extern mscorlib{} -.assembly extern xunit.core {} -.assembly self_override_preparemethod{} - -.class public MyClass extends [mscorlib]System.Object -{ - .method public virtual newslot instance int32 A() - { - .maxstack 8 - ldstr "In MyClass::A" - call void [mscorlib]System.Console::WriteLine(string) - ldc.i4.4 - ret - } - - .method public virtual newslot instance int32 B() - { - .override MyClass::A - .maxstack 8 - ldstr "In MyClass::B" - call void [mscorlib]System.Console::WriteLine(string) - ldc.i4.5 - ret - } - - .method public hidebysig specialname rtspecialname - instance void .ctor() - { - .maxstack 8 - ldarg.0 - call instance void [mscorlib]System.Object::.ctor() - ret - } -} - -.class public beforefieldinit CMain extends [mscorlib]System.Object -{ - .method public hidebysig static int32 Main() cil managed - { - .custom instance void [xunit.core]Xunit.FactAttribute::.ctor() = ( - 01 00 00 00 - ) - .entrypoint - .maxstack 4 - - .locals init ( - bool V_0, - class MyClass V_1, - int32 V_2, - valuetype [mscorlib]System.RuntimeMethodHandle V_3 - ) - - ldc.i4.1 - stloc.0 - - // Create an instance of MyClass - newobj instance void MyClass::.ctor() - stloc.1 - - // Get typeof(MyClass).GetMethod("A").MethodHandle - ldtoken MyClass - call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle) - ldstr "A" - callvirt instance class [mscorlib]System.Reflection.MethodInfo [mscorlib]System.Type::GetMethod(string) - callvirt instance valuetype [mscorlib]System.RuntimeMethodHandle [mscorlib]System.Reflection.MethodBase::get_MethodHandle() - stloc.3 - - // Call RuntimeHelpers.PrepareMethod(h) - this is the key operation under test. - // On WASM/PortableEntryPoints, this must not crash even though B overrides A. - ldloc.3 - call void [mscorlib]System.Runtime.CompilerServices.RuntimeHelpers::PrepareMethod(valuetype [mscorlib]System.RuntimeMethodHandle) - - // Test 1: Virtual call to A() should execute B's body (returns 5) - // because B has .override on A, replacing A's vtable slot. - ldc.i4.5 - ldloc.1 - callvirt instance int32 MyClass::A() - beq.s TEST1_PASS - - ldstr "FAIL: Virtual call to A() did not execute B's body" - call void [mscorlib]System.Console::WriteLine(string) - ldc.i4.0 - stloc.0 - br.s TEST2 - - TEST1_PASS: - ldstr "PASS: Virtual call to A() correctly executed B's body" - call void [mscorlib]System.Console::WriteLine(string) - - TEST2: - // Test 2: Non-virtual call to A() should also execute B's body (returns 5) - // because .override replaces the method body at the slot level. - ldc.i4.5 - ldloc.1 - call instance int32 MyClass::A() - beq.s TEST2_PASS - - ldstr "FAIL: Non-virtual call to A() did not execute B's body" - call void [mscorlib]System.Console::WriteLine(string) - ldc.i4.0 - stloc.0 - br.s TEST3 - - TEST2_PASS: - ldstr "PASS: Non-virtual call to A() correctly executed B's body" - call void [mscorlib]System.Console::WriteLine(string) - - TEST3: - // Test 3: Direct virtual call to B() should execute B's body (returns 5) - ldc.i4.5 - ldloc.1 - callvirt instance int32 MyClass::B() - beq.s TEST3_PASS - - ldstr "FAIL: Call to B() did not return expected value" - call void [mscorlib]System.Console::WriteLine(string) - ldc.i4.0 - stloc.0 - br.s DONE - - TEST3_PASS: - ldstr "PASS: Call to B() returned expected value" - call void [mscorlib]System.Console::WriteLine(string) - - DONE: - // Return 100 for PASS, 101 for FAIL - ldloc.0 - brtrue.s PASS - - ldc.i4.s 101 - stloc.2 - ldstr "FAIL" - call void [mscorlib]System.Console::WriteLine(string) - br.s EXIT - - PASS: - ldc.i4.s 100 - stloc.2 - ldstr "PASS" - call void [mscorlib]System.Console::WriteLine(string) - - EXIT: - ldloc.2 - ret - } // end of method CMain::Main - - .method public hidebysig specialname rtspecialname - instance void .ctor() cil managed - { - .maxstack 8 - ldarg.0 - call instance void [mscorlib]System.Object::.ctor() - ret - } -} // end of class CMain diff --git a/src/tests/Loader/classloader/MethodImpl/self_override_preparemethod.ilproj b/src/tests/Loader/classloader/MethodImpl/self_override_preparemethod.ilproj deleted file mode 100644 index 2082409ea4431f..00000000000000 --- a/src/tests/Loader/classloader/MethodImpl/self_override_preparemethod.ilproj +++ /dev/null @@ -1,13 +0,0 @@ - - - 1 - - true - - - pdbonly - - - - - From 9937500b5aec2f921e6622edb64430b9961ff9d3 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Mon, 27 Apr 2026 16:51:26 +0200 Subject: [PATCH 8/9] Add generic .override regression test Tests that .override on generic methods (B overriding A) works correctly for both reference types (string) and value types (int32), via virtual and non-virtual calls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Desktop/self_override_generic.il | 130 ++++++++++++++++++ .../MethodImpl/self_override_generic.ilproj | 8 ++ 2 files changed, 138 insertions(+) create mode 100644 src/tests/Loader/classloader/MethodImpl/Desktop/self_override_generic.il create mode 100644 src/tests/Loader/classloader/MethodImpl/self_override_generic.ilproj diff --git a/src/tests/Loader/classloader/MethodImpl/Desktop/self_override_generic.il b/src/tests/Loader/classloader/MethodImpl/Desktop/self_override_generic.il new file mode 100644 index 00000000000000..b1cab5f7c9c713 --- /dev/null +++ b/src/tests/Loader/classloader/MethodImpl/Desktop/self_override_generic.il @@ -0,0 +1,130 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// Tests .override (MethodImpl) on generic methods. +// Program::B() explicitly overrides Program::A(). +// Both virtual and non-virtual calls to A() should execute B(), +// for both reference type (string) and value type (int32) instantiations. + +.assembly extern mscorlib{} +.assembly extern xunit.core {} +.assembly self_override_generic{} + +.class public Program extends [mscorlib]System.Object +{ + .method public virtual newslot instance int32 A() cil managed + { + ldstr "In A" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.1 + ret + } + + .method public virtual newslot instance int32 B() cil managed + { + .override method instance int32 Program::A<[1]>() + ldstr "In B" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.2 + ret + } + + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + ldarg.0 + call instance void [mscorlib]System.Object::.ctor() + ret + } +} + +.class public beforefieldinit CMain extends [mscorlib]System.Object +{ + .method public hidebysig static int32 Main() cil managed + { + .custom instance void [xunit.core]Xunit.FactAttribute::.ctor() = ( + 01 00 00 00 + ) + .entrypoint + .locals init (class Program o, bool pass) + + ldc.i4.1 + stloc.1 + + newobj instance void Program::.ctor() + stloc.0 + + // Test 1: o.A() virtually - should return 2 (B called) + ldstr "Test 1: calling A() virtually..." + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.2 + ldloc.0 + callvirt instance int32 Program::A() + beq.s T1_PASS + ldstr "FAIL: A() virtual call did not execute B" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.0 + stloc.1 +T1_PASS: + + // Test 2: o.A() virtually - should return 2 (B called) + ldstr "Test 2: calling A() virtually..." + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.2 + ldloc.0 + callvirt instance int32 Program::A() + beq.s T2_PASS + ldstr "FAIL: A() virtual call did not execute B" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.0 + stloc.1 +T2_PASS: + + // Test 3: o.A() non-virtually - should return 2 (body replaced) + ldstr "Test 3: calling A() non-virtually..." + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.2 + ldloc.0 + call instance int32 Program::A() + beq.s T3_PASS + ldstr "FAIL: A() non-virtual call did not execute B" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.0 + stloc.1 +T3_PASS: + + // Test 4: o.A() non-virtually - should return 2 (body replaced) + ldstr "Test 4: calling A() non-virtually..." + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.2 + ldloc.0 + call instance int32 Program::A() + beq.s T4_PASS + ldstr "FAIL: A() non-virtual call did not execute B" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.0 + stloc.1 +T4_PASS: + + // Return result + ldloc.1 + brtrue.s PASS + + ldstr "FAIL" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.s 101 + ret + +PASS: + ldstr "PASS" + call void [mscorlib]System.Console::WriteLine(string) + ldc.i4.s 100 + ret + } + + .method public hidebysig specialname rtspecialname instance void .ctor() cil managed + { + ldarg.0 + call instance void [mscorlib]System.Object::.ctor() + ret + } +} diff --git a/src/tests/Loader/classloader/MethodImpl/self_override_generic.ilproj b/src/tests/Loader/classloader/MethodImpl/self_override_generic.ilproj new file mode 100644 index 00000000000000..b4274e96c9993e --- /dev/null +++ b/src/tests/Loader/classloader/MethodImpl/self_override_generic.ilproj @@ -0,0 +1,8 @@ + + + 1 + + + + + From 4adec2fd5be4bd88aec557b0ec6c62ec18864390 Mon Sep 17 00:00:00 2001 From: Radek Doulik Date: Wed, 29 Apr 2026 16:27:11 +0200 Subject: [PATCH 9/9] Feedback Co-authored-by: Jan Kotas --- src/coreclr/vm/interpexec.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/coreclr/vm/interpexec.cpp b/src/coreclr/vm/interpexec.cpp index b92bee87d7255d..ccdbefd7311cb7 100644 --- a/src/coreclr/vm/interpexec.cpp +++ b/src/coreclr/vm/interpexec.cpp @@ -1157,9 +1157,7 @@ static InterpByteCodeStart* PrepareInterpreterCode(MethodDesc* targetMethod, Int MethodDesc* pOriginalMethod = targetMethod; if (targetMethod->IsVtableSlot()) { - MethodDesc* pResolved = MethodTable::MapMethodDeclToMethodImpl(targetMethod); - if (pResolved != targetMethod) - targetMethod = pResolved; + targetMethod = MethodTable::MapMethodDeclToMethodImpl(targetMethod); } #endif // FEATURE_PORTABLE_ENTRYPOINTS