3 points par GN⁺ 2026-04-30 | 1 commentaires | Partager sur WhatsApp
  • La sécurité mémoire s’améliore nettement, mais même dans du code Rust en production, les problèmes de gestion des frontières du système subsistent et peuvent toujours mener à des vulnérabilités
  • Les flux qui réinterprètent plusieurs fois le même chemin via plusieurs appels système, les approches qui modifient les permissions après création, et les comparaisons de chemins fondées sur des chaînes favorisent facilement des problèmes comme le TOCTOU et l’exposition de privilèges
  • Sous Unix, les chemins, variables d’environnement et données de flux circulent sous forme de bytes bruts ; un traitement centré sur String, ainsi que from_utf8_lossy, unwrap ou expect, peuvent donc conduire à une corruption des données ou à un DoS
  • Ignorer les erreurs peut faire passer un échec pour un succès, et les écarts de comportement avec GNU coreutils peuvent aussi se transformer immédiatement en problèmes de sécurité dans des scripts shell et des outils privilégiés
  • Lors de cet audit, aucun bug de la famille sécurité mémoire comme buffer overflow, use-after-free ou double-free n’a été relevé ; les principaux risques restants se concentraient sur les frontières en contact avec le monde extérieur plutôt qu’à l’intérieur de Rust

Les limites de Rust mises en lumière par l’audit

  • Les 44 CVE publiées par Canonical à propos de uutils montrent que, même dans du code Rust en production, des vulnérabilités peuvent subsister sans être détectées par le borrow checker, clippy ou cargo audit
  • Le cœur du problème se situait moins dans la sécurité mémoire que dans la gestion des frontières du système
    • Il existait un décalage temporel entre les chemins et les appels système
    • Les données bytes Unix et les chaînes UTF-8 n’étaient pas alignées
    • Il y avait des différences de comportement avec l’outil d’origine
    • Des traitements d’erreur manquants et des arrêts via panic! existaient
  • Cette liste de CVE montre de manière condensée où s’arrête la sécurité dans du code système Rust

Réinterpréter un chemin deux fois crée du TOCTOU

  • Vérifier un même chemin dans un appel système, puis retravailler ce même chemin dans l’appel suivant, mène facilement à une vulnérabilité TOCTOU
    • Entre les deux appels, un attaquant disposant de droits d’écriture sur le répertoire parent peut remplacer un composant du chemin par un lien symbolique
    • Lors du second appel, le noyau réinterprète entièrement le chemin, ce qui peut rediriger une opération privilégiée vers une cible choisie par l’attaquant
  • L’API std::fs de Rust repose par défaut sur une réinterprétation à partir de &Path, ce qui rend ce type d’erreur facile à commettre
  • Dans CVE-2026-35355, un flux qui supprimait un fichier puis en recréait un autre au même chemin a été exploité
    • Dans src/uu/install/src/install.rs, fs::remove_file(to)? était suivi de File::create(to)?
    • Si, entre la suppression et la création, to est remplacé par un lien symbolique pointant vers /etc/shadow ou une cible similaire, un processus privilégié peut écraser ce fichier
  • La correction a consisté à utiliser OpenOptions::create_new(true) afin de ne créer que de nouveaux fichiers
    • D’après la documentation, create_new n’autorise à l’emplacement cible ni fichier existant, ni dangling symlink
  • Lorsqu’il faut agir deux fois sur le même chemin, il est plus sûr de s’ancrer sur un descripteur de fichier
    • Hors création d’un nouveau fichier, il vaut mieux ouvrir le répertoire parent une seule fois et travailler ensuite avec des chemins relatifs à ce handle
    • Si une opération agit deux fois sur le même chemin, il faut la considérer comme un TOCTOU jusqu’à preuve du contraire
    Publicité

Les permissions doivent être définies à la création, pas modifiées après

  • Le flux qui crée un répertoire ou un fichier avec les permissions par défaut, puis exécute chmod ensuite, crée lui aussi une courte fenêtre d’exposition
    • Un code du type fs::create_dir(&path)? suivi de fs::set_permissions(&path, Permissions::from_mode(0o700))? laisse path exister avec ses permissions par défaut pendant cet intervalle
    • D’autres utilisateurs peuvent faire open() pendant cette fenêtre, et un chmod ultérieur ne révoquera pas les descripteurs de fichier déjà obtenus
  • Les permissions doivent être spécifiées au moment de la création
    • Il faut utiliser OpenOptions::mode() et DirBuilderExt::mode() pour que l’objet soit créé avec les permissions voulues
    • Le noyau y applique ensuite aussi le umask, donc si son impact compte, il faut le gérer explicitement

Comparer des chaînes de chemin ne prouve pas l’identité dans le système de fichiers

  • Le premier contrôle de --preserve-root dans chmod ne faisait qu’une comparaison de chaînes
    • recursive && preserve_root && file == Path::new("/")
    • Une entrée qui pointe en réalité vers la racine mais dont la chaîne n’est pas /, comme /../, /./, /usr/.. ou un lien symbolique vers /, contournait donc ce contrôle
  • La correction est passée à une comparaison après résolution du chemin réel absolu via fs::canonicalize
    • PR de correction
    • canonicalize renvoie le chemin réel après résolution de .., ., et des liens symboliques
  • Dans le cas de --preserve-root, cette méthode fonctionne parce que / n’a pas de répertoire parent
  • Pour comparer de manière générale si deux chemins arbitraires désignent le même objet du système de fichiers, il faut comparer non pas les chaînes, mais (dev, inode)
    • GNU coreutils procède lui aussi ainsi
  • Dans CVE-2026-35363, rm rejetait . et .., mais acceptait ./ et .///, ce qui permettait de supprimer le répertoire courant
    • Quand on ne traite que la forme textuelle de l’entrée, les contrôles se contournent facilement
    Publicité

Aux frontières Unix, il faut privilégier les bytes plutôt que les chaînes

  • Les types String et &str de Rust sont toujours en UTF-8, alors que sous Unix, chemins, variables d’environnement, arguments et contenus de flux appartiennent au monde des bytes bruts
  • Faire les mauvais choix au moment de franchir cette frontière mène à deux catégories de bugs
    • Une conversion avec perte comme from_utf8_lossy remplace les bytes invalides par U+FFFD et corrompt discrètement les données
    • Une conversion stricte comme unwrap ou ? peut rejeter l’entrée ou faire terminer le processus
  • Le CVE-2026-35346 de comm était un cas où la conversion avec perte dégradait la sortie
    • Dans src/uu/comm/src/comm.rs, les bytes d’entrée ra et rb étaient convertis par String::from_utf8_lossy avant un print!
    • GNU comm recopie les bytes tels quels, même avec des fichiers binaires, mais uutils remplaçait l’UTF-8 invalide par U+FFFD, ce qui altérait la sortie
    • La correction a consisté à écrire directement les bytes bruts vers stdout via BufWriter et write_all
  • print! impose un aller-retour UTF-8 via Display, alors que Write::write_all ne le fait pas
  • Dans du code système de type Unix, il faut choisir les bons types selon le contexte
    • Path, PathBuf pour les chemins de fichiers
    • OsString pour les variables d’environnement
    • Vec<u8> ou &[u8] pour le contenu des flux
  • Faire transiter les données par String pour des raisons de commodité de formatage introduit facilement une corruption des données

Tout panic peut devenir un déni de service

  • Dans un CLI, unwrap, expect, l’indexation de slices, l’arithmétique non vérifiée et from_utf8 peuvent devenir des points de DoS si un attaquant contrôle l’entrée
    • panic! déroule la pile puis interrompt le processus
    • Si l’outil tourne dans un cron job, une CI pipeline ou un script shell, cela peut arrêter toute la tâche
    • Dans un environnement relancé en boucle, cela peut même paralyser l’ensemble du système via une crash loop
  • Dans CVE-2026-35348 de sort --files0-from, la présence d’un nom de fichier non UTF-8 dans une liste de noms séparés par NUL faisait arrêter l’outil
    • Le parseur appelait std::str::from_utf8(bytes).expect(...) sur les bytes de chaque nom
    • GNU sort traite les noms de fichiers comme des bytes bruts, comme le noyau, alors que uutils imposait l’UTF-8 et arrêtait tout le processus au premier chemin non UTF-8
  • Dans le code qui traite des entrées non fiables, unwrap, expect, l’indexation et les conversions as doivent être considérés comme des CVE potentielles
    • Il faut préférer ?, get, checked_*, try_from et remonter les véritables erreurs à l’appelant
    Publicité
  • Des règles clippy à activer en CI sont aussi proposées
    • unwrap_used
    • expect_used
    • panic
    • indexing_slicing
    • arithmetic_side_effects
  • Dans le code de test, ces avertissements peuvent être excessifs ; il est donc pertinent de les limiter au périmètre cfg(test)

Ignorer une erreur peut faire passer un échec pour une réussite

  • Certaines CVE venaient de flux qui ignoraient des erreurs ou faisaient disparaître l’information d’erreur
  • chmod -R et chown -R ne renvoyaient que le code de sortie du dernier fichier traité
    • Même si de nombreux fichiers précédents échouaient, si le dernier réussissait, la commande pouvait se terminer avec 0
    • Un script pouvait alors croire à tort que l’opération entière s’était bien déroulée
  • dd appelait set_len() puis Result::ok() pour imiter le comportement GNU avec /dev/null
    • L’intention était de jeter l’erreur dans un cas limité, mais le même code s’appliquait aussi aux fichiers ordinaires
    • Même si le disque était plein, un fichier destination à moitié écrit pouvait rester silencieusement sur place
  • Jeter un Result avec .ok(), .unwrap_or_default() ou let _ = fait disparaître des causes d’échec importantes
  • Même s’il ne faut pas s’arrêter dès la première erreur, il faut au minimum mémoriser le code d’erreur le plus grave et terminer avec celui-ci
  • Si l’on doit vraiment jeter un Result, il faut laisser dans le code la raison pour laquelle cet échec peut être ignoré sans risque

La compatibilité exacte avec l’outil d’origine est aussi une fonction de sécurité

  • Plusieurs CVE n’étaient pas dues à des opérations dangereuses, mais au fait que le code se comportait différemment de GNU
    • En pratique, les scripts shell réels dépendent du comportement du GNU d’origine, et un écart sémantique peut donc se transformer en problème de sécurité
  • Le CVE-2026-35369 de kill -1 est emblématique
    • GNU interprète -1 comme le signal 1 et attend ensuite un PID
    • uutils l’interprétait comme l’envoi du signal par défaut au PID -1
    • Sous Linux, le PID -1 vise tous les processus visibles ; une simple faute de frappe pouvait donc tuer tout le système
    Publicité
  • Dans un outil réimplémenté, la compatibilité bug-for-bug devient une protection qui couvre aussi les codes de sortie, messages d’erreur, edge cases et sémantique des options
  • À chaque point où le comportement diverge de GNU, la probabilité qu’un script shell prenne une mauvaise décision augmente
  • uutils exécute désormais aussi en CI la suite de tests upstream de GNU coreutils
    • Cela semble une défense à l’échelle appropriée contre ce type d’écart

Il faut résoudre les éléments avant de franchir une frontière de confiance

  • CVE-2026-35368 concernait une exécution de code locale en root dans chroot
  • Le motif du bug venait du fait que, après chroot(new_root)?, le programme résolvait encore le nom d’utilisateur à l’intérieur du nouveau root contrôlé par l’attaquant
    • get_user_by_name(name)? amenait à lire les bibliothèques partagées du nouveau système de fichiers racine pour résoudre le nom
    • Si l’attaquant plaçait les bons fichiers dans le chroot, cela pouvait mener à une exécution de code avec l’uid 0
  • GNU chroot effectue la résolution de l’utilisateur avant le chroot
    • La correction a adopté le même ordre
  • Une fois une frontière de confiance franchie, chaque appel de bibliothèque peut potentiellement exécuter du code contrôlé par l’attaquant
  • Même l’édition de liens statique ne résout pas ce problème
    • get_user_by_name passe par NSS et peut dlopen des modules libnss_* à l’exécution

Les bugs que Rust a effectivement empêchés

  • Il est aussi clair quels types de bugs n’ont pas été trouvés lors de cet audit
    • Aucun buffer overflow
    • Aucun use-after-free
    • Aucun double-free
    • Aucune data race sur état mutable partagé
    • Aucun null-pointer dereference
    • Aucune lecture de mémoire non initialisée
  • Même quand les outils comportaient des bugs, l’audit n’a trouvé aucun cas exploitable en lecture arbitraire de mémoire
  • Ces dernières années, GNU coreutils a pourtant continué d’accumuler des CVE de sécurité mémoire de ce type
    • pwd deep path buffer overflow
    • numfmt out-of-bounds read
    • unexpand --tabs heap buffer overflow
    • od --strings -N écriture de NUL hors heap buffer
    • sort lecture d’1 byte avant heap buffer
    • split --line-bytes heap overwrite avec CVE-2024-0684
    • b2sum --check lecture de mémoire non allouée sur entrée mal formée
    • tail -f stack buffer overrun
    Publicité
  • Sur la même période, la réimplémentation Rust est restée à 0 bug dans cette catégorie
    • Avec la précision toutefois que l’audit ne prouve pas l’absence de bugs de sécurité mémoire ; il montre seulement qu’il n’en a pas trouvé
  • Les problèmes restants surgissent surtout aux frontières avec le monde extérieur plutôt qu’au sein même de Rust
    • chemins
    • bytes et chaînes
    • appels système
    • décalages temporels et changements d’état du système de fichiers

Un Rust correct est aussi un Rust idiomatique

  • Le Rust idiomatique ne se limite pas à passer le borrow checker et à produire un code silencieux dans clippy
  • La correction doit aussi faire partie de l’idiomaticité
    • Parce que les formes de code qui survivent au réel se sont consolidées par l’expérience collective de la communauté
  • Un système robuste doit non pas masquer le désordre du monde réel, mais le refléter tel quel
    • des descripteurs de fichier plutôt que des chemins
    • OsStr plutôt que String
    • ? plutôt que unwrap
    • la compatibilité bug-for-bug avec l’original plutôt qu’une sémantique plus élégante en apparence
  • Le système de types peut exprimer beaucoup de choses, mais pas des conditions hors de contrôle comme le temps qui s’écoule entre deux appels système
  • Le Rust idiomatique suppose que les types, noms et flux de contrôle du code révèlent la vérité de l’environnement d’exécution
    • Même si c’est moins esthétique qu’un joli code de tableau blanc, il faut une forme plus honnête

Références

1 commentaires

 
GN⁺ 2026-04-30
Commentaires sur Hacker News
  • En tant que mainteneur de GNU Coreutils, j’ai trouvé l’article intéressant, mais dans le peu de Rust que j’ai utilisé, il était bien trop facile de créer une TOCTOU race avec std::fs
    J’espère qu’une API de type openat finira par entrer dans la bibliothèque standard

    Et je ne suis pas d’accord avec la règle résoudre les chemins avant de les comparer
    En général, il vaut mieux appeler fstat et comparer st_dev et st_ino, et l’article le mentionnait un peu aussi

    Un effet secondaire moins souvent pris en compte, c’est le coût en performances
    Dans un exemple concret, sur un chemin de répertoires très profond, cp prenait 0,010 seconde alors que uu_cp mettait 12,857 secondes

    Dans la réalité, on crée rarement ce genre de chemin exprès, mais les logiciels GNU font de gros efforts pour éviter les limites arbitraires
    https://www.gnu.org/prep/standards/standards.html#Semantics

    Et l’article disait aussi que la réécriture en Rust avait eu zéro bug de sécurité mémoire sur une période comparable, mais ce n’est pas vrai :)
    https://github.com/advisories/GHSA-w9vv-q986-vj7x

    • Oui, std::fs souffre d’un problème de lowest common denominator
      Il fallait bien mettre quelque chose dans Rust 1.0, et malheureusement cet état s’est figé très longtemps

      Je pense que uutils est un bon endroit pour essayer de concevoir une API de remplacement à std::fs sur laquelle il est plus difficile de se tromper

    • Merci d’avoir expliqué ce point de vue avec autant de concision depuis l’autre camp

      J’aimerais demander ce qu’il faut retenir de tout ça
      C’est une question volontairement assez agressive pour un échange sur Internet, parce qu’un contraste net aide à mieux voir les différences et les erreurs
      Bien sûr, tu n’as absolument aucune obligation d’y consacrer du temps ou de l’énergie mentale

      Je me demande pourquoi la vitesse, les performances, les race conditions et st_ino reviennent toujours ensemble
      On dirait que la latence, l’écriture réelle sur le support de stockage, l’atomicité, ACID et la vitesse finie de transmission de l’information convergent au fond vers une même nature
      J’ai l’impression que les systèmes à forte fiabilité, comme la comptabilité, finissent forcément par aller vers ACID, et que les systèmes peu fiables sont oubliés si vite que les différences entre ordinateurs paraissent moins importantes

      Je me demande aussi si, dans les applications du quotidien, le throughput est vraiment plus important que la latency

      Et je comprends qu’on se concentre sur les numéros d’inode à cause de l’histoire de C, des OS de type Unix et de GNU coreutils,
      mais je me demande ce que ça donnerait si on prenait un exemple très simple comme faire en sorte qu’une clé USB fonctionne simplement correctement pour stocker des fichiers
      sans éviter la complexité du buffering I/O de libc, de fflush, du buffering du noyau, du multicœur, du partage du temps processeur et de l’exécution simultanée de plusieurs applications

    • Je suis complètement débutant, mais je me demandais pourquoi il fallait une boucle while au lieu de faire simplement cd directement avec $(yes a/ | head -n $((32 * 1024)) | tr -d '\n')

      Édition : j’ai compris. C’était à cause de -bash: cd: a/a/a/....../a/a/: File name too long

    • Je ne sais pas si vous l’avez vu, mais il existe une démo qui convertit automatiquement des utilitaires GNU comme wget vers un sous-ensemble C++ sûr pour la mémoire
      https://duneroadrunner.github.io/scpp_articles/PoC_autotranslation_of_wget

      L’idée consiste à remplacer presque à l’identique les éléments dangereux de C par des éléments C++ sûrs ayant un comportement correspondant, ce qui semble moins susceptible d’introduire de nouveaux bugs et de nouveaux écarts de comportement qu’une réécriture

      Si on nettoie un peu le code source d’origine, la conversion peut être totalement automatisée, ce qui permettrait de produire à l’étape de build un exécutable sûr pour la mémoire, un peu plus lent, à partir des sources C d’origine

    • C’est peut-être une question un peu bête, mais je me demande si le côté GNU Coreutils envisage ou prévoit une réécriture en Rust de son côté

  • Ils savaient peut-être utiliser Rust, mais ils n’étaient pas vraiment assez familiers avec l’API Unix, sa sémantique et ses pièges
    La plupart de ces erreurs paraissent assez débutantes du point de vue de développeurs issus de GNU coreutils, BSD ou Solaris
    Beaucoup de ces problèmes ont déjà été découverts et traités il y a des décennies, et même si les bases de code existantes traînent encore une longue queue de correctifs, on est maintenant plutôt à un rythme où il n’en arrive plus qu’un petit nombre

    • En lisant ce thread Canonical, j’étais sidéré
      L’idée générale, c’était à peu près : « Rust est plus sûr, la sécurité est la priorité absolue, donc déployer d’urgence une réécriture complète de coreutils est nécessaire. Même si ça casse des choses, on corrigera plus tard. »

      Je n’ai aucune envie d’exécuter sur ma machine du code produit par des gens qui pensent comme ça
      Moi aussi je suis favorable à Rust, mais dire que Rust est plus sûr n’est vrai que toutes choses égales par ailleurs
      Ici, les autres conditions ne sont absolument pas égales

      Une réécriture aura inévitablement bien plus de bugs et de vulnérabilités qu’un code maintenu depuis des décennies, donc l’argument de sécurité peut avoir du sens dans une stratégie de transition à long terme, mais ne justifie pas un déploiement précipité

      Minimiser l’impact sur les utilisateurs après déploiement, ou dire « c’est comme ça qu’on fait ressortir les bugs » ou « les coreutils existants n’avaient pas non plus de vrais tests », c’est bien trop irresponsable
      Les utilisateurs ne sont pas des cobayes
      À mon avis, les mainteneurs ont une responsabilité morale de ne pas nuire à la fiabilité des systèmes de leurs utilisateurs

    • Plus fondamentalement, on dirait que la bibliothèque standard de Rust pousse les développeurs vers une API propre au mauvais niveau d’abstraction
      Par exemple vers des opérations sur fichiers basées sur les chemins plutôt que sur des handles
      J’espère me tromper

    • Pour moi, l’intérêt de Rust est justement de faire en sorte qu’on n’ait pas à se soucier des plus gros pièges, ceux dans lesquels il est le plus facile de tomber

      Le cœur de cet article semble justement être que les API du système de fichiers devraient jouer ce rôle

    • Quelqu’un a déjà forgé une expression similaire : disassembler rage
      L’idée, c’est que si on regarde d’assez près, toute erreur finit par avoir l’air amateur

      L’expression vient aussi de cette tendance à ne regarder qu’un désassembleur et à reprocher à un programmeur de haut niveau d’avoir utilisé un if au lieu d’un switch dans une fonction 100 frames plus bas dans la pile d’appels

      Là, on ne voit que quelques-unes de leurs erreurs, et on ne voit presque pas les milliers de lignes de code correctement écrit autour

    • Qu’un tel utilitaire fasse panic, même selon les critères de Rust, c’est une erreur assez amateur
      Pour une erreur d’allocation irrécupérable, passe encore, mais expect et unwrap sont difficiles à défendre sauf si on garantit vraiment de façon très stricte l’invariant qui empêche absolument ce chemin de code d’être exécuté

  • Une des difficultés d’une réécriture, c’est que le code d’origine a été transformé progressivement en réponse à des problèmes qui n’apparaissaient qu’en conditions réelles d’exploitation

    Les leçons tirées de ce processus s’infusent discrètement dans le code, et si elles ne sont pas documentées, il y a énormément de travail caché avant d’atteindre un niveau équivalent

    Le texte original montre justement très bien ce type de liste

    Cela dit, avant de traiter tout de suite ça d’amateurisme, il faut aussi voir que c’est l’un des phénomènes les plus typiquement logiciels du logiciel
    Sauf s’ils ont ignoré une excellente documentation technique de coreutils et des tests couvrant ces cas, c’était presque inévitable

    • Le bon exemple donné dans l’article, c’est le CVE chroot + NSS
      Le fait que NSS soit dynamique et qu’il dlopen des bibliothèques à l’intérieur d’un chroot n’est écrit nulle part de manière visible

      C’est plutôt quelque chose que les administrateurs système ont appris à force de s’y heurter pendant plus de 25 ans, et une réécriture en clean room le redécouvre en général sous la forme d’un nouveau CVE
      Ce serait pareil en portant le même code avec un LLM
      On peut lire les signatures de fonctions, mais ce qu’il faut vraiment, ce sont les blessures et cicatrices laissées dans ce code

    • Si on fait ce travail sans même lire le code source original pour éviter la GPL, c’est encore plus difficile

      À mon avis, uutils aurait été bien meilleur s’il avait été sous GPL et avait pu s’inspirer directement du code source original de coreutils

    • Il faut aussi dire clairement que ne pas documenter ces leçons, ou au moins les bugs et vulnérabilités qu’on a essayé d’éviter, est une mauvaise pratique

      Bien sûr, il est difficile de documenter tous les bugs implicitement évités juste en écrivant bien le code au départ,
      mais pour les futurs lecteurs, il est important de laisser des explications du genre : « on utilise foo ici au lieu de bar, parce qu’avec bar dans la condition ABC, on crée un baz dangereux pour telle raison XYZ »
      Même si cela semble gaspiller un peu de temps et d’espace documentaire, je pense que c’est préférable

  • J’ai l’impression qu’un bon nombre des points soulevés dans cet article, surtout en les comparant au code source GNU coreutils, auraient dû être détectés par des unit tests ou une relecture manuelle raisonnable
    Cette réécriture de coreutils semble être une très mauvaise idée, et
    https://www.joelonsoftware.com/2000/04/06/things-you-should-never-do-part-i/
    elle semble avoir été menée de travers, sans reprendre suffisamment les connaissances accumulées par le logiciel précédent

    Si on veut réécrire, il faut comprendre et apprendre complètement de ce qui existait avant
    Sinon, on répète les mêmes erreurs, et franchement c’est assez embarrassant

    Pour être clair, j’aime Rust, je l’utilise dans plusieurs projets et c’est excellent
    Mais Rust ne sauve pas une mauvaise ingénierie

    • Fait intéressant, uutils utilise la suite de tests GNU coreutils

      Et ils précisent aussi qu’ils n’accepteront pas de contributions écrites après lecture du code source GPL

    • De la part de ceux qui ont créé unity, upstart et snap, ce genre de chose reste tout à fait prévisible

    • Il faudrait peut-être accueillir les nouveaux programmeurs système avec ce message :
      Unix est cassé, et au final il faut écrire soi-même des contournements laids et peu pédagogiques, et faire aussi des tests empiriques
      C’est comme ça que fonctionnent les logiciels fiables et la bonne ingénierie logicielle

  • Je me demande pourquoi le differential fuzzing n’a pas permis d’attraper ce genre de bugs

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

  • Le schéma qui consiste à vérifier un chemin par un syscall, puis à refaire un syscall sur le même chemin pour agir ensuite dessus, mène toujours au même problème
    Un attaquant qui a le droit d’écrire dans le répertoire parent peut remplacer entre-temps un composant du chemin par un lien symbolique, et le noyau résoudra de nouveau tout le chemin depuis le début lors du second appel, de sorte que l’opération privilégiée s’appliquera à la cible choisie par l’attaquant

    • En réalité, c’est encore pire que ça
      Un attaquant qui a le droit d’écrire dans le répertoire parent peut aussi jouer avec des liens physiques
      Même si on ne peut toucher qu’aux fichiers ordinaires, il n’existe en pratique presque aucune vraie mesure d’atténuation
      Voir cet exemple : https://michael.orlitzky.com/articles/posix_hardlink_heartache.xhtml
    • Hum… il existe peut-être un moyen de poser un write lock sur le répertoire, mais dès qu’on ajoute des questions comme les timeouts, ça a l’air de devenir vite encore plus compliqué
  • Il semble que la cause profonde de plusieurs de ces bugs soit l’opacité excessive des API Unix

    Par exemple, le fait que get_user_by_name charge des bibliothèques partagées dans le nouveau système de fichiers racine pour résoudre un nom d’utilisateur, et qu’un attaquant capable de déposer des fichiers dans le chroot puisse ainsi exécuter du code avec l’uid 0, ressemble presque à un piège explosif

    Qu’une fonction de récupération de données utilisateur se mette soudain à charger aussi des bibliothèques partagées ressemble à un mélange des responsabilités dans la conception
    À mon avis, il faudrait séparer, au niveau des fonctions, la consultation des données utilisateur et le chargement de bibliothèques, ou au moins rendre ce comportement évident rien qu’au nom de la fonction

    • C’est peut-être vrai en partie, mais si on a décidé de réécrire coreutils à partir de zéro, comprendre les API POSIX fait littéralement partie du cœur du travail

      Et si, en plus, le code qui vérifie si un chemin pointe vers la racine du système de fichiers faisait file == Path::new("/"), alors ce n’est pas un problème d’API
      La personne qui a écrit ça n’avait à peu près pas sa place sur ce projet

    • Au contraire, je pense qu’utiliser un langage sûr fonctionnel peut donner l’illusion que les données manipulées sont elles aussi sans état
      Mais dans un système d’exploitation, énormément de choses changent en permanence

      Tant qu’on n’a pas un système de fichiers fournissant des snapshots, il faut continuellement revérifier

      Au final, il nous faut des API qui, quand on leur donne une entrée, ne renvoient qu’un résultat de succès ou un échec
      pas des API qui renvoient l’un des trois états succès, échec ou erreur

    • Oui, musl libc supprime justement l’un de ces éléments

    • À mon avis, la cause profonde n’est pas tant l’opacité des API Unix que le fait de ne pas avoir vraiment réfléchi au cas où root fait un chroot dans un répertoire qu’il ne contrôle pas

      Tout ce qui est ciblé par chroot est sous le contrôle de celui qui l’a préparé, et si on ne comprend pas ça, on n’a rien à faire avec chroot()

      get_user_by_name peut sembler piégeux, mais en réalité il n’y a presque pas de différence de fond entre utiliser newroot/etc/passwd et utiliser newroot/usr/lib/x86_64-linux-gnu/libnss_compat.so, newroot/bin/sh, etc.

      C’est pour ça qu’à mon avis /usr/sbin/chroot n’a aucune raison d’aller consulter un identifiant utilisateur au départ
      toybox chroot ne le fait pas non plus
      Au fond, le bug n’était pas d’avoir mal fait quelque chose, mais d’avoir fait cette chose tout court

    • Unix et POSIX sont fractals : quel que soit l’endroit où on coupe, c’est rempli de pièges

  • Même en supposant que les gens côté Rust aient réécrit coreutils sans expérience Linux, ce que je comprends encore moins, c’est comment Ubuntu a pu accepter ça en mainline

    • On dirait presque qu’Ubuntu suit une politique consistant à remplacer à chaque version ou presque un élément fondamental du système par une expérimentation bancale et inachevée

      À mon avis, l’essentiel ici n’est pas « mince, il y avait des bugs dans du code Rust », mais bien cela

    • L’original est sous licence GPL, et la réécriture sous licence MIT

  • Si c’est vrai que « ces bugs provenaient de code Rust réellement déployé, et que leurs auteurs savaient ce qu’ils faisaient »,
    alors je me demande si cela signifie que les utilitaires d’origine n’avaient pas de test harness, et que la réécriture n’a pas commencé par en construire un

    Même s’il y a beaucoup de cas limites, on devrait pouvoir abstraire au moins en partie l’OS et le FS pour vérifier qu’un rm .// ne supprime pas réellement le répertoire courant, par exemple

    Ce n’est pas tant un problème de code sale ou une critique du langage qu’une nouvelle manifestation de cette vieille attitude selon laquelle on ne teste pas en programmation système

    À l’inverse, si les utilitaires d’origine avaient bien des tests et qu’il y avait malgré tout autant de trous, il se pourrait que la suite de tests d’origine soit elle-même très insuffisante

    • C’est probable

      Mais je suis moins convaincu qu’on puisse abstraire suffisamment l’OS et le FS pour tout valider
      Des gens essaient ça depuis avant ma naissance, et ils n’ont apparemment toujours pas réussi

      Rien que décider combien de / il faut ajouter dans les essais n’est déjà pas évident

      Plus loin encore, si on suppose que rm refuse de supprimer un fichier lorsque ses 9 premiers octets valent important,
      il est difficile d’imaginer comment un test pourrait découvrir ce comportement sans connaître à l’avance cette chaîne
      Et ce serait encore plus difficile si le mot magique n’était même pas un mot du dictionnaire

      Je n’ai presque jamais vu quelqu’un dire sérieusement « on ne teste pas en programmation système »
      En revanche, j’entends souvent dire que les tests ne jouent pas toujours le rôle que les gens attendent d’eux

    • Si j’ai bien compris, le développement de uutils comprenait de vastes tests de comparaison de comportement avec les utilitaires d’origine, allant jusqu’à essayer de préserver leurs bugs

    • C’est aussi pour ce genre de raison que Windows désactive les symlinks par défaut
      Au lieu de résoudre le problème par abstraction, ils retirent pratiquement la fonctionnalité elle-même

      Les systèmes de type Unix ne peuvent pas faire ça, parce qu’ils ont des décennies de logiciels dépendant des symlinks

      MacOS a des réponses du même ordre
      Par exemple, le bug chroot() n’y pose généralement pas un vrai problème dans la configuration par défaut, parce que MacOS bloque chroot() par défaut
      Pour l’utiliser, il faut désactiver system integrity protection

      Le problème fondamental se trouve dans les angles vifs des API POSIX, et la solution n’est pas tant de les abstraire que de les faire disparaître

  • Je pense que laisser les gens expérimenter et tâtonner maladroitement, c’est très bien
    C’est comme ça qu’on apprend et qu’on grandit

    Ce qui m’intrigue vraiment, c’est de savoir à quel point la chaîne de décision d’Ubuntu a dysfonctionné pour que ça arrive en production

    • Parfois, grandir, c’est juste devenir plus grand