Skip to content

Memory leak in COM interop (IDispatch↔IReflect) #76350

@ClearScriptLib

Description

@ClearScriptLib

Description

When an object that implements IReflect is queried for IDispatch, the runtime enumerates the object's members and constructs a CCW. This operation seems to create managed arrays that outlive the CCW, and subsequent short-lived CCWs leak increasing amounts of managed memory.

Reproduction Steps

The following program reproduces the leak:

using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;

while (true)
{
    var pDispTest = Marshal.GetIDispatchForObject(new Test());
    Marshal.Release(pDispTest);
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

internal sealed class Test : IReflect
{
    private static readonly FieldInfo[] _fields = Enumerable.Range(0, 10).Select(i => (FieldInfo)new Field($"f{i}")).ToArray();
    private static readonly MethodInfo[] _methods = Enumerable.Range(0, 10).Select(i => (MethodInfo)new Method($"m{i}")).ToArray();
    private static readonly PropertyInfo[] _properties = Enumerable.Range(0, 10).Select(i => (PropertyInfo)new Property($"p{i}")).ToArray();

    public FieldInfo[] GetFields(BindingFlags bindingAttr) => _fields;
    public MethodInfo[] GetMethods(BindingFlags bindingAttr) => _methods;
    public PropertyInfo[] GetProperties(BindingFlags bindingAttr) => _properties;
    // all other IReflect members throw NotImplementedException

    private sealed class Property : PropertyInfo
    {
        public Property(string name) => Name = name;
        public override string Name { get; }
        // all other required overrides throw NotImplementedException
    }

    private sealed class Field : FieldInfo
    {
        public Field(string name) => Name = name;
        public override string Name { get; }
        // all other required overrides throw NotImplementedException
    }

    private sealed class Method : MethodInfo
    {
        public Method(string name) => Name = name;
        public override string Name { get; }
        // all other required overrides throw NotImplementedException
    }
}

Expected behavior

No memory leak.

Actual behavior

Snapshots at program start and after 10,000 iterations:

image

Managed heap diff:

image

Leaked arrays:

image

Leaked array contents:

image

Our best guess: It looks like each CCW adds all reflected member references to a long-lived, ever-growing managed collection that uses arrays under the hood.

Regression?

Unknown.

Known Workarounds

None.

Configuration

  • .NET SDK 6.0.401
  • Windows 11 21H2
  • x64

Other information

None.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions