Les défaillances en production coûtent généralement « seulement » cher. Un pager sonne, le chiffre d’affaires fluctue, un client crie, on corrige et on passe à autre chose.
Therac-25 était différent : un ensemble de décisions logicielles et systémiques a transformé un appareil médical en une machine capable d’administrer
des surdoses catastrophiques de radiation — tout en indiquant avec assurance à l’opérateur que tout allait bien.
Si vous développez des services, des plateformes, du stockage ou n’importe quoi qui déploie du code dans le monde, ce cas n’est pas de l’histoire ancienne.
C’est un rapport de terrain sur ce qui arrive quand vous retirez des verrouillages matériels, faites confiance à une concurrence que vous ne comprenez pas, et traitez
les incidents comme des « erreurs utilisateur » jusqu’à ce que la chance vous abandonne.
Ce qu’était Therac-25 et pourquoi cela importait
Therac-25 était un accélérateur linéaire médical contrôlé par ordinateur utilisé pour la radiothérapie au milieu des années 1980.
Il pouvait fonctionner en plusieurs modes, y compris un mode faisceau d’électrons et un mode rayons X haute énergie. En fonctionnement sûr,
le matériel et le logiciel devaient coordonner pour s’assurer que l’énergie du faisceau et la configuration physique étaient alignées — cible,
filtres, collimateurs, et tout le bazar mécanique qui maintient les patients en vie.
La partie « pourquoi cela importait » n’est pas que le logiciel avait des bugs. Tout a des bugs. L’histoire, c’est que
la conception de l’appareil s’appuyait sur le logiciel pour fournir des garanties de sécurité que les machines plus anciennes imposaient avec des verrouillages matériels.
Quand le logiciel se trompait — et il se trompait de façon subtile, dépendante du timing — la machine pouvait délivrer une énorme surdose
sans alarme crédible ni verrouillage d’arrêt fiable.
Si vous avez déjà remplacé une rampe de sécurité physique par de la « logique dans le contrôleur », vous avez touché la même plaque chauffante.
Si vous avez déjà supprimé une vérification « lente » parce qu’elle « n’arrive jamais », vous vous êtes tenu dans le même rayon d’explosion.
Et si vous avez déjà traité le signal d’un opérateur comme du bruit parce que « le système dit OK », vous tenez déjà l’allumette.
Faits concrets et contexte historique (court, utile, pas pour le quiz)
- Les incidents du Therac-25 ont eu lieu au milieu des années 1980, avec plusieurs événements de surdose liés à des défauts de conception logiciel et système.
- Les machines antérieures (Therac-6 et Therac-20) utilisaient davantage de verrouillages matériels ; Therac-25 s’appuyait plus sur le logiciel pour la sécurité.
- Les opérateurs pouvaient déclencher un état dangereux via des séquences d’entrée rapides (comportement dépendant du timing), un symptôme classique de conditions de course/concurrence.
- Les messages d’erreur étaient cryptiques (par ex., codes de « dysfonctionnement » vagues), poussant le personnel à réessayer plutôt qu’à arrêter et escalader.
- Certaines surdoses ont d’abord été rejetées comme réactions de patients ou erreurs d’opérateur, retardant la mise en quarantaine efficace et la correction de la cause racine.
- Le code réutilisé était supposé sûr parce qu’il existait dans des produits antérieurs, même si le contexte de sécurité avait changé en réduisant les verrouillages matériels.
- La journalisation et les diagnostics étaient insuffisants pour reconstituer les événements rapidement, un cadeau pour le déni et une malédiction pour la réponse aux incidents.
- Les pratiques réglementaires et industrielles en matière de sécurité logicielle étaient moins matures qu’aujourd’hui ; les méthodes formelles et la V&V indépendante rigoureuse n’étaient pas uniformément appliquées.
- Le cas est devenu une leçon fondamentale en sécurité logicielle, enseignée pendant des décennies en éthique de l’ingénierie et en fiabilité.
Deux de ces faits devraient vous mettre mal à l’aise : « code réutilisé supposé sûr » et « erreurs cryptiques entraînant des réessais ».
C’est 2026 sous un autre chapeau.
La chaîne de défaillance : des frappes clavier à la dose létale
1) La sécurité est passée du matériel vers le logiciel, mais le logiciel n’était pas construit comme un système de sécurité
Dans un appareil critique pour la sécurité, vous n’avez pas le droit de dire « le code doit faire X ». Vous devez dire « le système ne peut pas faire Y »,
même en cas de défaillance partielle : bits bloqués, anomalies de timing, séquences inattendues, entrées erronées, capteurs dégradés, coupures d’alimentation.
Les verrouillages matériels forçaient historiquement des transitions vers un état sûr, indépendamment de la confusion logicielle.
Therac-25 a réduit les verrouillages matériels et s’est reposé sur la coordination logicielle. Ce choix n’est pas automatiquement erroné,
mais il change la charge de la preuve. Si vous retirez des contraintes physiques, votre logiciel doit être conçu, vérifié
et surveillé comme s’il tenait le seul parachute.
2) Conditions de course : le genre qui se moque de votre plan de tests
Le motif tristement célèbre dans les discussions sur Therac-25 est que certaines séquences rapides d’opérateur pouvaient pousser le système dans un état interne invalide.
L’opérateur pouvait éditer les paramètres de traitement rapidement, et le système pouvait accepter les nouvelles valeurs alors que d’autres
parties de la logique de contrôle présumaient encore les anciennes valeurs. Vous avez alors « énergie du faisceau réglée pour le mode A » et « configuration mécanique
positionnée pour le mode B ». Ce décalage n’est pas « un bug ». C’est une configuration létale.
Les conditions de course sont des échecs de coordination. Elles passent les tests unitaires, elles se cachent des flux de travail normaux, et elles adorent
les « opérateurs rapides » parce que les humains sont étonnamment bons pour générer des timings bizarres. L’équivalent moderne est un système distribué qui « converge généralement »
mais écrit occasionnellement une configuration obsolète dans le mauvais groupe. Dans un contexte de sécurité, « occasionnellement » est inacceptable.
3) L’interface utilisateur et les messages d’erreur ont appris au personnel à continuer
Si vous voulez qu’un système échoue en sécurité, vous devez apprendre aux humains autour de lui ce à quoi ressemble « dangereux ». Dans le cas du Therac-25,
les opérateurs ont rapporté des codes d’erreur confus. L’appareil pouvait afficher une erreur, permettre à l’opérateur de continuer après l’avoir reconnue,
et n’indiquait pas clairement la gravité ni l’action correcte.
Une UI de sécurité n’optimise pas le débit. Elle optimise l’escalade correcte. Elle utilise un langage clair,
une gravité explicite et des actions requises non ambiguës. Quand l’interface rend le « réessayer » le chemin le plus simple,
ce n’est pas une erreur utilisateur lorsque des gens réessayeront. C’est du design.
4) Une faible observabilité a permis le déni
Après un incident, vous avez besoin de forensique : logs, instantanés d’état, relevés de capteurs, séquences de commandes, horodatages,
et un moyen de les corréler. Les diagnostics du Therac-25 n’étaient pas suffisants pour une reconstruction rapide et fiable.
En l’absence de preuves, les organisations basculent vers des récits. Le récit le plus commun est « erreur opérateur ».
L’observabilité est un contrôle, pas un luxe. Dans les systèmes critiques pour la sécurité, c’est aussi une obligation morale :
vous ne pouvez pas réparer ce que vous ne voyez pas, et vous ne pouvez pas prouver la sécurité avec des impressions.
5) La réponse aux incidents était traitée comme une anomalie, pas comme un signal
Une surdose devrait déclencher une réponse de sécurité immédiate : confinement, avis de sécurité, enquête indépendante et biais
en faveur de « le système est coupable jusqu’à preuve du contraire ». Au lieu de cela, le schéma historique montre une reconnaissance tardive
et des dommages répétés à travers les événements.
C’est un mode de défaillance organisationnelle reconnaissable : traiter un incident comme un « cas étrange » isolé, patcher localement, et passer à autre chose.
Le problème est que le deuxième incident n’est pas « étrange ». C’est votre système qui vous dit la vérité, plus fort.
6) La sécurité est une propriété du système ; Therac-25 était optimisé comme un produit
Les gens résument parfois Therac-25 par « un bug logiciel a tué des patients ». C’est incomplet et légèrement rassurant,
ce qui explique sa persistance. L’histoire plus profonde est que plusieurs couches ont échoué :
hypothèses de conception, verrouillages manquants, défauts de concurrence, diagnostics médiocres, et une culture de réponse qui ne considérait pas
les rapports comme urgents.
Une citation appartient ici, parce qu’elle coupe à travers beaucoup de fadaises modernes :
idée paraphrasée : « L’espoir n’est pas une stratégie. »
— Général Gordon R. Sullivan (souvent cité dans les contextes de fiabilité et de planification).
Remplacez « espoir » par « devrait », « habituellement » ou « ça n’arrivera pas », et vous obtenez un générateur fiable de post-mortem.
Blague n°1 : Les conditions de course sont comme les cafards — si vous en voyez un en production, supposez qu’il y en a quarante autres cachés derrière « ça marche sur ma machine ».
Leçons systèmes : à copier, à ne jamais copier
Leçon A : Ne retirez pas les interlocks sans les remplacer par des garanties plus fortes
Les verrouillages matériels sont des instruments rugueux, mais ils sont brutalement efficaces. Ils échouent de manières prévisibles et sont difficiles
à contourner accidentellement. Les verrouillages logiciels sont flexibles, mais la flexibilité n’est pas la même chose que la sécurité.
Si vous déplacez la sécurité dans le logiciel, vous devez augmenter la discipline d’ingénierie en conséquence :
analyse formelle des dangers, exigences explicites de sécurité, vérification indépendante, bancs de test qui cherchent à casser le timing,
et surveillance à l’exécution qui suppose que le code peut être incorrect.
En termes SRE : ne supprimez pas vos disjoncteurs simplement parce que « le nouveau service mesh gère les retries ».
Leçon B : Traitez le timing et la concurrence comme des risques de première classe
Les modes de défaillance les plus notoires de Therac-25 dépendent du timing. La leçon opérationnelle clé est que vous devez tester le système
sous un timing adversaire : entrées rapides, capteurs retardés, réordonnancement, mises à jour partielles, caches obsolètes, et périphériques lents.
Si vous ne pouvez pas le modéliser, fuzzez-le. Si vous ne pouvez pas le fuzz-er, isolez-le avec des contraintes strictes.
Dans les plateformes modernes : si votre sécurité dépend de « ce gestionnaire d’événements ne sera pas réentré », vous pariez des vies sur un commentaire.
Utilisez des verrous, l’idempotence, des machines à états avec transitions explicites, et des invariants vérifiés à l’exécution.
Leçon C : L’UI fait partie de l’enveloppe de sécurité
Les interfaces doivent rendre l’action sûre facile et l’action dangereuse difficile. Les messages d’erreur doivent être exploitables. Les alarmes doivent être graduées.
Et le système ne doit pas afficher « tout va bien » quand il ne peut pas réellement le prouver.
Si votre système est incertain, il doit dire « je suis incertain », et il doit se défausser vers un état sûr. Les ingénieurs détestent cela parce que
c’est « conservateur ». Les patients aiment ça parce qu’ils restent en vie pour se plaindre de votre conception conservatrice.
Leçon D : Les postmortems sont des contrôles de sécurité, pas de la bureaucratie
Therac-25 porte aussi sur l’apprentissage institutionnel. Quand des incidents se produisent, vous avez besoin d’un processus d’incident qui extrait le signal :
ton sans blâme, analyse impitoyable, et actions de suivi orientées.
« Nous avons re-formé les opérateurs » n’est pas une correction quand le système a permis des discordances d’état létales. C’est du cosplay administratif.
Leçon E : La réutilisation n’est pas gratuite ; le code réutilisé hérite d’obligations nouvelles
Réutiliser du code d’anciens systèmes peut être intelligent. Ça peut aussi être la façon dont vous faites passer des anciennes hypothèses dans un nouvel environnement.
Si Therac-25 a réutilisé du code de systèmes avec verrouillages matériels, alors le modèle de sécurité implicite du code a changé.
En termes de fiabilité : vous avez changé le graphe de dépendances mais gardé le même SLO.
Lorsque vous réutilisez, vous devez re-valider le dossier de sécurité. Si vous ne pouvez pas écrire le cas de sécurité en langage clair et le défendre sous examen,
vous n’avez pas de cas de sécurité.
Trois mini-récits d’entreprise (anonymisés, plausibles, et malheureusement courants)
Mini-récit 1 : La mauvaise hypothèse qui a fait tomber la facturation (et presque la confiance)
Une entreprise de taille moyenne exécutait un pipeline d’événements « simple » : l’API écrit dans une file, des workers traitent, la base de données stocke les résultats.
Le système avait été stable pendant un an, ce qui est souvent le signe qu’il était sur le point de devenir la personnalité de tout le monde pendant une semaine.
Un ingénieur senior fit une hypothèse raisonnable : les IDs de message étaient globalement uniques. Ils étaient uniques par partition, pas globaux.
Le code utilisait l’ID du message comme clé d’idempotence dans un store Redis partagé. En charge normale, les collisions étaient rares.
Lors d’un pic de trafic, les collisions sont passées de « rares » à « suffisamment fréquentes pour être un bug ».
Le résultat fut des suppressions silencieuses : les workers croyaient avoir déjà traité un message et le sautaient. Les événements de facturation manquaient.
Les clients n’étaient pas surfacturés ; ils étaient sous-facturés. Ça paraît amusant jusqu’à l’arrivée du service finance avec des tableaux et des questions.
La surveillance ne le détecta pas rapidement parce que les taux d’erreur restaient bas. Le système était « sain » alors que la correction brûlait.
Le postmortem fut brutal : l’hypothèse était non documentée, non testée, et non observée. Ils corrigèrent en scindant les clés d’idempotence
en partition+offset, ajoutant des invariants, et construisant un job de réconciliation qui signalait les lacunes. Ils ajoutèrent aussi du trafic canari
qui créait intentionnellement des collisions pour vérifier la correction en conditions proches de la production.
La leçon de Therac-25 apparaît ici : lorsque les hypothèses sont fausses, le système ne plante pas toujours. Parfois il sourit et ment.
Mini-récit 2 : Une optimisation qui s’est retournée contre eux (parce que « rapide » n’est pas une exigence)
Une autre organisation disposait d’un service analytique appuyé sur du stockage. Ils voulurent réduire la latence, alors ils ajoutèrent un cache local sur chaque nœud
et supprimèrent une étape « redondante » de validation de checksum lors de la lecture de blobs depuis l’objet storage. La vérification coûtait cher,
disaient-ils, et la couche de stockage « gérait déjà l’intégrité ».
Pendant des mois tout alla bien. Latence en baisse, CPU en baisse, graphiques au vert. Puis une mise à jour du noyau introduisit un rare problème de corruption de données lié au DMA sur un type d’instance.
C’était peu fréquent, difficile à reproduire, et le store d’objets n’était pas en cause.
La corruption se produisait entre la mémoire et l’espace utilisateur à la lecture, et le checksum supprimé était le seul point de détection pratique.
Ce qui échoua ne fut pas seulement l’intégrité des données ; ce fut la réponse à l’incident. L’équipe chassa « mauvaises requêtes » et « clients bizarres »
pendant des jours parce que leurs tableaux de bord montraient du succès. La corruption était sémantique, pas au niveau transport.
Ils la trouvèrent finalement en comparant les résultats entre nœuds et en remarquant qu’un nœud était « créativement faux ».
Le rollback fut humiliant mais efficace : réactiver les checksums, ajouter une validation inter-nœuds dans les canaris,
et exécuter des jobs de scrub périodiques. L’optimisation fut réintroduite plus tard avec des garde-fous : les checksums étaient échantillonnés à fort taux,
et la validation complète forcée pour les jeux de données critiques.
Therac-25 avait retiré des couches qui rendaient les états dangereux plus difficiles. C’était la même erreur, juste avec moins de funérailles et plus de dashboards.
Mini-récit 3 : La pratique ennuyeuse qui a sauvé la mise (et personne n’a été promu pour ça)
Une plateforme de paiements avait une pratique que les ingénieurs moquaient comme « paranoïaque » : chaque release nécessitait un déploiement progressif
avec des invariants automatisés vérifiés à chaque étape. Pas seulement taux d’erreur et latence — invariants business : totaux, comptes, réconciliation contre
sources indépendantes.
Un vendredi après-midi (parce que bien sûr), une nouvelle release introduisit un subtil bug de double-application déclenché par la logique de retry
quand un service en aval faisait un timeout. Ça n’arrivait pas dans les tests d’intégration car les tests ne modélisaient pas réalistement les timeouts.
Ça arriva dans la première cellule canari avec du vrai trafic.
Les invariants le repérèrent en quelques minutes : les totaux du grand livre de la cellule canari dérivèrent comparés à la cellule témoin.
Le déploiement s’arrêta automatiquement. Pas d’interruption totale. Aucun impact client au-delà d’une poignée de transactions qui furent automatiquement
inversées avant settlement.
La correction fut simple : les jetons d’idempotence passèrent de « best effort » à « requis », et le chemin de retry fut modifié pour vérifier le statut de commit avant de rejouer.
Le point clé est que le processus a détecté le problème, pas des actes héroïques. Personne n’a reçu d’ovation. Tout le monde est rentré chez soi.
Therac-25 n’avait pas ce type de phase de staging guidée par des invariants, et le résultat fut des dommages répétés avant que le schéma ne soit accepté.
Ennuyeux, c’est bien. Ennuyeux, c’est survivable.
Mode d’emploi pour un diagnostic rapide : ce qu’il faut vérifier en premier / deuxième / troisième
Le désastre opérationnel du Therac-25 fut amplifié par un diagnostic lent et ambigu. Dans les systèmes en production, la vitesse compte — mais pas le
« changez des trucs au hasard vite » : le genre « établir le mode de défaillance rapidement ».
Premier : décider si le système ment ou s’il échoue bruyamment
- Cherchez un décalage entre les signaux « sains » et les dommages rapportés par les utilisateurs. Si les utilisateurs voient de la corruption, des résultats erronés ou un comportement dangereux, traitez les tableaux de bord comme non fiables.
- Vérifiez les invariants. Comptes, totaux, transitions d’état, verrouillages de sécurité. Si vous n’avez pas d’invariants, félicitations : vous avez des impressions.
- Confirmez le rayon d’impact. Un nœud ? Une région ? Un appareil ? Un flux de travail opérateur ?
Second : isoler la concurrence, l’état et le timing
- Soupçonnez les conditions de course lorsque : les symptômes sont intermittents, déclenchés par la vitesse, ou disparaissent quand vous ajoutez des logs.
- Forcez la sérialisation temporairement (worker unique, désactiver le parallélisme, verrouiller la section critique) pour voir si le problème disparaît.
- Capturez l’ordre des événements avec horodatages et IDs de corrélation. Si vous ne pouvez pas reconstruire l’ordre, vous ne pouvez pas raisonner sur la sécurité.
Troisième : vérifier l’enveloppe de sécurité et le comportement fail-safe
- Testez la transition vers l’« état sûr ». Le système s’arrête-t-il en cas d’incertitude, ou continue-t-il ?
- Vérifiez les interlocks : matériel, logiciel, gate de configuration, feature flags, disjoncteurs.
- Confirmez les directives opérateur : les messages d’erreur provoquent-ils l’action correcte, ou entraînent-ils des réessais ?
Blague n°2 : Si votre playbook d’incident dit « redémarrer et voir », ce n’est pas un playbook — c’est une planche Ouija avec une meilleure disponibilité.
Tâches pratiques : commandes, sorties et la décision que vous en tirez
Therac-25 n’a pas échoué parce que quelqu’un a oublié une commande Linux. Mais le thème opérationnel — preuves insuffisantes, diagnostic lent,
et invariants manquants — apparaît partout. Ci‑dessous des tâches concrètes que vous pouvez exécuter sur de vrais systèmes pour détecter les équivalents modernes :
conditions de course, transitions d’état dangereuses, « sain mais faux », et suppression de couches de sécurité.
Le schéma pour chaque tâche est : commande → sortie typique → ce que cela signifie → décision à prendre.
Tâche 1 : Confirmer la synchronisation temporelle (l’ordre compte)
cr0x@server:~$ timedatectl status
Local time: Mon 2026-01-22 10:14:07 UTC
Universal time: Mon 2026-01-22 10:14:07 UTC
RTC time: Mon 2026-01-22 10:14:06
Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
Ce que cela signifie : « System clock synchronized: yes » réduit les chances que les logs mentent sur l’ordre des événements.
Décision : Si non synchronisé, corrigez NTP/chrony avant de faire confiance à des timelines multi-nœuds.
Tâche 2 : Chercher des problèmes au niveau du noyau qui peuvent imiter une corruption « aléatoire »
cr0x@server:~$ dmesg -T | tail -n 12
[Mon Jan 22 10:11:41 2026] nvme nvme0: I/O 42 QID 3 timeout, aborting
[Mon Jan 22 10:11:41 2026] nvme nvme0: Abort status: 0x371
[Mon Jan 22 10:11:43 2026] EXT4-fs (nvme0n1p2): I/O error while writing superblock
[Mon Jan 22 10:11:44 2026] blk_update_request: I/O error, dev nvme0n1, sector 91827364 op 0x1:(WRITE) flags 0x0 phys_seg 1 prio class 0
Ce que cela signifie : Les timeouts de stockage et les erreurs système de fichiers peuvent produire des corruptions silencieuses en amont.
Décision : Ne blâmez pas le code applicatif tant que la stabilité du matériel/du driver n’est pas confirmée ; drenez le nœud si les erreurs persistent.
Tâche 3 : Établir saturation CPU vs latence due à autre chose
cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.8.0 (prod-app-17) 01/22/2026 _x86_64_ (32 CPU)
10:14:22 AM CPU %usr %nice %sys %iowait %irq %soft %steal %idle
10:14:23 AM all 18.2 0.0 6.1 22.7 0.0 0.5 0.0 52.5
10:14:23 AM 7 88.9 0.0 9.1 0.0 0.0 0.0 0.0 2.0
Ce que cela signifie : Un %iowait élevé suggère que les CPU sont inactifs en attente d’I/O ; un CPU chaud pointe vers une contention de lock ou un goulot mono-thread.
Décision : Si iowait est élevé, pivotez vers les vérifications stockage/réseau ; si un CPU est saturé, suspectez des points de sérialisation et contention de mutex.
Tâche 4 : Vérifier si la machine thrash la mémoire (les bugs de timing aiment la pression)
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 1 0 31248 41216 829320 0 0 1024 2048 980 2100 17 6 55 22 0
6 3 0 28992 40128 821004 0 0 4096 8192 1200 8900 25 8 40 27 0
Ce que cela signifie : Un nombre élevé de commutations de contexte (cs) plus une augmentation des processus bloqués (b) signale souvent de la contention ou des stalls d’I/O.
Décision : Si la pression est élevée, réduisez le parallélisme et retestez ; les échecs dépendants du timing disparaissent souvent quand la charge baisse, ce qui est diagnostique.
Tâche 5 : Confirmer la santé du système de fichiers et les options de montage (les rails de sécurité comptent)
cr0x@server:~$ mount | grep " /var "
/dev/nvme0n1p2 on /var type ext4 (rw,relatime,errors=remount-ro)
Ce que cela signifie : errors=remount-ro est un fail-safe : le système de fichiers passera en lecture seule en cas d’erreurs graves au lieu de corrompre silencieusement.
Décision : Préférez des montages fail-safe pour l’état critique ; si vous voyez des options permissives, corrigez-les avant d’en avoir besoin.
Tâche 6 : Vérifier la dégradation RAID / mdadm (le piège « tout va bien »)
cr0x@server:~$ cat /proc/mdstat
Personalities : [raid1]
md0 : active raid1 sda1[0] sdb1[1]
104320 blocks super 1.2 [2/1] [U_]
[====>................] recovery = 23.4% (24448/104320) finish=2.1min speed=64000K/sec
Ce que cela signifie : [2/1] [U_] indique un miroir dégradé. Vous êtes à un disque d’une très mauvaise journée.
Décision : Traitez une redondance dégradée comme un incident ; suspendez les changements risqués, remplacez le disque défaillant, et vérifiez la fin de la reconstruction.
Tâche 7 : Valider l’état du pool ZFS (scrub, erreurs, corruption silencieuse)
cr0x@server:~$ sudo zpool status -v tank
pool: tank
state: DEGRADED
status: One or more devices has experienced an error resulting in data corruption.
action: Restore the file in question if possible.
scan: scrub repaired 0B in 00:12:44 with 1 errors on Mon Jan 22 09:58:13 2026
config:
NAME STATE READ WRITE CKSUM
tank DEGRADED 0 0 0
mirror-0 DEGRADED 0 0 0
sdc ONLINE 0 0 0
sdd ONLINE 0 0 3
errors: Permanent errors have been detected in the following files:
/tank/db/segments/00000042.log
Ce que cela signifie : ZFS vous indique qu’il a détecté des erreurs de checksum et peut pointer vers les fichiers affectés.
Décision : Restaurez les données impactées à partir de réplicas/sauvegardes ; ne « surveillez et voyez » pas. Si le stockage signale une corruption, croyez-le.
Tâche 8 : Repérer les retransmissions TCP et la perte de paquets (l’attaquant invisible du timing)
cr0x@server:~$ ss -s
Total: 1532
TCP: 812 (estab 214, closed 539, orphaned 0, timewait 411)
Transport Total IP IPv6
RAW 0 0 0
UDP 29 21 8
TCP 273 232 41
INET 302 253 49
FRAG 0 0 0
Ce que cela signifie : C’est un instantané grossier ; il ne montre pas les retransmissions directement, mais il aide à détecter le churn de connexions et les tempêtes de timewait.
Décision : Si estab est bas mais timewait énorme, suspectez des patterns de reconnexion agressifs ; passez aux stats réseau plus fines ensuite.
Tâche 9 : Vérifier les erreurs et drops d’interface
cr0x@server:~$ ip -s link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
link/ether 52:54:00:ab:cd:ef brd ff:ff:ff:ff:ff:ff
RX: bytes packets errors dropped missed mcast
9876543210 8123456 0 12456 0 12034
TX: bytes packets errors dropped carrier collsns
8765432109 7123456 0 342 0 0
Ce que cela signifie : Les drops peuvent se manifester par des retries, des timeouts et du réordonnancement — carburant parfait pour les conditions de course et les mises à jour partielles.
Décision : Si les drops montent, investiguez les queues NIC, les mismatches MTU, la congestion et les voisins bruyants ; ne touchez pas d’abord aux retries applicatifs.
Tâche 10 : Confirmer la pression au niveau des descripteurs de fichiers (peut causer des échecs bizarres)
cr0x@server:~$ cat /proc/sys/fs/file-nr
42352 0 9223372036854775807
Ce que cela signifie : Le premier nombre est le nombre de handles de fichiers alloués ; s’il croît près des limites système, vous aurez des échecs qui paraissent « aléatoires ».
Décision : Si la pression est élevée, localisez les processus qui fuient et corrigez ; définissez aussi des limites sensées et alertez sur le taux de croissance.
Tâche 11 : Voir si un service redémarre (le flapping cache les causes racines)
cr0x@server:~$ systemctl status api.service --no-pager
● api.service - Example API
Loaded: loaded (/etc/systemd/system/api.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2026-01-22 10:02:18 UTC; 11min ago
Main PID: 2187 (api)
Tasks: 34 (limit: 38241)
Memory: 512.4M
CPU: 2min 12.103s
CGroup: /system.slice/api.service
└─2187 /usr/local/bin/api --config /etc/api/config.yaml
Ce que cela signifie : Si Active continue de se réinitialiser, vous êtes en crash-loop ; si l’uptime est stable, concentrez-vous ailleurs.
Décision : En cas de flapping, stoppez l’hémorragie : figez les déploiements, réduisez le trafic, capturez les core dumps/logs avant que le redémarrage n’efface les preuves.
Tâche 12 : Interroger journald autour d’une fenêtre d’incident suspecte (obtenir la séquence)
cr0x@server:~$ journalctl -u api.service --since "2026-01-22 09:55" --until "2026-01-22 10:10" --no-pager | tail -n 8
Jan 22 10:01:02 prod-app-17 api[2187]: WARN request_id=9b3e retrying upstream due to timeout
Jan 22 10:01:02 prod-app-17 api[2187]: WARN request_id=9b3e retry attempt=2
Jan 22 10:01:03 prod-app-17 api[2187]: ERROR request_id=9b3e upstream commit status unknown
Jan 22 10:01:03 prod-app-17 api[2187]: WARN request_id=9b3e applying fallback path
Jan 22 10:01:04 prod-app-17 api[2187]: INFO request_id=9b3e response_status=200
Ce que cela signifie : « Commit status unknown » suivi de « fallback path » est un signal rouge de correction : vous pouvez avoir des double-applies ou des états partiels.
Décision : Ajoutez une vérification d’idempotence avant les retries ; si c’est critique pour la sécurité, échouez fermé au lieu de « fallback et espérer ».
Tâche 13 : Inspecter les connexions TCP ouvertes vers une dépendance (trouver les points chauds)
cr0x@server:~$ ss -antp | grep ":5432" | head
ESTAB 0 0 10.20.5.17:48422 10.20.9.10:5432 users:(("api",pid=2187,fd=41))
ESTAB 0 0 10.20.5.17:48424 10.20.9.10:5432 users:(("api",pid=2187,fd=42))
ESTAB 0 0 10.20.5.17:48426 10.20.9.10:5432 users:(("api",pid=2187,fd=43))
Ce que cela signifie : Confirme que le service parle à Postgres et n’est pas bloqué ailleurs. Indique aussi le comportement du pool de connexions.
Décision : Si les connexions explosent, capez les pools et ajoutez du backpressure ; les tempêtes de connexions sont l’équivalent logiciel de la suppression d’un interlock.
Tâche 14 : Valider les locks de base de données (les goulots de concurrence ressemblent à de la « lenteur aléatoire »)
cr0x@server:~$ psql -h 10.20.9.10 -U app -d appdb -c "select wait_event_type, wait_event, count(*) from pg_stat_activity where wait_event is not null group by 1,2 order by 3 desc;"
wait_event_type | wait_event | count
-----------------+---------------------+-------
Lock | relation | 12
IO | DataFileRead | 4
Client | ClientRead | 2
(3 rows)
Ce que cela signifie : Beaucoup de locks de relation indique de la contention ; votre équivalent d’« opérateur rapide » peut être une table chaude.
Décision : Si les waits de lock dominent, identifiez les requêtes bloquantes et corrigez le schéma/indexation ou la portée des transactions ; ne faites pas que scaler les nœuds applicatifs.
Tâche 15 : Valider les paramètres de checksum/vérification pour la réplication du stockage (vérification de la ceinture de sécurité)
cr0x@server:~$ rbd info rbd/patient-images
rbd image 'patient-images':
size 2 TiB in 524288 objects
order 22 (4 MiB objects)
snapshot_count: 2
id: 1a2b3c4d5e6f
block_name_prefix: rbd_data.1a2b3c4d5e6f
format: 2
features: layering, exclusive-lock, object-map, fast-diff, deep-flatten
op_features:
flags:
create_timestamp: Mon Jan 15 08:21:11 2026
Ce que cela signifie : Ce n’est pas une sortie de checksum en soi, mais confirme les features d’objet ; vous avez toujours besoin de vérifications d’intégrité de bout en bout au niveau applicatif.
Décision : Si la correction de votre système dépend de l’intégrité, ajoutez des checksums/hashes explicites aux frontières ; ne déléguez pas la vérité à une seule couche.
Tâche 16 : Confirmer qu’un processus est mono-thread ou bloqué sur un lock
cr0x@server:~$ top -H -p 2187 -b -n 1 | head -n 12
top - 10:14:55 up 12 days, 3:21, 1 user, load average: 6.12, 4.98, 4.77
Threads: 36 total, 1 running, 35 sleeping
%Cpu(s): 23.1 us, 7.2 sy, 0.0 ni, 47.3 id, 22.4 wa, 0.0 hi, 0.0 si, 0.0 st
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
2199 api 20 0 1452784 524200 49200 R 88.7 1.6 0:31.22 api
2201 api 20 0 1452784 524200 49200 S 2.3 1.6 0:02.10 api
Ce que cela signifie : Un thread domine le CPU ; les autres dorment. C’est souvent un lock, une boucle serrée, ou un chemin crítique unique.
Décision : Profilez ce chemin ; si cela protège un état partagé, redesign vers de l’état immuable ou des machines à états explicites avec transitions sûres.
Erreurs courantes : symptômes → cause racine → correctif
1) Symptôme : « Le système dit OK, mais les utilisateurs rapportent des résultats erronés »
Cause racine : Les health checks mesurent la vivacité (processus up) au lieu de la correction (invariants). Therac-25 passait effectivement ses propres contrôles tout en étant dangereux.
Correctif : Ajoutez des vérifications de correction : validation croisée, réconciliations et dashboards d’invariants. Gatez les rollouts sur les invariants, pas seulement sur des 200.
2) Symptôme : Échecs intermittents déclenchés par la vitesse, les retries ou une forte charge
Cause racine : Conditions de course et transitions d’état non atomiques. L’équivalent « opérateur rapide » est des requêtes parallèles ou des événements réordonnés.
Correctif : Modélisez l’état comme une machine à états finis, imposez des transitions atomiques, ajoutez des clés d’idempotence, et fuzz-gez le timing avec des tests chaos.
3) Symptôme : Les opérateurs réessaient sans cesse malgré les erreurs
Cause racine : L’UI/UX entraîne un comportement dangereux ; les alarmes sont vagues ; le système permet la continuation sans preuve de sécurité.
Correctif : Rendez la continuation dangereuse impossible. Utilisez une gravité explicite et des actions obligatoires. Si la sécurité est incertaine, échouez fermé.
4) Symptôme : L’enquête post-incident ne peut pas reconstituer ce qui s’est passé
Cause racine : Journaux insuffisants, IDs de corrélation manquants, absence d’horodatages, ou logs écrasés au redémarrage.
Correctif : Logs structurés avec IDs de corrélation, stockage immuable append-only pour les pistes d’audit, et captures d’état au moment de la faute.
5) Symptôme : Les corrections sont « re-former les utilisateurs » et « faire attention »
Cause racine : L’organisation substitue des politiques aux contrôles techniques parce que les contrôles techniques sont plus difficiles.
Correctif : Ajoutez des rails de sécurité contraignants : interlocks, transitions permissionnées, vérifications à l’exécution, et vérification indépendante. La formation est un complément, pas la solution principale.
6) Symptôme : Supprimer des validations « redondantes » rend tout plus rapide… jusqu’à ce que non
Cause racine : L’optimisation a enlevé des couches de détection/correction (checksums, validations, lectures de confirmation), transformant des fautes rares en corruption silencieuse.
Correctif : Conservez la validation bout-en-bout ; si vous devez optimiser, échantillonnez intelligemment et ajoutez une vérification canari. N’optimisez jamais la seule alarme.
7) Symptôme : Les incidents sont traités comme des anomalies isolées, pas comme des motifs
Cause racine : Gestion des incidents faible : pas de suivi central, pas d’escalade forcée, incitations à minimiser la gravité.
Correctif : Réponse aux incidents formelle avec classification de gravité, agrégation inter-sites, et autorité « stop-the-line » quand la sécurité/correctitude est en jeu.
Listes de contrôle / plan étape par étape
Étape par étape : construire des rails « à l’épreuve de Therac » dans les systèmes pilotés par logiciel
-
Écrivez la liste des dangers sérieusement.
Énumérez les états dangereux, pas seulement les pannes. Exemple : « mode haute énergie avec configuration physique erronée » correspond à « action privilégiée appliquée avec config obsolète ». -
Définissez les invariants.
Qu’est-ce qui doit toujours être vrai ? Exemple : « l’énergie du faisceau et le mode doivent correspondre à l’état mécanique vérifié » correspond à « le chemin d’écriture doit être cohérent avec une config versionnée ». -
Imposez des machines à états.
Chaque transition doit être explicite. Pas d’« édition en place » de paramètres critiques sans versioning et commit atomique. -
Échouez fermé en cas d’incertitude.
Si les capteurs ne sont pas d’accord ou si le statut de commit est inconnu, arrêtez et escaladez. Votre système doit préférer l’indisponibilité à l’erreur quand un dommage est possible. -
Conservez des interlocks en couches.
Utilisez du matériel quand c’est faisable, mais aussi des gates logicielles, du contrôle d’accès et des assertions runtime. Ne retirez rien sans remplacer par une preuve plus forte. -
Concevez l’UI pour l’escalade.
Messages clairs, niveaux de gravité, et actions sûres obligatoires. Ne laissez pas « Entrée » être un chemin vers un danger persistant. -
Testez avec un timing adversaire.
Fuzz inputs, réordonnez événements, injectez des délais, et simulez des pannes partielles. Les tests « usage normal » ne prouvent presque rien sur la sécurité. -
Vérification et validation indépendantes.
Séparez les incentives. L’équipe qui livre ne devrait pas être la seule à signer le comportement critique pour la sécurité. -
Instrumentez pour la forensique.
Capturez l’ordre des événements, les instantanés d’état, et les logs d’audit. Faites en sorte que « que s’est-il passé ? » puisse être répondu en moins d’une heure. -
Réponse aux incidents avec autorité « stop-the-line ».
Un rapport crédible d’un comportement dangereux déclenche le confinement. Pas de débat par email pendant que le système continue d’opérer. -
Pratiquez la défaillance.
Organisez des game days axés sur la correction et la sécurité, pas seulement la disponibilité. Incluez les opérateurs dans l’exercice ; ils vous apprendront où l’UI ment. -
Suivez les signaux faibles récurrents.
« Code d’erreur bizarre mais ça a marché après réessai » n’est pas un ticket clos. C’est un précurseur.
Checklist de release pour changements critiques de sécurité ou de correction
- Ce changement supprime-t-il une validation, un checksum, un verrou ou un interlock ? Si oui : qu’est-ce qui le remplace, et quelles preuves prouvent l’équivalence ?
- Les transitions d’état sont-elles versionnées et atomiques ? Sinon, vous expédiez une condition de course avec des étapes supplémentaires.
- Les invariants sont-ils mesurés et gateés en canari/staging ?
- Les messages d’erreur indiquent-ils aux opérateurs quoi faire, pas seulement ce qui s’est passé ?
- Pouvons-nous reconstituer l’ordre des événements entre composants en moins de 60 minutes via logs et horodatages ?
- L’autorité « stop the line » est-elle documentée pour l’on-call et pour les opérateurs ?
FAQ
1) Therac-25 était-il « juste un bug » ?
Non. Il y avait des bugs, y compris dépendants du timing, mais l’issue létale a nécessité une conception système qui permettait des états dangereux,
des interlocks faibles, des diagnostics médiocres, et une posture de réponse aux incidents qui ne traitait pas les signaux précoces comme des urgences.
2) Pourquoi les conditions de course reviennent-elles souvent dans les histoires de sécurité ?
Parce que les humains et les ordinateurs créent des timings imprévisibles. La concurrence produit des états que vous n’avez pas prévus, et les tests couvrent rarement
ces états à moins d’attaquer intentionnellement le timing avec du fuzzing, de l’injection de fautes et la modélisation explicite des machines à états.
3) Le matériel n’est-il pas plus sûr que le logiciel ?
Le matériel peut être plus sûr pour certaines garanties parce qu’il est plus difficile à contourner et plus simple à raisonner en cas de panne.
Mais le matériel peut aussi tomber en panne. La vraie réponse est des contrôles en couches : interlocks matériels quand c’est pratique, plus assertions logicielles,
monitoring et comportement fail-safe quand quelque chose ne colle pas.
4) Quel est l’équivalent moderne de Therac-25 hors du médical ?
Tout système où le logiciel commande une action à haute énergie ou à fort impact : automatisation industrielle, véhicules autonomes,
trading financier, identité et contrôle d’accès, et orchestration d’infrastructure. Si le logiciel peut nuire irréversiblement
aux personnes ou violer la confiance, vous êtes dans le même genre.
5) Pourquoi les « messages d’erreur cryptiques » sont-ils un si gros problème ?
Parce que les opérateurs se comportent de façon rationnelle sous pression. Si une erreur est vague et que le système permet de continuer,
les réessais deviennent la norme. Un bon message de sécurité est un contrôle : il dicte l’action sûre et empêche l’override occasionnel.
6) Comment tester les séquences d’« opérateur rapide » dans les applications modernes ?
Utilisez le fuzzing et les tests basés sur des modèles autour des transitions d’état. Simulez des éditions rapides, des retries, des délais réseau, et du réordonnancement.
Dans les systèmes distribués, injectez latence et perte de paquets ; dans les UI, scriptz des interactions à haute vitesse et vérifiez les invariants à chaque étape.
7) À quoi doit ressembler la réponse aux incidents quand la correction (et non la disponibilité) est en jeu ?
Traitez-la comme un événement de gravité. Contenez d’abord : mettez en pause les rollouts, réduisez le trafic, isolez les nœuds suspects, et préservez les preuves.
Puis vérifiez les invariants et réconciliez les données. Ce n’est qu’après avoir expliqué le mode de défaillance que vous devez reprendre l’activité normale.
8) Le « postmortem sans blâme » est-il compatible avec la responsabilité ?
Oui. Sans blâme signifie que l’on ne cherche pas de boucs émissaires pour des problèmes systémiques. La responsabilité signifie corriger le système et assurer le suivi.
Si vos postmortems concluent par « erreur humaine », vous éludez la responsabilité, vous ne l’appliquez pas.
9) Quelle est la leçon Therac-25 la plus directement actionnable pour les ingénieurs ?
N’autorisez pas d’états dangereux. Encodez les invariants et appliquez-les à l’exécution. Si le système ne peut pas prouver qu’il est sûr, il doit s’arrêter.
10) Et si la pression business exige la suppression de vérifications « lentes » ?
Alors traitez la vérification comme un contrôle de sécurité et exigez un dossier de sécurité pour la suppression : ce qui la remplace, comment elle sera surveillée,
et quels nouveaux modes de défaillance elle introduit. Si personne ne peut l’écrire, la réponse est non.
Conclusion : prochaines étapes qui réduisent réellement le risque
Therac-25 est une étude de cas sur la façon dont les systèmes tuent : pas avec une unique défaillance dramatique, mais avec une chaîne de petites décisions défendables
qui ont supprimé la friction des issues dangereuses. C’est aussi une étude de cas sur la façon dont les organisations échouent : en traitant les incidents précoces comme du bruit,
en faisant plus confiance aux indicateurs « sains » qu’aux rapports humains, et en livrant des systèmes incapables de s’expliquer quand quelque chose tourne mal.
Prochaines étapes pratiques, par ordre d’impact :
- Définir et surveiller des invariants pour la correction et la sécurité, et gatez les releases dessus.
- Modéliser explicitement les transitions d’état et éliminer les flux « édition en place » pour les paramètres critiques.
- Réintroduire ou ajouter des interlocks : matériel quand possible, assertions logicielles partout, et « échouer fermé » en cas d’incertitude.
- Améliorer l’observabilité pour la forensique : horodatages, IDs de corrélation, logs immuables, et instantanés d’état sur faute.
- Exécuter des tests adversariaux temporels : fuzzing, injection de fautes, et game days axés sur la correction, pas seulement la disponibilité.
- Corriger l’interface opérateur pour qu’elle enseigne le comportement sûr : gravité claire, actions explicites, pas de réessais banals à travers le danger.
Si vous construisez des systèmes pouvant blesser des personnes — physiquement, financièrement ou socialement — traitez la sécurité comme une propriété que vous devez prouver en continu.
Le jour où vous commencez à vous fier à « ça devrait aller » est le jour où vous commencez à écrire votre propre chapitre Therac-25.