Open Source

Ripgrep 15.2.0 apresenta falha de memória em binários musl em buscas massivas

Falha de segfault afeta o Ripgrep 15.2.0 compilado em musl ao varrer 1,8 milhão de arquivos em sistemas multi-core. Veja os detalhes da investigação técnica.

Compartilhar
Servidor de computação de alto desempenho processando dados de sistema
Servidor de computação de alto desempenho processando dados de sistema

O desenvolvedor dfoxfranke reportou uma falha crítica de degradação de memória (segmentation fault) no utilitário de busca em linha de comando ripgrep, registrada na issue #3494 do repositório oficial mantido por BurntSushi no GitHub. A falha ocorre especificamente no binário compilado para a arquitetura x86_64-unknown-linux-musl na versão 15.2.0 (revisão e89fff8), resultando no encerramento inesperado da aplicação por um sinal SIGSEGV durante varreduras em árvores de diretórios massivas executadas com alto nível de concorrência.

Servidor de computação de alto desempenho processando dados de sistema
Foto: Hacker News

A identificação inicial do problema deu-se por meio do binário do rg empacotado e distribuído junto à ferramenta OpenAI Codex. O pesquisador constatou que o executável fornecido no ambiente do Codex era idêntico, byte a byte, ao arquivo oficial disponibilizado nas compilações do projeto sob a denominação ripgrep-15.2.0-x86_64-unknown-linux-musl.tar.gz. A partir dessa constatação, dfoxfranke isolou o comportamento anômalo sem qualquer dependência direta do ecossistema da OpenAI.

Para aprofundar o diagnóstico técnico da falha sem mascarar o comportamento do código, dfoxfranke recompilou o ripgrep 15.2.0 com símbolos de depuração. A compilação utilizou a ferramenta de contêineres podman e o gerenciador de pacotes do Rust através do comando CROSS_CONTAINER_ENGINE=podman CARGO_PROFILE_RELEASE_DEBUG=true ~/.cargo/bin/cross build --release --target x86_64-unknown-linux-musl, executado em um sistema operacional OpenSUSE Tumbleweed Linux x86_64.

A análise da construção apontou que o binário 15.2.0 incorporava recursos de otimização de expressões regulares com a biblioteca PCRE2 10.45 (com compilação JIT habilitada). No que tange às instruções vetoriais, o executável foi compilado com o sinalizador +SSE2 (com -SSSE3 e -AVX2 em tempo de compilação), mas configurado para habilitar dinamicamente +SSE2, +SSSE3 e +AVX2 durante a execução.

O bug manifesta-se através de uma falha de asserção de integridade nos metadados de alocação de memória do mallocng, o gerenciador de heap moderno presente na biblioteca C alternativa musl. O disparo do sinal SIGSEGV ocorre de forma consistente dentro de uma chamada à função calloc, iniciada a partir do procedimento padrão opendir na leitura do sistema de arquivos.

Como o erro foi reproduzido

Para reproduzir o travamento do sistema de forma determinística, dfoxfranke criou um script em Python gerado com auxílio de um modelo de linguagem, denominado generate_repro_tree.py. Este script foi desenvolvido com o propósito de sintetizar uma estrutura de arquivos cujas métricas e distribuições estatísticas mimetizavam exatamente a estrutura do repositório onde a falha ocorreu originalmente no OpenAI Codex.

Ao ser executado, o script generate_repro_tree.py gerou uma árvore de diretórios massiva contendo aproximadamente 20 GiB de dados distribuídos ao longo de 1,8 milhão de arquivos (1.8M files). Esse enorme volume de dados e arquivos individuais provou ser um elemento indispensável para forçar a concorrência limite da ferramenta em tempo de execução.

Com a estrutura de testes montada a partir do diretório raiz da árvore de 20 GiB, o autor iniciou um loop contínuo de buscas em ambiente de terminal com o comando while true; do rg tnoheueunotshisnthukoethnsueothnsiuothonesuioseuinth; done. O termo pesquisado consistia em uma linha de texto arbitrária projetada para não estar presente em nenhum arquivo, forçando o ripgrep 15.2.0 a varrer 100% dos nós do sistema de arquivos.

O teste de estresse foi realizado em um hardware equipado com uma CPU de 24 núcleos (24-core system), configurado com memória RAM suficiente para garantir que a totalidade da árvore de arquivos de 20 GiB coubesse no block cache do kernel Linux. Sob essas condições de baixíssima latência de I/O de disco e máxima utilização paralela, a falha de SIGSEGV no ripgrep ocorria invariavelmente em cerca de um minuto de execução contínua.

Análise técnica do rastreamento de pilha

O relatório apresentado na issue #3494 traz o backtrace detalhado obtido a partir do core dump gerado pelo encerramento abrupto do binário do ripgrep. O ponto exato do travamento é registrado no frame #0 na função get_meta (), localizada no arquivo de cabeçalho ../src_musl/src/malloc/mallocng/meta.h na linha 141.

A sequência descendente da pilha mostra que get_meta () foi chamada pelo frame #1, na função __malloc_allzerop (), situada no arquivo ../src_musl/src/malloc/mallocng/malloc.c:384. Esta, por sua vez, foi acionada no frame #2 dentro de calloc (), no arquivo ../src_musl/src/malloc/calloc.c:41.

A requisição de memória no mallocng teve origem na biblioteca de chamadas de sistema POSIX, onde o frame #3 registra a rotina opendir () no arquivo ../src_musl/src/dirent/opendir.c:15. Essa função em C é a porta de entrada para a abertura de diretórios no Linux quando invocada pelas abstrações de I/O de baixo nível.

Do lado do ecossistema Rust, a chamada de opendir () origina-se no frame #4, na clausura do módulo de sistema de arquivos Unix da biblioteca padrão: std::sys::fs::unix::readdir::{closure#0}, localizado no código-fonte library/std/src/sys/fs/unix.rs:2081.

Antes de atingir a biblioteca C musl, a biblioteca padrão do Rust realiza a conversão e manipulação de rotas utilizando ajudantes de cadeias de caracteres C alocadas na pilha. Isso é demonstrado nos frames #5, #6 e #7, que correspondem respectivamente às rotinas small_c_string::run_with_cstr_stack, small_c_string::run_with_cstr e small_c_string::run_path_with_cstr no arquivo library/std/src/sys/helpers/small_c_string.rs.

A ponte entre o código de alto nível e a chamada de baixo nível no Rust é efetuada pelos frames #8, #9 e #10. Nestes pontos, a função std::sys::fs::unix::readdir repassa a requisição para std::sys::fs::read_dir e finalmente para std::fs::read_dir<&std::path::Path>, mapeado em library/std/src/fs.rs:3265 no ambiente do compilador (toolchain stable-x86_64-unknown-linux-gnu).

A biblioteca ignore e a concorrência

O processamento paralelo de diretórios que alimenta o ripgrep é estruturado sobre o crate ignore. No backtrace, o frame #11 aponta o método ignore::walk::Work::read_dir operando na estrutura interna localizada em crates/ignore/src/walk.rs:1551.

O gerenciamento das tarefas concorrentes pelos trabalhadores paralelos é demonstrado nos frames #12 e #13, com as execuções de ignore::walk::Worker::run_one (no arquivo crates/ignore/src/walk.rs:1749) e ignore::walk::Worker::run (no arquivo crates/ignore/src/walk.rs:1697). Essa arquitetura permite que os 24 núcleos do sistema processem simultaneamente as varreduras na árvore de 1,8 milhão de arquivos.

O ecossistema Rust gerencia o isolamento de travamentos de threads através de cláusulas de proteção visíveis nos frames #14 a #20. O código transita por std::sys::backtrace::__rust_begin_short_backtrace e pelos manipuladores de captura de pânico std::panicking::catch_unwind e std::panic::catch_unwind definidos em library/std/src/panicking.rs e library/std/src/panic.rs.

As operações de alocação de heap para o encadeamento de threads no Rust surgem no frame #23 com a implementação alloc::boxed::{impl#31}::call_once no arquivo library/alloc/src/boxed.rs:2275. Isso sustenta o modelo de despacho dinâmico de closures do tipo FnOnce enviadas entre threads no ripgrep 15.2.0.

Por fim, a inicialização das threads de sistema operacional no Linux é evidenciada nos frames #24, #25 e #26. O frame #24 invoca std::sys::thread::unix::{impl#2}::new::thread_start, que aciona a função start () em ../src_musl/src/thread/pthread_create.c:207 no frame #25 e culmina na instrução assembly __clone () em ../src_musl/src/thread/x86_64/clone.s:22 no frame #26.

Diferenças entre musl e glibc

A raiz desse comportamento em varreduras de 20 GiB está ligada às especificidades do alocador mallocng da biblioteca C musl, que difere fundamentalmente do alocador ptmalloc utilizado tradicionalmente pela glibc. O mallocng foi introduzido no ecossistema musl com o objetivo explícito de melhorar a segurança contra vazamentos de memória e corrupções de ponteiros.

Para atingir esses objetivos de segurança, o mallocng insere asserções rigorosas de verificação nos metadados do heap durante operações de alocação como calloc e malloc. Conforme demonstrado pela linha 141 de meta.h, o estouro do sinal SIGSEGV no ripgrep 15.2.0 ocorre no momento exato em que uma dessas asserções de integridade dos metadados detecta um estado inconsistente na memória dinâmica.

O relato feito por dfoxfranke no ticket #3494 indica que o alto grau de concorrência gerado pelos trabalhadores da biblioteca ignore em sistemas de 24 núcleos expõe uma condição de corrida ou uma corrupção nos metadados de memória gerenciados pelo mallocng ao executar requisições massivas e simultâneas de opendir.

Em contrapartida, as compilações do ripgrep direcionadas para o alvo x86_64-unknown-linux-gnu (que utilizam a biblioteca glibc) não costumam apresentar essa falha específica sob o mesmo volume de dados. No entanto, o binário estático compilado contra musl é amplamente preferido pela comunidade Open Source para distribuição em contêineres e ferramentas portáveis como o OpenAI Codex devido à ausência de dependências dinâmicas do sistema operacional.

Impacto no ecossistema e ferramentas

Para a comunidade de desenvolvimento e times de infraestrutura no Brasil e no exterior que integram o ripgrep 15.2.0 em fluxos de trabalho automatizados, sistemas CI/CD ou distribuições Docker baseadas em Alpine Linux (onde a biblioteca musl é padrão), a falha documentada no repositório BurntSushi/ripgrep acende um alerta sobre a estabilidade de binários musl em varreduras de larga escala.

A inclusão direta do executável afetado no pacote do OpenAI Codex evidencia como inconsistências em dependências de baixo nível podem se propagar para ferramentas avançadas de inteligência artificial e geração de código, afetando a confiabilidade de buscas automatizadas em repositórios com mais de 1,8 milhão de arquivos.

Como o autor da issue disponibilizou publicamente o core dump, o executável exato e o script de reprodução generate_repro_tree.py, os mantenedores do projeto ripgrep e os desenvolvedores da biblioteca musl dispõem de um ambiente de testes isolado para investigar se a correção deve ser desenvolvida na camada de alocação do mallocng ou no gerenciamento de diretórios do crate ignore.

O resultado esperado pelos usuários para as próximas edições do utilitário é a resolução definitiva dos disparos de SIGSEGV, garantindo que buscas intensivas com concorrência total em processadores multi-core operem de maneira estável no ambiente x86_64-unknown-linux-musl.

#ripgrep#rust#musl#linux#open-source
Compartilhar

Artigos Relacionados