Skip to content

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

Description

@EgorBo

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

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

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions