Skip to content

DispatchProxy cannot work with types from collectible assemblies. #62050

Closed
@teo-tsirpanis

Description

@teo-tsirpanis

Description

The System.Reflection.DispatchProxy type creates its types in an AssemblyBuilder created with AssemblyBuilderAccess.Run, leading to not just a resource leak when DispatchProxies with collectible types are used, but a total inability, because "[a] non-collectible assembly may not reference a collectible assembly".

Reproduction Steps

I wrote the following demo that tests the unloadability of an ALC:

using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Loader;

public class SimpleProxy<T> : DispatchProxy where T: class
{
    public T Target { get; private set; }

    public static T Decorate(T target = null)
    {
        var proxy = (SimpleProxy<T>) (object) Create<T, SimpleProxy<T>>();
        proxy.Target = target ?? Activator.CreateInstance<T>();
        return proxy as T;
    }

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        var result = targetMethod.Invoke(Target, args);
        return result;
    }
}

public interface ITestObject { }

public class TestObject : ITestObject
{
    public static ITestObject Create() => new TestObject();
    public static ITestObject CreateDecorated() =>
        SimpleProxy<ITestObject>.Decorate(Create());
}

static class Program
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    static WeakReference CreateObjectInUnloadableALC(bool createDecorated)
    {
        var alc = new AssemblyLoadContext(null, true);
        var asm = alc.LoadFromAssemblyPath(typeof(Program).Assembly.Location);
        var typ = asm.GetType(nameof(TestObject));
        var methodName = createDecorated ? nameof(TestObject.CreateDecorated) : nameof(TestObject.Create);
        var method = typ.GetMethod(methodName);
        method.Invoke(null, null);
        return new WeakReference(alc);
    }

    static void CollectUntilWeakReferenceDies(WeakReference wr)
    {
        int i;
        for (i = 0; i < 10 && wr.IsAlive; i++)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
        if (wr.IsAlive)
            Console.WriteLine("Still alive");
        else
            Console.WriteLine($"Collected after {i} GCs.");
    }

    static void Main()
    {
        Console.WriteLine("Testing with a raw object");
        var wr = CreateObjectInUnloadableALC(false);
        CollectUntilWeakReferenceDies(wr);
        Console.WriteLine("Testing with an object wrapped by DispatchProxy");
        wr = CreateObjectInUnloadableALC(true);
        CollectUntilWeakReferenceDies(wr);
    }
}

Expected behavior

Testing with a raw object
Collected after 1 GCs.
Testing with an object wrapped by DispatchProxy
Collected after 1 GCs.

Actual behavior

Testing with a raw object
Collected after 1 GCs.
Testing with an object wrapped by DispatchProxy
Unhandled exception. System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.NotSupportedException: A non-collectible assembly may not reference a collectible assembly.
   at System.Reflection.Emit.ModuleBuilder.GetTypeRef(QCallModule module, String strFullName, QCallModule refedModule, String strRefedModuleFileName, Int32 tkResolution)
   at System.Reflection.Emit.ModuleBuilder.GetTypeRefNested(Type type, Module refedModule, String strRefedModuleFileName)
   at System.Reflection.Emit.ModuleBuilder.GetTypeTokenWorkerNoLock(Type type, Boolean getGenericDefinition)
   at System.Reflection.Emit.ModuleBuilder.GetTypeTokenInternal(Type type, Boolean getGenericDefinition)
   at System.Reflection.Emit.SignatureHelper.AddOneArgTypeHelperWorker(Type clsArgument, Boolean lastWasGenericInst)
   at System.Reflection.Emit.SignatureHelper.AddOneArgTypeHelperWorker(Type clsArgument, Boolean lastWasGenericInst)
   at System.Reflection.Emit.SignatureHelper.GetTypeSigToken(Module module, Type type)
   at System.Reflection.Emit.ModuleBuilder.GetTypeTokenWorkerNoLock(Type type, Boolean getGenericDefinition)
   at System.Reflection.Emit.ModuleBuilder.GetTypeTokenInternal(Type type, Boolean getGenericDefinition)
   at System.Reflection.Emit.TypeBuilder..ctor(String fullname, TypeAttributes attr, Type parent, Type[] interfaces, ModuleBuilder module, PackingSize iPackingSize, Int32 iTypeSize, TypeBuilder enclosingType)
   at System.Reflection.Emit.ModuleBuilder.DefineType(String name, TypeAttributes attr, Type parent)
   at System.Reflection.DispatchProxyGenerator.ProxyAssembly.CreateProxy(String name, Type proxyBaseType)
   at System.Reflection.DispatchProxyGenerator.GenerateProxyType(Type baseType, Type interfaceType)
   at System.Reflection.DispatchProxyGenerator.GetProxyType(Type baseType, Type interfaceType)
   at System.Reflection.DispatchProxyGenerator.CreateProxyInstance(Type baseType, Type interfaceType)
   at System.Reflection.DispatchProxy.Create[T,TProxy]()
   at SimpleProxy`1.Decorate(T target) in Program.cs:line 12
   at TestObject.CreateDecorated() in Program.cs:line 33
   --- End of inner exception stack trace ---
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
   at Program.CreateObjectInCollectibleALC(Boolean createDecorated) in Program.cs:line 46
   at Program.Main() in Program.cs:line 70

Regression?

No

Known Workarounds

Use non-collectible ALCs.

Configuration

Tested in .NET 5, the relevant code did not change in 6.

Other information

The logic around creating AssemblyBuilders needs to change; I will try to do it in the future.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions