Debian 13 “Segfault” se bloquea tras la actualización: encuentre la incompatibilidad exacta de bibliotecas (Caso #55)

¿Te fue útil?

Actualizas un host Debian, arranca, los servicios se inician… y luego procesos aleatorios empiezan a morir con el mismo mensaje seco: Segmentation fault.
Sin un error nítido, sin un patrón obvio. Simplemente el kernel dejando núcleos como confeti y tu canal de guardia subiéndose de tono.

Este caso (#55) trata de convertir esa vaga queja de “se bloquea tras la actualización” en una falla específica y nombrable: la incompatibilidad exacta de una biblioteca compartida
(qué archivo, qué paquete, qué símbolo/ABI), más la cadena de decisiones para arreglarlo sin empeorar el sistema.

Qué ocurre realmente cuando “se produce un segfault”

Tras una actualización mayor, “segfault” normalmente no es un misterio de la informática moderna. Es un error de contabilidad.
El proceso cargó un conjunto de objetos compartidos que no concuerdan en un contrato: ABI, versiones de símbolos, convenciones de llamada, disposición de estructuras,
o incluso suposiciones sobre almacenamiento local por hilo. La CPU hace lo que le dicen, no lo que querías.

En Debian, esos contratos normalmente se aplican mediante disciplina de empaquetado: dependencias, versionado de símbolos, shlibs, triggers,
y la regla simple de que no mezclas las bibliotecas de la release estable con blobs aleatorios en /usr/local.
Cuando actualizas y aún aparecen segfaults, a menudo estás en una de estas situaciones:

  • Actualización parcial: tienes piezas del espacio de usuario nuevas usando bibliotecas antiguas (o viceversa).
  • Bibliotecas sombra: /usr/local/lib o un paquete de la aplicación está sobrescribiendo las librerías de la distribución.
  • Rarezas de RPATH/RUNPATH: el binario tiene rutas de búsqueda codificadas que prevalecen sobre valores sensatos.
  • Arquitectura equivocada / mezcla multiarch: el cargador encuentra una biblioteca con el mismo nombre pero clase/arquitectura ELF incompatible.
  • Plugins fuera del árbol: módulos compilados contra encabezados antiguos se cargan en un proceso más nuevo y escriben memoria ajena.
  • Archivos corruptos: menos común, pero una actualización interrumpida o un problema de disco puede producir objetos ELF sutilmente dañados.

Tu trabajo no es “detener el segfault”. Tu trabajo es “identificar el objeto desajustado y probarlo”. Cuando puedes nombrar el archivo y el paquete,
las correcciones se vuelven aburridas: reinstalar, eliminar overrides, alinear versiones, reconstruir plugins o revertir.

Guía rápida de diagnóstico

Si estás en producción, no empiezas con filosofía de depuración. Empiezas por la vía más rápida para reducir el radio de impacto y
obtener una hipótesis concreta.

1) Primero: confirma que es el cargador dinámico y no el kernel o el hardware

  • Revisa journalctl en busca de direcciones de fallo y nombres de bibliotecas consistentes.
  • Confirma si son muchos binarios o un servicio.
  • Escanea en busca de participación obvia de /usr/local o de un cargador personalizado.

2) Segundo: captura un core y genera un backtrace con rutas de bibliotecas

  • Usa coredumpctl o la ruta del fichero core.
  • En gdb, imprime las bibliotecas compartidas cargadas y el backtrace.
  • Identifica la primera ruta de biblioteca “rara”, la versión o la falta de información de símbolo.

3) Tercero: verifica la integridad del paquete y la alineación de versiones

  • apt policy para las bibliotecas sospechosas y el binario que falla.
  • dpkg -V y debsums para corrupción/sobrescrituras.
  • Busca paquetes retenidos, desviaciones (diversions) y prioridades fijadas.

4) Cuarto: traza las decisiones del cargador

  • LD_DEBUG=libs,versions sobre el binario que falla (en un entorno seguro).
  • Confirma qué .so exacto se cargó, desde qué directorio y por qué.

5) Quinto: arregla con el menor radio posible

  • Prefiere eliminar overrides y reinstalar paquetes oficiales.
  • No “crees enlaces simbólicos para salir del paso” a menos que te guste perseguir fantasmas después.

Una cita para mantenerte honesto. Werner Vogels (CTO de Amazon) lo puso más o menos así—idea parafraseada:
“Todo falla todo el tiempo; diseña y opera asumiendo el fallo.” La versión del operador: asume que tu actualización puede dejar un mundo mezclado atrás.

Hechos y contexto interesantes (para que dejes de sorprenderte)

  1. “Segmentation fault” es anterior a tu carrera. El término viene de la segmentación de memoria en diseños de modo protegido tempranos, mucho antes de que Linux fuera una chispa.
  2. Linux reporta “segfault” incluso cuando la causa raíz es una ruptura de contrato ABI. La CPU falla; el SO informa; tu bug real está en un nivel superior.
  3. El versionado de símbolos de glibc es una superpotencia de estabilidad. Las bibliotecas pueden exportar múltiples versiones del mismo símbolo para mantener binarios antiguos funcionando—hasta que burlas el sistema.
  4. El empaquetado de Debian tiene todo un sistema de metadatos para bibliotecas compartidas. El mecanismo shlibs/symbols existe para evitar exactamente esto, pero no puede defenderse contra overrides locales.
  5. RPATH vino primero; RUNPATH vino después. RUNPATH cambia cómo se resuelven dependencias transitivas; puede hacer que fallos “funciona en una caja” sean exasperantes.
  6. /usr/local está históricamente pensado para compilaciones locales del administrador. Es anterior a la cultura de contenedores y sigue siendo un lugar común para esconder bombas de tiempo.
  7. Las actualizaciones mayores exponen la deuda de “comportamiento indefinido”. Un programa que “funcionaba” confiando en UB puede caerse cuando cambian el compilador, la libc o el asignador.
  8. Los problemas de ABI de C++ son un género recurrente. Mezclar versiones de compilador y expectativas de libstdc++ puede producir caídas que parecen corrupción de memoria aleatoria.

Broma #1: Un segfault es simplemente la forma en que tu programa dice que le gustaría ir a acostarse un rato, preferentemente en la memoria de otra persona.

Tareas prácticas: comandos, salidas y decisiones

Estos no son comandos de “prueba y error”. Cada tarea incluye qué buscas y qué decisión tomas a continuación.
Ejecútalas en el host afectado o, mejor, en un clon/snapshot si estás en producción.

Task 1: Confirma la firma del fallo en el journal

cr0x@server:~$ journalctl -b -p warning..alert | grep -E "segfault|SIGSEGV|coredump" | tail -n 30
Dec 30 10:21:14 server kernel: myapp[22198]: segfault at 0 ip 00007f2d2a8f4c90 sp 00007ffd3b2a1c10 error 4 in libssl.so.3[7f2d2a860000+87000]
Dec 30 10:21:14 server systemd-coredump[22213]: Process 22198 (myapp) of user 1001 dumped core.
Dec 30 10:21:18 server kernel: myapp[22302]: segfault at 0 ip 00007f2d2a8f4c90 sp 00007ffd3b2a1c10 error 4 in libssl.so.3[7f2d2a860000+87000]

Qué significa: Si el kernel nombra una biblioteca (in libssl.so.3), ese es tu primer sospechoso, no el último.
La IP de fallo dentro de una biblioteca sugiere ya sea un bug real en esa biblioteca o (más probable tras una actualización) un desajuste en quién la llamó.

Decisión: Elige un PID/core que falle y analiza la ruta de archivo exacta y la versión del paquete de esa biblioteca.

Task 2: Comprueba si los fallos son generalizados o aislados

cr0x@server:~$ coredumpctl list --no-pager | tail -n 10
TIME                            PID   UID   GID SIG COREFILE  EXE
Mon 2025-12-30 10:21:14 UTC   22198  1001  1001  11 present   /usr/local/bin/myapp
Mon 2025-12-30 10:21:18 UTC   22302  1001  1001  11 present   /usr/local/bin/myapp
Mon 2025-12-30 10:22:03 UTC   22511     0     0  11 present   /usr/sbin/nginx

Qué significa: Si tanto binarios de la distro (/usr/sbin/nginx) como binarios locales (/usr/local/bin/myapp) se están bloqueando,
puede que tengas un problema de bibliotecas a nivel sistema. Si solo /usr/local falla, empieza por desconfiar de /usr/local.

Decisión: Si también se caen binarios del sistema, prioriza verificar las bibliotecas del sistema y la integridad de paquetes.
Si solo fallan apps locales, céntrate en rutas del cargador, bibliotecas empaquetadas y suposiciones de build.

Task 3: Identifica la base del SO y la arquitectura

cr0x@server:~$ cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
NAME="Debian GNU/Linux"
VERSION_ID="13"
VERSION="13 (trixie)"
ID=debian
cr0x@server:~$ uname -a
Linux server 6.12.0-1-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.0-1 (2025-11-20) x86_64 GNU/Linux

Qué significa: Kernel/arquitectura importan para símbolos de depuración y para descartar bibliotecas “de arquitectura equivocada”.

Decisión: Si ves arquitecturas mezcladas instaladas (por ejemplo, i386 en amd64), prepárate para verificar rutas multiarch y la selección del cargador.

Task 4: Verifica el origen y enlace del ejecutable que falla

cr0x@server:~$ file /usr/local/bin/myapp
/usr/local/bin/myapp: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b7c7d6..., for GNU/Linux 3.2.0, stripped

Qué significa: “dynamically linked” significa que estás en terreno de bibliotecas compartidas. “stripped” significa que los backtraces serán menos divertidos.

Decisión: Si el binario es local y está strippeado, planea instalar símbolos de depuración para bibliotecas del sistema y, si es posible, obtener una build sin strip.

Task 5: Vista rápida de dependencias con ldd (pero no lo adores)

cr0x@server:~$ ldd /usr/local/bin/myapp | head -n 20
	linux-vdso.so.1 (0x00007ffd7b7d7000)
	libssl.so.3 => /usr/local/lib/libssl.so.3 (0x00007f2d2a860000)
	libcrypto.so.3 => /usr/local/lib/libcrypto.so.3 (0x00007f2d2a3d0000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2d2a1ef000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f2d2a9f0000)

Qué significa: El cargador está escogiendo /usr/local/lib/libssl.so.3 en lugar del OpenSSL empaquetado por Debian en
/usr/lib/x86_64-linux-gnu. Eso es una gran señal de alarma tras una actualización.

Decisión: Trata /usr/local/lib/libssl.so.3 como sospechoso número 1. Confirma su versión y si coincide con el ABI esperado.

Task 6: Pregúntale al cargador dinámico qué está haciendo (LD_DEBUG)

cr0x@server:~$ LD_DEBUG=libs,versions /usr/local/bin/myapp 2>&1 | head -n 40
     22891:	find library=libssl.so.3 [0]; searching
     22891:	 search path=/usr/local/lib:/usr/lib/x86_64-linux-gnu:/lib/x86_64-linux-gnu	(system search path)
     22891:	  trying file=/usr/local/lib/libssl.so.3
     22891:	find library=libcrypto.so.3 [0]; searching
     22891:	  trying file=/usr/local/lib/libcrypto.so.3
     22891:	checking for version `OPENSSL_3.2.0' in file /usr/local/lib/libssl.so.3 [0] required by file /usr/local/bin/myapp [0]
     22891:	checking for version `GLIBC_2.38' in file /lib/x86_64-linux-gnu/libc.so.6 [0] required by file /usr/local/lib/libssl.so.3 [0]

Qué significa: Ahora tienes prueba del orden de resolución y de los requisitos de versión de símbolos.
Si ves que requiere una versión de símbolo no proporcionada por tus bibliotecas instaladas, obtendrás errores del cargador.
Pero los segfaults ocurren cuando el cargador tiene éxito y el ABI aún no coincide con lo que el llamador espera.

Decisión: Si la preferencia de rutas es incorrecta, corrige la preferencia de rutas. No intentes “parchar” incompatibilidades de ABI en tiempo de ejecución.

Task 7: Confirma qué paquete posee la biblioteca sospechosa (o que ninguna la posee)

cr0x@server:~$ dpkg -S /usr/local/lib/libssl.so.3
dpkg-query: no path found matching pattern /usr/local/lib/libssl.so.3

Qué significa: No es un archivo gestionado por Debian. Podría ser un OpenSSL instalado a mano, un tarball del proveedor o restos de algún “arreglo rápido”.

Decisión: Si una biblioteca criptográfica clave no está gestionada, considera seriamente eliminarla de las rutas del cargador y usar la biblioteca empaquetada por Debian.

Task 8: Inspecciona los metadatos ELF y el BuildID sospechoso

cr0x@server:~$ readelf -n /usr/local/lib/libssl.so.3 | sed -n '1,80p'
Displaying notes found in: .note.gnu.build-id
  Owner                Data size 	Description
  GNU                  0x00000014	NT_GNU_BUILD_ID (unique build ID bitstring)
    Build ID: 4b2f5d0a1c6b0f7a9e6d5d3a2b1f0c9d8e7a6b5c

Qué significa: Los BuildIDs te permiten emparejar binarios con símbolos de depuración y con builds conocidos. Una biblioteca local con un BuildID aleatorio es otra pista.

Decisión: Si no puedes rastrear la procedencia, planifica reemplazarla por una biblioteca empaquetada o recompilarla en una canalización controlada.

Task 9: Comprueba la versión de la biblioteca OpenSSL en Debian y los candidatos

cr0x@server:~$ apt-cache policy libssl3
libssl3:
  Installed: 3.3.2-1
  Candidate: 3.3.2-1
  Version table:
 *** 3.3.2-1 500
        500 http://deb.debian.org/debian trixie/main amd64 Packages
        100 /var/lib/dpkg/status

Qué significa: OpenSSL de Debian está instalado y consistente con la release.
Si tu app no lo está usando, tienes un problema de override, no “Debian rompió crypto”.

Decisión: Prefiere /usr/lib/x86_64-linux-gnu/libssl.so.3 de Debian a menos que tengas una razón auditada para no hacerlo.

Task 10: Verifica la configuración del cargador y las rutas de búsqueda

