📄 SKILL.md 🔒 ff455a30…535668c2 Se connecter pour télécharger ← Retour
---
name: detecteur-async-bloquant
description: Détecteur d'appels bloquants (I/O fichier std, hachage Argon2/BLAKE3, verrou std::sync::Mutex, thread::sleep, transactions redb) à l'intérieur d'une fonction async sans spawn_blocking, dans du code source Rust. Bibliothèque standard uniquement, zéro dépendance, zéro appel réseau.
theme: securite-souverainete
langages_cibles: rust
---

# detecteur-async-bloquant

## Objectif

Scanner un dossier de code source Rust, retrouver chaque fonction `async fn`
(en excluant les chaînes de caractères et commentaires du suivi
d'accolades — voir "Pourquoi Rust" ci-dessous), et signaler tout appel
bloquant probable dans son corps (I/O fichier `std::fs`, `thread::sleep`,
hachage `Argon2`/`BLAKE3`, verrou `.lock()` sans `.await` — signature d'un
`std::sync::Mutex` plutôt qu'un `tokio::sync::Mutex` —, transaction redb
`begin_write`/`begin_read`, `reqwest::blocking`) qui n'est pas délégué à
`tokio::task::spawn_blocking`. Un tel appel bloque le thread du runtime
tokio et peut, sous charge, geler l'ensemble d'un serveur asynchrone.

## Pourquoi Rust (et pas un autre langage) ?

Même famille de raisons que `linter-temps-constant`, le premier skill Rust
du projet : 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 — aucune dépendance vers `syn`/`proc-macro2` pour une analyse
syntaxique réelle.

**Différence de conception notable avec `linter-temps-constant`** : ce
skill ne peut pas se contenter d'un scan ligne par ligne, car la question
posée ("cet appel est-il DANS une fonction async, et cette fonction
contient-elle par ailleurs `spawn_blocking` ?") a besoin d'un minimum de
contexte de portée. `extraire_fonctions_async()` fait donc un suivi de
profondeur d'accolades/parenthèses caractère par caractère, avec exclusion
explicite des chaînes de caractères (normales et brutes `r#"..."#`) et des
commentaires — sans cette exclusion, un payload JSON littéral dans une
chaîne brute (très courant dans du code serveur qui sérialise des messages)
contient des accolades qui fausseraient le comptage et couperaient le corps
de fonction au mauvais endroit. Cette exclusion reste néanmoins un filtrage
textuel, pas un vrai analyseur syntaxique Rust (voir Limites connues).

## Garanties de conception (non négociables)

- **Zéro dépendance tierce** : `Cargo.toml` ne déclare aucune dépendance.
- **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** : `extraire_fonctions_async`,
  `trouver_corps_fonction`, `sauter_chaine_ou_commentaire`, `ligne_bloquante`
  et `analyser_fonction_async` ne font aucune I/O — testables sur de simples
  chaînes de caractères.
- **Deux mécanismes d'exemption distincts, à dessein** : `spawn_blocking`
  n'importe où dans le corps exempte TOUTE la fonction (le projet
  solivram-website délègue déjà systématiquement ainsi) ; le marqueur de
  commentaire `async-bloquant-ok` en fin de ligne exempte une ligne précise,
  pour les cas où l'appel est justifié sans passer par `spawn_blocking`
  (ex. une écriture non bloquante par construction malgré le nom de
  fonction).
- **`eprintln!` au lieu de `tracing`** : même raison que `linter-temps-constant`
  — 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-27 : 15 violations (§2, §18, §31b) + 2 avertissements
(§5), tous vérifiés un par un et confirmés faux positifs — 0 corrigé,
0 régression réelle.** Le code source de cet outil ne contient d'ailleurs
**aucune fonction `async fn` réelle** (c'est un CLI synchrone) : les
9 occurrences du texte `async fn` dans `main.rs` sont soit dans la
documentation, soit — pour la totalité des cas déclenchant une violation —
à l'intérieur de **chaînes de caractères** utilisées comme données de test
(des extraits de code Rust fictif passés en argument à
`extraire_fonctions_async()`/`ligne_bloquante()` pour vérifier que l'outil
les détecte correctement). Le scanner `verify_rules_rust.sh`, lui-même un
outil d'analyse textuelle sans AST, ne distingue pas une chaîne littérale
d'un vrai appel de code — **exactement la même catégorie de limite que ce
skill documente pour lui-même** ci-dessous (deux scanners textuels qui
partagent le même angle mort, de façon assez cohérente).

Détail des 3 catégories, toutes vérifiées par lecture directe du code
(aucune n'a de contrepartie réelle hors chaîne de caractères) :

- **§2 (.unwrap() interdit)** : `"std::fs::write(\"a\", \"b\").unwrap();".to_string()`
  — le texte `.unwrap()` fait partie du contenu de la chaîne, la seule
  méthode réellement appelée sur cette ligne est `.to_string()` sur un
  littéral `&str` (infaillible).
- **§18 (.await sans match interdit)** : les 3 occurrences signalées sont
  toutes soit un argument littéral de `.contains(".await")` (recherche de
  texte, pas une expression await), soit une chaîne de test comme
  `"let garde = mon_mutex.lock().await;"` passée en entrée à
  `ligne_bloquante()` pour vérifier qu'elle reconnaît bien ce cas comme
  "déjà correct" (voir test `t_ligne_bloquante_05_lock_avec_await_ignore`).
- **§31b (fn sync bloquante appelée sans spawn_blocking)** : le scanner
  attribue à tort le contenu textuel d'une chaîne de test (ex. la variable
  `corps` d'une fixture `FonctionAsync`, ou l'argument d'un `assert_eq!`) à
  la fonction Rust qui l'englobe (`ligne_bloquante`, une fonction de test,
  ou même une fonction fictive nommée `charger` qui n'existe que comme texte
  à l'intérieur d'une chaîne) — aucune de ces fonctions réelles n'appelle
  effectivement `std::fs`, `std::sync::Mutex` ou tout autre appel bloquant.

### 2 avertissements §5 — identiques au précédent déjà documenté

Les 2 avertissements §5 (`None => false` dans le calcul de `est_rs`) sont
strictement identiques, ligne pour ligne, au cas déjà rencontré et accepté
dans `linter-temps-constant/SKILL.md` (fonction `iter_fichiers_rust` reprise
à l'identique) — même raisonnement, non répété ici.

## Mode d'emploi

**Compiler :**

```bash
cargo build --release
```

**Lancer :**

```bash
./target/release/detecteur-async-bloquant <dossier_cible>
```

**Tests unitaires** (19 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, majoritairement asynchrone,
~1 seconde) : 27 signalement(s) au total — 17 `std::fs::` (essentiellement
des `remove_file`/`create_dir_all`/`metadata` en mode "best effort", non
critiques), 6 `fs::write` et 4 `.lock() sans .await`, ces deux dernières
catégories concentrées presque exclusivement dans du code de **test**
(fonctions `#[tokio::test]` qui écrivent dans un fichier temporaire ou
utilisent un verrou global de sérialisation de tests) plutôt que dans le
code de service lui-même. Ce résultat confirme deux choses : le mécanisme
d'exemption `spawn_blocking` fonctionne comme prévu (la très large majorité
des appels potentiellement bloquants du code de **service** sont déjà
enveloppés, cohérent avec un projet qui applique cette règle de façon
systématique — voir `feedback_arcswap_vs_spawn_blocking` et l'historique
`SPAWN-BLOCKING-CMS-FS-02` de ce projet), et l'exclusion `tokio::fs::`
(ajoutée après un premier passage de validation qui l'avait révélée
nécessaire — voir plus bas) évite bien de confondre l'équivalent async
officiel avec un appel bloquant.

**Correction apportée pendant la validation** : le premier passage sur ce
dépôt réel a révélé 2 faux positifs authentiques (pas liés à une chaîne de
test, cette fois) — `tokio::fs::create_dir_all(...)` et
`tokio::fs::rename(...)` étaient signalés comme `fs::create_dir`/`fs::rename`
bloquants, alors que `tokio::fs::*` est précisément le remplacement
asynchrone officiel de `std::fs::*` (mêmes noms de méthode par conception).
Corrigé en retirant le préfixe `tokio::fs::` avant le filtrage des motifs
(voir `ligne_bloquante()`) — 2 tests dédiés ajoutés
(`t_ligne_bloquante_08`/`09`), 19/19 tests passent, le nouveau passage de
validation sur le même dépôt ne contient plus aucune occurrence de
`tokio::fs`.

**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)

- **Pas un AST complet** : l'exclusion des chaînes/commentaires protège
  contre le cas le plus dommageable (accolades dans un payload JSON en
  chaîne brute) mais ne gère pas les littéraux de caractère (`'{'`), rares
  et à faible risque.
- **Fonctions `async` imbriquées** (une fonction async définie à l'intérieur
  du corps d'une autre) : chacune est capturée indépendamment, ce qui peut
  produire un signalement en double pour le même appel bloquant, vu depuis
  la fonction englobante ET la fonction imbriquée — cas rare en pratique.
- **`async move { ... }` / blocs `async { ... }` (pas `async fn`)** ne sont
  pas capturés — seule la forme `async fn nom(...)` déclenche l'extraction.
- **Position de ligne approximative** en cas de signature multi-lignes très
  inhabituelle avant l'accolade ouvrante (générique complexe, clause `where`
  sur plusieurs lignes) — la ligne de référence est toujours celle de
  l'accolade ouvrante réelle, pas celle du mot-clé `async fn`, ce qui limite
  fortement ce risque sans l'éliminer totalement.
- **Détection par motifs textuels, pas sémantique** : comme démontré par les
  faux positifs de `verify_rules_rust.sh` documentés ci-dessus sur ce skill
  lui-même, un texte qui *ressemble* à un appel bloquant (dans une chaîne de
  caractères, un nom de variable, un commentaire non préfixé par `//` en
  début de ligne visible) peut en théorie produire un faux positif
  symétrique dans ce skill — aucun cas de ce type n'a cependant été observé
  lors de la validation sur un dépôt réel.
- Ne remplace pas une revue de sécurité/performance humaine, ni un outil
  d'analyse dynamique (profiling sous charge réelle) — outil de
  dégrossissage rapide et reproductible, pas une preuve d'absence de
  blocage.
10.2 Ko BLAKE3 : ff455a30…535668c2