From df26e508a060d9ea9fbf103a6b8613454cd71a06 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Tue, 22 Sep 2026 23:26:11 +0200 Subject: [PATCH 1/5] JIT: Fix RangeOps::ShiftRight for logical shifts of possibly negative values x >>> [0..N] may preserve a negative x (shift by 0) or produce a large positive value, so the upper limit derived from r1's upper limit is unsound when r1 may be negative. Fixes #134455 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/rangecheck.h | 8 +++-- .../JIT/Regression_ro_2/Runtime_134455.cs | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/tests/JIT/Regression_ro_2/Runtime_134455.cs diff --git a/src/coreclr/jit/rangecheck.h b/src/coreclr/jit/rangecheck.h index 64c38ac7b687b9..ffbbf09c9d4538 100644 --- a/src/coreclr/jit/rangecheck.h +++ b/src/coreclr/jit/rangecheck.h @@ -442,9 +442,13 @@ struct RangeOps // For RSZ by N >= 1, result is in [0, UINT_MAX >> N] regardless of r1's signedness. // When r1 isn't proven non-negative, the bound above is unsound (negative r1 reinterprets // as large unsigned), so override with the type-based bound. - if (logical && (r2.LowerLimit().GetConstant() >= 1) && - !(r1.LowerLimit().IsConstant() && (r1.LowerLimit().GetConstant() >= 0))) + if (logical && !(r1.LowerLimit().IsConstant() && (r1.LowerLimit().GetConstant() >= 0))) { + if (r2.LowerLimit().GetConstant() == 0) + { + // A shift by 0 may preserve a negative r1, so nothing is known. + return Limit(Limit::keUnknown); + } result.lLimit = Limit(Limit::keConstant, 0); result.uLimit = Limit(Limit::keConstant, (int)(UINT32_MAX >> r2.LowerLimit().GetConstant())); } diff --git a/src/tests/JIT/Regression_ro_2/Runtime_134455.cs b/src/tests/JIT/Regression_ro_2/Runtime_134455.cs new file mode 100644 index 00000000000000..b112d54b04f657 --- /dev/null +++ b/src/tests/JIT/Regression_ro_2/Runtime_134455.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using Xunit; + +public class Runtime_134455 +{ + [Fact] + public static void TestEntryPoint() + { + Assert.Throws(() => Test(-1, 1)); + Assert.Equal(0, Test(-1, 0)); + Assert.Equal(0, Test(50, 0)); + } + + // "x >>> (y & 3)" is a large positive value for negative "x". + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static int Test(int x, int y) + { + int[] a = new int[51]; + if (x <= 50) + { + int v = x >>> (y & 3); + if (v >= 0) + return a[v]; + } + return 0; + } +} From 54eeff49d32abfbd639197a98c54018a211d0f51 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Tue, 22 Sep 2026 23:39:37 +0200 Subject: [PATCH 2/5] JIT: Don't compute integer ranges for floating point VNs in range check Casts between integral and floating point types aren't value-preserving (rounding, saturation), so the integral source range must not be propagated through them. Fixes #134452 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/rangecheck.cpp | 6 +++ .../JIT/Regression_ro_2/Runtime_134452.cs | 51 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/tests/JIT/Regression_ro_2/Runtime_134452.cs diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index 7b2a38a7b5171e..2d4cdfa688cfd5 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -599,6 +599,12 @@ Range RangeCheck::GetRangeFromAssertionsWorker( #endif } + if (varTypeIsFloating(vnType)) + { + // Integer ranges don't describe floating point values (e.g. int->float casts may round). + return Limit(Limit::keUnknown); + } + // // First, let's see if we can tighten the range based on VN information. // diff --git a/src/tests/JIT/Regression_ro_2/Runtime_134452.cs b/src/tests/JIT/Regression_ro_2/Runtime_134452.cs new file mode 100644 index 00000000000000..f90f963ad12d9b --- /dev/null +++ b/src/tests/JIT/Regression_ro_2/Runtime_134452.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using Xunit; + +public class Runtime_134452 +{ + [Fact] + public static void TestEntryPoint() + { + Assert.Equal(1, IntToFloatRounding(16777219)); + Assert.Equal(1, DoubleToUIntSaturation(-5)); + Assert.Throws(() => IndexThroughFloat(new byte[16777220], 16777219)); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static int IntToFloatRounding(int x) + { + if (x >= 0 && x <= 16777219) + { + // (float)16777219 rounds to 16777220 + int y = (int)(float)x; + if (y > 16777219) + return 1; + } + return 0; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static int DoubleToUIntSaturation(int x) + { + if (x < 0) + { + // Saturates to 0 + uint u = (uint)(double)x; + if (u == 0) + return 1; + } + return 0; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static int IndexThroughFloat(byte[] arr, int x) + { + if (arr.Length == 16777220 && x >= 0 && x <= 16777219) + return arr[(int)(float)x]; + return -1; + } +} From 86c00020eb90131becf31b0dbaa34484586a12ce Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Tue, 22 Sep 2026 23:43:17 +0200 Subject: [PATCH 3/5] JIT: Don't skip def overflow checks for dependent ranges in range check DoesVarDefOverflow skips checking a local's def when assertions bound the local within the computed range. That's unsound when the range has a dependent limit: Widen later resolves it by assuming the def chain is monotonic, i.e. doesn't overflow. Fixes #134451 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/rangecheck.cpp | 6 ++-- .../JIT/Regression_ro_2/Runtime_134451.cs | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/tests/JIT/Regression_ro_2/Runtime_134451.cs diff --git a/src/coreclr/jit/rangecheck.cpp b/src/coreclr/jit/rangecheck.cpp index 2d4cdfa688cfd5..4d5a45c83d2d60 100644 --- a/src/coreclr/jit/rangecheck.cpp +++ b/src/coreclr/jit/rangecheck.cpp @@ -2150,9 +2150,11 @@ bool RangeCheck::DoesVarDefOverflow(BasicBlock* block, GenTreeLclVarCommon* lcl, // But only if the range from the assertion is more strict than the global // range computed; otherwise we might still have used the def's value to - // tighten the range of the global range. + // tighten the range of the global range. A dependent limit doesn't qualify: + // Widen resolves it later assuming the defs don't overflow. Range merged = RangeOps::Merge(range, assertionRange, false); - if (merged.LowerLimit().Equals(range.LowerLimit()) && merged.UpperLimit().Equals(range.UpperLimit())) + if (!range.LowerLimit().IsDependent() && !range.UpperLimit().IsDependent() && + merged.LowerLimit().Equals(range.LowerLimit()) && merged.UpperLimit().Equals(range.UpperLimit())) { return false; } diff --git a/src/tests/JIT/Regression_ro_2/Runtime_134451.cs b/src/tests/JIT/Regression_ro_2/Runtime_134451.cs new file mode 100644 index 00000000000000..294c3edc4c5b7c --- /dev/null +++ b/src/tests/JIT/Regression_ro_2/Runtime_134451.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.CompilerServices; +using Xunit; + +public class Runtime_134451 +{ + [Fact] + public static void TestEntryPoint() + { + Assert.Throws(() => Test(new int[101], true)); + } + + // "x" wraps around, so "j += x" is not monotonic and "j" becomes negative. + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.AggressiveOptimization)] + private static int Test(int[] a, bool flag) + { + int j = 0, x = int.MaxValue - 10, sum = 0; + if (a.Length != 101) + return 0; + while (j < 101) + { + if (flag) + sum += a[j]; + x++; + if (x >= int.MinValue + 1 && x <= 10) + j += x; + } + return sum; + } +} From 015c0332085961922e72e8ac70298ee447ed18b4 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Tue, 22 Sep 2026 23:51:37 +0200 Subject: [PATCH 4/5] JIT: ARM64: Honor contained index extension in genLeaInstruction A LEA with a contained CAST/BFIZ index used the 32-bit cast operand register directly, dropping the sign/zero-extension (e.g. for write-barrier store addresses). Use the extended-register add form instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegenarmarch.cpp | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/coreclr/jit/codegenarmarch.cpp b/src/coreclr/jit/codegenarmarch.cpp index 37cca461351f49..6aa8861290be20 100644 --- a/src/coreclr/jit/codegenarmarch.cpp +++ b/src/coreclr/jit/codegenarmarch.cpp @@ -4278,31 +4278,33 @@ void CodeGen::genLeaInstruction(GenTreeAddrMode* lea) else { #ifdef TARGET_ARM64 - if (index->isContained()) { + // Only BFIZ/CAST nodes should be present for contained index on ARM64. + GenTreeCast* cast; if (index->OperIs(GT_BFIZ)) { - // Handle LEA with "contained" BFIZ assert(scale == 0); scale = (DWORD)index->gtGetOp2()->AsIntConCommon()->IconValue(); - index = index->gtGetOp1()->gtGetOp1(); - } - else if (index->OperIs(GT_CAST)) - { - index = index->AsCast()->gtGetOp1(); + cast = index->gtGetOp1()->AsCast(); } else { - // Only BFIZ/CAST nodes should be present for for contained index on ARM64. - // If there are more, we need to handle them here. - unreached(); + cast = index->AsCast(); } + + // The 32-bit index has to be sign/zero-extended as part of the add. + assert(genActualTypeIsInt(cast->CastOp()) && (scale <= 4)); + emit->emitIns_R_R_R_I(INS_add, size, lea->GetRegNum(), memBase->GetRegNum(), + cast->CastOp()->GetRegNum(), scale, + cast->IsUnsigned() ? INS_OPTS_UXTW : INS_OPTS_SXTW); } + else #endif - - // Then compute target reg from [base + index*scale] - genScaledAdd(size, lea->GetRegNum(), memBase->GetRegNum(), index->GetRegNum(), scale); + { + // Then compute target reg from [base + index*scale] + genScaledAdd(size, lea->GetRegNum(), memBase->GetRegNum(), index->GetRegNum(), scale); + } } } else if (lea->HasBase()) From 2083ddfda09d77c82e39dd16a25693e58ec11f44 Mon Sep 17 00:00:00 2001 From: Egor Bogatov Date: Wed, 30 Sep 2026 19:17:06 +0200 Subject: [PATCH 5/5] Skip Runtime_134452 on Mono Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 09f0a890-fed4-436a-a59b-29b9ffcb970a --- src/tests/JIT/Regression_ro_2/Runtime_134452.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/tests/JIT/Regression_ro_2/Runtime_134452.cs b/src/tests/JIT/Regression_ro_2/Runtime_134452.cs index f90f963ad12d9b..38bca98c6a189f 100644 --- a/src/tests/JIT/Regression_ro_2/Runtime_134452.cs +++ b/src/tests/JIT/Regression_ro_2/Runtime_134452.cs @@ -8,6 +8,7 @@ public class Runtime_134452 { [Fact] + [SkipOnMono("CoreCLR JIT regression test")] public static void TestEntryPoint() { Assert.Equal(1, IntToFloatRounding(16777219));