Über GDI-Lecks und die Bedeutung des Glücks


Im Mai 2019 wurde ich gebeten, mir einen potenziell gefährlichen Chrome-Fehler anzusehen. Zuerst diagnostizierte ich ihn als unwichtig und verschwendete auf diese Weise zwei Wochen. Später, als ich zur Untersuchung zurückkehrte, wurde sie zur Hauptursache für den Absturz des Browserprozesses im Chrome Beta-Kanal. Hoppla

Am 6. Juni, an dem Tag, an dem ich meinen Fehler bei der Interpretation der Abflugdaten bemerkte, wurde der Fehler als ReleaseBlock-Stable markiert. Dies bedeutete, dass wir für die meisten Nutzer erst dann eine neue Version von Chrome veröffentlichen können, wenn wir herausgefunden haben, was los ist.

Der Absturz tritt auf, weil uns die GDI-Objekte (Graphics Device Interface) ausgegangen sind, wir aber nicht wussten, um welche Art von GDI-Objekten es sich handelt. Die Diagnosedaten gaben keine Hinweise darauf, wo das Problem lag, und wir konnten es nicht neu erstellen.

Viele Leute aus unserem Team haben am 6. und 7. Juni hart an diesem Fehler gearbeitet, sie haben ihre Theorien getestet, sind aber nicht weitergekommen. Am 8. Juni beschloss ich, meine E-Mails zu überprüfen, und Chrome stürzte sofort ab. Es war der gleiche Fehler .

Was für eine Ironie. Während ich nach Änderungen suchte und Absturzberichte untersuchte, um herauszufinden, was dazu führen könnte, dass der Chrome-Browserprozess GDI-Objekte verliert, stieg die Anzahl der GDI-Objekte in meinem Browser unaufhaltsam an und überschritt am Morgen des 8. Juni die magische Anzahl von 10.000 . Zu diesem Zeitpunkt ist eine der Speicherzuweisungsvorgänge für das GDI-Objekt fehlgeschlagen, und der Browser ist absichtlich abgestürzt. Es war ein unglaubliches Glück.

Wenn Sie den Fehler reproduzieren können, können Sie ihn zwangsläufig beheben. Ich musste nur herausfinden, wie ich diesen Fehler verursacht habe, danach können wir ihn beseitigen.

Für den Anfang eine kurze Geschichte des Problems



Wenn wir versuchen, Speicher für ein GDI-Objekt zuzuweisen, überprüfen wir an den meisten Stellen im Chromium-Code zunächst, ob diese Zuordnung erfolgreich war. Wenn es nicht möglich war, Speicher zuzuweisen, schreiben wir einige Informationen auf den Stapel und führen absichtlich einen Absturz durch, wie in diesem Quellcode zu sehen ist . Der Fehler wird absichtlich verursacht, da wir, wenn wir keinen Speicher für GDI-Objekte zuweisen können, nicht auf dem Bildschirm rendern können. Es ist besser, ein Problem zu melden (wenn Absturzberichte aktiviert sind) und den Prozess neu zu starten, als eine leere Benutzeroberfläche anzuzeigen. Standardmäßig können Sie bis zu 10.000 GDI-Objekte pro Prozess erstellen. In der Regel werden nur einige Hundert verwendet. Wenn wir diese Grenze überschritten haben, ist etwas völlig schief gelaufen.

Wenn wir einen der Absturzberichte erhalten, der den Speicherzuordnungsfehler für das GDI-Objekt angibt, haben wir einen Aufrufstapel und alle möglichen anderen nützlichen Informationen. Fein! Das Problem ist jedoch, dass solche Crash-Dumps nicht unbedingt mit dem Fehler zusammenhängen. Dies liegt daran, dass der Code, der das Leck von GDI-Objekten verursacht, und der Code, der den Fehler meldet, möglicherweise nicht derselbe Code sind.

Das heißt, wir haben ungefähr zwei Arten von Code:

void GoodCode () {
   auto x = AllocateGDIObject ();
   if (! x)
     CollectGDIUsageAndDie ();
   UseGDIObject (x);
   FreeGDIObject (x);
}}

void BadCode () {
   auto x = AllocateGDIObject ();
   UseGDIObject (x);
}}

Der gute Code bemerkt, dass die Speicherzuweisung fehlgeschlagen ist, und meldet dies. Der schlechte Code ignoriert die Abstürze und verschüttet Objekte, wodurch guter Code "ersetzt" wird, sodass er die Verantwortung übernimmt.

Chrom enthält mehrere Millionen Codezeilen. Wir wussten nicht, welche Funktion einen Fehler aufwies, und wir wussten nicht einmal, welche Art von GDI-Objekten undicht waren. Einer meiner Kollegen fügte Code hinzu , der den Prozessumgebungsblock vor dem Absturz umging , um die Anzahl der GDI-Objekte jedes Typs zu ermitteln. Bei allen aufgezählten Typen (Gerätekontexte, Bereiche, Bitmaps, Paletten, Pinsel, Federn und Unbekannt) wurde die Anzahl jedoch nicht überschritten. Es ist komisch.

Es stellte sich heraus, dass sich die Objekte, für die wir direkt Speicher zuweisen, in dieser Tabelle befinden, aber keine vom Kernel in unserem Namen erstellten Objekte vorhanden sind und sie irgendwo im Windows-Objektmanager vorhanden sind. Dies bedeutete, dass GDIView für dieses Problem genauso blind ist wie wir (außerdem ist GDIView nur nützlich, wenn ein Fehler lokal abgespielt wird). Weil wir Cursor durchgesickert sind und Cursor USER32-Objekte sind, an die GDI-Objekte angehängt sind; Der Speicher für diese GDI-Objekte wird vom Kernel zugewiesen, und wir konnten nicht sehen, was passiert ist.

Fehlinterpretation


Unsere Funktion CollectGDIUsageAndDie hat einen sehr lebendigen Namen, und ich denke, Sie werden mir darin zustimmen. Sehr ausdrucksvoll.

Das Problem ist, dass zu viele Aktionen ausgeführt werden. CollectGDIUsageAndDie überprüfte etwa ein Dutzend verschiedene Arten von Speicherzuordnungsfehlern für GDI-Objekte. Aufgrund der Einbettung des Codes erhielten sie dieselbe Fehlersignatur - alle stürzten in die Hauptfunktionen ab und wurden zusammengeführt. Daher hat einer meiner Kollegen mit Bedacht eine Änderung vorgenommen und verschiedene Prüfungen in separate (nicht integrierte) Funktionen aufgeteilt. Dank dessen konnten wir jetzt auf den ersten Blick verstehen, welche Prüfung fehlgeschlagen ist.

Leider führte dies dazu, dass wir anfingen, Absturzberichte von CrashIfExcessiveHandles zu erhaltenIch sagte zuversichtlich: "Dies ist nicht die Ursache des Fehlers, sondern lediglich eine Änderung der Signatur."

Aber ich habe mich getäuscht. Dies war die Ursache für den Fehler und die Änderung der Signatur. Hoppla Umständliche Analyse, Dawson. Keine Cookies für Sie.

Zurück zu unserer Geschichte


Zu diesem Zeitpunkt wusste ich bereits, dass etwas, das ich am 7. Juni tat, fast 10.000 GDI-Objekte pro Tag verwendete. Wenn ich das verstehen könnte, würde ich das Rätsel lösen.


Der Windows Task-Manager verfügt über eine zusätzliche GDI- Objektspalte , in der Sie Lecks finden können. Am 7. Juni arbeitete ich von zu Hause aus und stellte eine Verbindung zu meiner Arbeitsmaschine her. Diese Spalte wurde auf der Arbeitsmaschine aktiviert, da ich Tests durchgeführt und versucht habe, das Absturzszenario zu reproduzieren. In der Zwischenzeit gab es jedoch Lecks von GDI-Objekten im Browser auf meinem Heimcomputer .

Die Hauptaufgabe, für die ich den Browser zu Hause verwendet habe, besteht darin, mithilfe der CRD- Anwendung (Chrome Remote Desktop) eine Verbindung zu einem funktionierenden Computer herzustellen . Also schaltete ich die Spalte GDI-Objekte auf dem Heimcomputer ein und begann zu experimentieren. Bald bekam ich die Ergebnisse.

Tatsächlich zeigt die Zeitleiste des Fehlers, dass von dem Moment „Ich hatte einen Fehler“ (14:00) bis „Es ist irgendwie mit der CRD verbunden“ und dann bis zum „Fall in Cursorn“ nur 35 Minuten vergangen sind. Ich habe bereits gesagt, wie viel einfacher es ist, Fehler zu untersuchen, wenn Sie sie lokal abspielen können.

Es stellte sich heraus, dass jedes Mal, wenn eine CRD-Anwendung (oder eine Chrome-Anwendung?) Die Cursor änderte, sechs GDI-Objekte verloren gingen. Wenn Sie während der Arbeit mit Chrome Remote Desktop die Maus über den gewünschten Teil des Bildschirms bewegen, können Hunderte von GDI-Objekten pro Minute und Tausende pro Stunde auslaufen.

Nach einem Monat ohne Fortschritte bei der Lösung dieses Problems verwandelte es sich plötzlich von einem nicht entfernbaren in eine einfache Korrektur. Ich habe schnell einen Fixentwurf geschrieben und dann hat einer meiner Kollegen (ich habe an diesem Fehler nicht gearbeitet) einen echten Fix erstellt. Es wurde am 10. Juni um 11:16 Uhr heruntergeladen und um 13:00 Uhr veröffentlicht. Nach einigen Zusammenführungen verschwand der Fehler.

Das ist alles?


Wir haben den Fehler behoben und es ist großartig, aber es ist viel wichtiger, dass solche Fehler nie wieder auftreten. Natürlich ist es richtig, C ++ ( RAII ) -Objekte für die Ressourcenverwaltung zu verwenden , aber in diesem Fall war der Fehler in der WebCursor-Klasse enthalten.

Wenn es um Speicherlecks geht, gibt es eine zuverlässige Reihe von Systemen. Microsoft verfügt über Heap-Snapshots , Chromium über Heap-Profilerstellung für Benutzerversionen und einen Leck-Eliminatorauf Testmaschinen. Es scheint jedoch, dass Lecks von GDI-Objekten der Aufmerksamkeit beraubt wurden. Der Prozessinformationsblock enthält unvollständige Informationen, einige GDI-Objekte können nur im Kernelmodus aufgelistet werden, und es gibt keinen einzigen Punkt zum Zuweisen und Freigeben von Speicher für Objekte, die die Ablaufverfolgung erleichtern können. Dies war nicht das erste Leck von GDI-Objekten, mit dem ich mich befassen musste, und es wird nicht das letzte sein, da es keinen zuverlässigen Weg gibt, sie zu verfolgen. Hier sind meine Empfehlungen für die folgenden Windows-Versionen:

  • Machen Sie den Prozess, bei dem die Anzahl aller Arten von GDI-Objekten ermittelt wird, trivial, ohne PEB dunkel lesen zu müssen (und ohne Cursor zu ignorieren).
  • Erstellen Sie eine unterstützte Methode zum Abfangen und Verfolgen aller Vorgänge zum Erstellen und Zerstören von GDI-Objekten für eine zuverlässige Nachverfolgung. auch für diejenigen, die indirekt erstellt wurden
  • Reflektieren Sie dies alles in der Dokumentation

Das ist alles. Eine solche Verfolgung ist nicht einmal schwierig zu implementieren, da GDI-Objekte notwendigerweise so begrenzt sind, dass der Speicher nicht begrenzt ist. Es wäre großartig, wenn die Verwendung dieser seltsamen, aber unvermeidlichen GDI-Objekte sicherer wäre. Aber bitte.

Hier können Sie die Diskussion über Reddit lesen. Das Thema auf Twitter beginnt hier .

All Articles