Sobre las filtraciones de GDI y la importancia de la suerte


En mayo de 2019, me pidieron que echara un vistazo a un error de Chrome potencialmente peligroso. Al principio, lo diagnostiqué como sin importancia, desperdiciando así dos semanas. Más tarde, cuando volví a la investigación, se convirtió en la causa número uno del proceso del navegador se bloquea en el canal beta de Chrome. Ups

El 6 de junio, el mismo día en que me di cuenta de mi error al interpretar los datos de las salidas, el error se marcó como ReleaseBlock-Stable. Esto significa que no podremos lanzar una nueva versión de Chrome para la mayoría de los usuarios hasta que descubramos qué está sucediendo.

El bloqueo se produce porque nos estábamos quedando sin objetos GDI (interfaz de dispositivo gráfico) , pero no sabíamos qué tipo de objetos GDI eran, los datos de diagnóstico no dieron ninguna pista sobre dónde estaba el problema y no pudimos recrearlo.

Muchas personas de nuestro equipo trabajaron duro en este error del 6 al 7 de junio, probaron sus teorías, pero no avanzaron. El 8 de junio, decidí revisar mi correo y Chrome se bloqueó de inmediato. Fue el mismo fracaso .

Que ironía. Mientras buscaba cambios y examinaba informes de fallas, tratando de descubrir qué podía causar que el proceso del navegador Chrome filtrara objetos GDI, la cantidad de objetos GDI en mi navegador aumentaba incesantemente y, en la mañana del 8 de junio, superó el número mágico de 10,000 . En este punto, una de las operaciones de asignación de memoria para el objeto GDI falló y bloqueamos intencionalmente el navegador. Fue una suerte increíble.

Si puede reproducir el error, inevitablemente puede solucionarlo. Solo tenía que averiguar cómo causé este error, después de lo cual podemos eliminarlo.

Para empezar, una breve historia del problema.



En la mayoría de los lugares en el código de Chromium, cuando intentamos asignar memoria para un objeto GDI, primero verificamos si esta asignación fue exitosa. Si no fue posible asignar memoria, entonces escribimos cierta información en la pila y realizamos un bloqueo intencionalmente, como se puede ver en este código fuente . La falla es causada intencionalmente, porque si no podemos asignar memoria para objetos GDI, entonces no podremos renderizar en la pantalla; es mejor informar un problema (si los informes de bloqueo están habilitados) y reiniciar el proceso que mostrar una IU vacía. De manera predeterminada, puede crear hasta 10,000 objetos GDI por proceso, y normalmente solo se usan unos pocos cientos. Por lo tanto, si excedimos este límite, entonces algo salió completamente mal.

Cuando recibimos uno de los informes de bloqueo que dice el error de asignación de memoria para el objeto GDI, tenemos una pila de llamadas y todo tipo de otra información útil. ¡Multa! Pero el problema es que dichos volcados no necesariamente están relacionados con el error. Esto se debe a que el código que causa la fuga de objetos GDI y el código que informa la falla puede no ser el mismo código.

Es decir, a grandes rasgos, tenemos dos tipos de código:

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

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

El código correcto advierte que la asignación de memoria falló, e informa esto, y el código incorrecto ignora los bloqueos y derrama objetos, "sustituyendo" así el código correcto para que asuma la responsabilidad.

Chromium contiene varios millones de líneas de código. No sabíamos qué función tenía un error, y ni siquiera sabíamos qué tipo de objetos GDI tenían fugas. Uno de mis colegas agregó un código que omitió el Bloque de entorno de proceso antes del bloqueo para obtener el número de objetos GDI de cada tipo, pero para todos los tipos enumerados (contextos de dispositivo, áreas, mapas de bits, paletas, pinceles, plumas y desconocido) el número no superó los cien. Es extraño.

Resultó que los objetos para los cuales asignamos memoria directamente están en esta tabla, pero no hay objetos creados por el núcleo en nuestro nombre, y existen en algún lugar del administrador de objetos de Windows. Esto significaba que GDIView es tan ciego a este problema como nosotros (además, GDIView solo es útil cuando se reproduce un fallo localmente). Porque hemos filtrado cursores, y los cursores son objetos USER32 con objetos GDI adjuntos; el núcleo asigna la memoria para estos objetos GDI, y no pudimos ver lo que estaba sucediendo.

Mala interpretación


Nuestra función CollectGDIUsageAndDie tiene un nombre muy vívido, y creo que estará de acuerdo conmigo en esto. Muy expresivo.

El problema es que realiza demasiadas acciones. CollectGDIUsageAndDie verificó cerca de una docena de diferentes tipos de fallas de asignación de memoria para objetos GDI, y debido a la incrustación del código, como resultado, recibieron la misma firma de falla: todos se estrellaron en las funciones principales y se fusionaron. Por lo tanto, uno de mis colegas hizo un cambio sabiamente , rompiendo diferentes controles en funciones separadas (no integradas). Gracias a esto, ahora, a primera vista, pudimos entender qué verificación terminó en falla.

Por desgracia, esto llevó al hecho de que cuando comenzamos a recibir informes de fallas de CrashIfExcessiveHandles, Dije con confianza: "esta no es la causa de la falla, simplemente es causada por un cambio en la firma".

Pero estaba equivocado. Esta fue la causa de la falla y el cambio de firma. Ups Análisis incómodo, Dawson. No hay cookies para ti.

De vuelta a nuestra historia


En este punto, ya sabía que algo que hice el 7 de junio usaba casi 10,000 objetos GDI por día. Si pudiera entender eso, resolvería el enigma.


El Administrador de tareas de Windows tiene una columna de objetos GDI adicionales que puede usar para buscar fugas. El 7 de junio, estaba trabajando desde casa, conectándome a mi máquina de trabajo, y esta columna se activó en la máquina de trabajo porque ejecuté pruebas e intenté reproducir el escenario de falla. Pero mientras tanto, había fugas de objetos GDI en el navegador de mi máquina doméstica .

La tarea principal para la que utilicé el navegador en casa es conectarme a una máquina que funcione usando la aplicación Chrome Remote Desktop (CRD) . Así que encendí la columna de objetos GDI en la máquina doméstica y comencé a experimentar. Pronto obtuve los resultados.

De hecho, la línea de tiempo del error muestra que desde el momento "tuve un error" (14:00) hasta "está de alguna manera conectado con el CRD", y luego al "caso en cursores" solo pasaron 35 minutos. Ya he dicho lo fácil que es investigar errores cuando puedes jugarlos localmente.

Resultó que cada vez que una aplicación CRD (¿o alguna aplicación de Chrome?) Cambiaba los cursores, esto provocaba la fuga de seis objetos GDI. Si mueve el mouse sobre la parte deseada de la pantalla mientras trabaja con Chrome Remote Desktop, se pueden perder cientos de objetos GDI por minuto y miles por hora.

Después de un mes de ausencia de progreso en la solución de este problema, de repente se convirtió en uno inamovible en una simple corrección. Rápidamente escribí un borrador de corrección, y luego uno de mis colegas (no trabajé en este error) creó una solución real. Se descargó el 10 de junio a las 11:16 y se lanzó a las 13:00. Después de algunas fusiones, el error desapareció.

¿Eso es todo?


Arreglamos el error, y es genial, pero es mucho más importante que esos errores nunca vuelvan a ocurrir. Obviamente, es correcto usar objetos C ++ ( RAII ) para la gestión de recursos , pero en este caso el error estaba contenido en la clase WebCursor.

Cuando se trata de pérdidas de memoria, hay un conjunto confiable de sistemas. Microsoft tiene instantáneas de montón , Chromium tiene perfiles de montón para versiones de usuario y un eliminador de fugasen máquinas de prueba. Pero parece que las fugas de objetos GDI han sido privadas de atención. El bloque de información de proceso contiene información incompleta, algunos objetos GDI se pueden enumerar solo en modo kernel y no existe un punto único para asignar y liberar memoria para objetos que puedan facilitar el rastreo. Esta no fue la primera fuga de objetos GDI con la que tuve que lidiar, y no será la última, porque no hay una forma confiable de rastrearlos. Aquí están mis recomendaciones para las siguientes versiones de Windows:

  • Haga que el proceso de obtener el número de todos los tipos de objetos GDI sea trivial, sin tener que leer PEB de manera oscura (y sin ignorar los cursores)
  • Cree una forma compatible para interceptar y rastrear todas las operaciones de creación y destrucción de objetos GDI para un seguimiento confiable; incluso para aquellos que fueron creados indirectamente
  • Refleja todo esto en la documentación

Eso es todo. Tal seguimiento ni siquiera es difícil de implementar, porque los objetos GDI están necesariamente limitados de una manera que la memoria no está limitada. Sería genial si usar estos objetos GDI extraños pero inevitables fuera más seguro. Oh por favor.

Aquí puedes leer la discusión sobre Reddit. El tema en Twitter comienza aquí .

All Articles