Skip to content

Wasm: Runtime_79354 fails with test assembly R2R #131886

Description

@AndyAyersMS

Description

On wasm (FEATURE_PORTABLE_ENTRYPOINTS + cached interface dispatch), invoking an open virtual/interface delegate from ReadyToRun-compiled code asserts in the interpreter:

Assert failure: portableEntryPoint->IsValid()
  src/coreclr/vm/precode_portable.cpp:109  ToPortableEntryPoint
  Frame (InterpreterFrame): MyDelegate::IL_STUB_DelegateShuffleThunk, IR_0014

Found while ReadyToRun-compiling and running the src/tests/JIT tree on wasm. The failing case is src/tests/JIT/Regression/JitBlue/Runtime_79354/Runtime_79354.cs — an open-instance delegate over an interface method (IGetContents.GetContents()), created via MethodInfo.CreateDelegate. IL/interpreter execution passes; only R2R fails.

Root cause

For an open virtual/interface delegate, COMDelegate sets _methodPtrAux = GetVirtualCallStub(...), which under cached interface dispatch is (PCODE)CID_VirtualOpenDelegateDispatch — a native helper, not a portable entry point (comdelegate.cpp:1201, :961).

The delegate's _methodPtr is the IL shuffle thunk (CreateILDelegateShuffleThunk), which does LDFLD _methodPtrAux; calli (comdelegate.cpp:853-854). On wasm the shuffle thunk runs interpreted.

The interpreter's INTOP_CALLDELEGATE path already handles the open-virtual case (detects the CID_VirtualOpenDelegateDispatch / VSD marker and resolves the real target — interpexec.cpp:3422-3448). But that path is bypassed when the delegate is invoked from R2R-compiled code, which calls the shuffle thunk directly. The shuffle thunk's own calli then reaches INTOP_CALLI, which does PortableEntryPoint::GetMethodDesc(calliFunctionPointer) (interpexec.cpp:3326) on the native CID_VirtualOpenDelegateDispatch helper -> ToPortableEntryPoint -> IsValid() assert.

In a release build this is not a checked assert, but the calli target is still not a valid portable entry point, so the dispatch is incorrect.

Repro (wasm R2R)

  1. Build clr+libs -os browser (Checked).
  2. ReadyToRun-compile Runtime_79354.dll (crossgen2, --targetarch:wasm --targetos:browser) and run under corerun.js with the R2R companion enabled (PLATFORM_NATIVE_R2R=1, APP_ASSEMBLIES=EXTERNAL).

Minimal shape:

public interface IGetContents { (string, int, string) GetContents(); }
public struct MyStruct : IGetContents { /* ... */ public (string,int,string) GetContents() => (s1, a, s2); }
public delegate (string, int, string) MyDelegate(IGetContents arg);

var mi = typeof(IGetContents).GetMethod("GetContents");
var func = (MyDelegate)mi.CreateDelegate(typeof(MyDelegate));  // open delegate over interface method
var r = func(new MyStruct { /* ... */ });                     // asserts under wasm R2R

Suggested fix

Teach the interpreter's INTOP_CALLI (portable-entrypoints path) to recognize the open-virtual dispatch marker — calliFunctionPointer == CID_VirtualOpenDelegateDispatch (cached dispatch) or VirtualCallStubManager::isStubStatic(...) (VSD) — before treating the target as a portable entry point, and resolve the real target the same way INTOP_CALLDELEGATE does (from the shuffle-thunk delegate this + receiver). A working prototype exists.

Notes

  • Area: wasm interpreter / R2R (portable entry points).

Note

This issue was drafted with GitHub Copilot.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions