Avoid clearing thread local handles for already unloaded loader - #99998

Merged
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free
May 25, 2024
Merged

Avoid clearing thread local handles for already unloaded loader#99998
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free

Conversation

@alexey-zakharov

@alexey-zakharovalexey-zakharov commented Mar 20, 2024

Copy link
Copy Markdown
Contributor

Motivation:

Fix crash in ThreadLocalBlock::FreeTLM code on thread exit which happens during Assembly unloading when Assembly contains a class with a ThreadStatic variable.

Details:

There seem to be a race condition between LoaderAllocator cleanup during garbage collection and thread locals cleanup on thread exit. Racing stacks are the following:

ALC/LoaderAllocator cleanup

 coreclr.dll!EEToProfInterfaceImpl::ModuleUnloadStarted(unsigned __int64 moduleId) Line 3735	C++	Symbols loaded.
coreclr.dll!ProfControlBlock::DoProfilerCallbackHelper<int (__cdecl*)(ProfilerInfo *),long (__cdecl*)(EEToProfInterfaceImpl *,unsigned __int64),unsigned __int64>(ProfilerInfo * pProfilerInfo, int(*)(ProfilerInfo *) condition, HRESULT(*)(EEToProfInterfaceImpl *, unsigned __int64) callback, HRESULT * pHR, unsigned __int64 <args_0>) Line 284	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoOneProfilerIteration(ProfilerInfo *) Line 199	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::IterateProfilers(ProfilerCallbackType) Line 207	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoProfilerCallback(ProfilerCallbackType) Line 295	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::ModuleUnloadStarted(unsigned __int64) Line 691	C++	Symbols loaded.
coreclr.dll!Module::Destruct() Line 662	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ClassLoader::FreeModules() Line 1884	C++	Symbols loaded.
coreclr.dll!ClassLoader::~ClassLoader() Line 1946	C++	Symbols loaded.
coreclr.dll!Assembly::Terminate(int) Line 311	C++	Symbols loaded.
coreclr.dll!Assembly::~Assembly() Line 244	C++	Symbols loaded.
coreclr.dll!Assembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
coreclr.dll!DomainAssembly::~DomainAssembly() Line 91	C++	Symbols loaded.
coreclr.dll!DomainAssembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
>	coreclr.dll!LoaderAllocator::GCLoaderAllocators(LoaderAllocator * pOriginalLoaderAllocator) Line 570	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 702	C++	Symbols loaded.
coreclr.dll!LoaderAllocator_Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 718	C++	Symbols loaded.
System.Private.CoreLib.dll!00007ffa9c4308f9()	Unknown	No symbols loaded.
coreclr.dll!FastCallFinalizeWorker() Line 26	Unknown	Symbols loaded.
coreclr.dll!MethodTable::CallFinalizer(Object * obj) Line 4908	C++	Symbols loaded.
[Inline Frame] coreclr.dll!CallFinalizer(Object *) Line 75	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizeAllObjects() Line 110	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadWorker(void * args) Line 354	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_DispatchInner(ManagedThreadCallState *) Line 7222	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchMiddle(ManagedThreadCallState * pCallState) Line 7266	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchOuter(ManagedThreadCallState * pCallState) Line 7425	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_NoADTransition(void(*)(void *)) Line 7494	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase::FinalizerBase(void(*)(void *)) Line 7513	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadStart(void * args) Line 403	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

Thread exit with threadlocal cleanup

>	coreclr.dll!LoaderAllocator::SetHandleValue(unsigned __int64 handle, Object *) Line 992	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::FreeHandle(unsigned __int64 handle) Line 884	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTLM(unsigned __int64 i, int isThreadShuttingdown) Line 59	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTable() Line 93	C++	Symbols loaded.
[Inline Frame] coreclr.dll!Thread::DeleteThreadStaticData() Line 7704	C++	Symbols loaded.
coreclr.dll!Thread::OnThreadTerminate(int holdingLock) Line 2956	C++	Symbols loaded.
coreclr.dll!DestroyThread(Thread * th) Line 924	C++	Symbols loaded.
coreclr.dll!ThreadNative::KickOffThread(void * pass) Line 239	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

with an exception which is thrown with the following details:

Exception thrown: read access violation.
**loaderAllocator** was nullptr.
devenv_iJFpiWBvYe

loaderAllocator value from the LOADERALLOCATORREF loaderAllocator = (LOADERALLOCATORREF)ObjectFromHandle(m_hLoaderAllocatorObjectHandle); call is NULL which makes sense, because finalizer thread just disposed data for some of the same LoaderAllocator assemblies.

Note: enabling COR_PRF_MONITOR_MODULE_LOADS CLR profiler flag increases chances for the race condition as it slows down a bit complete loader destruction.

Changes:

I've looked at the following options as a solution to the problem:

  • Null check for loaderAllocator
  • Wait for GC/Finalizers done before killing thread
  • Skip cleaning up thread locals for unloaded loaders

Based on the discussion the acceptable solution would be to:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 20, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 20, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree company="Unity Technologies"

@alexey-zakharov
alexey-zakharov marked this pull request as ready for review March 20, 2024 08:55
@jkotasjkotas added area-VM-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 20, 2024
@jkotas
jkotas requested a review from janvorliMarch 20, 2024 09:05
Comment threadsrc/coreclr/vm/threadstatics.cpp Outdated
}
if (entry->m_hNonGCStatics != NULL)
// Skip assemblies that are already unloaded
if (!pLoaderAllocator->IsUnloaded())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we run into a situation where the thread gets rescheduled right after this check, the code in GCLoaderAllocators sets the flag and start destroying the loader allocators, and then the thread running this code gets scheduled back and still hits this crash?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question! I think it may happen and ideally FreeHandle call of LoaderAllocator itself should deal with its own state then, perhaps we could use m_crstLoaderAllocator lock, but it would mean some perf hit for the fast pointer path

my ideal expectation would be that since DeleteThreadStaticData is under GCX_COOP in Thread::OnThreadTerminate that should hopefully mean that once we are in DeleteThreadStaticData call we know for sure whether or not LoaderAllocator is alive or not and that state can not change - perhaps IsAlive() should be used as IsUnloaded is set on finalizer thread and not relevant to GCX_COOP 🤔

@davidwrighton

Copy link
Copy Markdown
Member

I'm in the process of trying to redo how all thread statics work at the moment, could we hold off on this change for a week or two while I see if I can get my larger change checked in. As it happens I had already identified the problem and the general gist of the solution I came up with is the same, although the mechanism is a fair bit different.

@jkotasjkotas added the blocked Issue/PR is blocked on something - see comments label Mar 20, 2024
@simonferquel

Copy link
Copy Markdown
Contributor

We (Unity) can confirm this fix is not enough and the race is still here:
image
@davidwrighton would you mind to ping us when your fix is ready ? - also is it planned to be backported to .net 8 in addition to 9 ?

@jkotas

Copy link
Copy Markdown
Member

@davidwrighton 's fix is included in #99183.

it is it planned to be backported to .net 8

#99183 is a large refactoring, it is not back portable. The fix for .NET 8 servicing would have to be a small, targeted fix.

@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

The fix for .NET 8 servicing would have to be a small, targeted fix.

@davidwrighton@jkotas do you think using pLoaderAllocator->GetExposedObject(); check in conjunction with GCX_COOP() section would be a better 8.0 targeted approach? (something like this)

the idea there is that if exposed object is alive we can safely free LoaderAllocator handles and while we are doing that it won't be collected and thus finalized

@davidwrighton

Copy link
Copy Markdown
Member

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

@alexey-zakharov
alexey-zakharovforce-pushed the upstream-fix-threadlocal-free branch from b89d190 to fff7409CompareMay 17, 2024 09:47
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

thanks for the update @davidwrighton !
yes, I've updated the PR to address the issue raised by @jkotas keeping in mind the following ideas:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

the fix seemed to be running fine for us - no crashes or deadlocks occurred since we've incorporated it.

@davidwrighton
davidwrighton merged commit 35e4aad into dotnet:mainMay 25, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@davidwrighton thanks for taking the fix in!

I'd approve that change, and then we can port it to .NET 8

to clarify - should I make a similar PR against .NET 8 branch?

@jkotas

Copy link
Copy Markdown
Member

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

Could you please look into fixing this failure and resubmit the change? We should make sure to trigger outer loop tests on the resubmitted PR before it gets merged.

@alexey-zakharov

alexey-zakharov commented May 29, 2024

Copy link
Copy Markdown
ContributorAuthor

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

thanks for the ping @jkotas ! I've reverted the unnecessary constraint changes which caused issues and running tests in #102797, let me know if there is a specific test I can run to validate potential regressions

@alexey-zakharov
alexey-zakharov deleted the upstream-fix-threadlocal-free branch May 30, 2024 08:40
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 30, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrblockedIssue/PR is blocked on something - see commentscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@alexey-zakharov@davidwrighton@simonferquel@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Avoid clearing thread local handles for already unloaded loader - #99998

Merged
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free
May 25, 2024
Merged

Avoid clearing thread local handles for already unloaded loader#99998
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free

Conversation

@alexey-zakharov

