From 342786c77525c0f6da72793ae9e05b5ca83578c1 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Fri, 17 Jul 2026 11:47:09 -0700 Subject: [PATCH 1/4] Reject folding local addresses the emitter cannot encode The emitter only supports byte-sized offsets for locals numbered 32768 or greater (see emitLclVarAddr::initLclVarAddr). IsValidLclAddr did not account for this, so a struct block-init at such a local with an offset >= 256 could be folded into the stack addressing form and hit an IMPL_LIMITATION, surfacing as an InvalidProgramException under MinOpts. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/compiler.hpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/coreclr/jit/compiler.hpp b/src/coreclr/jit/compiler.hpp index 6b7b08ed31a4c2..8aad4186d05faf 100644 --- a/src/coreclr/jit/compiler.hpp +++ b/src/coreclr/jit/compiler.hpp @@ -3324,7 +3324,8 @@ inline bool Compiler::fgIsBigOffset(size_t offset) // IsValidLclAddr: Can the given local address be represented as "LCL_ADDR"? // // Local address nodes cannot point beyond the local and can only store -// 16 bits worth of offset. +// 16 bits worth of offset. Additionally, the emitter can only encode byte-sized +// offsets for locals numbered 32768 or greater. // // Arguments: // lclNum - The local's number @@ -3341,6 +3342,16 @@ inline bool Compiler::IsValidLclAddr(unsigned lclNum, unsigned offset) return (offset == 0); } #endif + + // The emitter only supports byte-sized offsets for locals numbered 32768 or greater + // (see emitLclVarAddr::initLclVarAddr). Reject larger offsets here so such accesses are + // kept as explicit address computations instead of being folded into a LCL_FLD or a + // contained LCL_ADDR, both of which the emitter would be unable to encode. + if ((lclNum >= 32768) && (offset >= 256)) + { + return false; + } + return (offset < UINT16_MAX) && (offset < lvaLclExactSize(lclNum)); } From 73bd9059a52650d305ea36be2e32adc4320b95b4 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Fri, 17 Jul 2026 12:04:44 -0700 Subject: [PATCH 2/4] Validate the resulting offset when folding local addresses fgOptimizeAddition folds ADD(LCL_ADDR(off), const) into LCL_ADDR(off+const) but validated only the addend against IsValidLclAddr, not the resulting offset. Repeated folds (or a fold onto an already-offset LCL_ADDR) could accumulate an offset the emitter cannot encode. Validate the computed offset instead. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/morph.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index 57c678dcb2ef6a..ee50514dbf0259 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -10569,16 +10569,15 @@ GenTree* Compiler::fgOptimizeAddition(GenTreeOp* add) GenTreeLclVarCommon* lclAddrNode = op1->AsLclVarCommon(); GenTreeIntCon* offsetNode = op2->AsIntCon(); ssize_t consVal = offsetNode->IconValue(); + ssize_t newOffset = static_cast(lclAddrNode->GetLclOffs()) + consVal; - // Note: the emitter does not expect out-of-bounds access for LCL_ADDR. - if (FitsIn(consVal) && IsValidLclAddr(lclAddrNode->GetLclNum(), (uint32_t)consVal)) + // Note: the emitter does not expect out-of-bounds access for LCL_ADDR. Validate the + // resulting offset rather than just the addend, so repeated folds cannot accumulate an + // offset the emitter is unable to encode. + if (FitsIn(newOffset) && IsValidLclAddr(lclAddrNode->GetLclNum(), (uint32_t)newOffset)) { - ClrSafeInt newOffset = - ClrSafeInt(lclAddrNode->GetLclOffs()) + ClrSafeInt(consVal); - assert(!newOffset.IsOverflow()); - lclAddrNode->SetOper(GT_LCL_ADDR); - lclAddrNode->AsLclFld()->SetLclOffs(newOffset.Value()); + lclAddrNode->AsLclFld()->SetLclOffs(static_cast(newOffset)); assert(lvaGetDesc(lclAddrNode)->lvDoNotEnregister); lclAddrNode->SetVNsFromNode(add); From 6215204cb372b35ba0c1be65b8c6f614fc44ce3f Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Fri, 17 Jul 2026 12:19:28 -0700 Subject: [PATCH 3/4] Add regression test for large-local block-init encoding Builds a method with more than 32768 locals via Reflection.Emit so the struct local's number crosses the emitter's large-varnum boundary, then block-inits it in-body reaching an offset >= 256 under MinOpts. Skips where dynamic code isn't supported. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../JitBlue/Runtime_126584/Runtime_126584.cs | 53 +++++++++++++++++++ .../Runtime_126584/Runtime_126584.csproj | 16 ++++++ 2 files changed, 69 insertions(+) create mode 100644 src/tests/JIT/Regression/JitBlue/Runtime_126584/Runtime_126584.cs create mode 100644 src/tests/JIT/Regression/JitBlue/Runtime_126584/Runtime_126584.csproj diff --git a/src/tests/JIT/Regression/JitBlue/Runtime_126584/Runtime_126584.cs b/src/tests/JIT/Regression/JitBlue/Runtime_126584/Runtime_126584.cs new file mode 100644 index 00000000000000..82dfb57f4c64c2 --- /dev/null +++ b/src/tests/JIT/Regression/JitBlue/Runtime_126584/Runtime_126584.cs @@ -0,0 +1,53 @@ +// 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.Reflection.Emit; +using TestLibrary; +using Xunit; + +// A struct large enough that initializing it reaches offsets >= 256. +public struct Big_126584 +{ + public long A0, A1, A2, A3, A4, A5, A6, A7, A8, A9; + public long A10, A11, A12, A13, A14, A15, A16, A17, A18, A19; + public long A20, A21, A22, A23, A24, A25, A26, A27, A28, A29; + public long A30, A31, A32, A33, A34, A35, A36, A37, A38, A39; +} + +public class Runtime_126584 +{ + // The emitter can only encode byte-sized offsets for locals numbered >= 32768. A struct + // block-init of such a local reaching offset >= 256 must not be folded into the stack + // addressing form, which the emitter cannot encode. Reflection.Emit is used to build a + // method with enough locals to push the struct past that boundary without a huge source file. + [ConditionalFact(typeof(Utilities), nameof(Utilities.IsReflectionEmitSupported))] + public static void TestEntryPoint() + { + var method = new DynamicMethod("M0", typeof(long), new[] { typeof(bool) }, typeof(Runtime_126584).Module); + ILGenerator il = method.GetILGenerator(); + + const int DummyLocalCount = 33000; + for (int i = 0; i < DummyLocalCount; i++) + { + il.DeclareLocal(typeof(int)); + } + LocalBuilder big = il.DeclareLocal(typeof(Big_126584)); + + // Initialize the struct behind a branch so it isn't hoisted into the prolog. + Label skip = il.DefineLabel(); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Brfalse, skip); + il.Emit(OpCodes.Ldloca, big); + il.Emit(OpCodes.Initobj, typeof(Big_126584)); + il.MarkLabel(skip); + + // Read a field past offset 255 so the access survives. + il.Emit(OpCodes.Ldloca, big); + il.Emit(OpCodes.Ldfld, typeof(Big_126584).GetField(nameof(Big_126584.A39))); + il.Emit(OpCodes.Ret); + + var func = (Func)method.CreateDelegate(typeof(Func)); + Assert.Equal(0, func(true)); + } +} diff --git a/src/tests/JIT/Regression/JitBlue/Runtime_126584/Runtime_126584.csproj b/src/tests/JIT/Regression/JitBlue/Runtime_126584/Runtime_126584.csproj new file mode 100644 index 00000000000000..1b5150958a87cf --- /dev/null +++ b/src/tests/JIT/Regression/JitBlue/Runtime_126584/Runtime_126584.csproj @@ -0,0 +1,16 @@ + + + + true + 1 + + + + + + + + + + + From b84d42c3439e7ad56d73f17346e8fa2145aae005 Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Fri, 17 Jul 2026 13:02:55 -0700 Subject: [PATCH 4/4] Avoid signed-overflow when validating folded local offsets Bound the addend to [0, UINT16_MAX] before adding it to the existing offset so the sum is computed in an unsigned domain that cannot overflow. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/morph.cpp | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index ee50514dbf0259..8750dba2963c4d 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -10569,23 +10569,28 @@ GenTree* Compiler::fgOptimizeAddition(GenTreeOp* add) GenTreeLclVarCommon* lclAddrNode = op1->AsLclVarCommon(); GenTreeIntCon* offsetNode = op2->AsIntCon(); ssize_t consVal = offsetNode->IconValue(); - ssize_t newOffset = static_cast(lclAddrNode->GetLclOffs()) + consVal; // Note: the emitter does not expect out-of-bounds access for LCL_ADDR. Validate the // resulting offset rather than just the addend, so repeated folds cannot accumulate an - // offset the emitter is unable to encode. - if (FitsIn(newOffset) && IsValidLclAddr(lclAddrNode->GetLclNum(), (uint32_t)newOffset)) + // offset the emitter is unable to encode. Both operands are bounded to [0, UINT16_MAX], + // so the addition below cannot overflow. + if (FitsIn(consVal)) { - lclAddrNode->SetOper(GT_LCL_ADDR); - lclAddrNode->AsLclFld()->SetLclOffs(static_cast(newOffset)); - assert(lvaGetDesc(lclAddrNode)->lvDoNotEnregister); + unsigned newOffset = lclAddrNode->GetLclOffs() + static_cast(consVal); - lclAddrNode->SetVNsFromNode(add); + if (FitsIn(newOffset) && IsValidLclAddr(lclAddrNode->GetLclNum(), newOffset)) + { + lclAddrNode->SetOper(GT_LCL_ADDR); + lclAddrNode->AsLclFld()->SetLclOffs(static_cast(newOffset)); + assert(lvaGetDesc(lclAddrNode)->lvDoNotEnregister); + + lclAddrNode->SetVNsFromNode(add); - DEBUG_DESTROY_NODE(offsetNode); - DEBUG_DESTROY_NODE(add); + DEBUG_DESTROY_NODE(offsetNode); + DEBUG_DESTROY_NODE(add); - return lclAddrNode; + return lclAddrNode; + } } }