Duas falhas com a mesma máscara
O tema é o Rockset, o sistema de pesquisa e análise nativo da cloud que a OpenAI adquiriu em 2024 e que agora usa para manter atualizado o índice da base de conhecimento de um workspace para o ChatGPT. A sua camada de execução está escrita em C++ e, poucos meses antes da publicação, começou a gerar falhas que pareciam fisicamente impossíveis: uma função normal parecia terminar e depois retornar para um endereço inválido, por vezes um slot de endereço de retorno a NULL, por vezes com o stack pointer %rsp desalinhado em exatamente 8 bytes.
Nenhum dos dois sintomas se explica por código de aplicação normal. Uma escrita descontrolada que atinja apenas um endereço de retorno guardado é extremamente improvável, e desalinhar o %rsp em 8 bytes sem assembly inline, setcontext ou longjmp — nenhum dos quais é usado pelo Rockset — é ainda mais estranho, porque o código compilado só toca esse registo no prólogo e no epílogo de uma função. Todas as hipóteses avançadas pela equipa ou pelo ChatGPT tinham fortes evidências contra si. A razão, acabou por se revelar, era que estavam a observar dois bugs não relacionados ao mesmo tempo: corrupção silenciosa de hardware num único servidor Azure e uma race condition de 18 anos na GNU libunwind.
Médico versus epidemiologista
O instinto inicial da equipa foi clínico: inspecionar de perto um punhado de core dumps, formular hipóteses e eliminá-las uma a uma. A maioria das falhas ocorria num único método fortemente inlined, DocumentTree::updateDocument, o que deixava um conjunto avassalador de funções candidatas. As queries aos logs produziam falsos positivos e falsos negativos, porque os bugs de corrupção da stack corrompiam precisamente os stack traces usados para os classificar. A inspeção manual não era escalável. Piorando as coisas, a equipa já tinha descartado uma causa de hardware porque as falhas surgiam em várias regiões e tipos de hardware — uma conclusão que estava exatamente invertida para uma das duas populações.
O ponto de viragem foi uma mudança de método. Em vez de diagnosticar um único paciente, trataram toda a frota como uma população. Pediram ao ChatGPT que escrevesse um script que descarregava um prefixo de cada core file, extraía os registos, filtrava falsos positivos conhecidos comparando com os logs, e classificava cada falha como retorno-a-null, stack desalinhada, ou outra. Ao executá-lo sobre todos os core dumps de produção do Rockset do ano anterior, as correlações surgiram de imediato: as falhas de retorno-a-null estavam dispersas por muitos clusters sem uma data de início clara, enquanto as falhas de stack desalinhada vinham de uma única região, tinham uma data de início bem definida e nunca ocorriam em nós de longa duração — a assinatura de uma única máquina física com hardware defeituoso.
Uma janela de cem picossegundos, e a estimativa de Fermi que a tornou credível
O servidor com defeito foi colocado em denylist, e as suas falhas de stack desalinhada desapareceram, apesar de a equipa nunca ter conseguido reproduzir a corrupção de registos em semanas de testes de stress. Removido esse cluster, os restantes core dumps de retorno-a-null ocorriam todos durante o unwinding de exceções em C++. A GNU libunwind sintetiza um ucontext_t na stack e entrega-o a uma rotina interna, _Ux86_64_setcontext, cujas instruções finais movem o %rsp para o novo fundo da stack antes de uma instrução posterior ler o instruction pointer de destino a partir dessa mesma struct. Depois de o %rsp se mover, a struct deixa de estar protegida pela red zone, pelo que um sinal entregue nesse intervalo permite ao kernel construir o seu signal frame sobre essa memória e zerar o instruction pointer restaurado.
A janela vulnerável é uma única instrução — cerca de cem picossegundos num CPU moderno fora de ordem — o que parecia demasiado estreito para explicar mais de uma dúzia de falhas de retorno-a-null por dia. Uma estimativa de Fermi fechou a lacuna: uma janela de 10⁻¹⁰ segundos face a um SIGUSR2 a chegar a cada 10⁻² segundos de tempo de CPU dá a cada handler de limpeza uma probabilidade de cerca de 10⁻⁸ de perder a corrida; um servidor a lançar 10⁴ exceções por segundo devido a contrapressão de ingestão falha então cerca de uma vez a cada poucas horas, o que, à escala da frota, é mais do que suficiente.
O Rockset era pouco comum nos três eixos que determinam a taxa de falhas. Lança exceções com frequência como controlo de sobrecarga, entrega SIGUSR2 com uma frequência inusitada através do seu coarse_thread_cputime_clock e, mais cedo nesse ano, tinha adicionado uma chamada timer_getoverrun que fazia com que o signal handler consumisse mais stack — o suficiente para alcançar e corromper o ucontext_t obsoleto. Antes dessa alteração, as falhas simplesmente não ocorriam. O bug de 18 anos esteve sempre presente; foi o produto da taxa de exceções, da taxa de sinais e do consumo de stack do handler que acabou de cruzar o limiar a partir do qual se tornou visível.
A instrumentação como verdadeira solução
A mitigação imediata foi mudar da GNU libunwind para o unwinder da libgcc — uma boa troca por si só, já que a libgcc tem recebido mais trabalho para reduzir a contenção de locks em VMs de grande dimensão — e a OpenAI submeteu à comunidade um reprodutor autocontido e a correção para a GNU libunwind. Para a falha de hardware, a equipa melhorou o fatal signal handler para registar o estado dos registos, permitindo detetar recorrências apenas a partir dos logs, sem precisar de um core dump, e alterou o control plane para reutilizar VMs em vez de as reciclar, o que facilita a deteção de nós defeituosos.
A lição fundamental não está em nenhum dos detalhes de ABI, DWARF ou mecanismos de exceção que a investigação revelou. Está no facto de a confusão só ter persistido porque dois fenómenos estavam misturados numa única história, e de se ter dissolvido no momento em que o conjunto de dados se tornou limpo e completo. É esse o ponto que vale a pena levar para outras equipas de infraestrutura: quando um bug parece impossível, o que falta é muitas vezes dados fiáveis sobre a população, e não um diagnóstico mais engenhoso de um único caso.
A fiabilidade não passa apenas por corrigir bugs depois de ocorrerem — passa por construir os dados, os fluxos de trabalho e as competências que transformam problemas impossíveis em problemas diagnosticáveis e solucionáveis. Montana Labs
Find this story relevant to you?
Contact us to find a unique solution