You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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:
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()
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.
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.
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 fieldload) and ends up dispatching on a null/garbage MethodTable.
The original failing test is
JIT/opt/OSR/synchronized, which manifests as anobject.cpp:548 pMT->Validate()assert duringVirtualDispatchHelpers::ResolveVirtualFunctionPointer:A minimized variant produces a clean managed exception that points at the same root
cause (helper sees a null object → MT validation fails):
Minimal Repro
Bisection of contributing factors
Variants of
JIT/opt/OSR/synchronized.cswere run as wasm R2R; the failingingredient is the struct-returning instance method called through an intermediate
non-inlined method on a class field, independent of
Synchronizedand independentof looping:
Gbodyfor { z.F() }loopSpMT->ValidateassertSynchronized, keep loopSZ, direct (non-virtual) callSG() => z.F()(no loop)intfor { z.F() }loopSGetMethodTable(null)for { sum += z.F() }loopintG() => z.F();(no loop)SG() => z.F();(sealedZ, non-virtual)SG, inline at call siteSgchelpers.cpp:573 HasEmptySyncBlockInfo) maskszrooted in astaticSz)Common ingredient in all repros: an instance method
G()on a class with a field ofreference type
Z, whereGreturns a struct produced by callingz.F(). Variant Lconfirms the issue is not GC reclaiming
z— it persists even whenzis rooted by astatic field.
Workaround
DOTNET_ReadyToRunExcludeList=synchronized(or the minimal-repro assembly name) makesthe test pass. Disabling R2R entirely (
DOTNET_ReadyToRun=0) also works.Notes / Suspected scope
interacting with the
thisarg, exposed whenz.F()is invoked through anintermediate method. The MethodTable that reaches the dispatch helper is null or
garbage.
issue was about an unhandled
PlatformNotSupportedExceptionrethrow path).LDVIRTFTNcalls throughCORINFO_HELP_VIRTUAL_FUNC_PTR, but variant J (non-virtualZ) repros without thathelper 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.