Repository navigation
[JIT] Uninitialized tracked-local bitset size read during early block compaction #134798
Description
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
dotnet-policy-service commentedon Sep 28, 2026
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.
Move test to Regression_ro_2 and rename it to Runtime_134798