1 pontos por GN⁺ 1 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • A segurança de memória melhora bastante, mas mesmo em código Rust de produção os problemas de tratamento nas fronteiras do sistema continuam existindo e podem levar a vulnerabilidades
  • Fluxos que reinterpretam o mesmo caminho em várias syscalls, mudam permissões depois da criação e fazem comparação de caminhos baseada em string tendem a criar problemas como TOCTOU e exposição indevida de permissões
  • Em Unix, caminhos, variáveis de ambiente e dados de stream circulam como bytes brutos, então um tratamento centrado em String ou o uso de from_utf8_lossy, unwrap e expect pode causar corrupção de dados ou DoS
  • Se erros forem descartados, falhas podem parecer sucesso, e diferenças de comportamento em relação ao GNU coreutils também podem virar problemas de segurança imediatamente em shell scripts e ferramentas privilegiadas
  • Nesta auditoria, não apareceram bugs da família de segurança de memória, como buffer overflow, use-after-free e double-free; os principais riscos restantes estavam concentrados menos dentro do Rust e mais nas fronteiras com o mundo externo

Os limites do Rust expostos pela auditoria

  • Os 44 CVEs divulgados pela Canonical no uutils mostram que, mesmo em código Rust de produção, ainda podem restar vulnerabilidades que borrow checker, clippy e cargo audit não conseguem detectar
  • O centro do problema estava menos na segurança de memória e mais no tratamento das fronteiras do sistema
    • Havia diferença de tempo entre caminho e syscall
    • Havia desencontro entre dados em bytes do Unix e strings UTF-8
    • Havia diferenças de comportamento em relação à ferramenta original
    • Havia omissões no tratamento de erros e encerramentos com panic!
  • Essa lista de CVEs mostra de forma condensada onde a segurança termina em código de sistema escrito em Rust

Reinterpretar o caminho duas vezes cria TOCTOU

  • Se o mesmo caminho é verificado em uma syscall e depois usado novamente em outra, isso facilmente leva a uma vulnerabilidade de TOCTOU
    • Entre as duas chamadas, um atacante com permissão de escrita no diretório pai pode trocar componentes do caminho por um link simbólico
    • Na segunda chamada, o kernel reinterpreta o caminho desde o início, fazendo a operação privilegiada apontar para um alvo escolhido pelo atacante
  • A API std::fs do Rust usa por padrão a reinterpretação baseada em &Path, o que facilita esse tipo de erro
  • Em CVE-2026-35355, foi explorado um fluxo que apaga um arquivo e depois cria um novo no mesmo caminho
    • Em src/uu/install/src/install.rs, havia fs::remove_file(to)? seguido de File::create(to)?
    • Se entre a remoção e a criação to for trocado por um link simbólico apontando para um alvo como /etc/shadow, um processo privilegiado pode sobrescrever esse arquivo
  • A correção passou a usar OpenOptions::create_new(true) para criar apenas arquivos novos
    • Segundo a documentação, create_new não permite nem arquivos existentes nem symlinks quebrados no local de destino
  • Se for necessário operar duas vezes sobre o mesmo caminho, o mais seguro é fixar-se em um descritor de arquivo
    • Fora do caso de criação de arquivo novo, o ideal é abrir o diretório pai uma vez e trabalhar com caminhos relativos a partir desse handle
    • Se você opera duas vezes sobre o mesmo caminho, deve assumir TOCTOU até prova em contrário

Permissões não devem ser alteradas depois da criação

  • Criar um diretório ou arquivo com permissões padrão e só depois aplicar chmod também cria uma pequena janela de exposição
    • Se você escreve algo como fs::create_dir(&path)? e depois fs::set_permissions(&path, Permissions::from_mode(0o700))?, nesse intervalo path existe com as permissões padrão
    • Outro usuário pode fazer open() durante essa janela, e mesmo que o chmod venha depois, descritores de arquivo já obtidos não são revogados
  • As permissões devem ser definidas no momento da criação
    • Use OpenOptions::mode() e DirBuilderExt::mode() para que o objeto já nasça com as permissões desejadas
    • O kernel ainda aplica umask, então, se esse impacto for importante, umask também precisa ser tratado explicitamente

Comparar strings de caminho não é o mesmo que identidade no sistema de arquivos

  • A checagem inicial de --preserve-root em chmod fazia apenas comparação de string
    • recursive && preserve_root && file == Path::new("/")
    • Entradas que na prática apontavam para a raiz, mas cuja string não era /, como /../, /./, /usr/.. ou um link simbólico para /, contornavam a checagem
  • A correção passou a usar fs::canonicalize para resolver o caminho para o caminho absoluto real antes da comparação
    • PR da correção
    • canonicalize resolve .., . e links simbólicos, retornando o caminho real
  • No caso de --preserve-root, esse método funciona porque / não tem diretório pai
  • Para comparar em geral se dois caminhos arbitrários apontam para o mesmo objeto no sistema de arquivos, deve-se comparar não a string, mas (dev, inode)
    • O GNU coreutils também usa essa abordagem
  • Em CVE-2026-35363, rm rejeitava . e .., mas aceitava ./ e .///, permitindo apagar o diretório atual
    • Quando a diferença de entrada é tratada só no nível da string, a checagem é facilmente contornada

Nas fronteiras Unix, bytes vêm antes de strings

  • String e &str em Rust são sempre UTF-8, mas caminhos, variáveis de ambiente, argumentos e dados de stream no Unix vivem no mundo dos bytes brutos
  • Escolhas erradas ao atravessar essa fronteira levam a duas categorias de bug
    • Conversões com perda, como from_utf8_lossy, trocam bytes inválidos por U+FFFD e corrompem dados silenciosamente
    • Conversões rígidas, como unwrap ou ?, podem rejeitar a entrada ou encerrar o processo
  • O CVE-2026-35346 do comm foi um caso em que a conversão com perda estragava a saída
    • Em src/uu/comm/src/comm.rs, os bytes de entrada ra e rb eram convertidos com String::from_utf8_lossy e impressos com print!
    • O GNU comm preserva os bytes mesmo em arquivos binários, mas o uutils trocava UTF-8 inválido por U+FFFD, corrompendo a saída
    • A correção foi usar BufWriter e write_all para escrever os bytes brutos diretamente em stdout
  • print! passa por Display e força uma ida e volta em UTF-8, enquanto Write::write_all não faz isso
  • Em código de sistema para Unix, é preciso usar o tipo adequado para cada caso
    • Para caminhos de arquivo: Path, PathBuf
    • Para variáveis de ambiente: OsString
    • Para conteúdo de streams: Vec<u8> ou &[u8]
  • Passar por String por conveniência de formatação facilita a entrada silenciosa de corrupção de dados

Todo panic pode virar negação de serviço

  • Em CLIs, unwrap, expect, indexação de slices, aritmética sem verificação e from_utf8 podem se tornar pontos de DoS quando o atacante controla a entrada
    • panic! faz unwind da stack e interrompe o processo
    • Se estiver rodando em um cron job, pipeline de CI ou shell script, a tarefa inteira pode parar
    • Em ambientes de execução repetida, isso pode até causar crash loops e paralisar o sistema
  • Em CVE-2026-35348 de sort --files0-from, o processo parava ao encontrar nomes de arquivo fora de UTF-8 em uma lista separada por NUL
    • O parser chamava std::str::from_utf8(bytes).expect(...) para cada nome
    • O GNU sort trata nomes de arquivo como bytes brutos, como o kernel, mas o uutils forçava UTF-8 e abortava o processo no primeiro caminho não UTF-8
  • Em código que lida com entrada não confiável, unwrap, expect, indexação e cast com as devem ser vistos como CVEs em potencial
    • O ideal é usar ?, get, checked_* e try_from, propagando o erro real ao chamador
  • Também foram sugeridas regras de clippy para pegar isso em CI
    • unwrap_used
    • expect_used
    • panic
    • indexing_slicing
    • arithmetic_side_effects
  • Em código de teste, esses alertas podem ser exagerados, então faz sentido restringi-los ao escopo cfg(test)

Descartar erros faz falha parecer sucesso

  • Alguns CVEs surgiram de erros ignorados ou de fluxos em que a informação de erro se perdia
  • chmod -R e chown -R retornavam apenas o código de saída do último arquivo processado
    • Mesmo que vários arquivos anteriores tivessem falhado, se o último desse certo o comando podia terminar com 0
    • Scripts poderiam interpretar isso como uma execução totalmente bem-sucedida
  • Para imitar o comportamento do GNU em /dev/null, dd chamava set_len() e então Result::ok() no resultado
    • A intenção era descartar erros apenas em uma situação limitada, mas o mesmo código acabou valendo também para arquivos comuns
    • Mesmo com o disco cheio, um arquivo de destino parcialmente gravado podia ficar para trás sem aviso
  • Quando se descarta Result com .ok(), .unwrap_or_default() ou let _ =, perde-se a causa de falhas importantes
  • Mesmo que o programa não interrompa na primeira falha, ele deve lembrar o código de erro mais grave e encerrar com ele
  • Se for realmente necessário descartar um Result, o código deve registrar por que essa falha pode ser ignorada com segurança

Compatibilidade exata com a ferramenta original também é um recurso de segurança

  • Vários CVEs não surgiram porque o código executava operações perigosas, e sim porque ele se comportava de forma diferente do GNU
    • Shell scripts reais dependem do comportamento do GNU, então diferenças de semântica acabam virando problemas de segurança
  • CVE-2026-35369 em kill -1 é o exemplo mais claro
    • O GNU interpreta -1 como signal 1 e exige um PID
    • O uutils interpretava isso como enviar o sinal padrão ao PID -1
    • No Linux, PID -1 significa todos os processos visíveis, então um simples erro de digitação podia virar uma finalização em massa do sistema
  • Em ferramentas reimplementadas, a compatibilidade bug-for-bug vira uma proteção que inclui código de saída, mensagens de erro, edge cases e semântica das opções
  • Cada ponto em que o comportamento diverge do GNU aumenta a chance de shell scripts tomarem decisões erradas
  • O uutils agora também roda em CI a suite de testes upstream do GNU coreutils
    • Isso parece um nível adequado de defesa contra esse tipo de diferença

É preciso resolver antes de cruzar a fronteira de confiança

  • CVE-2026-35368 foi uma execução local de código como root em chroot
  • O padrão do problema estava em fazer chroot(new_root)? e só então resolver o nome do usuário dentro da nova raiz controlada pelo atacante
    • get_user_by_name(name)? acabava lendo bibliotecas compartilhadas do novo sistema de arquivos raiz para resolver o nome do usuário
    • Se o atacante colocasse arquivos dentro do chroot, isso podia levar à execução de código como uid 0
  • O GNU chroot resolve o usuário antes do chroot
    • A correção passou a seguir a mesma ordem
  • Depois que a fronteira de confiança é cruzada, cada chamada de biblioteca pode virar uma forma de executar código do atacante
  • Nem mesmo linkagem estática impede esse problema
    • get_user_by_name passa por NSS e pode fazer dlopen de módulos libnss_* em tempo de execução

Bugs que o Rust realmente impediu

  • Também fica claro quais tipos de bug não apareceram nesta auditoria
    • Não houve buffer overflow
    • Não houve use-after-free
    • Não houve double-free
    • Não houve data race em estado mutável compartilhado
    • Não houve null-pointer dereference
    • Não houve leitura de memória não inicializada
  • Mesmo quando havia bugs nas ferramentas, a auditoria não encontrou casos exploráveis como leitura arbitrária de memória
  • O GNU coreutils vem acumulando nos últimos anos vários CVEs da família de segurança de memória
    • pwd deep path buffer overflow
    • numfmt out-of-bounds read
    • unexpand --tabs heap buffer overflow
    • od --strings -N escrita de NUL fora do heap buffer
    • sort leitura de 1 byte antes do heap buffer
    • split --line-bytes heap overwrite em CVE-2024-0684
    • b2sum --check leitura de memória não alocada em entrada malformada
    • tail -f stack buffer overrun
  • Na comparação do mesmo período, a reimplementação em Rust manteve zero ocorrências nessa categoria de bug
    • Ainda assim, a auditoria não prova a ausência de bugs de segurança de memória; apenas não encontrou nenhum
  • Os problemas que restam surgem principalmente não dentro do Rust, mas nas fronteiras com o mundo externo
    • caminhos
    • bytes e strings
    • syscall
    • diferenças de tempo e mudanças de estado no sistema de arquivos

Rust correto também é Rust idiomático

  • Rust idiomático não é só código que passa no borrow checker e deixa o clippy em silêncio
  • A correção também precisa fazer parte da idiomaticidade
    • Isso porque os formatos de código que sobrevivem na prática se consolidam a partir da experiência da comunidade
  • Sistemas robustos não devem esconder a bagunça do mundo real, e sim refleti-la como ela é
    • descritores de arquivo em vez de caminhos
    • OsStr em vez de String
    • ? em vez de unwrap
    • compatibilidade bug-for-bug com o original em vez de uma semântica “mais limpa” só na aparência
  • O sistema de tipos consegue expressar muita coisa, mas não condições fora de seu controle, como o tempo que passa entre duas syscalls
  • Rust idiomático exige que tipos, nomes e fluxo de controle do código revelem a verdade do ambiente de execução
    • Mesmo que fique menos bonito do que um código de quadro branco, é preciso um formato mais honesto

Referências

1 comentários

 
GN⁺ 1 시간 전
Comentários do Hacker News
  • Como mantenedor do GNU Coreutils, li o texto com interesse, mas no pouco que usei Rust achei fácil demais criar uma race TOCTOU com std::fs
    Espero que uma API parecida com openat entre na biblioteca padrão em algum momento

    E não concordo com a regra de resolver o caminho antes de comparar
    Em geral, é melhor chamar fstat e comparar st_dev e st_ino, e o texto até mencionava um pouco isso

    Um efeito colateral considerado com menos frequência é o custo de desempenho
    Num exemplo real, em um caminho de diretório muito profundo, cp levou 0,010 s, enquanto uu_cp levou 12,857 s

    Criar caminhos assim de propósito no mundo real é raro, mas o software GNU se esforça muito para evitar limites arbitrários
    https://www.gnu.org/prep/standards/standards.html#Semantics

    E o texto diz que a reescrita em Rust teve 0 bugs de segurança de memória no mesmo período, mas isso não é verdade :)
    https://github.com/advisories/GHSA-w9vv-q986-vj7x

    • Sim, std::fs sofre do problema do lowest common denominator
      Era preciso colocar alguma coisa no Rust 1.0 e, infelizmente, esse estado acabou se cristalizando por muito tempo

      Acho que o uutils é um bom lugar para tentar projetar uma API substituta de std::fs em que seja mais difícil errar

    • Obrigado por explicar essa visão do outro lado de forma tão concisa

      Queria perguntar o que deveríamos aprender aqui
      Para um post de internet, vou perguntar de propósito de forma bem agressiva, porque com contraste fica mais fácil enxergar diferenças e erros
      Claro que você não tem obrigação nenhuma de gastar seu tempo ou energia mental com isso

      Fico curioso por que velocidade, desempenho, race condition e st_ino vivem aparecendo juntos
      Latência, gravação em armazenamento real, atomicidade, ACID e a velocidade finita de propagação da informação parecem acabar convergindo para uma essência parecida
      Sistemas confiáveis, como os de contabilidade, parecem no fim precisar de ACID, e sistemas pouco confiáveis talvez sejam esquecidos tão rápido que dá a impressão de que as diferenças entre computadores nem importam tanto

      Também me pergunto se, em aplicações do dia a dia, throughput realmente é mais importante do que latency

      E entendo o foco em números de inode por causa da história de C, dos Unix-like e do GNU coreutils,
      mas fico pensando em um exemplo bem básico como fazer um pendrive simplesmente funcionar direito para armazenar arquivos
      sem evitar complexidades como buffering de I/O da libc, fflush, buffering do kernel, múltiplos núcleos, time-sharing e vários aplicativos rodando ao mesmo tempo

    • Sou totalmente iniciante, mas fiquei curioso por que foi necessário um loop while em vez de simplesmente dar cd direto com $(yes a/ | head -n $((32 * 1024)) | tr -d '\n')

      Edit: entendi. Era por causa de -bash: cd: a/a/a/....../a/a/: File name too long

    • Não sei se você viu, mas existe uma demo de conversão automática de um utilitário GNU como wget para um subset seguro de C++
      https://duneroadrunner.github.io/scpp_articles/PoC_autotranslation_of_wget

      A ideia é trocar quase em 1:1 elementos perigosos de C por elementos seguros de C++ com comportamento correspondente, então parece menos provável introduzir bugs novos e diferenças novas de comportamento do que numa reescrita

      Com uma pequena limpeza no código-fonte original, a conversão pode ser totalmente automatizada, então no estágio de build dá para gerar um executável seguro em memória, um pouco mais lento, a partir do código C original

    • Talvez seja uma pergunta meio boba, mas fico curioso se o pessoal do GNU Coreutils está avaliando ou planejando uma reescrita própria em Rust

  • Talvez soubessem usar Rust, mas não conheciam o suficiente a API Unix e sua semântica e armadilhas
    A maior parte desses erros entra na categoria de coisas bem iniciantes para quem vem de décadas de GNU coreutils ou desenvolvimento baseado em BSD e Solaris
    Muitos desses problemas já vieram à tona e foram organizados há décadas, e embora ainda exista uma longa cauda de correções no código legado, hoje em dia em geral continua entrando só um volume pequeno

    • Li aquela thread da Canonical e fiquei de queixo caído
      A ideia era mais ou menos: “Rust é mais seguro, segurança é prioridade máxima, então distribuir uma reescrita completa do coreutils é urgente. Se alguma coisa quebrar, tudo bem, consertamos depois”

      Eu não quero rodar na minha máquina código feito por gente que pensa assim
      Eu também sou a favor de Rust, mas Rust ser mais seguro só vale mantidas as demais condições iguais
      Aqui as demais condições estão longe de ser iguais

      Reescritas inevitavelmente vão ter muito mais bugs e vulnerabilidades do que código mantido por décadas, então o argumento de segurança faz sentido como estratégia de transição de longo prazo, mas não justifica um rollout apressado

      Minimizar o impacto nos usuários depois da distribuição, ou dizer “é assim que os bugs aparecem” e “o coreutils antigo também não tinha testes adequados”, é irresponsável demais
      Usuários não são cobaias
      Acho que mantenedores têm uma responsabilidade moral de não prejudicar a confiabilidade dos sistemas dos usuários

    • Mais fundamentalmente, parece que a biblioteca padrão do Rust empurra o desenvolvedor para uma API limpa no nível errado de abstração
      Por exemplo, para operações baseadas em caminho em vez de operações de arquivo baseadas em handle
      Espero estar enganado

    • Acho que a proposta do Rust é fazer com que as armadilhas maiores e mais comuns simplesmente não precisem de tanta atenção

      E o ponto central deste texto parece ser justamente que as APIs de sistema de arquivos deveriam cumprir esse papel

    • Alguém já cunhou a expressão disassembler rage para algo parecido
      A ideia é que, se você olhar de perto o bastante, qualquer erro parece amador

      A expressão também vem da postura de quem fica só no disassembler xingando o programador de alto nível por ter usado if em vez de switch numa função 100 frames abaixo na call stack

      Aqui estamos vendo só alguns pontos em que eles erraram, e quase nada das milhares de linhas de código escrito corretamente ao redor

    • Ter panic nesse tipo de utilitário é um erro bem amador, mesmo pelos padrões de Rust
      Se fosse algo como erro irrecuperável de alocação, tudo bem, mas expect e unwrap são difíceis de justificar a menos que a invariante que impede aquele caminho de código seja garantida de forma realmente rígida

  • Uma das dificuldades ao reescrever código é que o código original foi sendo moldado gradualmente para responder a problemas que só apareciam em ambientes reais de operação

    As lições aprendidas nesse processo ficam silenciosamente embutidas no código e, se não estiverem documentadas, há uma quantidade enorme de trabalho oculto até alcançar um nível equivalente

    O texto original mostra muito bem exatamente esse tipo de lista

    Ainda assim, antes de sair chamando de amador, também é preciso reconhecer que esse é um dos fenômenos mais próprios do software
    A menos que houvesse ótima documentação técnica no coreutils e testes cobrindo esses casos que foram simplesmente ignorados, isso era quase inevitável

    • Um bom exemplo citado no texto é o CVE de chroot + NSS
      A regra de que o NSS é dinâmico e vai fazer dlopen de bibliotecas dentro do chroot não está escrita de forma visível em lugar nenhum

      Isso é mais o tipo de coisa que administradores de sistemas aprenderam na prática por mais de 25 anos, e uma reescrita clean-room costuma reaprender isso como um novo CVE
      Mesmo que você porte o mesmo código com um LLM, a situação é parecida
      Dá para ler as assinaturas de função, mas o que realmente importa são as marcas e cicatrizes que ficaram naquele código

    • Fica ainda mais difícil se fizerem esse trabalho evitando a GPL e sem nem ler o código-fonte original

      Na minha opinião, se o uutils fosse GPL e pudesse se inspirar diretamente no código-fonte original do coreutils, teria sido muito melhor

    • Também é preciso deixar claro que não documentar essas lições, ou pelo menos os bugs e vulnerabilidades que se tentou evitar, também é uma prática ruim

      Claro, é difícil documentar todos os bugs evitados implicitamente só por escrever o código direito,
      mas para quem vier depois é importante deixar algo como “o motivo de usar foo aqui em vez de bar é que, sob a condição ABC, usar bar criaria um baz perigoso por causa de XYZ”
      Mesmo que pareça um pequeno desperdício de tempo e espaço de documentação, ainda assim é melhor

  • Sinto que várias das coisas apontadas neste texto, especialmente comparando com o código-fonte do GNU coreutils, deveriam ter sido pegas por testes unitários decentes ou por revisão manual
    Reescrever o coreutils parece uma ideia terrível
    https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/
    e parece ter sido conduzido da forma errada, sem absorver adequadamente o conhecimento acumulado pelo software anterior

    Para reescrever, você precisa entender e aprender completamente com a obra anterior
    Senão vai repetir os mesmos erros, e francamente isso é bem constrangedor

    Para deixar claro, gosto de Rust, uso em vários projetos e acho excelente
    Só que Rust não salva engenharia ruim

    • Curiosamente, o uutils usa a suite de testes do GNU coreutils

      E, além disso, eles deixam explícito que não aceitam contribuições escritas a partir da leitura do código GPL

    • Se foi o pessoal que fez unity, upstart e snap, isso já entra no campo do esperado

    • Parece até que deveríamos dar esta mensagem de boas-vindas a novos programadores de sistemas:
      Unix é quebrado, e no fim você vai ter de escrever gambiarras feias e nada didáticas com as próprias mãos, além de fazer testes empíricos
      É assim que software confiável e boa engenharia de software realmente funcionam

  • Fico curioso por que differential fuzzing não pegou esses bugs

    https://github.com/uutils/coreutils/tree/main/fuzz/uufuzz

  • O padrão de verificar um caminho com uma syscall e depois fazer outra syscall no mesmo caminho para executar a operação sempre traz o mesmo tipo de problema
    Um atacante com permissão de escrita no diretório pai pode trocar um componente do caminho por um link simbólico nesse intervalo, e o kernel vai resolver o caminho de novo do zero na segunda chamada, fazendo a operação privilegiada ir para o alvo escolhido pelo atacante

    • Na verdade é ainda pior que isso
      Um atacante com permissão de escrita no diretório pai também pode aprontar com hardlinks
      Mesmo que só possa mexer em arquivos regulares, na prática quase não há mitigação decente
      Veja o exemplo em https://michael.orlitzky.com/articles/posix_hardlink_heartache.xhtml
    • Hum… talvez exista alguma forma de colocar um write lock no diretório, mas se entrar na história de timeout e coisas do tipo, isso provavelmente fica mais complexo muito rápido
  • A causa raiz de alguns bugs parece ser que a API Unix é opaca demais

    Por exemplo, o fato de get_user_by_name carregar bibliotecas compartilhadas dentro do novo sistema de arquivos raiz para resolver nomes de usuário e, por causa disso, permitir que um atacante que pode plantar arquivos dentro do chroot execute código como uid 0, parece quase uma armadilha explosiva

    Uma função que obtém dados de usuário de repente também carregar bibliotecas compartilhadas parece um design que mistura responsabilidades
    Eu acharia melhor separar, no nível de função, a consulta de dados de usuário do carregamento de bibliotecas, ou pelo menos deixar esse comportamento evidente já no nome

    • Em parte até pode ser, mas se decidiram reescrever o coreutils do zero, então entender a API POSIX é literalmente parte central do trabalho

      Além disso, se o código que verifica se o caminho aponta para a raiz do sistema de arquivos era file == Path::new("/"), isso não é problema de API
      Quem escreveu isso quase não parece qualificado para participar desse projeto

    • Acho até que usar uma linguagem segura funcional pode levar a pessoa a imaginar que os dados com que ela lida também não têm estado
      Mas no sistema operacional muita coisa muda o tempo todo

      Até surgirem sistemas de arquivos que ofereçam snapshots, é preciso verificar tudo repetidamente

      No fim, o que se precisa é de uma API que, dado um input, devolva ou um resultado de sucesso ou uma falha
      não uma API que devolva uma entre três coisas: sucesso, falha ou erro

    • Sim, o musl libc remove justamente uma parte desse tipo de comportamento

    • Acho que a causa raiz não é tanto a opacidade da API Unix, mas o fato de não terem pensado direito no caso em que root faz chroot para um diretório que não controla

      Qualquer coisa para a qual se fez chroot fica sob controle de quem montou aquele chroot, e se a pessoa não entende isso, não tem qualificação para usar chroot()

      get_user_by_name pode até parecer uma pegadinha, mas na prática há pouca diferença real entre usar newroot/etc/passwd e usar newroot/usr/lib/x86_64-linux-gnu/libnss_compat.so, newroot/bin/sh e afins

      Por isso acho que /usr/sbin/chroot nem deveria ter motivo para consultar IDs de usuário
      O toybox chroot não faz isso
      No fim, o bug não foi a forma errada de fazer algo, e sim o fato de terem feito isso para começo de conversa

    • Unix e POSIX são um fractal de armadilhas em qualquer nível de zoom

  • Mesmo supondo que o pessoal de Rust tenha reescrito o coreutils sem experiência com Linux, o que eu entendo menos ainda é como o Ubuntu aceitou isso no mainline

    • Parece até que o Ubuntu tem uma política de trocar, quase a cada release, um componente fundamental do sistema por um experimento capenga e inacabado

      O ponto principal aqui não é “nossa, tinha bug em código Rust”, e sim justamente isso

    • O original é sob licença GPL, e a reescrita é sob licença MIT

  • Se for verdade que “esses bugs vieram de código Rust realmente distribuído, e os autores sabiam o que estavam fazendo”,
    fico me perguntando se isso significa que os utilitários originais não tinham test harness e que a reescrita também não começou por aí

    Mesmo com muitos edge cases, parece que daria para abstrair razoavelmente o SO e o FS e verificar coisas como se rm .// realmente não apaga o diretório atual quando deveria se comportar de um certo jeito

    Isso me parece menos um problema de código bagunçado ou uma crítica à linguagem e mais, de novo, aquela velha atitude de que programação de sistemas não testa

    Por outro lado, se os utilitários originais tinham testes e ainda assim havia tantas lacunas, então talvez a própria suite de testes original fosse bastante insuficiente

    • Acho que sim

      Mas não tenho tanta certeza assim de que dê para abstrair o SO e o FS o bastante para validar isso
      porque as pessoas tentam esse tipo de coisa desde antes de eu nascer e, pelo jeito, ainda não conseguiram

      Por exemplo, já é difícil até decidir quantas barras / você deve concatenar num teste

      Indo além, suponha que rm se recusasse a apagar um arquivo se os 9 primeiros bytes fossem important
      sem saber essa string de antemão, já é complicado imaginar como criar um teste que descubra esse comportamento
      E se essa palavra mágica nem estiver no dicionário, fica ainda pior

      Quase nunca vi alguém dizer seriamente que “programação de sistemas não testa”
      O que eu ouço com frequência é que testes nem sempre cumprem o papel que as pessoas esperam deles

    • Pelo que entendi, no processo de desenvolvimento do uutils houve testes extensivos de comparação de comportamento com os utilitários originais, e até tentativas de preservar os bugs

    • Este é um dos motivos pelos quais o Windows desativa symlink por padrão
      Não é uma solução por abstração, e sim praticamente pela remoção da funcionalidade

      Nos Unix-like isso não dá para fazer, porque existe software demais dependendo de symlinks há décadas

      O MacOS também tem uma resposta parecida
      Por exemplo, o bug de chroot() em geral não vira um problema real na configuração padrão, porque o MacOS bloqueia chroot() por padrão
      Para usar, é preciso desativar o system integrity protection

      O problema fundamental está nas arestas cortantes da API POSIX, e a solução fica mais perto de eliminar isso do que de abstrair

  • Acho normal que as pessoas experimentem e tentem, mesmo de forma desajeitada
    É assim que se aprende e cresce

    O que realmente me deixa curioso é como a cadeia de decisão do Ubuntu se quebrou a ponto de isso chegar em produção

    • Às vezes crescer significa só ficar maior mesmo