Skip to content

Commit 6d98f6d

Browse files
authored
Don't suspend for debugger while holding the slot backpatching lock (#40060)
- Mostly reverted the previous workaround for the issue (commit fc06054) - Added a forbid-suspend-for-debugger region in preemptive GC mode - Added a crst holder type that acquires a lock and enters the forbid region - Where the slot backpatching lock would be taken where cooperative GC mode may be entered inside that lock, the new crst holder is used - When a suspend for debugger is requested, a thread in preemptive GC mode that is in the forbid region is considered not yet suspended and is synched later similarly to threads in cooperative GC mode Fixes #37278
1 parent b552749 commit 6d98f6d

17 files changed

+231
-115
lines changed

src/coreclr/src/debug/ee/debugger.cpp

-9
Original file line numberDiff line numberDiff line change
@@ -15242,15 +15242,6 @@ HRESULT Debugger::FuncEvalSetup(DebuggerIPCE_FuncEvalInfo *pEvalInfo,
1524215242
return CORDBG_E_FUNC_EVAL_BAD_START_POINT;
1524315243
}
1524415244

15245-
if (MethodDescBackpatchInfoTracker::IsLockOwnedByAnyThread())
15246-
{
15247-
// A thread may have suspended for the debugger while holding the slot backpatching lock while trying to enter
15248-
// cooperative GC mode. If the FuncEval calls a method that is eligible for slot backpatching (virtual or interface
15249-
// methods that are eligible for tiering), the FuncEval may deadlock on trying to acquire the same lock. Fail the
15250-
// FuncEval to avoid the issue.
15251-
return CORDBG_E_FUNC_EVAL_BAD_START_POINT;
15252-
}
15253-
1525415245
// Create a DebuggerEval to hold info about this eval while its in progress. Constructor copies the thread's
1525515246
// CONTEXT.
1525615247
DebuggerEval *pDE = new (interopsafe, nothrow) DebuggerEval(filterContext, pEvalInfo, fInException);

src/coreclr/src/vm/callcounting.cpp

