- O binário x86_64-unknown-linux-musl do Ripgrep 15.2.0 encerra intermitentemente com
SIGSEGVao pesquisar árvores de arquivos grandes com alta concorrência - A falha ocorre dentro de
calloc, chamado poropendir, e o ponto de verificação de integridade dos metadados de heap do musl mallocng aparece no topo do rastreamento de pilha - O ambiente de reprodução é uma árvore com cerca de 20 GiB e 1,8 milhão de arquivos, repetindo buscas com
rgpor uma string inexistente - Em um sistema de 24 núcleos, se houver RAM suficiente para que a árvore de busca caiba no cache de blocos do kernel, o problema geralmente aparece em cerca de 1 minuto
- O problema foi reproduzido de forma independente não só no
rgincluído no OpenAI Codex, mas também em um binário idêntico byte a byte ao release oficial, confirmando que se trata de um problema não relacionado às dependências do Codex
Ambiente de ocorrência
- A versão usada é ripgrep 15.2.0 rev
e89fff8, com o recurso+pcre2- SIMD na compilação:
+SSE2,-SSSE3,-AVX2 - SIMD em execução:
+SSE2,+SSSE3,+AVX2 - PCRE2 10.45 e JIT disponíveis
- SIMD na compilação:
- O sistema operacional é OpenSUSE Tumbleweed Linux x86_64
- O
rgempacotado no OpenAI Codex, onde o problema foi visto pela primeira vez, é idêntico byte a byte ao release oficial x86_64-unknown-linux-musl - Separadamente do Codex, o problema também foi reproduzido no binário oficial, e o binário usado para análise foi compilado com símbolos de depuração usando o seguinte comando
CROSS_CONTAINER_ENGINE=podman CARGO_PROFILE_RELEASE_DEBUG=true ~/.cargo/bin/cross build --release --target x86_64-unknown-linux-musl
Passos para reproduzir
- generate_repro_tree.py gera uma árvore aleatória de arquivos imitando as estatísticas do repositório original onde o problema ocorreu
- Esse programa foi escrito por um LLM
- O resultado gerado tem cerca de 20 GiB e 1,8 milhão de arquivos
- Na raiz da árvore gerada, repete-se a busca por uma string aleatória inexistente
while true; do rg tnoheueunotshisnthukoethnsueothnsiuothonesuioseuinth; done
- Foi observado que uma árvore de busca suficientemente grande é essencial para reproduzir o problema
- Em um sistema de 24 núcleos, se houver RAM livre suficiente para que toda a árvore caiba no cache de blocos do kernel, a falha geralmente acontece após cerca de 1 minuto
Ponto da falha
- O resultado real é um
SIGSEGVque deixa core dump - O topo do rastreamento de pilha é
get_metado musl mallocng, e a falha ocorre no ponto de verificação de integridade dos metadados de heap - O fluxo de chamadas segue de
opendirparacalloc, depois para a travessia de diretórios da biblioteca padrão do Rust e para o workerignore::walkdo ripgrepget_meta→__malloc_allzerop→calloc→opendirstd::fs::read_dir→ignore::walk::Work::read_dir→ignore::walk::Worker::run
- Como material de análise, foram anexados um core dump e o binário rg correspondente
Comportamento esperado e estado atual
- O comportamento esperado é executar sem falha de segmentação, mesmo em buscas grandes e altamente concorrentes
- O conteúdo fornecido não inclui confirmação da causa, correção proposta, resultado da análise ou status final de resolução
1 comentários
Opiniões no Hacker News
Há um trecho interessante no patch do kernel: https://lore.kernel.org/all/CALCETrXbj__SFQMzPZhES5y6-sh4np-...
Dois anos atrás, uma análise dessas teria sido vista como o resultado de alguém doando generosamente seu tempo à comunidade, mas agora, sabendo a origem e que o custo em tokens foi de apenas US$ 0,06, ninguém quer ler. O fato de ela prenunciar o que vem pela frente também contribui.
Daqui a alguns anos, investigar bugs manualmente será o último recurso, e outros agentes de IA lerão os relatórios para validar as correções. Assim como ignoramos assembly gerado por compilador e usuários de celular em público, parece que vamos passar da fase de zombaria para a fase de indiferença.
Entendo usar o alocador padrão do musl por conveniência, mas é estranho não trocar por um alocador mais rápido em uma aplicação cujo propósito é justamente velocidade.
O mallocng sofre com contenção em múltiplas threads. Uma aplicação que normalmente era limitada por E/S passou a ter gargalo em
malloccom apenas 8 threads quando compilada com musl; ao trocar para mimalloc, o desempenho ficou 20 vezes melhor, chegando muito perto da configuração padrão com glibc, embora ainda um pouco mais lento que glibc+mimalloc.De fato há um problema interessante aí, mas ele nem deveria ter aparecido dessa forma.
opendir, da libc musl. A forma como o Rust substitui o alocador usado por ele não troca o alocador do processo inteiro, apenas o alocador chamado pelo código Rust.Ainda assim, se for preciso passar por um alocador que usa lock global, parece melhor o ripgrep evitar usar
opendirda libc.Se você está rodando ripgrep em um cluster HPC contra um sistema de arquivos de cluster em grande escala, pare imediatamente e redesenhe o fluxo de trabalho. Esse tipo de tarefa gera uma enorme quantidade de pequenas operações de E/S, que são o calcanhar de Aquiles de sistemas de arquivos de cluster em grande escala.
Você acaba empurrando para a camada de metadados do sistema de arquivos um trabalho que deveria ser feito na hierarquia de memória de alta largura de banda do cluster; bastam alguns usuários executando isso ao mesmo tempo para paralisar todo o sistema de arquivos de alta largura de banda.
Se houver qualquer verdade em https://isolveproblems.substack.com/p/how-microsoft-vaporize..., basta um único caminho não otimizado na abstração de sistema de arquivos do Azure para que um pico de uso se transforme em um raio de impacto enorme.
Talvez fosse melhor linkar diretamente a análise do bug do kernel: https://github.com/dfoxfranke/ripgrep-3494-analysis
Headline.Ele vem com frases como: “um valor armazenado por uma thread em uma página anônima recém-paginada desaparece ao ser relido pela mesma thread cerca de 10 instruções depois, e o backing da página é trocado durante a execução da função”, “ao ler o pagemap no momento da falha, o backing é a zero page do kernel”, “o mecanismo é localizado na interação entre o caminho rápido de falha anônima com lock por VMA e o TLB shootdown de um
munmapconcorrente”.É difícil entender o que é o
backingde uma página, o que significamfreshly-faultedou “cerca de 10 instruções depois”, ou como um mecanismo é “localizado”. Parece mais um amontoado de palavras do que uma explicação técnica.Um exemplo de documentação técnica bem-feita é este: https://yifan.lu/2019/01/11/the-first-f00d-exploit/
A conclusão parece ser, grosso modo, que há um problema na combinação Linux 7.0 e musl 1.2.5, mas a reprodução ainda só funciona intermitentemente, sob carga pesada, no mesmo CPU Threadripper físico, então não dá para descartar problema de hardware.
Parece uma condição de corrida complexa causada por uma migração de CPU em um momento estranho, ou um bug em que o caminho de remoção da tabela de páginas expôs temporariamente uma PTE incorreta. Também não acho que o PFN da zero page seja 0.
Se eu tivesse que chutar, diria que, durante a remoção direta da tabela de páginas, foi permitido que a CPU lesse uma tabela já liberada e reutilizada por meio de uma entrada em cache de uma estrutura de paginação superior. Já depurei esse tipo de problema antes, e é horrível.
Por que o bug acontece só na musl libc e não em outras libc?
Normalmente eu suspeitaria do tamanho da stack das threads no musl, mas fiquei curioso para saber se isso foi confirmado como bug do kernel.