Skip to content

Revert "Inline CORINFO_HELP_ARRADDR_ST helper call, remove WriteBarrier FCall"#126530

Merged
jkotas merged 1 commit intomainfrom
revert-117583-remove-wb-fcall
Apr 4, 2026
Merged

Revert "Inline CORINFO_HELP_ARRADDR_ST helper call, remove WriteBarrier FCall"#126530
jkotas merged 1 commit intomainfrom
revert-117583-remove-wb-fcall

Conversation

@jkotas
Copy link
Copy Markdown
Member

@jkotas jkotas commented Apr 4, 2026

Reverts #117583

Expected to fix #126516

Copilot AI review requested due to automatic review settings April 4, 2026 01:43
@jkotas jkotas requested a review from MichalStrehovsky as a code owner April 4, 2026 01:43
@jkotas jkotas requested review from EgorBo and mangod9 April 4, 2026 01:44
@jkotas jkotas added area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI and removed area-VM-coreclr labels Apr 4, 2026
@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR reverts #117583 by restoring the prior write-barrier and stelem.ref helper call patterns, removing the recent JIT-side inlining/intrinsic handling.

Changes:

  • Reintroduces a callable write-barrier entrypoint (JIT_WriteBarrier_Callable) and wires CastHelpers.WriteBarrier as an FCALL to it.
  • Removes the RuntimeHelpers.WriteBarrier intrinsic path and related JIT intrinsic plumbing (named intrinsic, importer expansion, helper->user-call mapping).
  • Reverts the CORINFO_HELP_ARRADDR_ST “helper-or-user-equivalent” handling back to helper-only detection.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs Removes RuntimeHelpers.WriteBarrier shim and reverts minor formatting.
src/coreclr/vm/wasm/helpers.cpp Adds a WASM stub for JIT_WriteBarrier_Callable (currently asserts).
src/coreclr/vm/riscv64/asmhelpers.S Adds JIT_WriteBarrier_Callable wrapper that jumps via JIT_WriteBarrier_Loc.
src/coreclr/vm/loongarch64/asmhelpers.S Adds JIT_WriteBarrier_Callable wrapper that jumps via JIT_WriteBarrier_Loc.
src/coreclr/vm/jitinterface.h Declares JIT_WriteBarrier_Callable and defines WriteBarrier_Helper alias to it.
src/coreclr/vm/i386/jithelp.S Adds JIT_WriteBarrier_Callable thunk for Unix i386.
src/coreclr/vm/i386/jithelp.asm Adds JIT_WriteBarrier_Callable thunk for Windows x86.
src/coreclr/vm/ecalllist.h Registers CastHelpers.WriteBarrier FCALL and adds CastHelpers class mapping.
src/coreclr/vm/arm64/asmhelpers.S Adds JIT_WriteBarrier_Callable thunk (moves args to x14/x15 then branches).
src/coreclr/vm/arm64/asmhelpers.asm Adds Windows ARM64 JIT_WriteBarrier_Callable thunk implementation.
src/coreclr/vm/arm/asmhelpers.S Adds ARM32 JIT_WriteBarrier_Callable thunk branching via JIT_WriteBarrier_Loc.
src/coreclr/vm/amd64/jithelpers_fast.S Adds JIT_WriteBarrier_Callable thunk for Unix AMD64.
src/coreclr/vm/amd64/JitHelpers_Fast.asm Adds JIT_WriteBarrier_Callable thunk for Windows AMD64.
src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/CastHelpers.cs Introduces CastHelpers.WriteBarrier internalcall and uses it for stelem.ref stores.
src/coreclr/nativeaot/Test.CoreLib/src/System/Runtime/RuntimeHelpers.cs Removes NativeAOT test-corelib RuntimeHelpers.WriteBarrier shim.
src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/TypeCast.cs Switches array ref stores to InternalCalls.RhpAssignRef instead of removed shim.
src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/InternalCalls.cs Adds RhpAssignRef runtime import/internalcall declaration.
src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/CompilerServices/RuntimeHelpers.cs Removes NativeAOT RuntimeHelpers.WriteBarrier shim.
src/coreclr/jit/namedintrinsiclist.h Removes RuntimeHelpers.WriteBarrier named intrinsic.
src/coreclr/jit/morph.cpp Reverts ARRADDR_ST morph condition to helper-only.
src/coreclr/jit/importercalls.cpp Removes intrinsic expansion and helper->user-call conversion plumbing.
src/coreclr/jit/importer.cpp Reverts stelem.ref import to emit helper call directly (no user-call conversion).
src/coreclr/jit/gentree.h Removes IsHelperCallOrUserEquivalent declaration.
src/coreclr/jit/gentree.cpp Removes IsHelperCallOrUserEquivalent implementation and related debug display.
src/coreclr/jit/compiler.h Removes helper-to-managed map and impConvertToUserCallAndMarkForInlining.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 4, 2026

🤖 Copilot Code Review — PR #126530

Note

This review was generated by Copilot.

Holistic Assessment

Motivation: This reverts PR #117583 which inlined the CORINFO_HELP_ARRADDR_ST helper call and removed the WriteBarrier FCall in favor of a JIT intrinsic. The revert description doesn't detail the specific issue, but reverting a complex JIT optimization that added significant infrastructure (HelperToManagedMap, IsHelperCallOrUserEquivalent, impConvertToUserCallAndMarkForInlining) is a reasonable conservative action when problems are discovered.

Approach: The revert correctly restores the FCall-based WriteBarrier in CastHelpers (as [MethodImpl(MethodImplOptions.InternalCall)]), removes all JIT intrinsic infrastructure, and adds JIT_WriteBarrier_Callable assembly implementations for every architecture. The NativeAOT portion is adapted (rather than purely reverted) to use InternalCalls.RhpAssignRef — which is the correct approach since the removed RuntimeHelpers.WriteBarrier intrinsic cannot simply be restored without re-adding the intrinsic expansion.

Summary: ✅ LGTM. This is a clean revert of a complex JIT optimization by a senior maintainer. The CoreCLR changes are a mechanical revert, the NativeAOT adaptation is correct, all architectures are covered, and there are no public API surface changes. Only minor cosmetic observations noted below.


Detailed Findings

✅ Correctness — CoreCLR FCall restoration is correct

The WriteBarrier method is correctly re-added to CastHelpers as a private [MethodImpl(MethodImplOptions.InternalCall)] extern, registered in ecalllist.h via FCFuncElement("WriteBarrier", ::WriteBarrier_Helper) where WriteBarrier_Helper is #defined to JIT_WriteBarrier_Callable. All three call sites in StelemRef, StelemRef_Helper, and StelemRef_Helper_NoCacheLookup correctly call the local WriteBarrier() instead of the removed RuntimeHelpers.WriteBarrier().

✅ Correctness — JIT infrastructure removal is clean

All the added JIT infrastructure from #117583 is fully removed:

  • HelperToManagedMap typedef and methods (GetHelperToManagedMap, HelperToManagedMapLookup) from compiler.h
  • IsHelperCallOrUserEquivalent from gentree.cpp/gentree.h
  • impConvertToUserCallAndMarkForInlining from importercalls.cpp
  • NI_System_Runtime_CompilerServices_RuntimeHelpers_WriteBarrier from namedintrinsiclist.h and all switch cases
  • The morph.cpp optimization correctly reverts to using IsHelperCall() instead of IsHelperCallOrUserEquivalent()

✅ Correctness — NativeAOT adaptation is appropriate

The NativeAOT code is not a pure revert — it switches from RuntimeHelpers.WriteBarrier (which relied on the now-removed intrinsic) to InternalCalls.RhpAssignRef (a direct native write barrier call). This is the correct adaptation: RhpAssignRef is the canonical NativeAOT write barrier entry point (imported from RuntimeLibrary via [RuntimeImport]), and using it directly avoids any dependency on the removed intrinsic.

✅ Platform coverage — All architectures implemented

JIT_WriteBarrier_Callable is implemented for all supported architectures:

  • x64: Jumps through JIT_WriteBarrier_Loc (both MASM and GNU syntax)
  • x86: Adapts calling convention (eax/edx swap) then jumps (both MASM and GNU syntax)
  • ARM: PC-relative load of JIT_WriteBarrier_Loc with Clang/GCC handling
  • ARM64: Moves args to x14/x15 (special ABI registers) then jumps (both MASM and GNU syntax)
  • LoongArch64: Moves args to $t6/$t7 then jumps
  • RISC-V64: Moves args to t3/t4 then jumps
  • WASM: PORTABILITY_ASSERT stub (consistent with other WASM write barrier stubs)

💡 Minor — Whitespace change in shared RuntimeHelpers.cs

In src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs, the revert changes where T : allows ref struct to where T: allows ref struct (removes space before colon). This appears to be restoring the pre-#117583 formatting. Standard C# style includes the space (where T :), but per the "match existing style in modified files" convention, this is fine as a revert artifact.

💡 Minor — Doubled comment in ARM64 assembly

In src/coreclr/vm/arm64/asmhelpers.S, the comment reads:

// ------------------------// ------------------------------------------------------------------

This appears to be a concatenation of two comment prefixes. Cosmetic only, no functional impact.

Generated by Code Review for issue #126530 ·

Copy link
Copy Markdown
Member

@mangod9 mangod9 left a comment

Choose a reason for hiding this comment

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

All dumps show the same stack, so reverting this is reasonable. Will let the analysis run a little longer to determine if it can find the actual root cause within the PR:

Key Finding: NOT ARM64-specific — affects ALL architectures

All 5 newly analyzed crash reports show the exact same stack:

 [SIGSEGV] → IsInstanceOfAny_NoCacheLookup
          → CastHelpers.StelemRef_Helper_NoCacheLookup
          → CastHelpers.StelemRef_Helper
          → SZArrayHelper.set_Item<__Canon>          ← StelemRef INLINED here
          → IList_Generic_Tests.IList_Generic_ItemSet_LastItemToNonDefaultValue
          → (xunit reflection invoke)

@jkotas
Copy link
Copy Markdown
Member Author

jkotas commented Apr 4, 2026

/ba-g build analysis stuck

@jkotas jkotas merged commit ab2f538 into main Apr 4, 2026
204 of 209 checks passed
@jkotas jkotas deleted the revert-117583-remove-wb-fcall branch April 4, 2026 05:08
@mangod9
Copy link
Copy Markdown
Member

mangod9 commented Apr 4, 2026

more detailed analysis:

Investigation Summary: dotnet/runtime#126516

Failure Pattern

8 failing builds across 3 platforms (ARM64 Linux, x64 Linux, macOS x64) all show the
identical crash:

┌────────────────────┬────────────────┐
│ Platform           │ Build IDs      │
├────────────────────┼────────────────┤
│ ARM64 Ubuntu 22.04 │ b1, b2         │
├────────────────────┼────────────────┤
│ x64 Ubuntu 22.04   │ b3, b4, b5, b6 │
├────────────────────┼────────────────┤
│ macOS x64          │ b7, b8         │
└────────────────────┴────────────────┘

All failures occur in System.Runtime.Tests → ArrayTests.CastAs_IListOfT().

Crash Stack (from crash report JSON + dotnet-dump)

 SIGSEGV / Access Violation

 System.Runtime.CompilerServices.CastHelpers.StelemRef_Helper(ref, long, object)
 System.Runtime.CompilerServices.CastHelpers.StelemRef(ref, long, object)  ← INLINED, absent
from stack
 SZArrayHelper.set_Item(int, T)
 System.Tests.ArrayTests+<>c.<CastAs_IListOfT>b__197_1(IList<string>)
 ...

The StelemRef frame is absent from the native stack because PR #117583 inlined it into
SZArrayHelper.set_Item. The crash occurs inside StelemRef_Helper which was called because the
element type check failed with a NULL elementType.

Dump Analysis (x86_64 — build b3)

Faulting instruction in SZArrayHelper.set_Item[String] (Tier1 OptimizedTier1):

 ; At RIP = 0x7e97bde7b43c inside SZArrayHelper.set_Item:
 mov  rsi, qword ptr [rip - 0x6cfc80b]   ; loads from absolute address 0x7e97b717ec38

This loads elementType from a hardcoded constant address 0x7e97b717ec38.

