Repository navigation
[wasm] R2R: open-instance delegate over a virtual/interface method faults in the interpreter's INTOP_CALLDELEGATE #130840
Description
Description
Under ReadyToRun on wasm, invoking an open-instance delegate whose target is a virtual or interface method faults in the interpreter: RuntimeError: memory access out of bounds (release) / NullReferenceException (checked). It passes interpreted and passes with only the corelib R2R'd; it fails when the calling method is itself R2R-compiled (so the R2R method calls the still-interpreted delegate Invoke via the R2R→interpreter thunk ExecuteInterpretedMethodWithArgs_PortableEntryPoint).
Root cause (source-pinned): The fault is in the interpreter's INTOP_CALLDELEGATE open-virtual branch (src/coreclr/vm/interpexec.cpp:3396-3405). Only open-instance delegates over a virtual/interface method reach this branch; it reads the receiver:
OBJECTREF *pThisArg = LOCAL_VAR_ADDR(callArgsOffset + INTERP_STACK_SLOT_SIZE, OBJECTREF); // 3399
NULL_CHECK(*pThisArg); // 3400
... targetMethod->GetMethodDescOfVirtualizedCode(pThisArg, targetMethod->GetMethodTable()); // 3403The receiver at callArgsOffset + INTERP_STACK_SLOT_SIZE is mis-located on the R2R→interpreter entry path: it holds a garbage value (observed 0x614F646C in release), so GetMethodDescOfVirtualizedCode dereferences a bad MethodTable → OOB (release), or the mislocated slot is null → NRE (checked). A valid allocated receiver always has a loaded MethodTable+EEClass, so the garbage value confirms mis-location (not a type-loading/activation gap).
The suspect is the delegate arg-shuffle sizing that "removes the delegate object from the argument list": ShiftDelegateCallArgs(stack, callArgsOffset, sizeOfArgsUpto16ByteAlignment /*ip[4]*/, targetArgsSize /*ip[5]*/) (3422/3428/3455), computed by the interpreter compiler's INTOP_CALLDELEGATE lowering — vs the actual argument layout when the delegate Invoke is entered via the PortableEntryPoint thunk's memcpy(sp, args, argsSize).
Proof (variant matrix → source branches, 1:1):
| Variant | Delegate / dispatch | Interp branch | Result |
|---|---|---|---|
| direct interface call, no delegate | — | no INTOP_CALLDELEGATE |
pass |
| closed delegate | closed | 3461 GetTarget |
pass |
| open delegate, non-virtual target | open, non-virtual | 3408 NonVirtualEntry2MethodDesc |
pass |
| open delegate, virtual/interface target | open, virtual | 3396-3405 receiver deref |
fault |
Multi-register struct return, boxing, and the shuffle thunk itself were ruled out by the variant matrix.
Reproduction Steps
Crossgen a test to R2R that invokes an open-instance delegate over an interface method, then run under R2R on wasm:
interface IGetInt { int GetInt(); }
struct MyStruct : IGetInt { public int a; public int GetInt() => a; }
delegate int D(IGetInt arg);
static int Main()
{
var s = new MyStruct { a = 42 };
var mi = typeof(IGetInt).GetMethod("GetInt");
var func = (D)mi.CreateDelegate(typeof(D)); // open-instance delegate
return func(s) == 42 ? 100 : 1;
}JIT/Regression Runtime_79354 reproduces it when crossgen'd to R2R.
Expected behavior
Open-instance delegate invoke over a virtual/interface method returns correctly under R2R (exit 100), matching the interpreter.
Actual behavior
RuntimeError: memory access out of bounds (release) / NullReferenceException (checked) inside the interpreter's INTOP_CALLDELEGATE open-virtual dispatch, from a mis-located receiver.
Regression?
No — new R2R-on-wasm bring-up path.
Known Workarounds
Run the test method without ReadyToRun (DOTNET_ReadyToRun=0), or use a closed delegate.
Configuration
CoreCLR, browser-wasm / wasi-wasm, FEATURE_PORTABLE_ENTRYPOINTS + interpreter, ReadyToRun enabled (calling method R2R-compiled).
Other information
Fix direction (for the interpreter/bridge owner): reconcile the INTOP_CALLDELEGATE open-virtual arg-shuffle sizing (ip[4]/ip[5]) and receiver slot (callArgsOffset + INTERP_STACK_SLOT_SIZE) with the argument layout produced by the R2R→interpreter thunk marshalling, so the receiver lands where line 3399 reads it. Exact faulting field (receiver offset vs shuffle size) pending a checked-crossgen runtime read. Related: #130634 (R2R↔interpreter bridge trap family).
Note
This issue was authored with the assistance of GitHub Copilot.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
dotnet-policy-service commentedon Jul 16, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
dotnet-policy-service commentedon Jul 16, 2026
Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.
lewing commentedon Jul 16, 2026
cc @dotnet/wasm-contrib
alexyakunin commentedon Jul 31, 2026
Not wasm-specific — we hit the same fault on iOS arm64 (CoreCLR + composite R2R + interpreter), so the
arch-wasmlabel may be too narrow.Config: .NET 11 preview 6 (
11.0.0-preview.6.26359.118),net11.0-ios/ios-arm64, iOS 26.5 device, MAUI Blazor Hybrid app, Release, composite ReadyToRun,UseInterpreter=true.Our trigger is the same construct as the repro in the description — an open-instance delegate over an interface method, created via
CreateDelegatewith no target and invoked from R2R-compiled code:The arm64 stack, faulting in the open-virtual dispatch beneath interpreted frames, with R2R frames below:
Secondary issue, possibly worth separating: on iOS this never presents as a crash.
EEPolicy::LogManagedCallstackForSignalsymbolicating the managed stack on-device outlasts iOS's 5s watchdog, so the process is killed with0x8BADF00D("Failed to terminate gracefully after 5.0s",WatchdogVisibility: Foreground) while stuck inTypeString::AppendMethodImpl. Four consecutive occurrences, always thread 0. To the user the app paints its UI and then silently stops responding to touch, with nothing in the app's own logs — considerably harder to attribute than a plain SIGSEGV. A variant showsPAL_DispatchException→cthread_yield.Workaround: don't enable the interpreter. Note this is coupled on Apple targets: with
UseInterpreter/MtouchInterpreterunset the macios SDK setsDynamicCodeSupport=false, soRuntimeFeature.IsDynamicCodeSupportedgoes false and anything relying on runtime codegen (for us, MessagePack'sDynamicObjectResolver) stops working too. So on iOS today the choice is effectively "interpreter and this crash" or "no dynamic code at all".No minimal standalone repro from us — field report from a large app, and the description already pins the root cause. Happy to pull further diagnostics off the device if the arm64 path is useful.
fix(reflection): avoid open-instance delegates over virtual accessors…
chore: upgrade Fusion 14.2.45 -> 14.2.50
fix(ios): re-enable the interpreter, with Blazor excluded from R2R
fix(ui): assign VirtualList parameters directly instead of via Blazor
dotnet-policy-service commentedon Jul 31, 2026
Tagging subscribers to 'os-ios': @vitek-karas, @kotlarmilos, @steveisok, @akoeplinger
See info in area-owners.md if you want to be subscribed.
dotnet-policy-service commentedon Jul 31, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @BrzVlad, @janvorli
See info in area-owners.md if you want to be subscribed.
[wasm][R2R] Re-enable open virtual delegate runtime tests (#134765)
Revert "feat(ios): allow dynamic code on iOS, to see how the app beha…