@alexey-zakharovalexey-zakharov commented Mar 20, 2024

Copy link
Copy Markdown
Contributor

Motivation:

Fix crash in ThreadLocalBlock::FreeTLM code on thread exit which happens during Assembly unloading when Assembly contains a class with a ThreadStatic variable.

Details:

There seem to be a race condition between LoaderAllocator cleanup during garbage collection and thread locals cleanup on thread exit. Racing stacks are the following:

ALC/LoaderAllocator cleanup

 coreclr.dll!EEToProfInterfaceImpl::ModuleUnloadStarted(unsigned __int64 moduleId) Line 3735	C++	Symbols loaded.
coreclr.dll!ProfControlBlock::DoProfilerCallbackHelper<int (__cdecl*)(ProfilerInfo *),long (__cdecl*)(EEToProfInterfaceImpl *,unsigned __int64),unsigned __int64>(ProfilerInfo * pProfilerInfo, int(*)(ProfilerInfo *) condition, HRESULT(*)(EEToProfInterfaceImpl *, unsigned __int64) callback, HRESULT * pHR, unsigned __int64 <args_0>) Line 284	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoOneProfilerIteration(ProfilerInfo *) Line 199	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::IterateProfilers(ProfilerCallbackType) Line 207	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoProfilerCallback(ProfilerCallbackType) Line 295	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::ModuleUnloadStarted(unsigned __int64) Line 691	C++	Symbols loaded.
coreclr.dll!Module::Destruct() Line 662	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ClassLoader::FreeModules() Line 1884	C++	Symbols loaded.
coreclr.dll!ClassLoader::~ClassLoader() Line 1946	C++	Symbols loaded.
coreclr.dll!Assembly::Terminate(int) Line 311	C++	Symbols loaded.
coreclr.dll!Assembly::~Assembly() Line 244	C++	Symbols loaded.
coreclr.dll!Assembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
coreclr.dll!DomainAssembly::~DomainAssembly() Line 91	C++	Symbols loaded.
coreclr.dll!DomainAssembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
>	coreclr.dll!LoaderAllocator::GCLoaderAllocators(LoaderAllocator * pOriginalLoaderAllocator) Line 570	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 702	C++	Symbols loaded.
coreclr.dll!LoaderAllocator_Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 718	C++	Symbols loaded.
System.Private.CoreLib.dll!00007ffa9c4308f9()	Unknown	No symbols loaded.
coreclr.dll!FastCallFinalizeWorker() Line 26	Unknown	Symbols loaded.
coreclr.dll!MethodTable::CallFinalizer(Object * obj) Line 4908	C++	Symbols loaded.
[Inline Frame] coreclr.dll!CallFinalizer(Object *) Line 75	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizeAllObjects() Line 110	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadWorker(void * args) Line 354	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_DispatchInner(ManagedThreadCallState *) Line 7222	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchMiddle(ManagedThreadCallState * pCallState) Line 7266	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchOuter(ManagedThreadCallState * pCallState) Line 7425	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_NoADTransition(void(*)(void *)) Line 7494	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase::FinalizerBase(void(*)(void *)) Line 7513	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadStart(void * args) Line 403	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

Thread exit with threadlocal cleanup

>	coreclr.dll!LoaderAllocator::SetHandleValue(unsigned __int64 handle, Object *) Line 992	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::FreeHandle(unsigned __int64 handle) Line 884	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTLM(unsigned __int64 i, int isThreadShuttingdown) Line 59	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTable() Line 93	C++	Symbols loaded.
[Inline Frame] coreclr.dll!Thread::DeleteThreadStaticData() Line 7704	C++	Symbols loaded.
coreclr.dll!Thread::OnThreadTerminate(int holdingLock) Line 2956	C++	Symbols loaded.
coreclr.dll!DestroyThread(Thread * th) Line 924	C++	Symbols loaded.
coreclr.dll!ThreadNative::KickOffThread(void * pass) Line 239	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

with an exception which is thrown with the following details:

Exception thrown: read access violation.
**loaderAllocator** was nullptr.
devenv_iJFpiWBvYe

loaderAllocator value from the LOADERALLOCATORREF loaderAllocator = (LOADERALLOCATORREF)ObjectFromHandle(m_hLoaderAllocatorObjectHandle); call is NULL which makes sense, because finalizer thread just disposed data for some of the same LoaderAllocator assemblies.

Note: enabling COR_PRF_MONITOR_MODULE_LOADS CLR profiler flag increases chances for the race condition as it slows down a bit complete loader destruction.

Changes:

I've looked at the following options as a solution to the problem:

  • Null check for loaderAllocator
  • Wait for GC/Finalizers done before killing thread
  • Skip cleaning up thread locals for unloaded loaders

Based on the discussion the acceptable solution would be to:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 20, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 20, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree company="Unity Technologies"

@alexey-zakharov
alexey-zakharov marked this pull request as ready for review March 20, 2024 08:55
@jkotasjkotas added area-VM-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 20, 2024
@jkotas
jkotas requested a review from janvorliMarch 20, 2024 09:05
Comment threadsrc/coreclr/vm/threadstatics.cpp Outdated
}
if (entry->m_hNonGCStatics != NULL)
// Skip assemblies that are already unloaded
if (!pLoaderAllocator->IsUnloaded())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we run into a situation where the thread gets rescheduled right after this check, the code in GCLoaderAllocators sets the flag and start destroying the loader allocators, and then the thread running this code gets scheduled back and still hits this crash?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question! I think it may happen and ideally FreeHandle call of LoaderAllocator itself should deal with its own state then, perhaps we could use m_crstLoaderAllocator lock, but it would mean some perf hit for the fast pointer path

my ideal expectation would be that since DeleteThreadStaticData is under GCX_COOP in Thread::OnThreadTerminate that should hopefully mean that once we are in DeleteThreadStaticData call we know for sure whether or not LoaderAllocator is alive or not and that state can not change - perhaps IsAlive() should be used as IsUnloaded is set on finalizer thread and not relevant to GCX_COOP 🤔

@davidwrighton

Copy link
Copy Markdown
Member

I'm in the process of trying to redo how all thread statics work at the moment, could we hold off on this change for a week or two while I see if I can get my larger change checked in. As it happens I had already identified the problem and the general gist of the solution I came up with is the same, although the mechanism is a fair bit different.

@jkotasjkotas added the blocked Issue/PR is blocked on something - see comments label Mar 20, 2024
@simonferquel

Copy link
Copy Markdown
Contributor

We (Unity) can confirm this fix is not enough and the race is still here:
image
@davidwrighton would you mind to ping us when your fix is ready ? - also is it planned to be backported to .net 8 in addition to 9 ?

@jkotas

Copy link
Copy Markdown
Member

@davidwrighton 's fix is included in #99183.

it is it planned to be backported to .net 8

#99183 is a large refactoring, it is not back portable. The fix for .NET 8 servicing would have to be a small, targeted fix.

@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

The fix for .NET 8 servicing would have to be a small, targeted fix.

@davidwrighton@jkotas do you think using pLoaderAllocator->GetExposedObject(); check in conjunction with GCX_COOP() section would be a better 8.0 targeted approach? (something like this)

the idea there is that if exposed object is alive we can safely free LoaderAllocator handles and while we are doing that it won't be collected and thus finalized

@davidwrighton

Copy link
Copy Markdown
Member

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

@alexey-zakharov
alexey-zakharovforce-pushed the upstream-fix-threadlocal-free branch from b89d190 to fff7409CompareMay 17, 2024 09:47
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

thanks for the update @davidwrighton !
yes, I've updated the PR to address the issue raised by @jkotas keeping in mind the following ideas:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

the fix seemed to be running fine for us - no crashes or deadlocks occurred since we've incorporated it.

@davidwrighton
davidwrighton merged commit 35e4aad into dotnet:mainMay 25, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@davidwrighton thanks for taking the fix in!

I'd approve that change, and then we can port it to .NET 8

to clarify - should I make a similar PR against .NET 8 branch?

@jkotas

Copy link
Copy Markdown
Member

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

Could you please look into fixing this failure and resubmit the change? We should make sure to trigger outer loop tests on the resubmitted PR before it gets merged.

@alexey-zakharov

alexey-zakharov commented May 29, 2024

Copy link
Copy Markdown
ContributorAuthor

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

thanks for the ping @jkotas ! I've reverted the unnecessary constraint changes which caused issues and running tests in #102797, let me know if there is a specific test I can run to validate potential regressions

@alexey-zakharov
alexey-zakharov deleted the upstream-fix-threadlocal-free branch May 30, 2024 08:40
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 30, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrblockedIssue/PR is blocked on something - see commentscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@alexey-zakharov@davidwrighton@simonferquel@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Avoid clearing thread local handles for already unloaded loader - #99998

Merged
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free
May 25, 2024
Merged

Avoid clearing thread local handles for already unloaded loader#99998
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free

Conversation

@alexey-zakharov

@alexey-zakharovalexey-zakharov commented Mar 20, 2024

Copy link
Copy Markdown
Contributor

Motivation:

Fix crash in ThreadLocalBlock::FreeTLM code on thread exit which happens during Assembly unloading when Assembly contains a class with a ThreadStatic variable.

Details:

There seem to be a race condition between LoaderAllocator cleanup during garbage collection and thread locals cleanup on thread exit. Racing stacks are the following:

ALC/LoaderAllocator cleanup

 coreclr.dll!EEToProfInterfaceImpl::ModuleUnloadStarted(unsigned __int64 moduleId) Line 3735	C++	Symbols loaded.
coreclr.dll!ProfControlBlock::DoProfilerCallbackHelper<int (__cdecl*)(ProfilerInfo *),long (__cdecl*)(EEToProfInterfaceImpl *,unsigned __int64),unsigned __int64>(ProfilerInfo * pProfilerInfo, int(*)(ProfilerInfo *) condition, HRESULT(*)(EEToProfInterfaceImpl *, unsigned __int64) callback, HRESULT * pHR, unsigned __int64 <args_0>) Line 284	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoOneProfilerIteration(ProfilerInfo *) Line 199	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::IterateProfilers(ProfilerCallbackType) Line 207	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoProfilerCallback(ProfilerCallbackType) Line 295	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::ModuleUnloadStarted(unsigned __int64) Line 691	C++	Symbols loaded.
coreclr.dll!Module::Destruct() Line 662	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ClassLoader::FreeModules() Line 1884	C++	Symbols loaded.
coreclr.dll!ClassLoader::~ClassLoader() Line 1946	C++	Symbols loaded.
coreclr.dll!Assembly::Terminate(int) Line 311	C++	Symbols loaded.
coreclr.dll!Assembly::~Assembly() Line 244	C++	Symbols loaded.
coreclr.dll!Assembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
coreclr.dll!DomainAssembly::~DomainAssembly() Line 91	C++	Symbols loaded.
coreclr.dll!DomainAssembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
>	coreclr.dll!LoaderAllocator::GCLoaderAllocators(LoaderAllocator * pOriginalLoaderAllocator) Line 570	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 702	C++	Symbols loaded.
coreclr.dll!LoaderAllocator_Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 718	C++	Symbols loaded.
System.Private.CoreLib.dll!00007ffa9c4308f9()	Unknown	No symbols loaded.
coreclr.dll!FastCallFinalizeWorker() Line 26	Unknown	Symbols loaded.
coreclr.dll!MethodTable::CallFinalizer(Object * obj) Line 4908	C++	Symbols loaded.
[Inline Frame] coreclr.dll!CallFinalizer(Object *) Line 75	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizeAllObjects() Line 110	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadWorker(void * args) Line 354	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_DispatchInner(ManagedThreadCallState *) Line 7222	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchMiddle(ManagedThreadCallState * pCallState) Line 7266	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchOuter(ManagedThreadCallState * pCallState) Line 7425	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_NoADTransition(void(*)(void *)) Line 7494	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase::FinalizerBase(void(*)(void *)) Line 7513	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadStart(void * args) Line 403	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

Thread exit with threadlocal cleanup

>	coreclr.dll!LoaderAllocator::SetHandleValue(unsigned __int64 handle, Object *) Line 992	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::FreeHandle(unsigned __int64 handle) Line 884	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTLM(unsigned __int64 i, int isThreadShuttingdown) Line 59	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTable() Line 93	C++	Symbols loaded.
[Inline Frame] coreclr.dll!Thread::DeleteThreadStaticData() Line 7704	C++	Symbols loaded.
coreclr.dll!Thread::OnThreadTerminate(int holdingLock) Line 2956	C++	Symbols loaded.
coreclr.dll!DestroyThread(Thread * th) Line 924	C++	Symbols loaded.
coreclr.dll!ThreadNative::KickOffThread(void * pass) Line 239	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

with an exception which is thrown with the following details:

Exception thrown: read access violation.
**loaderAllocator** was nullptr.
devenv_iJFpiWBvYe

loaderAllocator value from the LOADERALLOCATORREF loaderAllocator = (LOADERALLOCATORREF)ObjectFromHandle(m_hLoaderAllocatorObjectHandle); call is NULL which makes sense, because finalizer thread just disposed data for some of the same LoaderAllocator assemblies.

Note: enabling COR_PRF_MONITOR_MODULE_LOADS CLR profiler flag increases chances for the race condition as it slows down a bit complete loader destruction.

Changes:

I've looked at the following options as a solution to the problem:

  • Null check for loaderAllocator
  • Wait for GC/Finalizers done before killing thread
  • Skip cleaning up thread locals for unloaded loaders

Based on the discussion the acceptable solution would be to:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 20, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 20, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree company="Unity Technologies"

@alexey-zakharov
alexey-zakharov marked this pull request as ready for review March 20, 2024 08:55
@jkotasjkotas added area-VM-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 20, 2024
@jkotas
jkotas requested a review from janvorliMarch 20, 2024 09:05
Comment threadsrc/coreclr/vm/threadstatics.cpp Outdated
}
if (entry->m_hNonGCStatics != NULL)
// Skip assemblies that are already unloaded
if (!pLoaderAllocator->IsUnloaded())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we run into a situation where the thread gets rescheduled right after this check, the code in GCLoaderAllocators sets the flag and start destroying the loader allocators, and then the thread running this code gets scheduled back and still hits this crash?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question! I think it may happen and ideally FreeHandle call of LoaderAllocator itself should deal with its own state then, perhaps we could use m_crstLoaderAllocator lock, but it would mean some perf hit for the fast pointer path

my ideal expectation would be that since DeleteThreadStaticData is under GCX_COOP in Thread::OnThreadTerminate that should hopefully mean that once we are in DeleteThreadStaticData call we know for sure whether or not LoaderAllocator is alive or not and that state can not change - perhaps IsAlive() should be used as IsUnloaded is set on finalizer thread and not relevant to GCX_COOP 🤔

@davidwrighton

Copy link
Copy Markdown
Member

I'm in the process of trying to redo how all thread statics work at the moment, could we hold off on this change for a week or two while I see if I can get my larger change checked in. As it happens I had already identified the problem and the general gist of the solution I came up with is the same, although the mechanism is a fair bit different.

@jkotasjkotas added the blocked Issue/PR is blocked on something - see comments label Mar 20, 2024
@simonferquel

Copy link
Copy Markdown
Contributor

We (Unity) can confirm this fix is not enough and the race is still here:
image
@davidwrighton would you mind to ping us when your fix is ready ? - also is it planned to be backported to .net 8 in addition to 9 ?

@jkotas

Copy link
Copy Markdown
Member

@davidwrighton 's fix is included in #99183.

it is it planned to be backported to .net 8

#99183 is a large refactoring, it is not back portable. The fix for .NET 8 servicing would have to be a small, targeted fix.

@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

The fix for .NET 8 servicing would have to be a small, targeted fix.

@davidwrighton@jkotas do you think using pLoaderAllocator->GetExposedObject(); check in conjunction with GCX_COOP() section would be a better 8.0 targeted approach? (something like this)

the idea there is that if exposed object is alive we can safely free LoaderAllocator handles and while we are doing that it won't be collected and thus finalized

@davidwrighton

Copy link
Copy Markdown
Member

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

@alexey-zakharov
alexey-zakharovforce-pushed the upstream-fix-threadlocal-free branch from b89d190 to fff7409CompareMay 17, 2024 09:47
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

thanks for the update @davidwrighton !
yes, I've updated the PR to address the issue raised by @jkotas keeping in mind the following ideas:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

the fix seemed to be running fine for us - no crashes or deadlocks occurred since we've incorporated it.

@davidwrighton
davidwrighton merged commit 35e4aad into dotnet:mainMay 25, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@davidwrighton thanks for taking the fix in!

I'd approve that change, and then we can port it to .NET 8

to clarify - should I make a similar PR against .NET 8 branch?

@jkotas

Copy link
Copy Markdown
Member

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

Could you please look into fixing this failure and resubmit the change? We should make sure to trigger outer loop tests on the resubmitted PR before it gets merged.

@alexey-zakharov

alexey-zakharov commented May 29, 2024

Copy link
Copy Markdown
ContributorAuthor

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

thanks for the ping @jkotas ! I've reverted the unnecessary constraint changes which caused issues and running tests in #102797, let me know if there is a specific test I can run to validate potential regressions

@alexey-zakharov
alexey-zakharov deleted the upstream-fix-threadlocal-free branch May 30, 2024 08:40
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 30, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrblockedIssue/PR is blocked on something - see commentscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@alexey-zakharov@davidwrighton@simonferquel@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Avoid clearing thread local handles for already unloaded loader - #99998

Merged
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free
May 25, 2024
Merged

Avoid clearing thread local handles for already unloaded loader#99998
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free

Conversation

@alexey-zakharov

@alexey-zakharovalexey-zakharov commented Mar 20, 2024

Copy link
Copy Markdown
Contributor

Motivation:

Fix crash in ThreadLocalBlock::FreeTLM code on thread exit which happens during Assembly unloading when Assembly contains a class with a ThreadStatic variable.

Details:

There seem to be a race condition between LoaderAllocator cleanup during garbage collection and thread locals cleanup on thread exit. Racing stacks are the following:

ALC/LoaderAllocator cleanup

 coreclr.dll!EEToProfInterfaceImpl::ModuleUnloadStarted(unsigned __int64 moduleId) Line 3735	C++	Symbols loaded.
