[clr-interp] Fix EnC failure when adding a generic method to a generic type - #127755

Closed
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst
Closed

[clr-interp] Fix EnC failure when adding a generic method to a generic type#127755
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 4, 2026

Copy link
Copy Markdown
Member

Description

Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod takes m_AvailableTypesLock and then calls into code that needs the same lock to publish the new method's generic parameters.

The other lock that protects type-loader tables, m_AvailableClassLock, is already configured to allow this kind of nested take. Configure m_AvailableTypesLock the same way. The other places that use this lock take and release it as a single unit, so they are unaffected.

Tests:

  • CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
  • CrossPlatformEnC.AddGenericStaticMethodOnGenericType
  • CrossPlatformEnC.GenericMethodsWithExpressions

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates CoreCLR EnC method addition for generic types so EEClass::AddMethod no longer holds m_AvailableTypesLock while creating MethodDescs for existing generic instantiations. In the VM, this avoids same-thread lock re-entry during generic-method setup on the debugger/EnC path.

Changes:

  • Snapshot matching generic instantiations from GetAvailableParamTypes() while holding AvailableTypesLock.
  • Release the lock before calling AddMethodDesc for each collected instantiation.
  • Preserve the existing per-instantiation method/async-variant creation flow after the lock is released.
Show a summary per file
FileDescription
src/coreclr/vm/class.cppChanges EEClass::AddMethod to gather matching instantiated MethodTable* entries under the available-types lock, then add new MethodDescs after releasing that lock.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmiloskotlarmilos changed the title [interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethodMay 4, 2026
@jkotas

Copy link
Copy Markdown
Member

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

instantiations.Append(pMTMaybe);
}
}

@jkotasjkotasMay 4, 2026

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.

What guarantees that there won't be more instantiations added in the meantime by other threads?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good question. The original loop held m_AvailableTypesLock across the whole iteration, which prevented other threads from publishing new instantiations of the same generic type into m_AvailableParamTypes while we were attaching the new method.

I'll update the PR to keep the lock continuously.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Actually, I updated SetupGenericMethodDefinition skip taking the lock when the current thread already owns it. The lock protects Module.m_GenericParamToDescMap, so a caller that already holds it is in the same protected region.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Found better fix - mark m_AvailableTypesLock reentrant which matches m_AvailableClassLock, instead of conditionally skipping the inner acquire.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copilot pointed out that allowing reentrancy only fixes the self-recursive case and would still trip the lock-level violation when an instantiation lives in a different loader module, so I reverted to collecting the matching instantiations under the lock and releasing it before calling AddMethodDesc.

There is no guarantee that other threads won't insert new instantiations after we release, but they don't need to be in the collected set. We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method through the normal MethodTableBuilder::Build path.

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.

We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method

There is still a race condition: Some other thread could have started building an instantiation before the metadata edit and it may finish building it only after all this is done. This instantiation would be missing the newly added method.

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.

Hmm, this is actually a pre-existing bug. This change may make it more likely to be hit (the window where the race condition can happen is larger).

Discussion at #125397 (comment) is related to this. Type loader is designed to create new entities lazily. We try to create MethodDescs eagerly here. It turns out it is hard to do that 100% reliably. The correct fix would be to switch to creating these MethodDesc lazily, but that is likely to come with a bug tail.

The proposed fix should not make it worse.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Thanks, since it isn't interpreter-specific, I created a tracking issue to decide on approach #127851

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

Sorry I put the wrong method name. The actual call is InstantiatedMethodDesc::SetupGenericMethodDefinition

Test log:

 00:22.769: 3> Current IP: Main() [new G<int>().AsString<string>("2");] @ AddGenericInstanceMethodOnGenericType.cs(22,8) - IP=0x0000000D - Native IP=0x00000074
00:23.001: 3> Generation 1
STDERROR: 00:25.626: ASSERT FAILED
STDERROR: 00:25.626: Expression: g_fEEShutDown || !"Crst Reentrancy violation"
STDERROR: 00:25.626: Location: /Users/miloskotlar/dotnet/runtime-fu-enc/src/coreclr/vm/crst.cpp:638
STDERROR: 00:25.626: Function: IsSafeToTake
STDERROR: 00:25.626: Process: 13252
00:25.660: 18> Unexpected debuggee termination

Stack trace:

 DBG_DebugBreak
DebugBreak
CrstBase::IsSafeToTake (vm/crst.cpp:638)
CrstBase::Enter
CrstBase::AcquireLock
CrstBase::CrstHolder::CrstHolder
CrstBase::CrstHolder::CrstHolder
InstantiatedMethodDesc::SetupGenericMethodDefinition+0x550 (vm/genmeth.cpp:1473)
MethodTableBuilder::InitMethodDesc+0x4cc (vm/methodtablebuilder.cpp:6352)
EEClass::AddMethodDesc+0x714 (vm/class.cpp:765)
EEClass::AddMethod+0xbcc (vm/class.cpp:728) <-- holds m_AvailableTypesLock since vm/class.cpp:709
EditAndContinueModule::AddMethod+0x19c
EditAndContinueModule::ApplyEditAndContinue+0x1b8c
EEDbgInterfaceImpl::EnCApplyChanges+0x68
Debugger::ApplyChangesAndSendResult+0xb0
Debugger::HandleIPCEvent+0x20c4
HandleIPCEventWrapper+0x58
DebuggerRCThread::HandleRSEA+0x7c
DebuggerRCThread::MainLoop+0x5c8
DebuggerRCThread::ThreadProc+0x960
DebuggerRCThread::ThreadProcStatic+0xac
CorUnix::CPalThread::ThreadEntry+0x244
_pthread_start+0x88
thread_start+0x8

@kotlarmiloskotlarmilos changed the title [clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title [clr-interp] Fix EnC failure when adding a generic method to a generic typeAllow CrstAvailableParamTypes to be acquired recursivelyMay 5, 2026
CopilotAI review requested due to automatic review settings May 5, 2026 09:42
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from 3999b1e to d53448fCompareMay 5, 2026 09:42
@kotlarmiloskotlarmilos changed the title Allow CrstAvailableParamTypes to be acquired recursivelyFix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title Fix EnC failure when adding a generic method to a generic type[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

Comment threadsrc/coreclr/vm/clsload.cpp Outdated
Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod walks every existing instantiation of the type while holding the owning module's CrstAvailableParamTypes, and inside the loop calls AddMethodDesc. For a generic method that path reaches InstantiatedMethodDesc::SetupGenericMethodDefinition which itself takes CrstAvailableParamTypes (potentially on a different module's classloader if the instantiation lives there). Taking the same-level lock again trips the Crst lock-level assert in checked builds and would deadlock in release.
Split the walk into two phases. First, under the lock, iterate the type hash and collect the matching MethodTable* instantiations into a local array. Then release the lock and call AddMethodDesc on each collected instantiation. The hash iteration is still protected, and the per-instantiation method setup runs without holding the lock so SetupGenericMethodDefinition can acquire whatever CrstAvailableParamTypes instance it needs without violating lock ordering. This handles both the same-module and cross-loader-module cases.
Tests:
- CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
- CrossPlatformEnC.AddGenericStaticMethodOnGenericType
- CrossPlatformEnC.GenericMethodsWithExpressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from d53448f to 90ffacbCompareMay 5, 2026 10:31
Comment threadsrc/coreclr/vm/class.cpp
@jkotas
jkotas requested a review from noahfalkMay 6, 2026 03:49
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
CopilotAI review requested due to automatic review settings May 6, 2026 09:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 3

Comment on lines +696 to +698
// This code contains a race condition bug. Types that began loading before the metadata edit
// and are still loading at this point may not get updated. We assume that this issue does not
// have a meaningful impact on overall metadata update reliability.
Comment on lines +732 to +736
instantiations.Append(pMTMaybe);
}
}

for (COUNT_T i = 0; i < instantiations.GetCount(); i++)
Comment on lines +712 to 716
InlineSArray<MethodTable*, 8> instantiations;
{
TypeHandle th = pEntry->GetTypeHandle();
if (th.IsTypeDesc())
continue;
EETypeHashTable* paramTypes = pMod->GetAvailableParamTypes();
CrstHolder ch(pMod->GetClassLoader()->GetAvailableTypesLock());

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

The failures are not interpreter-specific and the work will be tracked in #127851

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 5, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@kotlarmilos@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

[clr-interp] Fix EnC failure when adding a generic method to a generic type - #127755

Closed
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst
Closed

[clr-interp] Fix EnC failure when adding a generic method to a generic type#127755
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 4, 2026

Copy link
Copy Markdown
Member

Description

Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod takes m_AvailableTypesLock and then calls into code that needs the same lock to publish the new method's generic parameters.

The other lock that protects type-loader tables, m_AvailableClassLock, is already configured to allow this kind of nested take. Configure m_AvailableTypesLock the same way. The other places that use this lock take and release it as a single unit, so they are unaffected.

Tests:

  • CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
  • CrossPlatformEnC.AddGenericStaticMethodOnGenericType
  • CrossPlatformEnC.GenericMethodsWithExpressions

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates CoreCLR EnC method addition for generic types so EEClass::AddMethod no longer holds m_AvailableTypesLock while creating MethodDescs for existing generic instantiations. In the VM, this avoids same-thread lock re-entry during generic-method setup on the debugger/EnC path.

Changes:

  • Snapshot matching generic instantiations from GetAvailableParamTypes() while holding AvailableTypesLock.
  • Release the lock before calling AddMethodDesc for each collected instantiation.
  • Preserve the existing per-instantiation method/async-variant creation flow after the lock is released.
Show a summary per file
FileDescription
src/coreclr/vm/class.cppChanges EEClass::AddMethod to gather matching instantiated MethodTable* entries under the available-types lock, then add new MethodDescs after releasing that lock.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmiloskotlarmilos changed the title [interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethodMay 4, 2026
@jkotas

Copy link
Copy Markdown
Member

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

instantiations.Append(pMTMaybe);
}
}

@jkotasjkotasMay 4, 2026

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.

What guarantees that there won't be more instantiations added in the meantime by other threads?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good question. The original loop held m_AvailableTypesLock across the whole iteration, which prevented other threads from publishing new instantiations of the same generic type into m_AvailableParamTypes while we were attaching the new method.

I'll update the PR to keep the lock continuously.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Actually, I updated SetupGenericMethodDefinition skip taking the lock when the current thread already owns it. The lock protects Module.m_GenericParamToDescMap, so a caller that already holds it is in the same protected region.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Found better fix - mark m_AvailableTypesLock reentrant which matches m_AvailableClassLock, instead of conditionally skipping the inner acquire.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copilot pointed out that allowing reentrancy only fixes the self-recursive case and would still trip the lock-level violation when an instantiation lives in a different loader module, so I reverted to collecting the matching instantiations under the lock and releasing it before calling AddMethodDesc.

There is no guarantee that other threads won't insert new instantiations after we release, but they don't need to be in the collected set. We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method through the normal MethodTableBuilder::Build path.

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.

We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method

There is still a race condition: Some other thread could have started building an instantiation before the metadata edit and it may finish building it only after all this is done. This instantiation would be missing the newly added method.

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.

Hmm, this is actually a pre-existing bug. This change may make it more likely to be hit (the window where the race condition can happen is larger).

Discussion at #125397 (comment) is related to this. Type loader is designed to create new entities lazily. We try to create MethodDescs eagerly here. It turns out it is hard to do that 100% reliably. The correct fix would be to switch to creating these MethodDesc lazily, but that is likely to come with a bug tail.

The proposed fix should not make it worse.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Thanks, since it isn't interpreter-specific, I created a tracking issue to decide on approach #127851

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

Sorry I put the wrong method name. The actual call is InstantiatedMethodDesc::SetupGenericMethodDefinition

Test log:

 00:22.769: 3> Current IP: Main() [new G<int>().AsString<string>("2");] @ AddGenericInstanceMethodOnGenericType.cs(22,8) - IP=0x0000000D - Native IP=0x00000074
00:23.001: 3> Generation 1
STDERROR: 00:25.626: ASSERT FAILED
STDERROR: 00:25.626: Expression: g_fEEShutDown || !"Crst Reentrancy violation"
STDERROR: 00:25.626: Location: /Users/miloskotlar/dotnet/runtime-fu-enc/src/coreclr/vm/crst.cpp:638
STDERROR: 00:25.626: Function: IsSafeToTake
STDERROR: 00:25.626: Process: 13252
00:25.660: 18> Unexpected debuggee termination

Stack trace:

 DBG_DebugBreak
DebugBreak
CrstBase::IsSafeToTake (vm/crst.cpp:638)
CrstBase::Enter
CrstBase::AcquireLock
CrstBase::CrstHolder::CrstHolder
CrstBase::CrstHolder::CrstHolder
InstantiatedMethodDesc::SetupGenericMethodDefinition+0x550 (vm/genmeth.cpp:1473)
MethodTableBuilder::InitMethodDesc+0x4cc (vm/methodtablebuilder.cpp:6352)
EEClass::AddMethodDesc+0x714 (vm/class.cpp:765)
EEClass::AddMethod+0xbcc (vm/class.cpp:728) <-- holds m_AvailableTypesLock since vm/class.cpp:709
EditAndContinueModule::AddMethod+0x19c
EditAndContinueModule::ApplyEditAndContinue+0x1b8c
EEDbgInterfaceImpl::EnCApplyChanges+0x68
Debugger::ApplyChangesAndSendResult+0xb0
Debugger::HandleIPCEvent+0x20c4
HandleIPCEventWrapper+0x58
DebuggerRCThread::HandleRSEA+0x7c
DebuggerRCThread::MainLoop+0x5c8
DebuggerRCThread::ThreadProc+0x960
DebuggerRCThread::ThreadProcStatic+0xac
CorUnix::CPalThread::ThreadEntry+0x244
_pthread_start+0x88
thread_start+0x8

@kotlarmiloskotlarmilos changed the title [clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title [clr-interp] Fix EnC failure when adding a generic method to a generic typeAllow CrstAvailableParamTypes to be acquired recursivelyMay 5, 2026
CopilotAI review requested due to automatic review settings May 5, 2026 09:42
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from 3999b1e to d53448fCompareMay 5, 2026 09:42
@kotlarmiloskotlarmilos changed the title Allow CrstAvailableParamTypes to be acquired recursivelyFix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title Fix EnC failure when adding a generic method to a generic type[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

Comment threadsrc/coreclr/vm/clsload.cpp Outdated
Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod walks every existing instantiation of the type while holding the owning module's CrstAvailableParamTypes, and inside the loop calls AddMethodDesc. For a generic method that path reaches InstantiatedMethodDesc::SetupGenericMethodDefinition which itself takes CrstAvailableParamTypes (potentially on a different module's classloader if the instantiation lives there). Taking the same-level lock again trips the Crst lock-level assert in checked builds and would deadlock in release.
Split the walk into two phases. First, under the lock, iterate the type hash and collect the matching MethodTable* instantiations into a local array. Then release the lock and call AddMethodDesc on each collected instantiation. The hash iteration is still protected, and the per-instantiation method setup runs without holding the lock so SetupGenericMethodDefinition can acquire whatever CrstAvailableParamTypes instance it needs without violating lock ordering. This handles both the same-module and cross-loader-module cases.
Tests:
- CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
- CrossPlatformEnC.AddGenericStaticMethodOnGenericType
- CrossPlatformEnC.GenericMethodsWithExpressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from d53448f to 90ffacbCompareMay 5, 2026 10:31
Comment threadsrc/coreclr/vm/class.cpp
@jkotas
jkotas requested a review from noahfalkMay 6, 2026 03:49
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
CopilotAI review requested due to automatic review settings May 6, 2026 09:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 3

Comment on lines +696 to +698
// This code contains a race condition bug. Types that began loading before the metadata edit
// and are still loading at this point may not get updated. We assume that this issue does not
// have a meaningful impact on overall metadata update reliability.
Comment on lines +732 to +736
instantiations.Append(pMTMaybe);
}
}

for (COUNT_T i = 0; i < instantiations.GetCount(); i++)
Comment on lines +712 to 716
InlineSArray<MethodTable*, 8> instantiations;
{
TypeHandle th = pEntry->GetTypeHandle();
if (th.IsTypeDesc())
continue;
EETypeHashTable* paramTypes = pMod->GetAvailableParamTypes();
CrstHolder ch(pMod->GetClassLoader()->GetAvailableTypesLock());

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

The failures are not interpreter-specific and the work will be tracked in #127851

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 5, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@kotlarmilos@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

[clr-interp] Fix EnC failure when adding a generic method to a generic type - #127755

Closed
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst
Closed

[clr-interp] Fix EnC failure when adding a generic method to a generic type#127755
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 4, 2026

Copy link
Copy Markdown
Member

Description

Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod takes m_AvailableTypesLock and then calls into code that needs the same lock to publish the new method's generic parameters.

The other lock that protects type-loader tables, m_AvailableClassLock, is already configured to allow this kind of nested take. Configure m_AvailableTypesLock the same way. The other places that use this lock take and release it as a single unit, so they are unaffected.

Tests:

  • CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
  • CrossPlatformEnC.AddGenericStaticMethodOnGenericType
  • CrossPlatformEnC.GenericMethodsWithExpressions

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates CoreCLR EnC method addition for generic types so EEClass::AddMethod no longer holds m_AvailableTypesLock while creating MethodDescs for existing generic instantiations. In the VM, this avoids same-thread lock re-entry during generic-method setup on the debugger/EnC path.

Changes:

  • Snapshot matching generic instantiations from GetAvailableParamTypes() while holding AvailableTypesLock.
  • Release the lock before calling AddMethodDesc for each collected instantiation.
  • Preserve the existing per-instantiation method/async-variant creation flow after the lock is released.
Show a summary per file
FileDescription
src/coreclr/vm/class.cppChanges EEClass::AddMethod to gather matching instantiated MethodTable* entries under the available-types lock, then add new MethodDescs after releasing that lock.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmiloskotlarmilos changed the title [interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethodMay 4, 2026
@jkotas

Copy link
Copy Markdown
Member

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

instantiations.Append(pMTMaybe);
}
}

@jkotasjkotasMay 4, 2026

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.

What guarantees that there won't be more instantiations added in the meantime by other threads?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good question. The original loop held m_AvailableTypesLock across the whole iteration, which prevented other threads from publishing new instantiations of the same generic type into m_AvailableParamTypes while we were attaching the new method.

I'll update the PR to keep the lock continuously.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Actually, I updated SetupGenericMethodDefinition skip taking the lock when the current thread already owns it. The lock protects Module.m_GenericParamToDescMap, so a caller that already holds it is in the same protected region.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Found better fix - mark m_AvailableTypesLock reentrant which matches m_AvailableClassLock, instead of conditionally skipping the inner acquire.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copilot pointed out that allowing reentrancy only fixes the self-recursive case and would still trip the lock-level violation when an instantiation lives in a different loader module, so I reverted to collecting the matching instantiations under the lock and releasing it before calling AddMethodDesc.

There is no guarantee that other threads won't insert new instantiations after we release, but they don't need to be in the collected set. We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method through the normal MethodTableBuilder::Build path.

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.

We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method

There is still a race condition: Some other thread could have started building an instantiation before the metadata edit and it may finish building it only after all this is done. This instantiation would be missing the newly added method.

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.

Hmm, this is actually a pre-existing bug. This change may make it more likely to be hit (the window where the race condition can happen is larger).

Discussion at #125397 (comment) is related to this. Type loader is designed to create new entities lazily. We try to create MethodDescs eagerly here. It turns out it is hard to do that 100% reliably. The correct fix would be to switch to creating these MethodDesc lazily, but that is likely to come with a bug tail.

The proposed fix should not make it worse.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Thanks, since it isn't interpreter-specific, I created a tracking issue to decide on approach #127851

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

Sorry I put the wrong method name. The actual call is InstantiatedMethodDesc::SetupGenericMethodDefinition

Test log:

 00:22.769: 3> Current IP: Main() [new G<int>().AsString<string>("2");] @ AddGenericInstanceMethodOnGenericType.cs(22,8) - IP=0x0000000D - Native IP=0x00000074
00:23.001: 3> Generation 1
STDERROR: 00:25.626: ASSERT FAILED
STDERROR: 00:25.626: Expression: g_fEEShutDown || !"Crst Reentrancy violation"
STDERROR: 00:25.626: Location: /Users/miloskotlar/dotnet/runtime-fu-enc/src/coreclr/vm/crst.cpp:638
STDERROR: 00:25.626: Function: IsSafeToTake
STDERROR: 00:25.626: Process: 13252
00:25.660: 18> Unexpected debuggee termination

Stack trace:

 DBG_DebugBreak
DebugBreak
CrstBase::IsSafeToTake (vm/crst.cpp:638)
CrstBase::Enter
CrstBase::AcquireLock
CrstBase::CrstHolder::CrstHolder
CrstBase::CrstHolder::CrstHolder
InstantiatedMethodDesc::SetupGenericMethodDefinition+0x550 (vm/genmeth.cpp:1473)
MethodTableBuilder::InitMethodDesc+0x4cc (vm/methodtablebuilder.cpp:6352)
EEClass::AddMethodDesc+0x714 (vm/class.cpp:765)
EEClass::AddMethod+0xbcc (vm/class.cpp:728) <-- holds m_AvailableTypesLock since vm/class.cpp:709
EditAndContinueModule::AddMethod+0x19c
EditAndContinueModule::ApplyEditAndContinue+0x1b8c
EEDbgInterfaceImpl::EnCApplyChanges+0x68
Debugger::ApplyChangesAndSendResult+0xb0
Debugger::HandleIPCEvent+0x20c4
HandleIPCEventWrapper+0x58
DebuggerRCThread::HandleRSEA+0x7c
DebuggerRCThread::MainLoop+0x5c8
DebuggerRCThread::ThreadProc+0x960
DebuggerRCThread::ThreadProcStatic+0xac
CorUnix::CPalThread::ThreadEntry+0x244
_pthread_start+0x88
thread_start+0x8

@kotlarmiloskotlarmilos changed the title [clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title [clr-interp] Fix EnC failure when adding a generic method to a generic typeAllow CrstAvailableParamTypes to be acquired recursivelyMay 5, 2026
CopilotAI review requested due to automatic review settings May 5, 2026 09:42
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from 3999b1e to d53448fCompareMay 5, 2026 09:42
@kotlarmiloskotlarmilos changed the title Allow CrstAvailableParamTypes to be acquired recursivelyFix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title Fix EnC failure when adding a generic method to a generic type[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

Comment threadsrc/coreclr/vm/clsload.cpp Outdated
Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod walks every existing instantiation of the type while holding the owning module's CrstAvailableParamTypes, and inside the loop calls AddMethodDesc. For a generic method that path reaches InstantiatedMethodDesc::SetupGenericMethodDefinition which itself takes CrstAvailableParamTypes (potentially on a different module's classloader if the instantiation lives there). Taking the same-level lock again trips the Crst lock-level assert in checked builds and would deadlock in release.
Split the walk into two phases. First, under the lock, iterate the type hash and collect the matching MethodTable* instantiations into a local array. Then release the lock and call AddMethodDesc on each collected instantiation. The hash iteration is still protected, and the per-instantiation method setup runs without holding the lock so SetupGenericMethodDefinition can acquire whatever CrstAvailableParamTypes instance it needs without violating lock ordering. This handles both the same-module and cross-loader-module cases.
Tests:
- CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
- CrossPlatformEnC.AddGenericStaticMethodOnGenericType
- CrossPlatformEnC.GenericMethodsWithExpressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from d53448f to 90ffacbCompareMay 5, 2026 10:31
Comment threadsrc/coreclr/vm/class.cpp
@jkotas
jkotas requested a review from noahfalkMay 6, 2026 03:49
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
CopilotAI review requested due to automatic review settings May 6, 2026 09:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 3

Comment on lines +696 to +698
// This code contains a race condition bug. Types that began loading before the metadata edit
// and are still loading at this point may not get updated. We assume that this issue does not
// have a meaningful impact on overall metadata update reliability.
Comment on lines +732 to +736
instantiations.Append(pMTMaybe);
}
}

for (COUNT_T i = 0; i < instantiations.GetCount(); i++)
Comment on lines +712 to 716
InlineSArray<MethodTable*, 8> instantiations;
{
TypeHandle th = pEntry->GetTypeHandle();
if (th.IsTypeDesc())
continue;
EETypeHashTable* paramTypes = pMod->GetAvailableParamTypes();
CrstHolder ch(pMod->GetClassLoader()->GetAvailableTypesLock());

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

The failures are not interpreter-specific and the work will be tracked in #127851

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 5, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@kotlarmilos@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

[clr-interp] Fix EnC failure when adding a generic method to a generic type - #127755

Closed
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst
Closed

[clr-interp] Fix EnC failure when adding a generic method to a generic type#127755
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 4, 2026

Copy link
Copy Markdown
Member

Description

Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod takes m_AvailableTypesLock and then calls into code that needs the same lock to publish the new method's generic parameters.

The other lock that protects type-loader tables, m_AvailableClassLock, is already configured to allow this kind of nested take. Configure m_AvailableTypesLock the same way. The other places that use this lock take and release it as a single unit, so they are unaffected.

Tests:

  • CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
  • CrossPlatformEnC.AddGenericStaticMethodOnGenericType
  • CrossPlatformEnC.GenericMethodsWithExpressions

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates CoreCLR EnC method addition for generic types so EEClass::AddMethod no longer holds m_AvailableTypesLock while creating MethodDescs for existing generic instantiations. In the VM, this avoids same-thread lock re-entry during generic-method setup on the debugger/EnC path.

Changes:

  • Snapshot matching generic instantiations from GetAvailableParamTypes() while holding AvailableTypesLock.
  • Release the lock before calling AddMethodDesc for each collected instantiation.
  • Preserve the existing per-instantiation method/async-variant creation flow after the lock is released.
Show a summary per file
FileDescription
src/coreclr/vm/class.cppChanges EEClass::AddMethod to gather matching instantiated MethodTable* entries under the available-types lock, then add new MethodDescs after releasing that lock.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmiloskotlarmilos changed the title [interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethodMay 4, 2026
@jkotas

Copy link
Copy Markdown
Member

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

instantiations.Append(pMTMaybe);
}
}

@jkotasjkotasMay 4, 2026

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.

What guarantees that there won't be more instantiations added in the meantime by other threads?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good question. The original loop held m_AvailableTypesLock across the whole iteration, which prevented other threads from publishing new instantiations of the same generic type into m_AvailableParamTypes while we were attaching the new method.

I'll update the PR to keep the lock continuously.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Actually, I updated SetupGenericMethodDefinition skip taking the lock when the current thread already owns it. The lock protects Module.m_GenericParamToDescMap, so a caller that already holds it is in the same protected region.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Found better fix - mark m_AvailableTypesLock reentrant which matches m_AvailableClassLock, instead of conditionally skipping the inner acquire.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copilot pointed out that allowing reentrancy only fixes the self-recursive case and would still trip the lock-level violation when an instantiation lives in a different loader module, so I reverted to collecting the matching instantiations under the lock and releasing it before calling AddMethodDesc.

There is no guarantee that other threads won't insert new instantiations after we release, but they don't need to be in the collected set. We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method through the normal MethodTableBuilder::Build path.

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.

We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method

There is still a race condition: Some other thread could have started building an instantiation before the metadata edit and it may finish building it only after all this is done. This instantiation would be missing the newly added method.

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.

Hmm, this is actually a pre-existing bug. This change may make it more likely to be hit (the window where the race condition can happen is larger).

Discussion at #125397 (comment) is related to this. Type loader is designed to create new entities lazily. We try to create MethodDescs eagerly here. It turns out it is hard to do that 100% reliably. The correct fix would be to switch to creating these MethodDesc lazily, but that is likely to come with a bug tail.

The proposed fix should not make it worse.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Thanks, since it isn't interpreter-specific, I created a tracking issue to decide on approach #127851

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

Sorry I put the wrong method name. The actual call is InstantiatedMethodDesc::SetupGenericMethodDefinition

Test log:

 00:22.769: 3> Current IP: Main() [new G<int>().AsString<string>("2");] @ AddGenericInstanceMethodOnGenericType.cs(22,8) - IP=0x0000000D - Native IP=0x00000074
00:23.001: 3> Generation 1
STDERROR: 00:25.626: ASSERT FAILED
STDERROR: 00:25.626: Expression: g_fEEShutDown || !"Crst Reentrancy violation"
STDERROR: 00:25.626: Location: /Users/miloskotlar/dotnet/runtime-fu-enc/src/coreclr/vm/crst.cpp:638
STDERROR: 00:25.626: Function: IsSafeToTake
STDERROR: 00:25.626: Process: 13252
00:25.660: 18> Unexpected debuggee termination

Stack trace:

 DBG_DebugBreak
DebugBreak
CrstBase::IsSafeToTake (vm/crst.cpp:638)
CrstBase::Enter
CrstBase::AcquireLock
CrstBase::CrstHolder::CrstHolder
CrstBase::CrstHolder::CrstHolder
InstantiatedMethodDesc::SetupGenericMethodDefinition+0x550 (vm/genmeth.cpp:1473)
MethodTableBuilder::InitMethodDesc+0x4cc (vm/methodtablebuilder.cpp:6352)
EEClass::AddMethodDesc+0x714 (vm/class.cpp:765)
EEClass::AddMethod+0xbcc (vm/class.cpp:728) <-- holds m_AvailableTypesLock since vm/class.cpp:709
EditAndContinueModule::AddMethod+0x19c
EditAndContinueModule::ApplyEditAndContinue+0x1b8c
EEDbgInterfaceImpl::EnCApplyChanges+0x68
Debugger::ApplyChangesAndSendResult+0xb0
Debugger::HandleIPCEvent+0x20c4
HandleIPCEventWrapper+0x58
DebuggerRCThread::HandleRSEA+0x7c
DebuggerRCThread::MainLoop+0x5c8
DebuggerRCThread::ThreadProc+0x960
DebuggerRCThread::ThreadProcStatic+0xac
CorUnix::CPalThread::ThreadEntry+0x244
_pthread_start+0x88
thread_start+0x8

@kotlarmiloskotlarmilos changed the title [clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title [clr-interp] Fix EnC failure when adding a generic method to a generic typeAllow CrstAvailableParamTypes to be acquired recursivelyMay 5, 2026
CopilotAI review requested due to automatic review settings May 5, 2026 09:42
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from 3999b1e to d53448fCompareMay 5, 2026 09:42
@kotlarmiloskotlarmilos changed the title Allow CrstAvailableParamTypes to be acquired recursivelyFix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title Fix EnC failure when adding a generic method to a generic type[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

Comment threadsrc/coreclr/vm/clsload.cpp Outdated
Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod walks every existing instantiation of the type while holding the owning module's CrstAvailableParamTypes, and inside the loop calls AddMethodDesc. For a generic method that path reaches InstantiatedMethodDesc::SetupGenericMethodDefinition which itself takes CrstAvailableParamTypes (potentially on a different module's classloader if the instantiation lives there). Taking the same-level lock again trips the Crst lock-level assert in checked builds and would deadlock in release.
Split the walk into two phases. First, under the lock, iterate the type hash and collect the matching MethodTable* instantiations into a local array. Then release the lock and call AddMethodDesc on each collected instantiation. The hash iteration is still protected, and the per-instantiation method setup runs without holding the lock so SetupGenericMethodDefinition can acquire whatever CrstAvailableParamTypes instance it needs without violating lock ordering. This handles both the same-module and cross-loader-module cases.
Tests:
- CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
- CrossPlatformEnC.AddGenericStaticMethodOnGenericType
- CrossPlatformEnC.GenericMethodsWithExpressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from d53448f to 90ffacbCompareMay 5, 2026 10:31
Comment threadsrc/coreclr/vm/class.cpp
@jkotas
jkotas requested a review from noahfalkMay 6, 2026 03:49
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
CopilotAI review requested due to automatic review settings May 6, 2026 09:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 3

Comment on lines +696 to +698
// This code contains a race condition bug. Types that began loading before the metadata edit
// and are still loading at this point may not get updated. We assume that this issue does not
// have a meaningful impact on overall metadata update reliability.
Comment on lines +732 to +736
instantiations.Append(pMTMaybe);
}
}

for (COUNT_T i = 0; i < instantiations.GetCount(); i++)
Comment on lines +712 to 716
InlineSArray<MethodTable*, 8> instantiations;
{
TypeHandle th = pEntry->GetTypeHandle();
if (th.IsTypeDesc())
continue;
EETypeHashTable* paramTypes = pMod->GetAvailableParamTypes();
CrstHolder ch(pMod->GetClassLoader()->GetAvailableTypesLock());

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

The failures are not interpreter-specific and the work will be tracked in #127851

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 5, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@kotlarmilos@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

[clr-interp] Fix EnC failure when adding a generic method to a generic type - #127755

Closed
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst
Closed

[clr-interp] Fix EnC failure when adding a generic method to a generic type#127755
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 4, 2026

Copy link
Copy Markdown
Member

Description

Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod takes m_AvailableTypesLock and then calls into code that needs the same lock to publish the new method's generic parameters.

The other lock that protects type-loader tables, m_AvailableClassLock, is already configured to allow this kind of nested take. Configure m_AvailableTypesLock the same way. The other places that use this lock take and release it as a single unit, so they are unaffected.

Tests:

  • CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
  • CrossPlatformEnC.AddGenericStaticMethodOnGenericType
  • CrossPlatformEnC.GenericMethodsWithExpressions

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates CoreCLR EnC method addition for generic types so EEClass::AddMethod no longer holds m_AvailableTypesLock while creating MethodDescs for existing generic instantiations. In the VM, this avoids same-thread lock re-entry during generic-method setup on the debugger/EnC path.

Changes:

  • Snapshot matching generic instantiations from GetAvailableParamTypes() while holding AvailableTypesLock.
  • Release the lock before calling AddMethodDesc for each collected instantiation.
  • Preserve the existing per-instantiation method/async-variant creation flow after the lock is released.
Show a summary per file
FileDescription
src/coreclr/vm/class.cppChanges EEClass::AddMethod to gather matching instantiated MethodTable* entries under the available-types lock, then add new MethodDescs after releasing that lock.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmiloskotlarmilos changed the title [interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethodMay 4, 2026
@jkotas

Copy link
Copy Markdown
Member

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

instantiations.Append(pMTMaybe);
}
}