cr0x@server:~$ grep -R --line-number "/usr/local/lib" /etc/ld.so.conf /etc/ld.so.conf.d/* 2>/dev/null
/etc/ld.so.conf.d/local.conf:1:/usr/local/lib
cr0x@server:~$ ldconfig -v 2>/dev/null | head -n 25
/usr/local/lib: (from /etc/ld.so.conf.d/local.conf:1)
	libssl.so.3 -> libssl.so.3
	libcrypto.so.3 -> libcrypto.so.3
/usr/lib/x86_64-linux-gnu:
	libssl.so.3 -> libssl.so.3
	libcrypto.so.3 -> libcrypto.so.3

Qué significa: La caché del cargador del sistema incluye ambas, pero la preferencia da efectivamente a /usr/local/lib un asiento en primera fila.
Así obtienes desajustes tras actualizaciones: las bibliotecas de la distro cambiaron y las locales no lo hicieron (o cambiaron de forma distinta).

Decisión: Elimina /usr/local/lib de la configuración global del cargador salvo que estés absolutamente seguro de querer que sobreescriba las bibliotecas de la distro.
Si necesitas bibliotecas locales, constriñelas por app, no globalmente.

Task 11: Captura un core y extrae las bibliotecas cargadas

cr0x@server:~$ coredumpctl info /usr/local/bin/myapp | sed -n '1,80p'
           PID: 22198
           UID: 1001
           GID: 1001
        Signal: 11 (SEGV)
     Timestamp: Mon 2025-12-30 10:21:14 UTC
  Command Line: /usr/local/bin/myapp --serve
    Executable: /usr/local/bin/myapp
 Control Group: /system.slice/myapp.service
          Unit: myapp.service
       Message: Process 22198 (myapp) of user 1001 dumped core.
cr0x@server:~$ coredumpctl debug /usr/local/bin/myapp
GNU gdb (Debian 15.2-1) 15.2
Reading symbols from /usr/local/bin/myapp...
(No debugging symbols found in /usr/local/bin/myapp)
(gdb) info sharedlibrary
From                To                  Syms Read   Shared Object Library
0x00007f2d2a9f5000  0x00007f2d2aa1b000  Yes (*)     /lib64/ld-linux-x86-64.so.2
0x00007f2d2a860000  0x00007f2d2a8e2000  Yes (*)     /usr/local/lib/libssl.so.3
0x00007f2d2a3d0000  0x00007f2d2a84d000  Yes (*)     /usr/local/lib/libcrypto.so.3
0x00007f2d2a1ef000  0x00007f2d2a3b8000  Yes (*)     /lib/x86_64-linux-gnu/libc.so.6
(gdb) bt
#0  0x00007f2d2a8f4c90 in ?? () from /usr/local/lib/libssl.so.3
#1  0x000000000040f23a in ?? ()
#2  0x0000000000410a11 in ?? ()
#3  0x00007f2d2a20a2ca in __libc_start_call_main () from /lib/x86_64-linux-gnu/libc.so.6
#4  0x00007f2d2a20a385 in __libc_start_main () from /lib/x86_64-linux-gnu/libc.so.6
#5  0x000000000040b4e5 in ?? ()

Qué significa: Has confirmado que el fallo ocurre dentro del libssl local. Eso no prueba que libssl sea “defectuoso”; prueba que está en la línea de fuego.
La clave es el desajuste entre las expectativas del llamador y el ABI/estructuras del callee.

Decisión: Instala símbolos de depuración para las bibliotecas sospechosas (o cambia a las bibliotecas de Debian) para obtener un backtrace significativo.

Task 12: Instala símbolos de depuración para bibliotecas del sistema (cuando estén disponibles)

cr0x@server:~$ sudo apt-get install -y gdb libc6-dbg libssl3-dbgsym
Reading package lists... Done
Building dependency tree... Done
The following NEW packages will be installed:
  gdb libc6-dbg libssl3-dbgsym
0 upgraded, 3 newly installed, 0 to remove and 0 not upgraded.

Qué significa: Con símbolos puedes ver nombres de funciones y a veces pistas de argumentos. Si la biblioteca que falla está en /usr/local,
los dbgsym de la distro no ayudarán para ese archivo exacto—pero te ayudarán a entender el límite de llamada y si glibc está implicada.

Decisión: Si la biblioteca dañina no está gestionada, obtén símbolos coincidentes de quien la compiló, o deja de usarla.

Task 13: Comprueba actualizaciones parciales, paquetes retenidos y pins

cr0x@server:~$ apt-mark showhold
libc6
cr0x@server:~$ apt-get -s dist-upgrade | sed -n '1,80p'
Reading package lists... Done
Building dependency tree... Done
Calculating upgrade... Done
The following packages will be upgraded:
  libc6 libc6-dev
2 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.

Qué significa: Un libc6 retenido es básicamente un fork de compatibilidad autoinfligido. Si actualizaste el espacio de usuario pero fijaste glibc, estás jugando a la ruleta.

Decisión: Quita la retención y completa la actualización, o revierte y mantén todo el sistema consistente. “Debian medio-nuevo” es donde nacen los segfaults.

Task 14: Verifica la integridad de paquetes de componentes centrales

cr0x@server:~$ sudo dpkg -V libc6 libssl3 | head -n 50
..5......  /lib/x86_64-linux-gnu/libc.so.6

Qué significa: El 5 indica una discrepancia MD5: el archivo difiere de lo que el paquete espera.
Esto puede suceder si algo lo sobrescribió, o si estás en un snapshot de filesystem con rarezas, o si la actualización fue interrumpida.

Decisión: Reinstala los paquetes afectados inmediatamente. Si la discrepancia vuelve, sospecha de corrupción de disco o de una herramienta de gestión de configuración escribiendo en rutas del sistema.

Task 15: Reinstala los paquetes dañados (reparación quirúrgica)

cr0x@server:~$ sudo apt-get install --reinstall -y libc6 libssl3
Reading package lists... Done
Building dependency tree... Done
0 upgraded, 0 newly installed, 2 reinstalled, 0 to remove and 0 not upgraded.

Qué significa: Has restaurado las bibliotecas gestionadas por paquetes a los bits esperados.

Decisión: Prueba de nuevo el servicio que fallaba. Si aún carga /usr/local/lib, no has solucionado el desajuste—solo has limpiado la línea de base.

Localizar la incompatibilidad exacta (el objetivo real)

“Incompatibilidad de biblioteca” es un diagnóstico vago. Queremos algo que puedas poner en un informe de incidente sin vergüenza:
/usr/local/lib/libssl.so.3 compilado contra OpenSSL X y glibc Y fue precargado vía ld.so.conf global y falló cuando lo llamó myapp compilado contra libssl3 de Debian.”
Ese nivel de especificidad es alcanzable con unas comprobaciones más dirigidas.

Step A: Prueba que se selecciona la biblioteca incorrecta

Ya lo viste en ldd y LD_DEBUG. Ahora demuestra que se debe a la configuración global del cargador versus ajustes específicos de la aplicación.

cr0x@server:~$ /lib64/ld-linux-x86-64.so.2 --list /usr/local/bin/myapp | head -n 30
	linux-vdso.so.1 (0x00007ffda91f2000)
	libssl.so.3 => /usr/local/lib/libssl.so.3 (0x00007f6b4d3c0000)
	libcrypto.so.3 => /usr/local/lib/libcrypto.so.3 (0x00007f6b4cf30000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6b4cd4f000)

Decisión: Si el propio cargador confirma el mapeo, deja de debatir. Arregla el mapeo.

Step B: Comprueba RPATH/RUNPATH y hábitos de build antiguos

cr0x@server:~$ readelf -d /usr/local/bin/myapp | grep -E "RPATH|RUNPATH"
 0x000000000000001d (RUNPATH)            Library runpath: [/usr/local/lib]

Qué significa: El binario está codificado para preferir /usr/local/lib.
Incluso si quitas /usr/local/lib de ld.so.conf, este binario seguirá buscándolo.

Decisión: Reconstruye el binario sin ese RUNPATH, o aplícale un parche con cuidado (y documenta el cambio como adulto).

Step C: Identifica expectativas de versión de símbolos

A veces la incompatibilidad es sutil: la biblioteca existe, se carga, exporta los nombres correctos, pero las versiones no coinciden con lo que el binario espera.

cr0x@server:~$ objdump -T /usr/local/bin/myapp | grep -E "OPENSSL_|GLIBC_" | head -n 20
0000000000000000      DF *UND*	0000000000000000 (OPENSSL_3.3.0) EVP_MD_fetch
0000000000000000      DF *UND*	0000000000000000 (OPENSSL_3.3.0) SSL_CTX_new
0000000000000000      DF *UND*	0000000000000000 (GLIBC_2.38) memcpy
cr0x@server:~$ objdump -T /usr/local/lib/libssl.so.3 | grep -E "OPENSSL_" | head -n 20
000000000003d2a0 g    DF .text	00000000000000f5  OPENSSL_3.2.0 SSL_CTX_new
0000000000047b10 g    DF .text	0000000000000120  OPENSSL_3.2.0 EVP_MD_fetch

Qué significa: El binario quiere símbolos OPENSSL_3.3.0. La biblioteca exporta OPENSSL_3.2.0.
Normalmente esto fallaría en la carga con “version not found”. Pero las incompatibilidades pueden esconderse si el binario es menos estricto, o si plugins llaman versiones distintas indirectamente.
De cualquier modo, ahora tienes una declaración de incompatibilidad nítida.

Decisión: Alinea las versiones de OpenSSL: usa el libssl de Debian que coincide con las suposiciones de build del binario, o recompila el binario contra la biblioteca que piensas desplegar.

Step D: Detecta “dos copias de la misma dependencia” en un proceso

Aquí es donde la cosa se pone picante: dos copias de una biblioteca con símbolos similares, cargadas desde diferentes rutas, en el mismo proceso.
Eso puede ocurrir con plugins, dlopen y SDKs de proveedores.

cr0x@server:~$ sudo gdb -q /usr/local/bin/myapp -ex 'set pagination off' -ex 'run --serve' -ex 'info proc mappings' -ex 'quit'
Reading symbols from /usr/local/bin/myapp...
(No debugging symbols found in /usr/local/bin/myapp)
Starting program: /usr/local/bin/myapp --serve
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
process 23102
Mapped address spaces:
      Start Addr           End Addr       Size     Offset  Perms  objfile
      0x7f2d2a860000     0x7f2d2a8e2000    0x82000        0x0  r-xp   /usr/local/lib/libssl.so.3
      0x7f2d29f00000     0x7f2d29f82000    0x82000        0x0  r-xp   /usr/lib/x86_64-linux-gnu/libssl.so.3

Qué significa: Ambas copias están mapeadas. Eso no es “un poquito malo.” Es “comportamiento indefinido con corbata.”

Decisión: Encuentra quién está cargando la segunda copia (a menudo un plugin o una ruta dlopen) y elimina la duplicación. Un proceso, una sola libssl.

Step E: Encuentra el desencadenante del cargador: entorno, unidad systemd, scripts wrapper

cr0x@server:~$ systemctl cat myapp.service
# /etc/systemd/system/myapp.service
[Service]
ExecStart=/usr/local/bin/myapp --serve
Environment=LD_LIBRARY_PATH=/usr/local/lib

Qué significa: Alguien estableció LD_LIBRARY_PATH en una unidad systemd. Eso derrota la mayoría de las protecciones del empaquetado de Debian.

Decisión: Elimina la sobreescritura de entorno y despliega dependencias correctamente. Si debes usar LD_LIBRARY_PATH, acótalo y documenta por qué.

Step F: Confirma que el estado del paquete es consistente tras la actualización

cr0x@server:~$ dpkg -l | awk '$1 ~ /^(ii|iF|iU|rc)$/ {print $0}' | grep -E '^(iF|iU)'
iU  libgcc-s1:amd64  14.2.0-3  amd64  GCC support library
cr0x@server:~$ sudo dpkg --configure -a
Setting up libgcc-s1:amd64 (14.2.0-3) ...
Processing triggers for libc-bin (2.38-6) ...

Qué significa: Un paquete “sin configurar” puede dejar un estado medio escrito, incluyendo entradas antiguas en la caché de bibliotecas, triggers sin ejecutar y dependencias inconsistentes.

Decisión: Completa siempre la configuración y los triggers tras una actualización interrumpida, luego vuelve a ejecutar ldconfig si es necesario.

Step G: Valida expectativas ABI de glibc y libstdc++ (vector común post-actualización)

Si tus frames de fallo están dentro de libstdc++.so.6, libgcc_s.so.1 o asignadores de memoria, podrías estar en territorio de ABI de C++.

cr0x@server:~$ ldd /usr/local/bin/myapp | grep -E "libstdc\+\+|libgcc_s"
	libstdc++.so.6 => /usr/local/lib/libstdc++.so.6 (0x00007f7a1a400000)
	libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f7a1a1e0000)
cr0x@server:~$ strings /usr/local/lib/libstdc++.so.6 | grep -E "GLIBCXX_" | tail -n 5
GLIBCXX_3.4.29
GLIBCXX_3.4.30
GLIBCXX_3.4.31
GLIBCXX_3.4.32
GLIBCXX_3.4.33

Qué significa: Un libstdc++ local que sobreescribe el libstdc++ de la distro es causa frecuente de “caídas tras una actualización”, especialmente cuando plugins se compilaron con otra toolchain.
Las versiones GLIBCXX_ te ayudan a ver lo que la biblioteca afirma soportar.

Decisión: Evita sobreescribir libstdc++ globalmente. Usa la toolchain de la distro de punta a punta, o lleva todo el runtime en un contenedor/sandbox controlado.

Broma #2: Si depuras un segfault con LD_LIBRARY_PATH establecido a nivel sistema, no estás haciendo troubleshooting—estás recreando una película de terror.

Tres microhistorias del mundo corporativo (anonimizadas, dolorosamente reales)

Microhistoria 1: El incidente causado por una suposición equivocada

Una empresa mediana operaba una flota de servidores Debian para una API de cara al cliente. La actualización a Debian 13 fue por etapas: primero el SO, más tarde la aplicación.
El equipo asumió que los binarios de la app eran “lo suficientemente autocontenidos” porque desplegaban un tarball con unas cuantas .so junto al ejecutable.

La primera señal de problemas no fue una caída total. Fue ruido: un nuevo goteo de 502s y algunos workers muriendo por hora. Los logs del kernel culpaban a libcrypto.
Los desarrolladores se encogieron de hombros—“no tocamos crypto”—y el incidente quedó en una zona incómoda: no lo bastante roto para revertir, no estable para ignorarlo.

La suposición equivocada fue sutil: creían que “las bibliotecas empaquetadas solo las usa nuestra app”. En realidad, un admin anterior había añadido /opt/vendor/lib
en /etc/ld.so.conf.d años antes para satisfacer una herramienta legacy. Tras la actualización del SO, el cargador empezó a seleccionar primero el OpenSSL del proveedor
para múltiples procesos, no solo para los workers de la API.

La solución fue aburrida e inmediata: quitar la ruta global del cargador, ejecutar ldconfig, reiniciar los servicios afectados y asegurar que los workers de la API usaran OpenSSL de Debian
(o empaquetar su propio runtime en un runpath aislado que no contamine el host).

La lección que quedó: las bibliotecas compartidas son compartidas. Si enseñas al cargador un truco nuevo globalmente, todos los procesos lo aprenden, incluidos los que quieres proteger.

Microhistoria 2: La optimización que salió mal

Otra organización optimizó su pipeline de build produciendo un único paquete binario “portátil”. Compilaron con una toolchain más nueva y enlazaron dinámicamente
contra un conjunto curado de bibliotecas copiado en /usr/local/lib en cada host. Reducía artefactos y aceleraba despliegues.
También hacía las actualizaciones frágiles.

Tras moverse a Debian 13, vieron segfaults intermitentes bajo carga. Las fallas se correlacionaban con handshakes TLS y tráfico HTTP/2,
y los stack traces variaban. Algunos apuntaban a libssl, otros al asignador de memoria. Los ingenieros persiguieron condiciones de carrera durante días.
Añadieron reintentos. Ajustaron pools de hilos. Incluso cambiaron flags del compilador. Los segfaults sonrieron cortesmente y continuaron.

Finalmente alguien ejecutó LD_DEBUG=libs y notó un patrón: a veces el proceso cargaba libcrypto de Debian, otras veces la local,
dependiendo de cómo se cargaban los plugins y qué rutas estaban activas. Dos copias de OpenSSL en un mismo espacio de direcciones no es una configuración soportada; es un generador de caos.

La “optimización” fue usar rutas compartidas del host como sustrato de despliegue. Parecía eficiente, pero difuminó la propiedad y las fronteras de versiones.
La solución requirió deshacer eso: enlazar estáticamente cuando fuera posible, o desplegar un runtime aislado apropiado (contenedor, chroot o al menos runpath por servicio),
y dejar de mezclar copias del host y del proveedor.

La conclusión del postmortem fue directa: la conveniencia es una dependencia. Si optimizas saltándote el solucionador de dependencias de la distro, te conviertes en el solucionador.

Microhistoria 3: La práctica aburrida pero correcta que salvó el día

Una firma de servicios financieros tenía una regla: antes de cualquier actualización mayor de Debian, tomar un snapshot del filesystem, capturar las selecciones de dpkg,
y exportar el estado exacto de pinning de APT. Nadie amaba esa regla. Parecía papeleo para computadoras.

Durante su actualización a Debian 13, un job de background empezó a crashar con segfaults dentro de libpq. La aplicación tenía plugins, y
el proveedor del plugin suministró una extensión binaria compilada “para Debian”. Esa frase no significa nada sin detalles, pero procurement estaba contento.

El SRE de guardia comparó inventarios de bibliotecas pre y post actualización y notó que el instalador del proveedor había dejado una copia de libstdc++.so.6
en /usr/local/lib en un subconjunto de hosts semanas antes. La mayoría de cajas no la tenía. Las que sí, estaban fallando.
Como tenían snapshots, pudieron diffear el estado de la caché del cargador y confirmar el cambio rápidamente.

La solución fue simple: eliminar la biblioteca local suelta, restaurar la propiedad de los paquetes de Debian y aplicar una política que prohíba a los instaladores de terceros modificar rutas globales del cargador.
La extensión del proveedor se recompiló correctamente después, pero el incidente se detuvo en una hora porque el equipo pudo probar “qué cambió” sin adivinar.

La práctica aburrida no fue heroica. Fue higiene: snapshots, inventario y reproducibilidad. No te hace más rápido todos los días. Te hace rápido cuando importa.

Errores habituales: síntoma → causa raíz → arreglo

Aquí están los reincidentes que veo tras actualizaciones de Debian. El objetivo es reconocer patrones: ves el síntoma, vas al subsistema correcto,
y evitas la “depuración aleatoria”.

1) Síntoma: Solo las apps desplegadas localmente se bloquean; los servicios de Debian están bien

  • Causa raíz: Binarios locales compilados contra libs antiguas/nuevas; RUNPATH apunta a /usr/local/lib; librerías empaquetadas ocultan las de la distro.
  • Arreglo: Elimina RUNPATH a ubicaciones globales; recompila contra la toolchain de Debian 13; despliega dependencias en un directorio runtime aislado, no en rutas globales del cargador.

2) Síntoma: Muchos binarios no relacionados se segfaultan poco después de la actualización

  • Causa raíz: Actualización parcial o paquetes núcleo retenidos (glibc, libgcc); caché del cargador inconsistente; configuración de dpkg interrumpida.
  • Arreglo: Completa la actualización (dpkg --configure -a, apt-get dist-upgrade), quita retenciones, reinstala libs críticas, reconstruye initramfs si procede.

3) Síntoma: El backtrace apunta a libssl/libcrypto pero el código de la app cambió poco

  • Causa raíz: Dos copias de OpenSSL cargadas; libssl.so resuelto desde /usr/local o directorios de proveedor; plugin compilado contra otra versión de OpenSSL.
  • Arreglo: Asegura una sola instancia de OpenSSL por proceso; elimina overrides globales; recompila plugins; usa libssl de la distro o hecha toda la pila en un entorno aislado.

4) Síntoma: El fallo ocurre solo cuando se carga un plugin/módulo

  • Causa raíz: Incompatibilidad ABI del plugin (ABI C++, disposición de estructuras, expectativas de glibc). La app host se actualizó; el plugin no.
  • Arreglo: Recompila el plugin contra los headers/libs actualizados; aplica checks de compatibilidad de versión para plugins; evita plugins binarios a menos que controles la toolchain.

5) Síntoma: dpkg -V muestra discrepancias en libc u otras libs núcleo

  • Causa raíz: Archivo sobrescrito, corrupción, actualización interrumpida o gestión de configuración escribiendo en directorios del sistema.
  • Arreglo: Reinstala paquetes; investiga quién modificó archivos (audita CM, reglas de infraestructura inmutable); ejecuta chequeos de salud de disco si las discrepancias persisten.

6) Síntoma: Errores del cargador como “version `GLIBC_2.xx’ not found” o “undefined symbol”

  • Causa raíz: Incompatibilidad clara en la versión de símbolo requerida; binario compilado en glibc/libstdc++ más nuevo que el objetivo.
  • Arreglo: Compila sobre el objetivo más antiguo que necesites (o dentro de un entorno de build de Debian 13); no copies al tuntún libc.so.6; alinea toolchain.

Listas de verificación / plan paso a paso

Checklist A: Triaje en los primeros 15 minutos

  1. Obtén una firma clara de fallo desde journalctl (biblioteca con fallo, servicio, frecuencia).
  2. Confirma si el ejecutable que falla es gestionado por la distro (dpkg -S) o es local (/usr/local, /opt).
  3. Captura un core con coredumpctl y lista bibliotecas compartidas cargadas en gdb.
  4. Ejecuta ldd y readelf -d para identificar RPATH/RUNPATH y la selección de bibliotecas.
  5. Busca LD_LIBRARY_PATH en unidades systemd, wrappers y cronjobs.

Checklist B: Confirma consistencia de paquetes (deja de vivir en una actualización parcial)

  1. Revisa retenciones: apt-mark showhold. Quita retenciones salvo que tengas una razón probada.
  2. Revisa estado roto: dpkg --audit, luego dpkg --configure -a.
  3. Simula la actualización completa: apt-get -s dist-upgrade.
  4. Verifica integridad: dpkg -V para libc y las bibliotecas sospechosas. Reinstala lo que no coincida.

Checklist C: Arregla la incompatibilidad con daño colateral mínimo

  1. Preferido: deja de sobreescribir bibliotecas de la distro globalmente. Elimina /usr/local/lib de ld.so.conf si no es estrictamente necesario.
  2. Elimina o renombra bibliotecas conflictivas no gestionadas (mantén una copia de rollback en un lugar seguro, no en rutas del cargador).
  3. Ejecuta ldconfig y reinicia solo los servicios afectados (o reinicia si el estado del cargador es cuestionable).
  4. Si la app necesita libs personalizadas, desplégalas en un directorio propiedad de la app y referencia mediante RUNPATH acotado a esa app—o usa contenedores.
  5. Recompila plugins/módulos contra las bibliotecas y la toolchain de Debian 13; trata plugins binarios como sospechosos hasta que se prueben.

Checklist D: Evita que vuelva a suceder

  1. Política: ningún instalador de proveedor puede modificar /etc/ld.so.conf.d o dejar libs en /usr/local/lib sin revisión.
  2. Inventario: escanea periódicamente en busca de libs ELF no gestionadas en rutas del cargador.
  3. Runbook de actualización: exige “sin retenciones para glibc/libgcc/libstdc++ durante actualizaciones de release.”
  4. Disciplina de build: compila en un entorno Debian 13; publica un manifiesto de versiones de biblioteca requeridas.

Preguntas frecuentes

1) ¿Por qué una incompatibilidad de biblioteca causa un segfault en lugar de un error de cargador claro?

Los errores del cargador ocurren cuando no se pueden resolver símbolos requeridos. Los segfaults ocurren cuando los símbolos se resuelven pero el contrato ABI está roto:
disposición de estructuras equivocada, expectativas distintas sobre propiedad de memoria, tipos C++ incompatibles o bibliotecas duplicadas en un proceso.

2) ¿Es fiable ldd para diagnosticar lo que ocurrirá en tiempo de ejecución?

Es un buen vistazo inicial, no un evangelio. ldd puede verse afectado por variables de entorno y no muestra todo el comportamiento de dlopen/plugins.
Para la verdad en tiempo de ejecución, usa LD_DEBUG=libs e inspecciona los mapeos de un proceso vivo o un core dump.

3) ¿Puede una actualización a Debian 13 introducir legítimamente segfaults en software estable?

Es posible pero menos común de lo que se piensa. La mayoría de los segfaults post-actualización son desencadenados por overrides locales, plugins, actualizaciones parciales
o binarios compilados sobre una base distinta. Trata “Debian lo rompió” como una hipótesis que debes demostrar con evidencia.

4) ¿Debo “arreglarlo” haciendo un symlink de /usr/local/lib/libssl.so.3 al libssl de Debian?

No. Ese es un parche frágil que vuelve opaca la procedencia y rompe las expectativas del gestor de paquetes. Elimina el override y deja que el cargador seleccione la biblioteca empaquetada,
o recompila la app correctamente. Los hacks con symlinks son la forma en que el futuro-tú termina haciendo respuesta a incidentes en un festivo.

5) ¿Qué pasa si debo desplegar un OpenSSL personalizado por razones de cumplimiento?

Entonces desplégalo en un directorio aislado propiedad de la aplicación y asegúrate de que solo esa aplicación lo use.
Evita cambios globales en las rutas del cargador. Haz explícita la frontera de dependencia (RUNPATH acotado a la app, runtime en contenedor o chroot).

6) ¿Cómo detecto si se han cargado dos copias de la misma biblioteca?

Usa gdb (info proc mappings), inspecciona /proc/$PID/maps o analiza los mapeos del core dump.
Si ves tanto /usr/local/lib/libssl.so.3 como /usr/lib/x86_64-linux-gnu/libssl.so.3, has encontrado una causa grave.

7) ¿Por qué estos fallos a veces aparecen solo bajo carga?

Las incompatibilidades ABI y las bibliotecas duplicadas pueden comportarse como condiciones de carrera porque la disposición de memoria y el timing cambian con la concurrencia.
Bajo poca carga, puede que no se active la ruta corruptora. Bajo carga, lo hará—con suficiente fiabilidad para arruinar tu semana.

8) ¿Cuál es la remediación más limpia cuando el binario que falla está en /usr/local?

Recompílalo contra Debian 13 en un entorno controlado, elimina RUNPATH codificado a ubicaciones globales y elimina la dependencia de LD_LIBRARY_PATH global.
Si no puedes recompilar, aisla el runtime (contenedor/chroot) para que deje de interferir con el host.

9) ¿Los core dumps suponen un riesgo de seguridad?

Sí: los cores pueden contener secretos. Usa políticas de almacenamiento de systemd-coredump, restringe el acceso y considera deshabilitar cores para servicios sensibles.
Pero para esta clase de problema, un core bien manejado puede ahorrarte días de conjeturas.

10) ¿Cuándo debería revertir en lugar de depurar hacia adelante?

Si las bibliotecas del sistema muestran discrepancias de integridad que no puedes explicar, si múltiples servicios críticos se están cayendo, o si sospechas corrupción de almacenamiento,
revertir a un snapshot conocido bueno suele ser la vía más segura. Depura en un clon, no en el host que sangra.

Conclusión: siguientes pasos que puedes ejecutar

“Segfault tras actualización” deja de ser aterrador cuando lo tratas como un problema de inventario: qué binario cargó qué objetos compartidos exactos desde qué rutas exactas,
y si esos objetos coinciden en ABI y versiones de símbolos.

Si no te llevas otra cosa del caso #55, llévate esto: deja de permitir que /usr/local/lib (o rutas de proveedor) sobreescriban las bibliotecas de Debian globalmente.
Localiza la incompatibilidad con coredumpctl + gdb + LD_DEBUG, pruébala con rutas de archivo y versiones de símbolos,
y después arréglala restaurando un conjunto de bibliotecas consistente y único.

Pasos prácticos siguientes:

  1. Elige un fallo y extrae la lista de bibliotecas cargadas del core dump.
  2. Usa LD_DEBUG=libs,versions para probar la elección del cargador y los requisitos de versión.
  3. Elimina overrides globales (ld.so.conf.d, LD_LIBRARY_PATH, RUNPATH a ubicaciones compartidas).
  4. Reinstala y verifica paquetes núcleo si dpkg -V indica discrepancias.
  5. Reconstruye o aísla todo aquello que no esté gestionado por el empaquetado de Debian.
← Anterior
Virtualización doméstica: qué características de la CPU importan realmente
Siguiente →
Ubuntu 24.04 tmpfs/ramdisk desbocado: evita que devore RAM (sin romper aplicaciones)

Deja un comentario