Actualizas a Ubuntu 24.04, tu canal de despliegue sigue en verde y entonces un servicio se cae con un mensaje seco:
Illegal instruction. No hay rastro de pila fiable. No hay logs útiles. Solo un core dump y un pager que ahora te ocupa la noche.
Este fallo normalmente no es “Ubuntu portándose raro”. Es física: la CPU intentó ejecutar un opcode que no soporta porque tu binario (o alguna de sus librerías) asumió un conjunto de flags de CPU distinto al que realmente tiene la máquina. La solución no es “reinstalar el paquete” ni “probar otro kernel”. La solución es hacer que tu compilación y tu despliegue estén de acuerdo sobre qué instrucciones son legales.
Guía rápida de diagnóstico
Cuando un proceso muere con Illegal instruction, estás depurando un desajuste del conjunto de instrucciones hasta que se demuestre lo contrario. No empieces reinstalando paquetes. Comienza probando qué opcode falló y qué características de CPU están disponibles.
Primero: confirma que es realmente SIGILL y captura dónde
- Revisa journald / mensajes del kernel para SIGILL y el puntero de instrucción que falló.
- Confirma el binario que se estrelló (no solo el script envoltorio).
- Obtén un backtrace (aunque sea burdo) desde un core file.
Segundo: compara flags de CPU con lo que el binario espera
- Recopila flags de CPU desde
/proc/cpuinfo(y desde dentro del contenedor/VM si aplica). - Identifica si el binario fue compilado con
-march=native,-mavx2o una baseline de microarquitectura x86-64. - Verifica si tu libc está seleccionando una variante optimizada vía directorios hwcaps.
Tercero: decide la vía limpia de corrección
- Si es tu código: recompila con la baseline correcta, publica múltiples targets o añade dispatch en tiempo de ejecución.
- Si es un binario de terceros: consigue una build compatible, fija una versión compatible o cambia el modelo de CPU expuesto por tu hipervisor.
- Si es un problema de heterogeneidad en la flota: segmenta despliegues por capacidad de CPU y haz que tu scheduler lo haga cumplir.
Idea parafraseada (atribuida): La esperanza no es una estrategia
— comúnmente atribuida a practicantes de ingeniería de fiabilidad.
En este caso específico: “Esperamos que todos los hosts tuvieran AVX2” no es un plan. Es una línea de tiempo de incidente.
Qué significa realmente “Illegal instruction” en Linux
En Linux, “Illegal instruction” casi siempre corresponde a una señal SIGILL entregada al proceso.
La CPU intentó decodificar/ejecutar un opcode que es inválido para el nivel ISA actual, o se topó con una instrucción privilegiada/prohibida en espacio de usuario.
En la práctica de producción, la causa común es la discordancia de extensiones ISA: tu binario usa AVX/AVX2/AVX-512, SSE4.2, BMI1/2, FMA, etc.,
pero la CPU (o la CPU virtual presentada al guest) no las soporta.
La firma de fallo es tosca: sin error útil, a menudo sin logs de aplicación, a veces ni siquiera un backtrace útil si no activaste core dumps.
Lo arreglas alineando para qué compilaste con sobre qué desplegaste.
Una pequeña pero recurrente confusión: SIGILL no es lo mismo que un segfault. Un segfault es acceso a memoria.
SIGILL es “la CPU se niega”. También puede ocurrir por binarios corruptos o salida JIT mala, pero en flotas es mayormente desajuste de características.
Chiste #1: Una instrucción ilegal es la forma en que la CPU dice “no hablo ese dialecto”, salvo que lo expresa volcándole la mesa.
Hechos y contexto que puedes usar en un postmortem
Estos son el tipo de detalles cortos y concretos que ayudan a un equipo a pasar de “crash misterioso” a “modo de fallo entendido”.
- SIGILL es más viejo que tu sistema de compilación. Las señales Unix como SIGILL existen desde hace décadas; es una forma de OS para reportar opcodes ilegales.
- x86-64 ya no es una sola cosa. Las distribuciones modernas distinguen cada vez más niveles baseline de x86-64 (a menudo referidos como x86-64-v1/v2/v3/v4).
- SSE2 se volvió prácticamente obligatorio en x86 de 64 bits. Por eso los binarios con baseline “x86-64” tienden a asumir al menos SSE2.
- AVX y AVX2 no son “velocidad gratis”. Pueden disparar reducción de frecuencia en algunas CPUs, así que “compilado con AVX2” puede ser tanto más rápido como más lento según la carga.
- La virtualización puede mentir por omisión. Una VM puede correr en un host con AVX2 y presentar un modelo de CPU virtual sin AVX2 al guest, provocando instrucciones ilegales en binarios del guest.
- Los contenedores comparten el kernel del host, no las decisiones de conjunto de características de CPU. Ven la misma CPU, pero tu imagen de contenedor pudo haberse construido en una máquina distinta con diferentes supuestos.
- glibc puede seleccionar caminos de código optimizados en tiempo de ejecución. Con las “capacidades de hardware” (hwcaps), libc puede cargar implementaciones optimizadas según las características de CPU, cambiando el comportamiento tras upgrades.
- “Funciona en mi máquina” muchas veces es literalmente “funciona en mi CPU”. Las máquinas de build suelen ser más nuevas que los nodos de producción; este desajuste es una típica trampa silenciosa.
- Algunos runtimes de lenguaje envían múltiples caminos de código. Otros no. Si tu runtime carece de dispatch en tiempo de ejecución, tus módulos de extensión compilados pueden ser los que desencadenen SIGILL.
Flags de CPU vs binarios: de dónde vienen los desajustes
Las tres formas en que obtienes SIGILL en despliegues reales
-
Compilaste “demasiado moderno”. El binario incluye instrucciones no soportadas por una fracción de la flota.
Culpables comunes:-march=native, compilar en un portátil/estación de trabajo con CPU más nueva, o usar una build optimizada del proveedor. -
Desplegaste sobre hardware “demasiado viejo”. Los ciclos de renovación de hardware son desordenados. La flota queda mixta:
nodos nuevos con AVX2, nodos viejos sin él; o Intel más nuevo vs AMD más viejo; o familias de instancias cloud con conjuntos de características distintos. -
Tu plataforma enmascaró características de CPU. Hipervisores, políticas de live migration o modelos de CPU conservadores pueden ocultar características.
Así compilas contra un conjunto de flags, pero el entorno de ejecución no coincide.
Por qué aparece Ubuntu 24.04 en estos incidentes
Ubuntu 24.04 no está “rompiendo CPUs”. Lo que trae es un toolchain más nuevo, librerías más recientes y un entorno de empaquetado más moderno.
Eso importa porque:
- Un compilador nuevo puede habilitar patrones de auto-vectorización diferentes al mismo nivel de optimización.
- Librerías más nuevas pueden incluir variantes optimizadas más agresivas y seleccionarlas según hwcaps.
- Tus reconstrucciones activadas por la actualización del SO pueden haber cambiado flags baseline (por ejemplo, runners de CI actualizados que ahora compilan para CPUs más nuevas).
Qué hacer con este conocimiento
Trata el nivel ISA como un contrato API. Si no lo especificas, el toolchain lo inferirá. Y lo inferirá desde la máquina en la que compilaste,
que casi nunca es la CPU más limitada en la que despliegas.
Tareas prácticas: comandos, salidas, decisiones (12+)
Estas son las tareas que realmente ejecuto cuando un servicio empieza a caerse con SIGILL. Cada tarea incluye el comando, una salida realista,
lo que significa la salida y la decisión que tomas después.
Tarea 1: Confirmar SIGILL en 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'.
Significado: systemd reporta status=4/ILL. Eso es SIGILL, no un segfault.
Decisión: deja de perseguir teorías de corrupción de memoria; pasa al flujo de trabajo de desajuste ISA y captura un core.
Tarea 2: Comprobar si el kernel registró el RIP que falló
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]
Significado: “invalid opcode” es la forma del kernel de decir “la CPU rechazó la instrucción”.
Decisión: usa la dirección IP con addr2line o gdb después; además confirma qué imagen binaria se estaba ejecutando.
Tarea 3: Identificar el binario exacto y su arquitectura
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
Significado: Es un ELF x86-64 de 64 bits. Nada exótico como un contenedor de arquitectura equivocada.
Decisión: sigue con la comparación de flags de CPU y comprobaciones de selección de librerías.
Tarea 4: Recopilar flags de CPU del host
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
Significado: Esta CPU tiene AVX pero no AVX2 (no aparece la flag avx2).
Decisión: si tu binario usa AVX2, hará SIGILL aquí. Siguiente: verifica si el binario o alguna librería espera AVX2.
Tarea 5: Confirmar flags desde /proc/cpuinfo (útil en contenedores también)
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
Significado: Coincide con lscpu. No hay AVX2.
Decisión: trata este host como “x86-64 con AVX pero sin AVX2” para apuntar despliegues.
Tarea 6: Comprobar si el binario contiene instrucciones AVX2 (búsqueda rápida)
cr0x@server:~$ objdump -d /usr/local/bin/myservice | grep -m1 -E '\bvpmaddubsw\b|\bvpbroadcastd\b|\bvpandd\b'
000000000000f7c0: vpbroadcastd 0x10(%rdi),%ymm0
Significado: Esa es una instrucción YMM comúnmente asociada con AVX2. No es concluyente por sí sola, pero es sospechosa.
Decisión: valida vía gdb en la dirección que falló, o revisa flags de compilación si controlas la build.
Tarea 7: Usar gdb con un core para confirmar la instrucción que falló
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
Significado: El crash ocurre en vpbroadcastd, una instrucción AVX2. Tu CPU no tiene AVX2. Caso cerrado.
Decisión: desplegar una build sin AVX2, añadir dispatch en tiempo de ejecución o limitar la programación a nodos con AVX2.
Tarea 8: Averiguar qué librerías compartidas carga el binario (y desde dónde)
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)
Significado: Enlace dinámico normal; nada aparentemente fuera de lugar.
Decisión: si el binario en sí no es AVX2 pero una librería sí, tendrás que inspeccionar las librerías también. Si no, céntrate en recompilar la app.
Tarea 9: Revisar la selección de hwcaps de glibc (diagnosticar “cambió tras el upgrade”)
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
Significado: El loader busca primero en los directorios hwcaps. En algunos sistemas, se puede cargar una variante v3/v2.
Decisión: si SIGILL empezó tras actualizar glibc y estás en CPUs viejos, asegúrate de que se seleccione la variante correcta (o elimina una anulación incompatible).
Tarea 10: Verificar qué variante de libc cargaste realmente
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
Significado: Este host al menos tiene presentes los directorios; la selección depende de las capacidades de la CPU.
Decisión: si depuras un contenedor o chroot, confirma la libc en ese árbol de filesystem y no la del host.
Tarea 11: Determinar nivel baseline de ISA soportado (heurística rápida)
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']
Significado: Este host es aproximadamente “v2-ish” pero no v3 (le faltan AVX2/BMI).
Decisión: no despliegues binarios x86-64-v3 aquí; compila para v2 o provee una alternativa v2.
Tarea 12: Inspeccionar metadatos de build de un binario Go (ejemplo de auto-informe)
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
Significado: Es un binario Go y CGO está habilitado. Eso significa que librerías C/C++ nativas o extensiones podrían estar inyectando instrucciones específicas de CPU.
Decisión: si ocurre SIGILL, inspecciona las librerías enlazadas por CGO o recompila con CFLAGS controlados (no -march=native).
Tarea 13: Para proyectos C/C++, probar si “native” se coló en los flags de compilación
cr0x@server:~$ strings /usr/local/bin/myservice | grep -E -- '-march=|-mavx|-mavx2|-msse4\.2' | head
-march=native
-mavx2
Significado: El binario probablemente contiene flags del compilador incrustadas (no siempre, pero común en builds con metadata).
Decisión: asume que la build no es portable. Recompila con una baseline explícita: por ejemplo, -march=x86-64-v2 o un target conservador.
Tarea 14: Validar que el contenedor ve las mismas flags CPU (y captar “falla solo en un entorno”)
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
Significado: Los contenedores ven las flags de CPU del host (como se espera).
Decisión: si tu imagen de contenedor falla con SIGILL en este host, son los binarios de la imagen, no el enmascaramiento de CPU del contenedor.
Tarea 15: Validar modelo de CPU expuesto en VMs y features (ejemplo 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:
Significado: La VM está explícitamente configurada para deshabilitar AVX2. Un binario guest compilado para AVX2 hará SIGILL aunque el host lo soporte.
Decisión: arregla el modelo de CPU de la VM (si es seguro) o compila/despliega un binario no-AVX2 a esa VM.
Tres relatos del mundo corporativo (anonimizados)
Historia 1: El incidente causado por una suposición equivocada
Una compañía operaba un pequeño clúster on-prem Kubernetes con una mezcla de servidores. Algunos eran relativamente nuevos con AVX2; otros eran viejos pero fiables, mantenidos porque “todavía tienen mucha RAM” y nadie quería tocar el rack.
El equipo actualizó primero su flota de runners de CI. Eso silenciosamente cambió el entorno de compilación para un servicio sensible a latencia escrito en C++.
También activaron una opción de compilación a “native” porque vieron una mejora en un microbenchmark en la máquina del runner.
Se mergeó sin problemas. Las pruebas pasaron. El servicio se desplegó.
Entonces el rollout alcanzó uno de los nodos más viejos. El pod se reinició al instante y siguió reiniciándose. Los logs mostraban una línea: Illegal instruction.
El on-call hizo las cosas normales: borrar el pod, reubicar, drenar el nodo. El scheduler seguía colocándolo en los nodos viejos porque no había ninguna restricción.
La suposición oculta fue simple: “Todo x86_64 es básicamente igual”. No lo es. La solución también fue simple, pero requirió disciplina:
recompilar con un target baseline explícito y publicarlo como artefacto por defecto. También añadieron una etiqueta de nodo para AVX2 y fijaron el build AVX2
solo a nodos etiquetados. Después de eso, la clase de incidentes desapareció.
Historia 2: La optimización que se volvió en contra
Otra organización usó un binario provisto por un vendor para un agente de procesamiento de datos. El vendor ofrecía dos descargas:
una build “estándar” y una “optimizada”. Alguien eligió “optimizada” porque la palabra suena a dinero gratis, y el agente efectivamente fue más rápido en su entorno de staging.
Producción estaba dividida entre dos familias de instancias cloud. Una familia exponía AVX2; la otra no (o, más precisamente, una generación en particular no lo hacía).
El binario optimizado asumía AVX2. La mitad de la flota se estrelló al inicio con SIGILL, pero solo después de que un rolling upgrade alcanzó la familia más vieja.
El problema no fue solo el crash. Fue el radio de impacto operativo. Los bucles de reinicio saturaron el logging. El autoscaling intentó compensar.
Una cola downstream se fue acumulando porque una parte de los agentes estaba muerta, y los “saludables” no podían con la carga.
La optimización se convirtió en un amplificador de fallo distribuido.
La solución no fue heroica. Revirtieron a la build estándar y luego introdujeron un despliegue “consciente de capacidad”:
pools de nodos separados por familia de instancia y selección de artefacto distinta. La lección fue aburrida pero cara: si no conoces la CPU baseline de tu
flota, no puedes ejecutar binarios “optimized” por defecto.
Historia 3: La práctica aburrida pero correcta que salvó el día
Un tercer equipo tenía una política: cada artefacto de servicio debía declarar una baseline de CPU en sus metadatos de release, y cada clúster debía publicar un hecho de “ISA mínima”
que el scheduler hiciera cumplir. No era glamuroso. La gente refunfuñaba. Parecía papeleo.
Entonces Ubuntu 24.04 entró en el entorno. Una versión nueva del toolchain empujó builds orientadas al rendimiento a emitir instrucciones vectoriales distintas en caminos calientes.
Algunos servicios se recompilaron. Uno de ellos se habría caído en nodos viejos si se hubiera desplegado masivamente.
No se cayó. El sistema de despliegue se negó a programarlo en nodos incompatibles porque los metadatos del artefacto decían “requires x86-64-v3”.
El rollout fue solo a los nodos que cumplían el requisito. Los usuarios no lo notaron. El on-call no recibió paginaciones. El equipo siguió durmiendo, que es el
resultado correcto para casi todo trabajo de ingeniería.
El postmortem fue una no-historia: un ticket para ampliar el pool v3, una nota para mantener un fallback v2 para pools legacy, y un agradecimiento silencioso
por guardrails que no negocian.
Chiste #2: Lo único más rápido que una build AVX2 es una build AVX2 que se estrella instantáneamente en una CPU solo AVX.
Contenedores y VMs: la CPU que crees tener vs la que obtienes
La historia de características de CPU es diferente según estés en metal desnudo, contenedores o máquinas virtuales.
Los outages de producción ocurren cuando los equipos aplican el modelo mental equivocado.
Contenedores: misma CPU, diferentes supuestos
Los contenedores no emulan la CPU. Si tu contenedor ejecuta vpbroadcastd en una CPU sin AVX2, morirá igual que un proceso en el host.
El desajuste suele venir del entorno de build:
imágenes construidas en runners más nuevos, o builds multi-stage que compilan con optimizaciones “native” porque nadie fijó los CFLAGS.
El enfoque limpio: construir artefactos portables y, opcionalmente, enviar variantes “aceleradas” adicionales y elegir en tiempo de ejecución o mediante programación por nodo.
No envíes una sola imagen “optimizada” y reces porque el clúster sea homogéneo. Nunca lo es por mucho tiempo.
VMs: modelos de CPU, migración y valores por defecto conservadores
Los hipervisores a menudo exponen un modelo de CPU virtual. Esto no es solo marketing; determina qué extensiones del set de instrucciones están disponibles en el guest.
Algunos entornos intencionalmente ocultan características para permitir live migration entre un rango más amplio de hosts.
Si compilas dentro de una VM que expone AVX2 y luego despliegas en una VM que no lo hace, obtendrás SIGILL. Si compilas en metal con AVX2 y despliegas en una VM
con un modelo de CPU conservador, obtendrás SIGILL. Si compilas dentro del mismo tipo de VM pero la configuración del hipervisor difiere, obtendrás SIGILL.
La solución es gobernanza: define modelos de CPU por entorno, documéntalos como contratos de compatibilidad y haz que tu pipeline de build los tenga como objetivo.
“Lo que la nube nos dé” no es un contrato de CPU. Es un generador de sorpresas.
glibc hwcaps y por qué Ubuntu 24.04 puede elegir código más rápido “de repente”
glibc ha soportado múltiples implementaciones optimizadas de funciones durante mucho tiempo mediante mecanismos como IFUNC resolvers.
Más recientemente, las distribuciones han adoptado los directorios hwcaps:
rutas de filesystem que contienen builds de librerías optimizadas para niveles baseline específicos de x86-64.
El cargador dinámico busca estos directorios primero (como viste en la salida de LD_DEBUG=libs).
En una CPU que cumple los criterios, glibc puede cargar una variante v2/v3 de una librería, habilitando implementaciones más rápidas.
En una CPU que no los cumple, debería retroceder a la variante genérica.
Cuando esto falla operativamente, a menudo es por uno de estos patrones:
- Una imagen chroot/contenedor contiene variantes hwcaps que no coinciden con la CPU real donde se ejecuta (por ejemplo, copiadas de otro rootfs).
- Una variable de entorno o una configuración del loader causa rutas de búsqueda inesperadas.
- Una librería de terceros empaqueta código optimizado y hace su propia detección de forma deficiente.
La conclusión práctica: si SIGILL aparece tras una actualización de la imagen base, no asumas que solo es “tu binario”.
Puede ser “la variante de librería que ahora cargas”. Pruébalo con análisis de core y salida de debug del loader.
Errores comunes: síntoma → causa raíz → arreglo
1) Se cae inmediatamente al arrancar tras una actualización
Síntoma: el servicio arranca y luego muere instantáneamente con Illegal instruction (core dumped).
Causa raíz: un binario recién compilado asume AVX2/SSE4.2/FMA debido a flags del host de compilación o decisiones del nuevo toolchain.
Arreglo: recompilar con una baseline explícita (-march=x86-64-v2 o target conservador), y hacer cumplir esa baseline en CI.
2) Solo algunos nodos se caen; reubicar “lo arregla”
Síntoma: la misma imagen de contenedor funciona en algunos nodos y se cae en otros.
Causa raíz: flota heterogénea en características de CPU; la imagen se compiló para la mitad “mejor”.
Arreglo: etiquetar nodos por capacidad; restringir programación; enviar múltiples variantes de imagen o una baseline portable con dispatch en runtime.
3) Funciona en bare metal, falla en VM (o viceversa)
Síntoma: el binario corre en una workstation pero hace SIGILL en un guest VM.
Causa raíz: el modelo de CPU de la VM oculta características (AVX2 deshabilitado, baseline conservadora para migración).
Arreglo: alinear el modelo de CPU de la VM con los requisitos, o compilar para las flags expuestas por el guest; no compilar dentro de un entorno con flags diferentes a producción.
4) Solo una ruta de código falla bajo carga
Síntoma: el servicio corre un tiempo y luego SIGILL durante operaciones específicas.
Causa raíz: dispatch en tiempo de ejecución/JIT o un plugin selecciona un camino AVX2 condicionalmente; o se invoca una función poco usada.
Arreglo: captura core en el crash; identifica la librería/función; desactiva la ruta optimizada o arregla la lógica de dispatch para comprobar correctamente las flags.
5) “Pero la CPU del host soporta AVX2” y aun así se cae
Síntoma: alguien señala la hoja de especificaciones y insiste en que el servidor soporta la instrucción.
Causa raíz: microcódigo/configuración BIOS, enmascaramiento en VM, o el contenedor corriendo en un nodo distinto al asumido.
Arreglo: confía en lscpu y en el core dump, no en la hoja de especificaciones; valida las flags en el entorno de ejecución real.
6) Un binario de terceros de internet falla en hardware antiguo
Síntoma: herramienta del vendor funciona en staging, falla en un entorno “legacy”.
Causa raíz: el vendor compiló para una baseline más nueva (x86-64-v3) sin ofrecer una build portable.
Arreglo: exige una build compatible con la baseline; si no está disponible, aísla en nodos compatibles o reemplaza el componente.
Listas de verificación / plan paso a paso
Checklist A: Cuando te pagen por SIGILL (flujo de operador)
-
Confirma SIGILL: revisa
journalctly estado systemd. Si no esstatus=4/ILL, detente y replantea el alcance. -
Captura la ubicación del crash: busca
invalid opcode ip:endmesg. - Asegura que existan core dumps: si faltan, habilítalos temporalmente y reproduce de forma controlada.
-
Abre el core: usa
coredumpctl gdb, desensambla en RIP, identifica la instrucción. - Compárala con flags de CPU: ¿la CPU tiene la extensión requerida por esa instrucción?
- Identifica si es tu binario o una librería compartida: revisa frames del backtrace y objetos cargados.
- Decide mitigación: revertir a una build portable, fijar a nodos compatibles o cambiar modelo de CPU de la VM.
- Anota el contrato baseline y hazlo cumplir en CI/CD para no repetir esto la próxima semana.
Checklist B: Política limpia de build y release (flujo de equipo)
- Declara una ISA baseline para la flota (por entorno). Ejemplo: “prod-x86 debe soportar x86-64-v2; algunos pools soportan v3.”
-
Construye artefactos con targets explícitos; prohíbe
-march=nativeen builds de release. - Si necesitas velocidad, envía múltiples builds: baseline + acelerado (v3/v4). Haz la selección explícita (labels del scheduler o dispatch en runtime).
- Añade un self-check de arranque: loguea flags de CPU detectadas y rehúsa iniciar si los requisitos no se cumplen (falla ruidosamente, no aleatoriamente).
- Mantén un host de pruebas de compatibilidad (o modelo de CPU VM) que imite la CPU más antigua soportada. Ejecuta smoke tests allí.
- Versiona y fija toolchains en CI para reducir la “deriva silenciosa de recompilaciones”.
Checklist C: Despliegue consciente de capacidad en Kubernetes (práctico)
- Etiqueta nodos según flags de CPU (presencia de AVX2 o no).
- Usa node selectors/affinity para cargas aceleradas.
- Mantén la imagen baseline como defecto; usa despliegues separados para imagen acelerada.
- Monitorea crash loops y correlaciónalos con etiquetas de nodo para detectar deriva temprano.
Preguntas frecuentes
1) ¿“Illegal instruction” es siempre un desajuste de flags de CPU?
No, pero en producción es la causa dominante. Existen otras causas: binarios corruptos en disco, RAM defectuosa, JIT con codegen roto o ejecutar datos como código.
Tu primera tarea es probar la instrucción que falló mediante un core dump.
2) ¿Por qué empezó tras mover a Ubuntu 24.04?
Porque las actualizaciones cambian toolchains y el comportamiento de selección de librerías. Aunque tu código fuente no cambió, los artefactos de build sí pueden cambiar.
Además, glibc hwcaps y caminos optimizados pueden seleccionarse de forma distinta tras una actualización de distro.
3) Si compilo con -O2, ¿el compilador puede emitir AVX2 de todos modos?
No, a menos que tu target lo permita. El target por defecto depende de la configuración del compilador y de los flags.
La trampa real es cuando sistemas de build inyectan -march=native o cuando compilas en una CPU con más capacidades y luego distribuyes el binario a otra menos capaz.
4) ¿Cuál es la opción más limpia de “un artefacto para todos”?
Compilar para una baseline conservadora (a menudo x86-64-v2 en flotas modernas, a veces incluso más antigua según tu hardware) y usar dispatch en tiempo de ejecución en código caliente.
Pierdes algo de rendimiento pico pero ganas predictibilidad y simplicidad operativa.
5) ¿Cómo sé si un binario requiere AVX2 sin hacerlo fallar?
Puedes desensamblarlo y buscar mnemónicos asociados a AVX2, pero el método más fiable es: ejecutarlo en condiciones controladas,
capturar un core en SIGILL e inspeccionar la instrucción que falló en RIP.
6) ¿Por qué solo falla bajo ciertas peticiones?
Algunas librerías usan dispatch en tiempo de ejecución: verifican flags de CPU y eligen un camino rápido. Si esa detección es errónea, o si el camino rápido está en un plugin
usado solo para cargas específicas, ves crashes “aleatorios” ligados a ciertos inputs.
7) ¿La virtualización puede causar esto aunque la CPU del host soporte la instrucción?
Sí. El guest ve el modelo de CPU virtual, no el host. Los hipervisores pueden deshabilitar features (intencionalmente, por compatibilidad o migración).
Siempre revisa flags de CPU dentro del guest.
8) ¿Y en servidores ARM con “Illegal instruction”?
Misma señal, extensiones diferentes. En ARM verás desajustes como compilar para características ARMv8.x más nuevas y ejecutar en núcleos más antiguos,
o usar extensiones crypto ausentes. El flujo es el mismo: prueba el opcode, compáralo con las características de la CPU y alinea targets de compilación.
9) ¿Deberíamos simplemente estandarizar la flota en AVX2 y listo?
Si puedes, sí—la homogeneidad reduce modos de fallo. Pero la estandarización es un programa, no un deseo. Hasta que esté completa, asume heterogeneidad y despliega en consecuencia.
10) ¿Cuál es la mejor mitigación a largo plazo?
Trata los requisitos ISA como restricciones de despliegue. Decláralos, pruébalos, hazlos cumplir. La “mejor” solución es la que previene la clase de incidentes,
no la que solo gana la pelea actual.
Próximos pasos que puedes ejecutar esta semana
- Elige una ISA baseline para cada entorno (prod/staging/dev) y consígnala en el contrato de plataforma.
-
Audita builds de release en busca de
-march=nativey otros flags dependientes del host. Elimínalos de los artefactos de producción. - Añade un job de CI “mínimo común denominador” que ejecute tests en un modelo de CPU VM que coincida con tus nodos más antiguos soportados.
- Para flotas heterogéneas, etiqueta nodos por capacidad y hace cumplir la programación. Deja de permitir que el scheduler “descubra” la compatibilidad de la forma difícil.
- Habilita core dumps de forma controlada para servicios donde SIGILL sería catastrófico; ten preparada la perilla para respuesta a incidentes.
- Si debes enviar builds acelerados, hazlo deliberadamente: baseline + v3/v4, lógica de selección clara y rutas de rollback definidas.
La historia del despliegue limpio no es “nunca usamos características avanzadas de CPU”. Es “las usamos a propósito”. Ubuntu 24.04 no te traicionó.
Tu pipeline de build hizo lo que implícitamente le pediste. Ahora haz la petición explícita.