Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit c5d3d75

Browse files
authored
[3.1] Fail FuncEval if slot backpatching lock is held by any thread (#28006)
- In many cases cooperative GC mode is entered after acquiring the slot backpatching lock and the thread may block for debugger suspension while holding the lock. A FuncEval may time out on entering the lock if for example it calls a virtual or interface method for the first time. Failing the FuncEval when the lock is held enables the debugger to fall back to other options for expression evaluation. - Also added polls for debugger suspension before acquiring the slot backpatching lock on background threads that often operate in preemptive GC mode. A common case is when the debugger breaks while the tiering delay timer is active, the timer ticks shortly afterwards (after debugger suspension completes) and if a thread pool thread is already available, the background thread would block while holding the lock. The poll checks for debugger suspension and pulses the GC mode to block before acquiring the lock. Risks: - The fix is only a heuristic and lessens the problem when it is detected that the lock is held by some thread. Since the lock is acquired in preemptive GC mode, it is still possible that after the check at the start of a FuncEval, another thread acquires the lock and the FuncEval may time out. The polling makes it less likely for the lock to be taken by background tiering work, for example if a FuncEval starts while rejitting a method. - The expression evaluation experience may be worse when it is detected that the lock is held, and may still happen from unfortunate timing - Low risk for the change itself Port of dotnet/runtime#2380 Fix for dotnet/runtime#1537
1 parent b4b4098 commit c5d3d75

File tree

4 files changed

+89
-3
lines changed

4 files changed

+89
-3
lines changed

src/debug/ee/debugger.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15313,6 +15313,15 @@ HRESULT Debugger::FuncEvalSetup(DebuggerIPCE_FuncEvalInfo *pEvalInfo,
1531315313
return CORDBG_E_FUNC_EVAL_BAD_START_POINT;
1531415314
}
1531515315

15316+
if (MethodDescBackpatchInfoTracker::IsLockedByAnyThread())
15317+
{
15318+
// A thread may have suspended for the debugger while holding the slot backpatching lock while trying to enter
15319+
// cooperative GC mode. If the FuncEval calls a method that is eligible for slot backpatching (virtual or interface
15320+
// methods that are eligible for tiering), the FuncEval may deadlock on trying to acquire the same lock. Fail the
15321+
// FuncEval to avoid the issue.
15322+
return CORDBG_E_FUNC_EVAL_BAD_START_POINT;
15323+
}
15324+
1531615325
// Create a DebuggerEval to hold info about this eval while its in progress. Constructor copies the thread's
1531715326
// CONTEXT.
1531815327
DebuggerEval *pDE = new (interopsafe, nothrow) DebuggerEval(filterContext, pEvalInfo, fInException);

src/vm/methoddescbackpatchinfo.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ void EntryPointSlots::Backpatch_Locked(TADDR slot, SlotType slotType, PCODE entr
6666
// MethodDescBackpatchInfoTracker
6767

6868
CrstStatic MethodDescBackpatchInfoTracker::s_lock;
69+
bool MethodDescBackpatchInfoTracker::s_isLocked = false;
6970

7071
#ifndef DACCESS_COMPILE
7172

@@ -140,4 +141,30 @@ bool MethodDescBackpatchInfoTracker::MayHaveEntryPointSlotsToBackpatch(PTR_Metho
140141

141142
#endif // _DEBUG
142143

144+
#ifndef DACCESS_COMPILE
145+
void MethodDescBackpatchInfoTracker::PollForDebuggerSuspension()
146+
{
147+
CONTRACTL
148+
{
149+
NOTHROW;
150+
GC_TRIGGERS;
151+
MODE_PREEMPTIVE;
152+
}
153+
CONTRACTL_END;
154+
155+
_ASSERTE(!IsLockedByCurrentThread());
156+
157+
// If suspension is pending for the debugger, pulse the GC mode to suspend the thread here. Following this call, typically
158+
// the lock is acquired and the GC mode is changed, and suspending there would cause FuncEvals to fail (see
159+
// Debugger::FuncEvalSetup() at the reference to IsLockOwnedByAnyThread()). Since this thread is in preemptive mode, the
160+
// debugger may think it's already suspended and it would be unfortunate to suspend the thread with the lock held.
161+
Thread *thread = GetThread();
162+
_ASSERTE(thread != nullptr);
163+
if (thread->HasThreadState(Thread::TS_DebugSuspendPending))
164+
{
165+
GCX_COOP();
166+
}
167+
}
168+
#endif
169+
143170
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

src/vm/methoddescbackpatchinfo.h

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class MethodDescBackpatchInfoTracker
6666
{
6767
private:
6868
static CrstStatic s_lock;
69+
static bool s_isLocked;
6970

7071
class BackpatchInfoTrackerHashTraits : public NoRemoveDefaultCrossLoaderAllocatorHashTraits<MethodDesc *, UINT_PTR>
7172
{
@@ -93,9 +94,23 @@ class MethodDescBackpatchInfoTracker
9394
static bool IsLockedByCurrentThread();
9495
#endif
9596

97+
#ifndef DACCESS_COMPILE
98+
public:
99+
static bool IsLockedByAnyThread()
100+
{
101+
LIMITED_METHOD_CONTRACT;
102+
return VolatileLoadWithoutBarrier(&s_isLocked);
103+
}
104+
105+
static void PollForDebuggerSuspension();
106+
#endif
107+
96108
public:
97109
class ConditionalLockHolder : CrstHolderWithState
98110
{
111+
private:
112+
bool m_isLocked;
113+
99114
public:
100115
ConditionalLockHolder(bool acquireLock = true)
101116
: CrstHolderWithState(
@@ -104,9 +119,34 @@ class MethodDescBackpatchInfoTracker
104119
#else
105120
nullptr
106121
#endif
107-
)
122+
),
123+
m_isLocked(false)
124+
{
125+
WRAPPER_NO_CONTRACT;
126+
127+
#ifndef DACCESS_COMPILE
128+
if (acquireLock)
129+
{
130+
_ASSERTE(IsLockedByCurrentThread());
131+
_ASSERTE(!s_isLocked);
132+
m_isLocked = true;
133+
s_isLocked = true;
134+
}
135+
#endif
136+
}
137+
138+
~ConditionalLockHolder()
108139
{
109-
LIMITED_METHOD_CONTRACT;
140+
WRAPPER_NO_CONTRACT;
141+
142+
#ifndef DACCESS_COMPILE
143+
if (m_isLocked)
144+
{
145+
_ASSERTE(IsLockedByCurrentThread());
146+
_ASSERTE(s_isLocked);
147+
s_isLocked = false;
148+
}
149+
#endif
110150
}
111151
};
112152

src/vm/tieredcompilation.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,13 @@ void TieredCompilationManager::ResumeCountingCalls(MethodDesc* pMethodDesc)
522522
{
523523
WRAPPER_NO_CONTRACT;
524524
_ASSERTE(pMethodDesc != nullptr);
525-
MethodDescBackpatchInfoTracker::ConditionalLockHolder lockHolder(pMethodDesc->MayHaveEntryPointSlotsToBackpatch());
525+
526+
bool mayHaveEntryPointSlotsToBackpatch = pMethodDesc->MayHaveEntryPointSlotsToBackpatch();
527+
if (mayHaveEntryPointSlotsToBackpatch)
528+
{
529+
MethodDescBackpatchInfoTracker::PollForDebuggerSuspension();
530+
}
531+
MethodDescBackpatchInfoTracker::ConditionalLockHolder lockHolder(mayHaveEntryPointSlotsToBackpatch);
526532

527533
EX_TRY
528534
{
@@ -746,6 +752,10 @@ void TieredCompilationManager::ActivateCodeVersion(NativeCodeVersion nativeCodeV
746752
ILCodeVersion ilParent;
747753
HRESULT hr = S_OK;
748754
bool mayHaveEntryPointSlotsToBackpatch = pMethod->MayHaveEntryPointSlotsToBackpatch();
755+
if (mayHaveEntryPointSlotsToBackpatch)
756+
{
757+
MethodDescBackpatchInfoTracker::PollForDebuggerSuspension();
758+
}
749759
MethodDescBackpatchInfoTracker::ConditionalLockHolder lockHolder(mayHaveEntryPointSlotsToBackpatch);
750760

751761
{

0 commit comments

Comments
 (0)