A solução de problemas usual em tais casos é examinar cuidadosamente o ciclo de vida do objeto afetado: veja como a memória é alocada para ele, como é liberada, como os contadores de referência são obtidos e liberados corretamente, prestando atenção especial aos caminhos de erro. No entanto, em nosso caso, diferentes objetos foram encapsulados, e a verificação de seu ciclo de vida não encontrou bugs.
O cache kmalloc-192 é bastante popular no kernel, ele combina várias dezenas de objetos diferentes. Um bug no ciclo de vida de um deles é o motivo mais provável para esse tipo de bug. Até mesmo listar todos esses objetos é bastante problemático, e não há como verificar todos eles. Relatórios de bugs continuaram chegando, mas não conseguimos encontrar sua causa por investigação direta. Uma dica era necessária.
De nossa parte, esses bugs foram investigados por Andrey Ryabinin, um especialista em gerenciamento de memória, amplamente conhecido em círculos restritos de desenvolvedores de kernel como o desenvolvedor do KASAN, uma tecnologia incrível para detectar erros de acesso à memória. Na verdade, foi o KASAN o mais adequado para descobrir as causas do nosso bug. O KASAN não foi incluído no kernel RHEL7 original, mas Andrey portou os patches necessários para nós no OpenVz. Não incluímos KASAN na versão de produção de nosso kernel, mas está presente na versão de depuração do kernel e ajuda ativamente nosso controle de qualidade a encontrar bugs.
Além do KASAN, o kernel de depuração inclui muitos outros recursos de depuração que herdamos da Red Hat. Como resultado da depuração, o kernel ficou bastante lento. QA diz que os mesmos testes em um kernel de depuração demoram 4 vezes mais. Para nós, isso não é fundamental, não medimos desempenho ali, mas procuramos bugs. No entanto, tal desaceleração era inaceitável para os clientes, e nossos pedidos para colocar um kernel de depuração em produção eram invariavelmente rejeitados.
Como alternativa ao KASAN, os clientes foram solicitados a habilitar slub_debug nos nós afetados... Essa tecnologia também permite a detecção de corrupção de memória. Usando uma zona vermelha e envenenamento de memória para cada objeto, o alocador de memória verifica se tudo está em ordem cada vez que aloca e libera memória. Se algo der errado, ele emite uma mensagem de erro, se possível, corrige o dano detectado e permite que o kernel continue funcionando. Além disso, as informações sobre quem alocou e liberou um objeto por último são armazenadas, de forma que, no caso de detecção pós-fato de corrupção de memória, seja possível entender "quem" esse objeto era em uma "vida passada". Slub_debug pode ser habilitado na linha de comando do kernel em um kernel de produção, mas essas verificações também consomem memória e recursos de CPU. Para desenvolvimento e depuração de controle de qualidade, isso é bom, mas os clientes de produção usam sem muito entusiasmo.
Seis meses se passaram, o Ano Novo se aproximava. Testes locais no kernel de depuração com KASAN não detectaram o problema, não recebemos relatórios de erros dos nós com slub_debug habilitado, não encontramos nada nas matérias-primas e não encontramos o problema. Andrey estava carregado com outras tarefas, pelo contrário, recebi um gap e fui instruído a analisar o próximo relatório de bug.
Depois de analisar o despejo de memória, logo descobri o objeto kmalloc-192 problemático: sua memória estava preenchida com algum tipo de lixo, informações pertencentes a outro tipo de objeto. Foi muito semelhante às consequências do uso após livre, mas após examinar cuidadosamente o ciclo de vida do objeto danificado nas matérias-primas, também não encontrei nada suspeito.
Olhei os relatórios de bug antigos, tentei encontrar alguma pista lá, mas também sem sucesso.
Por fim, voltei ao meu bug e comecei a olhar para o objeto anterior. Também acabou por estar em uso, mas pelo seu conteúdo era completamente incompreensível o que era - não havia constantes, referências a funções ou outros objetos. Depois de rastrear várias gerações de referências a esse objeto, acabei descobrindo que se tratava de um bitmap redutor. Este objeto fazia parte da técnica de otimização para liberar memória do contêiner. A tecnologia foi desenvolvida originalmente para nossos kernels, mais tarde seu autor Kirill Tkhai a comprometeu com a linha principal do Linux.
"Os resultados mostram que o desempenho aumenta em pelo menos 548 vezes."
Vários milhares de patches complementam o kernel RHEL7 estável como rocha, tornando o kernel Virtuozzo o mais conveniente possível para os hosters. Sempre que possível, tentamos enviar nossos desenvolvimentos para a linha principal, pois isso facilita a manutenção do código em boas condições.
Seguindo os links, encontrei uma estrutura que descreve meu bitmap. O Descritor acreditava que o tamanho do bitmap deveria ser de 240 bytes, o que não poderia ser verdade de forma alguma, pois na verdade o objeto foi alocado do cache kmalloc-192.
Bingo!
Descobriu-se que as funções que trabalham com bitmap acessam a memória além de seu limite superior e podem alterar o conteúdo do próximo objeto. No meu caso, havia um refcount no início do objeto e, quando o bitmap o anulou, o put subsequente levou à liberação repentina do objeto. Posteriormente, a memória foi alocada novamente para um novo objeto, cuja inicialização foi percebida como lixo pelo código do objeto antigo, o que mais cedo ou mais tarde inevitavelmente levaria ao crash do nó.

É bom quando você pode consultar o autor do código!
Olhando seu código com Kirill, logo encontramos a causa raiz da discrepância detectada. Conforme o número de contêineres aumentava, o bitmap deveria ter aumentado, mas deixamos um dos casos de fora e, como resultado, às vezes pulamos o bitmap de redimensionamento. Em nossos testes locais, essa situação não foi encontrada, e na versão do patch que Kirill enviou para a linha principal, o código foi redesenhado e não havia bug ali.
Com 4 tentativas, Kirill e eu trabalhamos juntos para compor tal patch , durante um mês rodamos em testes locais e no final de fevereiro lançamos uma atualização com um kernel corrigido. Verificamos seletivamente outros despejos de memória, também encontramos o bitmap errado na vizinhança, comemoramos a vitória e eliminamos erros antigos às escondidas.
No entanto, as velhas continuavam caindo e caindo. O fluxo desses tipos de relatórios de bugs diminuiu, mas não secou completamente.
Em geral, isso era esperado. Nossos clientes são hosters. Eles odeiam muito reiniciar seus nós, porque reboot == tempo de inatividade == dinheiro perdido. Também não gostamos de lançar kernels com frequência. O lançamento oficial da atualização é um procedimento bastante trabalhoso que requer a execução de vários testes diferentes. Portanto, novos kernels estáveis são lançados aproximadamente trimestralmente.
Para garantir a entrega imediata de correções de bugs aos nós de produção do cliente, usamos os patches ativos ReadyKernel. Na minha opinião, ninguém mais faz isso, exceto nós. Virtuozzo 7 usa uma estratégia incomum para usar caminhos ao vivo.
Normalmente, o lifepatch é apenas segurança. Em nosso país, 3/4 das correções são correções de bugs. Correções para bugs que nossos clientes já encontraram ou podem facilmente encontrar no futuro. Efetivamente, essas coisas podem ser feitas apenas para o seu kit de distribuição: sem feedback dos usuários, é impossível entender o que é importante para eles e o que não é.
Patching ao vivo certamente não é uma panacéia. Geralmente, é impossível corrigir tudo em uma linha - a tecnologia não permite. A nova funcionalidade também não é adicionada dessa forma. No entanto, uma parte significativa dos bugs é corrigida com os patches de uma linha mais simples, que são excelentes para patching vitalício. Em casos mais complexos, o patch original tem que ser “modificado criativamente com um arquivo”, às vezes a máquina de patching ao vivo tem bugs, mas nosso mago da vida patching Zhenya Shatokhin conhece seu trabalho perfeitamente. Recentemente, por exemplo, ele desenterroubug encantador no kpatch , sobre o qual, por boas razões , geralmente vale a pena escrever uma ópera separada.
Conforme as correções de bug apropriadas se acumulam, geralmente uma vez a cada uma ou duas semanas, Zhenya lança outra série de patches ativos do ReadyKernel. Após o lançamento, eles voam instantaneamente para os nós do cliente e evitam o ataque ao rake que já conhecemos. E tudo isso sem reiniciar os nós clientes. E libere os kernels desnecessariamente com frequência. Benefícios contínuos.
No entanto, muitas vezes o patch ativo chega aos clientes tarde demais: o problema que ele fecha já aconteceu, mas o nó, mesmo assim, ainda não travou.
É por isso que o surgimento de novos relatórios de bug com o problema que já corrigimos não foi inesperado para nós. Analisá-los repetidamente mostrou sintomas familiares: kernel antigo, lixo em kmalloc-192, bitmap "errado" na frente dele e um patch live descarregado ou carregado posteriormente com uma correção.
OVZ-7188 da FastVPS , que chegou no final de fevereiro, parecia um desses casos . “Muito obrigado pelo relatório de bug. Nossas condolências. Imediatamente muito semelhante ao problema conhecido. É uma pena que não haja patches ativos no OpenVZ. Espere por um lançamento de kernel estável, mude para Virtuozzo ou use kernels instáveis com uma correção de bug. "
Relatórios de bugs são uma das coisas mais valiosas que o OpenVZ nos oferece. Pesquisá-los nos dá a chance de detectar problemas sérios antes que qualquer um dos clientes gordos entre em ação. Portanto, apesar do problema conhecido, eu pedi para preencher despejos de memória para nós.
Analisar o primeiro deles me desencorajou um pouco: o bitmap "errado" na frente do objeto kmalloc-192 "torto" não foi encontrado.
Um pouco depois, o problema foi reproduzido no novo kernel. E depois outro, outro e outro.
Ops!
Como assim? Não corrigido? Verifiquei novamente as matérias-primas - está tudo bem, o patch está no lugar, nada está perdido.
De novo, corrupção? No mesmo lugar?
Eu tinha que descobrir novamente.
(O que é isso? Veja aqui )
Em cada um dos novos despejos de memória, a investigação novamente encontrou o objeto kmalloc-192. Em geral, esse objeto parecia bastante normal, mas no início do objeto, o endereço errado era encontrado todas as vezes. Rastreando o relacionamento do objeto, descobri que dois bytes internos foram anulados no endereço.
in all cases corrupted pointer contains nulls in 2 middle bytes: (mask 0xffffffff0000ffff)
0xffff9e2400003d80
0xffff969b00005b40
0xffff919100007000
0xffff90f30000ccc0
No primeiro dos casos listados, em vez do endereço "incorreto" 0xffff9e2400003d80, o endereço "correto" 0xffff9e24740a3d80 deveria ser. Situação semelhante foi encontrada em outros casos.
Descobriu-se que algum código estranho anulou nosso objeto com 2 bytes. O cenário mais provável é o uso após a liberação, quando um objeto, depois de ser liberado, zera algum campo em seus primeiros bytes. Verifiquei os objetos usados com mais frequência, mas nada suspeito foi encontrado. Novamente um beco sem saída.
FastVPSa nosso pedido, rodei o kernel de depuração com KASAN por uma semana, mas não ajudou, o problema nunca foi reproduzido. Pedimos para registrar o slub_debug, mas foi necessário reiniciar e o processo demorou muito. Em março-abril, os nós travaram várias vezes, mas slub_debug foi desligado, e isso não nos deu novas informações.
E então houve uma calmaria, o problema parou de se reproduzir. Abril terminou, maio passou - não houve novas quedas.
A espera terminou em 7 de junho - finalmente um problema atingiu o núcleo com slub_debug habilitado. Ao verificar a zona vermelha ao liberar o objeto slub_debug, encontrei dois bytes zero além de seu limite superior. Em outras palavras, descobriu-se que não era de uso posterior, o objeto anterior era novamente o culpado. Havia uma estrutura de aparência normal nf_ct_ext. Essa estrutura se refere ao rastreamento de conexão, uma descrição da conexão de rede que o firewall usa.
No entanto, ainda não estava claro por que isso estava acontecendo.
Comecei a perscrutar conntrack: em um dos containers alguém bateu na porta aberta 1720 usando ipv6. Por porta e protocolo, encontrei o nf_conntrack_helper correspondente.
static struct nf_conntrack_helper nf_conntrack_helper_q931[] __read_mostly = {
{
.name = "Q.931",
.me = THIS_MODULE,
.data_len = sizeof(struct nf_ct_h323_master),
.tuple.src.l3num = AF_INET, <<<<<<<< IPv4
.tuple.src.u.tcp.port = cpu_to_be16(Q931_PORT),
.tuple.dst.protonum = IPPROTO_TCP,
.help = q931_help,
.expect_policy = &q931_exp_policy,
},
{
.name = "Q.931",
.me = THIS_MODULE,
.tuple.src.l3num = AF_INET6, <<<<<<<< IPv6
.tuple.src.u.tcp.port = cpu_to_be16(Q931_PORT),
.tuple.dst.protonum = IPPROTO_TCP,
.help = q931_help,
.expect_policy = &q931_exp_policy,
},
};
Comparando as estruturas, percebi que o helper ipv6 não definia .data_len. Entrei no git para descobrir de onde veio, descobri um patch de 2012.
commit 1afc56794e03229fa53cfa3c5012704d226e1dec
Autor: Pablo Neira Ayuso <pablo@netfilter.org>
Data: Thu Jun 7 12:11:50 2012 +0200
netfilter: nf_ct_helper: implementar dados privados auxiliares de comprimento variável
.
Em vez de usar a união nf_conntrack_help que contém todas as
informações dos dados privados do auxiliar, alocamos uma
área de comprimento variável para armazenar os dados do auxiliar privado.
Este patch inclui a modificação de todos os ajudantes existentes.
Também inclui alguns cabeçalhos de inclusão para evitar a compilação
avisos.
O patch adicionou um novo campo .data_len ao helper que indicava quanta memória o manipulador para a conexão de rede correspondente exigia. O patch deveria definir .data_len para todos os nf_conntrack_helpers disponíveis naquele momento, mas falhou a estrutura que encontrei.
Como resultado, descobriu-se que a conexão via ipv6 com a porta aberta 1720 lançou a função q931_help (), ela gravou em uma estrutura para a qual ninguém havia alocado memória. Uma simples varredura de porta anulou alguns bytes, a transmissão de uma mensagem de protocolo normal preencheu a estrutura com informações mais significativas, mas em qualquer caso, a memória de outra pessoa foi desgastada e mais cedo ou mais tarde isso levou ao crash do nó.
Florian Westphal redesenhou o código novamente em 2017e removeu .data_len, e o problema que descobri passou despercebido.
Apesar do bug não ser mais encontrado na linha principal do kernel linux atual, o problema foi herdado pelos kernels de várias distribuições Linux, incluindo o ainda atual RHEL7 / CentOS7, SLES 11 e 12, Oracle Unbreakable Enterprise Kernel 3 e 4, Debian 8 e 9 e Ubuntu 14.04 e 16.04 LTS.
O bug foi reproduzido trivialmente no nó de teste, tanto em nosso kernel quanto no RHEL7 original. Segurança explícita: corrupção de memória gerenciada remotamente. Onde a porta 1720 ipv6 está aberta - praticamente ping da morte.
Em 9 de junho, fiz um patch de uma linha com uma descrição vaga e enviei para a linha principal. Enviei a descrição detalhada para o Red Hat Bugzilla e a escrevi separadamente para a Red Hat Security.
Outros eventos se desenvolveram sem minha participação.
Em 15 de junho, Zhenya Shatokhin lançou o patch live ReadyKernel para nossos kernels antigos.
https://readykernel.com/patch/Virtuozzo-7/readykernel-patch-131.10-108.0-1.vl7/
Em 18 de junho, lançamos um novo kernel estável em Virtuozzo e OpenVz.
https://virtuozzosupport.force.com/s/article/VZA-2020-043
Em 24 de junho, a Red Hat Security atribuiu um ID CVE ao bug
https://access.redhat.com/security/cve/CVE-2020-14305
Problema recebeu um impacto moderado com um CVSS v3 Pontuação 8.1 incomumente alto e, nos dias seguintes, outras distribuições do
SUSE responderam ao bug do chapéu público https://bugzilla.suse.com/show_bug.cgi?id=CVE-2020-14305
Debian https: / /security-tracker.debian.org/tracker/CVE-2020-14305
Ubuntuhttps://people.canonical.com/~ubuntu-security/cve/2020/CVE-2020-14305.html
Em 6 de julho, a KernelCare lançou um livepatch para as distribuições afetadas.
https://blog.kernelcare.com/new-kernel-vulnerability-found-by-virtuozzo-live-patched-by-kernelcare
Em 9 de julho, o problema foi corrigido nos kernels Linux estáveis 4.9.230 e 4.4.230.
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=linux-4.9.y&id=396ba2fc4f27ef6c44bbc0098bfddf4da76dc4c9 As
distribuições, no entanto, ainda não fecharam o buraco ...
“Olha, Kostya”, digo ao meu parceiro Kostya Khorenko, “nosso projétil atingiu a mesma cratera duas vezes! Eu e um acesso além do fim do objeto da última vez que encontrei nepoymi quando, e aqui, ele nos visitou duas vezes seguidas. Diga-me, é como uma probabilidade quadrada? Ou não quadrado?
- A probabilidade é quadrada, sim. Mas aqui você tem que olhar - qual evento é a probabilidade? A probabilidade quadrada do evento de que bugs incomuns foram encontrados exatamente 2 vezes consecutivas. É uma sequência.
Bem, Kostya é inteligente, ele sabe melhor.