diff --git a/docs/design/coreclr/jit/investigate-stress.md b/docs/design/coreclr/jit/investigate-stress.md index 0a70f8ae6d4410..b94faec09f8be3 100644 --- a/docs/design/coreclr/jit/investigate-stress.md +++ b/docs/design/coreclr/jit/investigate-stress.md @@ -24,6 +24,8 @@ Enabling GC Hole Stress causes GCs to always occur in specific locations and tha - **0x8** – GC on every allowable R2R instr. - **0x10** – GC only on a unique stack trace. +R2R instruction stress is not supported on Apple platforms, where write/execute restrictions can prevent patching image-backed code. + ### Common combinations **0x1 | 0x2** – 0x3 are "in the VM". Failures in 0x1 or 0x2 can be due to VM-related reasons, like lack of GC reporting/pinning in interop frames. diff --git a/src/coreclr/vm/gccover.cpp b/src/coreclr/vm/gccover.cpp index 3092d77359d81e..ee3db49e357025 100644 --- a/src/coreclr/vm/gccover.cpp +++ b/src/coreclr/vm/gccover.cpp @@ -8,7 +8,7 @@ /* This file holds code that is designed to test GC pointer tracking in fully interruptible code. We basically do a GC everywhere we can in - jitted code + managed code */ /****************************************************************************/ @@ -32,6 +32,18 @@ /****************************************************************************/ +GCStressCodeWriter::GCStressCodeWriter(BYTE* address, size_t size) + : m_addressRW(address) +{ + WRAPPER_NO_CONTRACT; + + if (!ExecutionManager::IsReadyToRunCode(PINSTRToPCODE((TADDR)address))) + { + m_writer.AssignExecutableWriterHolder(address, size); + m_addressRW = m_writer.GetRW(); + } +} + bool IsGcCoverageInterruptInstruction(PBYTE instrPtr) { UINT32 instrVal; @@ -93,9 +105,6 @@ void SetupAndSprinkleBreakpoints( { _ASSERTE(!nativeCodeVersion.IsNull()); - // CONSIDER: does anyone call this with fZapped == true ? are there plans? - _ASSERTE(!fZapped); - // Allocate room for the GCCoverageInfo and copy of the method instructions MethodDesc *pMD = nativeCodeVersion.GetMethodDesc(); size_t memSize = sizeof(GCCoverageInfo) + methodRegionInfo.hotSize + methodRegionInfo.coldSize; @@ -130,7 +139,7 @@ void SetupAndSprinkleBreakpoints( nativeCodeVersion.SetGCCoverageInfo(gcCover); } -void SetupAndSprinkleBreakpointsForJittedMethod(NativeCodeVersion nativeCodeVersion, +void SetupAndSprinkleBreakpointsForMethod(NativeCodeVersion nativeCodeVersion, PCODE codeStart ) { @@ -151,12 +160,12 @@ void SetupAndSprinkleBreakpointsForJittedMethod(NativeCodeVersion SetupAndSprinkleBreakpoints(nativeCodeVersion, &codeInfo, methodRegionInfo, - FALSE + ExecutionManager::IsReadyToRunCode(codeStart) ); } /****************************************************************************/ -/* called when a method is first jitted when GCStress level 4 or 8 is on */ +/* called before publishing a method when GCStress level 4 or 8 is on */ void SetupGcCoverage(NativeCodeVersion nativeCodeVersion, BYTE* methodStartPtr) { @@ -192,7 +201,7 @@ void SetupGcCoverage(NativeCodeVersion nativeCodeVersion, BYTE* methodStartPtr) } PCODE codeStart = (PCODE) methodStartPtr; - SetupAndSprinkleBreakpointsForJittedMethod(nativeCodeVersion, codeStart); + SetupAndSprinkleBreakpointsForMethod(nativeCodeVersion, codeStart); } // There are some code path in DoGcStress to return without doing a GC but @@ -373,11 +382,19 @@ static PBYTE getTargetOfCall(PBYTE instrPtr, PCONTEXT regs, PBYTE* nextInstr) { // There is a call target instruction, try to find the MethodDesc for where target points to. // Returns nullptr if it can't find it. -static MethodDesc* getTargetMethodDesc(PCODE target) +static MethodDesc* getTargetMethodDesc(PCODE target, PCONTEXT regs) { MethodDesc* targetMD = ExecutionManager::GetCodeMethodDesc(target); if (targetMD != nullptr) { + // Interior targets such as callfinally do not follow the parent's return convention + // and can return into a no-GC region. + EECodeInfo targetCodeInfo(target); + if (targetCodeInfo.IsValid() && targetCodeInfo.GetRelOffset() != 0) + { + return nullptr; + } + // It is JIT/NGened call. return targetMD; } @@ -395,6 +412,19 @@ static MethodDesc* getTargetMethodDesc(PCODE target) // It is a VSD stub manager. DispatchToken token(VirtualCallStubManager::GetTokenFromStubQuick(pVSDStubManager, target, stubKind)); _ASSERTE(token.IsValid()); + if (token.IsThisToken()) + { + // Class tokens encode only a slot. The receiver is available in ECX at the call, + // but not while sprinkling breakpoints, and null receivers must fault in the stub. + if (regs == nullptr || regs->Ecx == 0) + { + return nullptr; + } + + Object* receiver = reinterpret_cast(regs->Ecx); + return VirtualCallStubManager::GetRepresentativeMethodDescFromToken(token, receiver->GetMethodTable()); + } + return VirtualCallStubManager::GetInterfaceMethodDescFromToken(token); } } @@ -486,7 +516,8 @@ void GCCoverageInfo::SprinkleBreakpoints( if (fZapped) { DWORD oldProtect; - ClrVirtualProtect(codeStart, codeSize, PAGE_EXECUTE_READWRITE, &oldProtect); + if (!ClrVirtualProtect(codeStart, codeSize, PAGE_EXECUTE_READWRITE, &oldProtect)) + EEPOLICY_HANDLE_FATAL_ERROR_WITH_MESSAGE(COR_E_EXECUTIONENGINE, W("Failed to change code protection for GC stress")); writeableOffset = 0; } else @@ -559,7 +590,7 @@ void GCCoverageInfo::SprinkleBreakpoints( if (target != 0) { - targetMD = getTargetMethodDesc((PCODE)target); + targetMD = getTargetMethodDesc((PCODE)target, nullptr); } } break; @@ -813,7 +844,7 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) PBYTE target = getTargetOfCall((BYTE*) instrPtr, regs, (BYTE**)&nextInstr); if (target != 0) { - ExecutableWriterHolder nextInstrWriterHolder(nextInstr, sizeof(DWORD)); + GCStressCodeWriter nextInstrWriterHolder(nextInstr, sizeof(DWORD)); if (!pThread->PreemptiveGCDisabled()) { // We are in preemptive mode in JITTed code. This implies that we are into IL stub @@ -822,7 +853,7 @@ void DoGcStress (PCONTEXT regs, NativeCodeVersion nativeCodeVersion) } else { - MethodDesc* targetMD = getTargetMethodDesc((PCODE)target); + MethodDesc* targetMD = getTargetMethodDesc((PCODE)target, regs); if (targetMD != 0) { @@ -956,7 +987,7 @@ void replaceSafePointInstructionWithGcStressInstr(GcInfoDecoder* decoder, UINT32 // The instruction about to be replaced cannot already be a gcstress instruction _ASSERTE(!IsGcCoverageInterruptInstruction(instrPtr)); - ExecutableWriterHolder instrPtrWriterHolder(instrPtr, sizeof(DWORD)); + GCStressCodeWriter instrPtrWriterHolder(instrPtr, sizeof(DWORD)); #if defined(TARGET_ARM) size_t instrLen = GetARMInstructionLength(instrPtr); @@ -1031,7 +1062,7 @@ bool replaceInterruptibleRangesWithGcStressInstr (UINT32 startOffset, UINT32 sto // Need to do two iterations if interruptible range spans across hot & cold region while(acrossHotRegion--) { - ExecutableWriterHolder instrPtrWriterHolder(rangeStart, rangeStop - rangeStart); + GCStressCodeWriter instrPtrWriterHolder(rangeStart, rangeStop - rangeStart); PBYTE instrPtrRW = instrPtrWriterHolder.GetRW(); PBYTE rangeStopRW = instrPtrRW + (rangeStop - rangeStart); while(instrPtrRW < rangeStopRW) @@ -1099,11 +1130,13 @@ void GCCoverageInfo::SprinkleBreakpoints( if (fZapped) { DWORD oldProtect; - ClrVirtualProtect((BYTE*)methodRegion.hotStartAddress, methodRegion.hotSize, PAGE_EXECUTE_READWRITE, &oldProtect); + if (!ClrVirtualProtect((BYTE*)methodRegion.hotStartAddress, methodRegion.hotSize, PAGE_EXECUTE_READWRITE, &oldProtect)) + EEPOLICY_HANDLE_FATAL_ERROR_WITH_MESSAGE(COR_E_EXECUTIONENGINE, W("Failed to change code protection for GC stress")); if (methodRegion.coldSize > 0) { - ClrVirtualProtect((BYTE*)methodRegion.coldStartAddress, methodRegion.coldSize, PAGE_EXECUTE_READWRITE, &oldProtect); + if (!ClrVirtualProtect((BYTE*)methodRegion.coldStartAddress, methodRegion.coldSize, PAGE_EXECUTE_READWRITE, &oldProtect)) + EEPOLICY_HANDLE_FATAL_ERROR_WITH_MESSAGE(COR_E_EXECUTIONENGINE, W("Failed to change code protection for GC stress")); } } @@ -1258,7 +1291,7 @@ bool IsGcCoverageInterrupt(LPVOID ip) void RemoveGcCoverageInterrupt(TADDR instrPtr, BYTE * savedInstrPtr, GCCoverageInfo* gcCover, DWORD offset) { - ExecutableWriterHolder instrPtrWriterHolder((void*)instrPtr, 4); + GCStressCodeWriter instrPtrWriterHolder((BYTE*)instrPtr, 4); #ifdef TARGET_ARM if (GetARMInstructionLength(savedInstrPtr) == 2) *(WORD *)instrPtrWriterHolder.GetRW() = *(WORD *)savedInstrPtr; diff --git a/src/coreclr/vm/gccover.h b/src/coreclr/vm/gccover.h index 3310c3d8bc9bce..6fe2a94b3eec3a 100644 --- a/src/coreclr/vm/gccover.h +++ b/src/coreclr/vm/gccover.h @@ -8,6 +8,19 @@ #ifdef HAVE_GCCOVER +#ifndef DACCESS_COMPILE +// R2R image pages are not owned by the JIT executable allocator. +class GCStressCodeWriter +{ + ExecutableWriterHolderNoLog m_writer; + BYTE* m_addressRW; + +public: + GCStressCodeWriter(BYTE* address, size_t size); + BYTE* GetRW() const { return m_addressRW; } +}; +#endif // !DACCESS_COMPILE + /****************************************************************************/ /* GCCOverageInfo holds the state of which instructions have been visited by a GC and which ones have not */ diff --git a/src/coreclr/vm/jitinterface.cpp b/src/coreclr/vm/jitinterface.cpp index 5c3388339b4d50..c7e493b793cc1b 100644 --- a/src/coreclr/vm/jitinterface.cpp +++ b/src/coreclr/vm/jitinterface.cpp @@ -15597,6 +15597,15 @@ NativeCodeVersion EECodeInfo::GetNativeCodeVersion() const return NativeCodeVersion(); } +#if defined(HAVE_GCCOVER) && defined(FEATURE_READYTORUN) + // R2R code is the default version, even when its entry point was registered + // by a different instantiation or by diagnostic enumeration before publication. + if (m_pJM == ExecutionManager::GetReadyToRunJitManager()) + { + return NativeCodeVersion(pMD); + } +#endif // HAVE_GCCOVER && FEATURE_READYTORUN + #ifdef FEATURE_CODE_VERSIONING if (pMD->IsVersionable()) { diff --git a/src/coreclr/vm/method.hpp b/src/coreclr/vm/method.hpp index 1f860e4c8cd3c3..6fb97d70e8d4bf 100644 --- a/src/coreclr/vm/method.hpp +++ b/src/coreclr/vm/method.hpp @@ -2377,6 +2377,9 @@ class MethodDesc public: static void Init(); +#ifdef FEATURE_READYTORUN + static void SetupGcCoverageForPrecompiledCode(PCODE pCode); +#endif // FEATURE_READYTORUN #endif friend struct ::cdac_data; diff --git a/src/coreclr/vm/prestub.cpp b/src/coreclr/vm/prestub.cpp index 81bbe0c06f1587..1c954319292a53 100644 --- a/src/coreclr/vm/prestub.cpp +++ b/src/coreclr/vm/prestub.cpp @@ -65,6 +65,26 @@ void MethodDesc::Init() m_GCCoverCrst.Init(CrstGCCover); } +#ifdef FEATURE_READYTORUN +void MethodDesc::SetupGcCoverageForPrecompiledCode(PCODE pCode) +{ + STANDARD_VM_CONTRACT; + + // Image-backed code may not be patchable under Apple's write/execute restrictions. +#ifndef HOST_APPLE + if (GCStress::IsEnabled()) + { + CrstHolder gcCoverLock(&m_GCCoverCrst); + EECodeInfo codeInfo(pCode); + _ASSERTE(codeInfo.IsValid()); + + // Shared R2R code must use the same MethodDesc as the interrupt handler. + SetupGcCoverage(NativeCodeVersion(codeInfo.GetMethodDesc()), (BYTE*)pCode); + } +#endif // !HOST_APPLE +} +#endif // FEATURE_READYTORUN + #endif #define LOG_USING_R2R_CODE(method) LOG((LF_ZAP, LL_INFO10000, \ diff --git a/src/coreclr/vm/readytoruninfo.cpp b/src/coreclr/vm/readytoruninfo.cpp index b0d0d865506f7a..10a8c24394c15f 100644 --- a/src/coreclr/vm/readytoruninfo.cpp +++ b/src/coreclr/vm/readytoruninfo.cpp @@ -1276,7 +1276,12 @@ void ReadyToRunInfo::RegisterResumptionStub(PCODE stubEntryPoint) // Use the entry point hashtable to check if another thread already registered a MethodDesc if (m_pCompositeInfo->GetMethodDescForEntryPointInNativeImage(stubEntryPoint) != NULL) + { +#ifdef HAVE_GCCOVER + MethodDesc::SetupGcCoverageForPrecompiledCode(stubEntryPoint); +#endif // HAVE_GCCOVER return; + } MethodDesc* pStubMD = pStubCache->CreateR2RBackedILStub( pLoaderAllocator, @@ -1300,6 +1305,9 @@ void ReadyToRunInfo::RegisterResumptionStub(PCODE stubEntryPoint) { amTracker.SuppressRelease(); } +#ifdef HAVE_GCCOVER + MethodDesc::SetupGcCoverageForPrecompiledCode(stubEntryPoint); +#endif // HAVE_GCCOVER } PCODE ReadyToRunInfo::GetEntryPoint(MethodDesc * pMD, PrepareCodeConfig* pConfig, BOOL fFixups) @@ -1444,6 +1452,11 @@ PCODE ReadyToRunInfo::GetEntryPoint(MethodDesc * pMD, PrepareCodeConfig* pConfig #endif #endif +#ifdef HAVE_GCCOVER + // Instrument before any caller can publish or execute the entry point. + MethodDesc::SetupGcCoverageForPrecompiledCode(pEntryPoint); +#endif // HAVE_GCCOVER + #ifdef PROFILING_SUPPORTED { BEGIN_PROFILER_CALLBACK(CORProfilerTrackCacheSearches()); diff --git a/src/coreclr/vm/threadsuspend.cpp b/src/coreclr/vm/threadsuspend.cpp index b1fdd4a2ff8231..ff31dddd81d069 100644 --- a/src/coreclr/vm/threadsuspend.cpp +++ b/src/coreclr/vm/threadsuspend.cpp @@ -18,6 +18,7 @@ #include #include "exinfo.h" +#include "gccover.h" #define HIJACK_NONINTERRUPTIBLE_THREADS @@ -3449,7 +3450,7 @@ void Thread::CommitGCStressInstructionUpdate() assert(pbDestCode != NULL); assert(pbSrcCode != NULL); - ExecutableWriterHolder destCodeWriterHolder(pbDestCode, sizeof(DWORD)); + GCStressCodeWriter destCodeWriterHolder(pbDestCode, sizeof(DWORD)); #if defined(TARGET_X86) || defined(TARGET_AMD64) diff --git a/src/tests/readytorun/GCStress/GCStress.cs b/src/tests/readytorun/GCStress/GCStress.cs new file mode 100644 index 00000000000000..071dc5f37aa92a --- /dev/null +++ b/src/tests/readytorun/GCStress/GCStress.cs @@ -0,0 +1,175 @@ +// 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 GCStress +{ + private static int s_finallyValue; + + [Theory] + [InlineData(false)] + [InlineData(true)] + public static void TestEntryPoint(bool generic) + { + object value = new object(); + GC.CollectionCount(0); + int collections; + object result = generic ? R2RGCStress(value, out collections) : R2RGCStress(value, out collections); + + Console.WriteLine($"R2R GC stress (generic: {generic}): {collections} collections"); + Assert.Same(value, result); + Assert.True(collections > 0, "R2R instruction stress did not collect."); + + if (generic) + { + string other = "shared generic code"; + Assert.Same(other, R2RGCStress(other, out _)); + } + } + + [Fact] + public static void TestFinally() + { + object value = new object(); + Assert.Same(value, R2RGCStressFinally(value)); + Assert.Equal(42, s_finallyValue); + Console.WriteLine("R2R GC stress finally: passed"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static object R2RGCStressFinally(object value) + { + try + { + Poll(); + return value; + } + finally + { + // Nested handlers prevent finally cloning from eliminating the callfinally. + try + { + Poll(); + } + finally + { + // Leave a non-reference in EAX, unlike the parent method's return value. + FinallyReturnNonGcValue(); + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static int FinallyReturnNonGcValue() + { + s_finallyValue = 42; + return 42; + } + + public enum DispatchKind + { + Class, + Interface, + OpenDelegate + } + + [Theory] + [InlineData(DispatchKind.Class)] + [InlineData(DispatchKind.Interface)] + [InlineData(DispatchKind.OpenDelegate)] + public static void TestVirtualCalls(DispatchKind kind) + { + VirtualCallTarget target = new DerivedVirtualCallTarget(); + Func callback = typeof(VirtualCallTarget) + .GetMethod(nameof(VirtualCallTarget.GetValue)) + .CreateDelegate>(); + WarmupVirtualCalls(target, target, callback); + + for (int i = 0; i < 10; i++) + { + int[] result = R2RGCStressVirtualCall(target, target, callback, kind); + Assert.Equal(42, result[0]); + } + + Assert.Throws(() => R2RGCStressVirtualCall(null, null, callback, kind)); + Console.WriteLine($"R2R GC stress virtual calls ({kind}): passed"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void WarmupVirtualCalls( + VirtualCallTarget target, IVirtualCallTarget interfaceTarget, Func callback) + { + // Resolve the shared R2R imports before the stressed caller consumes its call breakpoints. + target.GetValue(); + interfaceTarget.GetValue(); + callback(target); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static int[] R2RGCStressVirtualCall( + VirtualCallTarget target, IVirtualCallTarget interfaceTarget, + Func callback, DispatchKind kind) + { + int[] result = kind switch + { + DispatchKind.Class => target.GetValue(), + DispatchKind.Interface => interfaceTarget.GetValue(), + DispatchKind.OpenDelegate => callback(target), + _ => throw new ArgumentOutOfRangeException(nameof(kind)) + }; + + Poll(); + return result; + } + + private interface IVirtualCallTarget + { + int[] GetValue(); + } + + private class VirtualCallTarget : IVirtualCallTarget + { + [MethodImpl(MethodImplOptions.NoInlining)] + public virtual int[] GetValue() => new[] { -1 }; + } + + private class DerivedVirtualCallTarget : VirtualCallTarget + { + [MethodImpl(MethodImplOptions.NoInlining)] + public override int[] GetValue() => new[] { 42 }; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static object R2RGCStress(object value, out int collections) + { + Poll(); + int before = GC.CollectionCount(0); + for (int i = 0; i < 100; i++) + { + Poll(); + } + + collections = GC.CollectionCount(0) - before; + return value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static T R2RGCStress(T value, out int collections) where T : class + { + Poll(); + int before = GC.CollectionCount(0); + for (int i = 0; i < 100; i++) + { + Poll(); + } + + collections = GC.CollectionCount(0) - before; + return value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void Poll() { } +} diff --git a/src/tests/readytorun/GCStress/GCStress.csproj b/src/tests/readytorun/GCStress/GCStress.csproj new file mode 100644 index 00000000000000..5242378e52cae6 --- /dev/null +++ b/src/tests/readytorun/GCStress/GCStress.csproj @@ -0,0 +1,20 @@ + + + true + true + true + + true + + + + + + + + + + + + +