Therac-25: cuando fallos de software mataron pacientes

¿Te fue útil?

Los fallos en producción suelen ser “solo” costosos. Suena un pager, ingresos que titubean, un cliente que se queja, aplicamos un parche y seguimos.
Therac-25 fue distinto: una serie de decisiones de software y de sistema convirtieron un dispositivo médico en una máquina capaz de administrar
sobredosis catastróficas de radiación —mientras le decía con confianza al operador que todo estaba bien.

Si construyes servicios, plataformas, almacenamiento o cualquier cosa que despliegue código al mundo, este caso no es historia antigua.
Es un informe de campo sobre lo que pasa cuando eliminas interbloqueos hardware, confías en concurrencia que no entiendes y tratas
los incidentes como “error de usuario” hasta que se acaba la suerte.

Qué fue Therac-25 y por qué importó

Therac-25 fue un acelerador lineal médico controlado por ordenador usado para radioterapia a mediados de los años 80.
Podía operar en varios modos, incluyendo modo de haz de electrones y modo de rayos X de alta energía. En operación segura,
hardware y software deberían coordinarse para asegurar que la energía del haz y la configuración física estuvieran alineadas —objetivo,
filtros, colimadores y todo el tedioso equipo mecánico que mantiene a los pacientes con vida.

La parte de “por qué importó” no es que tuviera bugs de software. Todo tiene bugs. La historia es que
el diseño del dispositivo delegó en el software garantías de seguridad que las máquinas anteriores aplicaban mediante interbloqueos hardware.
Cuando el software fallaba —y fallaba de formas sutiles dependientes del tiempo—, la máquina podía administrar una sobredosis masiva
sin una alarma creíble ni un seguro fiable.

Si alguna vez reemplazaste una baranda de seguridad física por “lógica en el controlador”, has tocado la misma placa caliente.
Si alguna vez eliminaste una comprobación “lenta” porque “nunca pasa”, has estado en la misma zona de riesgo.
Y si alguna vez trataste un informe de un operador como ruido porque “el sistema dice OK”, ya estás sosteniendo el fósforo.

Hechos concretos y contexto histórico (breve, útil, no para trivialidades)

  • Los incidentes del Therac-25 ocurrieron a mediados de los años 80, con múltiples eventos de sobredosis vinculados a fallos de diseño de software y sistema.
  • Máquinas anteriores relacionadas (Therac-6 y Therac-20) usaban más interbloqueos hardware; Therac-25 dependía más del software para la seguridad.
  • Los operadores podían provocar un estado peligroso mediante secuencias de entrada rápidas (comportamiento dependiente del tiempo), un síntoma clásico de condiciones de carrera/concurrencia.
  • Los mensajes de error eran crípticos (por ejemplo, códigos vagos de “malfunción”), lo que llevó al personal a reintentar en lugar de apagar y escalar de forma segura.
  • Algunas sobredosis fueron inicialmente descartadas como reacciones de pacientes o errores de operador, retrasando la contención efectiva y la corrección de la causa raíz.
  • Se asumió que el software reutilizado era seguro porque existía en productos anteriores, aunque el contexto de seguridad cambió al reducirse los interbloqueos hardware.
  • La telemetría y diagnósticos fueron insuficientes para reconstruir rápidamente los eventos, un favor a la negación y una maldición para la respuesta a incidentes.
  • Las prácticas regulatorias e industriales de seguridad de software eran menos maduras que hoy; los métodos formales y la V&V independiente rigurosa no se aplicaban uniformemente.
  • El caso se convirtió en una lección fundacional sobre seguridad de software, utilizada durante décadas en ética de la ingeniería y formación en fiabilidad.

Dos de estos hechos deberían incomodarte: “software reutilizado asumido seguro” y “errores crípticos que inducen reintentos.”
Eso es 2026 con otra cara.

La cadena de fallos: desde las pulsaciones hasta la dosis letal

1) La seguridad se trasladó del hardware al software, pero el software no se construyó como un sistema de seguridad

En un dispositivo crítico para la seguridad, no puedes decir “el código debe hacer X.” Debes decir “el sistema no puede hacer Y,”
incluso bajo fallos parciales: bits atascados, anomalías de temporización, secuencias inesperadas, entradas erróneas, sensores degradados, fallos de alimentación.
Los interbloqueos hardware históricamente forzaban transiciones a estados seguros sin importar la confusión del software.

Therac-25 redujo interbloqueos hardware y confió en la coordinación por software. Esa elección no es automáticamente incorrecta,
pero cambia la carga de la prueba. Si eliminas restricciones físicas, tu software debe ser diseñado, verificado
y monitorizado como si fuera el único paracaídas.

2) Condiciones de carrera: del tipo que se ríe de tu plan de pruebas

El patrón infame en las discusiones sobre Therac-25 es que ciertas secuencias rápidas por parte del operador podían empujar el sistema a un estado interno inválido.
El operador podía editar parámetros de tratamiento rápidamente, y el sistema podía aceptar los valores nuevos mientras otras
partes de la lógica de control seguían suponiendo los valores antiguos. Ahora tienes “energía del haz ajustada para modo A” y “configuración mecánica
posicionada para modo B.” Esa descoordinación no es “un bug.” Es una configuración letal.

Las condiciones de carrera son fallos de coordinación. Se escabullen de las pruebas unitarias, se ocultan de los flujos normales y adoran
a los “operadores veloces” porque los humanos son sorprendentemente buenos generando temporizaciones extrañas. El equivalente moderno es un sistema distribuido
que “por lo general converge” pero ocasionalmente escribe una configuración obsoleta en el cohorte equivocado. En contextos de seguridad, “ocasionalmente”
es inaceptable.

3) La interfaz y los mensajes de error enseñaron al personal a seguir adelante

Si quieres que un sistema falle de forma segura, debes enseñar a los humanos a su alrededor qué significa “inseguro.” En el Therac-25,
los operadores encontraron códigos y mensajes confusos. El dispositivo podía mostrar un error, permitir que el operador continuara tras reconocerlo,
y no indicar claramente la gravedad ni la acción correcta.

Una interfaz de seguridad no optimiza para el rendimiento. Optimiza para la escalada correcta. Usa lenguaje claro,
gravedad explícita y acciones requeridas inequívocas. Cuando la interfaz hace que “reintentar” sea el camino más fácil,
no es error del usuario cuando la gente reintenta. Es diseño.

4) Observabilidad débil habilitó la negación

Después de un incidente, necesitas forense: logs, snapshots de estado, lecturas de sensores, secuencias de comandos, marcas temporales,
y una forma de correlacionarlos. Los diagnósticos del Therac-25 no fueron suficientes para una reconstrucción rápida y confiable.
Ante la ausencia de evidencia, las organizaciones suelen defaultear a narrativas. La más común es “error del operador.”

La observabilidad es un control, no un lujo. En sistemas críticos para la seguridad, también es una obligación moral:
no puedes arreglar lo que no ves, y no puedes probar seguridad con sensaciones.

5) La respuesta a incidentes se trató como anomalía, no como señal

Una sobredosis debería desencadenar una respuesta de seguridad drástica: contención, aviso de seguridad, investigación independiente y una inclinación
hacia “el sistema es culpable hasta que se demuestre lo contrario.” En cambio, el patrón histórico muestra reconocimiento retrasado y daño repetido.

Esto es un modo de fallo organizativo reconocible: tratar un incidente como un “caso raro,” parchear localmente y seguir adelante.
El problema es que el segundo incidente no es “raro.” Es tu sistema diciéndote la verdad, más fuerte.

6) La seguridad es una propiedad del sistema; Therac-25 fue optimizado como producto

A veces la gente resume Therac-25 como “un bug mató pacientes.” Eso es incompleto y ligeramente reconfortante,
por eso persiste. La historia más profunda es que fallaron múltiples capas:
suposiciones de diseño, interbloqueos ausentes, defectos de concurrencia, diagnósticos pobres y una cultura de respuesta que no trató
los informes como evidencia urgente.

Una cita pertenece aquí porque corta mucha tontería moderna:
idea parafraseada: “La esperanza no es una estrategia.” — General Gordon R. Sullivan (comúnmente citado en contextos de fiabilidad y planificación).

Sustituye “esperanza” por “debería,” “normalmente” o “no puede pasar,” y obtendrás un generador fiable de postmortems.

Broma #1: Las condiciones de carrera son como las cucarachas—si ves una en producción, asume que hay cuarenta más escondidas detrás de “funcionó en mi máquina.”

Lecciones de sistemas: qué copiar y qué nunca copiar

Lección A: No elimines interbloqueos a menos que los reemplaces por garantías más fuertes

Los interbloqueos hardware son instrumentos toscos, pero increíblemente eficaces. Fallan de formas predecibles y son difíciles de
eludir accidentalmente. Los interbloqueos software son flexibles, pero flexibilidad no es lo mismo que seguridad.
Si mueves la seguridad al software, debes aumentar la disciplina de ingeniería en consecuencia:
análisis formal de peligros, requisitos de seguridad explícitos, verificación independiente, arneses de prueba que intenten romper la temporización,
y monitorización en tiempo de ejecución que asuma que el código puede estar equivocado.

En términos de SRE: no borres tus interruptores de circuito porque “la nueva malla de servicios tiene reintentos.”

Lección B: Trata el tiempo y la concurrencia como riesgos de primera clase

Los modos de fallo más notorios del Therac-25 dependen del tiempo. La lección operacional central es que debes probar el sistema
bajo temporización adversaria: entrada rápida, sensores retardados, reordenamiento, actualizaciones parciales, caches obsoletos y periféricos lentos.
Si no puedes modelarlo, haz fuzzing. Si no puedes hacer fuzzing, aíslalo con restricciones duras.

En plataformas modernas: si tu seguridad depende de que “este manejador de eventos no será reentrado,” estás apostando vidas a un comentario.
Usa locks, idempotencia, máquinas de estado con transiciones explícitas e invariantes comprobadas en tiempo de ejecución.

Lección C: La UI es parte del envoltorio de seguridad

Las interfaces deben hacer la acción segura fácil y la acción insegura difícil. Los mensajes de error deben ser accionables. Las alarmas deben graduarse.
Y el sistema no debe presentar “todo está bien” cuando no puede probarlo.

Si tu sistema está inseguro, debería decir “estoy inseguro” y debería predeterminar a un estado seguro. A los ingenieros les disgusta esto porque
es “conservador.” A los pacientes les encanta porque siguen vivos para quejarse de tu diseño conservador.

Lección D: Los postmortems son controles de seguridad, no burocracia

Therac-25 también trata sobre el aprendizaje institucional. Cuando ocurren incidentes, necesitas un proceso de incidentes que extraiga señal:
tono sin culpa, análisis implacable y seguimiento orientado a acciones.
“Reentrenamos a los operadores” no es una solución cuando el sistema permitió desajustes de estado letales. Eso es postureo documental.

Lección E: Reutilizar no es gratis; el código reutilizado hereda nuevas obligaciones

Reutilizar código de sistemas antiguos puede ser inteligente. También puede ser la forma en que introduces por contrabando suposiciones antiguas en un nuevo entorno.
Si Therac-25 reutilizó código de sistemas con interbloqueos hardware, entonces el modelo implícito de seguridad del código cambió.
En términos de fiabilidad: cambiaste el grafo de dependencias pero mantuviste el antiguo SLO.

Cuando reutilizas, debes revalidar el caso de seguridad. Si no puedes escribir el caso de seguridad en lenguaje llano y defenderlo bajo interrogatorio,
no tienes un caso de seguridad.

Tres mini-historias corporativas (anonimizadas, plausibles y desafortunadamente comunes)

Mini-historia 1: La suposición equivocada que tumbó facturación (y casi la confianza)

Una empresa mediana ejecutaba una canalización de eventos “simple”: la API escribe en una cola, los workers procesan, la base de datos guarda resultados.
El sistema había sido estable durante un año, que es como sabes que estaba a punto de convertirse en la personalidad de todos por una semana.

Un ingeniero senior hizo una suposición razonable: los IDs de mensaje eran globalmente únicos. Eran únicos por partición, no globales.
El código usaba el ID de mensaje como clave de idempotencia en un Redis compartido. En carga normal, las colisiones eran raras.
Bajo un pico de tráfico, las colisiones pasaron de “raras” a “suficientemente frecuentes como para ser un bug.”

El resultado fueron descartes silenciosos: los workers creían haber procesado ya un mensaje y lo saltaban. Faltaron eventos de facturación.
Los clientes no fueron sobrecobrados; fueron subcobrados. Suena divertido hasta que finanzas llega con hojas de cálculo y preguntas.
El monitoreo no lo detectó rápido porque las tasas de error se mantuvieron bajas. El sistema estaba “saludable” mientras la corrección ardía.

El postmortem fue franco: la suposición no estaba documentada, no tenía pruebas y no se observaba. Lo arreglaron acotando claves de idempotencia
a partición+offset, añadiendo invariantes y construyendo un job de reconciliación que señalara huecos. También añadieron tráfico canario que
intencionalmente creara colisiones para verificar la corrección en condiciones similares a producción.

La lección de Therac-25 aparece aquí: cuando las suposiciones son erróneas, el sistema no siempre se cae. A veces sonríe y miente.

Mini-historia 2: Una optimización que salió mal (porque “rápido” no es un requisito)

Otra organización tenía un servicio analítico respaldado por almacenamiento. Querían reducir latencia, así que añadieron una caché local en cada nodo
y eliminaron una validación de checksum “redundante” al leer blobs del almacenamiento de objetos. La validación era costosa, argumentaron, y
la capa de almacenamiento “ya maneja la integridad.”

Durante meses pareció genial. Latencia y CPU bajaron, los gráficos subieron. Entonces una actualización de kernel introdujo un raro
problema de corrupción de datos relacionado con DMA en un tipo de instancia. Fue poco común, difícil de reproducir y el object store no era el culpable.
La corrupción ocurría entre memoria y espacio de usuario en las lecturas, y la checksum eliminada era el único punto práctico de detección.

Lo que falló no fue solo la integridad de datos; fue la respuesta a incidentes. El equipo persiguió “consultas malas” y “clientes raros”
durante días porque sus dashboards mostraban éxito. La corrupción fue semántica, no a nivel de transporte.
Finalmente la encontraron comparando resultados entre nodos y notando que uno estaba “creativamente equivocado.”

El rollback fue humillante pero efectivo: reactivar checksums, añadir validación cruzada entre nodos en canarios
y ejecutar jobs de scrub periódicos. La optimización se reintrodujo más tarde con guardarraíles: los checksums se muestrearon a alta tasa
y la validación completa se forzó para datasets críticos.

Therac-25 eliminó capas que hacían más difícil llegar a estados inseguros. Este fue el mismo error, solo con menos funerales y más dashboards.

Mini-historia 3: La práctica aburrida que salvó el día (y nadie fue promovido por ello)

Una plataforma de pagos tenía una práctica que los ingenieros se burlaban como “paranoica”: cada release requería un despliegue escalonado con invariantes automatizados
verificados en cada etapa. No solo tasa de error y latencia—invariantes de negocio: totales, cuentas, reconciliación contra fuentes independientes.

Un viernes por la tarde (porque por supuesto), una nueva release introdujo un sutil bug de doble-aplicación activado por la lógica de reintentos
cuando un servicio aguas abajo hizo timeout. No ocurrió en pruebas de integración porque las pruebas no modelaban timeouts realistas.
Sí ocurrió en la primera célula canaria con tráfico real.

Las invariantes lo detectaron en minutos: los totales del ledger de la célula canaria se desviaron comparados con la célula control.
El despliegue se detuvo automáticamente. Sin outage total. Sin impacto al cliente más allá de unas pocas transacciones que se revirtieron automáticamente antes del settlement.

La corrección fue directa: los tokens de idempotencia pasaron de “mejor esfuerzo” a “requeridos”, y la ruta de reintento se cambió
para verificar el estado del commit antes de reproducir. El punto clave es que el proceso lo detectó, no las gestas heroicas.
Nadie recibió una ovación. Todos se fueron a casa.

Therac-25 no tuvo ese tipo de staging impulsado por invariantes, y el resultado fue daño repetido antes de que el patrón fuera aceptado.
Aburrido es bueno. Aburrido es sobrevivible.

Guía para diagnóstico rápido: qué comprobar primero/segundo/tercero

El desastre operacional de Therac-25 se amplificó por un diagnóstico lento y ambiguo. En sistemas en producción, la velocidad importa—pero no del tipo
“cambia cosas al azar rápido.” Del tipo “establecer el modo de fallo rápido.”

Primero: decide si el sistema está mintiendo o fallando ruidosamente

  • Busca discrepancias entre señales “saludables” y daño reportado por usuarios. Si los usuarios ven corrupción, resultados erróneos o comportamiento inseguro, trata los dashboards como no confiables.
  • Comprueba invariantes. Recuentos, totales, transiciones de estado, interbloqueos de seguridad. Si no tienes invariantes, felicidades: tienes sensaciones.
  • Confirma el radio de daño. ¿Un nodo? ¿Una región? ¿Un dispositivo? ¿Un flujo de trabajo de operador?

Segundo: aísla concurrencia, estado y temporización

  • Sospecha condiciones de carrera cuando: los síntomas son intermitentes, se disparan por velocidad o desaparecen cuando añades logging.
  • Forza la serialización temporalmente (worker único, desactivar paralelismo, bloquear la sección crítica) para ver si el problema desaparece.
  • Captura el orden de eventos con marcas temporales e IDs de correlación. Si no puedes reconstruir el orden, no puedes razonar sobre seguridad.

Tercero: verifica el envoltorio de seguridad y el comportamiento fail-safe

  • Prueba la transición a “estado seguro”. ¿El sistema se detiene cuando está inseguro, o sigue funcionando?
  • Revisa interbloqueos: hardware, software, gating por configuración, feature flags, circuit breakers.
  • Confirma la guía al operador: ¿los mensajes de error inducen la acción correcta, o entrenan reintentos?

Broma #2: Si tu runbook de incidentes dice “reinicia y observa,” eso no es un runbook — es una ouija con mejor uptime.

Tareas prácticas: comandos, salidas y la decisión que tomas a partir de ellas

Therac-25 no falló porque alguien olvidara un comando de Linux. Pero el tema operacional —evidencia insuficiente, diagnóstico lento
e invariantes ausentes— aparece en todas partes. A continuación hay tareas concretas que puedes ejecutar en sistemas reales para atrapar los equivalentes modernos:
condiciones de carrera, transiciones de estado inseguras, “saludable pero equivocado” y la eliminación de capas de seguridad.

El patrón para cada tarea es: comando → salida típica → qué significa → qué decisión tomas.

Tarea 1: Confirma la sincronización horaria (el orden importa)

cr0x@server:~$ timedatectl status
               Local time: Mon 2026-01-22 10:14:07 UTC
           Universal time: Mon 2026-01-22 10:14:07 UTC
                 RTC time: Mon 2026-01-22 10:14:06
                Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

Qué significa: “System clock synchronized: yes” reduce la probabilidad de que los logs mientan sobre el orden de eventos.

Decisión: Si no está sincronizado, arregla NTP/chrony antes de confiar en líneas de tiempo multi-nodo.

Tarea 2: Busca problemas a nivel de kernel que puedan imitar “corrupción aleatoria”

cr0x@server:~$ dmesg -T | tail -n 12
[Mon Jan 22 10:11:41 2026] nvme nvme0: I/O 42 QID 3 timeout, aborting
[Mon Jan 22 10:11:41 2026] nvme nvme0: Abort status: 0x371
[Mon Jan 22 10:11:43 2026] EXT4-fs (nvme0n1p2): I/O error while writing superblock
[Mon Jan 22 10:11:44 2026] blk_update_request: I/O error, dev nvme0n1, sector 91827364 op 0x1:(WRITE) flags 0x0 phys_seg 1 prio class 0

Qué significa: Timeouts de almacenamiento y errores de sistema de archivos pueden producir incorrecciones silenciosas aguas arriba.

Decisión: Deja de culpar al código de aplicación hasta confirmar la estabilidad de hardware/controlador; drena el nodo si los errores persisten.

Tarea 3: Establece si hay saturación de CPU vs. latencia por otro lado

cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.8.0 (prod-app-17) 	01/22/2026 	_x86_64_	(32 CPU)

10:14:22 AM  CPU   %usr %nice %sys %iowait %irq %soft %steal %idle
10:14:23 AM  all   18.2  0.0   6.1   22.7    0.0  0.5    0.0   52.5
10:14:23 AM   7   88.9  0.0   9.1    0.0    0.0  0.0    0.0    2.0

Qué significa: Alto %iowait sugiere que las CPUs están ociosas esperando I/O; una CPU caliente indica contención por lock o cuello de botella mono-hilo.

Decisión: Si iowait es alto, pivotar a verificaciones de almacenamiento/red; si una CPU está saturada, sospecha puntos de serialización y contención de mutex.

Tarea 4: Comprueba si la máquina está intercambiando memoria (los bugs de temporización adoran la presión)

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  1      0  31248  41216 829320    0    0  1024  2048  980 2100 17  6 55 22  0
 6  3      0  28992  40128 821004    0    0  4096  8192 1200 8900 25  8 40 27  0

Qué significa: Altos switches de contexto (cs) más procesos bloqueados crecientes (b) suelen indicar contención o stalls de I/O.

Decisión: Si la presión es alta, reduce la concurrencia y vuelve a probar; las fallas dependientes del tiempo suelen desaparecer cuando se reduce la carga, lo cual es diagnóstico.

Tarea 5: Confirma la salud del sistema de archivos y las opciones de montaje (los rieles de seguridad importan)

cr0x@server:~$ mount | grep " /var "
/dev/nvme0n1p2 on /var type ext4 (rw,relatime,errors=remount-ro)

Qué significa: errors=remount-ro es un fail-safe: el sistema de archivos pasará a solo lectura ante errores serios en lugar de corromper silenciosamente.

Decisión: Prefiere montajes con fail-safe para estado crítico; si ves opciones permisivas, arréglalas antes de necesitarlas.

Tarea 6: Revisa degradación de RAID / mdadm (la trampa de “está bien”)

cr0x@server:~$ cat /proc/mdstat
Personalities : [raid1]
md0 : active raid1 sda1[0] sdb1[1]
      104320 blocks super 1.2 [2/1] [U_]
      [====>................]  recovery = 23.4% (24448/104320) finish=2.1min speed=64000K/sec

Qué significa: [2/1] [U_] indica un mirror degradado. Estás a un disco de un día muy malo.

Decisión: Trata la redundancia degradada como un incidente; pausa cambios riesgosos, reemplaza el disco fallado y verifica la reconstrucción.

Tarea 7: Valida el estado del pool ZFS (scrub, errores, corrupción silenciosa)

cr0x@server:~$ sudo zpool status -v tank
  pool: tank
 state: DEGRADED
status: One or more devices has experienced an error resulting in data corruption.
action: Restore the file in question if possible.
  scan: scrub repaired 0B in 00:12:44 with 1 errors on Mon Jan 22 09:58:13 2026
config:

	NAME        STATE     READ WRITE CKSUM
	tank        DEGRADED     0     0     0
	  mirror-0  DEGRADED     0     0     0
	    sdc     ONLINE       0     0     0
	    sdd     ONLINE       0     0     3

errors: Permanent errors have been detected in the following files:
/tank/db/segments/00000042.log

Qué significa: ZFS te está diciendo que detectó errores de checksum y puede señalar archivos afectados.

Decisión: Restaura los datos impactados desde réplicas/backups; no “monitorees y veas.” Si el almacenamiento indica corrupción, créelo.

Tarea 8: Detecta retransmisiones TCP y pérdida de paquetes (el atacante invisible de temporización)

cr0x@server:~$ ss -s
Total: 1532
TCP:   812 (estab 214, closed 539, orphaned 0, timewait 411)

Transport Total     IP        IPv6
RAW	  0         0         0
UDP	  29        21        8
TCP	  273       232       41
INET	  302      253       49
FRAG	  0         0         0

Qué significa: Esta es una instantánea gruesa; no muestra retransmisiones directamente, pero ayuda a detectar churn de conexiones y tormentas timewait.

Decisión: Si estab es bajo pero timewait es enorme, sospecha patrones agresivos de reconexión; pasa a estadísticas de red más profundas.

Tarea 9: Revisa errores y drops en la interfaz

cr0x@server:~$ ip -s link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:ab:cd:ef brd ff:ff:ff:ff:ff:ff
    RX:  bytes packets errors dropped  missed   mcast
    9876543210 8123456      0   12456       0   12034
    TX:  bytes packets errors dropped carrier collsns
    8765432109 7123456      0     342       0       0

Qué significa: Los drops pueden manifestarse como reintentos, timeouts y reordenamientos—combustible perfecto para condiciones de carrera y actualizaciones parciales.

Decisión: Si los drops suben, investiga colas NIC, desajustes MTU, congestión y vecinos ruidosos; no ajustes reintentos de app primero.

Tarea 10: Confirma presión de descriptores de archivo a nivel de proceso (puede causar fallos extraños)

cr0x@server:~$ cat /proc/sys/fs/file-nr
42352	0	9223372036854775807

Qué significa: El primer número son handles de archivo asignados; si crece cerca de límites del sistema, tendrás fallos que parecen “aleatorios.”

Decisión: Si la presión es alta, localiza procesos con fugas y arregla; también establece límites sensatos y alerta sobre la tasa de crecimiento.

Tarea 11: Comprueba si un servicio se está reiniciando (flapping oculta causas raíz)

cr0x@server:~$ systemctl status api.service --no-pager
● api.service - Example API
     Loaded: loaded (/etc/systemd/system/api.service; enabled; vendor preset: enabled)
     Active: active (running) since Mon 2026-01-22 10:02:18 UTC; 11min ago
   Main PID: 2187 (api)
      Tasks: 34 (limit: 38241)
     Memory: 512.4M
        CPU: 2min 12.103s
     CGroup: /system.slice/api.service
             └─2187 /usr/local/bin/api --config /etc/api/config.yaml

Qué significa: Si Active sigue reiniciándose, estás en territorio de crash-loop; si el uptime es estable, enfócate en otro lado.

