Obiekt SafeHandle

W ostatnim poście pisałem jak prawidłowo wykonać finalizację obiektu jeśli mowa o zasobach niezarządzanych, których zwolnienie jest krytyczne. Dzisiaj o obiekcie, który jest bardzo często wykorzystywany w sytuacjach gdzie należy przechowywać wskaźnik do zasobów niezarządzanych. Zacznijmy od jego definicji:

[SecurityPermissionAttribute(SecurityAction.InheritanceDemand, UnmanagedCode = true)]
public abstract class SafeHandle : CriticalFinalizerObject, IDisposable

Co to oznacza? Wszystkie rzeczy jakie daje nam CriticalFinalizerObject (patrz poprzedni post), SafeHandle również zawiera. Zatem mamy do dyspozycji CER co oznacza w praktyce bezpieczny finalizer. Oczywiście klasa również implementuje IDisposable wiec możemy korzystać z metody Dispose lub using dzięki czemu mamy większa kontrolę nad zasobami.  Klasa jest jednak abstrakcyjna więc bezpośrednio nie możemy jej użyć. W praktyce jednak korzystać będziemy z innych klas, które dziedziczą po SafeHandle i implementują potrzebne metody. Przyjrzymy się teraz abstrakcyjnym elementom SafeHandle:

public abstract class SafeHandle : CriticalFinalizerObject, IDisposable 
{
    protected abstract Boolean ReleaseHandle();
    public abstract Boolean IsInvalid{get;}
    //... (reszta kodu)
}

W ReleaseHandle umieszczamy kod, który zwalnia zasoby, z kolei IsInvalid zwraca flagę określającą czy uchwyt (wskaźnik) do zasobów niezarządzanych jest prawidłowy. Zwykle uchwyty są nieprawidłowe gdy mają wartość 0 lub –1 – tak działa WinApi. Z tego względu, .NET dostarcza dodatkową klasę SafeHandleZeroOrMinusOneIsInvalid, która przeładowuje w odpowiedni sposób IsInvalid:

public abstract class SafeHandleZeroOrMinusOneIsInvalid : SafeHandle 
{
    //...
    // Implementaca Invalid prawdopodobnie wygląda następująco
    public override Boolean IsInvalid 
    {
        get 
        {        
            if (base.handle == IntPtr.Zero) return true;
            if (base.handle == (IntPtr) (-1)) return true;
        
            return false;
        }
    }
    //...
}

Z tego względu, jeśli musimy napisać jakiś własny SafeHandle wtedy lepiej skorzystać z powyższej klasy ponieważ prawie zawsze implementacja Invalid będzie wyglądać jak przedstawiono to w powyższym przykładzie. Oczywiście klasa jest wciąż abstrakcyjna ponieważ należy przeładować i zaimplementować ReleaseHandle. Microsoft dostarcza kilka implementacji:

System.Object
  System.Runtime.ConstrainedExecution.CriticalFinalizerObject
    System.Runtime.InteropServices.SafeHandle
      Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid
        Microsoft.Win32.SafeHandles.SafeFileHandle
        Microsoft.Win32.SafeHandles.SafeMemoryMappedFileHandle
        Microsoft.Win32.SafeHandles.SafeNCryptHandle
        Microsoft.Win32.SafeHandles.SafePipeHandle
        Microsoft.Win32.SafeHandles.SafeRegistryHandle
        Microsoft.Win32.SafeHandles.SafeWaitHandle
        System.Runtime.InteropServices.SafeBuffer
        System.Security.Authentication.ExtendedProtection.ChannelBinding

Jak widać, mamy implementacje takie jak SafeFileHandle, SafeWaitHandle czy SafeRegistryHandle. Zajrzyjmy do implementacji ReleaseHandle klasy SafeFileHandle (można użyć do tego np. .NET Reflector):

[SecurityCritical]
protected override bool ReleaseHandle()
{
    return Win32Native.CloseHandle(base.handle);
}

Jak wygląda SafeWaitHandle?

[SecurityCritical]
protected override bool ReleaseHandle()
{
    if (!this.bIsMutex || Environment.HasShutdownStarted)
    {
        return Win32Native.CloseHandle(base.handle);
    }
    bool flag = false;
    bool bHandleObtained = false;
    try
    {
        if (!this.bIsReservedMutex)
        {
            Mutex.AcquireReservedMutex(ref bHandleObtained);
        }
        flag = Win32Native.CloseHandle(base.handle);
    }
    finally
    {
        if (bHandleObtained)
        {
            Mutex.ReleaseReservedMutex();
        }
    }
    return flag;
}
 