Memory verification:

 ; The constant address is SZArrayHelper.MethodTable + 0x38 (ElementType offset in Checked
builds)
 dp 0x7e97b717ec38 → 0x0000000000000000   ← NULL! (SZArrayHelper is not an array, has no
ElementType)

 ; The actual array being stored to is String[] with a DIFFERENT MethodTable:
 dumpmt <String[].MT>  → ElementType at +0x38 = valid String MethodTable pointer

Dump Analysis (ARM64 — build b1)

Identical pattern confirmed via ARM64 dump (analyzed using dotnet-dump + Cross-DAC ARM64):

 ; Builds constant 0xf796beedec38 = SZArrayHelper.MT + 0x38, then loads:
 mov  x14, #0xec38
 movk x14, #0xbeed, lsl #16
 movk x14, #0xf796, lsl #32
 ldr  x1, [x14]          ; loads elementType = 0x0 (NULL)

Memory verification:

 dp 0xf796beedec38 → 0x0000000000000000   ← NULL (same bug, same pattern)

Regression Source

PR #117583 ("Inline CORINFO_HELP_ARRADDR_ST helper call, remove WriteBarrier FCall") by
@EgorBo — merged 2026-04-02T20:23:28Z, ~24 hours before all failures appeared. 

This PR converted stelem.ref from a JIT helper call to an inlined managed call to
CastHelpers.StelemRef(). The inlining exposed a pre-existing JIT type-tracking bug that was
dormant when StelemRef was compiled as a standalone helper.

Root Cause: JIT Constant-Folds GetMethodTable(array) Using Wrong MethodTable

The bug is in how the JIT's Unsafe.As<T> intrinsic interacts with VN-based constant folding.
Here is the exact call chain:

Step 1 — this gets "exact type" SZArrayHelper (lclvars.cpp:2687-2694)

When SZArrayHelper.set_Item<T> is compiled at Tier1, the this parameter has declared type
SZArrayHelper. In lvaSetClass():

 if (clsHnd != NO_CLASS_HANDLE && !isExact && JitConfig.JitEnableExactDevirtualization())
 {
     CORINFO_CLASS_HANDLE exactClass;
     if (info.compCompHnd->getExactClasses(clsHnd, 1, &exactClass) == 1)
     {
         isExact = true;   // ← SZArrayHelper is sealed, so this fires
         clsHnd  = exactClass;
     }
 }

SZArrayHelper is sealed with no subclasses, so getExactClasses returns 1 → this is marked
lvClassIsExact = true. But SZArrayHelper is a phantom type — the CLR redirects IList<T>
interface calls on arrays to SZArrayHelper methods, so at runtime this is always an array
like String[], never actually SZArrayHelper.

Step 2 — Unsafe.As<T[]>(this) fails to update the type (importercalls.cpp:5220-5227)

The method body does Unsafe.As<T[]>(this) to reinterpret this as an array. The JIT handles
this intrinsic:

 CORINFO_CLASS_HANDLE oldClass = gtGetClassHandle(op, &isExact, &isNonNull);
 // oldClass = SZArrayHelper, inst = T[]
 if ((oldClass != NO_CLASS_HANDLE) &&
     ((oldClass == inst) || !info.compCompHnd->isMoreSpecificType(oldClass, inst)))
 {
     JITDUMP("Unsafe.As: Keep using old '%s' type\n", eeGetClassName(oldClass));
     return op;  // ← BUG: Returns original node, type stays SZArrayHelper!
 }

isMoreSpecificType(SZArrayHelper, T[]) asks "is T[] more specific than SZArrayHelper?" The VM
calls MergeTypeHandlesToCommonParent(SZArrayHelper, T[]) → merged = System.Object (unrelated
types) → merged != SZArrayHelper → returns false. So !false = true → condition is true →
keeps the old SZArrayHelper type instead of creating a temp with type T[].

Step 3 — StelemRef is inlined, GetMethodTable(array) creates invariant IND (
compiler.hpp:1884-1890)

PR #117583 converts stelem.ref to a call to CastHelpers.StelemRef() which gets inlined.
Inside the inlined body, RuntimeHelpers.GetMethodTable(array) expands to:

 GenTreeIndir* result = gtNewIndir(TYP_I_IMPL, object, GTF_IND_INVARIANT | GTF_IND_NONNULL);

This creates IND(array) flagged as invariant — telling the JIT "the MethodTable of an object
never changes."

Step 4 — VN constant-folds using the wrong MethodTable (valuenum.cpp:12588-12614)

During value numbering, the JIT sees IND(array, GTF_IND_INVARIANT) and tries to resolve it:

 if (tree->gtFlags & GTF_IND_INVARIANT)
 {
     if ((oper == GT_IND) && addr->TypeIs(TYP_REF) && tree->TypeIs(TYP_I_IMPL))
     {
         CORINFO_CLASS_HANDLE handle = gtGetClassHandle(addr, &isExact, &isNonNull);
         if (isExact && (handle != NO_CLASS_HANDLE))   // isExact=true from Step 1!
         {
             void* embedClsHnd = (void*)info.compCompHnd->embedClassHandle(handle,
&pEmbedClsHnd);
             ValueNum handleVN = vnStore->VNForHandle((ssize_t)embedClsHnd,
GTF_ICON_CLASS_HDL);
             tree->gtVNPair = vnStore->VNPWithExc(ValueNumPair(handleVN, handleVN),
addrXvnp);
             // ← Assigns SZArrayHelper.MethodTable as a CONSTANT value number!
         }
     }
 }

gtGetClassHandle(array) returns SZArrayHelper with isExact=true (propagated from Step 1
through Step 2). The VN engine embeds SZArrayHelper.MethodTable as a compile-time constant.

Step 5 — Assertion propagation replaces IND with constant (assertionprop.cpp:2719+)

optVNBasedFoldConstExpr sees the constant VN and replaces the IND node with
gtNewIconHandleNode(SZArrayHelper.MethodTable).

Step 6 — Runtime crash

The JIT-generated code loads ElementType from SZArrayHelper.MethodTable + 0x38:

 - SZArrayHelper.MT + 0x38 = 0x0 (NULL) — SZArrayHelper is not an array, it has no element
type
 - The code compares elementType against the value being stored, comparison fails (NULL ≠
anything)
 - Falls through to StelemRef_Helper(ref, NULL_elementType, obj) → dereferences NULL →
SIGSEGV

Proposed Fix

Location: importercalls.cpp line 5223 — change the Unsafe.As<T> type-keeping condition from:

 ((oldClass == inst) || !info.compCompHnd->isMoreSpecificType(oldClass, inst))

to:

 ((oldClass == inst) || info.compCompHnd->isMoreSpecificType(inst, oldClass))

Why this works: The current code asks "is inst NOT more specific than oldClass?" — which is
true for unrelated types (like SZArrayHelper vs T[]), incorrectly keeping the stale type. The
fix asks "IS oldClass more specific than inst?" — which is false for unrelated types, so it
correctly falls through to create a new temp with the target type T[].

This preserves the existing optimization for the normal case: Unsafe.As<Base>(derivedObj) →
isMoreSpecificType(Base, Derived) = true → keeps the more precise Derived type. ✓

Note: The corinfo.h documentation for isMoreSpecificType explicitly states: "This is a hint
to the jit for optimization; it does not have correctness implications." The current code at
line 5223 violates this contract by using the hint for a correctness-critical decision.

This is a pre-existing JIT bug exposed by PR #117583's inlining change. Before that PR,
StelemRef was compiled as a standalone helper with no caller type context, so the VN
constant-fold path was never reached. Once StelemRef was inlined into SZArrayHelper.set_Item,
the caller's (incorrect) type info for this propagated into the inlined body and triggered
the constant fold.

@EgorBo, please review the analysis and proposed fix for accuracy and possibly reapply your PR.

@EgorBo
Copy link
Copy Markdown
Member

EgorBo commented Apr 4, 2026

@EgorBo, please review the analysis and proposed fix for accuracy and possibly reapply your PR.

Unfortunately, it doesn't make a lot of sense to me, e.g. it mentions getExactClasses that always returns false on CoreCLR (not implemented)

EgorBo added a commit to EgorBo/runtime-1 that referenced this pull request Apr 4, 2026
@mangod9
Copy link
Copy Markdown
Member