@jkotasjkotasMay 4, 2026

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.

What guarantees that there won't be more instantiations added in the meantime by other threads?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good question. The original loop held m_AvailableTypesLock across the whole iteration, which prevented other threads from publishing new instantiations of the same generic type into m_AvailableParamTypes while we were attaching the new method.

I'll update the PR to keep the lock continuously.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Actually, I updated SetupGenericMethodDefinition skip taking the lock when the current thread already owns it. The lock protects Module.m_GenericParamToDescMap, so a caller that already holds it is in the same protected region.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Found better fix - mark m_AvailableTypesLock reentrant which matches m_AvailableClassLock, instead of conditionally skipping the inner acquire.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copilot pointed out that allowing reentrancy only fixes the self-recursive case and would still trip the lock-level violation when an instantiation lives in a different loader module, so I reverted to collecting the matching instantiations under the lock and releasing it before calling AddMethodDesc.

There is no guarantee that other threads won't insert new instantiations after we release, but they don't need to be in the collected set. We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method through the normal MethodTableBuilder::Build path.

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.

We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method

There is still a race condition: Some other thread could have started building an instantiation before the metadata edit and it may finish building it only after all this is done. This instantiation would be missing the newly added method.

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.

Hmm, this is actually a pre-existing bug. This change may make it more likely to be hit (the window where the race condition can happen is larger).

Discussion at #125397 (comment) is related to this. Type loader is designed to create new entities lazily. We try to create MethodDescs eagerly here. It turns out it is hard to do that 100% reliably. The correct fix would be to switch to creating these MethodDesc lazily, but that is likely to come with a bug tail.

The proposed fix should not make it worse.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Thanks, since it isn't interpreter-specific, I created a tracking issue to decide on approach #127851

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

Sorry I put the wrong method name. The actual call is InstantiatedMethodDesc::SetupGenericMethodDefinition

Test log:

 00:22.769: 3> Current IP: Main() [new G<int>().AsString<string>("2");] @ AddGenericInstanceMethodOnGenericType.cs(22,8) - IP=0x0000000D - Native IP=0x00000074
00:23.001: 3> Generation 1
STDERROR: 00:25.626: ASSERT FAILED
STDERROR: 00:25.626: Expression: g_fEEShutDown || !"Crst Reentrancy violation"
STDERROR: 00:25.626: Location: /Users/miloskotlar/dotnet/runtime-fu-enc/src/coreclr/vm/crst.cpp:638
STDERROR: 00:25.626: Function: IsSafeToTake
STDERROR: 00:25.626: Process: 13252
00:25.660: 18> Unexpected debuggee termination

Stack trace:

 DBG_DebugBreak
DebugBreak
CrstBase::IsSafeToTake (vm/crst.cpp:638)
CrstBase::Enter
CrstBase::AcquireLock
CrstBase::CrstHolder::CrstHolder
CrstBase::CrstHolder::CrstHolder
InstantiatedMethodDesc::SetupGenericMethodDefinition+0x550 (vm/genmeth.cpp:1473)
MethodTableBuilder::InitMethodDesc+0x4cc (vm/methodtablebuilder.cpp:6352)
EEClass::AddMethodDesc+0x714 (vm/class.cpp:765)
EEClass::AddMethod+0xbcc (vm/class.cpp:728) <-- holds m_AvailableTypesLock since vm/class.cpp:709
EditAndContinueModule::AddMethod+0x19c
EditAndContinueModule::ApplyEditAndContinue+0x1b8c
EEDbgInterfaceImpl::EnCApplyChanges+0x68
Debugger::ApplyChangesAndSendResult+0xb0
Debugger::HandleIPCEvent+0x20c4
HandleIPCEventWrapper+0x58
DebuggerRCThread::HandleRSEA+0x7c
DebuggerRCThread::MainLoop+0x5c8
DebuggerRCThread::ThreadProc+0x960
DebuggerRCThread::ThreadProcStatic+0xac
CorUnix::CPalThread::ThreadEntry+0x244
_pthread_start+0x88
thread_start+0x8

@kotlarmiloskotlarmilos changed the title [clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title [clr-interp] Fix EnC failure when adding a generic method to a generic typeAllow CrstAvailableParamTypes to be acquired recursivelyMay 5, 2026
CopilotAI review requested due to automatic review settings May 5, 2026 09:42
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from 3999b1e to d53448fCompareMay 5, 2026 09:42
@kotlarmiloskotlarmilos changed the title Allow CrstAvailableParamTypes to be acquired recursivelyFix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title Fix EnC failure when adding a generic method to a generic type[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

Comment threadsrc/coreclr/vm/clsload.cpp Outdated
Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod walks every existing instantiation of the type while holding the owning module's CrstAvailableParamTypes, and inside the loop calls AddMethodDesc. For a generic method that path reaches InstantiatedMethodDesc::SetupGenericMethodDefinition which itself takes CrstAvailableParamTypes (potentially on a different module's classloader if the instantiation lives there). Taking the same-level lock again trips the Crst lock-level assert in checked builds and would deadlock in release.
Split the walk into two phases. First, under the lock, iterate the type hash and collect the matching MethodTable* instantiations into a local array. Then release the lock and call AddMethodDesc on each collected instantiation. The hash iteration is still protected, and the per-instantiation method setup runs without holding the lock so SetupGenericMethodDefinition can acquire whatever CrstAvailableParamTypes instance it needs without violating lock ordering. This handles both the same-module and cross-loader-module cases.
Tests:
- CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
- CrossPlatformEnC.AddGenericStaticMethodOnGenericType
- CrossPlatformEnC.GenericMethodsWithExpressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from d53448f to 90ffacbCompareMay 5, 2026 10:31
Comment threadsrc/coreclr/vm/class.cpp
@jkotas
jkotas requested a review from noahfalkMay 6, 2026 03:49
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
CopilotAI review requested due to automatic review settings May 6, 2026 09:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 3

Comment on lines +696 to +698
// This code contains a race condition bug. Types that began loading before the metadata edit
// and are still loading at this point may not get updated. We assume that this issue does not
// have a meaningful impact on overall metadata update reliability.
Comment on lines +732 to +736
instantiations.Append(pMTMaybe);
}
}

for (COUNT_T i = 0; i < instantiations.GetCount(); i++)
Comment on lines +712 to 716
InlineSArray<MethodTable*, 8> instantiations;
{
TypeHandle th = pEntry->GetTypeHandle();
if (th.IsTypeDesc())
continue;
EETypeHashTable* paramTypes = pMod->GetAvailableParamTypes();
CrstHolder ch(pMod->GetClassLoader()->GetAvailableTypesLock());

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

The failures are not interpreter-specific and the work will be tracked in #127851

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 5, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@kotlarmilos@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

[clr-interp] Fix EnC failure when adding a generic method to a generic type - #127755

Closed
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst
Closed

[clr-interp] Fix EnC failure when adding a generic method to a generic type#127755
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 4, 2026

Copy link
Copy Markdown
Member

Description

Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod takes m_AvailableTypesLock and then calls into code that needs the same lock to publish the new method's generic parameters.

The other lock that protects type-loader tables, m_AvailableClassLock, is already configured to allow this kind of nested take. Configure m_AvailableTypesLock the same way. The other places that use this lock take and release it as a single unit, so they are unaffected.

Tests:

  • CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
  • CrossPlatformEnC.AddGenericStaticMethodOnGenericType
  • CrossPlatformEnC.GenericMethodsWithExpressions

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates CoreCLR EnC method addition for generic types so EEClass::AddMethod no longer holds m_AvailableTypesLock while creating MethodDescs for existing generic instantiations. In the VM, this avoids same-thread lock re-entry during generic-method setup on the debugger/EnC path.

Changes:

  • Snapshot matching generic instantiations from GetAvailableParamTypes() while holding AvailableTypesLock.
  • Release the lock before calling AddMethodDesc for each collected instantiation.
  • Preserve the existing per-instantiation method/async-variant creation flow after the lock is released.
Show a summary per file
FileDescription
src/coreclr/vm/class.cppChanges EEClass::AddMethod to gather matching instantiated MethodTable* entries under the available-types lock, then add new MethodDescs after releasing that lock.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmiloskotlarmilos changed the title [interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethodMay 4, 2026
@jkotas

Copy link
Copy Markdown
Member

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

instantiations.Append(pMTMaybe);
}
}

