diff --git a/src/coreclr/jit/objectalloc.cpp b/src/coreclr/jit/objectalloc.cpp index 012248266bf701..d8751ac07c686a 100644 --- a/src/coreclr/jit/objectalloc.cpp +++ b/src/coreclr/jit/objectalloc.cpp @@ -3517,6 +3517,21 @@ void ObjectAllocator::CheckForGuardedAllocationOrCopy(BasicBlock* block, RecordAppearance(lclNum, block, stmt, use); } } + else if (!data->IsIntegralConst(0)) + { + // Store into a tracked enumerator local from an unrecognized source + // (e.g. a virtual GetEnumerator call that did not devirtualize, into + // a local Roslyn shares between two enumerator scopes). Record it so + // CheckCanClone's multiple-defs check bails out of unsafe cloning. + // Null/zero stores are skipped: the inliner emits these as GC cleanup + // of dead temps. See https://github.com/dotnet/runtime/issues/127075. + // + unsigned pseudoIndex = BAD_VAR_NUM; + if (m_EnumeratorLocalToPseudoIndexMap.TryGetValue(lclNum, &pseudoIndex)) + { + RecordAppearance(lclNum, block, stmt, use); + } + } } //------------------------------------------------------------------------------ diff --git a/src/tests/JIT/Regression/JitBlue/Runtime_127075/Runtime_127075.cs b/src/tests/JIT/Regression/JitBlue/Runtime_127075/Runtime_127075.cs new file mode 100644 index 00000000000000..8120052d1875ed --- /dev/null +++ b/src/tests/JIT/Regression/JitBlue/Runtime_127075/Runtime_127075.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.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using Xunit; + +public class Runtime_127075 +{ + public interface I; + public sealed class A : I; + public sealed class B : I; + + // Roslyn lowers this collection literal stored to IReadOnlyCollection + // into a `new <>z__ReadOnlyArray(new I[]{...})` wrapper. The wrapper's + // GetEnumerator() does NOT devirtualize even under PGO (the array element + // type is inexact), so it stays as a virtual call. That is what triggered + // the bug: the second loop's enumerator local store was hidden from + // conditional escape analysis. + // + private static readonly IReadOnlyCollection s_tail = [new B(), new B()]; + + [MethodImpl(MethodImplOptions.NoInlining)] + private static I[] Combine(IReadOnlyCollection head) + { + return [.. head, .. s_tail]; + } + + [Fact] + public static void TestEntryPoint() + { + I[] head = [new A(), new A(), new A(), new A()]; + for (int i = 0; i < 300; i++) + { + I[] result = Combine(head); + Assert.Equal(head.Length + s_tail.Count, result.Length); + for (int j = 0; j < head.Length; j++) + { + Assert.Same(head[j], result[j]); + } + int k = head.Length; + foreach (I t in s_tail) + { + Assert.Same(t, result[k++]); + } + Thread.Sleep(1); + } + } +} diff --git a/src/tests/JIT/Regression/JitBlue/Runtime_127075/Runtime_127075.csproj b/src/tests/JIT/Regression/JitBlue/Runtime_127075/Runtime_127075.csproj new file mode 100644 index 00000000000000..88d79098489f98 --- /dev/null +++ b/src/tests/JIT/Regression/JitBlue/Runtime_127075/Runtime_127075.csproj @@ -0,0 +1,16 @@ + + + True + + true + + + + + + + + +