Les bugs que Rust ne détecte pas
(corrode.dev)- 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 quefrom_utf8_lossy,unwrapouexpect, 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::fsde Rust repose par défaut sur une réinterprétation à partir de&Path, ce qui rend ce type d’erreur facile à commettrefs::metadata,File::create,fs::remove_fileetfs::set_permissionsréinterprètent le chemin à chaque appel- Dans les outils privilégiés qui doivent résister à des attaquants locaux, ce comportement par défaut devient dangereux
- 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 deFile::create(to)? - Si, entre la suppression et la création,
toest remplacé par un lien symbolique pointant vers/etc/shadowou une cible similaire, un processus privilégié peut écraser ce fichier
- Dans
- La correction a consisté à utiliser
OpenOptions::create_new(true)afin de ne créer que de nouveaux fichiers- D’après la documentation,
create_newn’autorise à l’emplacement cible ni fichier existant, ni dangling symlink
- D’après la documentation,
- 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
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
chmodensuite, crée lui aussi une courte fenêtre d’exposition- Un code du type
fs::create_dir(&path)?suivi defs::set_permissions(&path, Permissions::from_mode(0o700))?laissepathexister avec ses permissions par défaut pendant cet intervalle - D’autres utilisateurs peuvent faire
open()pendant cette fenêtre, et unchmodultérieur ne révoquera pas les descripteurs de fichier déjà obtenus
- Un code du type
- Les permissions doivent être spécifiées au moment de la création
- Il faut utiliser
OpenOptions::mode()etDirBuilderExt::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
- Il faut utiliser
Comparer des chaînes de chemin ne prouve pas l’identité dans le système de fichiers
- Le premier contrôle de
--preserve-rootdanschmodne faisait qu’une comparaison de chaînesrecursive && 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
canonicalizerenvoie 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,rmrejetait.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
Aux frontières Unix, il faut privilégier les bytes plutôt que les chaînes
- Les types
Stringet&strde 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_lossyremplace les bytes invalides parU+FFFDet corrompt discrètement les données - Une conversion stricte comme
unwrapou?peut rejeter l’entrée ou faire terminer le processus
- Une conversion avec perte comme
- Le
CVE-2026-35346decommétait un cas où la conversion avec perte dégradait la sortie- Dans
src/uu/comm/src/comm.rs, les bytes d’entréeraetrbétaient convertis parString::from_utf8_lossyavant unprint! - GNU
commrecopie les bytes tels quels, même avec des fichiers binaires, mais uutils remplaçait l’UTF-8 invalide parU+FFFD, ce qui altérait la sortie - La correction a consisté à écrire directement les bytes bruts vers
stdoutviaBufWriteretwrite_all
- Dans
print!impose un aller-retour UTF-8 viaDisplay, alors queWrite::write_allne le fait pas- Dans du code système de type Unix, il faut choisir les bons types selon le contexte
- Faire transiter les données par
Stringpour 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 etfrom_utf8peuvent devenir des points de DoS si un attaquant contrôle l’entréepanic!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-35348desort --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
sorttraite 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
- Le parseur appelait
- Dans le code qui traite des entrées non fiables,
unwrap,expect, l’indexation et les conversionsasdoivent être considérés comme des CVE potentielles- Il faut préférer
?,get,checked_*,try_fromet remonter les véritables erreurs à l’appelant
- Il faut préférer
- Des règles clippy à activer en CI sont aussi proposées
unwrap_usedexpect_usedpanicindexing_slicingarithmetic_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 -Retchown -Rne 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
- Même si de nombreux fichiers précédents échouaient, si le dernier réussissait, la commande pouvait se terminer avec
ddappelaitset_len()puisResult::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
Resultavec.ok(),.unwrap_or_default()oulet _ =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-35369dekill -1est emblématique- GNU interprète
-1comme 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
- GNU interprète
- 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-35368concernait une exécution de code locale en root danschroot- 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’attaquantget_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
chrooteffectue la résolution de l’utilisateur avant lechroot- 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_namepasse par NSS et peutdlopendes moduleslibnss_*à 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
pwddeep path buffer overflownumfmtout-of-bounds readunexpand --tabsheap buffer overflowod --strings -Nécriture de NUL hors heap buffersortlecture d’1 byte avant heap buffersplit --line-bytesheap overwrite avec CVE-2024-0684b2sum --checklecture de mémoire non allouée sur entrée mal forméetail -fstack buffer overrun
- 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
OsStrplutôt queString?plutôt queunwrap- 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
- An update on rust-coreutils : publication des résultats de l’audit
- Patterns for Defensive Programming in Rust : motifs de Rust défensif à lire en complément
- Pitfalls of Safe Rust : erreurs fréquentes possibles même en safe Rust
- Sharp Edges In The Rust Standard Library : comportements surprenants de
std - uutils/coreutils on GitHub : réimplémentation de GNU coreutils en Rust
Aucun commentaire pour le moment.