Repository navigation
Fix 32-bit PGO schema snapshot alignment - #135515
JulieLeeMSFT wants to merge 3 commits into
Conversation
…Instance allocPgoInstrumentationBySchemaInstance lays the instrumentation schema out with offsets relative to the start of the header's data region, starting the first entry at countsOffset. getPgoInstrumentationResultsInstance read that schema back with an initial offset of 0 and copied the counters starting at GetData() + countsOffset, rebasing every offset by countsOffset. That rebasing is only valid when countsOffset is aligned to the largest alignment an entry can request, which is 8. countsOffset is only aligned to sizeof(size_t), so on 64-bit it is always a multiple of 8 and the rebasing is exact, but on 32-bit it is frequently 4 mod 8. When that happens an 8-byte aligned entry - in practice ValueHistogram, which is recorded for every inlined SpanHelpers.Memmove and SpanHelpers.SequenceEqual call and is EightByte even on 32-bit targets - lands 4 bytes away from where it was written, shifting it and every entry after it. The computed copy size also grows 4 bytes past the end of the loader heap allocation, which faults when the allocation happens to end on a commit boundary. Read the schema back using the same initial offset it was written with and copy the whole data region from GetData(), so that reader and writer share one offset space. This matches the three other readers of this data (LogInstrumentationData, WritePgoData and getPgoInstrumentationResultsFromText), none of which rebase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Share the VM snapshot implementation with a native regression test that verifies aligned and four-byte-misaligned schema prefixes using exact histogram and counter values. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 61ee4221-2b7a-4ab0-8d9c-fc23d737aed0
|
Azure Pipelines: Successfully started running 4 pipeline(s). 12 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch |
Parameterize the native regression by the number of block counters in front of the value histogram, so both original misplacements are covered: the histogram read four bytes early, and four bytes late, which read past the end of the data. On Windows the data now ends at an inaccessible page and a fault is reported with its address, so the over-read fails as a deterministic access violation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 61ee4221-2b7a-4ab0-8d9c-fc23d737aed0
|
@EgorBo PTAL. cc @dotnet/jit-contrib |
There was a problem hiding this comment.
🟢 Approval recommended
The fix preserves allocator layout semantics and is covered by deterministic cross-alignment tests.
0 open findings
What changed in this PR
Fixes 32-bit live-PGO snapshot alignment while preserving schema-relative instrumentation offsets.
Changes:
- Copies snapshots from the original data-region base.
- Adds deterministic native alignment regression coverage.
- Retains an end-to-end managed PGO smoke test.
| File | Description |
|---|---|
src/coreclr/vm/pgo.cpp |
Uses the shared snapshot helper. |
src/coreclr/inc/pgo_formatprocessing.h |
Implements offset-preserving snapshot logic. |
src/tests/JIT/opt/PGO/Runtime_134695.cs |
Adds managed and native regression tests. |
src/tests/JIT/opt/PGO/PgoSchemaNative.cpp |
Validates aligned and misaligned schemas. |
src/tests/JIT/opt/PGO/CMakeLists.txt |
Builds the native test library. |
src/tests/JIT/opt/PGO/Runtime_134695.csproj |
Configures native assets and PGO settings. |
🧠 Review effort: Balanced
|
I went through the allocator and the reader and I agree with the analysis. To check the test independently of a Windows x86 build, I compiled In a sweep of 200k random schemas using the allocator's own A read-only merge check against main at dcc404c found no conflicts. In the PGO files this PR touches, the only change on main since the PR's base is the removal of the unused Two small observations, neither blocking:
The change looks right to me. |
|
@JulieLeeMSFT, is anything holding up a review of this PR? If you would like a particular JIT reviewer, could you request one? The crash it fixes, #134695, is hitting CI more often. At 18:08 UTC on 2026-10-22, its Build Analysis report showed 8 hits in the last 24 hours, 45 in 7 days and 145 in a month. On 2026-10-19 the 7-day figure was 41. One of the latest hits was on my PR #136214. I went through the fix on 2026-10-18 and agree with the analysis. |
|
I'm thinking of fixing it differently. This change fixes the symptom, not the cause. |
|
@pcshrosbree, thanks for taking a look. EgorBo will work on a proper fix. Closing this PR. |
|
Thanks Julie. Whatever makes CI more reliable. |
Fix live PGO schema offset rebasing on 32-bit targets. Fixes #134695
The allocator lays out instrumentation entries relative to
Header::GetData(), after a compressed schema prefix that is aligned only tosizeof(size_t). Reconstructing the layout from offset zero and copying from the counter region changes the padding for 8-byte-aligned entries whencountsOffset % 8 == 4. This shifts histogram reads and subsequent entries and can copy past the end of the instrumentation allocation. On 64-bit targetscountsOffsetis always a multiple of 8, so only 32-bit targets are affected.Preserve the allocator's data-relative offsets and copy from the original data-region base. Extract the snapshot logic into a shared helper used by both the VM and a native regression test, so the test exercises the production algorithm rather than a duplicate implementation.
Tests
SnapshotPreservesHistogramValuesruns the shared helper over a schema withcountsOffset % 8equal to 0 or 4 and with one or two block counters in front of a value histogram, and compares every entry's bytes. On 32-bit targets the two counter counts misplace the original layout in opposite directions: with one it read the histogram four bytes early, with two it read four bytes past the end of the data. On Windows the data is placed directly before an inaccessible page, so that over-read is a deterministic access violation, reported with the faulting address, instead of depending on heap layout. The existing managed PGO test is unchanged.Validation
All commands below were run locally on Windows.
.\build.cmd clr.native -a x86 -c Checked.\build.cmd clr.native -a x64 -c Checked.\src\tests\build.cmd x86 Checked -Test JIT\opt\PGO\Runtime_134695.csproj.\src\tests\build.cmd x64 Checked -Test JIT\opt\PGO\Runtime_134695.csprojcorerun.exeon the x86 CheckedRuntime_134695.dllcorerun.exeon the x64 CheckedRuntime_134695.dllThe test runs used the environment settings specified by the test project:
DOTNET_TieredCompilation=1,DOTNET_TieredPGO=1,DOTNET_TC_CallCountingDelayMs=0andDOTNET_TieredPGO_InstrumentedTierAlwaysOptimized=1.With the original layout temporarily restored in the shared helper, each row run on its own:
countsOffset % 8, leading countersIn both access violations the faulting address equals the end of the data. The test project's standalone runner reports only the first failing row of a theory.
The native test also compiles without warnings of its own with g++ 13 and clang++ on x64 Linux (
-Wall -Wextra) and passes all four rows there. With the original layout, both misaligned rows fail with shifted data, since that path has no guard page. 32-bit Linux, ARM, macOS and the repo's CMake path on Unix were not run.Reproducing the runtime crash
The crash needs a PGO block to end exactly at the end of committed loader-heap memory, which is rare. To make it deterministic I temporarily (not part of this PR) allocated every PGO block directly before an inaccessible page and ran an 8-second tiering stress workload (strings, spans, LINQ, regex) with
DOTNET_TieredPGO=1andDOTNET_TC_CallCountingDelayMs=0.On x86 Release, the configuration of the report, the original algorithm killed the process with exit code
0xC0000005(-1073741819, the exit code in the CI failures) right after it logged the 76th live-PGO snapshot,EqualsHelper, a block that algorithm reads four bytes past the end of. With this change the same run consumed 667 live-PGO snapshots and exited normally. In that run 476 snapshots hadcountsOffset % 8 == 4; the original algorithm would have misplaced entries in 28 of them and read past the block in 18 (EqualsHelper,InternalSubString,CopyStringContent,Append).On x86 Checked the fatal-error report names the faulting stack:
PgoManager::getPgoInstrumentationResultsInstance, called fromPgoManager::getPgoInstrumentationResults,CEECodeGenInfo::getPgoInstrumentationResultsandTieredCompilationManager::OptimizeMethod. There the fixed run consumed 1841 snapshots without a fault (1426 withcountsOffset % 8 == 4; the original algorithm would have misplaced 39 and read past the block in 26).In the real loader heap such a read faults only when a block ends exactly at the end of committed memory, which would explain why the CI failures are intermittent and x86-only.
Not done: reproducing the crash without the page-heap placement, analyzing a CI crash dump, and symbolizing the Release crash stack.
Resolves #134695
Note
This PR description and the regression-test changes were generated with GitHub Copilot.