Stop using IntPtr for dealing with system handles

 
 
  • Gérald Barré

When using system handles such as file handles, process handles, or any other handles provided by the kernel, you must release them correctly when you no longer need them. Native APIs typically provide a method to acquire a handle, a method to release it, and sometimes a few methods to work with the resource. For example, you can get a file handle using CreateFile, release it using CloseHandle, and write to the file using WriteFile. To avoid leaking the resource, you need to call CloseHandle exactly once as soon as you are done with the file. But what if an exception is thrown before you can release the handle? The exception may not even originate from your own code: a thread abort or an out-of-memory condition could prevent the cleanup from running.

SafeHandle was introduced in .NET but is still not widely used outside the .NET Framework itself. SafeHandle provides many advantages:

  • The finalizer logic is already implemented, preventing mistakes when writing one. In addition, SafeHandle inherits from CriticalFinalizerObject, making it more reliable than other objects (it can be freed even when a ThreadAbortException or OutOfMemoryException is raised).
  • Classes that own a SafeHandle do not need their own finalizer, making them easier to write.
  • Objects with finalizers receive special treatment from the Garbage Collector. You should keep them as small as possible to avoid promoting a large object graph during finalization. SafeHandle is a minimal wrapper around an unmanaged resource, which reduces this concern.
  • SafeHandle is well-integrated with P/Invoke. You can use a SafeHandle-derived class in method signatures instead of IntPtr, giving you strong typing.
  • SafeHandle is well-integrated with the Garbage Collector. You do not need to use HandleRef and GC.KeepAlive explicitly when calling a native method; the CLR ensures the SafeHandle is not finalized while a native call is in progress.
  • SafeHandle prevents handle-recycling vulnerabilities: Lifetime, GC.KeepAlive, handle recycling

SafeHandle is an abstract class, so you must inherit from it. When doing so, you must override two members: IsInvalid and ReleaseHandle. You should also provide a default constructor that calls the base constructor with a value representing an invalid handle, and a boolean indicating whether the SafeHandle owns the native handle and should therefore free it on disposal. Note that ReleaseHandle is guaranteed to be called at most once and only when the handle is considered valid according to IsInvalid. Alternatively, you can inherit from SafeHandleZeroOrMinusOneIsInvalid, which already implements IsInvalid for the common case.

C#
// inherits from SafeHandleZeroOrMinusOneIsInvalid, so IsInvalid is already implemented.
internal sealed class MySafeHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    // A default constructor is required for P/Invoke to instantiate the class
    public MySafeHandle()
        : base(ownsHandle: true)
    {
    }

    protected override bool ReleaseHandle()
    {
        return NativeMethods.CloseHandle(handle);
    }
}

internal static class NativeMethods
{
    // Returns the SafeHandle instead of IntPtr
    [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Unicode)]
    internal extern static MySafeHandle CreateFile(String fileName, int dwDesiredAccess, System.IO.FileShare dwShareMode, IntPtr securityAttrs_MustBeZero, System.IO.FileMode dwCreationDisposition, int dwFlagsAndAttributes, IntPtr TemplateFile_MustBeZero);

    // Take a SafeHandle in parameter instead of IntPtr
    [DllImport("kernel32", SetLastError = true)]
    internal extern static int ReadFile(MySafeHandle handle, byte[] bytes, int numBytesToRead, out int numBytesRead, IntPtr overlapped_MustBeZero);

    [DllImport("kernel32", SetLastError = true)]
    internal extern static bool CloseHandle(IntPtr handle);
}

As all the hard work is done by the SafeHandle, your code is much simpler! Here's an example of usage of MySafeHandle:

C#
public sealed class MyFileWrapper : IDisposable
{
    private readonly MySafeHandle _handle;

    public MyFileWrapper(string fullPath)
    {
        _handle = NativeMethods.CreateFile(fullPath, ...);
    }

    // - There is no need to implement a finalizer, MySafeHandle already has one
    // - You do not need to protect against multiple disposing, MySafeHandle already does
    public void Dispose()
    {
        _handle.Dispose();
    }
}

#Guidelines

When working with unmanaged resources, you should consider:

  1. Using an existing SafeHandle if possible
  2. If not possible, subclass SafeHandle to create one that meets your needs. This class should not do anything more than managing unmanaged resources. It should be sealed.
  3. If that's not possible, create your class which implements IDisposable and a finalizer
    1. The class should be sealed
    2. If sealing the class is not possible, add a protected void Dispose(bool disposing) method so subclasses can implement the dispose pattern correctly.

#Resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?