Fallo de segmentación en producción: por qué un solo fallo puede arruinar un trimestre

¿Te fue útil?

Un fallo de segmentación parece pequeño. Un proceso muere. El supervisor lo reinicia. Alguien se encoge de hombros y dice “rayo cósmico”,
y vuelve a enviar funciones. Y así es como un trimestre termina por un único crash: no por el fallo en sí,
sino por la reacción en cadena para la que no diseñaste tu sistema.

En producción, un segfault rara vez es “un bug en un binario”. Es un evento de sistemas. Pone a prueba tu higiene de despliegue,
tu modelo de durabilidad de datos, tu gestión de carga, tu observabilidad y la honestidad organizacional. Si lo tratas como
una molestia local de desarrollador, lo volverás a descubrir—durante la reunión de la junta.

Qué significa realmente un segfault (y qué no)

Un segfault es el sistema operativo haciendo cumplir la protección de memoria. Tu proceso intentó acceder a memoria que no debía:
una dirección inválida, una página sin permisos, o una dirección que antes era válida hasta que la liberaste y seguiste usándola.
El kernel envía una señal (usualmente SIGSEGV), y a menos que la manejes (casi nunca deberías), el proceso muere.

“Segfault” es una etiqueta de síntoma. La causa subyacente podría ser:

  • Use-after-free y otros bugs de vida útil (los clásicos).
  • Desbordamiento de buffer (escribir más allá de los límites, corromper metadatos, estallar después en otra parte no relacionada).
  • Desreferencia de puntero nulo (barata, embarazosa, aún ocurre en 2026).
  • Desbordamiento de pila (recursión o marcos grandes de pila, a menudo desencadenado por entradas inesperadas).
  • Incompatibilidad ABI (un plugin carga contra una versión incorrecta de la librería; el código “corre” hasta que deja de hacerlo).
  • Problemas de hardware (raro, pero no mítico: RAM defectuosa y CPUs inestables existen).
  • Violaciones del contrato kernel/usuariospace (p. ej., filtros seccomp, mapeos de memoria inusuales).

Esto es lo que generalmente no significa: “el SO nos mató al azar.” Linux no quiere atacar tu servicio.
Si un proceso hace segfault, algo escribió en memoria que no debía, o ejecutó código que no debía, o volvió a una dirección que no debía.
El kernel es simplemente el portero.

Una verdad operativa seca: si tu respuesta a incidentes empieza con “probablemente es solo un crash”, ya vas tarde.
Los crashes son cómo se manifiesta el comportamiento indefinido.

Broma #1: Un segfault es la forma en que tu programa dice “me gustaría hablar con el gerente”, excepto que el gerente es el kernel y no negocia.

Crash ≠ outage, a menos que lo hayas diseñado así

Un único crash puede ser un no-evento si tu sistema está diseñado para ello:
degradación gradual, reintentos con jitter, operaciones idempotentes, colas acotadas y estado duradero.
Pero si tu crash provoca la caída de un shard, corrompe una caché en memoria de la que tu base de datos depende de repente,
bloquea una elección de líder, o desencadena una tormenta de reinicios, has convertido “un puntero malo” en un evento de ingresos.

Conceptos clave que deberías tener en mente durante el triage

  • Sitio del crash vs. sitio del bug: la instrucción que falló a menudo está lejos del write que corrompió la memoria.
  • Determinismo: si falla en la misma instrucción con la misma solicitud, es probablemente un bug directo; si no, sospecha corrupción de memoria, races o hardware.
  • Radio de impacto: el bug técnico puede ser diminuto; el bug operacional (acoplamiento) es lo que duele.
  • Tiempo hasta la primera señal: tu primer trabajo no es “causa raíz”. Es “detener la hemorragia con la evidencia intacta”.

Idea parafraseada de John Gall (pensador de sistemas): los sistemas complejos que funcionan a menudo evolucionan de sistemas más simples que funcionaban. Si no puedes sobrevivir un crash, tu sistema aún no ha terminado de evolucionar.

Por qué un crash puede arruinar un trimestre

La parte del “terminar el trimestre” no es melodramática. Es cómo los sistemas modernos y los negocios modernos amplifican pequeños eventos técnicos:
acoplamientos fuertes, dependencias compartidas, SLOs agresivos y la costumbre empresarial de apilar lanzamientos.

La cascada estándar

Una secuencia típica se ve así:

  1. Un proceso hace segfault bajo una determinada forma de solicitud o patrón de carga.
  2. El supervisor lo reinicia rápidamente (systemd, Kubernetes, tu propio watchdog).
  3. Se pierde estado (solicitudes en vuelo, almacenamiento de sesión en memoria, tokens de autenticación en caché, offsets de colas, leases de líder).
  4. Los clientes reintentan (a veces correctamente, a menudo en una estampida sincronizada).
  5. La latencia se dispara porque el proceso reiniciado calienta caches, reconstruye pools de conexión, reproduce logs.
  6. Los sistemas aguas abajo se ven golpeados (bases de datos, almacenamiento de objetos, APIs dependientes).
  7. El autoscaling responde tarde (o no) porque las métricas van con retraso y las políticas de escalado son conservadoras.
  8. Ocurren más crashes debido a presión de memoria, timeouts y acumulación en las colas.
  9. Los operadores reinician en pánico cosas y borran la evidencia (core dumps y logs desaparecen).
  10. Finanzas se da cuenta porque cambia el comportamiento visible al cliente: pagos fallidos, subastas de anuncios perdidas, liquidaciones retrasadas.

Cómo el almacenamiento lo empeora (sí, incluso si “es un crash”)

Como ingeniero de almacenamiento, diré lo que muchos callan: el almacenamiento convierte crashes en problemas monetarios porque es donde el estado “temporal”
se vuelve duradero. Un segfault puede:

  • Corromper estado local si tu proceso escribe de forma no atómica y no hace fsync adecuadamente.
  • Corromper estado remoto vía escrituras parciales, mal uso del protocolo o bugs en replay.
  • Desencadenar replays (consumidores de Kafka, replay de WAL, reintentos de listado de objetos) que multiplican carga y coste.
  • Provocar pérdida silenciosa de datos cuando el proceso muere entre el acknowledgment y el commit.

Muchos equipos “manejan” crashes añadiendo reintentos. Los reintentos no son fiabilidad; son multiplicadores de carga.
Si tu política de reintentos no está acotada, con jitter y basada en idempotencia, estás fabricando un generador de outages.

Capa de negocio: por qué esto sale en las llamadas de resultados

El negocio no se preocupa por tu stack trace. Le importa:

  • Tasa de conversión que cae cuando la latencia sube y los timeouts aumentan.
  • Penalizaciones de SLA/SLO (penalizaciones explícitas o churn).
  • Costes de soporte y daño de marca cuando los clientes experimentan comportamiento inconsistente.
  • Coste de oportunidad: congelas despliegues y retrasas lanzamientos para estabilizar.
  • Distracción de ingeniería: la gente más valiosa se ve absorbida por la respuesta a incidentes durante días.

Un segfault puede ser “una línea de código”. Pero en producción es una prueba de los amortiguadores de choque de tu sistema.
La mayoría de los sistemas fallan esa prueba porque los amortiguadores nunca se instalaron—solo se prometieron.

Hechos e historia: por qué nos seguimos haciendo esto

  • La protección de memoria es más antigua que tu empresa. La protección de páginas impuesta por hardware y las fallas de página fueron habituales mucho antes de Linux; los segfaults son la “característica funcionando”.
  • Unix temprano popularizó el archivo “core”. Volcar la imagen del proceso al fallar era una herramienta pragmática de depuración cuando depurar interactivamente era más difícil.
  • SIGSEGV no es lo mismo que SIGBUS. Segfault es acceso inválido; bus error a menudo indica fallos de alineación o problemas con archivos mapeados/dispositivos.
  • ASLR cambió el juego. Address Space Layout Randomization dificultó la explotabilidad y complicó ligeramente la depuración; los backtraces simbolizados importan más.
  • Los asignadores de heap evolucionaron porque los crashes eran caros. Los allocators modernos (ptmalloc, jemalloc, tcmalloc) intercambian velocidad, fragmentación y características de depuración de maneras distintas.
  • Los símbolos de depuración se convirtieron en una preocupación de producción. El auge del deployment continuo convirtió el “lo reproduciremos localmente” en una fantasía; necesitas símbolos y build IDs para depurar lo que realmente corrió.
  • La contenedorización complicó los core dumps. Namespaces y aislamiento de sistema de archivos significan que los cores pueden desaparecer a menos que deliberadamente los redirijas a algún sitio.
  • “Fail fast” fue malinterpretado. Fallar rápido es bueno cuando evita corrupción; es malo cuando desencadena reintentos coordinados y pérdida de estado sin salvaguardas.
  • Los kernels modernos son parlanchines de formas útiles. dmesg puede incluir la dirección fallida, el instruction pointer e incluso offsets de librería—si preservaste los logs.

Los segfaults no se hicieron más comunes. Simplemente construimos torres de dependencias más altas alrededor de ellos.

Manual de diagnóstico rápido (primero/segundo/tercero)

Este es el playbook de 15–30 minutos para encontrar el cuello de botella y elegir la estrategia de contención adecuada.
No para resolver el bug para siempre—aún.

Primero: detener la tormenta de reinicios y preservar evidencia

  • Estabilizar: escala hacia fuera, deshabilita temporalmente reintentos agresivos y añade límites de tasa.
  • Preservar: asegura que los core dumps y logs sobrevivan a los reinicios (o al menos preserva el último crash).
  • Verificar radio de impacto: ¿es un host, una AZ, una versión, una carga de trabajo?

Segundo: identificar la firma del crash

  • ¿Dónde falló? nombre de función, módulo, offset, dirección que falló.
  • ¿Cuándo empezó? correlaciona con deploy, cambio de config, patrón de tráfico.
  • ¿Es determinista? mismas solicitudes disparan, mismo stack trace, mismo host?

Tercero: elige la rama basada en la señal más fuerte

  • Misma versión binaria solamente: haz rollback o desactiva el flag de función; procede con análisis de core.
  • Sólo ciertos hosts: sospecha hardware, kernel, libc o deriva de configuración; drena y compara.
  • Sólo alta carga: sospecha race, presión de memoria, timeouts que conducen a caminos de limpieza inseguros.
  • Después de problemas con dependencias: sospecha bug en manejo de errores (null deref en respuesta fallida, etc.).

La trampa: lanzarse a GDB inmediatamente mientras el sistema aún está oscilando. Arregla el modo de fallo operacional primero.
Necesitas un paciente estable antes de operar.

Tareas prácticas: comandos, salidas, decisiones (12+)

Estas son tareas reales que espero que los ingenieros on-call ejecuten durante una investigación de crash. Cada una tiene: un comando, salida realista,
qué significa y qué decisión tomar a continuación.

Task 1: Confirmar el crash en el journal y obtener la señal

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.

Significado de la salida: el estado 11/SEGV confirma SIGSEGV. code=dumped sugiere que existe un core dump (o systemd cree que existe).

Decisión: ralentizar el bucle de reinicios (RestartSec), y buscar el core dump antes de que sea rotado.

Task 2: Revisar mensajes del kernel para la dirección que falló y el 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

Significado de la salida: fallo en 0x38 es un patrón clásico de puntero nulo + offset (desreferenciar un campo de struct desde una base nula). El ip apunta dentro del segmento de texto de tu binario.

Decisión: priorizar hipótesis de “puntero malo en camino de error”; extraer el core y los símbolos para confirmar qué campo es.

Task 3: Localizar el core vía 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

Significado de la salida: los archivos core están presentes para esos PIDs. Bien—la evidencia existe.

Decisión: extraer inmediatamente el core más nuevo a una ubicación segura (los cores se vacuumean).

Task 4: Extraer el core al disco para que sobreviva la rotación

cr0x@server:~$ sudo coredumpctl dump 24891 --output=/var/tmp/checkout-api.core
/var/tmp/checkout-api.core

Significado de la salida: core escrito en una ruta estable.

Decisión: snapshot del ejecutable exacto y sus librerías a continuación, con build IDs coincidentes.

Task 5: Verificar el build ID del binario (para que los símbolos coincidan)

cr0x@server:~$ readelf -n /opt/checkout/bin/checkout-api | grep -A2 "Build ID"
    Build ID: 9b7c6fd3b2a9cdb5d3d1c9e0a4f2f7aa12c0f2ab

Significado de la salida: este build ID debe coincidir con tu paquete/artifact de símbolos.

Decisión: obtener símbolos para ese build ID; si no puedes, aún harás depuración basada en offsets pero será más lenta y arriesgada.

Task 6: Backtrace rápido desde el core (mejor esfuerzo)

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 ?? ()

Significado de la salida: sin símbolos, así que los frames son desconocidos. Aun así, tienes la dirección de crash 0x55c1b2f9a4d2.

Decisión: traduce la dirección de crash a una función usando addr2line cuando haya símbolos disponibles; mientras tanto, usa el mapa del binario para calcular offsets.

Task 7: Confirmar los mapeos de memoria para calcular el 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

Significado de la salida: el binario está mapeado empezando en 0x55c1b2f82000. Tu IP de crash está dentro de ese rango.

Decisión: calcula el offset: 0x55c1b2f9a4d2 - 0x55c1b2f82000. Usa ese offset con herramientas de símbolos.

Task 8: Comprobar si los core dumps están siendo truncados por límites

cr0x@server:~$ ulimit -c
0

Significado de la salida: el límite de tamaño de core es cero para el shell actual; dependiendo del gestor de servicios, la unidad puede sobrescribirlo, pero a menudo esto significa “sin cores”.

Decisión: asegurar que la unidad systemd tenga LimitCORE=infinity o configurar coredump.conf adecuadamente; de lo contrario depurarás a ciegas.

Task 9: Verificar la política de almacenamiento de core de systemd

cr0x@server:~$ sudo grep -E '^(Storage|ProcessSizeMax|ExternalSizeMax|MaxUse|KeepFree)=' /etc/systemd/coredump.conf
Storage=external
ProcessSizeMax=2G
ExternalSizeMax=2G
MaxUse=8G
KeepFree=2G

Significado de la salida: los cores se almacenan externamente con topes. Si tu proceso tiene >2G RSS al crash, el core podría estar cortado o ausente.

Decisión: si faltan cores o están truncados, eleva temporalmente el tope en los nodos afectados, o reproduce en condiciones controladas con menor huella de memoria.

Task 10: Descartar OOM-kill (fallo diferente, mismo síntoma “murió”)

cr0x@server:~$ sudo journalctl -k -S "1 hour ago" | grep -i -E "oom|killed process" | tail -n 5

Significado de la salida: salida vacía sugiere que no hubo OOM kill en la última hora.

Decisión: sigue enfocándote en segfault; si ves OOM kills, trátalo como presión de memoria primero (y el segfault podría ser corrupción secundaria bajo estrés).

Task 11: Comprobar tormentas de reinicio y limitarlas

cr0x@server:~$ systemctl show checkout-api.service -p Restart -p RestartUSec -p NRestarts
Restart=always
RestartUSec=200ms
NRestarts=37

Significado de la salida: 200ms de retardo de reinicio y 37 reinicios es una prueba de carga autoinfligida. Amplificará reintentos aguas abajo y puede asfixiar el host.

Decisión: aumenta el retardo de reinicio (segundos a minutos) y considera StartLimitIntervalSec/StartLimitBurst para evitar que el flapping deje fuera de servicio el nodo.

Task 12: Ver si el crash se correlaciona con un deploy (no supongas)

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.

Significado de la salida: tienes una versión y un estado de feature flag capturados en logs. Eso es oro.

Decisión: si el crash empezó después de activar una flag, desactívala primero. Si empezó tras un bump de versión, haz rollback mientras analizas.

Task 13: Validar la resolución de librerías compartidas (captura de ABI)

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)

Significado de la salida: confirma qué libs se cargan. Si esperabas versiones diferentes (p. ej., en /opt), tienes un problema de empaquetado o entorno de ejecución.

Decisión: si hay deriva de dependencias, fija versiones y redepliega; depurar un crash sobre un ABI no intencionado es pérdida de tiempo.

Task 14: Revisar anomalías a nivel host (disco lleno puede arruinar coredumps y 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

Significado de la salida: estás casi sin espacio donde se escriben los cores. Eso significa “sin evidencia” pronto, y posiblemente problemas de servicio si los logs no pueden escribir.

Decisión: libera espacio inmediatamente (vacía journal, rota cores), o redirige el almacenamiento de cores a un filesystem más grande durante el incidente.

Task 15: Confirmar si el crash está ligado a una solicitud específica (logs de aplicación + muestreo)

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

Significado de la salida: un request ID y path justo antes del crash es una pista fuerte. No es prueba, pero sí una dirección.

Decisión: extrae una muestra de solicitudes recientes para ese endpoint, inspecciona las formas de payload y considera limitar temporalmente la tasa o feature-gatear ese camino de código.

Tres mini-historias corporativas desde las trincheras de crashes

Mini-historia 1: La suposición equivocada (el puntero nulo que “no podía pasar”)

Un servicio adyacente a pagos empezaba a hacer segfault una vez al día. Luego una vez por hora. El equipo on-call lo trató como un bug ruidoso pero manejable:
el proceso se reiniciaba rápido y la mayoría de los clientes no lo notaban—hasta que lo notaron. La latencia se fue acumulando, las tasas de error subieron en horas pico,
y el canal de incidentes se volvió un estilo de vida.

La primera investigación apuntó a infraestructura: upgrades de kernel, firmware del host, un parche de librería reciente. Suposiciones razonables, objetivo equivocado.
La única pista consistente era la dirección que fallaba: siempre un pequeño offset desde cero. Desreferencia nula clásica.
Y sin embargo la base de código “obviamente” verificaba null. “Obviamente” es una palabra costosa.

El bug fue una suposición equivocada sobre una dependencia upstream: “este campo siempre existe”. Existía el 99.98% del tiempo.
Luego un partner desplegó un cambio que lo omitía para un tier de producto nicho. El código parseó JSON en una struct, dejó un puntero sin establecer,
y más tarde lo usó en un camino de limpieza que nadie había probado bajo carga porque, de nuevo, “no podía pasar”.

La solución fue un guard simple de una línea y un mejor modelo de errores. La solución operacional fue más interesante:
añadieron un circuit breaker en la integración con el partner, dejaron de reintentar en inputs malformados e introdujeron un canario que simulaba el caso del “campo faltante”.
El segfault dejó de ser un riesgo trimestral porque el sistema dejó de asumir que el mundo era educado.

Mini-historia 2: La optimización que salió mal (un allocator más rápido, una compañía más lenta)

Una canal de ingestión de alto rendimiento cambió de allocator para reducir CPU y mejorar latencias de cola. Funcionó en benchmarks.
En producción, las tasas de crash aumentaron lentamente, luego de golpe. Segfaults. A veces double free. A veces lectura inválida.
El equipo sospechó de su propio código, y con razón, se ganaron esa sospecha.

El problema no fue “el allocator es malo.” Fue que el cambio alteró el layout de memoria y el timing lo suficiente como para exponer una race existente.
Antes, la race garabateaba memoria que nadie tocaba por un rato. Con el nuevo allocator, la misma región se reutilizaba antes.
El comportamiento indefinido pasó de “latente” a “estruendoso.”

Entonces apareció el multiplicador operacional: su orquestador tenía checks de liveness agresivos y reinicios inmediatos.
Bajo bucles de crash, el lag de ingestión creció. Los consumidores se quedaron atrás. La retropresión no se propagó correctamente.
La canal intentó ponerse al día tirando de más trabajo. Se convirtió en una máquina que se hacía daño a sí misma.

La solución a largo plazo fue corregir la race y añadir cobertura TSAN/ASAN en CI para componentes clave.
La solución a corto plazo que salvó la semana del negocio fue revertir el allocator y poner un tope duro a la concurrencia mientras el lag estaba por encima de un umbral.
Aprendieron la verdad aburrida: los cambios de rendimiento son cambios de corrección disfrazados.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día (símbolos, cores y un rollback calmado)

Un servicio RPC interno hizo segfault tras una actualización rutinaria de dependencia. El ingeniero on-call no empezó a hurgar el código.
Empezó por asegurarse de que el sistema dejara de oscilar y de que el crash fuera depurable.
Se aumentó el retardo de reinicio. Se drenaron nodos afectados. Se preservaron cores.

El equipo había hecho algo aburrido y poco celebrado meses antes: cada build publicaba símbolos de depuración indexados por build ID, y el runtime registraba el build ID al arrancar.
También había un runbook que decía “si un proceso vuelca core, cópialo fuera del nodo antes de hacer algo ingenioso.”
Nadie celebró esta práctica cuando se introdujo. Deberían haberlo hecho.

En una hora tenían un backtrace simbolizado apuntando a una función específica que manejaba una respuesta de error.
Una dependencia ahora devolvía una lista vacía donde antes devolvía null; el código trató “vacío” como “tiene al menos un elemento” e indexó.
Una línea de arreglo, una prueba dirigida y un despliegue canario.

El servicio estuvo estable todo el tiempo porque la vía de rollback era limpia y ensayada.
El crash no se convirtió en historia de trimestre porque la compañía invirtió en evidencia, no en héroes.

Broma #2: Nada acelera la “alineación del equipo” como un bucle de crash a las 3 a.m.; de repente todos se ponen de acuerdo en las prioridades.

Errores comunes: síntoma → causa raíz → solución

Estos son patrones que aparecen una y otra vez. Apréndelos una vez. Ahorra repetirlos.

1) Síntoma: segfault en dirección 0x0 o pequeño offset (0x10, 0x38, 0x40)

Causa raíz: desreferencia de puntero nulo, a menudo en un camino de manejo de errores o limpieza.

Solución: añade comprobaciones explícitas de null; pero también arregla invariantes—¿por qué se permite que ese puntero sea null? Añade pruebas para inputs faltantes/invalidos.

2) Síntoma: sitio del crash cambia cada vez; stack traces parecen aleatorios

Causa raíz: corrupción de memoria (desbordamiento, use-after-free), a menudo anterior al crash.

Solución: reproduce con ASAN/UBSAN; activa hardening del allocator en staging; reduce concurrencia para acotar ventanas de timing; audita código inseguro y límites FFI.

3) Síntoma: segfault aparece sólo bajo carga, desaparece cuando añades logs

Causa raíz: condición de carrera; los cambios de timing la ocultan. Que el logging “arregle” un crash es un signo clásico de concurrencia.

Solución: corre con TSAN en CI; añade disciplina de locks; usa estructuras thread-safe; aplica reglas de propiedad (especialmente para callbacks).

4) Síntoma: sólo una AZ/pool de hosts ve el crash

Causa raíz: deriva de configuración, diferentes versiones de librería, características CPU, diferencias de kernel o hardware defectuoso.

Solución: compara paquetes y versiones de kernel; mueve la carga; ejecuta tests de memoria si persiste la sospecha; reconstruye imágenes golden y elimina drift.

5) Síntoma: crash coincide con una optimización “exitosa” de rendimiento

Causa raíz: la optimización cambió layout de memoria o timing, exponiendo UB; o eliminó comprobaciones de límites.

Solución: revertir primero; luego reintroducir con guardrails y sanitizadores; trata el trabajo de perf como tan riesgoso como una migración de esquema.

6) Síntoma: no hay core dumps en ninguna parte, a pesar de mensajes “core-dump”

Causa raíz: límites de core en cero, cores demasiado grandes y recortados, disco lleno, runtime de contenedores que no permite cores, o systemd-coredump configurado para descartarlos.

Solución: establece LimitCORE=infinity; eleva topes de tamaño de coredump; asegura espacio de almacenamiento; prueba volcar cores intencionalmente en staging.

7) Síntoma: “segfault” pero el log del kernel muestra fault de protección general o instrucción ilegal

Causa raíz: ejecución de código corrompido, salto a través de un puntero de función malo, o ejecución en características CPU incompatibles (raro, pero real con flags de compilación agresivos).

Solución: verifica objetivos de build, flags de CPU y ABI de librerías; revisa corrupción de memoria; confirma que no estás desplegando binarios compilados para otra microarquitectura.

8) Síntoma: los crashes cesan cuando desactivas un endpoint o feature flag

Causa raíz: forma de input específica que desencadena comportamiento indefinido; a menudo parsing, límites o manejo de campos opcionales.

Solución: mantiene el flag apagado hasta arreglar; añade validación de entradas; añade fuzzing para ese parser y tests de contrato para dependencias upstream.

Listas de verificación / plan paso a paso

Checklist de contención (primera hora)

  1. Reducir radio de impacto: drena nodos afectados, reduce tráfico o enruta lejos de la versión.
  2. Detener tormentas de reinicio: aumenta delays de reinicio; capea el flapping; evita azotar dependencias.
  3. Preservar evidencia: copia cores fuera del nodo; snapshot de logs; registra build IDs y config/flags.
  4. Obtener una firma del crash: línea del kernel (dirección de fallo, IP), un backtrace (aunque no esté simbolizado), frecuencia y disparadores.
  5. Elegir el camino más seguro: rollback gana a “depuración en vivo” cuando los clientes están quemándose.

Checklist de diagnóstico (mismo día)

  1. Simbolizar el crash: coteja build IDs, obtén símbolos, produce un backtrace legible.
  2. Clasificar: desreferencia nula vs. corrupción de memoria vs. race vs. entorno/hardware.
  3. Reproducir: captura la forma de solicitud que dispara; construye un repro mínimo; corre con sanitizadores.
  4. Confirmar alcance: versiones afectadas, pools de hosts afectados, inputs afectados.
  5. Aplicar parche con seguridad: añade pruebas; canary; rollout gradual; valida tiempo libre de crashes antes del deploy completo.

Checklist de prevención (higiene trimestral que paga cuentas)

  1. Siempre publicar build IDs y símbolos. Haz que obtener símbolos sea aburrido y automático.
  2. Mantén feature flags para rutas de código riesgosas. No todo necesita flag; el código propenso a crashes sí.
  3. Define idempotencia y reintentos. “Reintentar todo” es cómo te haces un DDoS a ti mismo.
  4. Fuzza parsers y código límite. Las esquinas son donde nacen los segfaults.
  5. Usa sanitizadores en CI para componentes clave. Especialmente para C/C++ y límites FFI.
  6. Planea la muerte de procesos. Stateless donde sea posible; duradero donde se requiera; degradación elegante siempre.

Preguntas frecuentes

1) ¿Un segfault es siempre un bug de software?

Casi siempre, sí. El hardware puede provocarlo (RAM defectuosa), pero trata al hardware como culpable solo después de tener evidencia:
crashes específicos de host, errores de memoria corregidos o fallos repetibles en una máquina.