Decisión: Si hay flapping, detén la hemorragia: congela despliegues, reduce tráfico y captura core dumps/logs antes de que el reinicio borre evidencia.

Tarea 12: Consulta journald alrededor de una ventana de incidente sospechada (consigue la secuencia)

cr0x@server:~$ journalctl -u api.service --since "2026-01-22 09:55" --until "2026-01-22 10:10" --no-pager | tail -n 8
Jan 22 10:01:02 prod-app-17 api[2187]: WARN request_id=9b3e retrying upstream due to timeout
Jan 22 10:01:02 prod-app-17 api[2187]: WARN request_id=9b3e retry attempt=2
Jan 22 10:01:03 prod-app-17 api[2187]: ERROR request_id=9b3e upstream commit status unknown
Jan 22 10:01:03 prod-app-17 api[2187]: WARN request_id=9b3e applying fallback path
Jan 22 10:01:04 prod-app-17 api[2187]: INFO request_id=9b3e response_status=200

Qué significa: “Commit status unknown” seguido de “fallback path” es una bandera roja de corrección: podrías tener doble-aplicación o estado parcial.

Decisión: Añade verificación de idempotencia antes de reintentos; si es crítico para la seguridad, falla cerrado en vez de “fallback y esperar.”

Tarea 13: Inspecciona conexiones TCP abiertas a una dependencia (encuentra puntos calientes)

cr0x@server:~$ ss -antp | grep ":5432" | head
ESTAB 0 0 10.20.5.17:48422 10.20.9.10:5432 users:(("api",pid=2187,fd=41))
ESTAB 0 0 10.20.5.17:48424 10.20.9.10:5432 users:(("api",pid=2187,fd=42))
ESTAB 0 0 10.20.5.17:48426 10.20.9.10:5432 users:(("api",pid=2187,fd=43))

Qué significa: Confirma que el servicio habla con Postgres y no está atascado en otro lado. También sugiere comportamiento del pool de conexiones.

Decisión: Si las conexiones explotan, limita pools y añade backpressure; las tormentas de conexiones son el equivalente software a quitar un interbloqueo.

Tarea 14: Valida locks en la base de datos (los cuellos de botella de concurrencia parecen “lentitud aleatoria”)

cr0x@server:~$ psql -h 10.20.9.10 -U app -d appdb -c "select wait_event_type, wait_event, count(*) from pg_stat_activity where wait_event is not null group by 1,2 order by 3 desc;"
 wait_event_type |     wait_event      | count
-----------------+---------------------+-------
 Lock            | relation            |    12
 IO              | DataFileRead        |     4
 Client          | ClientRead          |     2
(3 rows)

Qué significa: Muchos locks de relación indican contención; tu equivalente de “operador rápido” podría ser una tabla caliente.

Decisión: Si las esperas por locks dominan, identifica consultas bloqueantes y arregla esquema/índices o el alcance de transacciones; no solo escales nodos de aplicación.

Tarea 15: Valida configuraciones de checksum/verificación para replicación de almacenamiento (chequeo del cinturón de seguridad)

cr0x@server:~$ rbd info rbd/patient-images
rbd image 'patient-images':
	size 2 TiB in 524288 objects
	order 22 (4 MiB objects)
	snapshot_count: 2
	id: 1a2b3c4d5e6f
	block_name_prefix: rbd_data.1a2b3c4d5e6f
	format: 2
	features: layering, exclusive-lock, object-map, fast-diff, deep-flatten
	op_features:
	flags:
	create_timestamp: Mon Jan 15 08:21:11 2026

Qué significa: No es una salida de checksum en sí, pero confirma características del objeto; aún necesitas comprobaciones de integridad end-to-end en la capa de aplicación.

Decisión: Si la corrección del sistema depende de integridad, añade checksums/hashes explícitos en los límites; no externalices la verdad a una sola capa.

Tarea 16: Confirma si un proceso es mono-hilo o está bloqueado en un lock

cr0x@server:~$ top -H -p 2187 -b -n 1 | head -n 12
top - 10:14:55 up 12 days,  3:21,  1 user,  load average: 6.12, 4.98, 4.77
Threads:  36 total,   1 running,  35 sleeping
%Cpu(s): 23.1 us,  7.2 sy,  0.0 ni, 47.3 id, 22.4 wa,  0.0 hi,  0.0 si,  0.0 st
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   2199 api       20   0 1452784 524200  49200 R  88.7   1.6   0:31.22 api
   2201 api       20   0 1452784 524200  49200 S   2.3   1.6   0:02.10 api

Qué significa: Un hilo domina la CPU; los demás duermen. Eso suele ser un lock, un bucle apretado o un camino crítico mono-hilo.

Decisión: Perfila ese camino; si protege estado compartido, rediseña hacia estado inmutable o máquinas de estado explícitas con transiciones seguras.

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

1) Síntoma: “El sistema dice OK, pero los usuarios reportan resultados erróneos”

Causa raíz: Los health checks miden vivacidad (proceso arriba) en lugar de corrección (invariantes). Therac-25 efectivamente “pasaba” sus propias comprobaciones mientras era inseguro.

Solución: Añade comprobaciones de corrección: validaciones cruzadas, reconciliaciones e dashboards de invariantes. Gatea despliegues por invariantes, no solo por 200s.

2) Síntoma: Fallos intermitentes disparados por velocidad, reintentos o carga alta

