2

I've read that on Windows, malloc is different from CoTaskMemAlloc, which is different from AllocHGlobal. For C# consumers, this supposedly means if I've got a C function that returns a malloc'd pointer, I need to call free on it. If I P/Invoke and specify a string return type, the CLR is supposed to call CoTaskMemFree and should fail on a malloc'd pointer.

However, I'm not able to see this in practice. I created a DLL that just returns a malloc'd char* and P/Invoked it as a string. No memory leak. Indeed, no matter what I try, I cannot get things to fail. I call malloc, GlobalHAlloc, CoTaskMemAlloc, then use any free implementation and things just work. No memory leak. Same memory space is re-used.

How can I force this to fail? Or is this one of those things that's supposed to be "implementation defined" but in reality just works one way?

This is on VS2015 Update 2, Windows 8.1.

MichaelGG
  • 9,976
  • 1
  • 39
  • 82
  • You cannot expect your code to fail with an error always. Sometimes it will, sometimes it won't. If you allocate with malloc and deallocate with CoTaskMemFree then you will leak. I guess your leak detection is faulty. – David Heffernan Apr 05 '16 at 08:24
  • I've tried new/free, CoTaskAllocMem/free, AllocHGlobal/free, in that order. They return the same pointer. So it's certainly not leaking. I've also tried allocating 20GB using malloc (1MB at a time), then freeing with P/Invoke default and FreeHGlobal/CoTaskFreeMem. No leak (no increase in memory usage). If I don't call any free function, then the process quickly balloons. – MichaelGG Apr 05 '16 at 08:32
  • 1
    You are allocating a very large block of memory? Probably all the allocators delegate that to a direct call to `VirtualAlloc`. Try allocating a handful of bytes. – David Heffernan Apr 05 '16 at 08:52
  • Hm you seem correct on the large allocations - if I use 64-byte allocations then malloc v CoTaskMemAlloc return different addresses after free. However, doing a loop of malloc(64) in C, P/Invoked, then CoTaskMemFree/FreeHGlobal, even after 100M iterations shows no increase in memory use. (If I don't call any free, then memory quickly rises.) – MichaelGG Apr 05 '16 at 09:22
  • Why are you trying to allocate with one allocator, and deallocate with another? Why don't you just follow the rules? – David Heffernan Apr 05 '16 at 09:35
  • I'm debugging something with >500Kloc of autogenerated code doing C-interop, and, from what I've read, it shouldn't work at all (most return values are malloc'd, but default P/Invoke is used). So I'm trying to repro what's going on and figure out if the docs are just overly cautious or apply to some other implementation. Or if there are trigger-able conditions in which case the docs do apply. – MichaelGG Apr 05 '16 at 09:38
  • 1
    It's plausible that some implementations of malloc and some implementation of CoTaskMemAlloc use HeapAlloc. Perhaps others do different things. You cannot expect failure though. Nothing promises that. If I were you I'd just fix the defects. – David Heffernan Apr 05 '16 at 09:42

1 Answers1

8

This certainly did not used to work. But things have been changing. The C runtime library was changed in VS2012 and it no longer creates its own heap anymore. For VS2015, this code in C:\Program Files (x86)\Windows Kits\10\Source\10.0.10240.0\ucrt\heap\heap_handle.cpp is relevant:

// Initializes the heap.  This function must be called during CRT startup, and
// must be called before any user code that might use the heap is executed.
extern "C" bool __cdecl __acrt_initialize_heap()
{
    __acrt_heap = GetProcessHeap();
    if (__acrt_heap == nullptr)
        return false;

    return true;
}

Note the call to GetProcessHeap(), it returns the same heap that Marshal.AllocHGlobal() allocates from. So yes, you are not going to get an exception from the debug heap nor a leak when you de-allocate with Marshal.FreeHGlobal().

Much the same for CoTaskMemAlloc(). Single-stepping into the function, I see:

7638D1E1  mov         esi,dword ptr [g_CMalloc (76485EE0h)]  
7638D1E7  push        dword ptr [ebp+8]  
7638D1EA  mov         esi,dword ptr [esi+0Ch]  
7638D1ED  cmp         esi,offset CRetailMalloc_Alloc (763732C0h)  
7638D1F3  jne         CoTaskMemAlloc+44h (7638D214h)  
7638D1F5  push        0  
7638D1F7  push        dword ptr [g_hHeap (76485E68h)]  
7638D1FD  call        dword ptr [__imp__HeapAlloc@12 (76488228h)] 

Note the usage of the g_hHeap variable. I see it have the same value as GetProcessHeap() returns. So again, releasing with any of the deallocation functions is going to work just fine.

Do note that this I got this from Windows 10, an older version of Windows is not going to behave the same way. And note CRetailMalloc_Alloc, a not so pleasant randomizer with an unpredictable usage.

While this was certainly meant to be helpful, a lot less ways in which programs can fail, it isn't actually useful when you test your app. Nasty, really, it invokes the "only works on my machine" failure mode. Bah.

Community
  • 1
  • 1
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • That's excellent info. It'd be nice if some docs somewhere mentioned this -- just a bunch of questions and posts stating it doesn't work. Anyways, I guess this isn't my actual problem then and it "works" on all platforms I care about, unfortunately. Cheers! – MichaelGG Apr 05 '16 at 10:12
  • Why would documentation mention implementation details that are subject to change? – David Heffernan Apr 05 '16 at 10:50
  • Not sure if the official docs say it'll crash, but several more authoritative posts on the subject do. Knowing that for years this hasn't been the case might help someone figure out why it works on some platforms when it shouldn't, and help rule out this as a source of problem on an app more quickly. – MichaelGG Apr 05 '16 at 18:22
  • Well, they are not going to get it from this Q+A at the rate it is going. Pretty unclear to me why it isn't marked answered yet, I don't know what else to tell you. Put a bounty on the question when you need more answers. – Hans Passant Apr 05 '16 at 18:32
  • Sorry Hans, it slipped my mind; definitely a great answer and I appreciate it very much. – MichaelGG Apr 07 '16 at 07:04