IMallocSpy Woes:
COM Runtime Holds on to Memory after CoUninitialize()


The information in this article applies to: NOTE: although this article loosely follows traditional structure of MSDN articles, it is not affiliated with MSDN in any way.

Symptoms

COM provides CoRegisterMallocSpy() API that allows user code to monitor COM memory allocations and deallocations (see IMalloc, IMallocSpy, CoTaskMemAlloc(), CoTaskMemFree()). This includes memory allocations made explicitly by user code as well as memory allocations made implicitly by COM runtime on behalf of the user.

Experiment shows that on Windows 2000/XP some memory still remains allocated even after COM has been shut down by calling CoUninitialize(). This memory is eventually freed by program termination code in ExitProcess().

Behavior on Windows XP is the same.

This makes detection of COM memory leaks difficult. At no moment in our program we can say "OK, all COM memory should be released by now. If anything is not released, it is definitely a leak".

Life used to be easier on Windows NT. On Windows NT CoUninitialize() released all the memory allocated by COM runtime. Thus, a good time to check for leaks was right after CoUninitialize(). Anything not released by then could be safely declared a leak. Not anymore.

Cause

Windows 2000 introduces a new system DLL named clbcatq.dll. This DLL is loaded by COM runtime when you call CLSIDFromProgID() (and of course porbably in some other cases too). clbcatq.dll startup code located in DllMain() allocates some COM memory using CoTaskMemAlloc(). This memory is released by clbcatq.dll shutdown code, also in DllMain().

The caveat is that CoUninitialize() does not unload clbcatq.dll! This appears to contradict statements from CoUninitialize() help page:

CoUninitialize closes the COM library on the current thread, unloads all DLLs loaded by the thread, frees any other resources that the thread maintains, and forces all RPC connections on the thread to close.

Anyway, clbcatq.dll DLL gets unloaded by ExitProcess() upon program termination. The memory is released at this time using CoTaskMemFree().

Test Project

The test project MallocSpyTest is a console application. It tries to create an instance of a COM coclass specified on the command line. It also implements IMallocSpyinterface and prints to standard output information about all COM alocations and deallocations.

After CoUninitialize() it tries to revoke malloc spy, and displays list of memory blocks that are not freed.

Test project can be invoked as follows:

  MallocSpyTest ADODB.Recordset

You can use any COM coclass you like instead of ADODB.Recordset.

Test Results

Test results for NT are pretty straightforward:
C:\TEMP>MallocSpyTest.exe ADODB.Recordset
CoInitialize() returns 0x00000000
CoRegisterMallocSpy() returns 0x00000000
CLSIDFromProgID() returns 0x00000000
MALLOC SPY: 0x00139E90 - 216 bytes allocated
MALLOC SPY: 0x00138650 - 16 bytes allocated
MALLOC SPY: 0x00139F70 - 16 bytes allocated
CoCreateInstance() returns 0x00000000
Object released
MALLOC SPY: 0x00138650 - freed, 2 blocks remaining
MALLOC SPY: 0x00139F70 - freed, 1 blocks remaining
MALLOC SPY: 0x00139E90 - freed, 0 blocks remaining
Called CoUninitialize()
CoRevokeMallocSpy() returns 0x00000000

C:\TEMP>

However, results on Windows 2000 are astonishing:
C:\Ivan\cpp\MallocSpyTest>MallocSpyTest ADODB.Recordset
CoInitialize() returns 0x00000000
CoRegisterMallocSpy() returns 0x00000000
MALLOC SPY: 0x0013BBB0 - 28 bytes allocated
more allocations...
CLSIDFromProgID() returns 0x00000000
[...]
CoCreateInstance() returns 0x00000000
Object released
[...]
MALLOC SPY: 0x0013C150 - freed, 18 blocks remaining
[...]
Called CoUninitialize()
CoRevokeMallocSpy() returns 0x80070005
MALLOC SPY: detected memory leaks!
0x0013A8F8 - 44 bytes: 98 B1 5A 77 08 BC 13 00 01 00 00 00 01 00 11 00 ..Zw............
more leaks...
MALLOC SPY: 0x0013C0C0 - freed, 11 blocks remaining
[...]
MALLOC SPY: 0x0013BED0 - freed, 1 blocks remaining

Take a note of two things:

  1. Some memory is still held by COM runtime after CoUninitialize()
  2. We cannot revoke our spy. CoRevokeMallocSpy() fails with error code is 0x8007005 which stands for E_ACCESSDENIED. I.e., we are not allowed to revoke the spy, despite the fact that COM has been shut down. This obviously makes using spy very problematic.

Since our spy continues to work while the program is being terminated, and continues to call printf() while inside ExitProcess(), weird things happen. printf() is not really designed to work during ExitProcess(). One side effect of this is that if you redirect output of MallocSpyTest to a file (as in  MallocSpyTest ADODB.Recordset >out.txt ), last lines of output won't go to the file.

Results on XP are similar to those on 2000, main difference is more allocations.

Download