Minimal repro
using System;
using System.Runtime.CompilerServices;
public class P
{
[MethodImpl(MethodImplOptions.NoInlining)]
static object Alloc() => new object();
// Fills the stack region that Victim's frame will occupy with bogus pointers.
[MethodImpl(MethodImplOptions.NoInlining)]
static long Dirty(int d, long seed)
{
long a0 = seed, a1 = seed + 1, a2 = seed + 2, a3 = seed + 3;
long a4 = seed + 4, a5 = seed + 5, a6 = seed + 6, a7 = seed + 7;
long a8 = seed + 8, a9 = seed + 9, b0 = seed + 10, b1 = seed + 11;
long b2 = seed + 12, b3 = seed + 13, b4 = seed + 14, b5 = seed + 15;
long s = 0;
if (d > 0) s = Dirty(d - 1, seed + 16);
return s + a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + b0 + b1 + b2 + b3 + b4 + b5;
}
[MethodImpl(MethodImplOptions.NoInlining)]
static string Victim(bool c, string s)
{
string v = "init"; // first store: JIT sets lvHasExplicitInit -> prolog zero-init is skipped
Alloc(); // GC safepoint
if (c) v = s; // if-conversion sinks `v = "init"` down to here and deletes the original
return v;
}
public static void Main()
{
long acc = 0;
for (int i = 0; i < 200; i++)
{
acc += Dirty(6, 0x0000123412341234L + i);
string r = Victim((i & 1) == 0, "xyz");
if (r != "init" && r != "xyz") { Console.WriteLine("BAD " + r); return; }
}
Console.WriteLine("ok " + (acc != 0));
}
}
Run: DOTNET_TieredCompilation=0 DOTNET_GCStress=0x4 DOTNET_JitMaxLocalsToTrack=1 corerun repro.dll
Expected
ok True
Actual
Assert failure: !"Detected use of a corrupted OBJECTREF. Possible GC hole." (vm\object.cpp:618)
CORECLR! Object::Validate
CORECLR! TGcInfoDecoder<AMD64GcInfoEncoding>::ReportStackSlotToGC
CORECLR! TGcInfoDecoder<AMD64GcInfoEncoding>::ReportUntrackedSlots
CORECLR! EECodeManager::EnumGcRefs
Process dies with 0xC0000409. In Victim, V02 loc0 ... ref -> [rsp+0x28] is untracked and on-frame, the prolog does not zero it, and call [P:Alloc()] is emitted before mov gword ptr [rsp+0x28], rax — so the slot is reported live to the GC while still holding stack garbage.
Notes
- Clean with
DOTNET_JitDoIfConversion=0. x64 Checked, .NET 11 main b44cd904110.
JitMaxLocalsToTrack (RELEASE_CONFIG_INTEGER, default 0x400) only forces the GC local untracked; also reproduces at default config with >1024 locals in Victim.
ifconversion.cpp:258-320 IfConvertTryGetElseFromJtrueBlock / optIfConvert's fgRemoveStmt check neither lvHasExplicitInit (optimizer.cpp:5967-5986, codegencommon.cpp:3960-3964) nor intervening GC safepoints.
Minimal repro
Run:
DOTNET_TieredCompilation=0 DOTNET_GCStress=0x4 DOTNET_JitMaxLocalsToTrack=1 corerun repro.dllExpected
ok TrueActual
Process dies with 0xC0000409. In
Victim,V02 loc0 ... ref -> [rsp+0x28]is untracked and on-frame, the prolog does not zero it, andcall [P:Alloc()]is emitted beforemov gword ptr [rsp+0x28], rax— so the slot is reported live to the GC while still holding stack garbage.Notes
DOTNET_JitDoIfConversion=0. x64 Checked, .NET 11 mainb44cd904110.JitMaxLocalsToTrack(RELEASE_CONFIG_INTEGER, default 0x400) only forces the GC local untracked; also reproduces at default config with >1024 locals inVictim.ifconversion.cpp:258-320IfConvertTryGetElseFromJtrueBlock/optIfConvert'sfgRemoveStmtcheck neitherlvHasExplicitInit(optimizer.cpp:5967-5986,codegencommon.cpp:3960-3964) nor intervening GC safepoints.