Skip to content

JIT: don't keep a stale range when assertion tightening yields an empty range - #132898

Open
EgorBo wants to merge 3 commits into
mainfrom
egor/fix-rangecheck-stale-range
Open

JIT: don't keep a stale range when assertion tightening yields an empty range#132898
EgorBo wants to merge 3 commits into
mainfrom
egor/fix-rangecheck-stale-range

Conversation

@EgorBo

@EgorBo EgorBo commented Aug 28, 2026

Copy link
Copy Markdown
Member

RangeCheck::MergeEdgeAssertionsWorker tightens *pRange with each incoming assertion. When tightening yielded an empty range it returned early and left the previously tightened range in place -- a range the assertions had just contradicted. That is only safe if the assertion set truly holds (then the block is unreachable), but ComputeRangeForLocalDef merges the use block's bbAssertionIn into the range of a definition in another block, and that set can be self-contradictory.

Since #129354 assertion prop folds conditions using these ranges, so the stale range becomes wrong code. In #132879 an enum local's range was computed as [5..7] while its value was 78, folding the op > 69 test guarding a switch to false and sending execution to default:

#197 VN $502 == 7    -> pRange [<7, 7>]
#202 VN $502 == 12   -> [<12, 7>] invalid range after tightening
         Computed Range [008382] => <7, 7>   <- stale range escapes

Fix: bail out to Unknown.

.NET 11 regression (.NET 10 unaffected). Needs full opts without PGO, which is what crossgen2 does -- hence it only showed up on a PublishReadyToRun=true publish, and why DOTNET_ReadyToRun=0 didn't help.

Same root cause as #132841, whose analysis points at this exact return. #132849 fixes the ComputeDoesOverflow path instead: that resolves #132841's repro but not #132879 (verified). This change fixes both, so #132849 may be redundant.

SPMI: replay clean on benchmarks.run + libraries.pmi; asmdiffs +48 bytes / 2 contexts (benchmarks.run), +3172 bytes / 46 contexts (libraries.pmi), 0.00% both.

Regression test uses AggressiveOptimization since tier1 profile data hides the bad range.

Fixes #132879

…ty range

RangeCheck::MergeEdgeAssertionsWorker tightens *pRange with each incoming
assertion. When the tightened range came out empty it logged "invalid range
after tightening" and returned, leaving the previously tightened range in
place. That range has just been contradicted by the assertions, so the caller
ends up holding a fact that was disproven.

Keeping it would only be safe if the assertion set really held at that point
(then the block is unreachable and any range is vacuously fine). It does not
always hold: ComputeRangeForLocalDef merges the use block's bbAssertionIn into
the range of a definition that lives in another block, and that set can contain
mutually exclusive assertions.

Since #129354 assertion propagation folds conditions using these ranges, so the
stale range turns into wrong code. In #132879 the range of an enum local was
computed as [5..7] while its value was 78, which folded the "op > 69" test
guarding a switch to false and sent execution to the default case.

Bail out to Unknown instead.

Fixes #132879

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 71e21c42-60e7-47f1-9026-4bf2df7a8e51
Copilot AI lite review requested due to automatic review settings August 28, 2026 17:40
@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 Aug 28, 2026
@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.

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.

Pull request overview

Fixes an unsound RangeCheck range-tightening behavior where encountering contradictory assertions could leave a previously-tightened (stale) range in place, enabling incorrect assertion-prop folding in optimized JIT/crossgen2 scenarios. Adds a targeted JIT regression test for the reported miscompile involving an enum-backed switch.

Changes:

  • In RangeCheck::MergeEdgeAssertionsWorker, when tightening yields an empty range, explicitly bail out by setting *pRange to Unknown instead of leaving the prior range intact.
  • Add a new JitBlue regression test (Runtime_132879) exercising the miscompile shape under AggressiveOptimization.
  • Wire the new test into Regression_ro_2.csproj.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/coreclr/jit/rangecheck.cpp Makes contradictory assertion tightening conservative by resetting the output range to Unknown on empty intersections.
src/tests/JIT/Regression/JitBlue/Runtime_132879/Runtime_132879.cs New xUnit regression test covering the switch/enum miscompile scenario.
src/tests/JIT/Regression/Regression_ro_2.csproj Includes the new regression test in the merged test project.

Comment on lines 1679 to +1683
JITDUMP("invalid range after tightening\n");
// The tightened range is empty, i.e. the assertions contradict the range we computed.
// If the assertion set really did hold here the block would be unreachable, but we get
// here with sets that do not hold at this point (e.g. assertions of a use block merged
// into the range of a definition that lives in another block). Returning while leaving
Copilot AI review requested due to automatic review settings August 28, 2026 19:40

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.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

src/coreclr/jit/rangecheck.cpp:1686

  • assertedRange.IsValid() bail-out returns without resetting *pRange. Since assertedRange is accumulated across assertions in this loop, it can become invalid when assertions contradict each other; returning here can still leak a partially-tightened (stale) range to the caller, similar to the empty-range case fixed below. To keep range inference sound in the presence of self-contradictory assertion sets, set *pRange to Unknown before returning (same pattern as the copy.IsValid() failure path).
            JITDUMP("invalid range after tightening\n");
            // The tightened range is empty, i.e. the assertions contradict the range we computed.
            // If the assertion set really did hold here the block would be unreachable, but we get
            // here with sets that do not hold at this point (e.g. assertions of a use block merged
            // into the range of a definition that lives in another block). Returning while leaving
            // the previously tightened range in place lets a fact that has just been disproven
            // escape to the caller, which then folds branches with it. Bail out to Unknown instead.
            *pRange = Range(Limit(Limit::keUnknown));

Comment thread src/tests/JIT/Regression/JitBlue/Runtime_132879/Runtime_132879.cs
Copilot AI review requested due to automatic review settings August 29, 2026 06:50

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

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

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

2 participants