diff --git a/src/coreclr/tools/Common/Compiler/TypeMapMetadata.cs b/src/coreclr/tools/Common/Compiler/TypeMapMetadata.cs
index c0a98386343a21..d966c1d672032d 100644
--- a/src/coreclr/tools/Common/Compiler/TypeMapMetadata.cs
+++ b/src/coreclr/tools/Common/Compiler/TypeMapMetadata.cs
@@ -107,6 +107,14 @@ public Map(TypeDesc typeMapGroup)
public TypeDesc TypeMapGroup { get; }
+ ///
+ /// Indicates whether any TypeMapAssemblyTarget attributes were processed for this group,
+ /// regardless of whether the target assembly was successfully resolved. When true and
+ /// is empty, it means all target attributes failed to resolve
+ /// and the runtime should fall back to attribute processing.
+ ///
+ public bool HasAssemblyTargetAttributes { get; set; }
+
public void AddAssociatedTypeMapEntry(TypeDesc type, TypeDesc associatedType)
{
if (!_associatedTypeMap.TryAdd(type, associatedType))
@@ -205,6 +213,7 @@ public void MergePendingMap(ModuleDesc stubModule, Map pendingMap)
}
_targetModules.AddRange(pendingMap._targetModules);
+ HasAssemblyTargetAttributes |= pendingMap.HasAssemblyTargetAttributes;
}
public void AddTargetModule(ModuleDesc targetModule)
@@ -383,6 +392,11 @@ public static TypeMapMetadata CreateFromAssembly(EcmaAssembly assembly, ModuleDe
typeMapStates[typeMapGroup] = value;
}
+ if (attrKind is TypeMapAttributeKind.TypeMapAssemblyTarget)
+ {
+ value.HasAssemblyTargetAttributes = true;
+ }
+
if (attrKind is TypeMapAttributeKind.TypeMapAssemblyTarget or TypeMapAttributeKind.TypeMap)
{
value.SetExternalTypeMapException(throwHelperEmitModule, ex);
@@ -404,6 +418,8 @@ public static TypeMapMetadata CreateFromAssembly(EcmaAssembly assembly, ModuleDe
void ProcessTypeMapAssemblyTargetAttribute(CustomAttributeValue attrValue, Map typeMapState)
{
+ typeMapState.HasAssemblyTargetAttributes = true;
+
if (attrValue.FixedArguments is not [{ Value: string assemblyName }])
{
ThrowHelper.ThrowBadImageFormatException();
diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/TypeMapAssemblyTargetsNode.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/TypeMapAssemblyTargetsNode.cs
index a3bd9dddb54c54..0806d702d7929c 100644
--- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/TypeMapAssemblyTargetsNode.cs
+++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/TypeMapAssemblyTargetsNode.cs
@@ -32,6 +32,14 @@ protected override DependencyList ComputeNonRelocationBasedDependencies(NodeFact
DependencyList dependencies = [];
foreach (var map in _assemblyTypeMaps.Maps)
{
+ // Skip groups where assembly target attributes were present but all failed to resolve
+ // (e.g. when the target assembly name doesn't exist). The runtime will fall back to
+ // attribute processing for these groups. Groups with no assembly target attributes
+ // at all should still emit an entry so the runtime knows they are precached and
+ // avoids unnecessary fallback to attribute scanning.
+ if (map.Value.TargetModules.Count == 0 && map.Value.HasAssemblyTargetAttributes)
+ continue;
+
var groupType = map.Key;
dependencies.Add(new DependencyListEntry(_importReferenceProvider.GetImportToType(groupType), "Type Map Assembly Target"));
foreach (var targetModule in map.Value.TargetModules)
@@ -59,6 +67,10 @@ public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false)
foreach (var map in _assemblyTypeMaps.Maps)
{
+ // Skip groups where assembly target attributes were present but all failed to resolve.
+ if (map.Value.TargetModules.Count == 0 && map.Value.HasAssemblyTargetAttributes)
+ continue;
+
var groupType = map.Key;
Vertex groupTypeVertex = _importReferenceProvider.EncodeReferenceToType(writer, groupType);
VertexSequence modules = new();
diff --git a/src/coreclr/vm/assemblynative.cpp b/src/coreclr/vm/assemblynative.cpp
index ce938e9549ff38..a4d71202470ed7 100644
--- a/src/coreclr/vm/assemblynative.cpp
+++ b/src/coreclr/vm/assemblynative.cpp
@@ -1773,16 +1773,19 @@ extern "C" void QCALLTYPE TypeMapLazyDictionary_ProcessAttributes(
(newProxyTypeEntry != nullptr && !hasPrecachedProxy) ||
!hasPrecachedTargets)
{
- // Only fall back to attribute parsing for the assembly targets if they were
- // not found in the pre-cached R2R section.
- if (!hasPrecachedTargets)
- {
- ProcessTypeMapAttribute(
- TypeMapAssemblyTargetAttributeName,
- assemblies,
- groupTypeMT,
- currAssembly);
- }
+ // Fall back to attribute parsing for the assembly targets if they were
+ // not found in the pre-cached R2R section, or if the external/proxy type
+ // maps were not pre-cached. When CrossGen2 fails to resolve an assembly
+ // target, it emits an assembly targets entry with count=0 but marks the
+ // external/proxy maps as invalid (state=0). Re-processing the assembly
+ // target attributes in that case ensures the runtime correctly loads (and
+ // throws for) unresolvable assemblies. The AssemblyTargetProcessor already
+ // deduplicates, so re-processing is safe.
+ ProcessTypeMapAttribute(
+ TypeMapAssemblyTargetAttributeName,
+ assemblies,
+ groupTypeMT,
+ currAssembly);
// We will only process the specific type maps if we have a callback to process
// the entry and the precached map was not calculated for this module.
diff --git a/src/tests/Interop/TypeMap/TypeMapApp.cs b/src/tests/Interop/TypeMap/TypeMapApp.cs
index 02259e4738eb54..9c07412d5e3282 100644
--- a/src/tests/Interop/TypeMap/TypeMapApp.cs
+++ b/src/tests/Interop/TypeMap/TypeMapApp.cs
@@ -265,6 +265,54 @@ public static void Validate_MissingAssemblyTarget()
Assert.Throws(() => TypeMapping.GetOrCreateProxyTypeMapping());
}
+ [Fact]
+ public static void Validate_MissingAssemblyTarget_DoesNotAffectGroupsWithoutTargets()
+ {
+ // Validates that groups without TypeMapAssemblyTarget attributes (like TypicalUseCase)
+ // still work correctly alongside groups with failing targets (like UnknownAssemblyReference).
+ // In R2R, groups without assembly targets must have their precached entry emitted so the
+ // runtime doesn't unnecessarily fall back to attribute scanning.
+ Console.WriteLine(nameof(Validate_MissingAssemblyTarget_DoesNotAffectGroupsWithoutTargets));
+
+ Assert.Throws(() => TypeMapping.GetOrCreateExternalTypeMapping());
+
+ IReadOnlyDictionary externalMap = TypeMapping.GetOrCreateExternalTypeMapping();
+ Assert.Equal(typeof(C1), externalMap["1"]);
+ Assert.Equal(typeof(S1), externalMap["2"]);
+
+ IReadOnlyDictionary proxyMap = TypeMapping.GetOrCreateProxyTypeMapping();
+ Assert.Equal(typeof(C1), proxyMap[new C1().GetType()]);
+ Assert.Equal(typeof(S1), proxyMap[((object)default(S1)).GetType()]);
+
+ // When running in R2R mode, verify the R2R image contains TypeMap sections.
+ // This confirms CrossGen2 correctly emitted entries for groups without
+ // TypeMapAssemblyTarget attributes alongside groups with failed targets.
+ string assemblyLocation = typeof(TypeMap).Assembly.Location;
+ if (!string.IsNullOrEmpty(assemblyLocation))
+ {
+ string r2rDumpFile = assemblyLocation + ".r2rdump";
+ if (File.Exists(r2rDumpFile))
+ {
+ string[] lines = File.ReadAllLines(r2rDumpFile);
+ bool hasExternalTypeMaps = false;
+ bool hasProxyTypeMaps = false;
+ bool hasTypeMapAssemblyTargets = false;
+ foreach (string line in lines)
+ {
+ if (line.Contains("ExternalTypeMaps", StringComparison.Ordinal))
+ hasExternalTypeMaps = true;
+ if (line.Contains("ProxyTypeMaps", StringComparison.Ordinal))
+ hasProxyTypeMaps = true;
+ if (line.Contains("TypeMapAssemblyTargets", StringComparison.Ordinal))
+ hasTypeMapAssemblyTargets = true;
+ }
+ Assert.True(hasExternalTypeMaps, "R2R image should contain ExternalTypeMaps section");
+ Assert.True(hasProxyTypeMaps, "R2R image should contain ProxyTypeMaps section");
+ Assert.True(hasTypeMapAssemblyTargets, "R2R image should contain TypeMapAssemblyTargets section");
+ }
+ }
+ }
+
[Fact]
public static void Validate_EmptyOrInvalidMappings()
{