mangod9 commented Apr 4, 2026

Unfortunately, it doesn't make a lot of sense to me, e.g. it mentions getExactClasses that always returns false on CoreCLR (not implemented)

Ok will check why it came to that conclusion. Is the issue with inlining understood though which causes it to deref the wrong MT?

@EgorBo
Copy link
Copy Markdown
Member

EgorBo commented Apr 5, 2026

Unfortunately, it doesn't make a lot of sense to me, e.g. it mentions getExactClasses that always returns false on CoreCLR (not implemented)

Ok will check why it came to that conclusion. Is the issue with inlining understood though which causes it to deref the wrong MT?

Hard to tell honestly 😞. I assume there is a chance it halucinated the LLDB dump or it's 100% real?
From my understanding, it blames the pre-existing code that intrinsifies Unsafe.As in JIT.
I currently suspect a different thing, but I need to be able to repro it

@mangod9
Copy link
Copy Markdown
Member

mangod9 commented Apr 5, 2026

Hard to tell honestly 😞. I assume there is a chance it halucinated the LLDB dump or it's 100% real?

Dumps should be real, since it had access to many and overall dump analysis looks quite genuine. I let it run little longer and used two different models and both are standing by its analysis. Its changed its theory for how isExact gets set in step 2 below. 🤷

Root Cause: JIT Constant-Folds GetMethodTable(array) Using Wrong MethodTable

The bug is a pre-existing JIT type-tracking issue involving Unsafe.As<T> on a phantom
type (SZArrayHelper), exposed by inlining StelemRef which brought
GetMethodTable(array)->ElementType into scope of VN-based constant folding.

Call chain (with source locations)

Step 1 — this gets type SZArrayHelper (lclvars.cpp:486)

lvaInitThisPtr calls lvaSetClass(this, info.compClassHnd) where info.compClassHnd =
SZArrayHelper. On CoreCLR, getExactClasses returns -1, so lvClassIsExact stays false.

Step 2 — Unsafe.As<__Canon[]>(this) fails to update the type (
importercalls.cpp:5220-5226)

 CORINFO_CLASS_HANDLE oldClass = gtGetClassHandle(op, &isExact, &isNonNull);
 if ((oldClass != NO_CLASS_HANDLE) &&
     ((oldClass == inst) || !info.compCompHnd->isMoreSpecificType(oldClass, inst)))
 {
     return op;  // ← BUG: keeps SZArrayHelper type!
 }

oldClass = SZArrayHelper, inst = __Canon[]. These are unrelated types.
isMoreSpecificType(SZArrayHelper, __Canon[]) asks "is __Canon[] a subtype of
SZArrayHelper?" → calls MergeTypeHandlesToCommonParent → merged = System.Object (neither
is parent) → merged != SZArrayHelper → returns false. So !false = true → the condition
is true → returns the original node with type SZArrayHelper still attached.

Note: inside gtGetClassHandle, at gentree.cpp:19414-19424, the type is also promoted to
exact:

 if (!*pIsExact && JitConfig.JitEnableExactDevirtualization())
 {
     if (getExactClasses(objClass, 1, &exactClass) == 1) { ... } // returns -1 on 
CoreCLR
     else
     {
         *pIsExact = info.compCompHnd->isExactType(objClass); // SZArrayHelper is sealed
 → TRUE
     }
 }

isExactType(SZArrayHelper) returns true because SZArrayHelper is sealed (pMT->IsSealed()
at jitinterface.cpp:4463). But SZArrayHelper is a phantom type — the CLR redirects
IList<T> interface calls on arrays to SZArrayHelper methods, so this is always an array
at runtime, never a real SZArrayHelper instance.

Step 3 — PR #117583 inlines StelemRef (importercalls.cpp, importer.cpp:7268)

stelem.ref → CORINFO_HELP_ARRADDR_ST → impConvertToUserCallAndMarkForInlining() converts
the helper to a managed call to CastHelpers.StelemRef(array, index, value) and marks it
for inlining. The array argument traces back to the local that still has type
SZArrayHelper.

Step 4 — GetMethodTable(array) creates invariant IND (compiler.hpp:1884-1890)

