Skip to content

[ILLink] Fix crash when array types are used as TypeMap trim targets with PublishTrimmed#126380

Open
Copilot wants to merge 5 commits intomainfrom
copilot/fix-array-type-map-build-failure
Open

[ILLink] Fix crash when array types are used as TypeMap trim targets with PublishTrimmed#126380
Copilot wants to merge 5 commits intomainfrom
copilot/fix-array-type-map-build-failure

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 31, 2026

PublishTrimmed crashes with NotSupportedException: TypeDefinition cannot be resolved from 'Mono.Cecil.ArrayType' when a TypeMap attribute specifies an array type as its trim target (e.g., typeof(Foo[])). PublishAot is unaffected (handled in #123031).

Root cause: LinkContext.Resolve throws for any TypeSpecification that isn't a GenericInstanceType. Array types, pointer types, and byref types all fall into this category.

Description

  • TypeMapHandler.AddExternalTypeMapEntry: Strip non-resolvable TypeSpecification wrappers (array, pointer, byref, etc.) from the trim target before passing to RecordTypeMapEntry, using the element type instead. Loop terminates at the first TypeDefinition or GenericInstanceType (both resolvable).
  • TypeMapHandler.AddProxyTypeMapEntry: Same fix applied symmetrically to the source type parameter. Corrected the comment to accurately state that TypeMapAssociationAttribute has two constructor type arguments (source and proxy).
  • TypeMapHandler.MarkTypeMapAttribute: Same TypeSpecification-stripping fix applied before calling _context.Resolve(targetType), preventing the crash when a reachable entry's target type is an array or other parameterized type.
  • All three stripping loops use idiomatic C# property pattern matching (TypeSpecification { ElementType: var elementType }) to avoid explicit casts.
// Before fix: crashes with NotSupportedException
[assembly: TypeMap<TestTypeMap>("Key", typeof(SomeTarget), typeof(Foo[]))]

// After fix: strips Foo[] → Foo, uses Foo as the trim dependency
// The TypeMap entry is included iff Foo (the element type) is trimmer-reachable

The loop handles nested arrays (Foo[][]Foo), pointer types, and mixed cases like List<Foo>[] (strips to List<Foo>, which is a GenericInstanceType and resolvable).

Customer Impact

PublishTrimmed builds fail with an unhandled NotSupportedException / fatal ILLink error when any assembly attribute uses an array type as a TypeMap trim target. No workaround short of removing the array type.

Regression

No — this is a new feature (TypeMap) that never handled array trim targets correctly in ILLink.

Testing

Added test cases to TypeMap.cs:

  • TrimTargetIsUsedArrayType: array trim target whose element type is reachable — attribute must be preserved; verified with KeptAttributeAttribute.
  • TrimTargetIsUnusedArrayType: array trim target whose element type is unreachable — attribute and target types are removed; verified with explicit RemovedTypeInAssembly("test", ...) assertions on both ArrayTypeTrimTargetUnusedClass and ArrayTypeTrimTargetUnusedTarget.

Both existing Reflection.TypeMap and Reflection.TypeMapEntryAssembly tests pass.

Risk

Low. The change is localized to three private methods in TypeMapHandler. The stripping logic mirrors the existing TypeSpecification/GenericInstanceType guard already present in LinkContext.Resolve. No behavior change for existing valid (non-array) trim targets.

Package authoring no longer needed in .NET 9

IMPORTANT: Starting with .NET 9, you no longer need to edit a NuGet package's csproj to enable building and bump the version.
Keep in mind that we still need package authoring in .NET 8 and older versions.

Strip parameterized types (array, pointer, etc.) from TypeMap trim targets
and source types, using the element type instead. This fixes the crash
when typeof(SomeType[]) is used as a trim target in TypeMap attributes
during PublishTrimmed.

Add test cases for array type trim targets (used and unused) to TypeMap.cs.

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/82e75422-6094-47d8-be6f-7a4527457908

Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
@dotnet-policy-service dotnet-policy-service bot added the linkable-framework Issues associated with delivering a linker friendly framework label Mar 31, 2026
Copilot AI changed the title [WIP] Fix build failure for array TypeMap entries when PublishTrimmed [ILLink] Fix crash when array types are used as TypeMap trim targets with PublishTrimmed Mar 31, 2026
Copilot AI requested a review from jkoritzinsky March 31, 2026 23:01
@jkoritzinsky jkoritzinsky marked this pull request as ready for review March 31, 2026 23:34
@jkoritzinsky jkoritzinsky requested a review from sbomer as a code owner March 31, 2026 23:34
Copilot AI review requested due to automatic review settings March 31, 2026 23:34
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

Fixes an ILLink crash in PublishTrimmed when TypeMap attributes specify non-resolvable TypeSpecification types (notably arrays) as trim targets by stripping wrappers down to a resolvable element type before calling LinkContext.Resolve.

Changes:

  • Normalize TypeMap trim targets and association source types by unwrapping TypeSpecification (array/pointer/byref/…) to an element type until reaching a TypeDefinition or GenericInstanceType.
  • Add new linker test coverage for an array-type trim target scenario (kept case), and introduce a second unused-case scenario that should assert removal.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/tools/illink/src/linker/Linker/TypeMapHandler.cs Unwraps TypeSpecification wrappers before resolving trim targets / proxy source types to avoid NotSupportedException crashes.
src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMap.cs Adds test inputs for array-type trim targets and ensures at least the reachable-element case is preserved.

@jkoritzinsky
Copy link
Copy Markdown
Member

@copilot please address the feedback from @copilot and validate test changes

- Add RemovedTypeInAssembly("test", ...) for unused array trim target types
  to make the expected removal behavior explicit
- Fix misleading comment in AddProxyTypeMapEntry: TypeMapAssociationAttribute
  has two constructor type arguments (source and proxy), not one

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/8d0dbb63-91fb-4d87-8ccd-60e91da2e1e9

Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 1, 2026

@copilot please address the feedback from @copilot and validate test changes

Done in af0ac93:

  1. Added explicit RemovedTypeInAssembly("test", typeof(ArrayTypeTrimTargetUnusedClass)) and RemovedTypeInAssembly("test", typeof(ArrayTypeTrimTargetUnusedTarget)) assertions on the TypeMap class so the expected removal is explicitly verified by the test runner.
  2. Fixed the misleading comment in AddProxyTypeMapEntry — it now says "two constructor type arguments (source and proxy)" instead of "single type argument".

Both Reflection.TypeMap and Reflection.TypeMapEntryAssembly tests still pass.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 6, 2026

Note

This review was generated by Copilot.

TypeMapHandler PR Review

✅ Core fix is correct and well-targeted

The while (trimTarget is TypeSpecification and not GenericInstanceType) loop in both AddExternalTypeMapEntry and AddProxyTypeMapEntry correctly mirrors the pattern already established in:

  • MarkStep.GetOriginalType (MarkStep.cs:2880–2904) — same while + TypeSpecification + ElementType unwinding
  • LinkContext.TryResolve (LinkContext.cs:955–966) — same fallthrough to GetElementType() for non-GenericInstanceType specs

The loop terminates safely because each iteration removes one wrapper layer from a finite chain. Excluding GenericInstanceType is correct since those can be resolved by LinkContext.Resolve.

⚠️ MarkTypeMapAttribute has the same latent vulnerability (pre-existing)

At TypeMapHandler.cs:110:

if (entry.TargetType is { } targetType && _context.Resolve(targetType) is TypeDefinition targetTypeDef)

TargetType (line 330) returns the raw ConstructorArguments[1] — the target/proxy type, not the trim target/source. If a user passes typeof(Foo[]) in that position, _context.Resolve() (the throwing variant, line 928-929) will throw NotSupportedException. This path is not covered by the new stripping logic.

This is pre-existing and low-probability (target/proxy types are meant to be instantiated, so array types are semantically unusual there), but it's the exact same class of bug this PR fixes. Consider either applying the same stripping to the TargetType property or switching to TryResolve at line 110.

✅ Test coverage is correct and complete

  • Used case: ArrayTypeTrimTargetClass[] as trim target, class instantiated via new ArrayTypeTrimTargetClass() at line 214 → triggers ProcessType → entry kept. Matching KeptAttributeAttribute at line 46 ✓, [Kept] on both target class (line 565) and trim-target class (line 568) ✓.
  • Unused case: ArrayTypeTrimTargetUnusedClass[] as trim target, class never instantiated → entry removed. No KeptAttributeAttribute ✓, RemovedTypeInAssembly for both types (lines 143-144) ✓.

✅ No infinite-loop risk

The while loop terminates because each ElementType access peels exactly one TypeSpecification layer. The chain of wrappers is finite (e.g., int[][]int[]int).

💡 Minor style: could use pattern-matching cast

// Current
while (trimTarget is TypeSpecification and not GenericInstanceType)
    trimTarget = ((TypeSpecification)trimTarget).ElementType;

// Alternative (slightly more idiomatic)
while (trimTarget is TypeSpecification { ElementType: var el } and not GenericInstanceType)
    trimTarget = el;

Not blocking — the current form is perfectly readable.

✅ NativeAOT changes are sound and separate

The ExternalTypeMapNode / ProxyTypeMapNode changes add a GenericTypesTemplateMap.IsEligibleToHaveATemplate(canon) guard before emitting conditional dependencies on template type layouts. This is a correctness tightening for the NativeAOT type-loader path and is independent of the ILLink stripping fix.

Summary

The core fix is correct and surgical. The one item to consider is the ⚠️ on MarkTypeMapAttribute's Resolve call, which has the same class of vulnerability but on a different argument position. The test additions properly validate both the kept and removed cases.

Generated by Code Review for issue #126380 ·

@jkoritzinsky
Copy link
Copy Markdown
Member

@copilot please apply the same fix in MarkTypeMapAttribute as the review identified and use the pattern matching style it recommends throughout your changes.

…se property pattern matching style

- Strip TypeSpecification wrappers in MarkTypeMapAttribute before calling
  _context.Resolve(targetType), same as AddExternalTypeMapEntry/AddProxyTypeMapEntry
- Use property pattern matching style (TypeSpecification { ElementType: var elementType })
  in all three stripping loops to avoid explicit casts

Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/83c6e763-b98c-4183-b1b6-03ae760139de

Co-authored-by: jkoritzinsky <1571408+jkoritzinsky@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 6, 2026

@copilot please apply the same fix in MarkTypeMapAttribute as the review identified and use the pattern matching style it recommends throughout your changes.

Done in 7ecb95a:

  1. Applied the same TypeSpecification-stripping fix in MarkTypeMapAttribute — it now strips array/pointer/byref wrappers before calling _context.Resolve(targetType).
  2. Updated all three stripping loops to use the property pattern matching style: while (targetType is TypeSpecification { ElementType: var elementType } and not GenericInstanceType), avoiding the explicit ((TypeSpecification)...) cast throughout.

@jkoritzinsky
Copy link
Copy Markdown
Member

/azp run runtime,runtime-dev-innerloop,dotnet-linker-tests

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 3 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-TypeSystem-coreclr linkable-framework Issues associated with delivering a linker friendly framework

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[area-Tools-ILLink]: TypeMap entries for array types result in build failure when PublishTrimmed

3 participants