44 CVEs em uutils: O que Rust pega e o que não pega na fronteira com o sistema

44 CVEs em uutils: O que Rust pega e o que não pega na fronteira com o sistema

Uma análise detalhada de 44 vulnerabilidades (CVEs) encontradas no uutils, uma reimplementação em Rust dos utilitários GNU coreutils. O artigo explora as limitações do Rust em relação à segurança, especialmente na interação com o sistema operacional, e destaca as áreas onde a linguagem não consegue prevenir certos tipos de erros.

MundiX News·12 de maio de 2026·15 min de leitura·👁 8 views

44 CVEs em uutils: O que Rust pega e o que não pega na fronteira com o sistema

Em abril de 2026, a Canonical revelou 44 CVEs no uutils, uma versão reescrita em Rust do GNU coreutils, que vem por padrão no Ubuntu 25.10. A revelação veio de uma auditoria externa encomendada antes do lançamento do 26.04 LTS. A maior parte das vulnerabilidades foi encontrada através de uma revisão de código comum. Nem o verificador de empréstimos (borrow checker), nem as verificações clippy, nem o cargo audit pegaram nenhuma delas.

Esta auditoria é talvez o exemplo mais claro existente do que Rust pega e o que não pega. A análise mais clara da lista foi feita por Matthias Endler em sua postagem "Bugs Rust Won’t Catch" de 29 de abril. Endler dirige a consultoria corrode e o podcast Rust in Production; recentemente, ele recebeu John Seager, vice-presidente de engenharia da Canonical. A postagem é construída como uma análise da própria revelação: as 44 CVEs são distribuídas em oito categorias; a maioria delas tem um git diff de correção.

Abaixo, analisarei a estrutura de Endler e adicionarei dois argumentos. O primeiro: um dos mantenedores do GNU coreutils no tópico do HN mostrou um benchmark no qual a correção recomendada por Endler não sobrevive. O segundo: um argumento estrutural sobre o que 40 anos de cicatrizes POSIX acumuladas fazem com qualquer reescrita, independentemente da linguagem.

O que Rust pegou e o que não pegou

Endler lista oito categorias. A forma é sempre a mesma: um idioma Rust, aprovado pelo sistema de tipos, é aplicado em um contexto onde o sistema de tipos não vê o que está errado.

TOCTOU em caminhos

Este é o maior cluster. Por causa dele, cp, mv e rm no 26.04 LTS ainda são do GNU, e não do uutils. O padrão é o seguinte: uma chamada de sistema verifica, a segunda age, ambas aceitam &Path. Entre elas, um invasor com permissões de gravação no diretório pai substitui o componente do caminho por um link simbólico, o kernel resolve novamente o caminho na segunda chamada, e a ação privilegiada aterrissa no endereço escolhido pelo invasor.

O exemplo mais claro é CVE-2026-35355 em install:

// 1. Remove o arquivo de destino
fs::remove_file(to)?;
// ...
// 2. Resolve o caminho novamente. Segue o link simbólico, corta.
let mut dest = File::create(to)?;
copy(from, &mut dest)?;

Qualquer usuário com permissões de gravação no diretório pai entre as etapas 1 e 2 pode substituir to por um link simbólico que leva a /etc/shadow. O processo privilegiado irá alegremente sobrescrevê-lo com o conteúdo de from. É tratado através de OpenOptions::create_new(true). A documentação desta chamada diz diretamente:

"nenhum arquivo deve existir no caminho de destino, incluindo (links simbólicos pendurados)".

Permissão definida após a criação

Um parente próximo de TOCTOU. O padrão fs::create_dir(&path)?; fs::set_permissions(&path, ...)?; deixa uma janela na qual o diretório já existe com as permissões padrão, e qualquer usuário local pode chamar open() e obter um descritor de arquivo que sobreviverá ao chmod subsequente. É tratado através de OpenOptions::mode() e DirBuilderExt::mode(), para que o arquivo ou diretório seja criado imediatamente com as permissões necessárias.

Igualdade de strings de caminho não é igual à identidade no sistema de arquivos

Inicialmente, a verificação --preserve-root em chmod parecia literalmente assim:

if recursive && preserve_root && file == Path::new("/") { return Err(PreserveRoot); }

Esta verificação é contornada por tudo o que resolve em /, mas é escrito de forma diferente: /../, /./, um link simbólico, qualquer coisa que canonicalize reduza à raiz. Execute chmod -R 000 /../ e observe como todo o sistema é bloqueado.

O caso mais absurdo desta categoria é CVE-2026-35363:

rm .    # ❌ rejeitado
rm ..   # ❌ rejeitado
rm ./   # ✅ aceito
rm ./// # ✅ aceito

rm rejeitou . e .., mas aceitou ./, excluiu o diretório atual e depois imprimiu "Entrada inválida". A comparação de strings tropeçou na barra final.

UTF-8 vs bytes brutos nas fronteiras do Unix

Em Rust, os tipos String e &str são sempre UTF-8. Caminhos Unix, variáveis de ambiente e fluxos de bytes que passam por cut, comm, tr, não garantem UTF-8. Onde quer que um programa Rust encontre isso, ele tem três opções: conversão com perdas (substitui silenciosamente bytes inválidos por U+FFFD; Endler chama isso de "dano elegante aos dados"), conversão estrita (falha no primeiro byte não UTF-8) ou permanecer em OsStr / &[u8]. A auditoria encontrou bugs em ambas as primeiras opções.

CVE-2026-35346 em comm usou String::from_utf8_lossy, e passar um arquivo binário por comm distorceu silenciosamente a saída. A correção substituiu print! por BufWriter::write_all e deixou os dados em bytes.

Pânico como DoS

Qualquer unwrap, qualquer expect, qualquer indexação de fatia, qualquer aritmética sem verificações no código que processa a entrada pode se transformar em uma negação de serviço se o invasor controlar a forma da entrada.

CVE-2026-35348 em sort --files0-from chamou expect() ao converter UTF-8 de cada nome de arquivo:

$ python3 -c "open('list0','wb').write(b'weird\xffname\0')"
$ coreutils sort --files0-from=list0
thread 'main' panicked at uu_sort-0.2.2/src/sort.rs:1076:18:
Could not parse string from zero terminated input.

GNU sort trata nomes de arquivos como bytes brutos, porque os nomes de arquivos são bytes. A versão uutils falha no primeiro caminho não UTF-8. Nas palavras de Endler:

"sua tarefa cron noturna está morta, adeus fim de semana".

Erros ignorados

chmod -R e chown -R retornaram o código de saída do último arquivo processado, e não o pior. chmod -R 600 /etc/secrets/* pode ter falhado em metade dos arquivos e sair com o código 0.

dd descartou o erro de seu set_len() através de Result::ok(), para reproduzir o comportamento do GNU para /dev/null. Mas o mesmo código funcionou em arquivos regulares, e o disco lotado interrompeu silenciosamente a gravação no meio do arquivo receptor.

Incompatibilidade de comportamento com GNU

Endler observa:

"Uma parte notável dessas CVEs não é sobre o código fazer algo inseguro, mas sobre o código fazer algo diferente do GNU, e algum script de shell em algum lugar dependia exatamente do comportamento do GNU".

O caso mais marcante: CVE-2026-35369 em kill. GNU lê kill -1 como "enviar o sinal número 1 para o PID especificado". uutils lê o mesmo como "enviar o sinal padrão para o processo com PID -1", o que no Linux significa cada processo que você vê. Um erro de digitação se transforma em um interruptor em todo o sistema.

Resolver antes de cruzar a fronteira

CVE-2026-35368 é o bug individual mais sério na auditoria. É root local via chroot. Um padrão simplificado:

chroot(new_root)?;
// Ainda uid 0, mas já dentro do sistema de arquivos do invasor.
let user = get_user_by_name(name)?;
setgid(user.gid())?;
setuid(user.uid())?;
exec(cmd)?;

get_user_by_name passa pelo NSS, e o NSS chama dlopen para os módulos libnss_* em tempo de execução. Após chroot, esses módulos são carregados do novo diretório raiz. Um invasor que conseguiu colocar um arquivo em chroot executa seu código com uid 0. GNU chroot resolve o usuário antes de chroot. A correção é exatamente a mesma. A vinculação estática não ajuda: o NSS é dinâmico em qualquer caso.

Estas são oito classes e 44 CVEs. Nenhum deles é pego pelo verificador de empréstimos.

O que os mantenedores do GNU coreutils responderam

A thread no HN sob a postagem de Endler se estendeu por 361 comentários. O comentário mais útil está em primeiro lugar. Um dos mantenedores do GNU coreutils contesta uma das regras de Endler e mostra o que isso custa.

Endler recomenda corrigir bugs com igualdade de strings de caminho através de fs::canonicalize: descompactar .., ., e links simbólicos no caminho absoluto real antes da comparação. O mantenedor tem um benchmark específico na objeção em um diretório profundamente aninhado:

$ mkdir -p $(yes a/ | head -n $((32 * 1024)) | tr -d '\n')
$ while cd $(yes a/ | head -n 1024 | tr -d '\n'); do :; done 2>/dev/null
$ echo a > file
$ time cp file copy
real 0m0.010s
$ time uu_cp file copy
real 0m12.857s

GNU cp funciona em 10 milissegundos. A versão uutils com lógica canonicalize lida com o mesmo em quase treze segundos. 1200 vezes mais lento em um caminho artificial, mas plausível. O pensamento geral do mantenedor: GNU "trabalha duro para evitar limites arbitrários". É nas entradas patológicas que os scripts de produção tropeçam, e um cp de 12 segundos em uma árvore de compilação profunda já se transforma em um incidente.

O mesmo comentário também corrige uma das afirmações mais fortes de Endler. A postagem diz:

"A reescrita em Rust não enviou nenhum desses bugs [de segurança de memória] por um período de atividade comparável".

O mantenedor aponta para GHSA-w9vv-q986-vj7x, um aviso real de segurança de memória em Rust uutils. A vulnerabilidade foi corrigida antes do lançamento do LTS, mas "zero" agora está em questão, e a formulação de Endler enfraquece com isso.

A thread não fecha o argumento de Endler. As categorias de bugs de fronteira que ele lista são reais. Mas a declaração categórica sobre a vitória sobre a segurança da memória muda de "zero, ponto" para "perto de zero, com ressalvas". A diferença é menor do que o que sustenta a disputa "reescrever ou deixar"; mas isso não é mais um slogan, mas uma medida.

Argumento estrutural: quarenta anos de cicatrizes POSIX

A segunda ramificação da thread do HN afirma que o eixo "Rust vs. C" não é o certo.

Um dos participantes da thread formulou da seguinte forma:

"eles leem a assinatura da função, mas precisam das cicatrizes".

A linha do GNU coreutils se estende desde os pacotes fileutils / textutils / shellutils do final dos anos 80 (combinados em coreutils em 2002). Atrás dela estão décadas de correções pontuais: estouro de buffer em pwd em caminhos mais longos que 2 * PATH_MAX, gravação fora dos limites do heap em od --strings -N, leitura de memória não alocada em b2sum --check. Cada um deles armazena uma grande camada de conhecimento do tipo "não faça isso, aqui está o porquê", e esse conhecimento vive na base de código com cicatrizes, e não com documentação. A reescrita do zero ganha parte dessas cicatrizes novamente nas máquinas dos usuários.

A formulação de outro comentarista é mais dura:

"Eles sabiam escrever em Rust, mas claramente não sabiam o suficiente sobre a API Unix, sua semântica e armadilhas. A maioria desses erros são extremamente amadores do ponto de vista daqueles que trabalham com coreutils há muito tempo".

Um terceiro transforma isso em uma reclamação sobre o processo:

"A reescrita inevitavelmente tem ordens de magnitude mais bugs e vulnerabilidades do que o código que foi mantido por décadas. O argumento de segurança só funcionou para uma transição de longo prazo, e não para uma apressada".

Há também um contra-argumento, e a postagem de Endler é sua versão mais forte. A reescrita em Rust não enviou uma classe de bugs de segurança de memória. A lista de CVEs da auditoria não contém estouros de buffer, nem uso após liberação, nem liberação dupla, nem condições de corrida de dados em estado mutável compartilhado, nem desreferências de ponteiro nulo, nem leituras de memória não inicializada. E o GNU coreutils teve CVEs em cada uma dessas categorias nos últimos anos:

Classe de vulnerabilidade (segurança de memória)GNU coreutils CVE
Estouro de buffer (pwd, caminhos profundos > 2×PATH_MAX)9.11, 2026
Leitura OOB (numfmt, espaços em branco finais)9.9, 2025
Estouro de buffer de heap (unexpand --tabs)9.9, 2025
Gravação fora do buffer no heap (od --strings -N)9.8, 2025
Leitura de um byte antes do buffer no heap (sort com deslocamento da chave SIZE_MAX)9.8, 2025
Sobrescrita de heap (split --line-bytes)CVE-2024-0684, 9.5, 2024
Estouro de buffer de pilha (tail -f com um grande ulimit -n)9.0, 2021

Seja o que for que você pense sobre a decisão da Canonical de colocar o uutils por padrão no Ubuntu, a comparação que Endler oferece é justa. A lista de CVEs da reescrita em Rust é lida como uma classe diferente de falha em comparação com a lista do GNU. A superfície de ataque é a mesma, utilitários de sistema privilegiados, e a classe de bugs já é diferente.

Onde a fronteira de segurança mudou

Se você colocar todas as 44 CVEs lado a lado, a imagem não é "Rust não pegou bugs". A imagem é diferente:

"Rust pegou a classe de bugs que pega, e a auditoria encontrou a próxima ao lado".

A próxima classe vive na fronteira entre o programa Rust e o sistema operacional. Endler a descreve diretamente:

"Ela passa na junção entre nosso ambiente controlado Rust e o mundo exterior sujo, onde caminhos, bytes, strings e chamadas de sistema são enrolados em um único emaranhado eterno de tristeza. Esta é a nova fronteira de segurança do código de sistema moderno".

A biblioteca padrão do Rust é feita em vários lugares de forma que o caminho de menor resistência não esteja do outro lado dessa fronteira. Parâmetros &Path em todos os lugares; String onde na verdade é uma sequência de bytes; chamar unwrap ou expect não custa nada. APIs em descritores (openat, fstatat, unlinkat) existem em todos os Unix; std::fs não os coloca em primeiro plano.[^1]

Esta não é uma crítica à linguagem Rust. É uma observação sobre o design de std::fs, e essa decisão em si se estende às restrições multiplataforma da biblioteca padrão Rust 1.0. Um comentarista no HN formulou da seguinte forma:

"std::fs sofre porque é o menor denominador comum. A linguagem precisava de algo para 1.0, e, infelizmente, esse algo permaneceu".

A mesma forma se repete em quase todas as linguagens com uma abstração portátil sobre o sistema de arquivos; esta não é uma característica apenas do Rust. Mas o argumento a favor da reescrita em Rust no espírito de "reescrevemos em Rust, então é mais seguro" depende de a linguagem fazer mais do que std::fs atualmente faz na fronteira com chamadas de sistema.

A própria história sobre a segurança do Rust permanece real e útil. Ela simplesmente termina na chamada do sistema.

As categorias da auditoria (TOCTOU, definição de permissões após a criação, igualdade de strings de caminho, UTF-8 vs bytes, pânico como DoS, erros ignorados, compatibilidade com GNU, resolução antes de cruzar a fronteira) são a nova lista de verificação para qualquer reescrita de um utilitário privilegiado em Rust. Nenhum deles é pego pelo compilador. Todos são pegos na revisão do código por uma pessoa que leu esta lista.

O que Rust pega e o que não pega: lista de verificação

Se você encaixar as conclusões da auditoria em uma página:

Classe de bugsO que procurar no códigoO que usar em vez de
TOCTOU em caminhosduas chamadas de sistema consecutivas por &PathOpenOptions::create_new, *at-família de chamadas de sistema
Permissão definida após a criaçãocreate_dir + set_permissionsOpenOptions::mode(), DirBuilderExt::mode()
Igualdade de strings de caminhopath == Path::new("/"), == diretofs::canonicalize (com uma ressalva sobre desempenho)
UTF-8 vs bytes brutosfrom_utf8_lossy, print! em dados do usuárioOsStr, &[u8], BufWriter::write_all
Pânico como DoSunwrap, expect, indexação de fatia na entradaRetorno Result, verificações de limites
Erros ignoradosResult::ok() sem processamento, código de saída do últimoagregação de erros, extremo em vez do último
Incompatibilidade com GNUcomportamento diferente do GNU em casos de fronteiraexecutar o conjunto de testes do GNU coreutils
Resolver após cruzar a fronteirachroot / setuid antes da resolução de recursosresolver tudo antes de cruzar

A lista não é complicada, é curta e fácil de procurar.

O que significa "reescrevemos em Rust"

A mesma conclusão segue tanto da postagem de Endler quanto da troca de opiniões na thread do HN. A frase "reescrevemos em Rust" é uma declaração coerente sobre uma classe específica de bugs. Por si só, não é uma declaração sobre a segurança da ferramenta reescrita. É uma declaração sobre a ausência de classes de bugs que o sistema de tipos Rust pega. As classes que o sistema de tipos não pega (oito peças, conforme visto pelas 44 CVEs) devem ser pegas por outra coisa.

A lista de verificação de Endler é a forma correta desse "outra coisa". Procuramos na base de código por from_utf8_lossy, unwrap() descuidado, Result ignorado, File::create, comparações de strings com "/", operações "criar diretório e, em seguida, definir permissões". Procurar por tudo isso não é mais difícil do que um bug de violação de segurança de memória. Isso requer um tipo diferente de auditoria e um revisor diferente: aquele que tem 40 anos de cicatrizes POSIX servindo como documentação que o upstream não escreveu.

44 CVEs não são um veredicto sobre Rust, mas uma medida de onde olhar na próxima auditoria. A frase "reescrevemos em Rust" deve ser lida da mesma forma que a frase "temos testes de unidade": uma declaração útil sobre uma classe específica de falhas, sem reivindicações sobre as outras.

Uma nota de rodapé importante de Endler: a classe de bugs "caminho versus descritor" é, de certa forma, mais fácil de evitar em C do que em Rust. O código C naturalmente tende ao descritor de arquivo aberto e à família de chamadas de sistema *at (openat, fstatat, unlinkat, mkdirat), e a maioria das chamadas de sistema de criação aceitam o modo diretamente. As APIs de alto nível em std::fs abstraem o descritor de arquivo e trabalham com valores &Path, portanto, a chamada de resolução repetida via caminho se torna a opção mais fácil. As APIs em descritores existem em todas as plataformas Unix; Rust simplesmente não as coloca em primeiro plano.

Tags: rust, cve, uutils, coreutils, canonical, ubuntu, toctou, segurança, programação de sistemas

🛡️⚡

Pare de pesquisar. Comece a hackear.

O MundiX é seu copiloto de pentest com IA: comandos exatos, análise de outputs e próximo passo na kill chain — em segundos.

Testar grátis por 7 dias →

Sem cartão para começar · Planos a partir de R$49/mês

📤 Compartilhar & Baixar

📩 Newsletter MundiX

Receba novidades de cibersegurança + um checklist de pentest grátis. Sem spam.

Ao assinar você concorda em receber e-mails. Cancele quando quiser.