@jkotasjkotasMay 4, 2026

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.

What guarantees that there won't be more instantiations added in the meantime by other threads?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good question. The original loop held m_AvailableTypesLock across the whole iteration, which prevented other threads from publishing new instantiations of the same generic type into m_AvailableParamTypes while we were attaching the new method.

I'll update the PR to keep the lock continuously.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Actually, I updated SetupGenericMethodDefinition skip taking the lock when the current thread already owns it. The lock protects Module.m_GenericParamToDescMap, so a caller that already holds it is in the same protected region.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Found better fix - mark m_AvailableTypesLock reentrant which matches m_AvailableClassLock, instead of conditionally skipping the inner acquire.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copilot pointed out that allowing reentrancy only fixes the self-recursive case and would still trip the lock-level violation when an instantiation lives in a different loader module, so I reverted to collecting the matching instantiations under the lock and releasing it before calling AddMethodDesc.

There is no guarantee that other threads won't insert new instantiations after we release, but they don't need to be in the collected set. We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method through the normal MethodTableBuilder::Build path.

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.

We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method

There is still a race condition: Some other thread could have started building an instantiation before the metadata edit and it may finish building it only after all this is done. This instantiation would be missing the newly added method.

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.

Hmm, this is actually a pre-existing bug. This change may make it more likely to be hit (the window where the race condition can happen is larger).

Discussion at #125397 (comment) is related to this. Type loader is designed to create new entities lazily. We try to create MethodDescs eagerly here. It turns out it is hard to do that 100% reliably. The correct fix would be to switch to creating these MethodDesc lazily, but that is likely to come with a bug tail.

The proposed fix should not make it worse.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Thanks, since it isn't interpreter-specific, I created a tracking issue to decide on approach #127851

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

Sorry I put the wrong method name. The actual call is InstantiatedMethodDesc::SetupGenericMethodDefinition

Test log:

 00:22.769: 3> Current IP: Main() [new G<int>().AsString<string>("2");] @ AddGenericInstanceMethodOnGenericType.cs(22,8) - IP=0x0000000D - Native IP=0x00000074
00:23.001: 3> Generation 1
STDERROR: 00:25.626: ASSERT FAILED
STDERROR: 00:25.626: Expression: g_fEEShutDown || !"Crst Reentrancy violation"
STDERROR: 00:25.626: Location: /Users/miloskotlar/dotnet/runtime-fu-enc/src/coreclr/vm/crst.cpp:638
STDERROR: 00:25.626: Function: IsSafeToTake
STDERROR: 00:25.626: Process: 13252
00:25.660: 18> Unexpected debuggee termination

Stack trace:

 DBG_DebugBreak
DebugBreak
CrstBase::IsSafeToTake (vm/crst.cpp:638)
CrstBase::Enter
CrstBase::AcquireLock
CrstBase::CrstHolder::CrstHolder
CrstBase::CrstHolder::CrstHolder
InstantiatedMethodDesc::SetupGenericMethodDefinition+0x550 (vm/genmeth.cpp:1473)
MethodTableBuilder::InitMethodDesc+0x4cc (vm/methodtablebuilder.cpp:6352)
EEClass::AddMethodDesc+0x714 (vm/class.cpp:765)
EEClass::AddMethod+0xbcc (vm/class.cpp:728) <-- holds m_AvailableTypesLock since vm/class.cpp:709
EditAndContinueModule::AddMethod+0x19c
EditAndContinueModule::ApplyEditAndContinue+0x1b8c
EEDbgInterfaceImpl::EnCApplyChanges+0x68
Debugger::ApplyChangesAndSendResult+0xb0
Debugger::HandleIPCEvent+0x20c4
HandleIPCEventWrapper+0x58
DebuggerRCThread::HandleRSEA+0x7c
DebuggerRCThread::MainLoop+0x5c8
DebuggerRCThread::ThreadProc+0x960
DebuggerRCThread::ThreadProcStatic+0xac
CorUnix::CPalThread::ThreadEntry+0x244
_pthread_start+0x88
thread_start+0x8

@kotlarmiloskotlarmilos changed the title [clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title [clr-interp] Fix EnC failure when adding a generic method to a generic typeAllow CrstAvailableParamTypes to be acquired recursivelyMay 5, 2026
CopilotAI review requested due to automatic review settings May 5, 2026 09:42
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from 3999b1e to d53448fCompareMay 5, 2026 09:42
@kotlarmiloskotlarmilos changed the title Allow CrstAvailableParamTypes to be acquired recursivelyFix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title Fix EnC failure when adding a generic method to a generic type[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

Comment threadsrc/coreclr/vm/clsload.cpp Outdated
Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod walks every existing instantiation of the type while holding the owning module's CrstAvailableParamTypes, and inside the loop calls AddMethodDesc. For a generic method that path reaches InstantiatedMethodDesc::SetupGenericMethodDefinition which itself takes CrstAvailableParamTypes (potentially on a different module's classloader if the instantiation lives there). Taking the same-level lock again trips the Crst lock-level assert in checked builds and would deadlock in release.
Split the walk into two phases. First, under the lock, iterate the type hash and collect the matching MethodTable* instantiations into a local array. Then release the lock and call AddMethodDesc on each collected instantiation. The hash iteration is still protected, and the per-instantiation method setup runs without holding the lock so SetupGenericMethodDefinition can acquire whatever CrstAvailableParamTypes instance it needs without violating lock ordering. This handles both the same-module and cross-loader-module cases.
Tests:
- CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
- CrossPlatformEnC.AddGenericStaticMethodOnGenericType
- CrossPlatformEnC.GenericMethodsWithExpressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from d53448f to 90ffacbCompareMay 5, 2026 10:31
Comment threadsrc/coreclr/vm/class.cpp
@jkotas
jkotas requested a review from noahfalkMay 6, 2026 03:49
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
CopilotAI review requested due to automatic review settings May 6, 2026 09:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 3

Comment on lines +696 to +698
// This code contains a race condition bug. Types that began loading before the metadata edit
// and are still loading at this point may not get updated. We assume that this issue does not
// have a meaningful impact on overall metadata update reliability.
Comment on lines +732 to +736
instantiations.Append(pMTMaybe);
}
}

for (COUNT_T i = 0; i < instantiations.GetCount(); i++)
Comment on lines +712 to 716
InlineSArray<MethodTable*, 8> instantiations;
{
TypeHandle th = pEntry->GetTypeHandle();
if (th.IsTypeDesc())
continue;
EETypeHashTable* paramTypes = pMod->GetAvailableParamTypes();
CrstHolder ch(pMod->GetClassLoader()->GetAvailableTypesLock());

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

The failures are not interpreter-specific and the work will be tracked in #127851

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 5, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@kotlarmilos@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

[clr-interp] Fix EnC failure when adding a generic method to a generic type - #127755

Closed
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst
Closed

[clr-interp] Fix EnC failure when adding a generic method to a generic type#127755
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 4, 2026

Copy link
Copy Markdown
Member

Description

Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod takes m_AvailableTypesLock and then calls into code that needs the same lock to publish the new method's generic parameters.

The other lock that protects type-loader tables, m_AvailableClassLock, is already configured to allow this kind of nested take. Configure m_AvailableTypesLock the same way. The other places that use this lock take and release it as a single unit, so they are unaffected.

Tests:

  • CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
  • CrossPlatformEnC.AddGenericStaticMethodOnGenericType
  • CrossPlatformEnC.GenericMethodsWithExpressions

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates CoreCLR EnC method addition for generic types so EEClass::AddMethod no longer holds m_AvailableTypesLock while creating MethodDescs for existing generic instantiations. In the VM, this avoids same-thread lock re-entry during generic-method setup on the debugger/EnC path.

Changes:

  • Snapshot matching generic instantiations from GetAvailableParamTypes() while holding AvailableTypesLock.
  • Release the lock before calling AddMethodDesc for each collected instantiation.
  • Preserve the existing per-instantiation method/async-variant creation flow after the lock is released.
Show a summary per file
FileDescription
src/coreclr/vm/class.cppChanges EEClass::AddMethod to gather matching instantiated MethodTable* entries under the available-types lock, then add new MethodDescs after releasing that lock.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmiloskotlarmilos changed the title [interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethodMay 4, 2026
@jkotas

Copy link
Copy Markdown
Member

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

instantiations.Append(pMTMaybe);
}
}

