À propos des fuites GDI et de l'importance de la chance


En mai 2019, on m'a demandé de jeter un œil à un bogue Chrome potentiellement dangereux. Au début, je l'ai diagnostiqué comme sans importance, perdant ainsi deux semaines. Plus tard, lorsque je suis retourné à l'enquête, cela est devenu la première cause de blocage du processus du navigateur dans le canal bêta de Chrome. Oops

Le 6 juin, le jour même où j'ai réalisé mon erreur d'interprétation des données de départs, le bug a été marqué comme ReleaseBlock-Stable. Cela signifie que nous ne pourrons pas publier une nouvelle version de Chrome pour la plupart des utilisateurs tant que nous n'aurons pas compris ce qui se passe.

Le crash se produit car nous manquions d'objets GDI (Graphics Device Interface) , mais nous ne savions pas de quel type d'objets GDI il s'agissait, les données de diagnostic ne donnaient aucun indice sur l'emplacement du problème et nous n'avons pas pu le recréer.

Beaucoup de gens de notre équipe ont travaillé dur sur ce bug du 6 au 7 juin, ils ont testé leurs théories, mais n'ont pas avancé. Le 8 juin, j'ai décidé de vérifier mon courrier et Chrome s'est immédiatement écrasé. Ce fut le même échec .

Quelle ironie. Alors que je cherchais des modifications et que j'examinais les rapports de plantage, essayant de comprendre ce qui pouvait provoquer une fuite des objets GDI dans le navigateur Chrome, le nombre d'objets GDI dans mon navigateur augmentait sans relâche, et au matin du 8 juin, il dépassait le nombre magique de 10000 . À ce stade, l'une des opérations d'allocation de mémoire pour l'objet GDI a échoué et nous avons intentionnellement planté le navigateur. Ce fut une chance incroyable.

Si vous pouvez reproduire le bogue, vous pouvez inévitablement le corriger. Je devais juste comprendre comment j'ai causé ce bogue, après quoi nous pouvons l'éliminer.

Pour commencer, un bref historique du problème



Dans la plupart des endroits du code Chromium, lorsque nous essayons d'allouer de la mémoire à un objet GDI, nous vérifions d'abord si cette allocation a réussi. S'il n'était pas possible d'allouer de la mémoire, nous écrivons des informations sur la pile et effectuons intentionnellement un plantage, comme on peut le voir dans ce code source . L'échec est provoqué intentionnellement, car si nous ne pouvons pas allouer de mémoire pour les objets GDI, nous ne pourrons pas restituer à l'écran - il est préférable de signaler un problème (si les rapports de plantage sont activés) et de redémarrer le processus plutôt que d'afficher une interface utilisateur vide. Par défaut, vous pouvez créer jusqu'à 10 000 objets GDI par processus, et généralement quelques centaines seulement sont utilisés. Par conséquent, si nous avons dépassé cette limite, quelque chose s'est complètement passé.

Lorsque nous obtenons l'un des rapports d'erreur indiquant l'erreur d'allocation de mémoire pour l'objet GDI, nous avons une pile d'appels et toutes sortes d'autres informations utiles. Bien! Mais le problème est que ces vidages sur incident ne sont pas nécessairement liés au bogue. Cela est dû au fait que le code qui provoque la fuite d'objets GDI et le code qui signale l'échec peuvent ne pas être le même code.

Autrement dit, nous avons deux types de code:

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

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

Le bon code remarque que l'allocation de mémoire a échoué, et le signale, et le mauvais code ignore les plantages et renverse des objets, «substituant» ainsi le bon code pour qu'il prenne ses responsabilités.

Le chrome contient plusieurs millions de lignes de code. Nous ne savions pas quelle fonction avait une erreur et nous ne savions même pas quel type d' objets GDI fuyait. Un de mes collègues a ajouté du code qui contournait le bloc d'environnement de processus avant le crash pour obtenir le nombre d'objets GDI de chaque type, mais pour tous les types énumérés (contextes de périphérique, zones, bitmaps, palettes, pinceaux, plumes et inconnus), le nombre ne dépassait pas cent. C'est étrange.

Il s'est avéré que les objets pour lesquels nous allouons directement de la mémoire se trouvent dans ce tableau, mais il n'y a aucun objet créé par le noyau en notre nom, et ils existent quelque part dans le gestionnaire d'objets Windows. Cela signifie que GDIView est tout aussi aveugle à ce problème que nous le sommes (en outre, GDIView n'est utile que lors de la lecture d'un échec localement). Parce que nous avons des fuites de curseurs, et les curseurs sont des objets USER32 avec des objets GDI attachés à eux; la mémoire de ces objets GDI est allouée par le noyau, et nous n'avons pas pu voir ce qui se passait.

Interprétation erronée


Notre fonction CollectGDIUsageAndDie a un nom très vif, et je pense que vous serez d'accord avec moi sur ce point. Très expressif.

Le problème est qu'il effectue trop d'actions. CollectGDIUsageAndDie a vérifié environ une douzaine de différents types d'échecs d'allocation de mémoire pour les objets GDI, et en raison de l'intégration du code, ils ont reçu la même signature d'échec en conséquence - ils se sont tous écrasés dans les fonctions principales et ont fusionné. Par conséquent, l'un de mes collègues a judicieusement apporté un changement , en répartissant différents contrôles en fonctions distinctes (non intégrées). Grâce à cela, maintenant, à première vue, nous pouvions comprendre quel contrôle s'était soldé par un échec.

Hélas, cela a conduit au fait que lorsque nous avons commencé à obtenir des rapports d' erreur de CrashIfExcessiveHandles, J'ai dit avec confiance: "ce n'est pas la cause de l'échec, c'est simplement causé par un changement de signature".

Mais je me trompais. Ce fut la cause de l'échec et du changement de signature. Oops Analyse maladroite, Dawson. Pas de cookies pour vous.

Retour à notre histoire


À ce stade, je savais déjà que quelque chose que j'ai fait le 7 juin utilisait près de 10 000 objets GDI par jour. Si je pouvais comprendre cela, je résoudrais l'énigme.


Le Gestionnaire des tâches de Windows possède une colonne d' objets GDI supplémentaire que vous pouvez utiliser pour rechercher des fuites. Le 7 juin, je travaillais de chez moi, me connectant à ma machine de travail, et cette colonne a été activée sur la machine de travail parce que j'ai effectué des tests et essayé de reproduire le scénario de plantage. Mais en attendant, il y a eu des fuites d'objets GDI dans le navigateur de ma machine domestique .

La principale tâche pour laquelle j'ai utilisé le navigateur à la maison est de se connecter à une machine en état de marche à l'aide de l'application Chrome Remote Desktop (CRD) . J'ai donc activé la colonne des objets GDI sur la machine domestique et j'ai commencé à expérimenter. Bientôt, j'ai obtenu les résultats.

En fait, la chronologie du bogue montre qu'à partir du moment où «j'ai eu un échec» (14h00) à «il est en quelque sorte connecté avec CRD», et ensuite il n'a fallu que 35 minutes pour «gérer les curseurs». J'ai déjà dit à quel point il est plus facile d'enquêter sur les bogues lorsque vous pouvez les jouer localement?

Il s'est avéré que chaque fois qu'une application CRD (ou toute application Chrome?) Changeait de curseur, cela entraînait la fuite de six objets GDI. Si vous déplacez la souris sur la partie souhaitée de l'écran lorsque vous travaillez avec Chrome Remote Desktop, des centaines d'objets GDI par minute et des milliers par heure peuvent fuir.

Après un mois d'absence de tout progrès dans la résolution de ce problème, il est soudainement passé d'un état inamovible à une simple correction. J'ai rapidement écrit un projet de correctif, puis un de mes collègues (je n'ai pas travaillé sur ce bogue) a créé un véritable correctif. Il a été téléchargé le 10 juin à 11:16 et a été publié à 13:00. Après quelques fusions, le bug a disparu.

C'est tout?


Nous avons corrigé le bogue, et c'est génial, mais il est beaucoup plus important que de tels bogues ne se reproduisent plus. Évidemment, il est correct d'utiliser des objets C ++ ( RAII ) pour la gestion des ressources , mais dans ce cas, le bogue était contenu dans la classe WebCursor.

En ce qui concerne les fuites de mémoire, il existe un ensemble fiable de systèmes. Microsoft a des instantanés de tas , Chromium a un profilage de tas pour les versions utilisateur et un éliminateur de fuitessur des machines d'essai. Mais il semble que les fuites d'objets GDI aient été privées d'attention. Le bloc d'informations sur le processus contient des informations incomplètes, certains objets GDI peuvent être répertoriés uniquement en mode noyau, et il n'y a pas de point unique pour allouer et libérer de la mémoire pour les objets qui peuvent faciliter le traçage. Ce n'était pas la première fuite d'objets GDI avec laquelle j'ai dû faire face, et ce ne sera pas la dernière, car il n'existe aucun moyen fiable de les suivre. Voici mes recommandations pour les versions Windows suivantes:

  • Rendre le processus d'obtention du nombre de tous les types d'objets GDI trivial, sans avoir à lire obscurement PEB (et sans ignorer les curseurs)
  • Créez un moyen pris en charge pour intercepter et tracer toutes les opérations de création et de destruction d'objets GDI pour un suivi fiable; y compris pour ceux qui ont été créés indirectement
  • Refléter tout cela dans la documentation

C'est tout. Un tel suivi n'est même pas difficile à mettre en œuvre, car les objets GDI sont nécessairement limités de manière à ce que la mémoire ne soit pas limitée. Ce serait formidable si l'utilisation de ces objets GDI étranges mais inévitables était plus sûre. Oh s'il te plait.

Ici, vous pouvez lire la discussion sur Reddit. Le sujet sur Twitter commence ici .

All Articles