Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NullReferenceException in finalizer during MSTest

Tags:

(I know, this is a ridiculously long question. I tried to separate the question from my investigation so far, so it's slightly easier to read.)

I'm running my unit tests using MSTest.exe. Occasionally, I see this test error:

On the individual unit test method: "The agent process was stopped while the test was running."

On the entire test run:

One of the background threads threw exception: 
System.NullReferenceException: Object reference not set to an instance of an object.
   at System.Runtime.InteropServices.Marshal.ReleaseComObject(Object o)
   at System.Management.Instrumentation.MetaDataInfo.Dispose()
   at System.Management.Instrumentation.MetaDataInfo.Finalize()

So, here's what I think I need to do: I need to track down what is causing the error in MetaDataInfo, but I'm drawing a blank. My unit test suite takes over half an hour to run, and the error doesn't happen every time, so it's hard to get it to reproduce.

Has anyone else seen this type of failure in running unit tests? Were you able to track it down to a specific component?

Edit:

The code under test is a mix of C#, C++/CLI, and a little bit of unmanaged C++ code. The unmanaged C++ is used only from the C++/CLI, never directly from the unit tests. The unit tests are all C#.

The code under test will be running in a standalone Windows Service, so there's no complication from ASP.net or anything like that. In the code under test, there's threads starting & stopping, network communication, and file I/O to the local hard drive.


My investigation so far:

I spent some time digging around the multiple versions of the System.Management assembly on my Windows 7 machine, and I found the MetaDataInfo class in System.Management that's in my Windows directory. (The version that's under Program Files\Reference Assemblies is much smaller, and doesn't have the MetaDataInfo class.)

Using Reflector to inspect this assembly, I found what seems to be an obvious bug in MetaDataInfo.Dispose():

// From class System.Management.Instrumentation.MetaDataInfo:
public void Dispose()
{
    if (this.importInterface == null) // <---- Should be "!="
    {
        Marshal.ReleaseComObject(this.importInterface);
    }
    this.importInterface = null;
    GC.SuppressFinalize(this);
}

With this 'if' statement backwards, MetaDataInfo will leak the COM object if present, or throw a NullReferenceException if not. I've reported this on Microsoft Connect: https://connect.microsoft.com/VisualStudio/feedback/details/779328/

Using reflector, I was able to find all uses of the MetaDataInfo class. (It's an internal class, so just searching the assembly should be a complete list.) There is only one place it is used:

public static Guid GetMvid(Assembly assembly)
{
    using (MetaDataInfo info = new MetaDataInfo(assembly))
    {
        return info.Mvid;
    }
}

Since all uses of MetaDataInfo are being properly Disposed, here's what's happening:

  • If MetaDataInfo.importInterface is not null:
    • static method GetMvid returns MetaDataInfo.Mvid
    • The using calls MetaDataInfo.Dispose
      • Dispose leaks the COM object
      • Dispose sets importInterface to null
      • Dispose calls GC.SuppressFinalize
    • Later, when the GC collects the MetaDataInfo, the finalizer is skipped.
  • .
  • If MetaDataInfo.importInterface is null:
    • static method GetMvid gets a NullReferenceException calling MetaDataInfo.Mvid.
    • Before the exception propagates up, the using calls MetaDataInfo.Dispose
      • Dispose calls Marshal.ReleaseComObject
        • Marshal.ReleaseComObject throws a NullReferenceException.
      • Because an exception is thrown, Dispose doesn't call GC.SuppressFinalize
    • The exception propagates up to GetMvid's caller.
    • Later, when the GC collects the MetaDataInfo, it runs the Finalizer
      • Finalize calls Dispose
        • Dispose calls Marshal.ReleaseComObject
          • Marshal.ReleaseComObject throws a NullReferenceException, which propagates all the way up to the GC, and the application is terminated.

For what it's worth, here's the rest of the relevant code from MetaDataInfo:

public MetaDataInfo(string assemblyName)
{
    Guid riid = new Guid(((GuidAttribute) Attribute.GetCustomAttribute(typeof(IMetaDataImportInternalOnly), typeof(GuidAttribute), false)).Value);
    // The above line retrieves this Guid: "7DAC8207-D3AE-4c75-9B67-92801A497D44"
    IMetaDataDispenser o = (IMetaDataDispenser) new CorMetaDataDispenser();
    this.importInterface = (IMetaDataImportInternalOnly) o.OpenScope(assemblyName, 0, ref riid);
    Marshal.ReleaseComObject(o);
}

private void InitNameAndMvid()
{
    if (this.name == null)
    {
        uint num;
        StringBuilder szName = new StringBuilder {
            Capacity = 0
        };
        this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid);
        szName.Capacity = (int) num;
        this.importInterface.GetScopeProps(szName, (uint) szName.Capacity, out num, out this.mvid);
        this.name = szName.ToString();
    }
}

public Guid Mvid
{
    get
    {
        this.InitNameAndMvid();
        return this.mvid;
    }
}

Edit 2:

I was able to reproduce the bug in the MetaDataInfo class for Microsoft. However, my reproduction is slightly different from the issue I'm seeing here.

  • Reproduction: I try to create a MetaDataInfo object on a file that isn't a managed assembly. This throws an exception from the constructor before importInterface is initialized.
  • My issue with MSTest: MetaDataInfo is constructed on some managed assembly, and something happens to make importInterface null, or to exit the constructor before importInterface is initialized.
    • I know that MetaDataInfo is created on a managed assembly, because MetaDataInfo is an internal class, and the only API that calls it does so by passing the result of Assembly.Location.

However, re-creating the issue in Visual Studio meant that it downloaded the source to MetaDataInfo for me. Here's the actual code, with the original developer's comments.

public void Dispose()
{ 
    // We implement IDisposable on this class because the IMetaDataImport
    // can be an expensive object to keep in memory. 
    if(importInterface == null) 
        Marshal.ReleaseComObject(importInterface);
    importInterface = null; 
    GC.SuppressFinalize(this);
}

~MetaDataInfo() 
{
    Dispose(); 
} 

The original code confirms what was seen in reflector: The if statement is backwards, and they shouldn't be accessing the managed object from the Finalizer.

I said before that because it was never calling ReleaseComObject, that it was leaking the COM object. I read up more on the use of COM objects in .Net, and if I understand it properly, that was incorrect: The COM object isn't released when Dispose() is called, but it is released when the garbage collector gets around to collecting the Runtime Callable Wrapper, which is a managed object. Even though it's a wrapper for an unmanaged COM object, the RCW is still a managed object, and the rule about "don't access managed objects from the finalizer" should still apply.

like image 200
David Yaw Avatar asked Feb 15 '13 21:02

David Yaw


People also ask

What does system NullReferenceException mean?

The NullReferenceException is designed as a valid runtime condition that can be thrown and caught in normal program flow. It indicates that you are trying to access member fields, or function types, on an object reference that points to null. That means the reference to an Object which is not initialized.

Why am I getting a NullReferenceException?

This error is caused when an object is trying to be used by a script but does not refer to an instance of an object. To fix this example we can acquire a reference to an instance of the script using GameObject.

Should I throw NullReferenceException?

You should never throw a NullReferenceException manually. It should only ever be thrown by the framework itself. From the NullReferenceException documentation: Note that applications throw the ArgumentNullException exception rather than the NullReferenceException exception discussed here.


1 Answers

Try to add the following code to your class definition:

bool _disposing = false  // class property

public void Dispose()
{
    if( !disposing ) 
        Marshal.ReleaseComObject(importInterface);
    importInterface = null; 
    GC.SuppressFinalize(this);

    disposing = true;
}
like image 131
user2106839 Avatar answered Nov 20 '22 22:11

user2106839