Inside the inlined StelemRef, RuntimeHelpers.GetMethodTable(array) expands to:

 GenTreeIndir* result = gtNewIndir(TYP_I_IMPL, object, GTF_IND_INVARIANT |
GTF_IND_NONNULL);

GTF_IND_INVARIANT tells the JIT "the MethodTable of an object never changes."

Step 5 — VN constant-folds using the wrong MethodTable (valuenum.cpp:12588-12614)

 if (tree->gtFlags & GTF_IND_INVARIANT)
 {
     if ((oper == GT_IND) && addr->TypeIs(TYP_REF) && tree->TypeIs(TYP_I_IMPL))
     {
         CORINFO_CLASS_HANDLE handle = gtGetClassHandle(addr, &isExact, &isNonNull);
         if (isExact && (handle != NO_CLASS_HANDLE))    // isExact=true (from 
isExactType!)
         {
             if (!eeIsSharedInst(handle))                // SZArrayHelper is not shared 
→ passes
             {
                 void* embedClsHnd = (void*)info.compCompHnd->embedClassHandle(handle,
&pEmbedClsHnd);
                 ValueNum handleVN = vnStore->VNForHandle((ssize_t)embedClsHnd,
GTF_ICON_CLASS_HDL);
                 tree->gtVNPair = vnStore->VNPWithExc(ValueNumPair(handleVN, handleVN),
addrXvnp);
                 // ← Assigns SZArrayHelper.MethodTable as a CONSTANT value number
             }
         }
     }
 }

gtGetClassHandle(addr) returns SZArrayHelper with isExact=true (from isExactType at Step
2). eeIsSharedInst(SZArrayHelper) = false. The VN engine embeds
SZArrayHelper.MethodTable as a compile-time constant.

Step 6 — Assertion propagation replaces IND with constant (assertionprop.cpp:2719+)

optVNBasedFoldConstExpr sees the constant VN and replaces the IND node with a constant
handle node pointing to SZArrayHelper.MethodTable.

Step 7 — Runtime crash

The JIT-generated code loads ElementType from SZArrayHelper.MethodTable + 0x38 → NULL.
Comparison with obj->MT fails. Falls to StelemRef_Helper(ref element, NULL, obj) →
IsInstanceOfAny_NoCacheLookup(NULL, obj) → dereferences NULL → SIGSEGV.

Two Contributing Bugs

(A) Primary — importercalls.cpp:5222-5226 (Unsafe.As intrinsic)

The condition !isMoreSpecificType(oldClass, inst) is true for unrelated types (like
SZArrayHelper vs __Canon[]), causing the JIT to keep the stale SZArrayHelper type
instead of creating a new temp with type __Canon[]. The corinfo.h documentation for
isMoreSpecificType explicitly states: "This is a hint to the jit for optimization; it
does not have correctness implications" — but this code uses it for a
correctness-critical decision.

Possible fix: Change the condition to only keep the old type when it's provably a
subtype:

 // Before (buggy): keeps old type for unrelated types
 ((oldClass == inst) || !info.compCompHnd->isMoreSpecificType(oldClass, inst))

 // After (fixed): only keeps old type when it's MORE specific than target
 ((oldClass == inst) || info.compCompHnd->isMoreSpecificType(inst, oldClass))

(B) Secondary — gentree.cpp:19424 (gtGetClassHandle exactness promotion)

isExactType(SZArrayHelper) returns true because SZArrayHelper is sealed. But
SZArrayHelper is a phantom type — no runtime object ever has its MethodTable. Marking it
exact enables VN constant-folding of GetMethodTable() loads, which produces wrong code.

Possible defense-in-depth: The VM could exclude phantom types like SZArrayHelper from
isExactType, or the JIT could avoid constant-folding MethodTable loads when the class
has CORINFO_FLG_ARRAY == 0 for a type that is the declaring class of an array interface
method.

@EgorBo
Copy link
Copy Markdown
Member

EgorBo commented Apr 5, 2026

@mangod9 thanks, I'll give it a try once CI back in shape

radekdoulik pushed a commit to radekdoulik/runtime that referenced this pull request Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

System.Runtime.Tests SIGSEGV crash on ARM64 Linux (azurelinux.3.arm64)

4 participants