Jak widać, wygląda  dość podobnie… Win32Native.CloseHandle służy do zwolnienia uchwytu.  Microsoft zdecydował się na taki krok ze względu na przejrzystość API i aby uniemożliwić przekazanie SafeFileHandle gdy metoda spodziewa się SafeWaitHandle.

Jeśli powyższe wprowadzenie teoretyczne jest niejasne, to warto przeanalizować praktyczny przykład i wtedy powrócić ponownie do opisanych mechanizmów.  Załóżmy, że chcemy skorzystać z niezarządzanego mutex’a, który znajduje się w kernel32.dll.  Zaglądając do dokumentacji, dowiemy się o sygnaturze metody CreateMutex:

HANDLE WINAPI CreateMutex(
  _In_opt_  LPSECURITY_ATTRIBUTES lpMutexAttributes,
  _In_      BOOL bInitialOwner,
  _In_opt_  LPCTSTR lpName
);

Widzimy, że funkcja zwraca uchwyt do nowego mutex’a.  Jeśli chcemy napisać brzydki kod, wtedy moglibyśmy zaimportować funkcję następująco:

internal class Program
{
   [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "CreateMutex")]
   private static extern IntPtr CreateUglyMutex(IntPtr lpMutexAttributes, bool bInitialOwner, string lpName);

   [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "ReleaseMutex")]
   public static extern bool ReleaseUglyMutex(IntPtr hMutex);

   public static void Main()
   {
       IntPtr mutex = IntPtr.Zero;

       try
       {
           mutex = CreateUglyMutex(IntPtr.Zero, true, "Nazwa");
       }
       finally
       {
           if (mutex != IntPtr.Zero)
               ReleaseUglyMutex(mutex);
       }
   }
}

Powyższa implementacja używa czystego wskaźnika (IntPtr) i z tego względu może okazać się to niebezpieczne. Co w przypadku gdy CreateUglyMutex stworzy obiekt ale nie przepisze go zmiennej mutex? Sytuacja jest jak najbardziej możliwa w przypadku ThreadAbortException. Z tego względu lepiej korzystać z SafeHandle. Jeśli takie niebezpieczeństwa nie są zrozumiałe, zachęcam do przeczytania poprzednich wpisów o CriticalFinalizerObject oraz CER. Dużo lepsze rozwiązanie to:

internal class Program
{
   [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "CreateMutex")]
   private static extern SafeWaitHandle CreateMutex(IntPtr lpMutexAttributes, bool bInitialOwner, string lpName);

   [DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "ReleaseMutex")]
   public static extern bool ReleaseMutex(SafeWaitHandle hMutex);

   public static void Main()
   {
       SafeWaitHandle mutex = null;

       try
       {
           mutex = CreateMutex(IntPtr.Zero, true, "Nazwa");
       }
       finally
       {
           if (mutex !=null&&!mutex.IsInvalid)
               ReleaseMutex(mutex);
       }
   }
}

Kod jest dużo bardziej bezpieczny. SafeHandle implementuje IDisposable oraz destruktory (finalizers). Z tego względu, zawsze zostanie uchwyt zwolniony bo w końcu GC musi w pewnym momencie zebrać każdy niedostępny obiekt.

Ponadto, SafeHandle posiada pewne zabezpieczenie gdy przekazuje uchwyt do niezarządzanej funkcji, o której GC nic w końcu nie wie. Może okazać się , że przekazujemy uchwyt do takiej funkcji a potem jakiś inny wątek chce zwolnić uchwyt. W momencie gdy SafeHandle jest przekazywany do niezarządzanej funkcji, wewnętrzny licznik jest zwiększany, co oznacza, że obiekt jest gdzieś wciąż używany i nie może zostać usunięty z pamięci. Gdy funkcja kończy swoje działanie, licznik jest z powrotem zmniejszany o jeden. Licznik można kontrolować również samemu poprzez metody DangerousAddRef, DangerousRelease oraz DangerousGetHandle. Dostępna jest również klasa CriticalHandle, która nie implementuje przedstawionego mechanizmu z wewnętrznym licznikiem. Skutkuje to większą wydajnością ale mniejszym bezpieczeństwem. Microsoft sugeruje aby w pierwszej kolejności używać SafeHandle z licznikiem.

Leave a Reply

Your email address will not be published.