Skip to content

JIT: fix stale VN in redundant branch opts dominating-branch simplification#129914

Open
EgorBo wants to merge 2 commits into
dotnet:mainfrom
EgorBo:EgorBo/fix-rbo-stale-vn-128062
Open

JIT: fix stale VN in redundant branch opts dominating-branch simplification#129914
EgorBo wants to merge 2 commits into
dotnet:mainfrom
EgorBo:EgorBo/fix-rbo-stale-vn-128062

Conversation

@EgorBo

@EgorBo EgorBo commented Jun 26, 2026

Copy link
Copy Markdown
Member

Fixes #128062.

Root cause

Compiler::optRedundantDominatingBranch (Redundant Branch Opts) walks up the dominator tree from a conditional block, trying to fold dominating branches whose outcome is implied by the current block's condition. While doing so it can simplify an intermediate dominating relop, rewriting the start block's relop in place:

tree->SetOper(newRelop);
...
fgValueNumberTree(tree);

However, treeNormVN (the start block's condition value number) was cached once as const before the dominator walk. After the relop is rewritten, the loop keeps reasoning about the stale VN on the next iteration, which can unsoundly fold a higher dominating guard.

Concrete miscompile

StreamReader.ValidateArgsAndOpenPath contains:

if (bufferSize != -1)
    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(bufferSize);

which lowers to three conditional blocks:

BB01: if (x == -1) goto Return        // the != -1 guard
BB02: if (x <  0)  goto Throw
BB06: if (x != 0)  goto Return else Throw

RBO starts at BB06, simplifies BB02's x < 0 away and rewrites BB06's relop to x > 0. It then walks up to BB01 but still uses the stale NE(x, 0) VN, concludes the x == -1 guard is redundant, and folds it. The result is if (x > 0) return; else throw, so x == -1 now throws ArgumentOutOfRangeException instead of being accepted.

This is exposed in CI on the libraries-pgo / jitosr_stress_random legs (which disable R2R, so framework code is JIT-compiled at optimized tiers), but it reproduces at plain FullOpts — no PGO/OSR needed.

Regression introduced by #127181.

Fix

Refresh treeNormVN right after the start block's relop is re-value-numbered, so subsequent dominator iterations reason about the block's current condition.

Testing

  • New regression test Runtime_128062 (fails on the pre-fix JIT, passes after).
  • SuperPMI asmdiffs on benchmarks.run (+pgo, +pgo_optrepeat) and libraries.pmi: ~530K contexts, no asserts, zero asm diffs — the change only suppresses the rare unsound fold.

)

optRedundantDominatingBranch walks up the dominator tree and may simplify
an intermediate dominating relop, rewriting the start block's relop via
tree->SetOper(newRelop) + fgValueNumberTree(tree). The cached treeNormVN
was not refreshed afterwards, so optimizing a higher dominating branch
reasoned about a stale value number and could unsoundly fold a guard.

This caused `if (x != -1) ThrowIfNegativeOrZero(x)` (as in StreamReader's
buffer-size validation) to throw for x == -1. Refresh treeNormVN after the
relop is rewritten.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 26, 2026 21:35
@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 Jun 26, 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

This PR fixes a correctness issue in CoreCLR RyuJIT’s redundant-branch optimization (optRedundantDominatingBranch) where a cached value number for the current block’s condition could become stale after in-loop relop simplification, leading to unsound folding of higher dominating guards.

Changes:

  • Update optRedundantDominatingBranch to refresh the cached normalized VN (treeNormVN) immediately after rewriting and re-value-numbering the start block’s relop.
  • Add a new JIT regression test (Runtime_128062) that exercises the bufferSize != -1 sentinel pattern to ensure -1 is accepted and not miscompiled into a throwing path.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/coreclr/jit/redundantbranchopts.cpp Refreshes the start-block condition VN after relop rewrites so subsequent dominator-walk reasoning uses the updated VN.
src/tests/JIT/Regression/JitBlue/Runtime_128062/Runtime_128062.cs Adds a focused regression test reproducing the stale-VN-driven unsound fold scenario.
src/tests/JIT/Regression/JitBlue/Runtime_128062/Runtime_128062.csproj Adds the project file for the new regression test with optimizations enabled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

[ci-scan] Known Build Error: StreamReader NegativeOneBufferSize test fails under PGO

2 participants