Skip to content

[dotnet-linker] Work around a crossgen2 crash compiling the trimmable-static type maps - #26345

Merged
rolfbjarne merged 5 commits into
mainfrom
dev/rolf/crossgen2-typemap-workaround
Jul 30, 2026
Merged

[dotnet-linker] Work around a crossgen2 crash compiling the trimmable-static type maps#26345
rolfbjarne merged 5 commits into
mainfrom
dev/rolf/crossgen2-typemap-workaround

Conversation

@rolfbjarne

@rolfbjarne rolfbjarne commented Jul 29, 2026

Copy link
Copy Markdown
Member

The types named by our type-map entries are only mentioned in the custom attribute blobs (as assembly-qualified names), so the generated type map assembly ends up without a TypeRef row for them.

crossgen2 assumes such a TypeRef row exists: it can't resolve the type back to a module token and crashes with a NotImplementedException in ModuleTokenResolver.GetModuleTokenForType when it ReadyToRun-compiles the type map assembly.

Unhandled exception. System.NotImplementedException: The method or operation is not implemented.
   at ILCompiler.DependencyAnalysis.ReadyToRun.ModuleTokenResolver.GetModuleTokenForType(EcmaType type, Boolean allowDynamicallyCreatedReference, Boolean throwIfNotFound)

Upstream issue: dotnet/runtime#131527
Tracking issue to remove this workaround: #26343

Is our metadata valid?

Yes — this is a crossgen2 bug, not a Cecil bug:

Rule Says
ECMA-335 §II.23.3 (custom attribute blob) A System.Type argument is stored as a SerString holding the canonical name. Nothing more.
ECMA-335 §II.22.10 (CustomAttribute validity) No requirement that a referenced type has a TypeRef row.
ECMA-335 §II.22.38 (TypeRef validity) Constrains rows that exist; doesn't require rows to exist.
Ecma-335-Augments.md Silent on this.
typemap.md (runtime design doc) Explicitly states the assembly name "does not need to map to an AssemblyRef row".

The runtime itself resolves these types by parsing the string, not via the TypeRef table.

The workaround

Emit an unused <TypeReferences>.KeepTypeReferences method into the type map assembly that ldtokens every externally defined type we name. That makes Cecil emit the corresponding TypeRef rows. The method is never called, so ILLink trims it away again.

This is scoped to CoreCLR — NativeAOT and MonoVM don't need it, and the extra metadata isn't free. It is deliberately not narrowed further to ReadyToRun-only builds, because ReadyToRun for CoreCLR is a .NET 11+ feature and that would mean the code behaves differently between branches; the cost for the interpreted CoreCLR configurations is small (~64 KB per runtime identifier in the platform type map assembly).

App size impact

Only the CoreCLR + trimmable-static configuration changes: MacOSX-CoreCLR-Interpreter-TrimmableStatic grows by 128,000 bytes (2 RIDs × 64,000 bytes). All NativeAOT and MonoVM sizes are unchanged (verified: tests-dotnet NativeAOT reports ≤5 bytes of drift across all 8 NativeAOT configs).

🤖 Pull request created by Copilot

rolfbjarne and others added 3 commits July 29, 2026 15:39
…-static type maps

The types named by our type-map entries only show up in the custom attribute
blobs, as assembly-qualified names. Nothing else in the generated type map
assembly refers to them from IL, so Cecil doesn't emit a TypeRef row for them.

That's valid metadata: ECMA-335 II.23.3 only requires a System.Type custom
attribute argument to be stored as a SerString holding the type's canonical name,
and neither the CustomAttribute (II.22.10) nor the TypeRef (II.22.38) validity
rules require a matching TypeRef row. The runtime agrees - it resolves these
types by parsing the string, not by looking at the TypeRef table - and the type
map design explicitly says the assembly name in TypeMapAssemblyTargetAttribute
doesn't need a matching AssemblyRef row either.

