Avoid unnecessary type loads during JIT-time access checks#127617
Avoid unnecessary type loads during JIT-time access checks#127617davidwrighton wants to merge 2 commits intodotnet:mainfrom
Conversation
- Respect the rule of 3 for TargetTypeForAccessCheck - Handle non-generic instantiations correctly
|
Tagging subscribers to this area: @agocke |
|
NOTE: I'm not convinced this amount of complexity is warranted here. A simpler approach of simply disabling access checks of this form when compiling System.Private.CoreLib would probably produce most of the benefits without all the shenanigans that were needed here. Also, this might be slower than the existing approach. I have not attempted to measure this. Overall though, this reduces type loads in a test scenario which is a small application from about 2725 to 2416 when the application is loaded without R2R enabled. Without R2R enabled, we don't do anywhere near as much jitting and the number of types which don't need to be loaded is about 30. I view this change as being more useful in our interpreter scenarios where we are intentionally limiting R2R for size reasons. |
There was a problem hiding this comment.
Pull request overview
This PR updates CoreCLR’s JIT-triggered accessibility checking to avoid eagerly loading fully-instantiated generic types (especially when instantiations involve VAR/MVAR type variables). It introduces “target for access check” wrapper types that can represent targets via signature data and only materialize full TypeHandle/MethodDesc when needed (primarily for throw paths), reducing unnecessary type loads during JIT access checks.
Changes:
- Added
TargetTypeForAccessCheck/TargetMethodForAccessCheck/TargetInstantiationForAccessCheckand a newSigPointer::AccessCheckTyperoutine to perform access checks by walking signatures. - Refactored
ClassLoaderaccess-check APIs (CanAccessClass,CanAccess, etc.) to accept the new wrapper types and to support signature-based instantiation checks. - Updated JIT-EE (
jitinterface.cpp) and other call sites to construct these wrappers from TypeSpec/MethodSpec signatures instead of forcing full instantiation.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/coreclr/vm/siginfo.hpp | Adds new wrapper types and declares SigPointer::AccessCheckType for signature-walking access checks. |
| src/coreclr/vm/siginfo.cpp | Implements wrapper behavior, signature-walking access checking, and method/type instantiation access helpers. |
| src/coreclr/vm/clsload.hpp | Refactors access-check API signatures to use wrapper types; removes unused access-check enum values. |
| src/coreclr/vm/clsload.inl | Updates AccessCheckOptions constructors/initializers to store wrapper pointers. |
| src/coreclr/vm/clsload.cpp | Implements signature-based instantiation access checks and updates throw/error plumbing for wrapper targets. |
| src/coreclr/vm/jitinterface.cpp | Uses signature-backed wrapper targets for access checks in getFieldInfo, canAccessClass, and getCallInfo. |
| src/coreclr/vm/methodtable.cpp | Wraps target types when calling CanAccessClass in DoAccessibilityCheck. |
| src/coreclr/vm/methodtable.h | Marks InterfaceMapIterator::HasSameTypeDefAs as const. |
| src/coreclr/vm/runtimehandles.cpp | Switches custom-attribute visibility check to pass wrapper type and no optional target method. |
| // Execute access checks for Method Instantiation siganatures. Used to avoid materializing methods we don't need to materialize. | ||
| bool AccessCheckType( | ||
| Module* pModule, | ||
| AccessCheckContext* pContext, | ||
| const AccessCheckOptions & accessCheckOptions | ||
| ) const; |
There was a problem hiding this comment.
Typo in comment: "siganatures" -> "signatures" (also comment mentions "Method Instantiation" but this method is named AccessCheckType and walks type signatures).
| @@ -3767,34 +3767,20 @@ BOOL AccessCheckOptions::DemandMemberAccess(AccessCheckContext *pContext, Method | |||
|
|
|||
| BOOL canAccessTarget = FALSE; | |||
|
|
|||
| // In CoreCLR kRestrictedMemberAccess means that one can access private/internal | |||
| // In CoreCLR kNormalAccessNoTransparency and kRestrictedMemberAccessNoTransparency means that one can access private/internal | |||
| // classes/members in app code. | |||
| if (m_accessCheckType != kMemberAccess && pTargetMT) | |||
| { | |||
| // We allow all transparency checks to succeed in LCG methods and reflection invocation. | |||
| if (m_accessCheckType == kNormalAccessNoTransparency || m_accessCheckType == kRestrictedMemberAccessNoTransparency) | |||
| return TRUE; | |||
| } | |||
|
|
|||
| // No Access | |||
| if (m_fThrowIfTargetIsInaccessible) | |||
| { | |||
| ThrowAccessException(pContext, pTargetMT, NULL); | |||
| } | |||
|
|
|||
|
|
|||
| return canAccessTarget; | |||
| return TRUE; | |||
There was a problem hiding this comment.
AccessCheckOptions::DemandMemberAccess now unconditionally returns TRUE, leaving canAccessTarget unused. This will likely fail the build under /WX or equivalent; remove the variable (and any now-dead parameters like visibilityCheck if they’re no longer needed).
| BOOL ClassLoader::CanAccessClass( // True if access is legal, false otherwise. | ||
| AccessCheckContext* pContext, // The caller context | ||
| MethodTable* pTargetClass, // The desired target class. | ||
| const TargetTypeForAccessCheck& pTargetClass, // The desired target class. | ||
| Assembly* pTargetAssembly, // Assembly containing the target class. | ||
| const AccessCheckOptions & accessCheckOptions)// = TRUE | ||
| { |
There was a problem hiding this comment.
CanAccessClass now takes const TargetTypeForAccessCheck&, but the function body’s contract block still contains a CheckPointer(pTargetClass) precondition (left over from the old MethodTable*). That’s either a compile error or a no-op; update it to a meaningful check for this wrapper (or remove it).
| ClassLoader::NotFoundAction notFoundAction; | ||
| CorInternalStates tdTypes; | ||
|
|
There was a problem hiding this comment.
SigPointer::AccessCheckType declares notFoundAction and tdTypes but never uses them. This will likely trigger unused-variable warnings under /WX; remove them (or use them if they’re intended for future logic).
| ClassLoader::NotFoundAction notFoundAction; | |
| CorInternalStates tdTypes; |
| TargetTypeForAccessCheck typeParam = calleeTypeForSecurity.GetTypeParam(); | ||
| while (typeParam.HasTypeParam()) | ||
| { | ||
| typeParam = typeParam.GetTypeParam(); | ||
| } | ||
|
|
||
| _ASSERTE(pCallerForSecurity != NULL && callerTypeForSecurity != NULL); | ||
| AccessCheckContext accessContext(pCallerForSecurity, callerTypeForSecurity.GetMethodTable()); | ||
|
|
||
| MethodTable* pTypeParamMT = typeParam.GetMethodTable(); | ||
|
|
||
| // No access check is need for Var, MVar, or FnPtr. | ||
| if (pTypeParamMT != NULL) | ||
| if (!typeParam.IsNull()) | ||
| { | ||
| _ASSERTE(pCallerForSecurity != NULL && callerTypeForSecurity != NULL); | ||
| AccessCheckContext accessContext(pCallerForSecurity, callerTypeForSecurity.GetMethodTable()); | ||
|
|
||
| canAccessMethod = ClassLoader::CanAccessClass(&accessContext, | ||
| pTypeParamMT, | ||
| typeParam.GetAssembly(), | ||
| accessCheckOptions); | ||
| typeParam, | ||
| typeParam.GetAssembly(), | ||
| accessCheckOptions); | ||
| } |
There was a problem hiding this comment.
The updated type-parameter access check uses if (!typeParam.IsNull()) to decide whether to call CanAccessClass. For generic variables / fnptr, the type handle is non-null but GetMethodTableOfRootTypeParam() is null, and CanAccessClass will dereference a null MethodTable via TargetTypeForAccessCheck::IsNested/GetProtection. This should preserve the previous behavior (if (pTypeParamMT != NULL)): skip the access check unless the (root) MethodTable exists.
| SigPointer sp = InstantiationForAccessCheck->GetSig(); | ||
|
|
||
| BYTE etype; | ||
| IfFailThrow(sp.GetByte(&etype)); |
There was a problem hiding this comment.
TargetMethodForAccessCheck::GetMethodDescForThrow reads the method-spec element byte into etype but never uses it. This is likely to trigger an unused-local warning under /WX; either remove etype or validate it (e.g., assert/THROW_BAD_FORMAT for IMAGE_CEE_CS_CALLCONV_GENERICINST).
| IfFailThrow(sp.GetByte(&etype)); | |
| IfFailThrow(sp.GetByte(&etype)); | |
| if (etype != IMAGE_CEE_CS_CALLCONV_GENERICINST) | |
| { | |
| COMPlusThrowHR(COR_E_BADIMAGEFORMAT); | |
| } |
| { | ||
| SigPointer sig = InstantiationForAccessCheck->GetSig(); | ||
|
|
||
| BYTE etype; | ||
| IfFailThrow(sig.GetByte(&etype)); | ||
| return ClassLoader::CanAccessInstantiationBySignature(pContext, sig, InstantiationForAccessCheck->GetModule(), accessCheckOptions); |
There was a problem hiding this comment.
TargetMethodForAccessCheck::CanAccessMethodInstantiation reads the method-spec element byte into etype but never uses it. This is likely to trigger an unused-local warning under /WX; either remove etype or validate it before proceeding.
Note
This PR description was generated with AI assistance.
Summary
Fixes #120554
When the JIT triggers access checks (for fields, methods, or types), the runtime previously loaded fully instantiated generic types even when the type arguments were type variables (
!0,!!0, etc.). This is unnecessary because access checks only need to verify visibility of the named types (typedefs) involved, not the fully constructed generic instantiation.This PR changes the access check infrastructure to perform access checks against the signature rather than materializing the full
TypeHandle, avoiding loading generic types instantiated over type variables of the enclosing generic method/type.Approach
The core idea is to introduce abstraction types (
TargetTypeForAccessCheck,TargetMethodForAccessCheck,TargetInstantiationForAccessCheck) that can either wrap a fully-loadedTypeHandle/MethodDescor lazily represent a type via its signature (SigPointer). Access checks operate on these abstractions, only loading types when truly needed (e.g., for error messages).Components Modified
src/coreclr/vm/siginfo.hpp/src/coreclr/vm/siginfo.cppTargetInstantiationForAccessCheck,TargetTypeForAccessCheck,TargetMethodForAccessCheckTargetInstantiationForAccessCheck: Holds aSigPointer,SigTypeContext, andModule*representing a deferred instantiation.TargetTypeForAccessCheck: Wraps aTypeHandlewith an optionalTargetInstantiationForAccessCheck*. Provides the same query surface asMethodTable*for access check purposes (e.g.,IsNested,GetProtection,HasSameTypeDefAs,GetModule,GetAssembly,HasTypeParam). Only callsGetTypeHandleThrowingwhen actually needed (error paths).TargetMethodForAccessCheck: Wraps aMethodDesc*with optional instantiation info; defersFindOrCreateAssociatedMethodDesctoGetMethodDescForThrow().SigPointer::AccessCheckType— walks a signature recursively performing access checks on all named types without loading generic instantiations.src/coreclr/vm/clsload.hpp/src/coreclr/vm/clsload.cpp/src/coreclr/vm/clsload.inlCanAccessClass,CanAccess,CheckAccessMember,CanAccessFamily,CanAccessMethodInstantiationto acceptconst TargetTypeForAccessCheck&/const TargetMethodForAccessCheck*instead of rawMethodTable*/MethodDesc*.ClassLoader::CanAccessInstantiation(operates on loadedInstantiation) andClassLoader::CanAccessInstantiationBySignature(operates onSigPointer).AccessCheckOptionsupdated: storesTargetTypeForAccessCheck*andTargetMethodForAccessCheck*; removed unusedkMemberAccessandkRestrictedMemberAccessenum values.DemandMemberAccesssimplified — always returns TRUE (the remaining access check modes all permit access at this point).src/coreclr/vm/jitinterface.cppgetFieldInfo,canAccessClass, andgetCallInfoupdated to constructTargetTypeForAccessCheck/TargetMethodForAccessCheckfrom the resolved token signatures instead of eagerly callingGetTypeHandleThrowing/FindOrCreateAssociatedMethodDesc.src/coreclr/vm/methodtable.cpp/src/coreclr/vm/methodtable.hDoAccessibilityCheckwraps the target inTargetTypeForAccessCheckbefore callingCanAccessClass.InterfaceMapIterator::HasSameTypeDefAsmarkedconst.src/coreclr/vm/runtimehandles.cppCheckCAVisibilityFromDecoratedTypeupdated to useTargetTypeForAccessCheck; passesNULLfor the method instantiation check since custom attribute constructors don't need method-instantiation access validation.