Partager via


SafeHandles: the best V2.0 feature of the .NET Framework [Ravi Krishnaswamy]

Rather than giving a short response to Jeff Atwood's question, 'If you had to pick one, what is the single most important feature in 2.0 that platform developers should be experimenting with and exploiting as soon as they get their hands on it? ', I thought I would give a fuller explanation of my response, which is: SafeHandles.

In Whidbey (V2.0), throughout the frameworks APIs we took advantage of the new reliability and safety features, namely SafeHandle and ConstrtainedExecutionRegion (CER). I’m sure there are tons of good material out there on these topics but I'll add my 2cents here (more the merrierJ).  

Let’s look at SafeHandle first. In V1.x, all OS handles had to be encapsulated in an IntPtr managed (wrapper) object. While this was a convenient way to interop with native code, there was no reliability and safety measures around it. You can easily leak handles stored inside IntPtr returned from a P/Invoke call, when we run into exceptional conditions such as rude thread abort, out-of-memory, stack overflow etc. Since this is just an integer value wrapped, you can also understand the possibilities for handle recycle attacks. I.e, OS recycles handle values periodically, this is more pronounced for long lived processes and resource constrained environments. You can get into a situation where you are holding a stale IntPtr object whose wrapped (recycled) handle could now be pointing to some other (secure) resource instead of the original one, thus opening a security backdoor!

(Btw, These failure points are present in native code programming as well).

SafeHandle was designed to address the OS handle issues above. The goal is to be able to be able to implement managed that will NOT leak a OS handle. This is a very bold requirement and let’s see how we can implement that. This means we need to assign and release OS handle to the SafeHandle managed object in a non-interrupted way (at least from a CLR perpective). This is where CER and CriticalFinalization comes into picture. I’ll explain about the mechanics of CERs later. For now, think of this as a way to guarantee that the execution will be deterministic between getting back an OS handle value (perhaps in a P/Invoke call) and assigning it to SafeHandle. This means there will not be any CLR induced failures that will leave us in an inconsistent state. Fortunately, most of this work is done for you by the P/Invoke layer in Whidbey, it is smart enough to recognize SafeHandle return value and OUT params and simply do the right marshalling behind the scenes.

SafeHandle inherits from CriticalFinalizerObject which guarantees that the finalizer will be run, won't be aborted by the host, and that it will be run after the finalizers of other objects collected at the same time (this is important because it ensures that classes like FileStream can run a normal finalizer to flush out existing buffered data without worrying about the state of the SafeFileHandle).

So we have solved the handle leak reliability issue but we are still left with handle recycle security issue. Fortunately, this can be solved by implementing simple ref-counting of pending handle usage in SafeHandle. In Whidbey, P/Invoke layer will automatically increment the SafeHandle ref-count anytime the underlying handle value is passed to the native code in a Win32 method call, and decrement it upon completion. This will ensure that any out-of-band async call to methods like CloseHandle (perhaps from another thread) will not release the OS handle while there are other P/Invoke calls that are pending. Of course, all of this only works as long as you don’t by-pass P/Invoke layer and SafeHandle.

Let’s now look at CER bit more in detail. It is a region of code within which CLR will guarantee that there will not be any infrastructure related failures such as out of memory from JIT’ing the code, ThreadAbortException etc. CLR will eagerly prepare this code block, meaning it will JIT any necessary code ahead of time before the first instruction of CER code block is executed so that any failure is caught before we enter into the CER. Similarly, once inside CLR will defer all rude failures such as thread abort until after the final instruction of the CER code block is executed.  

For CER explanation sake, let’s assume that P/Invoke layer is not intelligent about SafeHandle and that we need to do the marshalling of native handle ourselves. First of all, let’s implement our own SafeHandle derivative which inherits from SafeHandleZeroOrMinusOneIsInvalid (meaning 0 or -1 represents invalid handle)

  public sealed MySafeHandle : SafeHandleZeroOrMinusOneIsInvalid { 

      // This default ctor will be called by P/Invoke smart marshalling when returning MySafeHandle in a method call

      private MySafeHandle () : base(IntPtr.Zero, true)

      {

      }

 

      // We need this so that we can do our own marshalling (and may be for user supplied handles)

      internal MySafeHandle (IntPtr preexistingHandle, bool ownsHandle) : base(IntPtr.Zero, ownsHandle)

      {

          SetHandle(preexistingHandle);

      }

 

      // We should not provide a finalizer - SafeHandle's critical finalizer will call ReleaseHandle inside a CER for us.

      override protected bool ReleaseHandle()

      {

          return MyNativeMethods.CloseHandle(handle);

      }

  }

