Skip to content

Wasm / R2R: struct-returning instance method via field reference asserts in dispatch helper #129347

@AndyAyersMS

Description

@AndyAyersMS

Description

On wasm with R2R, calling a (virtual or non-virtual) instance method that returns a
struct via an intermediate instance method appears to corrupt this (or the field
load) and ends up dispatching on a null/garbage MethodTable.

The original failing test is JIT/opt/OSR/synchronized, which manifests as an
object.cpp:548 pMT->Validate() assert during VirtualDispatchHelpers::ResolveVirtualFunctionPointer:

ASSERT FAILED
        Expression: !CREATE_CHECK_STRING(pMT && pMT->Validate())
        Location:   src/coreclr/vm/object.cpp:548
   0) System.Runtime.CompilerServices.VirtualDispatchHelpers::ResolveVirtualFunctionPointer
   1) System.Runtime.CompilerServices.VirtualDispatchHelpers::VirtualFunctionPointerSlow
   2) System.Runtime.CompilerServices.VirtualDispatchHelpers::VirtualFunctionPointer
   3) System.Environment::CallEntryPoint

A minimized variant produces a clean managed exception that points at the same root
cause (helper sees a null object → MT validation fails):

System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Runtime.CompilerServices.RuntimeHelpers.GetMethodTable(Object obj)
   at X.G()
   at X.TestEntryPoint()
   at __GeneratedMainWrapper.Main()

Minimal Repro

using System.Runtime.CompilerServices;
using Xunit;

struct S { public long y; public int x; }

class Z { public virtual S F() => new S { x = 100, y = -1 }; }

public class X
{
    Z z;

    [MethodImpl(MethodImplOptions.NoInlining)]
    internal S G() => z.F();

    [Fact]
    public static int TestEntryPoint()
    {
        var x = new X();
        x.z = new Z();
        return x.G().x;   // expected 100
    }
}

Bisection of contributing factors

Variants of JIT/opt/OSR/synchronized.cs were run as wasm R2R; the failing
ingredient is the struct-returning instance method called through an intermediate
non-inlined method on a class field
, independent of Synchronized and independent
of looping:

Variant Method G body Z.F return Repros?
A (baseline) Synchronized + for { z.F() } loop struct S yes — pMT->Validate assert
B Drop Synchronized, keep loop struct S yes — same assert
C Sealed Z, direct (non-virtual) call struct S no
D G() => z.F() (no loop) int no
E/H for { z.F() } loop struct S yes — NRE on GetMethodTable(null)
F/G for { sum += z.F() } loop int no
I G() => z.F(); (no loop) struct S yes — same NRE
J G() => z.F(); (sealed Z, non-virtual) struct S yes — same NRE
K Drop G, inline at call site struct S inconclusive — different cctor assert (gchelpers.cpp:573 HasEmptySyncBlockInfo) masks
L Variant I, but z rooted in a static struct S yes — same NRE (so not a GC reclaim of z)

Common ingredient in all repros: an instance method G() on a class with a field of
reference type Z, where G returns a struct produced by calling z.F(). Variant L
confirms the issue is not GC reclaiming z — it persists even when z is rooted by a
static field.

Workaround

DOTNET_ReadyToRunExcludeList=synchronized (or the minimal-repro assembly name) makes
the test pass. Disabling R2R entirely (DOTNET_ReadyToRun=0) also works.

Notes / Suspected scope

  • Probably an R2R codegen issue around struct returns + a hidden retbuf argument
    interacting with the this arg, exposed when z.F() is invoked through an
    intermediate method. The MethodTable that reaches the dispatch helper is null or
    garbage.
  • The assert location matches [wasm][coreclr] !CREATE_CHECK_STRING(pMT && pMT->Validate()) assert failures #121107 but the failing call path is different (that
    issue was about an unhandled PlatformNotSupportedException rethrow path).
  • Possibly an interaction with [Wasm R2R] Bypass dynamic LDVIRTFTN helper #129075 which routed wasm R2R LDVIRTFTN calls through
    CORINFO_HELP_VIRTUAL_FUNC_PTR, but variant J (non-virtual Z) repros without that
    helper being involved — so the issue is broader than the virtual-dispatch helper.

cc @AndyAyersMS @davidwrighton @janvorli

Note

This issue was drafted with assistance from GitHub Copilot.

Metadata

Metadata

Assignees

Labels

arch-wasmWebAssembly architecturearea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions