dotnet / dotnet/runtime

ILLink: Marking a type forwarder should also mark its target type

Open
#126,583 3 comments 1 reaction 2 assignees Claimed by @sbomer View on GitHub
area-Tools-ILLink
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

> [!NOTE]
> This issue was filed with AI (Copilot) assistance.

Currently, when ILLink marks an `ExportedType` (type forwarder) row — whether through rooting an assembly with `-a`, or through `copy`/`save` action — it preserves the forwarder metadata but does **not** mark the target `TypeDefinition` the forwarder resolves to. This means the forwarder can become dangling, pointing to a type or assembly that was trimmed away.

This is tested and documented as expected behavior:

```csharp
// From RootAllLibraryBehavior.cs:
// Type forwarders are kept (but not necessarily the type definition that the forwarder points to)
[KeptTypeInAssembly("library.dll", typeof(RootAllLibrary_ExportedType))]
[RemovedAssembly("exportedtype.dll")]
```

The same behavior applies for `copy` action assemblies (`RootAllLibraryCopyBehavior.cs`, `UnusedForwarderWithAssemblyCopyIsKept.cs`).

## Why the current behavior may be correct

Shim assemblies like `mscorlib.dll` and `netstandard.dll` are composed almost entirely of type forwarders pointing to dozens of implementation assemblies. If marking a forwarder also marked its target, rooting one of these assemblies would pull in a large portion of the .NET surface area — defeating the purpose of trimming.

Some forwarder targets may not even be resolvable. The mscorlib shim forwards to assemblies like `System.Drawing.Common`, `System.Data.SqlClient`, and `System.Security.Permissions` that may not be present in the app. This is an established pattern that `--skip-unresolved true` was designed to accommodate (see #103831).

## Why it might be a problem

A dangling forwarder is broken at runtime — if any code attempts to load a type through the forwarder, it will fail. Issue #99592 reported a real-world case where rooting assemblies with forwarded types caused runtime failures because the forwarder targets were trimmed. @filipnavara also noted a production scenario with `System.Windows.Forms` serialized resources depending on forwarders being functional after trimming.

## Questions for discussion

1. Is the current behavior (dangling forwarders are acceptable) the right default?
2. Should there be an opt-in mechanism to mark forwarder targets? For example, a per-assembly or per-forwarder option to say "keep this forwarder and its target."
3. Should ILLink warn when a marked forwarder's target is not marked, so users can take action?

## Related

- #99592 — ILLink rooting assemblies with forwarded types
- #103831 — ILLink should error out when failing to resolve assemblies/types/members
- #126518 / #126519 — ILLink root assembly preserves referenced forwarders in other assemblies

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.