@jkotasjkotasMay 4, 2026

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.

What guarantees that there won't be more instantiations added in the meantime by other threads?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good question. The original loop held m_AvailableTypesLock across the whole iteration, which prevented other threads from publishing new instantiations of the same generic type into m_AvailableParamTypes while we were attaching the new method.

I'll update the PR to keep the lock continuously.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Actually, I updated SetupGenericMethodDefinition skip taking the lock when the current thread already owns it. The lock protects Module.m_GenericParamToDescMap, so a caller that already holds it is in the same protected region.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Found better fix - mark m_AvailableTypesLock reentrant which matches m_AvailableClassLock, instead of conditionally skipping the inner acquire.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copilot pointed out that allowing reentrancy only fixes the self-recursive case and would still trip the lock-level violation when an instantiation lives in a different loader module, so I reverted to collecting the matching instantiations under the lock and releasing it before calling AddMethodDesc.

There is no guarantee that other threads won't insert new instantiations after we release, but they don't need to be in the collected set. We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method through the normal MethodTableBuilder::Build path.

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.

We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method

There is still a race condition: Some other thread could have started building an instantiation before the metadata edit and it may finish building it only after all this is done. This instantiation would be missing the newly added method.

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.

Hmm, this is actually a pre-existing bug. This change may make it more likely to be hit (the window where the race condition can happen is larger).

Discussion at #125397 (comment) is related to this. Type loader is designed to create new entities lazily. We try to create MethodDescs eagerly here. It turns out it is hard to do that 100% reliably. The correct fix would be to switch to creating these MethodDesc lazily, but that is likely to come with a bug tail.

The proposed fix should not make it worse.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Thanks, since it isn't interpreter-specific, I created a tracking issue to decide on approach #127851

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

Sorry I put the wrong method name. The actual call is InstantiatedMethodDesc::SetupGenericMethodDefinition

Test log:

 00:22.769: 3> Current IP: Main() [new G<int>().AsString<string>("2");] @ AddGenericInstanceMethodOnGenericType.cs(22,8) - IP=0x0000000D - Native IP=0x00000074
00:23.001: 3> Generation 1
STDERROR: 00:25.626: ASSERT FAILED
STDERROR: 00:25.626: Expression: g_fEEShutDown || !"Crst Reentrancy violation"
STDERROR: 00:25.626: Location: /Users/miloskotlar/dotnet/runtime-fu-enc/src/coreclr/vm/crst.cpp:638
STDERROR: 00:25.626: Function: IsSafeToTake
STDERROR: 00:25.626: Process: 13252
00:25.660: 18> Unexpected debuggee termination

Stack trace:

 DBG_DebugBreak
DebugBreak
CrstBase::IsSafeToTake (vm/crst.cpp:638)
CrstBase::Enter
CrstBase::AcquireLock
CrstBase::CrstHolder::CrstHolder
CrstBase::CrstHolder::CrstHolder
InstantiatedMethodDesc::SetupGenericMethodDefinition+0x550 (vm/genmeth.cpp:1473)
MethodTableBuilder::InitMethodDesc+0x4cc (vm/methodtablebuilder.cpp:6352)
EEClass::AddMethodDesc+0x714 (vm/class.cpp:765)
EEClass::AddMethod+0xbcc (vm/class.cpp:728) <-- holds m_AvailableTypesLock since vm/class.cpp:709
EditAndContinueModule::AddMethod+0x19c
EditAndContinueModule::ApplyEditAndContinue+0x1b8c
EEDbgInterfaceImpl::EnCApplyChanges+0x68
Debugger::ApplyChangesAndSendResult+0xb0
Debugger::HandleIPCEvent+0x20c4
HandleIPCEventWrapper+0x58
DebuggerRCThread::HandleRSEA+0x7c
DebuggerRCThread::MainLoop+0x5c8
DebuggerRCThread::ThreadProc+0x960
DebuggerRCThread::ThreadProcStatic+0xac
CorUnix::CPalThread::ThreadEntry+0x244
_pthread_start+0x88
thread_start+0x8

@kotlarmiloskotlarmilos changed the title [clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title [clr-interp] Fix EnC failure when adding a generic method to a generic typeAllow CrstAvailableParamTypes to be acquired recursivelyMay 5, 2026
CopilotAI review requested due to automatic review settings May 5, 2026 09:42
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from 3999b1e to d53448fCompareMay 5, 2026 09:42
@kotlarmiloskotlarmilos changed the title Allow CrstAvailableParamTypes to be acquired recursivelyFix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title Fix EnC failure when adding a generic method to a generic type[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

Comment threadsrc/coreclr/vm/clsload.cpp Outdated
Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod walks every existing instantiation of the type while holding the owning module's CrstAvailableParamTypes, and inside the loop calls AddMethodDesc. For a generic method that path reaches InstantiatedMethodDesc::SetupGenericMethodDefinition which itself takes CrstAvailableParamTypes (potentially on a different module's classloader if the instantiation lives there). Taking the same-level lock again trips the Crst lock-level assert in checked builds and would deadlock in release.
Split the walk into two phases. First, under the lock, iterate the type hash and collect the matching MethodTable* instantiations into a local array. Then release the lock and call AddMethodDesc on each collected instantiation. The hash iteration is still protected, and the per-instantiation method setup runs without holding the lock so SetupGenericMethodDefinition can acquire whatever CrstAvailableParamTypes instance it needs without violating lock ordering. This handles both the same-module and cross-loader-module cases.
Tests:
- CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
- CrossPlatformEnC.AddGenericStaticMethodOnGenericType
- CrossPlatformEnC.GenericMethodsWithExpressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from d53448f to 90ffacbCompareMay 5, 2026 10:31
Comment threadsrc/coreclr/vm/class.cpp
@jkotas
jkotas requested a review from noahfalkMay 6, 2026 03:49
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
CopilotAI review requested due to automatic review settings May 6, 2026 09:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 3

Comment on lines +696 to +698
// This code contains a race condition bug. Types that began loading before the metadata edit
// and are still loading at this point may not get updated. We assume that this issue does not
// have a meaningful impact on overall metadata update reliability.
Comment on lines +732 to +736
instantiations.Append(pMTMaybe);
}
}

for (COUNT_T i = 0; i < instantiations.GetCount(); i++)
Comment on lines +712 to 716
InlineSArray<MethodTable*, 8> instantiations;
{
TypeHandle th = pEntry->GetTypeHandle();
if (th.IsTypeDesc())
continue;
EETypeHashTable* paramTypes = pMod->GetAvailableParamTypes();
CrstHolder ch(pMod->GetClassLoader()->GetAvailableTypesLock());

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

The failures are not interpreter-specific and the work will be tracked in #127851

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 5, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@kotlarmilos@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

[clr-interp] Fix EnC failure when adding a generic method to a generic type - #127755

Closed
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst
Closed

[clr-interp] Fix EnC failure when adding a generic method to a generic type#127755
kotlarmilos wants to merge 2 commits into
dotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst

Conversation

@kotlarmilos

@kotlarmiloskotlarmilos commented May 4, 2026

Copy link
Copy Markdown
Member

Description

Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod takes m_AvailableTypesLock and then calls into code that needs the same lock to publish the new method's generic parameters.

The other lock that protects type-loader tables, m_AvailableClassLock, is already configured to allow this kind of nested take. Configure m_AvailableTypesLock the same way. The other places that use this lock take and release it as a single unit, so they are unaffected.

Tests:

  • CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
  • CrossPlatformEnC.AddGenericStaticMethodOnGenericType
  • CrossPlatformEnC.GenericMethodsWithExpressions

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR updates CoreCLR EnC method addition for generic types so EEClass::AddMethod no longer holds m_AvailableTypesLock while creating MethodDescs for existing generic instantiations. In the VM, this avoids same-thread lock re-entry during generic-method setup on the debugger/EnC path.

Changes:

  • Snapshot matching generic instantiations from GetAvailableParamTypes() while holding AvailableTypesLock.
  • Release the lock before calling AddMethodDesc for each collected instantiation.
  • Preserve the existing per-instantiation method/async-variant creation flow after the lock is released.
Show a summary per file
FileDescription
src/coreclr/vm/class.cppChanges EEClass::AddMethod to gather matching instantiated MethodTable* entries under the available-types lock, then add new MethodDescs after releasing that lock.

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 0

@kotlarmiloskotlarmilos changed the title [interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethodMay 4, 2026
@jkotas

Copy link
Copy Markdown
Member

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

instantiations.Append(pMTMaybe);
}
}