+2-4
Original file line numberDiff line numberDiff line change
@@ -824,8 +824,7 @@ void CallCountingManager::CompleteCallCounting()
824824
{
825825
CodeVersionManager *codeVersionManager = appDomain->GetCodeVersionManager();
826826

827-
MethodDescBackpatchInfoTracker::PollForDebuggerSuspension();
828-
MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder;
827+
MethodDescBackpatchInfoTracker::ConditionalLockHolderForGCCoop slotBackpatchLockHolder;
829828

830829
// Backpatching entry point slots requires cooperative GC mode, see
831830
// MethodDescBackpatchInfoTracker::Backpatch_Locked(). The code version manager's table lock is an unsafe lock that
@@ -956,8 +955,7 @@ void CallCountingManager::StopAndDeleteAllCallCountingStubs()
956955
TieredCompilationManager *tieredCompilationManager = GetAppDomain()->GetTieredCompilationManager();
957956
bool scheduleTieringBackgroundWork = false;
958957
{
959-
MethodDescBackpatchInfoTracker::PollForDebuggerSuspension();
960-
MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder;
958+
MethodDescBackpatchInfoTracker::ConditionalLockHolderForGCCoop slotBackpatchLockHolder;
961959

962960
ThreadSuspend::SuspendEE(ThreadSuspend::SUSPEND_OTHER);
963961
struct AutoRestartEE

src/coreclr/src/vm/codeversion.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -1759,7 +1759,8 @@ PCODE CodeVersionManager::PublishVersionableCodeIfNecessary(
17591759
do
17601760
{
17611761
bool mayHaveEntryPointSlotsToBackpatch = doPublish && pMethodDesc->MayHaveEntryPointSlotsToBackpatch();
1762-
MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder(mayHaveEntryPointSlotsToBackpatch);
1762+
MethodDescBackpatchInfoTracker::ConditionalLockHolderForGCCoop slotBackpatchLockHolder(
1763+
mayHaveEntryPointSlotsToBackpatch);
17631764

17641765
// Try a faster check to see if we can avoid checking the currently active code version
17651766
// - For the default code version, if a profiler is attached it may be notified of JIT events and may request rejit

src/coreclr/src/vm/crst.cpp

+85
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,91 @@ BOOL CrstBase::IsSafeToTake()
772772

773773
#endif // _DEBUG
774774

775+
CrstBase::CrstAndForbidSuspendForDebuggerHolder::CrstAndForbidSuspendForDebuggerHolder(CrstBase *pCrst)
776+
: m_pCrst(pCrst), m_pThreadForExitingForbidRegion(nullptr)
777+
{
778+
CONTRACTL
779+
{
780+
NOTHROW;
781+
GC_TRIGGERS;
782+
MODE_PREEMPTIVE;
783+
}
784+
CONTRACTL_END;
785+
786+
if (pCrst == nullptr)
787+
{
788+
return;
789+
}
790+
791+
// Reentrant locks are currently not supported
792+
_ASSERTE((pCrst->m_dwFlags & CRST_REENTRANCY) == 0);
793+
794+
Thread *pThread = GetThread();
795+
_ASSERTE(pThread != nullptr);
796+
if (pThread->IsInForbidSuspendForDebuggerRegion())
797+
{
798+
AcquireLock(pCrst);
799+
return;
800+
}
801+
802+
while (true)
803+
{
804+
// Enter the forbid region and make that state change visible to other threads (memory barrier) before checking for the
805+
// TS_DebugSuspendPending state. The other interacting thread in SysStartSuspendForDebug(), sets TS_DebugSuspendPending
806+
// and makes that state change visible to other threads (memory barrier) before checking for whether this thread is in
807+
// the forbid region. This ensures that in race conditions where both threads update the respective state and make the
808+
// state change visible to other threads, at least one of those threads will see the state change made by the other
809+
// thread. If this thread sees the state change (TS_DebugSuspendPending), it will avoid entering the forbid region by
810+
// exiting the lock and pulsing the GC mode to try suspending for the debugger. If SysStartSuspendForDebug() sees the
811+
// state change (that this thread is in the forbid region), then it will flag this thread appropriately to sync for
812+
// suspend, and the debugger will later identify this thread as synced after this thread leaves the forbid region.
813+
//
814+
// The forbid region could be entered after acquiring the lock, but an additional memory barrier would be necessary. It
815+
// is entered before the lock just to make use of the implicit memory barrier from acquiring the lock. It is anyway a
816+
// prerequisite for entering a lock along with entering a forbid-suspend-for-debugger region, that the lock is not held
817+
// for too long such that the thread can suspend for the debugger in reasonable time.
818+
pThread->EnterForbidSuspendForDebuggerRegion();
819+
AcquireLock(pCrst); // implicit full memory barrier on all supported platforms
820+
821+
// This can be an opportunistic check (instead of a volatile load), because if the GC mode change below does not suspend
822+
// for the debugger (which is also possible with a volatile load), it will just loop around and try again if necessary
823+
if (!pThread->HasThreadStateOpportunistic(Thread::TS_DebugSuspendPending))
824+
{
825+
m_pThreadForExitingForbidRegion = pThread;
826+
return;
827+
}
828+
829+
// Cannot enter the forbid region when a suspend for the debugger is pending because there are likely to be subsequent
830+
// changes to the GC mode inside the lock, and this thread needs to suspend for the debugger in reasonable time. Exit
831+
// the lock and pulse the GC mode to suspend for the debugger.
832+
ReleaseLock(pCrst);
833+
pThread->ExitForbidSuspendForDebuggerRegion();
834+
GCX_COOP();
835+
}
836+
}
837+
838+
CrstBase::CrstAndForbidSuspendForDebuggerHolder::~CrstAndForbidSuspendForDebuggerHolder()
839+
{
840+
CONTRACTL
841+
{
842+
NOTHROW;
843+
GC_NOTRIGGER;
844+
MODE_PREEMPTIVE;
845+
}
846+
CONTRACTL_END;
847+
848+
if (m_pCrst == nullptr)
849+
{
850+
return;
851+
}
852+
853+
ReleaseLock(m_pCrst);
854+
if (m_pThreadForExitingForbidRegion != nullptr)
855+
{
856+
m_pThreadForExitingForbidRegion->ExitForbidSuspendForDebuggerRegion();
857+
}
858+
}
859+
775860
#endif // !DACCESS_COMPILE
776861

777862
#ifdef TEST_DATA_CONSISTENCY

src/coreclr/src/vm/crst.h

+19-7
Original file line numberDiff line numberDiff line change
@@ -362,13 +362,14 @@ friend class Crst;
362362
{
363363
m_dwFlags = 0;
364364
}
365+
365366
// ------------------------------- Holders ------------------------------
366-
public:
367-
//
368-
// CrstHolder is optimized for the common use that takes the lock in constructor
369-
// and releases it in destructor. Users that require all Holder features
370-
// can use CrstHolderWithState.
371-
//
367+
public:
368+
//
369+
// CrstHolder is optimized for the common use that takes the lock in constructor
370+
// and releases it in destructor. Users that require all Holder features
371+
// can use CrstHolderWithState.
372+
//
372373
class CrstHolder
373374
{
374375
CrstBase * m_pCrst;
@@ -397,11 +398,22 @@ friend class Crst;
397398
// Generally, it's better to use a regular CrstHolder, and then use the Release() / Acquire() methods on it.
398399
// This just exists to convert legacy OS Critical Section patterns over to holders.
399400
typedef DacHolder<CrstBase *, CrstBase::ReleaseLock, CrstBase::AcquireLock, 0, CompareDefault> UnsafeCrstInverseHolder;
401+
402+
class CrstAndForbidSuspendForDebuggerHolder
403+
{
404+
private:
405+
CrstBase *m_pCrst;
406+
Thread *m_pThreadForExitingForbidRegion;
407+
408+
public:
409+
CrstAndForbidSuspendForDebuggerHolder(CrstBase *pCrst);
410+
~CrstAndForbidSuspendForDebuggerHolder();
411+
};
400412
};
401413

402414
typedef CrstBase::CrstHolder CrstHolder;
403415
typedef CrstBase::CrstHolderWithState CrstHolderWithState;
404-
416+
typedef CrstBase::CrstAndForbidSuspendForDebuggerHolder CrstAndForbidSuspendForDebuggerHolder;
405417

406418
// The CRST.
407419
class Crst : public CrstBase

src/coreclr/src/vm/fptrstubs.cpp

+3-1
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,12 @@ PCODE FuncPtrStubs::GetFuncPtrStub(MethodDesc * pMD, PrecodeType type)
151151

152152
if (setTargetAfterAddingToHashTable)
153153
{
154+
GCX_PREEMP();
155+
154156
_ASSERTE(pMD->IsVersionableWithVtableSlotBackpatch());
155157

156158
PCODE temporaryEntryPoint = pMD->GetTemporaryEntryPoint();
157-
MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder;
159+
MethodDescBackpatchInfoTracker::ConditionalLockHolderForGCPreemp slotBackpatchLockHolder;
158160

159161
// Set the funcptr stub's entry point to the current entry point inside the lock and after the funcptr stub is exposed,
160162
// to synchronize with backpatching in MethodDesc::BackpatchEntryPointSlots()

src/coreclr/src/vm/method.cpp

+3-1
Original file line numberDiff line numberDiff line change
@@ -4903,8 +4903,10 @@ void MethodDesc::RecordAndBackpatchEntryPointSlot(
49034903
{
49044904
WRAPPER_NO_CONTRACT;
49054905

4906+
GCX_PREEMP();
4907+
49064908
LoaderAllocator *mdLoaderAllocator = GetLoaderAllocator();
4907-
MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder;
4909+
MethodDescBackpatchInfoTracker::ConditionalLockHolderForGCCoop slotBackpatchLockHolder;
49084910

49094911
RecordAndBackpatchEntryPointSlot_Locked(
49104912
mdLoaderAllocator,

src/coreclr/src/vm/methoddescbackpatchinfo.cpp

-27
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ void EntryPointSlots::Backpatch_Locked(TADDR slot, SlotType slotType, PCODE entr
6565
// MethodDescBackpatchInfoTracker
6666

6767
CrstStatic MethodDescBackpatchInfoTracker::s_lock;
68-
bool MethodDescBackpatchInfoTracker::s_isLocked = false;
6968

7069
#ifndef DACCESS_COMPILE
7170

@@ -123,30 +122,4 @@ bool MethodDescBackpatchInfoTracker::IsLockOwnedByCurrentThread()
123122
}
124123
#endif // _DEBUG
125124

126-
#ifndef DACCESS_COMPILE
127-
void MethodDescBackpatchInfoTracker::PollForDebuggerSuspension()
128-
{
129-
CONTRACTL
130-
{
131-
NOTHROW;
132-
GC_TRIGGERS;
133-
MODE_PREEMPTIVE;
134-
}
135-
CONTRACTL_END;
136-
137-
_ASSERTE(!IsLockOwnedByCurrentThread());
138-
139-
// If suspension is pending for the debugger, pulse the GC mode to suspend the thread here. Following this call, typically
140-
// the lock is acquired and the GC mode is changed, and suspending there would cause FuncEvals to fail (see
141-
// Debugger::FuncEvalSetup() at the reference to IsLockOwnedByAnyThread()). Since this thread is in preemptive mode, the
142-
// debugger may think it's already suspended and it would be unfortunate to suspend the thread with the lock held.
143-
Thread *thread = GetThread();
144-
_ASSERTE(thread != nullptr);
145-
if (thread->HasThreadState(Thread::TS_DebugSuspendPending))
146-
{
147-
GCX_COOP();
148-
}
149-
}
150-
#endif
151-
152125
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

src/coreclr/src/vm/methoddescbackpatchinfo.h

+29-38
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ class MethodDescBackpatchInfoTracker
6565
{
6666
private:
6767
static CrstStatic s_lock;
68-
static bool s_isLocked;
6968

7069
class BackpatchInfoTrackerHashTraits : public NoRemoveDefaultCrossLoaderAllocatorHashTraits<MethodDesc *, UINT_PTR>
7170
{
@@ -97,63 +96,55 @@ class MethodDescBackpatchInfoTracker
9796
static bool IsLockOwnedByCurrentThread();
9897
#endif
9998

100-
#ifndef DACCESS_COMPILE
10199
public:
102-
static bool IsLockOwnedByAnyThread()
100+
// To be used when the thread will remain in preemptive GC mode while holding the lock
101+
class ConditionalLockHolderForGCPreemp : private CrstHolderWithState
103102
{
104-
LIMITED_METHOD_CONTRACT;
105-
return VolatileLoadWithoutBarrier(&s_isLocked);
106-
}
107-
108-
static void PollForDebuggerSuspension();
109-
#endif
110-
111-
public:
112-
class ConditionalLockHolder : private CrstHolderWithState
113-
{
114-
private:
115-
bool m_isLocked;
116-
117103
public:
118-
ConditionalLockHolder(bool acquireLock = true)
104+
ConditionalLockHolderForGCPreemp(bool acquireLock = true)
119105
: CrstHolderWithState(
120106
#ifndef DACCESS_COMPILE
121107
acquireLock ? &s_lock : nullptr
122108
#else
123109
nullptr
124110
#endif
125-
),
126-
m_isLocked(false)
111+
)
127112
{
128-
WRAPPER_NO_CONTRACT;
129-
130-
#ifndef DACCESS_COMPILE
131-
if (acquireLock)
113+
CONTRACTL
132114
{
133-
_ASSERTE(IsLockOwnedByCurrentThread());
134-
_ASSERTE(!s_isLocked);
135-
m_isLocked = true;
136-
s_isLocked = true;
115+
NOTHROW;
116+
GC_NOTRIGGER;
117+
MODE_PREEMPTIVE;
137118
}
138-
#endif
119+
CONTRACTL_END;
139120
}
140121

141-
~ConditionalLockHolder()
142-
{
143-
WRAPPER_NO_CONTRACT;
122+
DISABLE_COPY(ConditionalLockHolderForGCPreemp);
123+
};
144124

145-
#ifndef DACCESS_COMPILE
146-
if (m_isLocked)
125+
#ifndef DACCESS_COMPILE
126+
public:
127+
// To be used when the thread may enter cooperative GC mode while holding the lock. The thread enters a
128+
// forbid-suspend-for-debugger region along with acquiring the lock, such that it would not suspend for the debugger while
129+
// holding the lock, as that may otherwise cause a FuncEval to deadlock when trying to acquire the lock.
130+
class ConditionalLockHolderForGCCoop : private CrstAndForbidSuspendForDebuggerHolder
131+
{
132+
public:
133+
ConditionalLockHolderForGCCoop(bool acquireLock = true)
134+
: CrstAndForbidSuspendForDebuggerHolder(acquireLock ? &s_lock : nullptr)
135+
{
136+
CONTRACTL
147137
{
148-
_ASSERTE(IsLockOwnedByCurrentThread());
149-
_ASSERTE(s_isLocked);
150-
s_isLocked = false;
138+
NOTHROW;
139+
GC_NOTRIGGER;
140+
MODE_PREEMPTIVE;
151141
}
152-
#endif
142+
CONTRACTL_END;
153143
}
154144

155-
DISABLE_COPY(ConditionalLockHolder);
145+
DISABLE_COPY(ConditionalLockHolderForGCCoop);
156146
};
147+
#endif
157148

158149
public:
159150
MethodDescBackpatchInfoTracker()

src/coreclr/src/vm/prestub.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ PCODE MethodDesc::DoBackpatch(MethodTable * pMT, MethodTable *pDispatchingMT, BO
9595

9696
// Only take the lock if the method is versionable with vtable slot backpatch, for recording slots and synchronizing with
9797
// backpatching slots
98-
MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder(isVersionableWithVtableSlotBackpatch);
98+
MethodDescBackpatchInfoTracker::ConditionalLockHolderForGCCoop slotBackpatchLockHolder(
99+
isVersionableWithVtableSlotBackpatch);
99100

100101
// Get the method entry point inside the lock above to synchronize with backpatching in
101102
// MethodDesc::BackpatchEntryPointSlots()

src/coreclr/src/vm/rejit.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -643,7 +643,7 @@ HRESULT ReJitManager::UpdateActiveILVersions(
643643
SHash<CodeActivationBatchTraits>::Iterator endIter = mgrToCodeActivationBatch.End();
644644

645645
{
646-
MethodDescBackpatchInfoTracker::ConditionalLockHolder slotBackpatchLockHolder;
646+
MethodDescBackpatchInfoTracker::ConditionalLockHolderForGCCoop slotBackpatchLockHolder;
647647

648648
for (SHash<CodeActivationBatchTraits>::Iterator iter = beginIter; iter != endIter; iter++)
649649
{

src/coreclr/src/vm/threads.cpp

+1
Original file line numberDiff line numberDiff line change
@@ -1606,6 +1606,7 @@ Thread::Thread()
16061606
m_DeserializationTracker = NULL;
16071607

16081608
m_currentPrepareCodeConfig = nullptr;
1609+
m_isInForbidSuspendForDebuggerRegion = false;
16091610

16101611
#ifdef _DEBUG
16111612
memset(dangerousObjRefs, 0, sizeof(dangerousObjRefs));

src/coreclr/src/vm/threads.h

+15
Original file line numberDiff line numberDiff line change
@@ -4750,6 +4750,21 @@ class Thread
47504750

47514751
private:
47524752
PrepareCodeConfig *m_currentPrepareCodeConfig;
4753+
4754+
#ifndef DACCESS_COMPILE
4755+
public:
4756+
bool IsInForbidSuspendForDebuggerRegion() const
4757+
{
4758+
LIMITED_METHOD_CONTRACT;
4759+
return m_isInForbidSuspendForDebuggerRegion;
4760+
}
4761+
4762+
void EnterForbidSuspendForDebuggerRegion();
4763+
void ExitForbidSuspendForDebuggerRegion();
4764+
#endif
4765+
4766+
private:
4767+
bool m_isInForbidSuspendForDebuggerRegion;
47534768
};
47544769

47554770
// End of class Thread

0 commit comments

Comments
 (0)