Skip to content

Commit bb952db

Browse files
github-actions[bot]Koundinya Veluri
andauthored
Fix frequent FuncEval abort upon hitting a breakpoint in an ASP.NET Core web app (#58218)
- `AssemblySpecBindingCache` uses a cooperative-GC-mode data structure for the cache and operates on the cache from inside a lock taken in preemptive-GC-mode - So when the cache is being used, cooperative-GC-mode is entered while holding the lock, which can in turn suspend for the debugger. Then a FuncEval that also happens to operate on the cache would deadlock on acquiring the lock and would have to be aborted. - This seems to be happening very frequently when hitting an early breakpoint in a default new ASP.NET Core web app in .NET 6 when hot reload is enabled - Fixed by using the same solution that was used for the slot backpatching lock. When cooperative-GC-mode would be entered inside the lock, a different lock holder based on `CrstAndForbidSuspendForDebuggerHolder` is used, which prevents the thread from suspending for the debugger while the lock is held. The thread would instead suspend for the debugger after leaving the forbid region after releasing the lock. Co-authored-by: Koundinya Veluri <kouvel@microsoft.com>
1 parent 824368b commit bb952db

3 files changed

Lines changed: 40 additions & 8 deletions

File tree

src/coreclr/vm/appdomain.cpp

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3709,7 +3709,9 @@ BOOL AppDomain::AddFileToCache(AssemblySpec* pSpec, PEAssembly *pFile, BOOL fAll
37093709
}
37103710
CONTRACTL_END;
37113711

3712-
CrstHolder holder(&m_DomainCacheCrst);
3712+
GCX_PREEMP();
3713+
DomainCacheCrstHolderForGCCoop holder(this);
3714+
37133715
// !!! suppress exceptions
37143716
if(!m_AssemblyCache.StoreFile(pSpec, pFile) && !fAllowFailure)
37153717
{
@@ -3739,7 +3741,9 @@ BOOL AppDomain::AddAssemblyToCache(AssemblySpec* pSpec, DomainAssembly *pAssembl
37393741
}
37403742
CONTRACTL_END;
37413743

3742-
CrstHolder holder(&m_DomainCacheCrst);
3744+
GCX_PREEMP();
3745+
DomainCacheCrstHolderForGCCoop holder(this);
3746+
37433747
// !!! suppress exceptions
37443748
BOOL bRetVal = m_AssemblyCache.StoreAssembly(pSpec, pAssembly);
37453749
return bRetVal;
@@ -3760,7 +3764,9 @@ BOOL AppDomain::AddExceptionToCache(AssemblySpec* pSpec, Exception *ex)
37603764
if (ex->IsTransient())
37613765
return TRUE;
37623766

3763-
CrstHolder holder(&m_DomainCacheCrst);
3767+
GCX_PREEMP();
3768+
DomainCacheCrstHolderForGCCoop holder(this);
3769+
37643770
// !!! suppress exceptions
37653771
return m_AssemblyCache.StoreException(pSpec, ex);
37663772
}
@@ -3778,7 +3784,7 @@ void AppDomain::AddUnmanagedImageToCache(LPCWSTR libraryName, NATIVE_LIBRARY_HAN
37783784
}
37793785
CONTRACTL_END;
37803786

3781-
CrstHolder lock(&m_DomainCacheCrst);
3787+
DomainCacheCrstHolderForGCPreemp lock(this);
37823788

37833789
const UnmanagedImageCacheEntry *existingEntry = m_unmanagedCache.LookupPtr(libraryName);
37843790
if (existingEntry != NULL)
@@ -3808,7 +3814,8 @@ NATIVE_LIBRARY_HANDLE AppDomain::FindUnmanagedImageInCache(LPCWSTR libraryName)
38083814
}
38093815
CONTRACT_END;
38103816

3811-
CrstHolder lock(&m_DomainCacheCrst);
3817+
DomainCacheCrstHolderForGCPreemp lock(this);
3818+
38123819
const UnmanagedImageCacheEntry *existingEntry = m_unmanagedCache.LookupPtr(libraryName);
38133820
if (existingEntry == NULL)
38143821
RETURN NULL;
@@ -3850,7 +3857,8 @@ BOOL AppDomain::RemoveAssemblyFromCache(DomainAssembly* pAssembly)
38503857
}
38513858
CONTRACTL_END;
38523859

3853-
CrstHolder holder(&m_DomainCacheCrst);
3860+
GCX_PREEMP();
3861+
DomainCacheCrstHolderForGCCoop holder(this);
38543862

38553863
return m_AssemblyCache.RemoveAssembly(pAssembly);
38563864
}

src/coreclr/vm/appdomain.hpp

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,6 +1171,30 @@ class BaseDomain
11711171
};
11721172
friend class LockHolder;
11731173

1174+
// To be used when the thread will remain in preemptive GC mode while holding the lock
1175+
class DomainCacheCrstHolderForGCPreemp : private CrstHolder
1176+
{
1177+
public:
1178+
DomainCacheCrstHolderForGCPreemp(BaseDomain *pD)
1179+
: CrstHolder(&pD->m_DomainCacheCrst)
1180+
{
1181+
WRAPPER_NO_CONTRACT;
1182+
}
1183+
};
1184+
1185+
// To be used when the thread may enter cooperative GC mode while holding the lock. The thread enters a
1186+
// forbid-suspend-for-debugger region along with acquiring the lock, such that it would not suspend for the debugger while
1187+
// holding the lock, as that may otherwise cause a FuncEval to deadlock when trying to acquire the lock.
1188+
class DomainCacheCrstHolderForGCCoop : private CrstAndForbidSuspendForDebuggerHolder
1189+
{
1190+
public:
1191+
DomainCacheCrstHolderForGCCoop(BaseDomain *pD)
1192+
: CrstAndForbidSuspendForDebuggerHolder(&pD->m_DomainCacheCrst)
1193+
{
1194+
WRAPPER_NO_CONTRACT;
1195+
}
1196+
};
1197+
11741198
class DomainLocalBlockLockHolder : public CrstHolder
11751199
{
11761200
public:

src/coreclr/vm/crst.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -789,8 +789,8 @@ CrstBase::CrstAndForbidSuspendForDebuggerHolder::CrstAndForbidSuspendForDebugger
789789
// Reentrant locks are currently not supported
790790
_ASSERTE((pCrst->m_dwFlags & CRST_REENTRANCY) == 0);
791791

792-
Thread *pThread = GetThread();
793-
if (pThread->IsInForbidSuspendForDebuggerRegion())
792+
Thread *pThread = GetThreadNULLOk();
793+
if (pThread == nullptr || pThread->IsInForbidSuspendForDebuggerRegion())
794794
{
795795
AcquireLock(pCrst);
796796
return;

0 commit comments

Comments
 (0)