Sobre vazamentos de GDI e a importância da sorte


Em maio de 2019, fui convidado a dar uma olhada em um bug do Chrome potencialmente perigoso. No começo, eu o diagnosticei sem importância, perdendo assim duas semanas. Mais tarde, quando voltei à investigação, ela se transformou na causa número um do travamento do processo do navegador no canal beta do Chrome. Opa

Em 6 de junho, no mesmo dia em que percebi meu erro ao interpretar os dados das partidas, o bug foi marcado como ReleaseBlock-Stable. Isso significava que não poderíamos lançar uma nova versão do Chrome para a maioria dos usuários até descobrirmos o que está acontecendo.

A falha ocorre porque estávamos ficando sem objetos GDI (Graphics Device Interface) , mas não sabíamos que tipo de objetos GDI eram, os dados de diagnóstico não deram nenhuma pista sobre onde estava o problema e não pudemos recriá-lo.

Muitas pessoas da nossa equipe trabalharam duro nesse bug nos dias 6 e 7 de junho, testaram suas teorias, mas não avançaram. Em 8 de junho, decidi verificar meus e-mails e o Chrome travou imediatamente. Foi o mesmo fracasso .

Que ironia. Enquanto eu procurava alterações e examinava os relatórios de falhas, tentando descobrir o que poderia causar o vazamento de objetos GDI no navegador Chrome, o número de objetos GDI no meu navegador estava aumentando incansavelmente e, na manhã de 8 de junho, excedeu o número mágico de 10.000 . Nesse ponto, uma das operações de alocação de memória para o objeto GDI falhou e travamos intencionalmente o navegador. Foi uma sorte incrível.

Se você pode reproduzir o bug, inevitavelmente poderá corrigi-lo. Eu só tinha que descobrir como eu causei esse bug, após o qual podemos eliminá-lo.

Para iniciantes, um breve histórico da questão



Na maioria dos lugares no código Chromium, quando tentamos alocar memória para um objeto GDI, primeiro verificamos se essa alocação foi bem-sucedida. Se não foi possível alocar memória, escrevemos algumas informações na pilha e intencionalmente executamos uma falha, como pode ser visto neste código-fonte . A falha é causada intencionalmente, porque se não pudermos alocar memória para objetos GDI, não poderemos renderizar na tela - é melhor relatar um problema (se os relatórios de falhas estiverem ativados) e reiniciar o processo do que exibir uma interface do usuário vazia. Por padrão, você pode criar até 10.000 objetos GDI por processo e, normalmente, apenas algumas centenas são usadas. Portanto, se excedermos esse limite, algo deu completamente errado.

Quando obtemos um dos relatórios de falha que indicam o erro de alocação de memória para o objeto GDI, temos uma pilha de chamadas e todo tipo de outras informações úteis. Bem! Mas o problema é que esses despejos de memória não estão necessariamente relacionados ao bug. Isso ocorre porque o código que causa o vazamento de objetos GDI e o código que relata a falha podem não ser o mesmo código.

Ou seja, grosso modo, temos dois tipos de código:

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

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

O código bom percebe que a alocação de memória falhou e relata isso, e o código incorreto ignora as falhas e espalha objetos, "substituindo" o código bom para que ele assuma a responsabilidade.

O Chromium contém vários milhões de linhas de código. Não sabíamos qual função apresentava um erro e nem sabíamos que tipo de objetos GDI estavam vazando. Um de meus colegas adicionou um código que ignorava o Process Environment Block antes da falha para obter o número de objetos GDI de cada tipo, mas para todos os tipos enumerados (contextos de dispositivo, áreas, bitmaps, paletas, pincéis, penas e desconhecidos) o número não excedia cem. É estranho.

Aconteceu que os objetos para os quais alocamos memória diretamente estão nesta tabela, mas não existem objetos criados pelo kernel em nosso nome e eles existem em algum lugar no gerenciador de objetos do Windows. Isso significava que o GDIView é tão cego para esse problema quanto nós (além disso, o GDIView é útil apenas ao reproduzir uma falha localmente). Porque tivemos vazamentos de cursores, e os cursores são objetos USER32 com objetos GDI anexados a eles; a memória desses objetos GDI é alocada pelo kernel e não conseguimos ver o que estava acontecendo.

Interpretação incorreta


Nossa função CollectGDIUsageAndDie tem um nome muito vívido e acho que você concorda comigo sobre isso. Muito expressivo.

O problema é que ele executa muitas ações. CollectGDIUsageAndDie verificou cerca de uma dúzia de tipos diferentes de falhas de alocação de memória para objetos GDI e, como resultado da incorporação do código, eles receberam a mesma assinatura de falha - todos entraram nas funções principais e se fundiram. Portanto, um dos meus colegas sabiamente fez uma alteração , dividindo verificações diferentes em funções separadas (não integradas). Graças a isso, agora, à primeira vista, pudemos entender qual verificação terminou em falha.

Infelizmente, isso levou ao fato de que, quando começamos a receber relatórios de falhas do CrashIfExcessiveHandles, Eu disse com confiança: "esta não é a causa do fracasso, é simplesmente causada por uma alteração na assinatura".

Mas eu estava errado. Essa foi a causa da falha e a alteração da assinatura. Opa Análise embaraçosa, Dawson. Sem cookies para você.

Voltar à nossa história


Neste ponto, eu já sabia que algo que fiz em 7 de junho utilizava quase 10.000 objetos GDI por dia. Se eu pudesse entender isso, resolveria o enigma.


O Gerenciador de tarefas do Windows possui uma coluna de objetos GDI adicionais que você pode usar para encontrar vazamentos. Em 7 de junho, eu estava trabalhando em casa, conectando-me à minha máquina de trabalho, e essa coluna foi ativada na máquina de trabalho porque eu executei testes e tentei reproduzir o cenário de falha. Entretanto, houve vazamentos de objetos GDI no navegador da minha máquina doméstica .

A principal tarefa para a qual eu usei o navegador em casa é conectar-se a uma máquina em funcionamento usando o aplicativo Chrome Remote Desktop (CRD) . Então, ativei a coluna de objetos GDI na máquina doméstica e comecei a experimentar. Logo eu consegui os resultados.

De fato, a linha do tempo do bug mostra que desde o momento em que "eu tive uma falha" (14:00) até "está de alguma forma conectado ao CRD" e depois ao "caso em cursores", apenas 35 minutos se passaram. Eu já disse como é mais fácil investigar bugs quando você pode reproduzi-los localmente?

Acontece que toda vez que um aplicativo CRD (ou qualquer aplicativo Chrome?) Altera os cursores, isso leva ao vazamento de seis objetos GDI. Se você mover o mouse sobre a parte desejada da tela enquanto trabalha com a Área de trabalho remota do Chrome, centenas de objetos GDI por minuto e milhares por hora podem vazar.

Após um mês de ausência de progresso na solução desse problema, de repente ele passou de um irremovível para uma correção simples. Eu escrevi rapidamente uma correção de rascunho e, em seguida, um dos meus colegas (eu não trabalhei nesse bug) criou uma correção real. Foi baixado em 10 de junho às 11:16 e foi lançado às 13:00. Após algumas mesclagens, o bug desapareceu.

Isso é tudo?


Corrigimos o bug, e é ótimo, mas é muito mais importante que esses erros nunca ocorram novamente. Obviamente, é correto usar objetos C ++ ( RAII ) para gerenciamento de recursos , mas nesse caso o bug estava contido na classe WebCursor.

Quando se trata de vazamentos de memória, existe um conjunto confiável de sistemas. A Microsoft possui instantâneos de heap , o Chromium possui perfis de heap para versões do usuário e um eliminador de vazamentosem máquinas de teste. Mas parece que vazamentos de objetos GDI foram privados de atenção. O Process Information Block contém informações incompletas, alguns objetos GDI podem ser listados apenas no modo kernel e não há um ponto único para alocar e liberar memória para objetos que podem facilitar o rastreamento. Este não foi o primeiro vazamento de objetos GDI com os quais tive que lidar e não será o último, porque não há uma maneira confiável de rastreá-los. Aqui estão minhas recomendações para os seguintes lançamentos do Windows:

  • Torne o processo de obter o número de todos os tipos de objetos GDI trivial, sem ter que ler o PEB de forma obscura (e sem ignorar os cursores)
  • Crie uma maneira suportada de interceptar e rastrear todas as operações de criação e destruição de objetos GDI para rastreamento confiável; inclusive para aqueles que foram criados indiretamente
  • Reflita tudo isso na documentação

Isso é tudo. Esse rastreamento nem é difícil de implementar, porque os objetos GDI são necessariamente limitados de uma maneira que a memória não é limitada. Seria ótimo se o uso desses objetos GDI estranhos, mas inevitáveis, fosse mais seguro. Oh, por favor.

Aqui você pode ler a discussão no Reddit. O tópico no Twitter começa aqui .

All Articles