Skip to content

[JIT] Uninitialized tracked-local bitset size read during early block compaction #134798

Closed

Description

@jonathandavies-arm

Description

Early flowgraph optimization can compact blocks before the first liveness analysis initializes the tracked-local counts.

Compiler::fgCompactBlock copies the target block's live-out set using VarSetOps::AssignAllowUninitRhs. Although this operation allows an uninitialized source set, it first calls IsShort, which reads the bitset size through BitSetTraits::GetArrSize. This accesses lvaTrackedCountInSizeTUnits before it has been initialized.

Both lvaTrackedCount and lvaTrackedCountInSizeTUnits should have defined initial values before these early operations run.

Reproduction Steps

The following program exercises block compaction after inlining, before early liveness:

using System;
using System.Runtime.CompilerServices;

public class Program
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static int Select(bool condition)
    {
        if (condition)
        {
            return 100;
        }

        return 101;
    }

    [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)]
    private static int Compact(bool condition) => Select(condition);

    public static void Main()
    {
        Console.WriteLine(Compact(true));
        Console.WriteLine(Compact(false));
    }
}

Build the program in Release configuration and run it with a Checked CoreCLR build. To inspect the compilation path, set:

COMPlus_JitDump='Program:Compact' COMPlus_JitDumpASCII=1 \
    <Core_Root>/corerun <path-to-program.dll>

Look for a Compacting message before Starting PHASE Early liveness.

On the unfixed source, add the following diagnostic assertion immediately before VarSetOps::AssignAllowUninitRhs in Compiler::fgCompactBlock, then rebuild the Checked JIT:

assert((GetCurLVEpoch() != 0) || (lvaTrackedCountInSizeTUnits == 0));

This negative control was tested using the equivalent regression test. An ordinary runtime run encounters the assertion earlier during System.AppContext.Setup. An isolated SuperPMI replay of the regression's Compact method also fails at this assertion, demonstrating that the test itself exercises the invalid read independently of startup compilation.

Expected behavior

The program prints:

100
101

The JIT should compile it without reading uninitialized tracked-local counts or bitset sizes. Before the first tracked-local epoch, both counts should be zero.

Actual behavior

On the unfixed source, lvaTrackedCount and lvaTrackedCountInSizeTUnits have no initializers. Early block compaction can reach the bitset-size read before liveness initializes them.

The Checked negative control (initializer removed, new use-site assertion retained) produced the following diagnostic excerpt when replaying the isolated regression method (class name normalized to Program to match the example above):

ISSUE: <ASSERT> #1 src/coreclr/jit/fgopt.cpp (1057) - Assertion failed '(GetCurLVEpoch() != 0) || (lvaTrackedCountInSizeTUnits == 0)' in 'Program:Compact(bool):int' during 'Update flow graph early pass' (IL size 7; FullOpts)

GDB showed the following state at the assertion:

lvaCurEpoch                  = 0
lvaTrackedCountInSizeTUnits  = 0xcdcdcdcd
block->bbLiveOut             = nullptr

0xcdcdcdcd is the Checked allocator's uninitialized-memory pattern. The isolated replay exits 1 with the initializer removed and exits 0 with it present.

This assertion was added to detect the invalid read; it is not an existing assertion in the original unfixed runtime. Without the check, compilation can succeed: for the observed null live-out sets, the stale size changes the native JIT execution path without changing the generated managed code.

Regression?

Both fields have lacked initializers since the initial public CoreCLR commit.

Known Workarounds

None known.

Configuration

  • .NET version: Built from the dotnet/runtime main branch (.NET 12 development), Checked configuration.
  • OS: Ubuntu 24.04.1 LTS.
  • Architecture: x64.
  • Configuration-specific: Confirmed on Linux x64; other configurations have not been tested. The affected code is shared JIT code.
  • Blazor: Not applicable.

Other information

Initializing lvaTrackedCount and lvaTrackedCountInSizeTUnits to zero prevents the uninitialized read. With the initialization in place, the regression test and isolated SuperPMI replay pass.

The assertion shown above was added for diagnosis; it is not present in the original unfixed runtime. No incorrect generated code was observed in the investigated case.

Activity

  1. added
    area-CodeGen-coreclrCLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI
    on Sep 28, 2026
  2. dotnet-policy-service commented on Sep 28, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
    See info in area-owners.md if you want to be subscribed.

  3. added a commit that references this issue on Sep 28, 2026
    350a0f1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions