You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The x64 JIT emits ror reg, 1 followed directly by jne to test whether BitOperations.RotateRight(value, 1) is non-zero, but ROR does not
modify ZF on x64. The jne therefore reads stale ZF from a prior
unrelated cmp, producing wrong-direction branches and corrupting control
flow. Discovered by an experimental in-development fuzzer.
Note
This issue was authored by GitHub Copilot CLI on @AndyAyersMS's machine,
based on a bug surfaced by an experimental in-development fuzzer.
The C# repro is verified.
Root cause
GenTree::SupportsSettingZeroFlag() in gentree.cpp:21685
treats rotates the same as shifts:
#if defined(TARGET_XARCH)
if (OperIs(GT_LSH, GT_RSH, GT_RSZ, GT_ROL, GT_ROR))
{
// Shift/Rotate instructions do not update the flags in case of count being zero.returngtGetOp2()->IsNeverZero();
}
Lowering uses this signal to drop a TEST/CMP before a zero-comparison
branch. That is correct for shifts (which set ZF when count != 0) but wrong for rotates: per the Intel SDM, ROL/ROR/RCL/RCR only
affect CF (and OF for 1-bit form). SF, ZF, AF, PF are not
affected, regardless of count. So even with a non-zero count, a rotate
does not produce a meaningful ZF.
GT_ROL and GT_ROR were added to the supports-zero-flag set in #113037 (Apr 2025) and
the partial repair in #117960 (Jul 2025)
moved the IsNeverZero check but kept rotates in the same block.
The proposed fix is to split them:
if (OperIs(GT_LSH, GT_RSH, GT_RSZ))
{
// Shift instructions do not update the flags in case of count being zero.returngtGetOp2()->IsNeverZero();
}
// GT_ROL / GT_ROR omitted: rotates never modify ZF on xarch.
Repro
C# source (verified to throw on FullOpts, succeed on MinOpts and tiered):
usingSystem;usingSystem.Numerics;usingSystem.Runtime.CompilerServices;publicstaticclassProgram{privatestaticulongInput_p0=0UL;privatestaticulongInput_p1=0UL;privatestaticint[]__trace=newint[64];privatestaticint__traceLen=0;[MethodImpl(MethodImplOptions.NoInlining)]publicstaticulongFn(ulongp0,ulongp1){unchecked{ulongv4=0UL,v11=0UL,v30=0UL,v33=0UL,v34=0UL;intv12=0;b0:__trace[__traceLen++]=0;v4=BitOperations.RotateLeft(p0,(int)(ulong)0x7FFFFFFFFFFFFFFFUL);b1:__trace[__traceLen++]=1;b2:__trace[__traceLen++]=2;v11=BitOperations.RotateRight(v4,1);v12=(v11<1UL)?1:0;if(v12!=0)gotob3;gotob4;b3:__trace[__traceLen++]=3;gotob6;b4:__trace[__traceLen++]=4;gotob7;b6:__trace[__traceLen++]=6;gotob0;b7:__trace[__traceLen++]=7;v34=v33%v30;// dead in source semanticsgotob0;}}publicstaticintMain(){ulongresult=Fn(Input_p0,Input_p1);Console.WriteLine($"{result:X16}");return0;}}
With DOTNET_TieredCompilation=0 (FullOpts), this throws System.DivideByZeroException on the very first iteration of the loop.
Under the source semantics, control should go b0 → b1 → b2 → b3 → b6 → b0 → ... repeatedly: v4 is RotateLeft(p0=0, ...) = 0, so v11 = RotateRight(0, 1) = 0, so v12 = (v11 < 1) ? 1 : 0 = 1, so the branch v12 != 0 is taken to b3. Block b7 (with the v33 % v30 where both are 0) is
unreachable. With __trace sized at 64, the correct semantics eventually
overflow the trace array on iteration ~13 with IndexOutOfRangeException; with default tiered or JitMinOpts=1 that
is what is observed.
Generated code (offending fragment, FullOpts x64)
G_M44088_IG04: ; BB07 (main loop body) ...cmpebx,r11d ; bounds check for __trace store (sets ZF)jae SHORT G_M44088_IG06mov dword ptr [rdx+4*rbx+0x10],2rorrax,1 ; ROR does NOT modify ZFjne G_M44088_IG03 ; reads STALE ZF from the cmp aboveG_M44088_IG03: ; BB04 -> b4/b7 ...xoreax,eaxxorr11d,r11dxoredx,edxdivrdx:rax,r11 ; <-- DivideByZeroException
The cmp ebx, r11d immediately preceding the rotate-then-branch
compares the trace cursor with __trace.Length = 64. Since ebx != 64, ZF = 0, and the jne is taken — into the unreachable
b4/b7 path that does % 0.
Environment
HEAD of main as of 2026-06-10 (Checked CoreCLR, x64 Windows).
Repros on corerun with DOTNET_TieredCompilation=0.
Discovered using ReifyCs (in-development C# fuzzer); seed 2000017594.
Suggested test
A regression test could synthesize any if (BitOperations.RotateRight(x, n) == 0) pattern preceded by a
flag-setting cmp whose ZF differs from "is rotate result zero",
asserting the branch direction matches the interpreter.
Summary
The x64 JIT emits
ror reg, 1followed directly byjneto test whetherBitOperations.RotateRight(value, 1)is non-zero, butRORdoes notmodify ZF on x64. The
jnetherefore reads stale ZF from a priorunrelated
cmp, producing wrong-direction branches and corrupting controlflow. Discovered by an experimental in-development fuzzer.
Note
This issue was authored by GitHub Copilot CLI on @AndyAyersMS's machine,
based on a bug surfaced by an experimental in-development fuzzer.
The C# repro is verified.
Root cause
GenTree::SupportsSettingZeroFlag()ingentree.cpp:21685
treats rotates the same as shifts:
Lowering uses this signal to drop a
TEST/CMPbefore a zero-comparisonbranch. That is correct for shifts (which set ZF when count != 0) but
wrong for rotates: per the Intel SDM,
ROL/ROR/RCL/RCRonlyaffect
CF(andOFfor 1-bit form).SF,ZF,AF,PFare notaffected, regardless of count. So even with a non-zero count, a rotate
does not produce a meaningful ZF.
GT_ROLandGT_RORwere added to the supports-zero-flag set in#113037 (Apr 2025) and
the partial repair in
#117960 (Jul 2025)
moved the
IsNeverZerocheck but kept rotates in the same block.The proposed fix is to split them:
Repro
C# source (verified to throw on FullOpts, succeed on MinOpts and tiered):
With
DOTNET_TieredCompilation=0(FullOpts), this throwsSystem.DivideByZeroExceptionon the very first iteration of the loop.Under the source semantics, control should go
b0 → b1 → b2 → b3 → b6 → b0 → ...repeatedly:v4isRotateLeft(p0=0, ...) = 0, sov11 = RotateRight(0, 1) = 0, sov12 = (v11 < 1) ? 1 : 0 = 1, so the branchv12 != 0is taken tob3. Blockb7(with thev33 % v30where both are0) isunreachable. With
__tracesized at 64, the correct semantics eventuallyoverflow the trace array on iteration ~13 with
IndexOutOfRangeException; with default tiered orJitMinOpts=1thatis what is observed.
Generated code (offending fragment, FullOpts x64)
The
cmp ebx, r11dimmediately preceding the rotate-then-branchcompares the trace cursor with
__trace.Length = 64. Sinceebx != 64, ZF = 0, and thejneis taken — into the unreachableb4/b7 path that does
% 0.Environment
mainas of 2026-06-10 (Checked CoreCLR, x64 Windows).corerunwithDOTNET_TieredCompilation=0.2000017594.Suggested test
A regression test could synthesize any
if (BitOperations.RotateRight(x, n) == 0)pattern preceded by aflag-setting
cmpwhose ZF differs from "is rotate result zero",asserting the branch direction matches the interpreter.