Background
When EnC adds a new method or field to a generic type, EEClass::AddMethod (and EEClass::AddField) eagerly walk every loaded assembly's m_AvailableParamTypes hash to find each existing instantiation of the modified type and attach the new MethodDesc/FieldDesc to it. This eager walk has two problems:
-
Pre-existing race. Even with m_AvailableTypesLock held throughout the iteration, another thread can begin building a new instantiation, read the EEClass method list before the EnC apply mutates it, and publish the resulting MethodTable after the EnC walk completes. That instantiation is silently missing the new method/field. Holding the lock longer narrows the window but does not close it.
-
Lock-level / reentrancy. EEClass::AddMethod for a generic method routes through InstantiatedMethodDesc::SetupGenericMethodDefinition, which itself takes CrstAvailableParamTypes to publish the new method's TypeVarTypeDesc into Module.m_GenericParamToDescMap. If the inner acquire targets a different loader module's lock instance, the lock-level ordering check fires in checked builds and would deadlock in release.
Proposed fix
Switch from eager attachment to lazy attachment, matching how the type loader handles other generic-instantiation lookups today.
-
Drop the per-instantiation walk in EEClass::AddMethod (vm/class.cpp lines ~692-755) and EEClass::AddField (vm/class.cpp lines ~380-422). The new MethodDesc/FieldDesc is only added to the canonical/generic EEClass.
-
Lazily create the per-instantiation MethodDesc in MethodTable::GetParallelMethodDescForEnC (vm/methodtable.cpp line ~7878). When the iterator-based search returns no match on the instantiation MT but the canonical MT has a matching EnC-added MD, create the per-instantiation MD on demand via EEClass::AddMethodDesc and append it to the chain. Same treatment for fields via the analogous lookup path.
-
Resolve the contract mismatch. GetParallelMethodDescForEnC is currently declared NOTHROW; GC_NOTRIGGER; MODE_ANY. EEClass::AddMethodDesc is THROWS; GC_NOTRIGGER; MODE_COOPERATIVE and allocates from LoaderHeap. Two options: relax the contracts on GetParallelMethodDesc and audit the ~16 call sites, or introduce a separate EnsureParallelMethodDesc API that callers invoke from safe contexts before the cheap lookup.
-
Add concurrency control to prevent two threads from racing to lazily create the same per-instantiation MD.
-
Apply symmetrically to fields so the EnC AddField path follows the same model.
Reference: #127755
Background
When EnC adds a new method or field to a generic type,
EEClass::AddMethod(andEEClass::AddField) eagerly walk every loaded assembly'sm_AvailableParamTypeshash to find each existing instantiation of the modified type and attach the newMethodDesc/FieldDescto it. This eager walk has two problems:Pre-existing race. Even with
m_AvailableTypesLockheld throughout the iteration, another thread can begin building a new instantiation, read theEEClassmethod list before the EnC apply mutates it, and publish the resultingMethodTableafter the EnC walk completes. That instantiation is silently missing the new method/field. Holding the lock longer narrows the window but does not close it.Lock-level / reentrancy.
EEClass::AddMethodfor a generic method routes throughInstantiatedMethodDesc::SetupGenericMethodDefinition, which itself takesCrstAvailableParamTypesto publish the new method'sTypeVarTypeDescintoModule.m_GenericParamToDescMap. If the inner acquire targets a different loader module's lock instance, the lock-level ordering check fires in checked builds and would deadlock in release.Proposed fix
Switch from eager attachment to lazy attachment, matching how the type loader handles other generic-instantiation lookups today.
Drop the per-instantiation walk in
EEClass::AddMethod(vm/class.cpplines ~692-755) andEEClass::AddField(vm/class.cpplines ~380-422). The newMethodDesc/FieldDescis only added to the canonical/genericEEClass.Lazily create the per-instantiation
MethodDescinMethodTable::GetParallelMethodDescForEnC(vm/methodtable.cppline ~7878). When the iterator-based search returns no match on the instantiation MT but the canonical MT has a matching EnC-added MD, create the per-instantiation MD on demand viaEEClass::AddMethodDescand append it to the chain. Same treatment for fields via the analogous lookup path.Resolve the contract mismatch.
GetParallelMethodDescForEnCis currently declaredNOTHROW; GC_NOTRIGGER; MODE_ANY.EEClass::AddMethodDescisTHROWS; GC_NOTRIGGER; MODE_COOPERATIVEand allocates fromLoaderHeap. Two options: relax the contracts onGetParallelMethodDescand audit the ~16 call sites, or introduce a separateEnsureParallelMethodDescAPI that callers invoke from safe contexts before the cheap lookup.Add concurrency control to prevent two threads from racing to lazily create the same per-instantiation MD.
Apply symmetrically to fields so the EnC
AddFieldpath follows the same model.Reference: #127755