Vous passez à Ubuntu 24.04, votre pipeline de déploiement reste vert, puis un service s’effondre avec un message sec :
Illegal instruction. Pas de trace de pile fiable. Pas de journaux utiles. Juste un core dump et une alarme qui vous occupe la soirée.
Cette panne n’est généralement pas « Ubuntu qui fait des siennes ». C’est de la physique : le CPU a tenté d’exécuter un opcode qu’il ne supporte pas parce que votre binaire (ou l’une de ses bibliothèques) supposait un jeu de drapeaux CPU différent de celui effectivement présent sur la machine. La solution n’est pas de « réinstaller le paquet » ni de « tester un noyau différent ». La solution consiste à faire converger votre build et votre déploiement sur ce qui est réellement autorisé comme instructions.
Playbook de diagnostic rapide
Lorsqu’un processus meurt avec Illegal instruction, vous déboguez une incompatibilité d’ensemble d’instructions jusqu’à preuve du contraire. Ne commencez pas par
réinstaller des paquets. Commencez par prouver quel opcode a planté et quelles fonctionnalités CPU sont disponibles.
Première étape : confirmer que c’est bien un SIGILL et capturer où
- Vérifiez journald / les messages du noyau pour SIGILL et pour l’instruction pointer fautive.
- Confirmez le binaire qui a planté (pas seulement le script wrapper).
- Obtenez une backtrace (même grossière) depuis un core file.
Deuxième étape : comparer les drapeaux CPU avec ce que le binaire attend
- Collectez les drapeaux CPU depuis
/proc/cpuinfo(et depuis l’intérieur du conteneur/VM si applicable). - Identifiez si le binaire a été compilé avec
-march=native,-mavx2, ou une baseline micro-architecture x86-64. - Vérifiez si votre libc sélectionne une variante optimisée via des répertoires hwcaps.
Troisième étape : décider de la voie de correction propre
- Si c’est votre code : rebuild avec la baseline correcte, publiez plusieurs cibles, ou ajoutez un dispatch à l’exécution.
- Si c’est un binaire tiers : procurez-vous une build compatible, figez une version compatible, ou changez le modèle CPU exposé par votre hyperviseur.
- Si c’est un problème d’hétérogénéité de flotte : segmentez les déploiements par capacité CPU et faites respecter cela par votre scheduler.
Idée paraphrasée (attribuée) : Espérer n’est pas une stratégie
— couramment utilisée en ingénierie de fiabilité.
Dans ce cas précis : « Nous espérions que tous les hôtes avaient AVX2 » n’est pas un plan. C’est une timeline d’incident.
Ce que signifie réellement « Illegal instruction » sur Linux
Sur Linux, « Illegal instruction » correspond presque toujours à un signal SIGILL délivré au processus.
Le CPU a tenté de décoder/exécuter un opcode invalide pour le niveau ISA courant, ou il a rencontré une instruction privilégiée/interdite en espace utilisateur.
En pratique opérationnelle, la cause courante est une incompatibilité d’extensions ISA : votre binaire utilise AVX/AVX2/AVX-512, SSE4.2, BMI1/2, FMA, etc.,
mais le CPU (ou le CPU virtuel présenté à l’invité) ne les prend pas en charge.
La signature de l’échec est rude : pas d’erreur utile, souvent aucun log applicatif, parfois même pas de backtrace exploitable si vous n’avez pas activé les core dumps.
Vous corrigez cela en alignant ce pour quoi vous avez compilé avec ce sur quoi vous avez déployé.
Une confusion petite mais récurrente : SIGILL n’est pas la même chose qu’un segfault. Un segfault est une violation mémoire.
SIGILL est « le CPU refuse ». Cela peut aussi arriver à cause de binaires corrompus ou d’un mauvais code JIT, mais dans les flottes c’est surtout une incompatibilité de fonctionnalités.
Blague n°1 : Une instruction illégale est la manière qu’a le CPU de dire « Je ne parle pas ce dialecte », sauf qu’il l’exprime en renversant la table.
Faits et contexte utilisables dans un postmortem
Ce sont des détails courts et concrets qui aident une équipe à passer de « crash mystérieux » à « mode de défaillance compris ».
- SIGILL est plus ancien que votre système de build. Les signaux Unix comme SIGILL existent depuis des décennies ; c’est un moyen premier du système d’exploitation pour signaler des opcodes illégaux.
- x86-64 n’est plus une chose unique. Les distributions modernes distinguent de plus en plus des niveaux de baseline x86-64 (souvent appelés x86-64-v1/v2/v3/v4).
- SSE2 est devenu effectivement obligatoire sur x86 64 bits. C’est pourquoi les binaires « baseline x86-64 » supposent au minimum SSE2.
- AVX et AVX2 ne sont pas des « vitesses gratuites ». Ils peuvent provoquer un downclocking de fréquence sur certains CPU, donc « compilé avec AVX2 » peut être à la fois plus rapide ou plus lent selon la charge.
- La virtualisation peut mentir par omission. Une VM peut tourner sur un hôte compatible AVX2 mais présenter un modèle de CPU virtuel sans AVX2 à l’invité, causant des instructions illégales dans les binaires invités construits pour AVX2.
- Les conteneurs partagent le noyau hôte, pas les décisions de support de fonctionnalités CPU. Ils voient le même CPU, mais votre image de conteneur peut avoir été buildée sur une machine différente avec des hypothèses différentes.
- glibc peut sélectionner des chemins de code optimisés à l’exécution. Avec les « hardware capabilities » (hwcaps), la libc peut charger des implémentations optimisées selon les fonctionnalités CPU, changeant le comportement après des mises à jour.
- « Ça marche sur ma machine » signifie souvent littéralement « ça marche sur mon CPU ». Les machines de build sont souvent plus récentes que les nœuds de production ; ce décalage est un classique piège silencieux.
- Certaines runtime de langage fournissent plusieurs chemins de code. D’autres non. Si votre runtime n’a pas de dispatch à l’exécution, vos modules d’extension compilés peuvent déclencher SIGILL.
Drapeaux CPU vs binaires : d’où viennent les incompatibilités
Les trois façons d’obtenir SIGILL en déploiement réel
-
Vous avez compilé « trop récent ». Le binaire contient des instructions non supportées par une fraction de la flotte.
Coupables fréquents :-march=native, compilation sur un laptop/workstation, ou usage d’une build optimisée fournisseur. -
Vous avez déployé sur du « trop ancien ». Les cycles de renouvellement hardware sont désordonnés. La flotte devient mixte :
nouveaux nœuds avec AVX2, anciens sans ; ou Intel plus récent vs AMD plus ancien ; ou familles d’instances cloud avec jeux de fonctionnalités différents. -
Votre plateforme a masqué des fonctionnalités CPU. Hyperviseurs, politiques de migration live, ou modèles CPU conservateurs peuvent cacher des fonctionnalités.
Donc vous compilez contre un ensemble de drapeaux, mais l’environnement d’exécution ne correspond pas.
Pourquoi Ubuntu 24.04 apparaît dans ces incidents
Ubuntu 24.04 ne « casse » pas les CPU. Ce qu’il apporte, ce sont des toolchains plus récentes, des bibliothèques mises à jour, et un environnement de packaging plus moderne.
Cela compte parce que :
- Un nouveau compilateur peut activer des patterns d’auto-vectorisation différents au même niveau d’optimisation.
- Des bibliothèques plus récentes peuvent livrer des variantes optimisées plus agressives et les sélectionner selon les hwcaps.
- Vos rebuilds déclenchés par la mise à niveau de l’OS peuvent avoir changé les flags de baseline (par exemple, des runners CI mis à jour compilant désormais pour des CPU plus récents).
Que faire avec cette connaissance
Traitez le niveau ISA comme un contrat d’API. Si vous ne le spécifiez pas, la toolchain l’inférera volontiers. Et elle l’inférera depuis la machine sur laquelle vous buildiez,
qui n’est presque jamais le pire CPU sur lequel vous déployez.
Tâches pratiques : commandes, sorties, décisions (12+)
Ce sont les tâches que j’exécute réellement quand un service s’effondre avec SIGILL. Chaque tâche inclut la commande, une sortie réaliste,
ce que signifie la sortie, et la décision suivante à prendre.
Tâche 1 : Confirmer SIGILL dans journald
cr0x@server:~$ sudo journalctl -u myservice --since "10 min ago" -n 50
Dec 30 10:11:02 node-17 systemd[1]: Started myservice.
Dec 30 10:11:03 node-17 myservice[24891]: Illegal instruction (core dumped)
Dec 30 10:11:03 node-17 systemd[1]: myservice.service: Main process exited, code=killed, status=4/ILL
Dec 30 10:11:03 node-17 systemd[1]: myservice.service: Failed with result 'signal'.
Signification : systemd rapporte status=4/ILL. C’est SIGILL, pas un segfault.
Décision : arrêter de poursuivre des théories de corruption mémoire ; passer au workflow de mismatch ISA et capturer un core.
Tâche 2 : Vérifier si le noyau a loggué le RIP fautif (instruction pointer)
cr0x@server:~$ sudo dmesg --ctime | tail -n 20
[Mon Dec 30 10:11:03 2025] myservice[24891]: trap invalid opcode ip:000055d2f2b1c3aa sp:00007ffeefb6f1d0 error:0 in myservice[55d2f2b00000+1f000]
Signification : « invalid opcode » est la manière dont le noyau indique « le CPU a rejeté l’instruction ».
Décision : utiliser l’adresse IP avec addr2line ou gdb plus tard ; confirmer aussi quelle image binaire s’exécutait.
Tâche 3 : Identifier le binaire exact et son architecture
cr0x@server:~$ file /usr/local/bin/myservice
/usr/local/bin/myservice: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a1b2c3..., for GNU/Linux 3.2.0, not stripped
Signification : C’est un ELF 64-bit x86-64. Rien d’exotique comme un conteneur de mauvaise architecture.
Décision : poursuivre la comparaison des drapeaux CPU et les vérifications de sélection des bibliothèques.
Tâche 4 : Collecter les drapeaux CPU depuis l’hôte
cr0x@server:~$ lscpu | sed -n '1,25p'
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 46 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 32
Vendor ID: GenuineIntel
Model name: Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx lm constant_tsc rep_good nopl xtopology cpuid tsc_known_freq pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 popcnt aes xsave avx
Signification : Ce CPU a AVX mais pas AVX2 (pas de flag avx2).
Décision : si votre binaire utilise AVX2, il provoquera un SIGILL ici. Ensuite : vérifier si le binaire ou une bibliothèque attend AVX2.
Tâche 5 : Confirmer les flags depuis /proc/cpuinfo (utile aussi dans les conteneurs)
cr0x@server:~$ grep -m1 -oE 'flags\s*:.*' /proc/cpuinfo | cut -c1-180
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx lm constant_tsc rep_good nopl xtopology cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 popcnt aes xsave avx
Signification : Correspond à lscpu. Pas d’AVX2.
Décision : traiter cet hôte comme « x86-64 avec AVX mais sans AVX2 » pour le ciblage de déploiement.
Tâche 6 : Vérifier si le binaire contient des instructions AVX2 (scan rapide)
cr0x@server:~$ objdump -d /usr/local/bin/myservice | grep -m1 -E '\bvpmaddubsw\b|\bvpbroadcastd\b|\bvpandd\b'
000000000000f7c0: vpbroadcastd 0x10(%rdi),%ymm0
Signification : C’est une instruction YMM souvent associée à AVX2. Pas définitif seul, mais suspect.
Décision : valider via gdb à l’adresse fautive, ou vérifier les flags de build si vous contrôlez la compilation.
Tâche 7 : Utiliser gdb avec un core file pour confirmer l’instruction fautive
cr0x@server:~$ coredumpctl gdb myservice
PID: 24891 (myservice)
UID: 1001 (svc-myservice)
Signal: 4 (ILL)
Timestamp: Mon 2025-12-30 10:11:03 UTC (3min ago)
Command Line: /usr/local/bin/myservice --config /etc/myservice/config.yml
Executable: /usr/local/bin/myservice
Control Group: /system.slice/myservice.service
Unit: myservice.service
Message: Process 24891 (myservice) of user 1001 dumped core.
(gdb) info registers rip
rip 0x55d2f2b1c3aa
(gdb) x/6i $rip
=> 0x55d2f2b1c3aa: vpbroadcastd 0x10(%rdi),%ymm0
0x55d2f2b1c3b0: vpmaddubsw %ymm1,%ymm0,%ymm0
0x55d2f2b1c3b5: vpmaddwd %ymm2,%ymm0,%ymm0
Signification : Le plantage se produit sur vpbroadcastd, une instruction AVX2. Votre CPU n’a pas AVX2. Cas clos.
Décision : déployer une build non-AVX2, ajouter un dispatch à l’exécution, ou contraindre l’ordonnancement aux nœuds compatibles AVX2.
Tâche 8 : Trouver quelles bibliothèques partagées le binaire charge (et d’où)
cr0x@server:~$ ldd /usr/local/bin/myservice | head -n 20
linux-vdso.so.1 (0x00007ffeefbf9000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f4b5a9d0000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f4b5a8e9000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f4b5a6d0000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f4b5a6b0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4b5a4a0000)
/lib64/ld-linux-x86-64.so.2 (0x00007f4b5aa0f000)
Signification : Liaison dynamique normale ; rien d’évidemment hors-chemin.
Décision : si le binaire lui-même n’est pas AVX2 mais qu’une bibliothèque l’est, il faudra inspecter les bibliothèques aussi. Sinon concentrez-vous sur la recompilation de l’appli.
Tâche 9 : Vérifier la sélection hwcaps de glibc (diagnostiquer « ça a changé après la mise à jour »)
cr0x@server:~$ LD_DEBUG=libs /usr/local/bin/myservice 2>&1 | head -n 25
24988: find library=libc.so.6 [0]; searching
24988: search path=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3:/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2:/lib/x86_64-linux-gnu/tls:/lib/x86_64-linux-gnu
24988: trying file=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/libc.so.6
24988: trying file=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/libc.so.6
24988: trying file=/lib/x86_64-linux-gnu/libc.so.6
Signification : Le loader recherche d’abord les répertoires hwcaps. Sur certains systèmes, une variante v3/v2 peut être chargée.
Décision : si SIGILL a commencé après une mise à jour de glibc et que vous êtes sur des CPU anciens, assurez-vous que la bonne variante est sélectionnée (ou supprimez un override incompatible).
Tâche 10 : Vérifier quelle variante libc vous avez effectivement chargée
cr0x@server:~$ LD_DEBUG=libs /bin/true 2>&1 | grep -E 'trying file=.*/glibc-hwcaps' | head -n 5
25033: trying file=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v3/libc.so.6
25033: trying file=/lib/x86_64-linux-gnu/glibc-hwcaps/x86-64-v2/libc.so.6
Signification : Cet hôte a au moins ces répertoires ; la sélection dépend des capacités CPU.
Décision : si vous déboguez un conteneur ou un chroot, confirmez la libc dans cet arbre de filesystem et pas seulement celle de l’hôte.
Tâche 11 : Déterminer le niveau ISA de baseline supporté (heuristique rapide)
cr0x@server:~$ python3 - <<'PY'
import re
flags = open("/proc/cpuinfo").read()
m = re.search(r'^flags\s*:\s*(.*)$', flags, re.M)
f = set(m.group(1).split()) if m else set()
need_v2 = {"sse3","ssse3","sse4_1","sse4_2","popcnt","cx16"}
need_v3 = need_v2 | {"avx","avx2","bmi1","bmi2","fma"}
print("has_v2:", need_v2.issubset(f))
print("has_v3:", need_v3.issubset(f))
print("missing_for_v3:", sorted(list(need_v3 - f))[:20])
PY
has_v2: True
has_v3: False
missing_for_v3: ['avx2', 'bmi1', 'bmi2']
Signification : Cet hôte est approximativement « v2-ish » mais pas v3 (il manque AVX2/BMI).
Décision : ne déployez pas de binaires x86-64-v3 ici ; build pour v2 ou fournissez un fallback v2.
Tâche 12 : Inspecter les métadonnées de build d’un binaire Go (exemple d’auto-déclaration)
cr0x@server:~$ go version -m /usr/local/bin/myservice | head -n 30
/usr/local/bin/myservice: go1.22.2
path example.com/myservice
build -ldflags="-s -w"
build CGO_ENABLED=1
Signification : C’est un binaire Go et CGO est activé. Cela signifie que des bibliothèques C/C++ natives ou des extensions pourraient injecter des instructions spécifiques au CPU.
Décision : si SIGILL survient, inspectez les bibliothèques CGO liées ou rebuild avec des CFLAGS contrôlés (pas -march=native).
Tâche 13 : Pour projets C/C++, prouver si « native » s’est glissé dans les flags de build
cr0x@server:~$ strings /usr/local/bin/myservice | grep -E -- '-march=|-mavx|-mavx2|-msse4\.2' | head
-march=native
-mavx2
Signification : Le binaire contient probablement des flags de compilateur embarqués (pas toujours, mais fréquent dans les builds avec métadonnées).
Décision : supposez que la build n’est pas portable. Rebuild avec une baseline explicite : par exemple, -march=x86-64-v2 ou une cible conservatrice.
Tâche 14 : Valider que le conteneur voit les mêmes drapeaux CPU (et attraper « ça échoue seulement dans un environnement »)
cr0x@server:~$ docker run --rm ubuntu:24.04 bash -lc "lscpu | grep -E 'Model name|Flags' | head -n 2"
Model name: Intel(R) Xeon(R) CPU E5-2670 v2 @ 2.50GHz
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss ht syscall nx lm constant_tsc rep_good nopl xtopology cpuid pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 popcnt aes xsave avx
Signification : Les conteneurs voient les flags CPU de l’hôte (comme attendu).
Décision : si votre image de conteneur plante avec SIGILL sur cet hôte, ce sont les binaires de l’image, pas un masquage CPU propre aux conteneurs.
Tâche 15 : Valider le modèle CPU et les fonctionnalités exposées par la VM (exemple KVM/libvirt)
cr0x@hypervisor:~$ sudo virsh dominfo appvm-03 | sed -n '1,12p'
Id: 7
Name: appvm-03
UUID: 9c6d1c9e-3a9a-4f62-9b61-0a0f3c7a2c11
OS Type: hvm
State: running
CPU(s): 8
CPU time: 18344.1s
cr0x@hypervisor:~$ sudo virsh dumpxml appvm-03 | grep -nE '
58: Haswell-noTSX
59:
60:
Signification : La VM est explicitement configurée pour désactiver AVX2. Un binaire invité compilé pour AVX2 provoquera SIGILL même si le CPU hôte le supporte.
Décision : corriger le modèle CPU de la VM (si sûr) ou compiler/déployer un binaire non-AVX2 pour cette VM.
Trois micro-histoires du monde corporate (anonymisées)
Histoire 1 : L’incident causé par une mauvaise hypothèse
Une entreprise exploitait un petit cluster Kubernetes on-prem avec un mélange de serveurs. Certains étaient récents avec AVX2 ; d’autres plus anciens mais fiables, conservés
parce qu’« ils ont encore beaucoup de RAM » et que personne ne voulait ouvrir la baie.
L’équipe a mis à jour sa flotte de runners CI en premier. Cela a silencieusement changé l’environnement de compilation pour un service sensible à la latence écrit en C++.
Ils ont aussi activé une option de build « native » parce qu’ils ont vu un microbenchmark s’améliorer sur le runner CI.
Le merge est passé proprement. Les tests sont verts. Le service a été déployé.
Puis le rollout a atteint un des nœuds plus anciens. Le pod a redémarré instantanément et a continué de redémarrer. Les logs n’affichaient qu’une ligne : Illegal instruction.
L’astreint a fait les opérations habituelles : supprimer le pod, rescheduler, vider le nœud. Le scheduler continuait à le placer sur les nœuds anciens car il n’y avait aucune contrainte.
L’hypothèse cachée était simple : « Tout x86_64 est à peu près identique. » Ce n’est pas vrai. La correction a été aussi simple, mais a exigé de la discipline :
rebuild avec une cible baseline explicite et publier cela comme l’artifact par défaut. Ils ont aussi ajouté un label de nœud pour AVX2 et n’ont ciblé la build AVX2
que sur les nœuds labelisés. Après ça, cette classe d’incident a disparu.
Histoire 2 : L’optimisation qui s’est retournée contre eux
Une autre organisation utilisait un binaire fourni par un vendeur pour un agent de traitement de données. Le fournisseur proposait deux téléchargements :
une build « standard » et une build « optimisée ». Quelqu’un a choisi « optimisée » parce que le mot semble promettre des gains, et l’agent est effectivement plus rapide sur leur
environnement de staging.
La production était répartie entre deux familles d’instances cloud. Une famille exposait AVX2 ; l’autre non (ou, plus précisément, une génération particulière ne l’exposait pas).
Le binaire optimisé supposait AVX2. La moitié de la flotte a crashé au démarrage avec SIGILL, mais seulement après qu’une mise à jour roulante ait atteint la famille plus ancienne.
Le retour de bâton n’était pas seulement le crash. C’était le rayon d’impact opérationnel. Les boucles de redémarrage ont saturé les logs. L’autoscaling a essayé de compenser.
Une file d’attente aval s’est enrayée parce qu’une portion des agents était morte, et les agents « sains » ne pouvaient pas suivre.
L’optimisation s’est transformée en amplificateur d’échec distribué.
La correction n’a pas été héroïque. Ils sont revenus à la build standard puis ont introduit un déploiement explicitement « aware » des capacités :
pools de nœuds séparés par famille d’instance et sélection d’artifact distincte. La leçon était ennuyeuse mais coûteuse : si vous ne connaissez pas la CPU baseline de votre
flotte, vous n’avez pas le droit d’exécuter des binaires « optimisés » par défaut.
Histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une troisième équipe avait une politique : chaque artifact de service doit déclarer une baseline CPU dans ses métadonnées de release, et chaque cluster doit publier un fait « ISA minimum »
que le scheduler applique. Ce n’était pas glamour. Les gens se plaignaient. Ça ressemblait à de la paperasserie.
Puis Ubuntu 24.04 est arrivé dans l’environnement. Une version de toolchain plus récente a poussé les builds orientés performance à émettre des instructions vectorielles différentes dans les hot paths.
Quelques services ont été rebuildés. L’un d’eux aurait planté sur des nœuds plus anciens s’il avait été déployé massivement.
Il n’a pas planté. Le système de déploiement a refusé de l’ordonner sur des nœuds incompatibles parce que les métadonnées de l’artifact indiquaient « requires x86-64-v3 ».
Le rollout s’est arrêté uniquement sur les nœuds qui satisfaisaient la condition. Les utilisateurs n’ont rien remarqué. L’astreint n’a pas été réveillé. L’équipe a conservé son sommeil, ce qui est
le bon résultat pour presque tout travail d’ingénierie.
Le postmortem a été une non-événement : un ticket pour étendre le pool v3, une note pour maintenir un fallback v2 pour les pools legacy, et une appréciation discrète
pour des garde-fous qui ne négocient pas.
Blague n°2 : La seule chose plus rapide qu’une build AVX2 est une build AVX2 qui plante instantanément sur un CPU avec seulement AVX.
Conteneurs et VM : le CPU que vous pensez avoir vs le CPU que vous obtenez
L’histoire des fonctionnalités CPU varie selon que vous êtes sur bare metal, conteneurs ou machines virtuelles.
Les interruptions de production surviennent quand les équipes appliquent le mauvais modèle mental.
Conteneurs : même CPU, hypothèses différentes
Les conteneurs n’émulent pas le CPU. Si votre conteneur exécute vpbroadcastd sur un CPU sans AVX2, il mourra de la même façon qu’un processus hôte.
Le mismatch vient généralement de l’environnement de build :
images construites sur des runners plus récents, ou builds multi-étapes qui compilent avec des optimisations « native » parce que personne n’a figé les CFLAGS.
L’approche propre : construire des artefacts portables, puis éventuellement expédier des variantes « accélérées » et choisir à l’exécution ou via l’ordonnancement.
N’expédiez pas une unique image « optimisée » en espérant l’homogénéité du cluster. Elle n’existe jamais longtemps.
VM : modèles CPU, migration et valeurs par défaut conservatrices
Les hyperviseurs exposent souvent un modèle de CPU virtuel. Ce n’est pas seulement du marketing ; cela détermine quelles extensions d’instruction sont disponibles dans l’invité.
Certains environnements cachent volontairement des fonctionnalités pour permettre la migration live sur une plus large gamme d’hôtes.
Si vous compilez dans une VM qui expose AVX2, puis déployez sur une VM qui ne l’expose pas, vous obtenez SIGILL. Si vous compilez sur bare metal avec AVX2 et déployez sur une VM
avec un modèle CPU conservateur, vous obtenez SIGILL. Si vous compilez dans le même type de VM mais la configuration hyperviseur diffère, vous obtenez SIGILL.
La solution est de la gouvernance : définir des modèles CPU par environnement, les documenter comme contrats de compatibilité, et faire en sorte que votre pipeline de build cible ces contrats.
« Peu importe ce que le cloud nous donne » n’est pas un contrat CPU. C’est un générateur de surprises.
glibc hwcaps et pourquoi Ubuntu 24.04 peut « soudainement » choisir du code plus rapide
glibc prend en charge depuis longtemps plusieurs implémentations optimisées de fonctions via des mécanismes comme les résolveurs IFUNC.
Plus récemment, les distributions ont recours aux répertoires hwcaps :
des chemins filesystem qui contiennent des builds de bibliothèques optimisées pour des niveaux de baseline x86-64 spécifiques.
Le chargeur dynamique recherche ces répertoires en premier (comme vous l’avez vu dans la sortie LD_DEBUG=libs).
Sur un CPU qui satisfait les critères, glibc peut charger une variante v2/v3 d’une bibliothèque, activant des implémentations plus rapides.
Sur un CPU qui ne les satisfait pas, il doit retomber sur la variante de compatibilité.
Quand cela tourne mal en production, c’est souvent à cause d’un des schémas suivants :
- Un chroot/image de conteneur contient des variantes hwcaps qui ne correspondent pas au CPU réel sur lequel il s’exécute (par exemple, copiées depuis un rootfs différent).
- Une variable d’environnement ou une configuration du loader provoque des chemins de recherche inattendus.
- Une bibliothèque tierce embarque du code optimisé et fait sa propre détection de manière douteuse.
La conclusion pratique : si SIGILL apparaît après une mise à jour de l’image de base, ne supposez pas que c’est uniquement « votre binaire ».
Ça peut être « la variante de bibliothèque que vous chargez maintenant ». Prouvez-le avec une analyse du core et la sortie debug du loader.
Erreurs courantes : symptôme → cause racine → fix
1) Crash immédiatement au démarrage après une mise à jour
Symptôme : le service démarre puis meurt instantanément avec Illegal instruction (core dumped).
Cause racine : un binaire nouvellement buildé suppose AVX2/SSE4.2/FMA à cause des flags de la machine de build ou de décisions du toolchain.
Fix : rebuild avec une baseline explicite (-march=x86-64-v2 ou cible conservatrice), et faites respecter cette baseline dans le CI.
2) Seulement certains nœuds plantent ; le rescheduling « règle » le problème
Symptôme : la même image de conteneur tourne bien sur certains nœuds et plante sur d’autres.
Cause racine : flotte hétérogène en fonctionnalités CPU ; l’image a été buildée pour la « moitié meilleure ».
Fix : labeliser les nœuds par capacité ; contraindre l’ordonnancement ; expédier plusieurs variantes d’image ou conserver une baseline portable avec dispatch runtime.
3) Marche sur bare metal, échoue dans une VM (ou vice versa)
Symptôme : le binaire tourne sur une station de travail mais SIGILL dans une VM invitée.
Cause racine : le modèle CPU de la VM cache des fonctionnalités (AVX2 désactivé, baseline conservatrice pour la migration).
Fix : aligner le modèle CPU de la VM sur les exigences, ou compiler pour les flags exposés par l’invité ; ne compilez pas dans un environnement avec des flags différents de la production.
4) Un seul chemin de code plante sous charge
Symptôme : le service tourne un moment, puis SIGILL lors d’opérations spécifiques.
Cause racine : dispatch à l’exécution / code JIT ou plugin sélectionne un chemin AVX2 conditionnellement ; ou une fonction rarement utilisée est appelée.
Fix : capturez le core au crash ; identifiez la fonction/bibliothèque ; désactivez le chemin optimisé, ou corrigez la logique de dispatch pour vérifier correctement les flags.
5) « Mais le CPU hôte supporte AVX2 » et ça plante quand même
Symptôme : quelqu’un brandit la fiche technique et affirme que le serveur supporte l’instruction.
Cause racine : microcode/paramètres BIOS, masquage VM, ou conteneur tournant sur un nœud différent de celui supposé.
Fix : faites confiance à lscpu et au core dump, pas à la fiche technique ; validez les flags dans l’environnement d’exécution réel.
6) Un binaire tiers téléchargé sur Internet plante sur du hardware ancien
Symptôme : l’outil du vendeur fonctionne en staging, plante en environnement « legacy ».
Cause racine : le fournisseur a buildé pour une baseline plus récente (x86-64-v3) sans fournir de build portable.
Fix : exiger une build compatible baseline ; si indisponible, isoler sur des nœuds compatibles ou remplacer le composant.
Listes de vérification / plan étape par étape
Checklist A : Quand vous êtes appelé pour SIGILL (workflow opérateur)
-
Confirmer SIGILL : vérifier
journalctlet le statut systemd. Si ce n’est passtatus=4/ILL, arrêtez et redéfinissez le périmètre. -
Capturer l’emplacement du crash : chercher
invalid opcode ip:dansdmesg. - S’assurer que les core dumps existent : s’ils sont absents, activez-les temporairement et reproduisez de manière contrôlée.
-
Ouvrir le core : utiliser
coredumpctl gdb, désassembler à RIP, identifier l’instruction. - Comparer avec les flags CPU : le CPU a-t-il l’extension requise par cette instruction ?
- Identifier s’il s’agit de votre binaire ou d’une bibliothèque partagée : vérifier les frames de la backtrace et les objets chargés.
- Décider la mitigation : rollback vers une build portable, pin à des nœuds compatibles, ou changer le modèle CPU des VM.
- Noter le contrat de baseline et l’appliquer dans CI/CD pour éviter de répéter l’incident la semaine suivante.
Checklist B : Politique propre de build-and-release (workflow équipe)
- Déclarer une ISA baseline de flotte (par environnement). Exemple : « prod-x86 doit supporter x86-64-v2 ; certains pools supportent v3 ».
-
Construire les artefacts avec des targets explicites ; interdire
-march=nativedans les builds de release. - Si vous avez besoin de performance, expédiez plusieurs builds : baseline + accéléré (v3/v4). Rendre la sélection explicite (labels scheduler ou dispatch runtime).
- Ajouter un self-check au démarrage : logger les flags CPU détectés et refuser de démarrer si les exigences ne sont pas remplies (échouer bruyamment, pas aléatoirement).
- Maintenir un hôte de test de compatibilité (ou modèle CPU VM) qui imite le CPU le plus ancien supporté. Exécuter des smoke tests dessus.
- Versionner et pinner les toolchains dans le CI pour réduire la « dérive silencieuse des rebuilds ».
Checklist C : Déploiement Kubernetes aware des capacités (pratique)
- Labeliser les nœuds selon les drapeaux CPU (présence ou non d’AVX2).
- Utiliser node selectors/affinity pour les workloads accélérés.
- Garder l’image baseline par défaut ; utiliser un déploiement séparé pour l’image accélérée.
- Surveiller les crash loops et corréler avec les labels des nœuds pour détecter la dérive tôt.
FAQ
1) Est-ce que « Illegal instruction » est toujours une incompatibilité de drapeaux CPU ?
Non, mais en production c’est la cause dominante. D’autres causes existent : binaires corrompus sur le disque, RAM défectueuse, génération de code JIT erronée, ou exécution de données comme code.
Votre premier travail est de prouver l’instruction fautive via un core dump.
2) Pourquoi cela a-t-il commencé après le passage à Ubuntu 24.04 ?
Parce que les mises à jour changent les toolchains et le comportement de sélection des bibliothèques. Même si votre code source n’a pas changé, vos outputs de build peuvent évoluer.
De plus, glibc hwcaps et les chemins de code optimisés peuvent être sélectionnés différemment après une mise à niveau de distribution.
3) Si je compile avec -O2, le compilateur peut-il quand même émettre de l’AVX2 ?
Pas à moins que votre target ne le permette. La cible par défaut dépend de la configuration du compilateur et des flags.
Le vrai piège est quand des systèmes de build injectent -march=native ou quand vous compilez sur un CPU avec des capacités plus larges puis déployez ailleurs.
4) Quelle est l’option la plus propre pour « un artefact pour partout » ?
Compiler pour une baseline conservatrice (souvent x86-64-v2 dans les flottes modernes, parfois plus ancien selon votre hardware) et utiliser le dispatch à l’exécution dans les hotspots.
Vous perdez un peu de performance de pointe mais gagnez en prévisibilité et simplicité opérationnelle.
5) Comment savoir si un binaire nécessite AVX2 sans le faire planter ?
Vous pouvez le désassembler et chercher des mnémoniques associés à AVX2, mais la méthode la plus fiable est : l’exécuter dans des conditions contrôlées,
collecter un core sur SIGILL, et inspecter l’instruction fautive à RIP.
6) Pourquoi cela ne plante-t-il que sur certaines requêtes ?
Certaines bibliothèques utilisent le dispatch runtime : elles vérifient les flags CPU et choisissent un chemin rapide. Si cette détection est fausse, ou si le chemin rapide est dans un plugin
utilisé uniquement pour certaines charges, vous verrez des crashes « aléatoires » liés à certains inputs.
7) La virtualisation peut-elle provoquer cela même si le CPU hôte supporte l’instruction ?
Oui. L’invité voit le modèle CPU virtuel, pas nécessairement l’hôte. Les hyperviseurs peuvent désactiver des fonctionnalités (volontairement, pour compatibilité ou migration).
Vérifiez toujours les flags CPU à l’intérieur de l’invité.
8) Qu’en est-il des serveurs ARM et de « Illegal instruction » ?
Même signal, extensions différentes. Sur ARM, vous verrez des incompatibilités comme compiler pour des fonctionnalités ARMv8.x plus récentes et exécuter sur des cœurs plus anciens,
ou utiliser des extensions crypto absentes. Le workflow est le même : prouver l’opcode, comparer aux fonctionnalités CPU, aligner les cibles de build.
9) Ne devrions-nous pas standardiser la flotte sur AVX2 et passer à autre chose ?
Si vous pouvez, oui — l’homogénéité réduit les modes de défaillance. Mais la standardisation est un programme, pas un souhait. Tant qu’elle n’est pas complète, supposez l’hétérogénéité et déployez en conséquence.
10) Quelle est la meilleure mitigation à long terme ?
Traitez les exigences ISA comme des contraintes de déploiement. Déclarez-les, testez-les, faites-les respecter. Le « meilleur » correctif est celui qui prévient la classe d’incident,
pas seulement celui qui éteint le feu actuel.
Étapes suivantes exécutables cette semaine
- Choisissez une ISA baseline pour chaque environnement (prod/staging/dev) et consignez-la dans votre contrat de plateforme.
-
Auditez les builds de release pour
-march=nativeet autres flags dépendants de l’hôte. Supprimez-les des artefacts de production. - Ajoutez un job CI « plus petit dénominateur commun » qui exécute des tests sur un modèle CPU VM correspondant à vos nœuds les plus anciens supportés.
- Pour les flottes hétérogènes, labelisez les nœuds par capacité et appliquez l’ordonnancement. Ne laissez plus le scheduler « découvrir » la compatibilité à la dure.
- Activez les core dumps de manière contrôlée pour les services où SIGILL serait catastrophique ; gardez le bouton prêt pour l’intervention incident.
- Si vous devez expédier des builds accélérées, faites-le explicitement : baseline + v3/v4, logique de sélection claire, et chemins de rollback évidents.
L’histoire d’un déploiement propre n’est pas « nous n’utilisons jamais de fonctionnalités CPU avancées ». C’est « nous les utilisons intentionnellement ». Ubuntu 24.04 ne vous a pas trahi.
Votre pipeline de build a fait exactement ce que vous lui aviez implicitement demandé. Maintenant rendez la demande explicite.