coreclr.dll!ProfControlBlock::DoProfilerCallbackHelper<int (__cdecl*)(ProfilerInfo *),long (__cdecl*)(EEToProfInterfaceImpl *,unsigned __int64),unsigned __int64>(ProfilerInfo * pProfilerInfo, int(*)(ProfilerInfo *) condition, HRESULT(*)(EEToProfInterfaceImpl *, unsigned __int64) callback, HRESULT * pHR, unsigned __int64 <args_0>) Line 284	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoOneProfilerIteration(ProfilerInfo *) Line 199	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::IterateProfilers(ProfilerCallbackType) Line 207	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoProfilerCallback(ProfilerCallbackType) Line 295	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::ModuleUnloadStarted(unsigned __int64) Line 691	C++	Symbols loaded.
coreclr.dll!Module::Destruct() Line 662	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ClassLoader::FreeModules() Line 1884	C++	Symbols loaded.
coreclr.dll!ClassLoader::~ClassLoader() Line 1946	C++	Symbols loaded.
coreclr.dll!Assembly::Terminate(int) Line 311	C++	Symbols loaded.
coreclr.dll!Assembly::~Assembly() Line 244	C++	Symbols loaded.
coreclr.dll!Assembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
coreclr.dll!DomainAssembly::~DomainAssembly() Line 91	C++	Symbols loaded.
coreclr.dll!DomainAssembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
>	coreclr.dll!LoaderAllocator::GCLoaderAllocators(LoaderAllocator * pOriginalLoaderAllocator) Line 570	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 702	C++	Symbols loaded.
coreclr.dll!LoaderAllocator_Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 718	C++	Symbols loaded.
System.Private.CoreLib.dll!00007ffa9c4308f9()	Unknown	No symbols loaded.
coreclr.dll!FastCallFinalizeWorker() Line 26	Unknown	Symbols loaded.
coreclr.dll!MethodTable::CallFinalizer(Object * obj) Line 4908	C++	Symbols loaded.
[Inline Frame] coreclr.dll!CallFinalizer(Object *) Line 75	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizeAllObjects() Line 110	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadWorker(void * args) Line 354	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_DispatchInner(ManagedThreadCallState *) Line 7222	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchMiddle(ManagedThreadCallState * pCallState) Line 7266	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchOuter(ManagedThreadCallState * pCallState) Line 7425	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_NoADTransition(void(*)(void *)) Line 7494	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase::FinalizerBase(void(*)(void *)) Line 7513	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadStart(void * args) Line 403	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

Thread exit with threadlocal cleanup

>	coreclr.dll!LoaderAllocator::SetHandleValue(unsigned __int64 handle, Object *) Line 992	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::FreeHandle(unsigned __int64 handle) Line 884	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTLM(unsigned __int64 i, int isThreadShuttingdown) Line 59	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTable() Line 93	C++	Symbols loaded.
[Inline Frame] coreclr.dll!Thread::DeleteThreadStaticData() Line 7704	C++	Symbols loaded.
coreclr.dll!Thread::OnThreadTerminate(int holdingLock) Line 2956	C++	Symbols loaded.
coreclr.dll!DestroyThread(Thread * th) Line 924	C++	Symbols loaded.
coreclr.dll!ThreadNative::KickOffThread(void * pass) Line 239	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

with an exception which is thrown with the following details:

Exception thrown: read access violation.
**loaderAllocator** was nullptr.
devenv_iJFpiWBvYe

loaderAllocator value from the LOADERALLOCATORREF loaderAllocator = (LOADERALLOCATORREF)ObjectFromHandle(m_hLoaderAllocatorObjectHandle); call is NULL which makes sense, because finalizer thread just disposed data for some of the same LoaderAllocator assemblies.

Note: enabling COR_PRF_MONITOR_MODULE_LOADS CLR profiler flag increases chances for the race condition as it slows down a bit complete loader destruction.

Changes:

I've looked at the following options as a solution to the problem:

  • Null check for loaderAllocator
  • Wait for GC/Finalizers done before killing thread
  • Skip cleaning up thread locals for unloaded loaders

Based on the discussion the acceptable solution would be to:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 20, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 20, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree company="Unity Technologies"

@alexey-zakharov
alexey-zakharov marked this pull request as ready for review March 20, 2024 08:55
@jkotasjkotas added area-VM-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 20, 2024
@jkotas
jkotas requested a review from janvorliMarch 20, 2024 09:05
Comment threadsrc/coreclr/vm/threadstatics.cpp Outdated
}
if (entry->m_hNonGCStatics != NULL)
// Skip assemblies that are already unloaded
if (!pLoaderAllocator->IsUnloaded())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we run into a situation where the thread gets rescheduled right after this check, the code in GCLoaderAllocators sets the flag and start destroying the loader allocators, and then the thread running this code gets scheduled back and still hits this crash?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question! I think it may happen and ideally FreeHandle call of LoaderAllocator itself should deal with its own state then, perhaps we could use m_crstLoaderAllocator lock, but it would mean some perf hit for the fast pointer path

my ideal expectation would be that since DeleteThreadStaticData is under GCX_COOP in Thread::OnThreadTerminate that should hopefully mean that once we are in DeleteThreadStaticData call we know for sure whether or not LoaderAllocator is alive or not and that state can not change - perhaps IsAlive() should be used as IsUnloaded is set on finalizer thread and not relevant to GCX_COOP 🤔

@davidwrighton

Copy link
Copy Markdown
Member

I'm in the process of trying to redo how all thread statics work at the moment, could we hold off on this change for a week or two while I see if I can get my larger change checked in. As it happens I had already identified the problem and the general gist of the solution I came up with is the same, although the mechanism is a fair bit different.

@jkotasjkotas added the blocked Issue/PR is blocked on something - see comments label Mar 20, 2024
@simonferquel

Copy link
Copy Markdown
Contributor

We (Unity) can confirm this fix is not enough and the race is still here:
image
@davidwrighton would you mind to ping us when your fix is ready ? - also is it planned to be backported to .net 8 in addition to 9 ?

@jkotas

Copy link
Copy Markdown
Member

@davidwrighton 's fix is included in #99183.

it is it planned to be backported to .net 8

#99183 is a large refactoring, it is not back portable. The fix for .NET 8 servicing would have to be a small, targeted fix.

@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

The fix for .NET 8 servicing would have to be a small, targeted fix.

@davidwrighton@jkotas do you think using pLoaderAllocator->GetExposedObject(); check in conjunction with GCX_COOP() section would be a better 8.0 targeted approach? (something like this)

the idea there is that if exposed object is alive we can safely free LoaderAllocator handles and while we are doing that it won't be collected and thus finalized

@davidwrighton

Copy link
Copy Markdown
Member

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

@alexey-zakharov
alexey-zakharovforce-pushed the upstream-fix-threadlocal-free branch from b89d190 to fff7409CompareMay 17, 2024 09:47
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

thanks for the update @davidwrighton !
yes, I've updated the PR to address the issue raised by @jkotas keeping in mind the following ideas:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

the fix seemed to be running fine for us - no crashes or deadlocks occurred since we've incorporated it.

@davidwrighton
davidwrighton merged commit 35e4aad into dotnet:mainMay 25, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@davidwrighton thanks for taking the fix in!

I'd approve that change, and then we can port it to .NET 8

to clarify - should I make a similar PR against .NET 8 branch?

@jkotas

Copy link
Copy Markdown
Member

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

Could you please look into fixing this failure and resubmit the change? We should make sure to trigger outer loop tests on the resubmitted PR before it gets merged.

@alexey-zakharov

alexey-zakharov commented May 29, 2024

Copy link
Copy Markdown
ContributorAuthor

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

thanks for the ping @jkotas ! I've reverted the unnecessary constraint changes which caused issues and running tests in #102797, let me know if there is a specific test I can run to validate potential regressions

@alexey-zakharov
alexey-zakharov deleted the upstream-fix-threadlocal-free branch May 30, 2024 08:40
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 30, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrblockedIssue/PR is blocked on something - see commentscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@alexey-zakharov@davidwrighton@simonferquel@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Avoid clearing thread local handles for already unloaded loader - #99998

Merged
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free
May 25, 2024
Merged

Avoid clearing thread local handles for already unloaded loader#99998
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free

Conversation

@alexey-zakharov

@alexey-zakharovalexey-zakharov commented Mar 20, 2024

Copy link
Copy Markdown
Contributor

Motivation:

Fix crash in ThreadLocalBlock::FreeTLM code on thread exit which happens during Assembly unloading when Assembly contains a class with a ThreadStatic variable.

Details:

There seem to be a race condition between LoaderAllocator cleanup during garbage collection and thread locals cleanup on thread exit. Racing stacks are the following:

ALC/LoaderAllocator cleanup

 coreclr.dll!EEToProfInterfaceImpl::ModuleUnloadStarted(unsigned __int64 moduleId) Line 3735	C++	Symbols loaded.
coreclr.dll!ProfControlBlock::DoProfilerCallbackHelper<int (__cdecl*)(ProfilerInfo *),long (__cdecl*)(EEToProfInterfaceImpl *,unsigned __int64),unsigned __int64>(ProfilerInfo * pProfilerInfo, int(*)(ProfilerInfo *) condition, HRESULT(*)(EEToProfInterfaceImpl *, unsigned __int64) callback, HRESULT * pHR, unsigned __int64 <args_0>) Line 284	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoOneProfilerIteration(ProfilerInfo *) Line 199	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::IterateProfilers(ProfilerCallbackType) Line 207	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoProfilerCallback(ProfilerCallbackType) Line 295	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::ModuleUnloadStarted(unsigned __int64) Line 691	C++	Symbols loaded.
coreclr.dll!Module::Destruct() Line 662	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ClassLoader::FreeModules() Line 1884	C++	Symbols loaded.
coreclr.dll!ClassLoader::~ClassLoader() Line 1946	C++	Symbols loaded.
coreclr.dll!Assembly::Terminate(int) Line 311	C++	Symbols loaded.
coreclr.dll!Assembly::~Assembly() Line 244	C++	Symbols loaded.
coreclr.dll!Assembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
coreclr.dll!DomainAssembly::~DomainAssembly() Line 91	C++	Symbols loaded.
coreclr.dll!DomainAssembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
>	coreclr.dll!LoaderAllocator::GCLoaderAllocators(LoaderAllocator * pOriginalLoaderAllocator) Line 570	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 702	C++	Symbols loaded.
coreclr.dll!LoaderAllocator_Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 718	C++	Symbols loaded.
System.Private.CoreLib.dll!00007ffa9c4308f9()	Unknown	No symbols loaded.
coreclr.dll!FastCallFinalizeWorker() Line 26	Unknown	Symbols loaded.
coreclr.dll!MethodTable::CallFinalizer(Object * obj) Line 4908	C++	Symbols loaded.
[Inline Frame] coreclr.dll!CallFinalizer(Object *) Line 75	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizeAllObjects() Line 110	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadWorker(void * args) Line 354	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_DispatchInner(ManagedThreadCallState *) Line 7222	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchMiddle(ManagedThreadCallState * pCallState) Line 7266	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchOuter(ManagedThreadCallState * pCallState) Line 7425	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_NoADTransition(void(*)(void *)) Line 7494	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase::FinalizerBase(void(*)(void *)) Line 7513	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadStart(void * args) Line 403	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

Thread exit with threadlocal cleanup

>	coreclr.dll!LoaderAllocator::SetHandleValue(unsigned __int64 handle, Object *) Line 992	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::FreeHandle(unsigned __int64 handle) Line 884	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTLM(unsigned __int64 i, int isThreadShuttingdown) Line 59	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTable() Line 93	C++	Symbols loaded.
[Inline Frame] coreclr.dll!Thread::DeleteThreadStaticData() Line 7704	C++	Symbols loaded.
coreclr.dll!Thread::OnThreadTerminate(int holdingLock) Line 2956	C++	Symbols loaded.
coreclr.dll!DestroyThread(Thread * th) Line 924	C++	Symbols loaded.
coreclr.dll!ThreadNative::KickOffThread(void * pass) Line 239	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

with an exception which is thrown with the following details:

Exception thrown: read access violation.
**loaderAllocator** was nullptr.
devenv_iJFpiWBvYe

loaderAllocator value from the LOADERALLOCATORREF loaderAllocator = (LOADERALLOCATORREF)ObjectFromHandle(m_hLoaderAllocatorObjectHandle); call is NULL which makes sense, because finalizer thread just disposed data for some of the same LoaderAllocator assemblies.

Note: enabling COR_PRF_MONITOR_MODULE_LOADS CLR profiler flag increases chances for the race condition as it slows down a bit complete loader destruction.

Changes:

I've looked at the following options as a solution to the problem:

  • Null check for loaderAllocator
  • Wait for GC/Finalizers done before killing thread
  • Skip cleaning up thread locals for unloaded loaders

Based on the discussion the acceptable solution would be to:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 20, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 20, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree company="Unity Technologies"

@alexey-zakharov
alexey-zakharov marked this pull request as ready for review March 20, 2024 08:55
@jkotasjkotas added area-VM-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 20, 2024
@jkotas
jkotas requested a review from janvorliMarch 20, 2024 09:05
Comment threadsrc/coreclr/vm/threadstatics.cpp Outdated
}
if (entry->m_hNonGCStatics != NULL)
// Skip assemblies that are already unloaded
if (!pLoaderAllocator->IsUnloaded())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we run into a situation where the thread gets rescheduled right after this check, the code in GCLoaderAllocators sets the flag and start destroying the loader allocators, and then the thread running this code gets scheduled back and still hits this crash?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question! I think it may happen and ideally FreeHandle call of LoaderAllocator itself should deal with its own state then, perhaps we could use m_crstLoaderAllocator lock, but it would mean some perf hit for the fast pointer path

my ideal expectation would be that since DeleteThreadStaticData is under GCX_COOP in Thread::OnThreadTerminate that should hopefully mean that once we are in DeleteThreadStaticData call we know for sure whether or not LoaderAllocator is alive or not and that state can not change - perhaps IsAlive() should be used as IsUnloaded is set on finalizer thread and not relevant to GCX_COOP 🤔

@davidwrighton

Copy link
Copy Markdown
Member

I'm in the process of trying to redo how all thread statics work at the moment, could we hold off on this change for a week or two while I see if I can get my larger change checked in. As it happens I had already identified the problem and the general gist of the solution I came up with is the same, although the mechanism is a fair bit different.

@jkotasjkotas added the blocked Issue/PR is blocked on something - see comments label Mar 20, 2024
@simonferquel

Copy link
Copy Markdown
Contributor

We (Unity) can confirm this fix is not enough and the race is still here:
image
@davidwrighton would you mind to ping us when your fix is ready ? - also is it planned to be backported to .net 8 in addition to 9 ?

@jkotas

Copy link
Copy Markdown
Member

@davidwrighton 's fix is included in #99183.

it is it planned to be backported to .net 8

#99183 is a large refactoring, it is not back portable. The fix for .NET 8 servicing would have to be a small, targeted fix.

@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

The fix for .NET 8 servicing would have to be a small, targeted fix.

@davidwrighton@jkotas do you think using pLoaderAllocator->GetExposedObject(); check in conjunction with GCX_COOP() section would be a better 8.0 targeted approach? (something like this)

the idea there is that if exposed object is alive we can safely free LoaderAllocator handles and while we are doing that it won't be collected and thus finalized

@davidwrighton

Copy link
Copy Markdown
Member

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

@alexey-zakharov
alexey-zakharovforce-pushed the upstream-fix-threadlocal-free branch from b89d190 to fff7409CompareMay 17, 2024 09:47
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

thanks for the update @davidwrighton !
yes, I've updated the PR to address the issue raised by @jkotas keeping in mind the following ideas:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

the fix seemed to be running fine for us - no crashes or deadlocks occurred since we've incorporated it.

@davidwrighton
davidwrighton merged commit 35e4aad into dotnet:mainMay 25, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@davidwrighton thanks for taking the fix in!

I'd approve that change, and then we can port it to .NET 8

to clarify - should I make a similar PR against .NET 8 branch?

@jkotas

Copy link
Copy Markdown
Member

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

Could you please look into fixing this failure and resubmit the change? We should make sure to trigger outer loop tests on the resubmitted PR before it gets merged.

@alexey-zakharov

alexey-zakharov commented May 29, 2024

Copy link
Copy Markdown
ContributorAuthor

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

thanks for the ping @jkotas ! I've reverted the unnecessary constraint changes which caused issues and running tests in #102797, let me know if there is a specific test I can run to validate potential regressions

@alexey-zakharov
alexey-zakharov deleted the upstream-fix-threadlocal-free branch May 30, 2024 08:40
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 30, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrblockedIssue/PR is blocked on something - see commentscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@alexey-zakharov@davidwrighton@simonferquel@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Avoid clearing thread local handles for already unloaded loader - #99998

Merged
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free
May 25, 2024
Merged

Avoid clearing thread local handles for already unloaded loader#99998
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free

Conversation

@alexey-zakharov

@alexey-zakharovalexey-zakharov commented Mar 20, 2024

Copy link
Copy Markdown
Contributor

Motivation:

Fix crash in ThreadLocalBlock::FreeTLM code on thread exit which happens during Assembly unloading when Assembly contains a class with a ThreadStatic variable.

Details:

There seem to be a race condition between LoaderAllocator cleanup during garbage collection and thread locals cleanup on thread exit. Racing stacks are the following:

ALC/LoaderAllocator cleanup

 coreclr.dll!EEToProfInterfaceImpl::ModuleUnloadStarted(unsigned __int64 moduleId) Line 3735	C++	Symbols loaded.
coreclr.dll!ProfControlBlock::DoProfilerCallbackHelper<int (__cdecl*)(ProfilerInfo *),long (__cdecl*)(EEToProfInterfaceImpl *,unsigned __int64),unsigned __int64>(ProfilerInfo * pProfilerInfo, int(*)(ProfilerInfo *) condition, HRESULT(*)(EEToProfInterfaceImpl *, unsigned __int64) callback, HRESULT * pHR, unsigned __int64 <args_0>) Line 284	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoOneProfilerIteration(ProfilerInfo *) Line 199	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::IterateProfilers(ProfilerCallbackType) Line 207	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoProfilerCallback(ProfilerCallbackType) Line 295	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::ModuleUnloadStarted(unsigned __int64) Line 691	C++	Symbols loaded.
coreclr.dll!Module::Destruct() Line 662	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ClassLoader::FreeModules() Line 1884	C++	Symbols loaded.
coreclr.dll!ClassLoader::~ClassLoader() Line 1946	C++	Symbols loaded.
coreclr.dll!Assembly::Terminate(int) Line 311	C++	Symbols loaded.
coreclr.dll!Assembly::~Assembly() Line 244	C++	Symbols loaded.
coreclr.dll!Assembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
coreclr.dll!DomainAssembly::~DomainAssembly() Line 91	C++	Symbols loaded.
coreclr.dll!DomainAssembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
>	coreclr.dll!LoaderAllocator::GCLoaderAllocators(LoaderAllocator * pOriginalLoaderAllocator) Line 570	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 702	C++	Symbols loaded.
coreclr.dll!LoaderAllocator_Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 718	C++	Symbols loaded.
System.Private.CoreLib.dll!00007ffa9c4308f9()	Unknown	No symbols loaded.
coreclr.dll!FastCallFinalizeWorker() Line 26	Unknown	Symbols loaded.
coreclr.dll!MethodTable::CallFinalizer(Object * obj) Line 4908	C++	Symbols loaded.
[Inline Frame] coreclr.dll!CallFinalizer(Object *) Line 75	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizeAllObjects() Line 110	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadWorker(void * args) Line 354	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_DispatchInner(ManagedThreadCallState *) Line 7222	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchMiddle(ManagedThreadCallState * pCallState) Line 7266	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchOuter(ManagedThreadCallState * pCallState) Line 7425	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_NoADTransition(void(*)(void *)) Line 7494	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase::FinalizerBase(void(*)(void *)) Line 7513	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadStart(void * args) Line 403	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

Thread exit with threadlocal cleanup

>	coreclr.dll!LoaderAllocator::SetHandleValue(unsigned __int64 handle, Object *) Line 992	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::FreeHandle(unsigned __int64 handle) Line 884	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTLM(unsigned __int64 i, int isThreadShuttingdown) Line 59	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTable() Line 93	C++	Symbols loaded.
[Inline Frame] coreclr.dll!Thread::DeleteThreadStaticData() Line 7704	C++	Symbols loaded.
coreclr.dll!Thread::OnThreadTerminate(int holdingLock) Line 2956	C++	Symbols loaded.
coreclr.dll!DestroyThread(Thread * th) Line 924	C++	Symbols loaded.
coreclr.dll!ThreadNative::KickOffThread(void * pass) Line 239	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

with an exception which is thrown with the following details:

Exception thrown: read access violation.
**loaderAllocator** was nullptr.
devenv_iJFpiWBvYe

loaderAllocator value from the LOADERALLOCATORREF loaderAllocator = (LOADERALLOCATORREF)ObjectFromHandle(m_hLoaderAllocatorObjectHandle); call is NULL which makes sense, because finalizer thread just disposed data for some of the same LoaderAllocator assemblies.

Note: enabling COR_PRF_MONITOR_MODULE_LOADS CLR profiler flag increases chances for the race condition as it slows down a bit complete loader destruction.

Changes:

I've looked at the following options as a solution to the problem:

  • Null check for loaderAllocator
  • Wait for GC/Finalizers done before killing thread
  • Skip cleaning up thread locals for unloaded loaders

Based on the discussion the acceptable solution would be to:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 20, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 20, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree company="Unity Technologies"

@alexey-zakharov
alexey-zakharov marked this pull request as ready for review March 20, 2024 08:55
@jkotasjkotas added area-VM-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 20, 2024
@jkotas
jkotas requested a review from janvorliMarch 20, 2024 09:05
Comment threadsrc/coreclr/vm/threadstatics.cpp Outdated
}
if (entry->m_hNonGCStatics != NULL)
// Skip assemblies that are already unloaded
if (!pLoaderAllocator->IsUnloaded())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we run into a situation where the thread gets rescheduled right after this check, the code in GCLoaderAllocators sets the flag and start destroying the loader allocators, and then the thread running this code gets scheduled back and still hits this crash?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question! I think it may happen and ideally FreeHandle call of LoaderAllocator itself should deal with its own state then, perhaps we could use m_crstLoaderAllocator lock, but it would mean some perf hit for the fast pointer path

my ideal expectation would be that since DeleteThreadStaticData is under GCX_COOP in Thread::OnThreadTerminate that should hopefully mean that once we are in DeleteThreadStaticData call we know for sure whether or not LoaderAllocator is alive or not and that state can not change - perhaps IsAlive() should be used as IsUnloaded is set on finalizer thread and not relevant to GCX_COOP 🤔

@davidwrighton

Copy link
Copy Markdown
Member

I'm in the process of trying to redo how all thread statics work at the moment, could we hold off on this change for a week or two while I see if I can get my larger change checked in. As it happens I had already identified the problem and the general gist of the solution I came up with is the same, although the mechanism is a fair bit different.

@jkotasjkotas added the blocked Issue/PR is blocked on something - see comments label Mar 20, 2024
@simonferquel

Copy link
Copy Markdown
Contributor

We (Unity) can confirm this fix is not enough and the race is still here:
image
@davidwrighton would you mind to ping us when your fix is ready ? - also is it planned to be backported to .net 8 in addition to 9 ?

@jkotas

Copy link
Copy Markdown
Member

@davidwrighton 's fix is included in #99183.

it is it planned to be backported to .net 8

#99183 is a large refactoring, it is not back portable. The fix for .NET 8 servicing would have to be a small, targeted fix.

@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

The fix for .NET 8 servicing would have to be a small, targeted fix.

@davidwrighton@jkotas do you think using pLoaderAllocator->GetExposedObject(); check in conjunction with GCX_COOP() section would be a better 8.0 targeted approach? (something like this)

the idea there is that if exposed object is alive we can safely free LoaderAllocator handles and while we are doing that it won't be collected and thus finalized

@davidwrighton

Copy link
Copy Markdown
Member

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

@alexey-zakharov
alexey-zakharovforce-pushed the upstream-fix-threadlocal-free branch from b89d190 to fff7409CompareMay 17, 2024 09:47
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

thanks for the update @davidwrighton !
yes, I've updated the PR to address the issue raised by @jkotas keeping in mind the following ideas:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

the fix seemed to be running fine for us - no crashes or deadlocks occurred since we've incorporated it.

@davidwrighton
davidwrighton merged commit 35e4aad into dotnet:mainMay 25, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@davidwrighton thanks for taking the fix in!

I'd approve that change, and then we can port it to .NET 8

to clarify - should I make a similar PR against .NET 8 branch?

@jkotas

Copy link
Copy Markdown
Member

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

Could you please look into fixing this failure and resubmit the change? We should make sure to trigger outer loop tests on the resubmitted PR before it gets merged.

@alexey-zakharov

alexey-zakharov commented May 29, 2024

Copy link
Copy Markdown
ContributorAuthor

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

thanks for the ping @jkotas ! I've reverted the unnecessary constraint changes which caused issues and running tests in #102797, let me know if there is a specific test I can run to validate potential regressions

@alexey-zakharov
alexey-zakharov deleted the upstream-fix-threadlocal-free branch May 30, 2024 08:40
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 30, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrblockedIssue/PR is blocked on something - see commentscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@alexey-zakharov@davidwrighton@simonferquel@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Avoid clearing thread local handles for already unloaded loader - #99998

Merged
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free
May 25, 2024
Merged

Avoid clearing thread local handles for already unloaded loader#99998
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free

Conversation

@alexey-zakharov

@alexey-zakharovalexey-zakharov commented Mar 20, 2024

Copy link
Copy Markdown
Contributor

Motivation:

Fix crash in ThreadLocalBlock::FreeTLM code on thread exit which happens during Assembly unloading when Assembly contains a class with a ThreadStatic variable.

Details:

There seem to be a race condition between LoaderAllocator cleanup during garbage collection and thread locals cleanup on thread exit. Racing stacks are the following:

ALC/LoaderAllocator cleanup

 coreclr.dll!EEToProfInterfaceImpl::ModuleUnloadStarted(unsigned __int64 moduleId) Line 3735	C++	Symbols loaded.
