Skip to content

Fix 32-bit PGO schema snapshot alignment - #135515

Closed
JulieLeeMSFT wants to merge 3 commits into
dotnet:mainfrom
JulieLeeMSFT:fix/134695-pgo-schema-offset
Closed

JulieLeeMSFT wants to merge 3 commits into
dotnet:mainfrom
JulieLeeMSFT:fix/134695-pgo-schema-offset

Conversation

@JulieLeeMSFT

@JulieLeeMSFT JulieLeeMSFT commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

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 to sizeof(size_t). Reconstructing the layout from offset zero and copying from the counter region changes the padding for 8-byte-aligned entries when countsOffset % 8 == 4. This shifts histogram reads and subsequent entries and can copy past the end of the instrumentation allocation. On 64-bit targets countsOffset is 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

SnapshotPreservesHistogramValues runs the shared helper over a schema with countsOffset % 8 equal 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.

Command Result
.\build.cmd clr.native -a x86 -c Checked Passed, zero warnings and errors.
.\build.cmd clr.native -a x64 -c Checked Passed, zero warnings and errors.
.\src\tests\build.cmd x86 Checked -Test JIT\opt\PGO\Runtime_134695.csproj Passed, zero warnings and errors.
.\src\tests\build.cmd x64 Checked -Test JIT\opt\PGO\Runtime_134695.csproj Passed, zero warnings and errors.
corerun.exe on the x86 Checked Runtime_134695.dll Passed, exit code 100; all four deterministic rows ran, along with the retained managed test.
corerun.exe on the x64 Checked Runtime_134695.dll Passed, exit code 100; all four deterministic rows ran, along with the retained managed test.

The test runs used the environment settings specified by the test project: DOTNET_TieredCompilation=1, DOTNET_TieredPGO=1, DOTNET_TC_CallCountingDelayMs=0 and DOTNET_TieredPGO_InstrumentedTierAlwaysOptimized=1.

With the original layout temporarily restored in the shared helper, each row run on its own:

countsOffset % 8, leading counters x86 x64
0, 1 pass pass
0, 2 pass pass
4, 1 fail: histogram shifted fail: access violation at the end of the data
4, 2 fail: access violation at the end of the data fail: histogram shifted

In 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=1 and DOTNET_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 had countsOffset % 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 from PgoManager::getPgoInstrumentationResults, CEECodeGenInfo::getPgoInstrumentationResults and TieredCompilationManager::OptimizeMethod. There the fixed run consumed 1841 snapshots without a fault (1426 with countsOffset % 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.

JulieLeeMSFT and others added 2 commits October 2, 2026 17:12
…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
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Oct 9, 2026
@azure-pipelines

Copy link
Copy Markdown
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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

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
@JulieLeeMSFT
JulieLeeMSFT requested a balanced review from Copilot October 9, 2026 21:39
@JulieLeeMSFT

Copy link
Copy Markdown
Member Author

@EgorBo PTAL. cc @dotnet/jit-contrib

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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

@pcshrosbree

Copy link
Copy Markdown
Contributor

I went through the allocator and the reader and I agree with the analysis. allocPgoInstrumentationBySchemaInstance lays entries out from countsOffset. On 32-bit, countsOffset is AlignUp(24 + schemaLen, 4) - 24, so it can be any multiple of 4. However, getPgoInstrumentationResultsInstance re-ran the layout from 0 and then copied from GetData() + countsOffset. With countsOffset % 8 == 4, AlignUp(countsOffset + x, 8) and countsOffset + AlignUp(x, 8) differ by 4 at the first 8-byte entry (ValueHistogram on x86), and the error carries to every later entry and to the copy length. The other readers in pgo.cpp (171, 331, 761, 793, 895) already use countsOffset as the initial offset, so this was the one inconsistent site.

To check the test independently of a Windows x86 build, I compiled PgoSchemaNative.cpp unchanged as real i386 code on Linux (g++ -m32, sizeof(size_t) == 4). I compiled it once against the PR-head header and once against the main header with the pre-PR algorithm dropped in under the new helper's signature. In a Unix variant of the test, I also added an mmap guard page so an over-read faults there too. Against the old algorithm the rows behave exactly as your table says: (4,1) reads the histogram four bytes early (entry 2 shifted), and (4,2) faults with the faulting address equal to the end of the data. Against the PR-head header, and against the header from a trial merge into main at 44253fe, all four rows passed on both i386 and x64.

In a sweep of 200k random schemas using the allocator's own countsOffset rule, the old reader misplaced entries in 87k of them on 32-bit (21k with the copy running past the block). The new reader misplaced entries in none. On 64-bit, both readers selected the same instrumentation bytes in the sweep: countsOffset is always a multiple of 8 because the allocator aligns to the 8-byte size_t and sizeof(HeaderList) is 32. The fixed snapshot also copies the compressed-schema prefix and keeps data-relative offsets.

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 ComparePgoSchemaEquals.

Two small observations, neither blocking:

  • On Unix the PR's rows report shifted data but not the over-read itself. The VirtualProtect guard-page arrangement can be reproduced there with mmap/mprotect and a SIGSEGV handler, so the Unix rows would report the fault as well. I have that variant if it is useful.
  • The same layout code runs on linux-arm, where HeaderList is also 24 bytes, so the fix applies there as well even though the reports are all Windows x86.

The change looks right to me.

@pcshrosbree

Copy link
Copy Markdown
Contributor

@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.

@EgorBo

EgorBo commented Oct 22, 2026

Copy link
Copy Markdown
Member

I'm thinking of fixing it differently. This change fixes the symptom, not the cause.

@JulieLeeMSFT

Copy link
Copy Markdown
Member Author

@pcshrosbree, thanks for taking a look. EgorBo will work on a proper fix. Closing this PR.

@pcshrosbree

Copy link
Copy Markdown
Contributor

Thanks Julie. Whatever makes CI more reliable.

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

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash: access violation in coreclr!PgoManager::getPgoInstrumentationResults during tier-1 background JIT (Windows x86)

4 participants