@jkotasjkotasMay 4, 2026

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.

What guarantees that there won't be more instantiations added in the meantime by other threads?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good question. The original loop held m_AvailableTypesLock across the whole iteration, which prevented other threads from publishing new instantiations of the same generic type into m_AvailableParamTypes while we were attaching the new method.

I'll update the PR to keep the lock continuously.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Actually, I updated SetupGenericMethodDefinition skip taking the lock when the current thread already owns it. The lock protects Module.m_GenericParamToDescMap, so a caller that already holds it is in the same protected region.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Found better fix - mark m_AvailableTypesLock reentrant which matches m_AvailableClassLock, instead of conditionally skipping the inner acquire.

@kotlarmiloskotlarmilosMay 5, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Copilot pointed out that allowing reentrancy only fixes the self-recursive case and would still trip the lock-level violation when an instantiation lives in a different loader module, so I reverted to collecting the matching instantiations under the lock and releasing it before calling AddMethodDesc.

There is no guarantee that other threads won't insert new instantiations after we release, but they don't need to be in the collected set. We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method through the normal MethodTableBuilder::Build path.

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.

We already added the new MethodDesc to the generic type definition itself before the loop starts, so any instantiation built after that point automatically includes the new method

There is still a race condition: Some other thread could have started building an instantiation before the metadata edit and it may finish building it only after all this is done. This instantiation would be missing the newly added method.

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.

Hmm, this is actually a pre-existing bug. This change may make it more likely to be hit (the window where the race condition can happen is larger).

Discussion at #125397 (comment) is related to this. Type loader is designed to create new entities lazily. We try to create MethodDescs eagerly here. It turns out it is hard to do that 100% reliably. The correct fix would be to switch to creating these MethodDesc lazily, but that is likely to come with a bug tail.

The proposed fix should not make it worse.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Thanks, since it isn't interpreter-specific, I created a tracking issue to decide on approach #127851

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

For a generic method that path eventually reaches InstantiatedMethodDesc::SetupGenericMethodDesc

I do not see a method with this name. What is actual name of this method?

What is the stacktrace that leads to this point?

Sorry I put the wrong method name. The actual call is InstantiatedMethodDesc::SetupGenericMethodDefinition

Test log:

 00:22.769: 3> Current IP: Main() [new G<int>().AsString<string>("2");] @ AddGenericInstanceMethodOnGenericType.cs(22,8) - IP=0x0000000D - Native IP=0x00000074
00:23.001: 3> Generation 1
STDERROR: 00:25.626: ASSERT FAILED
STDERROR: 00:25.626: Expression: g_fEEShutDown || !"Crst Reentrancy violation"
STDERROR: 00:25.626: Location: /Users/miloskotlar/dotnet/runtime-fu-enc/src/coreclr/vm/crst.cpp:638
STDERROR: 00:25.626: Function: IsSafeToTake
STDERROR: 00:25.626: Process: 13252
00:25.660: 18> Unexpected debuggee termination

Stack trace:

 DBG_DebugBreak
DebugBreak
CrstBase::IsSafeToTake (vm/crst.cpp:638)
CrstBase::Enter
CrstBase::AcquireLock
CrstBase::CrstHolder::CrstHolder
CrstBase::CrstHolder::CrstHolder
InstantiatedMethodDesc::SetupGenericMethodDefinition+0x550 (vm/genmeth.cpp:1473)
MethodTableBuilder::InitMethodDesc+0x4cc (vm/methodtablebuilder.cpp:6352)
EEClass::AddMethodDesc+0x714 (vm/class.cpp:765)
EEClass::AddMethod+0xbcc (vm/class.cpp:728) <-- holds m_AvailableTypesLock since vm/class.cpp:709
EditAndContinueModule::AddMethod+0x19c
EditAndContinueModule::ApplyEditAndContinue+0x1b8c
EEDbgInterfaceImpl::EnCApplyChanges+0x68
Debugger::ApplyChangesAndSendResult+0xb0
Debugger::HandleIPCEvent+0x20c4
HandleIPCEventWrapper+0x58
DebuggerRCThread::HandleRSEA+0x7c
DebuggerRCThread::MainLoop+0x5c8
DebuggerRCThread::ThreadProc+0x960
DebuggerRCThread::ThreadProcStatic+0xac
CorUnix::CPalThread::ThreadEntry+0x244
_pthread_start+0x88
thread_start+0x8

@kotlarmiloskotlarmilos changed the title [clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title [clr-interp] Fix EnC failure when adding a generic method to a generic typeAllow CrstAvailableParamTypes to be acquired recursivelyMay 5, 2026
CopilotAI review requested due to automatic review settings May 5, 2026 09:42
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from 3999b1e to d53448fCompareMay 5, 2026 09:42
@kotlarmiloskotlarmilos changed the title Allow CrstAvailableParamTypes to be acquired recursivelyFix EnC failure when adding a generic method to a generic typeMay 5, 2026
@kotlarmiloskotlarmilos changed the title Fix EnC failure when adding a generic method to a generic type[clr-interp] Fix EnC failure when adding a generic method to a generic typeMay 5, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 1

Comment threadsrc/coreclr/vm/clsload.cpp Outdated
Adding a generic method to an existing generic type during Edit and Continue currently fails. EEClass::AddMethod walks every existing instantiation of the type while holding the owning module's CrstAvailableParamTypes, and inside the loop calls AddMethodDesc. For a generic method that path reaches InstantiatedMethodDesc::SetupGenericMethodDefinition which itself takes CrstAvailableParamTypes (potentially on a different module's classloader if the instantiation lives there). Taking the same-level lock again trips the Crst lock-level assert in checked builds and would deadlock in release.
Split the walk into two phases. First, under the lock, iterate the type hash and collect the matching MethodTable* instantiations into a local array. Then release the lock and call AddMethodDesc on each collected instantiation. The hash iteration is still protected, and the per-instantiation method setup runs without holding the lock so SetupGenericMethodDefinition can acquire whatever CrstAvailableParamTypes instance it needs without violating lock ordering. This handles both the same-module and cross-loader-module cases.
Tests:
- CrossPlatformEnC.AddGenericInstanceMethodOnGenericType
- CrossPlatformEnC.AddGenericStaticMethodOnGenericType
- CrossPlatformEnC.GenericMethodsWithExpressions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
kotlarmilosforce-pushed the interp-enc-generic-method-crst branch from d53448f to 90ffacbCompareMay 5, 2026 10:31
Comment threadsrc/coreclr/vm/class.cpp
@jkotas
jkotas requested a review from noahfalkMay 6, 2026 03:49
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
CopilotAI review requested due to automatic review settings May 6, 2026 09:09

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot's findings

  • Files reviewed: 1/1 changed files
  • Comments generated: 3

Comment on lines +696 to +698
// This code contains a race condition bug. Types that began loading before the metadata edit
// and are still loading at this point may not get updated. We assume that this issue does not
// have a meaningful impact on overall metadata update reliability.
Comment on lines +732 to +736
instantiations.Append(pMTMaybe);
}
}

for (COUNT_T i = 0; i < instantiations.GetCount(); i++)
Comment on lines +712 to 716
InlineSArray<MethodTable*, 8> instantiations;
{
TypeHandle th = pEntry->GetTypeHandle();
if (th.IsTypeDesc())
continue;
EETypeHashTable* paramTypes = pMod->GetAvailableParamTypes();
CrstHolder ch(pMod->GetClassLoader()->GetAvailableTypesLock());

@kotlarmilos

Copy link
Copy Markdown
MemberAuthor

The failures are not interpreter-specific and the work will be tracked in #127851

@github-actionsgithub-actionsBot locked and limited conversation to collaborators Jun 5, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@kotlarmilos@jkotas