Fix CrossGen2 TypeMap blob-only type references
Project: dotnet/runtime | Language: C# | Concepts: refactoring, error-handling, api-design | Difficulty: mid
Link: https://github.com/dotnet/runtime/pull/131610
The problem
CrossGen2 failed to resolve types that are referenced only within custom attribute arguments—specifically, types that do not have corresponding TypeRef or AssemblyRef metadata rows. This led to missing or incorrect type references in generated TypeMap blobs, breaking scenarios where types are only referenced in metadata blobs and not directly in code.
Why it's non-trivial
A naive fix might assume all referenced types will have explicit TypeRef or AssemblyRef metadata, but custom attributes can reference types in ways that bypass these tables. Ensuring correct resolution requires updating all code paths that encode type references to handle cases where only a blob reference exists, and to make sure the correct module context is always provided for resolution.
The approach
1. Update the interface to require a module context for type references
The interface for encoding type references is changed to require a ModuleDesc parameter, which provides necessary context for resolving blob-only references:
- internal Vertex EncodeReferenceToType(NativeWriter writer, TypeDesc type);
+ internal Vertex EncodeReferenceToType(NativeWriter writer, TypeDesc type, ModuleDesc module);
This ensures that all implementations must consider the module context, which is essential for blob-only type references.
2. Update all call sites to pass the new parameter (often null for unchanged cases)
All usages of EncodeReferenceToType are updated to pass the new module parameter, usually as null where the context is not required:
- Vertex valueVertex = externalReferences.EncodeReferenceToType(writer, type);
+ Vertex valueVertex = externalReferences.EncodeReferenceToType(writer, type, null);
This propagates the new contract throughout the codebase, ensuring future call sites must consciously handle the module context.
3. Update the implementation to require and use the module context
In the ImportReferenceProvider, the implementation now requires the module and ensures that definition tokens are available for the type in the context of the given module:
- internal Vertex EncodeReferenceToType(NativeWriter writer, TypeDesc type)
+ internal Vertex EncodeReferenceToType(NativeWriter writer, TypeDesc type, ModuleDesc module)
{
- Import typeImport = GetImportToType(type);
+ Debug.Assert(module is not null);
+ Import typeImport = GetImportToType(type, module);
return writer.GetTuple(writer.GetUnsignedConstant((uint)typeImport.Table.IndexFromBeginningOfArray), writer.GetUnsignedConstant((uint)typeImport.IndexFromBeginningOfArray));
}
The Debug.Assert(module is not null) enforces that the module context is always provided, preventing silent errors in Release builds.
4. Update initialization to propagate new dependencies
The initialization of ImportReferenceProvider is updated to accept and store a new ExternalReferenceTokenManager dependency, which is needed for resolving blob-only types:
- public void Initialize(ReadyToRunSymbolNodeFactory symbolNodeFactory)
+ internal void Initialize(ReadyToRunSymbolNodeFactory symbolNodeFactory, ExternalReferenceTokenManager externalReferenceTokenManager)
{
_symbolNodeFactory = symbolNodeFactory;
+ _externalReferenceTokenManager = externalReferenceTokenManager;
}
This ensures that the provider has the necessary tools to resolve type tokens even when only a blob reference exists.
5. Update construction to provide the new dependency
The code that constructs and initializes ImportReferenceProvider is updated to pass the required ExternalReferenceTokenManager:
- if (nodeFactory.ImportReferenceProvider != null)
- nodeFactory.ImportReferenceProvider.Initialize(SymbolNodeFactory);
+ if (nodeFactory.ImportReferenceProvider != null)
+ nodeFactory.ImportReferenceProvider.Initialize(SymbolNodeFactory, _tokenManager);
This ties the new dependency into the object graph, ensuring all required context is available at runtime.
From the review thread
Copilot:
EncodeReferenceToTypedeclaresModuleDesc module = nullbut immediately requires it viaDebug.Assert(module is not null). SinceDebug.Assertis skipped in Release builds, a missingmoduleargument would silently skipEnsureDefTokensAreAvailable, risking reintroducing the missing-token behavior this change is trying to address. Make the parameter required (non-optional) and drop the assert so misuse is caught at compile time / runtime.
jtschuster: Could we change the name of
moduleto something more descriptive or add a doc comment for it? It's not immediately obvious to me what it's for here and in EncodeReferenceToType.
Takeaways
- Always require explicit context for APIs that depend on external state. Making the module parameter required (not optional) in
EncodeReferenceToTypeenforces correct usage at compile time and prevents subtle runtime bugs. - Propagate interface changes through all call sites. When an interface changes, update all implementations and usages to match, even if the new parameter is not always used—this prevents accidental misuse and clarifies intent.
- Ensure dependencies are initialized and injected where needed. When new dependencies are required (like
ExternalReferenceTokenManager), update constructors and initializers to provide them, ensuring all code paths have the context they need to function correctly.