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 intodotnet: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 intodotnet:mainfrom
kotlarmilos:interp-enc-generic-method-crst

Conversation

@kotlarmilos
Copy link
Copy Markdown
Member

@kotlarmilos kotlarmilos commented May 4, 2026

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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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
File Description
src/coreclr/vm/class.cpp Changes 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

@kotlarmilos kotlarmilos changed the title [interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod [clr-interp] Release AvailableTypesLock before adding new MethodDescs in EEClass::AddMethod May 4, 2026
@jkotas
Copy link
Copy Markdown
Member

jkotas commented May 4, 2026

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?

Comment thread src/coreclr/vm/class.cpp
instantiations.Append(pMTMaybe);
}
}

Copy link
Copy Markdown
Member

@jkotas jkotas May 4, 2026

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
Member Author

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.

Copy link
Copy Markdown
Member Author

@kotlarmilos kotlarmilos May 5, 2026

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.

Copy link
Copy Markdown
Member Author

@kotlarmilos kotlarmilos May 5, 2026

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.

Copy link
Copy Markdown
Member Author

@kotlarmilos kotlarmilos May 5, 2026

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
Member Author

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
Member Author

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

@kotlarmilos kotlarmilos 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 type May 5, 2026
@kotlarmilos kotlarmilos changed the title [clr-interp] Fix EnC failure when adding a generic method to a generic type Allow CrstAvailableParamTypes to be acquired recursively May 5, 2026
Copilot AI review requested due to automatic review settings May 5, 2026 09:42
@kotlarmilos kotlarmilos force-pushed the interp-enc-generic-method-crst branch from 3999b1e to d53448f Compare May 5, 2026 09:42
@kotlarmilos kotlarmilos changed the title Allow CrstAvailableParamTypes to be acquired recursively Fix EnC failure when adding a generic method to a generic type May 5, 2026
@kotlarmilos kotlarmilos 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 type May 5, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 thread src/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>
Comment thread src/coreclr/vm/class.cpp
@jkotas jkotas requested a review from noahfalk May 6, 2026 03:49
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Copilot AI review requested due to automatic review settings May 6, 2026 09:09
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 thread src/coreclr/vm/class.cpp
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 thread src/coreclr/vm/class.cpp
Comment on lines +732 to +736
instantiations.Append(pMTMaybe);
}
}

for (COUNT_T i = 0; i < instantiations.GetCount(); i++)
Comment thread src/coreclr/vm/class.cpp
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
Member Author

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

@kotlarmilos kotlarmilos closed this May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants