Repository navigation
Test failure: GC stress assertion (GetComponentSize() <= 2) || IsArray() in methodtable.cpp on x64 (Loader explicit-layout objrefandnonobjrefoverlap/case1) #129546
Description
Activity
- addedarea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
on Jun 17, 2026 - addeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Jun 17, 2026 dotnet-policy-service commented
on Jun 17, 2026 ContributorMore actionsTagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.- addedblocking-clean-ci-optionalBlocking optional rolling runsBlocking optional rolling runsand removeduntriagedNew issue has not been triaged by the area ownerNew issue has not been triaged by the area owner
on Jun 17, 2026 Please check if it's a duplicate to #129546.
Root-cause analysis: this is a VM exception-dispatch / GC-stackwalk bug, not a JIT (codegen) bug
I localized this with a minimal repro + VM instrumentation. The JIT's machine code, GC info and unwind data for the affected method are all correct. The hole is in how the VM's GC stackwalk recovers a callee-saved register holding a GC ref for a managed frame while an exception thrown from a prestub is being dispatched. #129545 and #129546 are the same root cause (different asserts depending on the garbage value).
Minimal repro (x64, reproduces ~100%)
using System; using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; public struct Wrap<T> { public T o; } // Invalid explicit layout: overlaps an objref (Wrap<T>) with an int field. // JIT-compiling Use<T> needs BadGen<T>'s layout, so it throws TypeLoadException from the prestub. [StructLayout(LayoutKind.Explicit)] public struct BadGen<T> where T : class { [FieldOffset(0)] public int i; [FieldOffset(0)] public Wrap<T> o; } public static class Trigger { [MethodImpl(MethodImplOptions.NoInlining)] public static int Use<T>() where T : class { BadGen<T> b; b.i = 0; b.o.o = default; return b.i; } } public static class Program { static volatile bool s_stop; // Distinct reference types so each Use<T> first-call JIT (and its TypeLoad failure) is *fresh*, // which widens the window in which a GC can hit while the exception is dispatching. static readonly Type[] s_types = MakeTypes(); static Type[] MakeTypes() { var list = new List<Type>(); Type t = typeof(string); for (int k = 0; k < 300; k++) { list.Add(t); t = typeof(List<>).MakeGenericType(t); } return list.ToArray(); } static void ThrowWorker() { MethodInfo open = typeof(Trigger).GetMethod("Use"); int i = 0; while (!s_stop) { // The reflection invoke stub boxes the int return value: that box (a GC ref) is held // in a callee-saved register across the call to Use<T>(), which throws from its prestub. try { open.MakeGenericMethod(s_types[i++ % s_types.Length]).Invoke(null, null); } catch (TargetInvocationException) { } } } static void AllocWorker() { var keep = new object[64]; int i = 0; while (!s_stop) keep[i++ % keep.Length] = new byte[(i & 0x3FFF) + 32]; } static int Main() { for (int i = 0; i < Math.Max(2, Environment.ProcessorCount / 2); i++) new Thread(ThrowWorker) { IsBackground = true }.Start(); for (int i = 0; i < 2; i++) new Thread(AllocWorker) { IsBackground = true }.Start(); Thread.Sleep(60_000); s_stop = true; return 100; } }
Run (checked corerun):
set DOTNET_TieredCompilation=0 set DOTNET_GCStress=0xC (0x3 reproduces just as reliably) corerun gchole_minimal.dllIt fires the exact asserts from both issues, e.g. (#129545):
Assert failure: !CREATE_CHECK_STRING(pMT && pMT->Validate()) CORECLR! Object::ValidateInner CORECLR! Object::Validate CORECLR! TGcInfoDecoder<AMD64GcInfoEncoding>::ReportRegisterToGC + 0x196 CORECLR! TGcInfoDecoder<AMD64GcInfoEncoding>::ReportSlotToGC CORECLR! TGcInfoDecoder<AMD64GcInfoEncoding>::EnumerateLiveSlots CORECLR! EECodeManager::EnumGcRefs CORECLR! GcStackCrawlCallBack CORECLR! Thread::StackWalkFramesEx CORECLR! ScanStackRoots File: src/coreclr/vm/object.cpp:548Mechanism
- A caller holds a GC reference in a callee-saved register (
rbxon x64) live across a call. In the repro the GC ref is the box for the boxedintreturn value in the reflection invoke stub; in general it's any GC ref kept live across the call (e.g.object Inner<T>(object box){ Use<T>(); return box; }). - The callee (
Use<T>) can't be JIT-compiled (invalid overlapped layout) and throwsTypeLoadExceptionfrom its prestub.ThePreStubsaved the caller's callee-saved registers (incl. the box) into thePrestubMethodFrame'sTransitionBlock. - While the exception dispatches, the
PrestubMethodFrameis popped. A GC then stackwalks and reports the caller frame, but the caller is scanned as the active frame (CrawlFrame::IsActiveFrame()==1, and noPrestubMethodFrameremains in the thread's Frame chain). Sorbxis read from the live/redirected context, which no longer holds the box → a bogus OBJECTREF →Object::Validateasserts.
VM-side evidence (I instrumented
ReportRegisterToGC→Object::Validate):- offending method = the caller of the prestub (
dynamicClass::InvokeStub_Trigger.Use, or for an equivalent direct reproTrigger.Inner[System.__Canon]),reg=3(rbx). cf->IsActiveFrame()==1; the thread's Frame chain at the failure contains anInlinedCallFrame+ResumableFrame(gcstress redirect) but noPrestubMethodFrame.- the stack slot it reads
rbxfrom is sometimes theTransitionBlockregion and sometimes a far, already-unwound location (slot-spobserved as both-0x38and-41680) — both garbage by scan time.
The JIT output for the caller is correct —
rbxis live at the call site, saved bypush rbx, and the unwind info records it:; Trigger:Inner[System.__Canon](System.Object):System.Object (partially interruptible) ; V01 arg0 ref -> rbx "box" push rbx ... mov rbx, rdx ; rbx = box (GC ref, callee-saved) call [Trigger:Use[System.__Canon]():int] ; throws TypeLoadException from prestub mov rax, rbx ... pop rbx ; GC info: slot rbx Live at the call site (0x1d); call site recorded ; Unwind: UWOP_PUSH_NONVOL rbxWhy it's narrow (and why it is not a codegen bug)
- Throwing the same exception from managed code does not repro: the managed callee's physical frame keeps the caller's callee-saved registers recoverable via normal unwind.
- A call with no GC ref live across it does not repro.
- Only prestub-thrown exception + GC ref in a callee-saved register live across the call triggers it.
Notes
- Reproduces under both
DOTNET_GCStress=0x3(alloc/safepoint stress) and0xC(instruction stress), and with both legacy and new EH (DOTNET_LegacyExceptionHandling=1still crashes). So it is not new-EH-specific, and in principle a rare real GC during such dispatch could hit it outside gcstress. - Not a recent regression — it reproduces unchanged at
7da2acb(2026-05-23) and earlier (I built and tested it); the CI "first failure" date is just the intermittent failure crossing the flakiness threshold. So there is no useful bisect target.
Candidate fix area (for the Exceptions team)
The GC stackwalk needs the faulting managed frame's callee-saved-register context to remain valid while a native-origin (prestub) exception dispatches.
src/coreclr/vm/stackwalk.cpp(~L1382-1383) notes theExInfosubstitutes a frame's context only "if the ExInfo is due to an exception in managed code"; for a prestub (native) throw, once thePrestubMethodFrameis popped the caller frame is scanned asactivewith the live context instead of recovering the registers from thePrestubMethodFrame/TransitionBlock(TransitionFrame::UpdateRegDisplay_Impl) or the saved exception context.Since the JIT output is correct and the defect is in the VM EH/GC-stackwalk path, re-routing from area-CodeGen-coreclr to area-Exceptions-coreclr.
- A caller holds a GC reference in a callee-saved register (
- removedarea-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMICLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
on Jun 18, 2026 dotnet-policy-service commented
on Jun 18, 2026 ContributorMore actionsTagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.- added 6 commits that reference this issue
on Jun 20, 2026 Duplicate of #129682 . Fixed about a week ago.
- locked and limited conversation to collaborators
on Aug 15, 2026
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Summary:
Under gcstress0x3 on x64, the Loader explicit-layout negative test objrefandnonobjrefoverlap/case1 (which expects a
System.TypeLoadExceptionbecauseMyUnion2overlaps an object field with a non-object field) aborts: while that expected exception propagates, a GC stackwalk validates an object whose MethodTable is inconsistent and trips(GetComponentSize() <= 2) || IsArray()atvm/methodtable.cpp:6304(exit code -1073740286 instead of the expected 100).Build Information:
Error Message:
Affected test legs:
Console log: Console Log
Source: runtime-coreclr gcstress0x3-gcstress0xc / coreclr windows x64 Checked gcstress0xc @ Windows.10.Amd64.Open / Loader.0.3
Analysis:
The test case1.dll deliberately loads an explicit-layout struct (
MyUnion2) that overlaps an object field with a non-object field, so aSystem.TypeLoadExceptionis expected. Under GC stress, a stackwalk (Thread::StackWalkFrames->EECodeManager::EnumGcRefs->Object::Validate->MethodTable::Validate->SanityCheck) runs while this type is in a half-loaded/invalid state and asserts(GetComponentSize() <= 2) || IsArray()atsrc/coreclr/vm/methodtable.cpp:6304, aborting the process (exit -1073740286). This is a closely-related sibling of theobject.cpp:548!CREATE_CHECK_STRING(pMT && pMT->Validate())assert seen in the other Loader work items (Loader.1.3/2.3/LoaderClassloaderGenerics) — both are GC object/MethodTable validation tripping on a mid-failed-load type under gcstress — but the verbatim assert site differs.Related issues:
!CREATE_CHECK_STRING(pMT && pMT->Validate())assert at object.cpp:548 on x64 (Loader.1.3/2.3/Generics).Milestone: 11.0.0
Note
This issue was generated with assistance from GitHub Copilot (AI).