crossgen2 nonetheless assumes the TypeRef row is there. It fails all four lookups
in ModuleTokenResolver.GetModuleTokenForType (the type doesn't version with the
compilation, there's no TypeRef in the input, and nothing has added it to the
manifest module), and falls through to a bare
`throw new NotImplementedException (type.ToString ())`, so ReadyToRun-compiling
an app with the trimmable-static registrar fails:

    System.NotImplementedException: [Microsoft.macOS]UserNotificationsUI.IUNNotificationContentExtension
       at ILCompiler.DependencyAnalysis.ReadyToRun.ModuleTokenResolver.GetModuleTokenForType(TypeDesc, Boolean, Boolean)
       at ILCompiler.DependencyAnalysis.ReadyToRun.SignatureContext.GetTargetModule(TypeDesc)
       at ILCompiler.DependencyAnalysis.ReadyToRun.TypeFixupSignature.GetData(NodeFactory, Boolean)
       at ILCompiler.DependencyAnalysis.ReadyToRun.ImportSectionNode.MaterializeSignature(NodeFactory)
       at ILCompiler.DependencyAnalysis.ReadyToRun.ManifestMetadataTableNode.ComputeLastSetOfModuleIndices()
       at ILCompiler.DependencyAnalysis.ReadyToRun.ManifestAssemblyMvidHeaderNode.GetData(NodeFactory, Boolean)

Work around it by adding an unused method to each generated type map assembly
that does an `ldtoken` for every externally defined type the maps name. That's
enough for Cecil to emit the TypeRef rows, and crossgen2 then resolves the types
through `TryGetModuleTokenForExternalType`. `ldtoken` is used rather than a field
or parameter because it also works for open generic types. The method is never
called, so it's trimmed away again by ILLink.

Only CoreCLR needs this, so the other runtimes don't pay for it. In particular
NativeAOT app size is unaffected (bit-for-bit identical). For CoreCLR it adds
~64 KB per runtime identifier to the platform type map assembly.

Reported upstream as dotnet/runtime#131527, and tracked
for removal in #26343.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6aeeddfd-3bd8-4129-ad90-7292860c4b66
The workaround emits TypeRef rows for every type map entry into the generated
type map assemblies when using CoreCLR, which makes those assemblies (and thus
the app) slightly bigger.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6aeeddfd-3bd8-4129-ad90-7292860c4b66
The previous commit was generated on a local machine whose build produces a
slightly different Info.plist / Microsoft.macOS.dll / executable than CI does
(a pre-existing ~5.9 KB difference that also shows up in the unmodified
CoreCLR_Interpreter test). Keep only the delta actually caused by this change:
+64,000 bytes per RID in the generated type map assembly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6aeeddfd-3bd8-4129-ad90-7292860c4b66
Two different assemblies can contain types with the same full name, in which case
deduplicating on the full name alone would skip a TypeRef we need.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 6aeeddfd-3bd8-4129-ad90-7292860c4b66
@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@rolfbjarne
rolfbjarne marked this pull request as ready for review July 29, 2026 14:47
Copilot AI review requested due to automatic review settings July 29, 2026 14:47
@vs-mobiletools-engineering-service2

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a targeted workaround in the trimmable-static type map generation step to prevent a crossgen2 ReadyToRun crash when compiling the generated type map assemblies. The workaround forces Mono.Cecil to emit TypeRef metadata rows for types that otherwise only appear inside custom attribute blobs.

Changes:

  • Emit an unused method that ldtokens all externally-scoped types referenced by type map custom attributes (CoreCLR only), ensuring TypeRef rows exist for crossgen2.
  • Invoke the new emission step during per-assembly type map assembly generation.
  • Update the macOS CoreCLR interpreter trimmable-static app size baseline to account for the increased type map assembly size.

Reviewed changes

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

