--- name: detecteur-lock-order description: Détecteur de 3 classes de risques d'interblocage sur des structures concurrentes (carte partagée type DashMap, verrou type Mutex) dans du code source Rust — réentrance sans libération, opération lourde ou verrou imbriqué sous verrou, ordre d'acquisition incohérent entre fonctions. Bibliothèque standard uniquement, zéro dépendance, zéro appel réseau. theme: securite-souverainete langages_cibles: rust --- # detecteur-lock-order ## Objectif Scanner un dossier de code source Rust et détecter 3 patrons distincts de risque d'interblocage (deadlock) sur des structures concurrentes : - **R1 — réentrance sans libération** : un guard obtenu via `.get(`/ `.get_mut(`/`.entry(` sur une structure `X` reste lié à une variable pendant qu'un second appel (`.get(`/`.get_mut(`/`.entry(`/`.insert(`/ `.remove(`/`.contains_key(`) sur la **même** structure `X` survient avant tout `drop()` explicite du premier guard — l'équivalent d'un « upgrade » interdit d'un verrou de lecture en écriture sur la même clé. - **R2 — opération lourde ou verrou imbriqué sous verrou** : un guard de verrou classique (`.lock()`) reste lié pendant qu'une opération jugée lourde (hachage/dérivation de clé, I/O fichier, appel réseau bloquant) ou un second verrou sur un identifiant **différent** survient dans sa portée avant tout `drop()`. - **R3 — ordre d'acquisition incohérent** : deux fonctions du dépôt verrouillent la même paire d'identifiants dans un ordre inversé — le scénario classique d'interblocage entre deux threads concurrents qui acquièrent les mêmes verrous dans des ordres opposés. ## Pourquoi ce skill existe Un bug réel de ce type — un guard obtenu par lecture sur une carte concurrente maintenu vivant pendant qu'un second appel sur la même carte était effectué, sans libération explicite entre les deux — a déjà été identifié et corrigé sur du code Rust concurrent audité par ailleurs. La règle de fond qui en découle est simple à énoncer mais facile à violer sans s'en rendre compte dans du code par ailleurs correct : > Pour éviter les interblocages sur une structure concurrente : extraire > les données nécessaires et libérer le verrou avant tout calcul lourd. > Ne jamais tenter de « mettre à niveau » un verrou de lecture en écriture > sur la même clé — libérer d'abord la lecture, puis demander l'écriture. > S'assurer que tous les threads acquièrent plusieurs verrous dans le même > ordre absolu, ou mieux, éviter de détenir plusieurs verrous à la fois. Ce skill généralise cette règle en 3 détecteurs statiques, applicables à n'importe quel dépôt Rust utilisant des structures concurrentes de ce type. ## 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`, `detecteur-toctou-fichiers`) : c'est le langage du code analysé, un binaire compilé scanne un gros répertoire `src/` en quelques secondes, et la stdlib (y compris `std::collections::HashMap`, utilisée uniquement pour le registre interne de R3 — jamais pour scanner du texte externe) suffit entièrement. **Conception** : suivi de profondeur d'accolades/parenthèses caractère par caractère (même technique que les autres skills Rust du projet), avec exclusion des chaînes de caractères (normales et brutes `r#"..."#`) et des commentaires. R1/R2 détectent une liaison `let NOM = X.methode(...)` ou `if let Some(NOM) = X.methode(...)` juste avant l'appel, puis cherchent un second appel conflictuel dans une fenêtre bornée (jusqu'à la fin de la fonction englobante, ou jusqu'au premier `drop(NOM)` rencontré). R3 agrège, par fonction, la séquence des identifiants distincts verrouillés via `.lock(` dans leur ordre de première apparition, puis compare toutes les paires entre toutes les fonctions du dépôt scanné. ## 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`, `detecter_binding_avant`, `detecter_reentrance_carte`, `detecter_lourd_sous_verrou`, `detecter_ordre_incoherent` ne font aucune I/O. - **Exemption ligne par ligne** via le marqueur `lock-order-ok` en commentaire, pour les cas où le patron détecté est délibérément sûr (revue manuelle effectuée). - **`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-30 : 0 violation bloquante (12 occurrences §18/§31b initialement signalées, toutes vérifiées et confirmées faux positifs — des chaînes de caractères utilisées comme fixtures de test contenant le texte littéral `fn`/`Mutex`/`.lock()`/`.await`, jamais du vrai code exécutable en dehors du module `#[cfg(test)]`, annotées `§18-ok`/`§31b-ok`) + 3 avertissements non bloquants (2× §5 déjà documentés dans les autres skills Rust du projet, 1× §26 suggestion de style clippy non retenue — cohérence avec le rejet de clippy du projet source, jugé trop générique).** ## Mode d'emploi **Compiler :** ```bash cargo build --release ``` **Lancer :** ```bash ./target/release/detecteur-lock-order <dossier_cible> ``` **Tests unitaires** (22 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 658 fichiers, ~5,1 secondes) : 42 signalements R1/R2, 0 incohérence R3. Analyse manuelle d'un échantillon représentatif : la majorité provient de l'absence de connaissance de TYPE de l'outil (voir Limites connues) — des méthodes `.get()`/`.insert()` de mêmes noms sur des structures **non concurrentes** (valeurs JSON désérialisées, tables de hachage simples) sont lexicalement indiscernables d'une vraie carte concurrente. Un sous-ensemble minoritaire touchait de vraies structures concurrentes déclarées comme telles dans le code, mais restait dans des fermetures/blocs internes distincts et sûrs en pratique — la fenêtre d'analyse du guard (bornée à la fin de la fonction englobante, pas au bloc exact) sur-approxime délibérément le risque plutôt que d'en manquer un vrai, au prix de ce type de faux positif. 0 vraie incohérence d'ordre (R3) trouvée sur ce dépôt. **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) - **Aucune connaissance de type — purement lexical** : l'outil ne sait pas distinguer une vraie carte concurrente d'une structure exposant les mêmes noms de méthode (une valeur JSON désérialisée, une table de hachage simple, une table de base embarquée) — c'est la source principale des faux positifs observés lors de la validation réelle. Une revue humaine reste nécessaire pour confirmer que le récepteur signalé est réellement une structure concurrente avant d'agir sur un signalement. - **`.lock()` sur un handle d'entrée/sortie standard** (verrouillage d'un flux stdin/stdout/stderr pour un accès exclusif à la console) partage la même syntaxe que le verrouillage d'un vrai Mutex de synchronisation — angle mort lexical déjà connu de ce type d'outil, qui ne peut être levé qu'en connaissant le type réel du récepteur. - **Portée du guard approximée par la fin de la fonction englobante**, pas par le bloc/fermeture exact où la liaison a été créée — un guard réellement droppé à la fin d'un bloc interne (fermeture, `if`, etc.) avant un second appel plus loin dans la même fonction peut donc être signalé à tort. Choix assumé : sur-approximation délibérée (jamais manquer un vrai risque au prix d'un signalement en trop), documentée honnêtement plutôt que masquée. - **R3 corrèle par identifiant TEXTUEL du récepteur**, même limite assumée que les autres skills du projet fondés sur une corrélation lexicale — un alias (`let b = a;`) casse la corrélation (faux négatif), et à l'inverse deux identifiants de même nom référençant des structures logiquement différentes dans deux fonctions distinctes pourraient être comparés à tort (faux positif). - **Un guard renommé via un pattern complexe** (destructuration, tuple, fermeture capturant implicitement) peut échapper à R1/R2 — seuls les patrons `let NOM = ...`/`let mut NOM = ...`/`if let Some(NOM) = ...` sont reconnus. - Ne remplace pas une revue de sécurité humaine ni des tests de concurrence réels (`loom`, tests de charge sous contention) — outil de dégrossissage rapide et reproductible, pas une preuve d'absence d'interblocage.