diff --git a/src/coreclr/jit/compiler.hpp b/src/coreclr/jit/compiler.hpp index 9db68788f7209d..c0db7ae36e18c8 100644 --- a/src/coreclr/jit/compiler.hpp +++ b/src/coreclr/jit/compiler.hpp @@ -3319,6 +3319,12 @@ inline bool Compiler::IsPotentialGCSafePoint(GenTree* tree) const } } + // A tree that may throw can call a throw helper, which is a gc safe point. + if ((tree->gtFlags & GTF_EXCEPT) != 0) + { + return true; + } + // TYP_STRUCT-typed stores might be converted into calls (with gc safe points) in Lower. // This is quite a conservative fix as it's hard to prove Lower won't do it at this point. if (tree->OperIsLocalStore()) diff --git a/src/tests/JIT/Regression_ro_2/Runtime_135966.cs b/src/tests/JIT/Regression_ro_2/Runtime_135966.cs new file mode 100644 index 00000000000000..5c4a09fc0f73a0 --- /dev/null +++ b/src/tests/JIT/Regression_ro_2/Runtime_135966.cs @@ -0,0 +1,107 @@ +// 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; + +// Each victim throws before its GC ref local is first stored, so a GC while the throw +// helper runs or the exception is dispatched must not see DirtyStack's garbage in it. +public class Runtime_135966 +{ + private class Holder + { + public object Value = new object(); + } + + [InlineArray(32)] + private struct Garbage + { + private nint _element; + } + + [Fact] + public static void TestEntryPoint() + { + int[] array = new int[1]; + + for (int i = 0; i < 10; i++) + { + DirtyStack(); + + bool caught = false; + try + { + NullCheckVictim(null); + } + catch (NullReferenceException) when (Collect()) + { + caught = true; + } + + Assert.True(caught); + + DirtyStack(); + + caught = false; + try + { + BoundsCheckVictim(array, 5); + } + catch (IndexOutOfRangeException) when (Collect()) + { + caught = true; + } + + Assert.True(caught); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void DirtyStack() + { + Garbage garbage = default; + Span span = garbage; + span.Fill(0x1000); + Consume(span); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void Consume(Span span) + { + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static object NullCheckVictim(Holder holder) + { + object value = holder.Value; + KeepAlive(value); + return value; + } + + // The local is address-exposed, so it is an untracked GC slot on every target. + [MethodImpl(MethodImplOptions.NoInlining)] + private static object BoundsCheckVictim(int[] array, int index) + { + int element = array[index]; + object value = array; + KeepAlive(ref value); + return element == 0 ? value : null; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void KeepAlive(object value) + { + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void KeepAlive(ref object value) + { + } + + private static bool Collect() + { + GC.Collect(); + return true; + } +}