Let us also define the associated native methods. We should specify the SuppressUnmanagedCodeSecurity attribute to avoid a runtime security check that can inject failures (even if the check is guaranteed to pass). This is okay because we already require unmanaged code access permission for sub classing SafeHandle. It is important that the methods that are called within CER must not currupt state but it may fail (OS handle creating failing due to resource situation perhaps).

  [SuppressUnmanagedCodeSecurity]

  internal static class MyNativeMethods {

     [DllImport("myNativeDll", SetLastError=true), ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]

      private static extern Int32 CreateHandle(out Int32 handle);

 

      [DllImport("myNativeDll", SetLastError=true), ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]

      private static extern bool CloseHandle(IntPtr handle);

  }

You might have noticed the odd looking ReliabilityContract custom attribute decoration for CloseHandle above. ReliabilityContractAttribute is a way to tell CLR (in a programmatically discoverable way) what kind of guarantees our method implementation makes when it encounters extraordinary conditions (such as rude thread abort, out-of-memory, stack overflow etc). Here we are saying that the call to CloseHandle which is called from MySafeHandle.ReleaseHandle will not corrupt the state and always guaranteed to succeed. These are the contracts as specified by the SafeHandle.ReleaseHandle base class virtual method and it is required that our derived implementation satisfies it.

    // SafeHandle.ReleaseHandle declaration    

    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]

    protected abstract bool ReleaseHandle();

We should be careful not to write any code that's subject to faults inside the ReleaseHandle method. The runtime will eagerly prepare this method and guarantee that there will not be any CLR induced failures inside this method but we shouldn’t allocate memory (and other such failure points) unless we can deal with the failure and still free the handle.

Let’s look at how we can assign OS handle to our SafeHandle object inside a CER. Calling RuntimeHelpers.PrepareConstrainedRegions() tells the CLR to treat the subsequent try-finally block as CER and eagerly prepare the code block.   

//Best practice to avoid object allocation inside CER.

MySafeHandle mySafeHandle = new MySafeHandle(0, true);

IntPtr myHandle;

IntPtr invalidHandle = new IntPtr(-1));

Int32 ret;

// The creation of myHandle and assignment to mySafeHandle should be done inside a CER

RuntimeHelpers.PrepareConstrainedRegions();

try {// Begin CER

}

finally {

ret = MyNativeMethods.CreateHandle(out myHandle);

if (ret ==0 && !myHandle.IsNull() && myHandle != invalidHandle)

mySafeHandle.SetHandle(myHandle);

}// End CER

That takes care of handle assignment and release but we still have to implement ref-counting. For this, SafeHandle defines the following methods to increment and decrement the ref-count respectively.

    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]

    [MethodImplAttribute(MethodImplOptions.InternalCall)]

    public extern void DangerousAddRef(ref bool success);

 

    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]

    [MethodImplAttribute(MethodImplOptions.InternalCall)]

    public extern void DangerousRelease();

Now that we understand CER, it should be easy to follow the code below which ensures that our ref-counting implementation satisfies the reliability contracts above.

    bool mustReleaseSafeHandle = false;

    RuntimeHelpers.PrepareConstrainedRegions();

    try {

    }

    finally {

        mySafeHandle.DangerousAddRef(ref mustReleaseSafeHandle);

        // Call some native method that takes IntPtr

        MyUnsafeNativeCall(mySafeHandle.DangerousGetHandle());

        if (mustReleaseSafeHandle)

            mySafeHandle.DangerousRelease();

    }

Note that DangerousAddRef is returning success via ref param instead of return value, this is done to make the call atomic and to avoid any failure points between calling the method and assigning the return value to a variable.

These helper methods are marked dangerous for obvious reasons. If not used carefully, this can easily regress the issues that we are trying to solve.

Bottomline, you should rely on P/Invoke to do the SafeHandle marshalling. The above technique is mostly for explanation purpose just in case you need to wrap native handle in the wild for one reason or other.

Comments

  • Anonymous
    March 15, 2005
    Do you happen to know whether Managed DirectX is using SafeHandles yet?

  • Anonymous
    March 15, 2005
    境界線ネタ

  • Anonymous
    March 17, 2005
    Tips

  • Anonymous
    March 21, 2005
    Tips

  • Anonymous
    April 15, 2005
    Dispose, Finalization, and Resource Management

  • Anonymous
    June 27, 2005
    A customer recently asked a good question about some of our new reliability features in Whidbey:

    There...

  • Anonymous
    April 19, 2007
    You might be wondering why I'm writing about the IDisposable interface a full five years after the

  • Anonymous
    June 17, 2007
    Prvni z clanku o P/Invoke, window stations a desktopech...