2) ¿Cuál es la diferencia entre SIGSEGV y SIGBUS?

SIGSEGV es acceso inválido a memoria (permisos o memoria no mapeada). SIGBUS a menudo involucra problemas de alineación o errores en archivos/dispositivos mapeados en memoria.
Ambos significan “tu proceso hizo algo que no debía”, pero la ruta de depuración difiere.

3) ¿Por qué el crash ocurre “en otro lugar” que el bug?

La corrupción de memoria es caos de acción retardada. Sobrescribes metadatos o un objeto vecino, y el programa sigue corriendo hasta que toca el área envenenada.
El sitio del crash es donde el sistema lo detectó, no donde cometiste el crimen.

4) ¿Debo capturar SIGSEGV y seguir corriendo?

No, no para código de aplicación general. Recuperar de forma segura es casi imposible porque el estado del proceso no es confiable.
Usa handlers de SIGSEGV sólo para registrar contexto diagnóstico mínimo y luego salir.

5) ¿Por qué faltan core dumps en contenedores?

A menudo porque los límites de core son cero, el sistema de archivos es read-only o limitado en tamaño, o el runtime del contenedor bloquea volcados core.
Debes configurar intencionalmente el comportamiento de core dump por runtime y asegurar que exista almacenamiento.

6) Si no tengo símbolos de depuración, ¿el core es inútil?

No inútil, solo más lento. Aún puedes usar instruction pointers, offsets de mapeo y build IDs para localizar código.
Pero perderás tiempo y aumentarás el riesgo de atribución errónea. Los símbolos son baratos comparados con el downtime.

7) ¿Puede un segfault corromper datos?

Sí. Si crasheas a mitad de una escritura sin garantías de atomicidad y durabilidad, puedes dejar estado parcial.
Si confirmas trabajo antes de que esté committeado, puedes perder datos. Si reintentas sin idempotencia, puedes duplicar datos.

8) ¿Cuál es la mitigación segura más rápida cuando los crashes se disparan tras un deploy?

Revertir o desactivar el feature flag. Hazlo pronto. Luego analiza con evidencia preservada.
Depurar con héroes mientras los clientes sufren es cómo prolongas incidentes y creas folklore en lugar de arreglos.

9) ¿Cómo sé si es una condición de carrera?

Crashes que desaparecen con logging extra, cambian con el número de CPUs o muestran distintos stack traces bajo carga son indicadores fuertes.
TSAN y pruebas de estrés controladas son tus aliados.

Conclusión: próximos pasos que realmente reducen crashes

Un segfault no es un “bug”. Es una falla de sistemas que revela lo que asumiste sobre memoria, entradas, dependencias y recuperación.
El crash es la parte más pequeña de la historia. El resto es acoplamiento, reintentos, manejo de estado y si preservaste la evidencia.

Haz esto a continuación, en este orden:

  1. Haz que cores y símbolos sean innegociables. Si no puedes depurar lo que corrió, estás funcionando con esperanza.
  2. Endurece el comportamiento de reinicio. Reinicios lentos, capar el flapping y prevenir estampidas de reintentos.
  3. Diseña para la muerte de procesos. Idempotencia, límites de durabilidad y degradación elegante son cómo un crash sigue siendo un solo crash.
  4. Invierte en sanitizadores y fuzzing donde importa. Especialmente en parsers, FFI y puntos críticos de concurrencia.
  5. Escribe el postmortem como si lo fueras a leer otra vez. Porque lo harás—a menos que arregles el amplificador operacional, no solo la línea de código.

La meta no es eliminar todos los segfaults para siempre. La meta es volver aburrido un segfault: contenido, depurable e incapaz de secuestrar un trimestre.

← Anterior
AVX-512: por qué algunos lo adoran y otros lo temen
Siguiente →
MySQL vs PostgreSQL: «timeouts aleatorios» — red, DNS y pooling culpables

Deja un comentario