coreclr.dll!ProfControlBlock::DoProfilerCallbackHelper<int (__cdecl*)(ProfilerInfo *),long (__cdecl*)(EEToProfInterfaceImpl *,unsigned __int64),unsigned __int64>(ProfilerInfo * pProfilerInfo, int(*)(ProfilerInfo *) condition, HRESULT(*)(EEToProfInterfaceImpl *, unsigned __int64) callback, HRESULT * pHR, unsigned __int64 <args_0>) Line 284	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoOneProfilerIteration(ProfilerInfo *) Line 199	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::IterateProfilers(ProfilerCallbackType) Line 207	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoProfilerCallback(ProfilerCallbackType) Line 295	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::ModuleUnloadStarted(unsigned __int64) Line 691	C++	Symbols loaded.
coreclr.dll!Module::Destruct() Line 662	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ClassLoader::FreeModules() Line 1884	C++	Symbols loaded.
coreclr.dll!ClassLoader::~ClassLoader() Line 1946	C++	Symbols loaded.
coreclr.dll!Assembly::Terminate(int) Line 311	C++	Symbols loaded.
coreclr.dll!Assembly::~Assembly() Line 244	C++	Symbols loaded.
coreclr.dll!Assembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
coreclr.dll!DomainAssembly::~DomainAssembly() Line 91	C++	Symbols loaded.
coreclr.dll!DomainAssembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
>	coreclr.dll!LoaderAllocator::GCLoaderAllocators(LoaderAllocator * pOriginalLoaderAllocator) Line 570	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 702	C++	Symbols loaded.
coreclr.dll!LoaderAllocator_Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 718	C++	Symbols loaded.
System.Private.CoreLib.dll!00007ffa9c4308f9()	Unknown	No symbols loaded.
coreclr.dll!FastCallFinalizeWorker() Line 26	Unknown	Symbols loaded.
coreclr.dll!MethodTable::CallFinalizer(Object * obj) Line 4908	C++	Symbols loaded.
[Inline Frame] coreclr.dll!CallFinalizer(Object *) Line 75	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizeAllObjects() Line 110	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadWorker(void * args) Line 354	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_DispatchInner(ManagedThreadCallState *) Line 7222	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchMiddle(ManagedThreadCallState * pCallState) Line 7266	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchOuter(ManagedThreadCallState * pCallState) Line 7425	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_NoADTransition(void(*)(void *)) Line 7494	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase::FinalizerBase(void(*)(void *)) Line 7513	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadStart(void * args) Line 403	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

Thread exit with threadlocal cleanup

>	coreclr.dll!LoaderAllocator::SetHandleValue(unsigned __int64 handle, Object *) Line 992	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::FreeHandle(unsigned __int64 handle) Line 884	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTLM(unsigned __int64 i, int isThreadShuttingdown) Line 59	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTable() Line 93	C++	Symbols loaded.
[Inline Frame] coreclr.dll!Thread::DeleteThreadStaticData() Line 7704	C++	Symbols loaded.
coreclr.dll!Thread::OnThreadTerminate(int holdingLock) Line 2956	C++	Symbols loaded.
coreclr.dll!DestroyThread(Thread * th) Line 924	C++	Symbols loaded.
coreclr.dll!ThreadNative::KickOffThread(void * pass) Line 239	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

with an exception which is thrown with the following details:

Exception thrown: read access violation.
**loaderAllocator** was nullptr.
devenv_iJFpiWBvYe

loaderAllocator value from the LOADERALLOCATORREF loaderAllocator = (LOADERALLOCATORREF)ObjectFromHandle(m_hLoaderAllocatorObjectHandle); call is NULL which makes sense, because finalizer thread just disposed data for some of the same LoaderAllocator assemblies.

Note: enabling COR_PRF_MONITOR_MODULE_LOADS CLR profiler flag increases chances for the race condition as it slows down a bit complete loader destruction.

Changes:

I've looked at the following options as a solution to the problem:

  • Null check for loaderAllocator
  • Wait for GC/Finalizers done before killing thread
  • Skip cleaning up thread locals for unloaded loaders

Based on the discussion the acceptable solution would be to:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 20, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 20, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree company="Unity Technologies"

@alexey-zakharov
alexey-zakharov marked this pull request as ready for review March 20, 2024 08:55
@jkotasjkotas added area-VM-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 20, 2024
@jkotas
jkotas requested a review from janvorliMarch 20, 2024 09:05
Comment threadsrc/coreclr/vm/threadstatics.cpp Outdated
}
if (entry->m_hNonGCStatics != NULL)
// Skip assemblies that are already unloaded
if (!pLoaderAllocator->IsUnloaded())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we run into a situation where the thread gets rescheduled right after this check, the code in GCLoaderAllocators sets the flag and start destroying the loader allocators, and then the thread running this code gets scheduled back and still hits this crash?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question! I think it may happen and ideally FreeHandle call of LoaderAllocator itself should deal with its own state then, perhaps we could use m_crstLoaderAllocator lock, but it would mean some perf hit for the fast pointer path

my ideal expectation would be that since DeleteThreadStaticData is under GCX_COOP in Thread::OnThreadTerminate that should hopefully mean that once we are in DeleteThreadStaticData call we know for sure whether or not LoaderAllocator is alive or not and that state can not change - perhaps IsAlive() should be used as IsUnloaded is set on finalizer thread and not relevant to GCX_COOP 🤔

@davidwrighton

Copy link
Copy Markdown
Member

I'm in the process of trying to redo how all thread statics work at the moment, could we hold off on this change for a week or two while I see if I can get my larger change checked in. As it happens I had already identified the problem and the general gist of the solution I came up with is the same, although the mechanism is a fair bit different.

@jkotasjkotas added the blocked Issue/PR is blocked on something - see comments label Mar 20, 2024
@simonferquel

Copy link
Copy Markdown
Contributor

We (Unity) can confirm this fix is not enough and the race is still here:
image
@davidwrighton would you mind to ping us when your fix is ready ? - also is it planned to be backported to .net 8 in addition to 9 ?

@jkotas

Copy link
Copy Markdown
Member

@davidwrighton 's fix is included in #99183.

it is it planned to be backported to .net 8

#99183 is a large refactoring, it is not back portable. The fix for .NET 8 servicing would have to be a small, targeted fix.

@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

The fix for .NET 8 servicing would have to be a small, targeted fix.

@davidwrighton@jkotas do you think using pLoaderAllocator->GetExposedObject(); check in conjunction with GCX_COOP() section would be a better 8.0 targeted approach? (something like this)

the idea there is that if exposed object is alive we can safely free LoaderAllocator handles and while we are doing that it won't be collected and thus finalized

@davidwrighton

Copy link
Copy Markdown
Member

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

@alexey-zakharov
alexey-zakharovforce-pushed the upstream-fix-threadlocal-free branch from b89d190 to fff7409CompareMay 17, 2024 09:47
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

thanks for the update @davidwrighton !
yes, I've updated the PR to address the issue raised by @jkotas keeping in mind the following ideas:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

the fix seemed to be running fine for us - no crashes or deadlocks occurred since we've incorporated it.

@davidwrighton
davidwrighton merged commit 35e4aad into dotnet:mainMay 25, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@davidwrighton thanks for taking the fix in!

I'd approve that change, and then we can port it to .NET 8

to clarify - should I make a similar PR against .NET 8 branch?

@jkotas

Copy link
Copy Markdown
Member

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

Could you please look into fixing this failure and resubmit the change? We should make sure to trigger outer loop tests on the resubmitted PR before it gets merged.

@alexey-zakharov

alexey-zakharov commented May 29, 2024

Copy link
Copy Markdown
ContributorAuthor

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

thanks for the ping @jkotas ! I've reverted the unnecessary constraint changes which caused issues and running tests in #102797, let me know if there is a specific test I can run to validate potential regressions

@alexey-zakharov
alexey-zakharov deleted the upstream-fix-threadlocal-free branch May 30, 2024 08:40
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 30, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrblockedIssue/PR is blocked on something - see commentscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@alexey-zakharov@davidwrighton@simonferquel@jkotas
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Avoid clearing thread local handles for already unloaded loader - #99998

Merged
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free
May 25, 2024
Merged

Avoid clearing thread local handles for already unloaded loader#99998
davidwrighton merged 1 commit into
dotnet:mainfrom
Unity-Technologies:upstream-fix-threadlocal-free

Conversation

@alexey-zakharov

@alexey-zakharovalexey-zakharov commented Mar 20, 2024

Copy link
Copy Markdown
Contributor

Motivation:

Fix crash in ThreadLocalBlock::FreeTLM code on thread exit which happens during Assembly unloading when Assembly contains a class with a ThreadStatic variable.

Details:

There seem to be a race condition between LoaderAllocator cleanup during garbage collection and thread locals cleanup on thread exit. Racing stacks are the following:

ALC/LoaderAllocator cleanup

 coreclr.dll!EEToProfInterfaceImpl::ModuleUnloadStarted(unsigned __int64 moduleId) Line 3735	C++	Symbols loaded.
coreclr.dll!ProfControlBlock::DoProfilerCallbackHelper<int (__cdecl*)(ProfilerInfo *),long (__cdecl*)(EEToProfInterfaceImpl *,unsigned __int64),unsigned __int64>(ProfilerInfo * pProfilerInfo, int(*)(ProfilerInfo *) condition, HRESULT(*)(EEToProfInterfaceImpl *, unsigned __int64) callback, HRESULT * pHR, unsigned __int64 <args_0>) Line 284	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoOneProfilerIteration(ProfilerInfo *) Line 199	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::IterateProfilers(ProfilerCallbackType) Line 207	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::DoProfilerCallback(ProfilerCallbackType) Line 295	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ProfControlBlock::ModuleUnloadStarted(unsigned __int64) Line 691	C++	Symbols loaded.
coreclr.dll!Module::Destruct() Line 662	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ClassLoader::FreeModules() Line 1884	C++	Symbols loaded.
coreclr.dll!ClassLoader::~ClassLoader() Line 1946	C++	Symbols loaded.
coreclr.dll!Assembly::Terminate(int) Line 311	C++	Symbols loaded.
coreclr.dll!Assembly::~Assembly() Line 244	C++	Symbols loaded.
coreclr.dll!Assembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
coreclr.dll!DomainAssembly::~DomainAssembly() Line 91	C++	Symbols loaded.
coreclr.dll!DomainAssembly::`scalar deleting destructor'(unsigned int __flags)	C++	Symbols loaded.
>	coreclr.dll!LoaderAllocator::GCLoaderAllocators(LoaderAllocator * pOriginalLoaderAllocator) Line 570	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 702	C++	Symbols loaded.
coreclr.dll!LoaderAllocator_Destroy(QCall::LoaderAllocatorHandle pLoaderAllocator) Line 718	C++	Symbols loaded.
System.Private.CoreLib.dll!00007ffa9c4308f9()	Unknown	No symbols loaded.
coreclr.dll!FastCallFinalizeWorker() Line 26	Unknown	Symbols loaded.
coreclr.dll!MethodTable::CallFinalizer(Object * obj) Line 4908	C++	Symbols loaded.
[Inline Frame] coreclr.dll!CallFinalizer(Object *) Line 75	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizeAllObjects() Line 110	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadWorker(void * args) Line 354	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_DispatchInner(ManagedThreadCallState *) Line 7222	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchMiddle(ManagedThreadCallState * pCallState) Line 7266	C++	Symbols loaded.
coreclr.dll!ManagedThreadBase_DispatchOuter(ManagedThreadCallState * pCallState) Line 7425	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase_NoADTransition(void(*)(void *)) Line 7494	C++	Symbols loaded.
[Inline Frame] coreclr.dll!ManagedThreadBase::FinalizerBase(void(*)(void *)) Line 7513	C++	Symbols loaded.
coreclr.dll!FinalizerThread::FinalizerThreadStart(void * args) Line 403	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

Thread exit with threadlocal cleanup

>	coreclr.dll!LoaderAllocator::SetHandleValue(unsigned __int64 handle, Object *) Line 992	C++	Symbols loaded.
coreclr.dll!LoaderAllocator::FreeHandle(unsigned __int64 handle) Line 884	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTLM(unsigned __int64 i, int isThreadShuttingdown) Line 59	C++	Symbols loaded.
coreclr.dll!ThreadLocalBlock::FreeTable() Line 93	C++	Symbols loaded.
[Inline Frame] coreclr.dll!Thread::DeleteThreadStaticData() Line 7704	C++	Symbols loaded.
coreclr.dll!Thread::OnThreadTerminate(int holdingLock) Line 2956	C++	Symbols loaded.
coreclr.dll!DestroyThread(Thread * th) Line 924	C++	Symbols loaded.
coreclr.dll!ThreadNative::KickOffThread(void * pass) Line 239	C++	Symbols loaded.
kernel32.dll!BaseThreadInitThunk()	Unknown	Symbols loaded.
ntdll.dll!RtlUserThreadStart()	Unknown	Symbols loaded.

with an exception which is thrown with the following details:

Exception thrown: read access violation.
**loaderAllocator** was nullptr.
devenv_iJFpiWBvYe

loaderAllocator value from the LOADERALLOCATORREF loaderAllocator = (LOADERALLOCATORREF)ObjectFromHandle(m_hLoaderAllocatorObjectHandle); call is NULL which makes sense, because finalizer thread just disposed data for some of the same LoaderAllocator assemblies.

Note: enabling COR_PRF_MONITOR_MODULE_LOADS CLR profiler flag increases chances for the race condition as it slows down a bit complete loader destruction.

Changes:

I've looked at the following options as a solution to the problem:

  • Null check for loaderAllocator
  • Wait for GC/Finalizers done before killing thread
  • Skip cleaning up thread locals for unloaded loaders

Based on the discussion the acceptable solution would be to:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

@ghostghost added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Mar 20, 2024
@dotnet-policy-servicedotnet-policy-serviceBot added the community-contribution Indicates that the PR has been added by a community member label Mar 20, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@dotnet-policy-service agree company="Unity Technologies"

@alexey-zakharov
alexey-zakharov marked this pull request as ready for review March 20, 2024 08:55
@jkotasjkotas added area-VM-coreclr and removed needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners labels Mar 20, 2024
@jkotas
jkotas requested a review from janvorliMarch 20, 2024 09:05
Comment threadsrc/coreclr/vm/threadstatics.cpp Outdated
}
if (entry->m_hNonGCStatics != NULL)
// Skip assemblies that are already unloaded
if (!pLoaderAllocator->IsUnloaded())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we run into a situation where the thread gets rescheduled right after this check, the code in GCLoaderAllocators sets the flag and start destroying the loader allocators, and then the thread running this code gets scheduled back and still hits this crash?

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question! I think it may happen and ideally FreeHandle call of LoaderAllocator itself should deal with its own state then, perhaps we could use m_crstLoaderAllocator lock, but it would mean some perf hit for the fast pointer path

my ideal expectation would be that since DeleteThreadStaticData is under GCX_COOP in Thread::OnThreadTerminate that should hopefully mean that once we are in DeleteThreadStaticData call we know for sure whether or not LoaderAllocator is alive or not and that state can not change - perhaps IsAlive() should be used as IsUnloaded is set on finalizer thread and not relevant to GCX_COOP 🤔

@davidwrighton

Copy link
Copy Markdown
Member

I'm in the process of trying to redo how all thread statics work at the moment, could we hold off on this change for a week or two while I see if I can get my larger change checked in. As it happens I had already identified the problem and the general gist of the solution I came up with is the same, although the mechanism is a fair bit different.

@jkotasjkotas added the blocked Issue/PR is blocked on something - see comments label Mar 20, 2024
@simonferquel

Copy link
Copy Markdown
Contributor

We (Unity) can confirm this fix is not enough and the race is still here:
image
@davidwrighton would you mind to ping us when your fix is ready ? - also is it planned to be backported to .net 8 in addition to 9 ?

@jkotas

Copy link
Copy Markdown
Member

@davidwrighton 's fix is included in #99183.

it is it planned to be backported to .net 8

#99183 is a large refactoring, it is not back portable. The fix for .NET 8 servicing would have to be a small, targeted fix.

@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

The fix for .NET 8 servicing would have to be a small, targeted fix.

@davidwrighton@jkotas do you think using pLoaderAllocator->GetExposedObject(); check in conjunction with GCX_COOP() section would be a better 8.0 targeted approach? (something like this)

the idea there is that if exposed object is alive we can safely free LoaderAllocator handles and while we are doing that it won't be collected and thus finalized

@davidwrighton

Copy link
Copy Markdown
Member

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

@alexey-zakharov
alexey-zakharovforce-pushed the upstream-fix-threadlocal-free branch from b89d190 to fff7409CompareMay 17, 2024 09:47
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@alexey-zakharov Yes, that looks pretty reasonable, and should fix the issue. If you create a PR against current runtime, I'd approve that change, and then we can port it to .NET 8. If you can do that soonish, you'll likely beat my larger change in, as that's proving harder to finalize than I hoped.

thanks for the update @davidwrighton !
yes, I've updated the PR to address the issue raised by @jkotas keeping in mind the following ideas:

  • use coop mode to ensure the loader is not collected while we delete thread local handles (we already do it when setting a handle to NULL, so we elevate the scope with a minimal performance consequences)
  • if loader was already collected we do not need to reset handles as they are going to be deallocated anyway further down the loader destruction

the fix seemed to be running fine for us - no crashes or deadlocks occurred since we've incorporated it.

@davidwrighton
davidwrighton merged commit 35e4aad into dotnet:mainMay 25, 2024
@alexey-zakharov

Copy link
Copy Markdown
ContributorAuthor

@davidwrighton thanks for taking the fix in!

I'd approve that change, and then we can port it to .NET 8

to clarify - should I make a similar PR against .NET 8 branch?

@jkotas

Copy link
Copy Markdown
Member

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

Could you please look into fixing this failure and resubmit the change? We should make sure to trigger outer loop tests on the resubmitted PR before it gets merged.

@alexey-zakharov

alexey-zakharov commented May 29, 2024

Copy link
Copy Markdown
ContributorAuthor

Number of tests hit failures after this change #102719 . I am sorry I have to revert it to keep the CI clean.

thanks for the ping @jkotas ! I've reverted the unnecessary constraint changes which caused issues and running tests in #102797, let me know if there is a specific test I can run to validate potential regressions

@alexey-zakharov
alexey-zakharov deleted the upstream-fix-threadlocal-free branch May 30, 2024 08:40
Ruihan-Yin pushed a commit to Ruihan-Yin/runtime that referenced this pull request May 30, 2024
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 30, 2024
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-VM-coreclrblockedIssue/PR is blocked on something - see commentscommunity-contributionIndicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@alexey-zakharov@davidwrighton@simonferquel@jkotas