File Description
tools/dotnet-linker/Steps/TrimmableRegistrarStep.cs Adds CoreCLR-only emission of ldtoken references to force TypeRef rows and avoids crossgen2 crash when compiling type map assemblies.
tests/dotnet/UnitTests/expected/MacOSX-CoreCLR-Interpreter-TrimmableStatic-size.txt Updates expected bundle/type map assembly sizes after the additional metadata.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@vs-mobiletools-engineering-service2

This comment has been minimized.

@rolfbjarne rolfbjarne added the ready-to-review This PR is ready to review/merge. label Jul 30, 2026
@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

✅ API diff for current PR / commit

NET (empty diffs)

✅ API diff vs stable

NET (empty diffs)

ℹ️ Generator diff

Generator Diff: vsdrops (html) vsdrops (raw diff) gist (raw diff) - Please review changes)

Pipeline on Agent
Hash: 0566dd884790e4c7d895a3db2c59c7446ff03d73 [PR build]

@vs-mobiletools-engineering-service2

Copy link
Copy Markdown
Collaborator

🚀 [CI Build #0566dd8] Test results 🚀

Test results

✅ All tests passed on VSTS: test results.

🎉 All 203 tests passed 🎉

Tests counts

✅ assembly-processing: All 1 tests passed. Html Report (VSDrops) Download
✅ cecil: All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (iOS): All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (MacCatalyst): All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (macOS): All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (Multiple platforms): All 1 tests passed. Html Report (VSDrops) Download
✅ dotnettests (tvOS): All 1 tests passed. Html Report (VSDrops) Download
✅ framework: All 2 tests passed. Html Report (VSDrops) Download
✅ fsharp: All 4 tests passed. Html Report (VSDrops) Download
✅ generator: All 5 tests passed. Html Report (VSDrops) Download
✅ interdependent-binding-projects: All 4 tests passed. Html Report (VSDrops) Download
✅ introspection: All 4 tests passed. Html Report (VSDrops) Download
✅ linker (iOS): All 15 tests passed. Html Report (VSDrops) Download
✅ linker (MacCatalyst): All 15 tests passed. Html Report (VSDrops) Download
✅ linker (macOS): All 21 tests passed. Html Report (VSDrops) Download
✅ linker (tvOS): All 15 tests passed. Html Report (VSDrops) Download
✅ monotouch (iOS): All 19 tests passed. Html Report (VSDrops) Download
✅ monotouch (MacCatalyst): All 18 tests passed. Html Report (VSDrops) Download
✅ monotouch (macOS): All 19 tests passed. Html Report (VSDrops) Download
✅ monotouch (tvOS): All 19 tests passed. Html Report (VSDrops) Download
✅ msbuild: All 2 tests passed. Html Report (VSDrops) Download
✅ sharpie: All 1 tests passed. Html Report (VSDrops) Download
✅ windows: All 3 tests passed. Html Report (VSDrops) Download
✅ xcframework: All 4 tests passed. Html Report (VSDrops) Download
✅ xtro: All 1 tests passed. Html Report (VSDrops) Download

macOS tests

✅ Tests on macOS Monterey (12): All 5 tests passed. Html Report (VSDrops) Download
✅ Tests on macOS Ventura (13): All 5 tests passed. Html Report (VSDrops) Download
✅ Tests on macOS Sonoma (14): All 5 tests passed. Html Report (VSDrops) Download
✅ Tests on macOS Sequoia (15): All 5 tests passed. Html Report (VSDrops) Download
✅ Tests on macOS Tahoe (26): All 5 tests passed. Html Report (VSDrops) Download

Linux Build Verification

Linux build succeeded

Pipeline on Agent
Hash: 0566dd884790e4c7d895a3db2c59c7446ff03d73 [PR build]

@rolfbjarne
rolfbjarne merged commit 280eff2 into main Jul 30, 2026
57 checks passed
@rolfbjarne
rolfbjarne deleted the dev/rolf/crossgen2-typemap-workaround branch July 30, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copilot ready-to-review This PR is ready to review/merge.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants