Skip to content
Open
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
26 changes: 26 additions & 0 deletions src/coreclr/vm/interpexec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,21 @@ static InterpByteCodeStart* PrepareInterpreterCode(MethodDesc* targetMethod, Int
// small subset of frames high.
pFrame->ip = ip;
pInterpreterFrame->SetTopInterpMethodContextFrame(pFrame);

Copy link
Copy Markdown
Member

@jkotas jkotas Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check that this fix works correctly for generics too?

using System;
using System.Runtime.CompilerServices;

class Program
{
    public virtual void A<T>() => Console.WriteLine("A");

     //  Add `.override Program::A` to .il
    public virtual void B<T>() => Console.WriteLine("B");

    static void Main() 
    {          
         Program o = new Program();
         o.A<string>(); // Also, try this for `A<int>`
    }
}

#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;
Comment on lines +1160 to +1162
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.

Suggested change
MethodDesc* pResolved = MethodTable::MapMethodDeclToMethodImpl(targetMethod);
if (pResolved != targetMethod)
targetMethod = pResolved;
targetMethod = MethodTable::MapMethodDeclToMethodImpl(targetMethod);

}
#endif // FEATURE_PORTABLE_ENTRYPOINTS

{
GCX_PREEMP();
if (targetMethod->ShouldCallPrestub())
Expand All @@ -1159,11 +1174,22 @@ static InterpByteCodeStart* PrepareInterpreterCode(MethodDesc* targetMethod, Int
}
}
InterpByteCodeStart* targetIp = targetMethod->GetInterpreterCode();

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;
}
Expand Down
9 changes: 9 additions & 0 deletions src/coreclr/vm/method.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2483,6 +2483,15 @@ BOOL MethodDesc::ShouldCallPrestub()

#ifdef FEATURE_PORTABLE_ENTRYPOINTS
methodEntryPoint = GetStableEntryPoint();
// When the .override directive remaps this method's vtable slot, the entry point
Copy link
Copy Markdown
Member

@jkotas jkotas Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar issue exists for some of the existing callers of ShouldCallPrestub on non-portable entrypoints targets.

For example, if you run the following on a regular x64 runtime, PrepareMethod won't actually compile the B method body.

using System;
using System.Runtime.CompilerServices;

class Program
{
    public virtual void A() => Console.WriteLine("A");

     //  Add `.override Program::A` to .il
    public virtual void B() => Console.WriteLine("B");

    static void Main() 
    {          
        var h = typeof(Program).GetMethod("A").MethodHandle;
        for (;;) RuntimeHelpers.PrepareMethod(h);
    }
}

You may want to consider and fix these cases too when devising the fix.

Copy link
Copy Markdown
Member

@jkotas jkotas Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should decide whether it is ok to call ShouldCallPrestub and DoPrestub with the decl method and then fix either ShouldCallPrestub/DoPrestub or its callers.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to consider and fix these cases too when devising the fix.

I have added test for that case and it is already handled with the current change.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should decide whether it is ok to call ShouldCallPrestub and DoPrestub with the decl method and then fix either ShouldCallPrestub/DoPrestub or its callers.

Is the updated change fine or do you prefer to explore this path as well? I can continue with it tomorrow.

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.

I have added test for that case and it is already handled with the current change.

I do not think the test is testing what I tried to highlight. PrepareMethod(methodHandle) is expected to JIT the code that is going to be executed when you call that method. I do think it is happening currently.

Your test is only verifying that we will end up executing the correct method once you actually try to call it. It is not verifying that the method was JITed upfront.

It is a bit involved to write tests for PrepareMethod. You would have to listed to JITed method event via EventSource (that we do not have enabled on wasm yet). I am not asking that you write a test for the PrepareMethod as part of this PR.

// 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));
Expand Down
61 changes: 61 additions & 0 deletions src/tests/Loader/classloader/MethodImpl/Desktop/self_override5.il
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -144,6 +168,43 @@ L1:
IL_0041: ret
} // end of method CMain::Main

// Helper method: creates a Func<int> 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<int32> del,
int32 result)

// Func<int> del = (Func<int>)Delegate.CreateDelegate(typeof(Func<int>), obj, "DoBar");
ldtoken class [mscorlib]System.Func`1<int32>
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<int32>
stloc.0

// int result = del();
ldloc.0
callvirt instance !0 class [mscorlib]System.Func`1<int32>::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
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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).
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.

This test can be deleted. It is not testing anything interesting.

//
// 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk.IL">
<PropertyGroup>
<CLRTestPriority>1</CLRTestPriority>
<!-- RuntimeHelpers.PrepareMethod is not supported on NativeAOT -->
<NativeAotIncompatible>true</NativeAotIncompatible>
</PropertyGroup>
<PropertyGroup>
<DebugType>pdbonly</DebugType>
</PropertyGroup>
<ItemGroup>
<Compile Include="Desktop\self_override_preparemethod.il" />
</ItemGroup>
</Project>
Loading