JIT: (bug) GC hole. If-conversion sinks the explicit init of an untracked GC local past a safepoint, leaving an uninitialized reported GC slot
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Minimal repro
```csharp
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::ReportStackSlotToGC
CORECLR! TGcInfoDecoder::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.
Contributor guide
Assessment
This issue has not been assessed yet.