Empfohlene Vorgehensweisen für die Zuverlässigkeit

Die folgenden Zuverlässigkeitsregeln sind auf SQL Server ausgerichtet, jedoch gelten sie auch für jede hostbasierte Serveranwendung. Es ist äußerst wichtig, dass es bei Servern wie SQL Server zu keinem Ressourcenverlust kommt und dass diese nicht zum Absturz gebracht werden. Dies kann jedoch nicht erreicht werden, indem Zurücksetzungscode für jede Methode geschrieben wird, die den Zustand eines Objekts ändert. Das Ziel ist nicht, 100 Prozent zuverlässigen verwalteten Code zu schreiben, der mit Zurücksetzungscode nach Fehlern an einer beliebigen Stelle wiederhergestellt wird. Das wäre eine schwierige Aufgabe mit wenig Aussicht auf Erfolg. Die Common Language Runtime (CLR) kann keine ausreichend starken Garantien für verwalteten Code bereitstellen, um das Schreiben von perfektem Code möglich zu machen. Beachten Sie, dass SQL Server im Gegensatz zu ASP.NET nur einen Prozess verwendet, der nicht wiederverwendet werden kann, ohne dass eine Datenbank für eine unzumutbar lange Zeit außer Betrieb genommen wird.

Mit diesen schwächeren Garantien und der Ausführung in einem einzelnen Prozess basiert die Zuverlässigkeit auf dem Beenden von Threads oder ggf. dem Wiederverwenden von Anwendungsdomänen sowie dem Ergreifen von Vorsichtsmaßnahmen, um sicherzustellen, dass Betriebssystemressourcen wie Handles oder Arbeitsspeicher nicht verloren gehen. Selbst mit dieser einfacheren Zuverlässigkeitseinschränkung bestehen immer noch erhebliche Zuverlässigkeitsanforderungen:

  • Betriebssystemressourcen dürfen niemals verloren gehen.

  • Alle verwalteten Sperren für die CLR müssen identifiziert werden.

  • Ein anwendungsdomänenübergreifender freigegebener Zustand darf niemals unterbrochen werden, damit das Wiederverwenden von AppDomain reibungslos funktioniert.

Obwohl es theoretisch möglich ist, verwalteten Code zu schreiben, um ThreadAbortException-, StackOverflowException- und OutOfMemoryException-Ausnahmen zu behandeln, kann von Entwicklern nicht erwartet werden, derart stabilen Code für die gesamte Anwendung schreiben. Aus diesem Grund führen Out-of-band-Ausnahmen dazu, dass der ausführende Thread beendet wird. Wenn der beendete Thread einen Freigabezustand geändert hat, was dadurch ermittelt werden kann, ob im Thread eine Sperre enthalten ist, wird AppDomain nicht geladen. Wenn eine Methode, die einen freigegebenen Zustand ändert, beendet wird, ist der Zustand inkonsistent, da es nicht möglich ist, verlässlichen Zurücksetzungscode zu schreiben, um den Freigabezustand zu aktualisieren.

In der Version 2.0 von .NET Framework ist SQL Server der einzige Host, der Zuverlässigkeit erfordert. Wenn Ihre Assembly auf SQL Server ausgeführt wird, sollten Sie Zuverlässigkeit für jeden Teil dieser Assembly gewährleisten, selbst wenn bestimmte Funktionen vorhanden sind, die bei der Ausführung in der Datenbank deaktiviert werden. Dies ist erforderlich, da die Codeanalyse-Engine Code auf Assemblyebene untersucht und nicht von deaktiviertem Code unterscheiden kann. Eine andere Überlegung bei der SQL Server-Programmierung ist, dass SQL Server alles in einem Prozess ausführt. Außerdem wird die AppDomain-Wiederverwendung zur Bereinigung aller Ressourcen wie Arbeitsspeicher und Betriebssystemhandles verwendet.

Sie können sich nicht auf Finalizer, Destruktoren oder try/finally-Blöcke für Zurücksetzungscode verlassen. Diese können unterbrochen oder nicht aufgerufen werden.

Asynchrone Ausnahmen wie ThreadAbortException, StackOverflowException und OutOfMemoryException können an unerwarteten Stellen und prinzipiell in jeder Computeranweisung auftreten.

Bei verwalteten Threads handelt es sich nicht notwendigerweise um Win32-Threads in SQL. Es kann sich auch um Fibers handeln.

Ein prozess- oder anwendungsdomänenübergreifender veränderlicher Freigabezustand lässt sich nur mit großem Aufwand sicher verändern und sollte wenn möglich vermieden werden.

Probleme aufgrund von nicht ausreichendem Arbeitsspeicher sind in SQL Server nicht selten.

Wenn in SQL Server gehostete Bibliotheken ihren Freigabezustand nicht ordnungsgemäß aktualisieren, ist die Wahrscheinlichkeit hoch, dass der Code nicht wiederhergestellt wird, bis die Datenbank neu gestartet wird. Zudem ist es in einigen Extremfällen möglich, dass dadurch beim SQL Server-Prozess ein Fehler auftritt, wodurch die Datenbank neu gestartet wird. Ein Neustart der Datenbank kann zum Absturz einer Website führen oder sich auf Unternehmensvorgänge auswirken, wobei die Verfügbarkeit beeinträchtigt wird. Ein langsamer Verlust von Betriebssystemressourcen wie Arbeitsspeicher oder Handles kann dazu führen, dass beim Server ein Fehler auftritt, wobei Handles ohne Möglichkeit zur Wiederherstellung zugewiesen werden. Möglicherweise kann auch die Leistung des Servers langsam abnehmen, wobei die Verfügbarkeit der Anwendung eines Kunden reduziert wird. Natürlich sollen diese Szenarios vermieden werden.

Regeln für empfohlene Vorgehensweisen

In der Einführung wurde der Fokus darauf gelegt, auf welche Fehler im Code Review für den verwalteten Code, der auf dem Server ausgeführt wird, geprüft werden muss, um die Stabilität und Zuverlässigkeit des Frameworks zu verbessern. All diese Überprüfungen werden allgemein empfohlen und sind auf einem Server ein absolutes Muss.

Bei einem Deadlock oder einer Ressourceneinschränkung bricht SQL Server einen Thread ab oder entfernt eine AppDomain. In diesem Fall wird nur Zurücksetzungscode in einem eingeschränkten Ausführungsbereich (CER) garantiert ausgeführt.

Verwenden Sie SafeHandle, um Ressourcenverluste zu vermeiden

Falls AppDomain entladen wird, können Sie sich nicht auf die Ausführung von finally-Blöcken oder Finalizern verlassen. Daher ist es wichtig, den Zugriff auf alle Betriebssystemressourcen über die SafeHandle-Klasse anstatt über IntPtr, HandleRef oder ähnliche Klassen zu abstrahieren. Dies ermöglicht der CLR, die von Ihnen verwendeten Handles selbst im Fall des Entfernens von AppDomain nachzuverfolgen und zu schließen. SafeHandle verwendet ein Objekt der Klasse CriticalFinalizerObject als Finalizer, der von der CLR immer ausgeführt wird.

Das Betriebssystemhandle wird vom Moment seiner Erstellung in einem SafeHandle bis zu dem Moment gespeichert, in dem es freigegeben wird. Es gibt kein Zeitfenster, in dem eine ThreadAbortException den Verlust eines Handles verursachen kann. Darüber hinaus führt der Plattformaufruf eine Verweiszählung des Handles aus, was eine genaue Nachverfolgung der Lebenszeit des Handles ermöglicht. Hierdurch wird ein Sicherheitsproblem mit einer Racebedingung zwischen Dispose und einer Methode verhindert, die das Handle momentan verwendet.

Die meisten Klassen, die derzeit über einen Finalizer zum einfachen Bereinigen eines Betriebssystemhandles verfügen, werden den Finalizer nicht mehr benötigen. Stattdessen wird sich der Finalizer in der abgeleiteten SafeHandle-Klasse befinden.

Beachten Sie, dass SafeHandle kein Ersatz für IDisposable.Dispose ist. Es bestehen immer noch potenzielle Vorteile im Hinblick auf Ressourcenkonflikte und Leistung gegenüber dem expliziten Löschen von Betriebssystemressourcen. Beachten Sie aber, dass finally-Blöcke, die Ressourcen explizit löschen, möglicherweise nicht vollständig ausgeführt werden.

SafeHandle ermöglicht es Ihnen, Ihre eigene ReleaseHandle-Methode zu implementieren, die Anweisungen zum Freigeben des Handles ausführt. Beispiele für derartige Anweisungen sind das Übergeben des Zustands an eine Routine zur Freigabe von Betriebssystemhandles oder das Freigeben von mehreren Handles in einer Schleife. Die CLR garantiert, dass diese Methode ausgeführt wird. Es liegt in der Verantwortung des Erstellers der ReleaseHandle-Implementierung, sicherzustellen, dass das Handle in allen Fällen freigegeben wird. Wird dies nicht sichergestellt, geht das Handle verloren, was oft dazu führt, dass native Ressourcen verloren gehen, die dem Handle zugewiesen sind. Daher ist es wichtig, abgeleitete SafeHandle-Klassen so zu strukturieren, dass die ReleaseHandle-Implementierung nicht die Zuordnung von Ressourcen erfordert, die zum Zeitpunkt des Aufrufs möglicherweise nicht verfügbar sind. Beachten Sie, dass es zulässig ist, Methoden aufzurufen, die möglicherweise innerhalb der Implementierung von ReleaseHandle fehlschlagen, vorausgesetzt, Ihr Code kann mit solchen Fehlern umgehen und die Vereinbarung zum Freigeben des nativen Handles erfüllen. Für das Debuggen verfügt ReleaseHandle über einen Rückgabewert vom Typ Boolean, der auf false gesetzt werden kann, wenn ein schwerwiegender Fehler auftritt, der die Freigabe der Ressource verhindert. Auf diese Weise wird der MDA releaseHandleFailed aktiviert, um im aktivierten Zustand bei der Identifizierung des Problems zu helfen. Er wirkt sich auf keine andere Weise auf die Laufzeit aus. ReleaseHandle wird nicht erneut für die dieselbe Ressource aufgerufen, und daher geht das Handle verloren.

SafeHandle ist in bestimmten Kontexten nicht geeignet. Da die ReleaseHandle-Methode in einem GC-Finalizerthread ausgeführt werden kann, sollten alle Handles, die in einem bestimmten Thread freigegeben werden müssen, nicht in eine SafeHandle-Klasse eingebunden sein.

Runtime Callable Wrapper (RCWs) können durch die CLR ohne zusätzlichen Code bereinigt werden. Bei Code, der den Plattformaufruf verwendet und ein COM-Objekt als IUnknown* oder IntPtr behandelt, muss der Code so umgeschrieben werden, dass er einen RCW verwendet. SafeHandle ist für dieses Szenario möglicherweise nicht geeignet, da die Möglichkeit besteht, dass eine nicht verwaltete Freigabemethode einen Rückruf für den verwalteten Code ausführt.

Regel für die Codeanalyse

Verwenden Sie SafeHandle, um Betriebssystemressourcen zu kapseln. Verwenden Sie nicht HandleRef oder Felder vom Typ IntPtr.

Achten Sie darauf, dass keine Finalizer ausgeführt werden müssen, damit der Verlust von Betriebssystemressourcen verhindert wird

Überprüfen Sie Ihre Finalizer sorgfältig, um sicherzustellen, dass eine wichtige Betriebssystemressource auch dann nicht verloren geht, wenn sie nicht ausgeführt werden. Im Gegensatz zum normalen Entladen von AppDomain werden Objekte während des unvorhergesehen Entladens von AppDomain nicht finalisiert, wenn die Anwendung in einem stabilen Zustand ausgeführt wird oder wenn ein Server wie SQL Server heruntergefahren wird. Stellen Sie sicher, dass Ressourcen im Fall eines plötzlichen Entladens nicht verloren gehen, da die korrekte Ausführung der Anwendung nicht garantiert werden kann, die Integrität des Servers jedoch erhalten werden muss, indem Ressourcen nicht verloren gehen. Verwenden Sie SafeHandle, um Betriebssystemressourcen freizugeben.

Achten Sie darauf, dass keine finally-Klauseln ausgeführt werden müssen, damit der Verlust von Betriebssystemressourcen verhindert wird

Für finally-Klauseln besteht keine Garantie, dass diese außerhalb von CERs ausgeführt werden, weshalb Bibliotheksentwickler sich nicht auf Code innerhalb eines finally-Blocks verlassen dürfen, um nicht verwaltete Ressourcen freizugeben. Die empfohlene Lösung ist die Verwendung von SafeHandle.

Regel für die Codeanalyse

Verwenden Sie SafeHandle anstatt Finalize zum Bereinigen von Betriebssystemressourcen. Verwenden Sie nicht IntPtr, sondern SafeHandle zum Kapseln von Ressourcen. Wenn die finally-Klausel ausgeführt werden muss, platzieren Sie diese in einer CER.

Alle Sperren sollten vorhandenen verwalteten Sperrcode durchlaufen

Der CLR muss bekannt sein, wann der Code gesperrt ist. So kann sichergestellt werden, dass die CLR die AppDomain löscht, anstatt einfach den Thread abzubrechen. Das Abbrechen des Threads könnte gefährlich sein, da die durch den Thread verarbeiteten Daten in einem inkonsistenten Zustand verbleiben könnten. Daher muss die gesamte AppDomain-Klasse wiederverwendet werden. Die Konsequenzen eines Fehlers beim Identifizieren einer Sperre können Deadlocks oder falsche Ergebnisse sein. Verwenden Sie die Methoden BeginCriticalRegion und EndCriticalRegion, um Sperrbereiche zu identifizieren. Erstere sind statisch, befinden sich in der Thread-Klasse und können nur auf den aktuellen Thread angewendet werden. Diese Methoden helfen dabei, zu verhindern, dass ein Thread die Sperrenanzahl eines anderen Threads bearbeitet.

In Enter und Exit ist diese CLR-Benachrichtigung integriert. Daher wird deren Verwendung und die Verwendung der lock-Anweisung empfohlen, die diese Methoden verwendet.

Andere Sperrmechanismen wie Spinlocks und AutoResetEvent müssen diese Methoden aufrufen, um die CLR darüber zu benachrichtigen, dass auf einen kritischen Bereich zugegriffen wird. Diese Methoden verwenden keine Sperren. Sie informieren die CLR darüber, dass Code in einem kritischen Bereich ausgeführt wird und dass der Abbruch des Threads zu einem inkonsistenten Freigabezustand führen könnte. Wenn Sie einen eigenen Sperrentyp wie z.B. eine benutzerdefinierte ReaderWriterLock-Klasse definiert haben, verwenden Sie diese Methoden zur Berechnung der Sperrenanzahl.

Regel für die Codeanalyse

Markieren und identifizieren Sie alle Sperren mithilfe von BeginCriticalRegion und EndCriticalRegion. Verwenden Sie nicht CompareExchange, Increment und Decrement in einer Schleife. Führen Sie keinen Plattformaufruf der Win32-Varianten dieser Methoden aus. Verwenden Sie Sleep nicht in einer Schleife. Verwenden Sie keine flüchtigen Felder (volatile-Felder).

Bereinigungscode muss sich in einem finally- oder catch-Block befinden und darf nicht auf einen catch-Block folgen

Bereinigungscode darf nie auf einen catch-Block folgen. Er sollte sich in einem finally-Block oder im catch-Block selbst befinden. Dies entspricht der üblichen Vorgehensweise. Ein finally-Block wird allgemein bevorzugt, da er den gleichen Code ausführt, wenn eine Ausnahme ausgelöst wird und wenn der Code am Ende des try-Blocks normal aufgerufen wird. Wenn eine unerwartete Ausnahme ausgelöst wird, z.B. eine ThreadAbortException, wird der Bereinigungscode nicht ausgeführt. Nicht verwaltete Ressourcen, die Sie in einem finally-Block bereinigen würden, sollten idealerweise in einem Objekt der SafeHandle-Klasse gekapselt sein, um Verluste zu vermeiden. Beachten Sie, dass das C#-Schlüsselwort using effektiv verwendet werden kann, um Objekte einschließlich Handles zu löschen.

Obwohl die Wiederverwendung von AppDomain Ressourcen im Finalizerthread bereinigen kann, ist es dennoch wichtig, Bereinigungscode an der richtigen Stelle zu platzieren. Wenn ein Thread eine asynchrone Ausnahme empfängt, ohne eine Sperre zu verwenden, sollten Sie beachten, dass die CLR versucht, den Thread selbst zu beenden, ohne AppDomain wiederherstellen zu müssen. Indem sichergestellt wird, dass Ressourcen besser früh als spät bereinigt werden, können mehr Ressourcen verfügbar gemacht und die Lebensdauer besser verwaltet werden. Wenn Sie einen Handle nicht explizit für eine Datei in einem Fehlercodepfad schließen, warten Sie darauf, dass der SafeHandle-Finalizer ihn bereinigt. Wenn Ihr Code das nächste Mal ausgeführt wird, tritt möglicherweise ein Fehler beim dem Versuch auf, auf dieselbe Datei zuzugreifen, falls der Finalizer nicht bereits ausgeführt wurde. Aus diesem Grund hilft das Sicherstellen, dass Bereinigungscode vorhanden ist und korrekt funktioniert, dabei, dass die Wiederherstellung nach Fehlern besser und schneller erfolgt, obwohl es nicht unbedingt erforderlich ist.

Regel für die Codeanalyse

Bereinigungscode nach einem catch-Block muss sich in einem finally-Block befinden. Platzieren Sie Aufrufe zum Löschen in einem finally-Block. catch-Blöcke sollten abschließend Ausnahmen auslösen bzw. neu auslösen. Es werden Ausnahmen auftreten, z.B. Code, der ermittelt, ob eine Netzwerkverbindung eingerichtet werden kann, wobei Sie eine große Anzahl von Ausnahmen erhalten könnten. Dennoch sollte jeder Code, der das Abfangen einer Reihe von Ausnahmen unter normalen Bedingungen erfordert, darauf hinweisen, dass der Code getestet werden sollte, um herauszufinden, ob er erfolgreich sein wird.

Ein prozessweiter veränderbarer Freigabezustand zwischen Anwendungsdomänen sollte beseitigt werden, oder es sollte ein eingeschränkter Ausführungsbereich verwendet werden

Wie in der Einleitung beschrieben, kann es sehr schwierig sein, verwalteten Code zu schreiben, der den prozessübergreifenden veränderbaren Freigabezustand zwischen Anwendungsdomänen auf verlässliche Weise überwacht. Ein prozessübergreifender veränderbarer Freigabezustand ist jede Art von Datenstruktur zwischen Anwendungsdomänen, entweder in Win32-Code, innerhalb der CLR oder in verwaltetem Code, der Remoting verwendet. Ein veränderbarer Freigabezustand ist sehr schwer in verwaltetem Code zu schreiben, und jeder statische Freigabezustand sollte nur mit großer Vorsicht erfolgen. Wenn es in Ihrem Fall zu einem prozessübergreifenden oder computerübergreifenden Freigabezustand kommt, suchen Sie nach einer Möglichkeit, diesen zu beseitigen, oder schützen Sie den Freigabezustand mithilfe eines eingeschränkten Ausführungsbereichs. Beachten Sie, dass jede Bibliothek mit Freigabezustand, der nicht identifiziert und korrigiert ist, dazu führen könnte, dass ein Host wie SQL Server abstürzt, der ein korrektes Entladen von AppDomain erfordert.

Wenn in Code ein COM-Objekt verwendet wird, vermeiden Sie, dieses COM-Objekt zwischen Anwendungsdomänen freizugeben.

Sperren funktionieren nicht prozessweit oder zwischen Anwendungsdomänen

In der Vergangenheit wurden Enter und die lock-Anweisung dazu verwendet, Sperren für den globalen Prozess zu erstellen. Dies ist beispielsweise beim Sperren von agilen AppDomain-Klassen wie Type-Instanzen von nicht freigegebenen Assemblys, Thread-Objekten, internalisierten Zeichenfolgen und einigen Zeichenfolgen der Fall, die mithilfe von Remoting zwischen Anwendungsdomänen freigegeben sind. Diese Sperren sind nicht mehr prozessübergreifend. Überprüfen Sie, ob der Code innerhalb der Sperre eine externe, persistente Ressource wie eine Datei auf einem Datenträger oder möglicherweise eine Datenbank verwendet, um eine prozessübergreifende Sperre zwischen Anwendungsdomänen zu identifizieren.

Beachten Sie, dass die Verwendung einer Sperre innerhalb einer AppDomain-Klasse Probleme verursachen kann, falls der geschützte Code eine externe Ressource verwendet, da dieser Code gleichzeitig in mehreren Anwendungsdomänen ausgeführt werden könnte. Dies kann zum Problem werden, wenn in eine Protokolldatei geschrieben oder für den gesamten Prozess eine Bindung an einen Socket erfolgt. Diese Änderungen bedeuten, dass bis auf die Verwendung einer benannten Mutex- oder Semaphore-Instanz keine einfache Möglichkeit besteht, verwalteten Code zu verwenden, um eine prozessglobale Sperre abzurufen. Erstellen Sie Code, der nicht gleichzeitig in zwei Anwendungsdomänen ausgeführt wird, oder verwenden Sie die Mutex- oder Semaphore-Klassen. Wenn vorhandener Code nicht geändert werden kann, verwenden Sie nicht ein benanntes Win32-Mutexobjekt für diese Synchronisierung, da das Ausführen im Fibermodus bedeutet, dass Sie nicht garantieren können, dass der gleiche Betriebssystemthread ein Mutex abrufen und freigeben wird. Sie müssen die verwaltete Mutex-Klasse oder eine benannte ManualResetEvent-, AutoResetEvent- oder Semaphore-Instanz verwenden, um die Codesperre so zu synchronisieren, dass die CLR die Sperre beachtet, anstatt diese mithilfe von nicht verwaltetem Code zu synchronisieren.

Vermeiden Sie lock(typeof(MyType))

Private und öffentliche Type-Objekte in freigegebenen Assemblys, bei denen nur eine Kopie des Codes zwischen allen Anwendungsdomänen freigegeben wird, stellen ebenfalls Probleme dar. Für freigegebene Assemblys ist nur eine Instanz einer Type-Klasse pro Prozess vorhanden. Dies bedeutet, dass mehrere Anwendungsdomänen genau dieselbe Type-Instanz freigeben. Durch eine Sperre für eine Type-Instanz wird nicht nur die AppDomain, sondern der gesamte Prozess beeinflusst. Wenn eine AppDomain ein Type-Objekt sperrt, wird der Thread unvermittelt unterbrochen, und die Sperre wird nicht aufgehoben. Diese Sperre kann dann bei anderen Anwendungsdomänen zu einem Deadlock führen.

Eine gute Möglichkeit zur Verwendung von Sperren in statischen Methoden besteht darin, ein statisches internes Synchronisierungsobjekt zum Code hinzuzufügen. Dieses kann im Klassenkonstruktor initialisiert werden, wenn ein solcher vorhanden ist. Andernfalls kann das Objekt wie folgt initialisiert werden:

private static Object s_InternalSyncObject;
private static Object InternalSyncObject
{
    get
    {
        if (s_InternalSyncObject == null)
        {
            Object o = new Object();
            Interlocked.CompareExchange(
                ref s_InternalSyncObject, o, null);
        }
        return s_InternalSyncObject;
    }
}

Wenn anschließend eine Sperre verwendet wird, verwenden Sie die InternalSyncObject-Eigenschaft, um ein Objekt zu erhalten, auf das die Sperre angewendet werden kann. Sie müssen die Eigenschaft nicht verwenden, wenn Sie das interne Synchronisierungsobjekt im Klassenkonstruktor initialisiert haben. Der zur Sperrung verwendete Initialisierungscode, der zwei Kontrollstrukturen zur Prüfung enthält, sollte wie im folgenden Beispiel aussehen:

public static MyClass SingletonProperty
{
    get
    {
        if (s_SingletonProperty == null)
        {
            lock(InternalSyncObject)
            {
                // Do not use lock(typeof(MyClass))
                if (s_SingletonProperty == null)
                {
                    MyClass tmp = new MyClass(…);
                    // Do all initialization before publishing
                    s_SingletonProperty = tmp;
                }
            }
        }
        return s_SingletonProperty;
    }
}

Hinweis zu lock(this)

Es ist im Allgemeinen zulässig, ein einzelnes Objekt zu sperren, auf das öffentlich zugegriffen werden kann. Wenn das Objekt jedoch ein Singleton-Objekt ist, das bei einem gesamten Subsystem zu einem Deadlock führen kann, empfiehlt sich ebenfalls das obige Entwurfsmuster. Das Sperren des SecurityManager-Objekts kann z.B. zu einem Deadlock in der AppDomain führen, wodurch die gesamte AppDomain unbrauchbar wird. Es wird daher empfohlen, ein als öffentlich deklariertes Objekt dieses Typs nicht zu sperren. Eine Sperre einer einzelnen Auflistung oder eines einzelnen Arrays sollte jedoch in der Regel kein Problem darstellen.

Regel für die Codeanalyse

Sperren Sie keine Typen, die möglicherweise über Anwendungsdomänen hinweg verwendet werden oder über eine starke Objektidentität verfügen. Rufen Sie nicht Enter für Objekte der Klassen Type, MethodInfo, PropertyInfo, String, ValueType, Thread oder für Objekte auf, die von MarshalByRefObject abgeleitet werden.

Entfernen Sie GC.KeepAlive-Aufrufe

In vorhandenem Code wird in vielen Fällen die KeepAlive-Methode entweder nicht verwendet, obwohl dies erforderlich wäre, oder sie wird falsch angewendet. Nach der Konvertierung in SafeHandle müssen Klassen nicht KeepAlive aufrufen. Die Voraussetzung dafür ist, dass sie auf SafeHandle anstelle eines Finalizers zurückgreifen, um die Betriebssystemhandles zu finalisieren. Der Leistungsabfall, der durch das anhaltende Aufrufen von KeepAlive zustande kommt, ist unerheblich. Durch den Eindruck, dass ein Aufruf von KeepAlive erforderlich oder ausreichend ist, um ein Problem mit der Lebensdauer zu lösen, das möglicherweise nicht mehr vorhanden ist, wird die Verwaltung des Codes jedoch erschwert. Beim Verwenden der Runtime Callable Wrapper (RCWs) von COM-Interop muss KeepAlive allerdings dennoch im Code vorhanden sein.

Regel für die Codeanalyse

Entfernen Sie KeepAlive.

Verwenden Sie das HostProtection-Attribut

Das HostProtectionAttribute (HPA) stellt deklarative Sicherheitsaktionen zur Festlegung von Hostschutzanforderungen zur Verfügung. Auf diese Weise kann sogar vom Host verhindert werden, dass in voll vertrauenswürdigem Code Methoden aufgerufen werden, die für einen bestimmten Host wie Exit oder Show für SQL Server ungeeignet sind.

Das HPA wirkt sich nur auf nicht verwaltete Anwendungen aus, die die Common Language Runtime hosten und den Hostschutz implementieren. Ein Beispiel ist SQL Server. Die Anwendung der Sicherheitsaktion führt basierend auf den Hostressourcen, die von der Klasse oder Methode verfügbar gemacht werden, zur Erstellung eines Linkaufrufs. Wenn der Code in einer Clientanwendung oder auf einem Server ausgeführt wird, der nicht Teil eines geschützten Hostbereichs ist, „verschwindet“ das Attribut. Es wird also nicht erkannt und daher nicht angewendet.

Wichtig

Der Zweck dieses Attributs besteht darin, hostspezifische Richtlinien für Programmiermodelle durchzusetzen. Ein erzwungenes Sicherheitsverhalten ist jedoch nicht das Ziel. Obwohl ein Linkaufruf verwendet wird, um auf Übereinstimmung mit den Programmiermodellanforderungen zu prüfen, ist das HostProtectionAttribute keine Sicherheitsberechtigung.

Wenn für den Host keine Programmiermodellanforderungen vorliegen, kommt es nicht zu einem Linkaufruf.

Das Attribut identifiziert Folgendes:

  • Methoden oder Klassen, die nicht mit dem Programmiermodell des Hosts kompatibel sind, jedoch abgesehen davon keine negativen Auswirkungen haben.

  • Methoden oder Klassen, die nicht mit dem Programmiermodell des Hosts kompatibel sind und zur Destabilisierung von serververwaltetem Benutzercode führen können.

  • Methoden oder Klassen, die nicht mit dem Programmiermodell des Hosts kompatibel sind und zur Destabilisierung des Serverprozesses selbst führen können.

Hinweis

Wenn Sie eine Klassenbibliothek erstellen, die von Anwendungen aufgerufen werden soll, die in einer geschützten Hostumgebung ausgeführt werden können, sollten Sie dieses Attribut auf Member anwenden, die HostProtectionResource-Kategorien verfügbar machen. Die Verwendung von Membern der .NET Framework-Klassenbibliothek mit diesem Attribut führt nur dazu, dass die unmittelbar aufrufende Methode überprüft wird. Auch der Bibliotheksmember muss auf gleiche Weise seine aufrufende Methode überprüfen.

Weitere Informationen zu HPA finden Sie unter HostProtectionAttribute.

Regel für die Codeanalyse

Alle Methoden, in denen Synchronisierung oder Threading zur Anwendung kommt, müssen für SQL Server mit dem HPA identifiziert werden. Dies gilt auch für Methoden, die Zustände freigeben, synchronisiert werden oder externe Prozesse verwalten. Zu den HostProtectionResource-Werten, die sich auf SQL Server auswirken, gehören SharedState, Synchronization und ExternalProcessMgmt. Von einem HPA sollten nicht nur Methoden identifiziert werden, die Ressourcen verwenden, die sich auf SQL auswirken. Vielmehr sollten alle Methoden, die eine HostProtectionResource verfügbar machen, von diesem Attribut identifiziert werden.

Verwenden Sie Blockierungen in nicht verwaltetem Code nicht unbegrenzt lange

Blockierungen in nicht verwaltetem Code anstatt in verwaltetem Code können zu einem Denial-of-Service-Angriff führen, da die CLR nicht in der Lage ist, den Thread abzubrechen. Durch einen blockierten Thread wird – zumindest bei Verzicht auf unsichere Vorgänge – die CLR daran gehindert, die AppDomain zu entladen. Die Blockierung mittels Windows-Synchronisierungsgrundtyp ist ein klares Beispiel für einen Vorgang, der verhindert werden muss. Eine Blockierung in einem Aufruf von ReadFile auf einem Socket sollte nach Möglichkeit vermieden werden. Im Idealfall sollte die Windows-API einen Mechanismus für einen solchen Vorgang zur Verfügung stellen, sodass ein Timeout ausgelöst werden kann.

Jede Methode, die in nativem Code aufgerufen wird, sollte idealerweise einen Win32-Aufruf mit einem angemessenen, zeitlich begrenzten Timeout verwenden. Wenn der Benutzer ein Timeout festlegen darf, sollte dieses nur dann zeitlich unbegrenzt sein dürfen, falls der Benutzer über bestimmte Sicherheitsberechtigungen verfügt. Wenn eine Methode länger als ca. 10 Sekunden blockiert wird, benötigen Sie entweder eine Version, die Timeouts unterstützt, oder zusätzliche CLR-Unterstützung.

Nachstehend sind einige Beispiele für problematische APIs aufgeführt. Sowohl anonyme als auch benannte Pipes können ohne Timeout erstellt werden. Im Code muss allerdings sichergestellt werden, dass nie CreateNamedPipe oder WaitNamedPipe mit NMPWAIT_WAIT_FOREVER aufgerufen werden. Darüber hinaus kommt es möglicherweise auch dann zu unerwarteten Blockierungen, wenn ein Timeout angegeben wird. Der Aufruf von WriteFile in einer anonymen Pipe wird blockiert, bis der Schreibvorgang für alle Bytes beendet ist. Wenn sich im Puffer ungelesene Daten befinden, wird der WriteFile-Aufruf blockiert, bis der Reader Speicherplatz im Puffer der Pipe freigegeben hat. Für Sockets sollte immer eine API verwendet werden, die einen Timeoutmechanismus unterstützt.

