dotnet / dotnet/runtime

JIT: (bug) GC hole. If-conversion sinks the explicit init of an untracked GC local past a safepoint, leaving an uninitialized reported GC slot

Open
#133,754 1 comment 1 reaction 2 assignees Claimed by @EgorBo View on GitHub
area-CodeGen-coreclr
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.