GCHandles, Boxing and Heap Corruption

A GCHandle is a struct used to hold onto a managed object to be used by unmanaged code.  With a GCHandle you can (among other things):

  • Prevent an object from being garbage collected if unmanaged code has the only live reference to it
  • Pin an object in memory, so it won’t be relocated in memory by the garbage collector
  • Get the memory address of the pinned object

The last point is interesting.  When dealing with managed code, addresses of reference objects (objects allocated on the heap) are not constant.  As the GC tries to reclaim memory, it moves objects around in memory and compacts the heap.  If you’re P/Invoking into an unmanaged DLL, you may need to pass the address of some object.  That’s where GCHandles come in.

The syntax for allocating a pinned GCHandle is as follows (C# code):

Int[] arr = new int[10];
GCHandle gch = GCHandle.Alloc(arr, GCHandleType.Pinned);

The above code pins arr in memory (so it won’t be moved), and creates a new GCHandle that “wraps” arr.  You can now get the address of arr using GCHandle.AddrOfPinnedObject.

Note: you can only pin blittable types in memory (types that have the same representation in managed and unmanaged code).  Blittable types include primitive types and arrays.

The problem occurs when you accidentally misuse Alloc.  For example, I’ve seen code like this:

Object obj = new Object();
GCHandle gch2 = GCHandle.Alloc(Marshal.SizeOf(typeof(obj)), GCHandleType.Pinned);

It looks like the developer tried to allocate a GCHandle with the same size as obj in memory.  So what does this code actually do?

Marshall.SizeOf returns an int that when passed to a method expecting an Object, is boxed into a newly heap-allocated Object.  So the new GCHandle obediently pins this new Object in memory.  Then when you pass gch2.AddrOfPinnedObject to your unmanaged code… trouble.

Consider an unmanaged method takes an array of ints, and increments each element.  You’ve passed it an address, and it dutifully increments each value it finds for the length of the array.  Congratulations, you’ve just corrupted memory.

If you’re lucky, this crashes the runtime right away.  If you’re unlucky, your app may continue to run and crash sometime in the future or your app’s data may be messed up.