Un défaut de segmentation semble anodin. Un processus meurt. Le superviseur le redémarre. Quelqu’un hausse les épaules en évoquant un « rayon cosmique »,
puis retourne à l’ajout de fonctionnalités. Et c’est ainsi qu’un trimestre peut être compromis par un seul crash : pas à cause du segfault lui‑même,
mais à cause de la réaction en chaîne pour laquelle vous n’aviez pas conçu.
En production, un segfault est rarement « un bug dans un binaire ». C’est un événement système. Il met à l’épreuve votre hygiène de déploiement,
votre modèle de durabilité des données, votre stratégie d’allégement de charge, votre observabilité et votre honnêteté organisationnelle. Si vous le traitez comme
une simple gêne pour le développeur local, vous en entendrez reparler — en réunion du conseil d’administration.
Ce que signifie vraiment un segfault (et ce que cela n’est pas)
Un segfault est le système d’exploitation qui applique la protection mémoire. Votre processus a tenté d’accéder à une mémoire qu’il ne devait pas :
une adresse invalide, une page sans permissions, ou une adresse qui était valide jusqu’à ce que vous la libériez et continuiez à l’utiliser.
Le noyau envoie un signal (généralement SIGSEGV), et à moins que vous le gériez (ce que vous ne devriez presque jamais faire), le processus meurt.
« Segfault » est une étiquette symptôme. La cause sous‑jacente peut être :
- Use‑after‑free et autres bugs de durée de vie (les classiques).
- Dépassement de tampon (écriture hors bornes, corruption de métadonnées, crash ultérieur ailleurs).
- Déréférencement de pointeur nul (simple, embarrassant, ça arrive encore en 2026).
- Dépassement de pile (récursion ou grande frame stack, souvent déclenché par une entrée inattendue).
- Incompatibilité ABI (plugin lié à une mauvaise version de librairie ; le code « tourne » jusqu’à ce qu’il ne tourne plus).
- Problèmes matériels (rare, mais pas mythique : RAM défaillante et CPU instables existent).
- Violation du contrat noyau/espace utilisateur (par ex. filtres seccomp, mappings mémoire atypiques).
Voici ce que cela signifie généralement pas : « l’OS nous a tués au hasard. » Linux n’est pas votre ennemi.
Si un processus segfault, quelque chose a écrit ou exécuté ou retourné une adresse qu’il n’aurait pas dû. Le noyau n’est que le videur.
Une vérité sèche pour l’exploitation : si votre réponse à incident commence par « c’est probablement juste un crash », vous êtes déjà en retard.
Les crashes sont la manière dont le comportement indéfini se manifeste.
Blague #1 : Un segfault, c’est la façon dont votre programme dit « j’aimerais parler au responsable », sauf que le responsable, c’est le noyau et il ne négocie pas.
Crash ≠ panne, sauf si vous l’avez conçu ainsi
Un crash unique peut être sans conséquence si votre système est conçu pour l’accepter :
dégradation gracieuse, retries avec jitter, opérations idempotentes, files bornées et état durable.
Mais si votre crash fait tomber une shard, corrompt un cache en mémoire dont dépend soudainement votre base de données,
bloque une élection de leader, ou déclenche une tempête de redémarrages, vous avez transformé « un mauvais pointeur » en événement financier.
Concepts clés à garder en tête pendant le triage
- Site du crash vs. site du bug : l’instruction qui a planté est souvent loin de l’écriture qui a corrompu la mémoire.
- Déterminisme : si ça plante à la même instruction pour la même requête, c’est probablement un bug simple ; sinon, suspectez corruption mémoire, races ou matériel.
- Rayon d’impact : le bug technique peut être minuscule ; le bug opérationnel (couplage) est celui qui fait mal.
- Temps jusqu’au premier signal : votre première tâche n’est pas « trouver la cause racine ». C’est « arrêter l’hémorragie en préservant les preuves ».
Idée paraphrasée de John Gall (penseur des systèmes) : les systèmes complexes qui fonctionnent évoluent souvent à partir de systèmes plus simples qui fonctionnaient. Si vous ne survivez pas à un crash, votre système n’a pas fini d’évoluer.
Pourquoi un crash peut ruiner un trimestre
Le côté « fin de trimestre » n’est pas du mélodrame. C’est la façon dont les systèmes modernes et les entreprises modernes amplifient de petits événements techniques :
couplage fort, dépendances partagées, SLOs agressifs, et l’habitude business d’enchaîner les lancements.
La cascade standard
Une séquence typique ressemble à ceci :
- Un processus segfault sous une forme de requête ou un pattern de charge particulier.
- Le superviseur le redémarre rapidement (systemd, Kubernetes, votre watchdog maison).
- État perdu (requêtes en vol, magasin de session en mémoire, tokens d’auth mis en cache, offsets de queue, leases de leader).
- Les clients retent (parfois correctement, souvent en stampede synchronisé).
- La latence explose car le processus redémarré réchauffe les caches, reconstruit les pools de connexion, rejoue les logs.
- Les systèmes en aval se font submerger (bases de données, stockage d’objets, API dépendantes).
- L’autoscaling répond en retard (ou pas) parce que les métriques traînent et les politiques de scaling sont prudentes.
- Plus de crashes surviennent à cause de la pression mémoire, des timeouts et de l’accumulation dans les files.
- Les opérateurs redémarrent frénétiquement et suppriment les preuves (les core dumps et logs disparaissent).
- La finance s’en aperçoit parce que le comportement visible client change : paniers abandonnés, enchères publicitaires manquées, règlements retardés.
Comment le stockage aggrave la situation (oui, même si « c’est un crash »)
En tant qu’ingénieur stockage, je dirai la partie cachée : le stockage transforme les crashes en problèmes monétaires parce que c’est là que l’état « temporaire »
devient état durable. Un segfault peut :
- Corrompre l’état local si votre processus écrit de manière non atomique et n’appelle pas fsync de façon appropriée.
- Corrompre l’état distant via écritures partielles, mauvaise utilisation du protocole, ou bugs de replay.
- Déclencher des replays (consommateurs Kafka, replay WAL, retries de listing d’objets) qui multiplient la charge et le coût.
- Causer une perte de données silencieuse quand le processus meurt entre l’accusé de réception et le commit.
Beaucoup d’équipes « traitent » les crashes en ajoutant des retries. Les retries ne sont pas de la fiabilité ; ce sont des multiplicateurs de charge.
Si votre politique de retry n’est pas bornée, avec jitter et informée par l’idempotence, vous créez un générateur de panne.
La couche business : pourquoi ça finit sur les appels de résultats
L’entreprise ne se soucie pas de votre trace de pile. Elle se soucie de :
- Taux de conversion en baisse quand la latence augmente et les timeouts montent.
- Pénalités SLA/SLO (pénalités explicites ou churn).
- Coûts support et dommages à la marque quand les clients observent un comportement incohérent.
- Coût d’opportunité : vous gèlezn les déploiements et retardez les lancements pour stabiliser.
- Distraction des ingénieurs : les meilleures ressources sont plongées dans la réponse à incident pendant des jours.
Un segfault peut être « une ligne de code ». Mais en production, c’est un test des amortisseurs de votre système.
La plupart des systèmes échouent à ce test parce que les amortisseurs n’ont jamais été installés — seulement promis.
Faits et histoire : pourquoi on se complique la vie
- La protection mémoire est plus vieille que votre entreprise. La protection de page matérielle et les fautes étaient courantes bien avant Linux ; les segfaults sont le « mécanisme qui fonctionne ».
- Unix ancien a popularisé le fichier « core ». Dumper l’image d’un processus lors d’un crash était un outil pragmatique quand le débogage interactif était plus difficile.
SIGSEGVn’est pas identique àSIGBUS. Segfault = accès invalide ; bus error indique souvent des problèmes d’alignement ou des erreurs avec des fichiers/devices mappés.- L’ASLR a changé la donne. L’Address Space Layout Randomization a rendu l’exploitabilité plus difficile et le débogage un peu plus pénible ; les backtraces symbolisées comptent davantage.
- Les allocateurs de heap ont évolué parce que les crashes coûtaient cher. Les allocateurs modernes (ptmalloc, jemalloc, tcmalloc) arbitrent vitesse, fragmentation et fonctionnalités de débogage différemment.
- Les symboles de débogage sont devenus une préoccupation de production. L’ère du continuous deployment a rendu « on le reproduira en local » illusoire ; vous avez besoin des symboles et des build IDs pour déboguer ce qui a réellement tourné.
- La containerisation a compliqué les core dumps. Les namespaces et l’isolation du système de fichiers font que les cores peuvent disparaître à moins de les rediriger délibérément.
- « Fail fast » a été mal lu. Échouer rapidement est bon quand ça évite la corruption ; c’est mauvais quand cela déclenche des retries coordonnés et une perte d’état sans garde‑fous.
- Les noyaux modernes sont bavards de manière utile.
dmesgpeut inclure l’adresse fautive, le pointeur d’instruction et même des offsets de librairie — si vous avez préservé les logs.
Les segfaults ne sont pas devenus plus fréquents. Nous avons simplement construit des tours de dépendances plus hautes autour d’eux.
Playbook de diagnostic rapide (premier/deuxième/troisième)
Voici le playbook de 15–30 minutes pour trouver le goulot d’étranglement et choisir la bonne stratégie de confinement.
Pas pour résoudre le bug définitivement — encore.
Premier : arrêter la tempête de redémarrages et préserver les preuves
- Stabiliser : scaler horizontalement, désactiver temporairement les retries agressifs et ajouter des limites de débit.
- Préserver : s’assurer que les core dumps et logs survivent aux redémarrages (ou au moins préserver le dernier crash).
- Vérifier le rayon d’impact : est‑ce un hôte, une zone de disponibilité, une version, une charge particulière ?
Deuxième : identifier la signature du crash
- Où a‑t‑il planté ? nom de fonction, module, offset, adresse fautive.
- Quand cela a‑t‑il commencé ? corréler avec un déploiement, un changement de config, un pattern de trafic.
- Est‑ce déterministe ? mêmes requêtes déclenchantes, même backtrace, même hôte ?
Troisième : choisir la branche basée sur le signal le plus fort
- Même version binaire uniquement : rollback ou désactiver le drapeau de fonctionnalité ; poursuivre avec l’analyse du core.
- Uniquement certains hôtes : suspecter le matériel, le noyau, libc ou la dérive de configuration ; drainer et comparer.
- Seulement sous forte charge : suspecter une race, une pression mémoire, des timeouts menant à des chemins de nettoyage dangereux.
- Après des problèmes de dépendance : suspecter un bug de gestion d’erreur (déréf nil après une réponse échouée, etc.).
Le piège : plonger dans GDB immédiatement alors que le système flanche encore. Réparez d’abord le mode défaillant opérationnel.
Vous avez besoin d’un patient stable avant d’opérer.
Tâches pratiques : commandes, sorties, décisions (12+)
Ce sont de véritables tâches que j’attends des ingénieurs on‑call pendant une investigation de crash. Chacune contient : une commande, une sortie réaliste,
ce que cela signifie, et la décision suivante à prendre.
Task 1: Confirm the crash in the journal and get the signal
cr0x@server:~$ sudo journalctl -u checkout-api.service -S "30 min ago" | tail -n 20
Jan 22 03:11:07 node-17 checkout-api[24891]: FATAL: worker 3 crashed
Jan 22 03:11:07 node-17 systemd[1]: checkout-api.service: Main process exited, code=dumped, status=11/SEGV
Jan 22 03:11:07 node-17 systemd[1]: checkout-api.service: Failed with result 'core-dump'.
Jan 22 03:11:08 node-17 systemd[1]: checkout-api.service: Scheduled restart job, restart counter is at 6.
Jan 22 03:11:08 node-17 systemd[1]: Stopped Checkout API.
Jan 22 03:11:08 node-17 systemd[1]: Started Checkout API.
Signification de la sortie : le statut 11/SEGV confirme SIGSEGV. code=dumped suggère qu’un core dump existe (ou que systemd le pense).
Décision : ralentir la boucle de redémarrage (RestartSec) et aller trouver le core dump avant qu’il ne soit tourné/ébriqué.
Task 2: Check kernel messages for the faulting address and instruction pointer
cr0x@server:~$ sudo dmesg -T | tail -n 8
[Wed Jan 22 03:11:07 2026] checkout-api[24891]: segfault at 0000000000000038 ip 000055c1b2f9a4d2 sp 00007ffd7b5d1a10 error 4 in checkout-api[55c1b2f82000+3e0000]
[Wed Jan 22 03:11:07 2026] Code: 48 8b 47 38 48 85 c0 74 0a 48 8b 00 48 8b 50 10 ff 52 08 48 8b 47 10 <48> 8b 40 08
Signification de la sortie : faute à 0x38 est un motif classique pointeur nul+offset (déréférencement d’un champ struct sur base nulle). L’ip pointe dans le segment texte de votre binaire.
Décision : prioriser les hypothèses « mauvais pointeur dans un chemin d’erreur » ; extraire le core et les symboles pour confirmer quel champ.
Task 3: Locate the core via systemd-coredump
cr0x@server:~$ coredumpctl list checkout-api | tail -n 3
TIME PID UID GID SIG COREFILE EXE
Wed 2026-01-22 03:09:55 UTC 24602 1001 1001 11 present /opt/checkout/bin/checkout-api
Wed 2026-01-22 03:11:07 UTC 24891 1001 1001 11 present /opt/checkout/bin/checkout-api
Signification de la sortie : des fichiers core sont présents pour ces PID. Bien — des preuves existent.
Décision : extraire immédiatement le core le plus récent vers un emplacement sûr (les cores se font vacuumer).
Task 4: Extract the core to disk so it survives rotation
cr0x@server:~$ sudo coredumpctl dump 24891 --output=/var/tmp/checkout-api.core
/var/tmp/checkout-api.core
Signification de la sortie : core écrit sur un chemin stable.
Décision : snapshotter l’exécutable exact et ses librairies ensuite, en faisant correspondre les build IDs.
Task 5: Verify the binary build ID (so symbols match)
cr0x@server:~$ readelf -n /opt/checkout/bin/checkout-api | grep -A2 "Build ID"
Build ID: 9b7c6fd3b2a9cdb5d3d1c9e0a4f2f7aa12c0f2ab
Signification de la sortie : cet ID de build doit correspondre à votre package/artéfact de symboles de débogage.
Décision : récupérer les symboles pour cet ID de build ; si vous ne pouvez pas, vous ferez du debugging par offsets mais plus lent et risqué.
Task 6: Quick backtrace from the core (best-effort)
cr0x@server:~$ gdb -q /opt/checkout/bin/checkout-api /var/tmp/checkout-api.core -ex "set pagination off" -ex "thread apply all bt" -ex "quit"
Reading symbols from /opt/checkout/bin/checkout-api...
(No debugging symbols found in /opt/checkout/bin/checkout-api)
[New LWP 24891]
Core was generated by `/opt/checkout/bin/checkout-api --config /etc/checkout/config.yaml'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x000055c1b2f9a4d2 in ?? ()
#1 0x000055c1b2f63c10 in ?? ()
#2 0x00007f2b8e31c1f5 in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6
#3 0x000055c1b2f6456a in ?? ()
Signification de la sortie : pas de symboles, donc les frames sont inconnues. Vous avez quand même l’adresse du crash 0x55c1b2f9a4d2.
Décision : traduire l’adresse de crash en fonction via addr2line une fois les symboles disponibles ; entre‑temps, utilisez la carte mémoire du binaire pour calculer les offsets.
Task 7: Confirm memory mappings to compute the offset
cr0x@server:~$ gdb -q /opt/checkout/bin/checkout-api /var/tmp/checkout-api.core -ex "info proc mappings" -ex "quit"
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x000055c1b2f82000 0x000055c1b3362000 0x003e0000 0x0000000000000000 /opt/checkout/bin/checkout-api
0x00007f2b8e2f0000 0x00007f2b8e4d0000 0x001e0000 0x0000000000000000 /lib/x86_64-linux-gnu/libc.so.6
Signification de la sortie : le binaire est mappé à partir de 0x55c1b2f82000. Votre IP de crash se situe dans cette plage.
Décision : calculer l’offset : 0x55c1b2f9a4d2 - 0x55c1b2f82000. Utiliser cet offset avec les outils de symboles.
Task 8: Check whether core dumps are being truncated by limits
cr0x@server:~$ ulimit -c
0
Signification de la sortie : la limite de taille de core est zéro pour le shell courant ; selon le gestionnaire de service, l’unité peut la surcharger, mais souvent cela signifie « pas de cores ».
Décision : s’assurer que l’unité systemd a LimitCORE=infinity ou configurer coredump.conf correctement ; sinon vous déboguerez à l’aveugle.
Task 9: Verify systemd core dump storage policy
cr0x@server:~$ sudo grep -E '^(Storage|ProcessSizeMax|ExternalSizeMax|MaxUse|KeepFree)=' /etc/systemd/coredump.conf
Storage=external
ProcessSizeMax=2G
ExternalSizeMax=2G
MaxUse=8G
KeepFree=2G
Signification de la sortie : les cores sont stockés à l’extérieur avec des plafonds. Si votre processus a >2G RSS au crash, le core peut être tronqué ou manquant.
Décision : si les cores manquent ou sont tronqués, augmenter temporairement le plafond sur les nœuds impactés, ou reproduire en conditions contrôlées avec une empreinte mémoire réduite.
Task 10: Rule out OOM-kill (different failure, same “it died” symptom)
cr0x@server:~$ sudo journalctl -k -S "1 hour ago" | grep -i -E "oom|killed process" | tail -n 5
Signification de la sortie : sortie vide suggère pas d’OOM kill la dernière heure.
Décision : continuer à se concentrer sur le segfault ; si vous voyez des OOM kills, traiter d’abord la pression mémoire (et le segfault peut être une corruption secondaire sous stress).
Task 11: Check for restart storms and rate-limit them
cr0x@server:~$ systemctl show checkout-api.service -p Restart -p RestartUSec -p NRestarts
Restart=always
RestartUSec=200ms
NRestarts=37
Signification de la sortie : délai de redémarrage 200ms et 37 redémarrages est un test de charge auto‑infligé. Cela amplifie les retries en aval et peut asphyxier l’hôte.
Décision : augmenter le délai de redémarrage (secondes à minutes) et envisager StartLimitIntervalSec/StartLimitBurst pour éviter que le flapping n’affecte le nœud.
Task 12: See if the crash correlates with a deploy (don’t guess)
cr0x@server:~$ sudo journalctl -u checkout-api.service -S "6 hours ago" | grep -E "Started|Stopping|version" | tail -n 15
Jan 21 22:10:02 node-17 systemd[1]: Started Checkout API.
Jan 22 02:58:44 node-17 checkout-api[19822]: version=2.18.7 git=9b7c6fd3 feature_flags=pricing_v4:on
Jan 22 03:11:08 node-17 systemd[1]: Started Checkout API.
Signification de la sortie : vous avez une version et l’état des feature flags capturés dans les logs. C’est de l’or.
Décision : si le crash a commencé après activation d’un flag, désactivez‑le d’abord. S’il a commencé après un bump de version, rollbackez pendant l’analyse.
Task 13: Validate shared library resolution (ABI mismatch catches)
cr0x@server:~$ ldd /opt/checkout/bin/checkout-api | head -n 12
linux-vdso.so.1 (0x00007ffd7b7d2000)
libssl.so.3 => /lib/x86_64-linux-gnu/libssl.so.3 (0x00007f2b8e600000)
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f2b8e180000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f2b8df00000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2b8e2f0000)
Signification de la sortie : confirme quelles libs sont chargées. Si vous attendiez d’autres versions (par ex. dans /opt), vous avez un problème de packaging ou d’environnement d’exécution.
Décision : si dérive de dépendances existe, pinner et redeployer ; déboguer un crash sur un ABI inattendu est une perte de temps.
Task 14: Check for host-level anomalies (disk full can ruin coredumps and logs)
cr0x@server:~$ df -h /var /var/tmp
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/vg0-var 60G 58G 1.6G 98% /var
/dev/mapper/vg0-var 60G 58G 1.6G 98% /var/tmp
Signification de la sortie : vous êtes presque à court d’espace là où les cores sont écrits. Cela signifie bientôt « pas de preuves », et possiblement des problèmes si les logs ne peuvent plus s’écrire.
Décision : libérer de l’espace immédiatement (vacuum journal, rotation des cores), ou rediriger le stockage des cores vers un FS plus grand pendant l’incident.
Task 15: Confirm whether the crash is tied to a specific request (application logs + sampling)
cr0x@server:~$ sudo journalctl -u checkout-api.service -S "10 min ago" | grep -E "request_id|panic|FATAL" | tail -n 10
Jan 22 03:11:07 node-17 checkout-api[24891]: request_id=9f2b4d1f path=/checkout/confirm user_agent=MobileApp/412.3
Jan 22 03:11:07 node-17 checkout-api[24891]: FATAL: worker 3 crashed
Signification de la sortie : un request ID et un chemin juste avant le crash est un indice fort. Pas une preuve, mais une direction.
Décision : extraire un échantillon des requêtes récentes pour cet endpoint, inspecter les formes de payload, et envisager de limiter temporairement le débit ou de gatekeeper ce chemin de code.
Trois mini‑histoires d’entreprise depuis les tranchées des crashes
Mini‑histoire 1 : La mauvaise hypothèse (le pointeur nul qui « ne pouvait pas arriver »)
Un service adjacent aux paiements a commencé à segfaulter une fois par jour. Puis une fois par heure. L’équipe on‑call traitait ça comme un bug bruyant mais gérable :
le processus redémarrait vite, et la plupart des clients ne remarquaient pas — jusqu’au jour où ils ont remarqué. La latence a grimpé, les taux d’erreur ont explosé aux heures de pointe,
et le canal d’incident est devenu un mode de vie.
La première investigation a visé l’infrastructure : mises à jour du noyau, firmware des hôtes, correctif de librairie récent. Des suppositions raisonnables, mais erronées.
Le seul indice constant était l’adresse fautive : toujours un petit offset depuis zéro. Déréférencement nul classique.
Pourtant la base de code « vérifiait évidemment » le nul. « Évidemment » est un mot coûteux.
Le bug provenait d’une mauvaise hypothèse sur une dépendance en amont : « ce champ est toujours présent ». Il était présent 99,98 % du temps.
Puis un partenaire a déployé un changement qui l’a omis pour un palier de produit de niche. Le code analysait du JSON dans une struct, laissait un pointeur non initialisé,
et l’utilisait ensuite dans un chemin de nettoyage qui n’avait jamais été testé en charge parce que, encore une fois, « ça ne pouvait pas arriver ».
Le correctif fut une garde d’un ligne et un meilleur modèle d’erreur. La réparation opérationnelle fut plus intéressante :
ils ont ajouté un disjoncteur sur l’intégration partenaire, arrêté de retryer sur des entrées malformées, et introduit un canary qui simulait le cas « champ manquant ».
Le segfault a cessé d’être un risque de trimestre parce que le système a arrêté de supposer que le monde était poli.
Mini‑histoire 2 : L’optimisation qui s’est retournée contre eux (un allocateur plus rapide, une entreprise plus lente)
Un pipeline d’ingestion à haut débit a changé d’allocateur pour réduire la CPU et améliorer la latence tail. Ça fonctionnait en benchmark.
En production, le taux de crash a augmenté lentement, puis soudainement. Segfaults. Parfois double free. Parfois lecture invalide.
L’équipe soupçonnait son propre code, et à juste titre, elle le méritait peut‑être.
Le retour de bâton n’était pas « l’allocateur est mauvais ». C’était que le changement a modifié la disposition mémoire et le timing suffisamment pour exposer une race existante.
Avant, la race gribouillait sur une zone mémoire que personne n’utilisait pendant un moment. Avec le nouvel allocateur, la même zone était réutilisée plus tôt.
Le comportement indéfini est passé de « latent » à « bruyant ».
Puis le multiplicateur opérationnel est arrivé : leur orchestrateur avait des probes de liveness agressives et des redémarrages immédiats.
Sous des crash loops, le retard d’ingestion a augmenté. Les consommateurs ont pris du retard. La rétro‑pression ne s’est pas propagée correctement.
Le pipeline a essayé de rattraper en tirant plus de travail. C’est devenu une machine qui se fait du mal.
Le correctif long terme fut de corriger la race et d’ajouter TSAN/ASAN dans le CI pour les composants clés.
Le correctif court terme qui a sauvé la semaine business fut de rollbacker l’allocateur et de mettre un plafond strict sur la concurrence tant que le lag dépassait un seuil.
Ils ont appris la vérité ennuyeuse : les changements de perf sont des changements de correction déguisés.
Mini‑histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la situation (symboles, cores et rollback calme)
Un service RPC interne a segfaulter après une mise à jour banale d’une dépendance. L’ingénieur on‑call n’a pas commencé par explorer le code.
Il a commencé par s’assurer que le système cesserait de flancher et que le crash serait déboguable.
Le délai de redémarrage a été augmenté. Les nœuds impactés ont été drainés. Les cores ont été préservés.
L’équipe avait fait une chose terne et inglorieuse des mois plus tôt : chaque build publiait des symboles de débogage indexés par build ID, et le runtime loggait le build ID au démarrage.
Il y avait aussi un runbook qui disait « si un processus dump core, copiez‑le hors du nœud avant de faire quelque chose d’astucieux ».
Personne n’a célébré cette pratique lors de son introduction. Ils auraient dû.
En moins d’une heure, ils avaient une backtrace symbolisée pointant sur une fonction spécifique gérant une réponse d’erreur.
Une dépendance retournait maintenant une liste vide là où elle renvoyait null ; le code traitait « vide » comme « contient au moins un élément » et indexait.
Un correctif d’une ligne, un test ciblé, et un déploiement canari.
Le service est resté stable tout le temps parce que la voie de rollback était propre et répétée.
Le crash n’est pas devenu une histoire de trimestre parce que l’entreprise avait investi dans les preuves, pas dans les exploits héroïques.
Blague #2 : Rien n’accélère « l’alignement d’équipe » comme une boucle de crash à 3h du matin ; soudain tout le monde est d’accord sur les priorités.
Erreurs courantes : symptôme → cause racine → correctif
Ce sont des schémas qui reviennent sans cesse. Apprenez‑les une fois. Évitez de les répéter.
1) Symbole : segfault à l’adresse 0x0 ou petit offset (0x10, 0x38, 0x40)
Cause racine : déréférencement de pointeur nul, souvent dans un chemin de gestion d’erreur ou de nettoyage.
Correctif : ajouter des checks explicites sur le nul ; mais aussi corriger les invariants — pourquoi ce pointeur peut‑il être nul ? Ajouter des tests pour entrées manquantes/invalides.
2) Symbole : le site du crash change à chaque fois ; les backtraces semblent aléatoires
Cause racine : corruption mémoire (overflow, use‑after‑free), souvent antérieure au crash.
Correctif : reproduire avec ASAN/UBSAN ; activer le durcissement de l’allocateur en staging ; réduire la concurrence pour resserrer les fenêtres temporelles ; auditer le code non sécurisé et les frontières FFI.
3) Symbole : le segfault n’apparaît qu’en charge, disparaît quand vous ajoutez des logs
Cause racine : condition de concurrence ; le changement de timing masque le bug. Le fait que le logging « corrige » le crash est un signe classique d’un bug de concurrence.
Correctif : exécuter TSAN dans le CI ; ajouter de la discipline de verrouillage ; utiliser des structures thread‑safe ; appliquer des règles de propriété (surtout pour les callbacks).
4) Symbole : seul un AZ/pool d’hôtes voit le crash
Cause racine : dérive de configuration, versions de librairies différentes, caractéristiques CPU, différences de noyau, ou matériel défectueux.
Correctif : comparer paquets et versions du noyau ; déplacer la charge ; exécuter des tests mémoire si le doute persiste ; reconstruire des images dorées et éliminer la dérive.
5) Symbole : le crash coïncide avec une optimisation « réussie »
Cause racine : l’optimisation a changé la disposition mémoire ou le timing, exposant un UB ; ou a supprimé des checks de bornes.
Correctif : rollbacker d’abord ; puis réintroduire avec garde‑fous et sanitizers ; traiter le travail de perf comme risqué comme une migration de schéma.
6) Symbole : aucun core dump nulle part, malgré des messages « core-dump »
Cause racine : limites de core à zéro, cores trop volumineux et plafonnés, disque plein, runtime container n’autorisant pas l’écriture de cores, ou systemd‑coredump configuré pour les jeter.
Correctif : définir LimitCORE=infinity ; augmenter les plafonds de taille de coredump ; assurer l’espace de stockage ; tester le dumping de core volontairement en staging.
7) Symbole : « segfault » mais le log noyau montre general protection fault ou illegal instruction
Cause racine : exécution de code corrompu, saut via un pointeur de fonction invalide, ou exécution sur des fonctionnalités CPU incompatibles (rare, mais réel avec des flags de build agressifs).
Correctif : vérifier les cibles de build, les flags CPU et l’ABI des librairies ; chercher la corruption mémoire ; confirmer que vous ne déployez pas des binaires construits pour une microarchitecture différente.
8) Symbole : les crashes cessent quand vous désactivez un endpoint ou un feature flag
Cause racine : une forme d’entrée spécifique déclenche un comportement indéfini ; souvent parsing, bornes ou gestion de champs optionnels.
Correctif : garder le flag désactivé jusqu’à correction ; ajouter de la validation d’entrée ; ajouter du fuzzing pour ce parseur et des tests de contrat pour les dépendances en amont.
Listes de contrôle / plan étape par étape
Checklist de confinement (première heure)
- Réduire le rayon d’impact : drainer les nœuds affectés, réduire le trafic, ou router loin de la version.
- Arrêter les tempêtes de redémarrages : augmenter les délais de redémarrage ; limiter les redémarrages ; éviter le thrashing des dépendances.
- Préserver les preuves : copier les cores hors du nœud ; snapshotter les logs ; enregistrer les build IDs et la config/flags.
- Obtenir une signature de crash : ligne noyau (adresse fautive, IP), une backtrace (même non symbolisée), fréquence et déclencheurs.
- Choisir la voie la plus sûre : rollback bat « débogage en direct » quand les clients brûlent.
Checklist de diagnostic (même jour)
- Symboliser le crash : faire correspondre les build IDs, récupérer les symboles, produire une backtrace lisible.
- Classer : déréf nil vs. corruption mémoire vs. race vs. environnement/matériel.
- Reproduire : capturer la forme de requête déclenchante ; construire un repro minimal ; exécuter sous sanitizers.
- Confirmer la portée : versions impactées, pools d’hôtes impactés, entrées impactées.
- Patch sûr : ajouter des tests ; canary ; déploiement progressif ; valider une période sans crash avant le déploiement complet.
Checklist de prévention (hygiène trimestrielle qui paie)
- Publier systématiquement build IDs et symboles. Rendre la récupération de symboles banale et automatisée.
- Conserver des feature flags pour les chemins risqués. Tout n’a pas besoin d’un flag ; le code sujet aux crashes oui.
- Définir idempotence et retries. « Retry everything » est la façon dont vous vous auto‑DDoS.
- Fuzzer les parseurs et le code limite. Ce sont souvent les bords qui engendrent des segfaults.
- Utiliser des sanitizers en CI pour les composants clés. Surtout pour C/C++ et les frontières FFI.
- Prévoir la mort du processus. Sans état quand possible ; état durable quand nécessaire ; comportement gracieux partout.
FAQ
1) Un segfault est‑il toujours un bug logiciel ?
Presque toujours, oui. Le matériel peut en être la cause (RAM défectueuse), mais considérez le matériel coupable seulement après preuve :
crashes spécifiques à un hôte, erreurs mémoire corrigées, ou pannes reproductibles sur une machine.
2) Quelle est la différence entre SIGSEGV et SIGBUS ?
SIGSEGV est un accès mémoire invalide (permissions ou mémoire non mappée). SIGBUS implique souvent des problèmes d’alignement ou des erreurs sur des fichiers/devices mappés.
Les deux signifient « votre processus a fait quelque chose qu’il ne devait pas faire », mais le chemin de débogage diffère.
3) Pourquoi le crash arrive‑t‑il « ailleurs » que le bug ?
La corruption mémoire est du chaos à action différée. Vous écrasez des métadonnées ou un objet voisin, et le programme continue jusqu’à toucher la zone empoisonnée.
Le site du crash est là où le système l’a remarqué, pas où vous avez commis le crime.
4) Dois‑je attraper SIGSEGV et continuer à tourner ?
Non, pas pour du code applicatif général. Se remettre de façon sûre est presque impossible car l’état du processus n’est plus digne de confiance.
Utilisez un handler SIGSEGV uniquement pour logger un contexte diagnostic minimal puis sortir.
5) Pourquoi les core dumps manquent‑ils dans les conteneurs ?
Souvent parce que les limites de core sont à zéro, le système de fichiers est en lecture seule ou limité, ou le runtime container bloque l’écriture des cores.
Il faut configurer intentionnellement le comportement de core dump par runtime et s’assurer que le stockage existe.
6) Si je n’ai pas de symboles de débogage, le core est‑il inutile ?
Pas inutile, juste plus lent. Vous pouvez toujours utiliser les pointeurs d’instruction, les offsets de mapping et les build IDs pour localiser du code.
Mais vous perdrez du temps et risquez de mal attribuer. Les symboles coûtent peu comparés au downtime.
7) Un segfault peut‑il corrompre des données ?
Oui. Si vous plantez en plein écrit sans garanties d’atomicité et de durabilité, vous pouvez laisser un état partiel.
Si vous accusez réception avant le commit, vous pouvez perdre des données. Si vous retryez sans idempotence, vous pouvez dupliquer des données.
8) Quelle est la mitigation la plus rapide et sûre quand les crashes augmentent après un déploiement ?
Rollbacker ou désactiver le feature flag. Faites‑le tôt. Ensuite analysez avec les preuves préservées.
Le débogage héroïque pendant que les clients souffrent prolonge l’incident et crée des légendes plutôt que des correctifs.
9) Comment savoir si c’est une condition de course ?
Les crashes qui disparaissent avec du logging supplémentaire, changent avec le nombre de CPU, ou montrent des backtraces différentes sous charge sont des indicateurs forts.
TSAN et des tests de stress contrôlés sont vos amis.
Conclusion : prochaines étapes qui réduisent réellement les crashes
Un segfault n’est pas un « bug ». C’est une défaillance système qui révèle ce que vous supposiez à propos de la mémoire, des entrées, des dépendances et de la récupération.
Le crash est la plus petite partie de l’histoire. Le reste, c’est le couplage, les retries, la gestion d’état et la préservation des preuves.
Faites ceci ensuite, dans cet ordre :
- Rendez cores et symboles non négociables. Si vous ne pouvez pas déboguer ce qui a tourné, vous fonctionnez à l’espoir.
- Durcissez le comportement de redémarrage. Ralentissez les redémarrages, limitez le flapping, et empêchez les stampedes de retries.
- Concevez pour la mort de processus. Idempotence, frontières de durabilité et dégradation gracieuse permettent qu’un crash reste un crash.
- Investissez dans les sanitizers et le fuzzing là où ça compte. Surtout aux parseurs, aux frontières FFI et aux points chauds de concurrence.
- Rédigez le postmortem comme si vous alliez le relire. Parce que vous le ferez — à moins que vous ne corrigiez l’amplificateur opérationnel, pas seulement la ligne de code.
L’objectif n’est pas d’éliminer chaque segfault pour toujours. L’objectif est de rendre un segfault ennuyeux : contenu, déboguable et incapable de hijacker un trimestre.