Regel für die Codeanalyse

Eine Blockierung ohne Timeout ist in nicht verwaltetem Code ein Denial-of-Service-Angriff. Führen Sie keine Plattformaufrufe von WaitForSingleObject, WaitForSingleObjectEx, WaitForMultipleObjects, MsgWaitForMultipleObjects und MsgWaitForMultipleObjectsEx aus. Verwenden Sie nicht NMPWAIT_WAIT_FOREVER.

Identifizieren Sie STA-abhängige Features

Identifizieren Sie Code, der COM-Single-Threaded-Apartments (STAs) verwendet. STAs sind für den SQL Server-Prozess deaktiviert. Von CoInitialize abhänge Funktionen wie z.B. Leistungsindikatoren oder die Zwischenablage müssen in SQL Server deaktiviert werden.

Achten Sie darauf, dass bei Finalizern keine Synchronisierungsprobleme auftreten

Es besteht die Möglichkeit, dass in zukünftigen Versionen von .NET Framework mehrere Finalizer-Threads vorhanden sind. Das bedeutet, dass Finalizer für verschiedene Instanzen desselben Typs gleichzeitig ausgeführt werden. Finalizer müssen nicht völlig threadsicher sein, da der Garbage Collector garantiert, dass ein Finalizer für eine vorhandene Objektinstanz in nur einem Thread ausgeführt wird. Finalizer müssen jedoch so programmiert werden, dass Racebedingungen und Deadlocks vermieden werden, wenn Finalizer gleichzeitig für unterschiedliche Objektinstanzen ausgeführt werden. Wenn beispielsweise beim Schreiben in eine Protokolldatei ein externer Zustand in einem Finalizer verwendet wird, müssen Threadingprobleme behandelt werden. Verlassen Sie sich bei der Gewährleistung von Threadsicherheit nicht auf die Finalisierung. Verwenden Sie außerdem keinen verwalteten oder nativen threadlokalen Speicher zum Speichern eines Zustands im Finalizer-Thread.

Regel für die Codeanalyse

In Finalizern dürfen keine Synchronisierungsprobleme auftreten. Verwenden Sie keinen statischen veränderlichen Zustand in einem Finalizer.

Vermeiden Sie nach Möglichkeit nicht verwalteten Speicher

Nicht verwalteter Speicher kann ebenso wie ein Betriebssystemhandle verloren gehen. Verwenden Sie daher falls möglich mit stackalloc Arbeitsspeicher im Stapel, ein fixiertes verwaltetes Objekt wie die fixed-Anweisung oder ein GCHandle unter Verwendung von „byte[]“. Der GC gibt diese Ressourcen abschließend frei. Wenn Sie jedoch nicht verwalteten Speicher zuordnen müssen, sollten Sie eine Klasse verwenden, die von SafeHandle abgeleitet wird, um den Code für die Speicherbelegung zu umschließen.

Beachten Sie, dass SafeHandle in mindestens einem Fall nicht verwendet werden sollte. Bei COM-Methodenaufrufen, die Speicher belegen oder freigeben, ist es üblich, dass eine DLL zunächst Speicher über CoTaskMemAlloc belegt und eine andere DLL anschließend diesen Speicher mit CoTaskMemFree freigibt. Die Verwendung eines SafeHandle-Objekts wäre hier nicht sinnvoll, da dieses versuchen würde, die Lebensdauer des nicht verwalteten Speichers mit der Lebensdauer von SafeHandle zu verknüpfen, anstatt zuzulassen, dass die andere DLL die Lebensdauer des Arbeitsspeichers verwaltet.

Überprüfen Sie alle Vorkommnisse von catch(Exception)

In catch-Blöcken zum Abfangen aller Ausnahmen anstatt einer bestimmten Ausnahme werden nun auch asynchrone Ausnahmen abgefangen. Überprüfen Sie jeden catch(Exception)-Block, und achten Sie darauf, dass keine wichtigen Ressourcen freigeben werden oder Zurücksetzungscode übersprungen wird. Suchen Sie außerdem nach potenziell fehlerhaftem Verhalten innerhalb des catch-Blocks selbst für die Behandlung der Ausnahmen ThreadAbortException, StackOverflowException oder OutOfMemoryException. Beachten Sie, dass im Code möglicherweise Annahmen protokolliert oder getroffen werden, dass bestimmte Ausnahmen sichtbar sind oder eine Ausnahme aus genau einem Grund aufgetreten ist. Diese Annahmen müssen möglicherweise aktualisiert werden, um ThreadAbortException einzuschließen.

Sie sollten alle Stellen, an denen Ausnahmen jeden Typs abgefangen werden, so ändern, dass ein erwarteter Ausnahmetyp abgefangen wird. Ein Beispiel ist eine FormatException, die bei einer Methode zur Formatierung von Zeichenfolgen auftritt. Auf diese Weise wird verhindert, dass der catch-Block bei unerwarteten Ausnahmen ausgeführt wird. Außerdem wird sichergestellt, dass Fehler nicht durch das Abfangen unerwarteter Ausnahmen maskiert werden. Allgemein gilt, dass Sie nie eine Ausnahme in Bibliothekscode behandeln dürfen. Code, in dem Sie eine Ausnahme abfangen müssen, kann auf einen Entwurfsfehler im abgerufenen Code hindeuten. In einigen Fällen empfiehlt es sich, eine Ausnahme abzufangen und einen anderen Ausnahmetyp auszulösen, um mehr Daten bereitzustellen. Verwenden Sie in diesem Fall geschachtelte Ausnahmen, da so die tatsächliche Ursache des Fehlers in der InnerException-Eigenschaft der neuen Ausnahme gespeichert wird.

Regel für die Codeanalyse

Prüfen Sie in verwaltetem Code alle catch-Blöcke, die alle Objekte oder Ausnahmen abfangen. In C# müssen sowohl catch{}- als auch catch(Exception){}-Blöcke gekennzeichnet werden. Sie sollten entweder einen ganz bestimmten Ausnahmetyp verwenden oder den Code prüfen, um sicherzustellen, dass es beim Abfangen unerwarteter Ausnahmetypen nicht zu Fehlern kommt.

Gehen Sie nicht davon aus, dass ein verwalteter Thread ein Win32-Thread ist: Ein verwalteter Thread ist eine Fiber

Die Verwendung von verwaltetem threadlokalen Speicher ist möglich. Sie können jedoch keinen nicht verwalteten threadlokalen Speicher verwenden oder davon ausgehen, dass der Code erneut im aktuellen Betriebssystemthread ausgeführt wird. Ändern Sie keine Einstellungen wie das Threadgebietsschema. Verwenden Sie zum Abruf der Methoden InitializeCriticalSection oder CreateMutex keinen Plattformaufruf, da diese darauf angewiesen sind, dass der gesperrte Betriebssystemthread wieder entsperrt wird. Da dies bei der Verwendung von Fibern nicht der Fall ist, können kritische Abschnitte in Win32 und Mutex-Verfahren nicht direkt in SQL verwendet werden. Beachten Sie, dass die verwaltete Mutex-Klasse keine Probleme hinsichtlich der Threadaffinität behandelt.

Sie können den Zustand eines verwalteten Thread-Objekts größtenteils sicher nutzen und dabei beispielsweise verwalteten threadlokalen Speicher und die aktuelle Benutzeroberflächenkultur des Threads verwenden. Sie können auch das ThreadStaticAttribute verwenden, wodurch nur der aktuell verwaltete Thread auf den Wert einer vorhandenen statischen Variable zugreifen kann. Dies ist eine alternative Methode zur Verwendung threadlokalen Speichers in einer Fiber in der CLR. Aus Gründen, die mit dem Programmiermodell zusammenhängen, können Sie die aktuelle Kultur eines Threads nicht ändern, wenn dieser in SQL ausgeführt wird.

Regel für die Codeanalyse

SQL Server wird im Fibermodus ausgeführt. Verwenden Sie keinen threadlokalen Speicher. Führen Sie außerdem keine Plattformaufrufe von TlsAlloc, TlsFree, TlsGetValue und TlsSetValue. aus.

Überlassen Sie den Identitätswechsel SQL Server

Da der Identitätswechsel auf der Threadebene ausgeführt wird und SQL im Fibermodus ausgeführt werden kann, sollte es in verwaltetem Code zu keinem Identitätswechsel von Benutzern und zu keinem Aufruf von RevertToSelf kommen.

Regel für die Codeanalyse

Überlassen Sie den Identitätswechsel SQL Server. Verwenden Sie nicht RevertToSelf, ImpersonateAnonymousToken, DdeImpersonateClient, ImpersonateDdeClientWindow, ImpersonateLoggedOnUser, ImpersonateNamedPipeClient, ImpersonateSelf, RpcImpersonateClient, RpcRevertToSelf, RpcRevertToSelfEx oder SetThreadToken.

Rufen Sie nicht Thread::Suspend auf

Einen Thread anzuhalten scheint ein unproblematischer Vorgang zu sein. Es kann dabei jedoch zu Deadlocks kommen. Wenn ein Thread, der eine Sperre verwendet, von einem zweiten Thread angehalten wird und dieser denselben Bereich sperrt, tritt ein Deadlock auf. Suspend kann aktuell die Sicherheit, das Laden von Klassen, Remoting und Reflektion beeinträchtigen.

Regel für die Codeanalyse

Rufen Sie Suspend nicht auf. Verwenden Sie stattdessen eine richtige Synchronisierungsprimitive wie ein Semaphore oder ManualResetEvent.

Schützen Sie kritische Vorgänge mit eingeschränkten Ausführungsbereichen und Zuverlässigkeitsvereinbarungen

Stellen Sie bei der Ausführung eines komplexen Vorgangs, der einen freigegebenen Zustand aktualisiert oder der deterministisch entweder vollständig gelingt oder fehlschlägt, sicher, dass dieser von einem eingeschränkten Ausführungsbereich (CER) geschützt ist. Auf diese Weise wird garantiert, dass der Code immer und sogar dann ausgeführt wird, wenn ein Thread unerwartet abgebrochen oder AppDomain unvorhergesehen entladen wird.

Ein CER ist ein besonderer try/finally-Block, dem ein Aufruf der Methode PrepareConstrainedRegions vorangestellt wird.

Hierdurch wird der Just-In-Time-Compiler angewiesen, den gesamten Code im finally-Block vorzubereiten, bevor der try-Block ausgeführt wird. So wird sichergestellt, dass der Code im finally-Block erstellt wird und in allen Fällen ausführbar ist. Es ist nicht ungewöhnlich, dass in einem CER ein leerer try-Block verwendet wird. Die Verwendung eines CER schützt vor asynchronen Threadabbrüchen und Ausnahmen bei unzureichendem Speicherplatz. Unter ExecuteCodeWithGuaranteedCleanup finden Sie einen CER, der zusätzlich Stapelüberläufe in überaus komplexem Code behandelt.

Siehe auch