Causa raíz: Condiciones de carrera y transiciones de estado no atómicas. El equivalente de “operador rápido” son solicitudes paralelas o eventos reordenados.

Solución: Modela el estado como una máquina de estados finita, aplica transiciones atómicas, añade claves de idempotencia y haz fuzzing de temporización con tests de caos.

3) Síntoma: Operadores reintentan repetidamente frente a errores

Causa raíz: La UI/UX entrena comportamiento inseguro; las alarmas son vagas; el sistema permite continuar sin prueba de seguridad.

Solución: Haz imposible la continuación insegura. Usa gravedad explícita y acciones obligatorias. Si la seguridad es incierta, falla cerrado.

4) Síntoma: La investigación post-incidente no puede reconstruir lo ocurrido

Causa raíz: Logging insuficiente, falta de IDs de correlación, ausencia de marcas temporales o logs sobreescritos en reinicios.

Solución: Logs estructurados con IDs de correlación, almacenamiento inmutable append-only para trazas de auditoría y snapshots de estado capturados al fallar.

5) Síntoma: Las soluciones son “reentrenar usuarios” y “tener más cuidado”

Causa raíz: La organización está sustituyendo controles de ingeniería por políticas porque los controles de ingeniería son más difíciles.

Solución: Añade rieles de seguridad duros: interbloqueos, transiciones con permisos, comprobaciones en tiempo de ejecución y verificación independiente. La formación es complementaria, no primaria.

6) Síntoma: Eliminar comprobaciones “redundantes” hace las cosas más rápidas… hasta que no

Causa raíz: La optimización eliminó capas de detección/corrección (checksums, validación, lecturas de confirmación), convirtiendo fallos raros en corrupción silenciosa.

Solución: Mantén validación end-to-end; si debes optimizar, muestrea de forma inteligente y añade verificación canaria. Nunca optimices fuera la única alarma.

7) Síntoma: Incidentes tratados como anomalías aisladas, no como patrones

Causa raíz: Gestión de incidentes débil: sin seguimiento central, sin escalado forzado, incentivos para minimizar la gravedad.

Solución: Respuesta formal a incidentes con clasificación de severidad, agregación cross-site y autoridad de “parar la línea” cuando la seguridad/corrección esté en riesgo.

Listas de verificación / plan paso a paso

Paso a paso: construir rieles de seguridad “a prueba de Therac” en sistemas guiados por software

  1. Escribe la lista de peligros como si lo sintieras de verdad.
    Enumera estados inseguros, no solo fallos. Ejemplo: “modo de alta energía con configuración física errónea” mapea a “acción privilegiada aplicada con config obsoleta.”
  2. Define invariantes.
    ¿Qué debe ser siempre cierto? Ejemplo: “La energía y el modo del haz deben coincidir con el estado mecánico verificado” mapea a “la ruta de escritura debe ser consistente con configuración versionada.”
  3. Haz cumplir máquinas de estado.
    Cada transición debe ser explícita. No “editar en sitio” parámetros críticos de seguridad sin versionado y commit atómico.
  4. Fallar cerrado ante la incertidumbre.
    Si los sensores discrepan o el estado del commit es desconocido, detente y escala. Tu sistema debe preferir tiempo de inactividad sobre errores cuando pueda haber daño.
  5. Mantén interbloqueos por capas.
    Usa hardware cuando sea factible, pero también gating por software, control de acceso y aserciones en tiempo de ejecución. No elimines ninguno sin reemplazar por evidencia más fuerte.
  6. Diseña la UI para la escalada.
    Errores en lenguaje llano, niveles de gravedad y acciones seguras obligatorias. No permitas que “Enter” sea un camino hacia el peligro.
  7. Prueba con temporización adversaria.
    Fuzzing de velocidad de entrada, reordenamiento de eventos, inyección de latencias y simulación de fallos parciales. Las pruebas de “uso normal” prueban casi nada sobre seguridad.
  8. Verificación y validación independiente.
    Incentivos separados. El equipo que envía características no debería ser el único equipo que firma el comportamiento crítico para la seguridad.
  9. Instrumenta para forense.
    Captura orden de eventos, snapshots de estado y logs de auditoría. Haz que “¿qué pasó?” sea respondible en menos de una hora.
  10. Respuesta a incidentes con autoridad de detener la línea.
    Un informe creíble de comportamiento inseguro desencadena contención. No debates por email mientras el sistema sigue operando.
  11. Practica la falla.
    Realiza game days centrados en corrección y seguridad, no solo en disponibilidad. Incluye a los operadores en el ejercicio; te enseñarán dónde la UI miente.
  12. Rastrea señales débiles recurrentes.
    “Código de error raro pero funcionó tras reintento” no es ticket cerrado. Es un precursor.

Lista de verificación de release para cambios críticos de seguridad o corrección

  • ¿Este cambio elimina alguna validación, checksum, lock o interbloqueo? Si sí: ¿qué lo reemplaza y qué evidencia prueba la equivalencia?
  • ¿Las transiciones de estado están versionadas y son atómicas? Si no, estás enviando una condición de carrera con pasos extra.
  • ¿Se miden las invariantes y se usan como gate en canary/staging?
  • ¿Los mensajes de error indican al operador qué hacer, no solo qué pasó?
  • ¿Podemos reconstruir el orden de eventos entre componentes en menos de 60 minutos usando logs y marcas temporales?
  • ¿Está documentada la autoridad de “parar la línea” para on-call y operadores?

Preguntas frecuentes

1) ¿Therac-25 fue “solo un bug”?

No. Hubo bugs, incluidos dependientes del tiempo, pero el desenlace letal requirió un diseño del sistema que permitió estados inseguros,
interbloqueos débiles, diagnósticos pobres y una postura de respuesta a incidentes que no trató señales tempranas como emergencias.

2) ¿Por qué las condiciones de carrera aparecen tanto en historias de seguridad?

Porque humanos y computadoras crean temporizaciones impredecibles. La concurrencia genera estados que no pretendías, y las pruebas rara vez cubren
esos estados a menos que ataques intencionalmente la temporización con fuzzing, inyección de fallos y modelado explícito de máquinas de estado.

3) ¿No es el hardware más seguro que el software?

El hardware puede ser más seguro para ciertas garantías porque es más difícil de eludir y más sencillo de razonar ante fallos.
Pero el hardware también puede fallar. La respuesta real son controles por capas: interbloqueos hardware donde sea práctico, más aserciones de software,
monitorización y comportamiento fail-safe cuando algo no cuadra.

4) ¿Cuál es el equivalente moderno de Therac-25 fuera del ámbito médico?

Cualquier sistema donde el software controle una acción de alta energía o alto impacto: automatización industrial, vehículos autónomos,
trading financiero, identidad y control de acceso, y orquestación de infraestructura. Si el software puede dañar irreversiblemente
a personas o violar la confianza, estás en el mismo género.

5) ¿Por qué son tan importantes los “mensajes de error crípticos”?

Porque los operadores actúan racionalmente bajo presión. Si un error es vago y el sistema permite continuar,
los reintentos se vuelven por defecto. Un buen mensaje de seguridad es un control: dicta la acción segura y evita anulaciones casuales.

6) ¿Cómo se prueba para secuencias de “operador rápido” en apps modernas?

Usa fuzzing y pruebas basadas en modelos alrededor de transiciones de estado. Simula ediciones rápidas, reintentos, retardos de red y reordenamientos.
En sistemas distribuidos, inyecta latencia y pérdida de paquetes; en UIs, automatiza interacciones de alta velocidad y verifica invariantes en cada paso.

7) ¿Cómo debe ser la respuesta a incidentes cuando está en riesgo la corrección (no la disponibilidad)?

Trátalo como un evento de severidad. Contén primero: pausa despliegues, reduce tráfico, aísla nodos sospechosos y preserva evidencia.
Luego verifica invariantes y reconcilia datos. Solo después de que puedas explicar el modo de fallo deberías reanudar la operación normal.

8) ¿Es compatible un postmortem sin culpa con la rendición de cuentas?

Sí. Sin culpa significa no escurrir responsabilidades personales por problemas sistémicos. Rendición de cuentas significa arreglar el sistema y dar seguimiento.
Si tus postmortems terminan con “error humano”, estás evitando la rendición de cuentas, no aplicándola.

9) ¿Cuál es la lección más accionable de Therac-25 para ingenieros?

No permitas estados inseguros. Codifica invariantes y aplícalas en tiempo de ejecución. Si el sistema no puede probar que es seguro, debe detenerse.

10) ¿Qué pasa si la presión del negocio exige eliminar comprobaciones “lentas”?

Entonces tratas la comprobación como un control de seguridad y requieres un caso de seguridad para su eliminación: qué lo reemplaza, cómo se monitorizará
y qué nuevos modos de fallo introduce. Si nadie puede escribir eso, la respuesta es no.

Conclusión: siguientes pasos que realmente reducen el riesgo

Therac-25 es un caso de estudio sobre cómo los sistemas matan: no con un fallo dramático único, sino con una cadena de decisiones pequeñas y justificables
que eliminaron fricción para resultados inseguros. También es un caso de estudio sobre cómo fallan las organizaciones: tratando incidentes tempranos como ruido,
confiando en indicadores “saludables” por encima de informes humanos y enviando sistemas que no pueden explicarse cuando algo sale mal.

Pasos prácticos siguientes, en orden de impacto:

  1. Define y monitorea invariantes para corrección y seguridad, y gatea los releases con ellas.
  2. Modela transiciones de estado explícitamente y elimina flujos de “editar en sitio” para parámetros críticos de seguridad.
  3. Reintroduce o añade interbloqueos: hardware cuando sea posible, aserciones de software en todas partes y “fallar cerrado” ante la incertidumbre.
  4. Mejora la observabilidad para forense: marcas temporales, IDs de correlación, logs inmutables y snapshots de estado al fallar.
  5. Ejecuta pruebas adversarias de temporización: fuzzing, inyección de fallos y game days centrados en corrección, no solo en disponibilidad.
  6. Arregla la interfaz del operador para que enseñe comportamiento seguro: gravedad clara, acciones explícitas, sin reintentos casuales en peligro.

Si construyes sistemas que pueden dañar personas—física, financiera o socialmente—trata la seguridad como una propiedad que debes probar continuamente.
El día que empieces a confiar en “debería estar bien” es el día que empiezas a escribir tu propio capítulo de Therac-25.

← Anterior
Docker “bind: address already in use”: encuentra el proceso y corrige limpiamente
Siguiente →
Heartbleed: el fallo que demostró que Internet funciona con cinta adhesiva

Deja un comentario