dotnet / dotnet/runtime

Collectible AssemblyLoadContext fails to unload if a Type object is inspected during debugging

Open
#124,876 10 comments 4 reactions 0 assignees View on GitHub
area-Diagnostics-coreclr tracking-external-issue
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### Description

When debugging an application that uses collectible AssemblyLoadContext, inspecting a Type object from the loaded assembly causes the ALC to remain in memory indefinitely. Even after calling Unload() and triggering a full garbage collection cycle, the WeakReference to the ALC remains alive.

From my investigations with WinDBG:
```
HandleTable:
000001aff35a13e8 (strong handle)
-> 01aff3800028 System.Object[]
-> 01aff58725a8 System.Collections.Generic.Dictionary (static variable: System.Reflection.MethodInfo.Type__GetType3_MethodInfo)
-> 01aff5880cb0 System.Collections.Generic.Dictionary+Entry[]
-> 01aff5857628 System.Reflection.RuntimeAssembly
-> 01aff58575e0 System.Reflection.LoaderAllocator
```

### Reproduction Steps

1. Create a library project (CollectibleLib) with a simple class

```c#
using System;

namespace CollectibleLib;

public class TestTarget;

public static class Program
{
public static void Main()
{
// Inspect this type with the debugger to cause a leak that will prevent the load context from being unloaded
Type testType = typeof(TestTarget);
}
}
```

2. Create a host application that loads this library into a collectible AssemblyLoadContext.

```c#
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;

namespace AssemblyContextLeakTest;

class Program
{
static void Main()
{
WeakReference alcWeakRef = LoadAndUnloadAssembly();

while (alcWeakRef.IsAlive)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}

Console.WriteLine("AssemblyLoadContext has been collected.");
}

[MethodImpl(MethodImplOptions.NoInlining)]
static WeakReference LoadAndUnloadAssembly()
{
AssemblyLoadContext alc = new AssemblyLoadContext(null, true);
Assembly pluginAssembly = alc.LoadFromAssemblyPath("PathToTheCollectibleAssembly");

Type? moduleTestType = pluginAssembly.GetType("CollectibleLib.Program");
MethodInfo? testMethod = moduleTestType.GetMethod("Main");
testMethod.Invoke(null, null);

alc.Unload();
return new WeakReference(alc);
}
}
```

3. Set a breakpoint in the Program.Main and which we declared in Step 1, and hover the `typeof(TestTarget)`
4. Resume execution and now the ALC won't unload and a infinite while loop will occur.

### Expected behavior

The assembly load context should unload.

### Actual behavior

The ALC stays in memory due to having a strong handle to itself through this:

```
HandleTable:
000001aff35a13e8 (strong handle)
-> 01aff3800028 System.Object[]
-> 01aff58725a8 System.Collections.Generic.Dictionary (static variable: System.Reflection.MethodInfo.Type__GetType3_MethodInfo)
-> 01aff5880cb0 System.Collections.Generic.Dictionary+Entry[]
-> 01aff5857628 System.Reflection.RuntimeAssembly
-> 01aff58575e0 System.Reflection.LoaderAllocator
```

### Regression?

_No response_

### Known Workarounds

_No response_

### Configuration

.NET Version: .NET 10
OS: Windows 11

### Other information

_No response_

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.