---
name: detecteur-toctou-fichiers
description: Détecteur de patrons check-then-act (.exists()/.is_file()/.is_dir() suivi d'une opération fichier mutante sur le même identifiant) dans du code source Rust — risque de race condition Time-Of-Check-To-Time-Of-Use. Bibliothèque standard uniquement, zéro dépendance, zéro appel réseau.
theme: securite-souverainete
langages_cibles: rust
---
# detecteur-toctou-fichiers
## Objectif
Scanner un dossier de code source Rust et détecter, à l'intérieur d'une
même fonction, une vérification d'existence sur un fichier (`.exists()`,
`.is_file()`, `.is_dir()`) suivie, plus loin, d'une opération mutante
(écriture, création, suppression, renommage, copie, ouverture) portant sur
le **même identifiant textuel**. Entre la vérification et l'action, un
autre processus ou thread peut avoir modifié l'état du fichier — c'est le
patron classique **TOCTOU** (Time-Of-Check-To-Time-Of-Use) : l'hypothèse
vérifiée n'est plus garantie au moment où elle est utilisée.
## Pourquoi ce skill existe
Le bug corrigé dans le projet qui a produit ce skill le jour même
(unicité de login insensible à la casse) était exactement un TOCTOU — pas
sur un fichier, mais sur une entrée d'un magasin clé-valeur : une lecture
(`lire_membre`) suivie d'une écriture (`inserer_membre`) sans transaction
atomique unique, laissant une fenêtre de course entre les deux. Ce skill
cible spécifiquement la variante **fichier** du même patron, souvent plus
facile à repérer statiquement qu'une variante sur un magasin de données
personnalisé.
## Pourquoi Rust (et pas un autre langage) ?
Même famille de raisons que les 3 autres skills Rust du projet
(`linter-temps-constant`, `detecteur-async-bloquant`, `detecteur-panic-points`) :
c'est le langage du code analysé, un binaire compilé scanne un gros
répertoire `src/` en quelques secondes, et la stdlib suffit entièrement.
**Conception** : suivi de profondeur d'accolades/parenthèses caractère par
caractère (même technique que les 2 autres skills Rust les plus récents du
projet), avec exclusion des chaînes de caractères (normales et brutes
`r#"..."#`) et des commentaires. La corrélation entre la vérification et la
mutation se fait par **identifiant textuel** : l'expression réceptrice de
`.exists()` (ex. `chemin`, `self.chemin`) doit apparaître littéralement
dans la fenêtre qui suit l'appel mutant repéré plus loin dans la même
fonction.
## Garanties de conception (non négociables)
- **Zéro dépendance tierce**, **zéro appel réseau**.
- **Contenu scanné traité comme donnée inerte**, jamais exécuté ni interprété.
- **Fonctions cœur 100% pures et testées isolément** : `construire_masque_code`,
`extraire_fonctions`, `extraire_identifiant_recepteur`,
`rechercher_mutation_associee`, `detecter_toctou` ne font aucune I/O.
- **Exemption ligne par ligne** via le marqueur `toctou-ok` en commentaire,
pour les cas où l'usage check-then-act est délibérément acceptable
(ex. simple UX, pas un chemin de sécurité).
- **`eprintln!` au lieu de `tracing`** : même raison que les autres skills
Rust du projet — outil CLI ponctuel, pas un serveur long-running.
## Conformité aux règles du projet (verify_rules_rust.sh)
```bash
bash ../../outils/verify_rules_rust.sh --src src
```
**Résultat au 2026-07-28 : 1 violation (§31b) + 2 avertissements (§5), tous
vérifiés et confirmés faux positifs.** La violation §31b pointe une chaîne
de caractères utilisée comme fixture de test (`"fn f(&self) {\n if
self.chemin.is_file() {\n File::open(self.chemin);\n }\n}\n"`, extrait de
code Rust fictif passé en entrée à `detecter_toctou()` pour vérifier la
détection) — vérifié explicitement que le fichier ne contient, en dehors du
module `#[cfg(test)]`, que l'import `use std::fs;` et les 2 littéraux du
tableau `MOTIFS_MUTATION` (`"File::create("`, `"File::open("`), jamais un
vrai appel. Les 2 avertissements §5 sont identiques, ligne pour ligne, au
cas déjà documenté dans les 3 autres skills Rust du projet (fonction
`iter_fichiers_rust` reprise à l'identique).
## Mode d'emploi
**Compiler :**
```bash
cargo build --release
```
**Lancer :**
```bash
./target/release/detecteur-toctou-fichiers <dossier_cible>
```
**Tests unitaires** (12 tests, aucun réseau, aucune I/O disque réelle) :
```bash
cargo test
```
**Résultat réel obtenu lors de la validation de ce skill** (dossier `src/`
d'un projet Rust d'environ 657 fichiers, ~2,4 secondes après correction —
voir bug de performance ci-dessous) : 5 signalements — 1 sur `fs::remove_file`
(faible risque réel, un fichier déjà absent produit juste une erreur gérée),
2 sur `fs::create_dir_all` (idempotent par nature, risque réel quasi nul,
voir Limites connues), et surtout **1 signalement à risque réel plausible**
sur `fs::write` : une vérification `!chemin.exists()` suivie d'une écriture
d'un contenu par défaut — si un autre processus crée légitimement ce
fichier entre les deux, l'écriture par défaut l'écraserait silencieusement.
Résultat cohérent avec un projet qui privilégie déjà, sur l'essentiel de
son code, les opérations atomiques (transactions redb) plutôt que des
vérifications préalables séparées de l'action.
**Bug de performance réel trouvé et corrigé pendant la validation** : le
premier passage sur ce dépôt a pris **plus de 120 secondes** (contre 1 à 5
secondes pour les 3 autres skills Rust du projet sur le même dépôt) —
`trouver_prochaine_occurrence()` ne recevait aucune borne supérieure et
recherchait systématiquement jusqu'à la fin du FICHIER entier, y compris
quand l'appelant (`rechercher_mutation_associee`, appelée pour chacun des
10 motifs de mutation à chaque vérification trouvée) ne s'intéressait qu'à
la fonction courante. Corrigé en ajoutant un paramètre `limite` explicite à
la fonction, borné à la fin de la fonction analysée dans tous les appels
concernés — **2,4 secondes après correction, résultats de détection
strictement identiques** (0 changement de comportement, vérifié par diff
byte à byte du rapport avant/après).
**Prérequis :** Rust stable (testé avec rustc 1.93, édition 2024). Aucune
dépendance à installer.
## Limites connues (honnêteté de l'outil, pas de sur-promesse)
- **Corrélation par identifiant TEXTUEL, pas une analyse de flot de
données réelle** — même limite assumée que la règle §32 (DashMap) du
projet qui a produit ce skill : un alias (`let c2 = chemin.clone()`) ou
un renommage de variable entre la vérification et la mutation casse la
corrélation (faux négatif), et à l'inverse deux variables de même nom
dans des portées différentes de la même fonction pourraient être
corrélées à tort (faux positif) — cas rare en pratique.
- **Récepteur limité à un enchaînement simple d'identifiants** : un appel
de fonction comme `obtenir_chemin().exists()` n'est pas reconnu (le
récepteur contient des parenthèses) — la vérification est alors ignorée
plutôt que de risquer une corrélation incorrecte. Cas courant à connaître
si le code cible utilise beaucoup de fonctions d'accès plutôt que des
variables directes.
- **Toutes les mutations détectées ne représentent pas le même niveau de
risque** : `fs::create_dir_all`/`fs::remove_dir_all` sont idempotents
(ne provoquent pas d'erreur si l'état a déjà changé), donc un
signalement sur ce motif est un risque bien plus faible qu'un
signalement sur `File::create`/`File::open` utilisé pour une création
exclusive supposée (le vrai risque de sécurité classique du TOCTOU) —
une lecture humaine reste nécessaire pour distinguer les deux.
- **Fenêtre de recherche bornée** (300 caractères après la mutation
détectée, jusqu'au prochain `;`) — une mutation très éloignée dans une
fonction longue, ou dont les arguments sont répartis sur une expression
complexe dépassant cette fenêtre, peut échapper à la détection.
- Ne remplace pas une revue de sécurité humaine ni des tests de
concurrence réels (`loom`, tests de charge) — outil de dégrossissage
rapide et reproductible, pas une preuve d'absence de race condition.