gioser

sergio/takana

> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…

TOML 1572Shell 203Rust 109Markdown 993728 Definiciones

⎇ main scripts/diagnostico/cazar-cuelgue-headless.sh Shell · 166 líneas · 11.8 KB Historia crudo

takana: la ruta del repo es /mnt/vvv/takana 17f8f4fb8 · Sergio ·

1#!/bin/bash
2# cazar-cuelgue-headless.sh — cazador de un cuelgue INTERMITENTE de firefox en headless.
3#
4# ══ EL CASO, Y POR QUÉ ESTE SCRIPT SOBREVIVE AUNQUE NO LO RESOLVIÓ ═════════════════════════════
5# El 2026-09-08, dos corridas del banco de PGO se colgaron: 120.034 y 120.030 ms, o sea EXACTAMENTE
6# el `timeout 120` del arnés. No eran ruido de carga —una corrida normal son ~20 s— sino bloqueos
7# hasta que las maté. Dos de 56 (~3,5 %).
8#
9# NO SE REPRODUJO. 202 corridas en cuatro condiciones, CERO cuelgues:
10#
11#   un binario, un sandbox compartido ............... 30 · 0
12#   4 binarios alternando, un sandbox ............... 60 · 0   (descarta churn de caché de página)
13#   4 binarios, UN SANDBOX NUEVO POR CORRIDA ........ 56 · 0   (descarta el montaje de namespaces)
14#   ídem, con LAS PÁGINAS EXACTAS del banco ......... 56 · 0   (descarta la página)
15#
16# Si la tasa fuera 3,5 %, ver cero en 202 tendría probabilidad ~0,06 %. O sea que la tasa real bajo
17# estas condiciones NO es ésa, y lo que faltaba estaba fuera de ellas.
18#
19# ⚠ UN ERROR DE MÉTODO QUE COSTÓ 146 CORRIDAS, ANOTADO PARA NO REPETIRLO: las tres primeras cazas
20# usaron `flex.html` y `tablas.html` porque las tenía a mano, cuando las colgadas habían sido en
21# `fuera-del-corpus.html` y `del-corpus.html`. Estuve probando una condición que NO era la
22# observada, creyendo que sí. Antes de concluir «no se reproduce», comprobar que el instrumento
23# reproduce lo que dice reproducir.
24#
25# LO QUE SÍ SE APRENDIÓ, que acota al culpable:
26#   · Es TODO O NADA. En 202 corridas ninguna pasó siquiera de 45 s: o terminan en ~20 s o se
27#     bloquean hasta el timeout. No es lentitud ocasional, es un bloqueo.
28#   · Sobrevive la hipótesis que no se puede probar retroactivamente: contención transitoria de otra
29#     cosa en la máquina (es compartida con otros agentes) durante esas dos ventanas.
30#
31# ⚠ EVIDENCIA A FAVOR DE ESA HIPÓTESIS, VISTA DESPUÉS (2026-09-08): al ir a montar OTRO banco, la
32#   carga estaba en 3,34 (contra 0,13 de las cazas) porque otro agente había arrancado un `cargo`,
33#   y `kswapd0` aparecía consumiendo CPU con CERO memoria libre — o sea recuperación de páginas
34#   bajo presión. Cada corrida mapea un `libxul` de 227 MB; con la caché apretada, eso es
35#   exactamente la condición que las cuatro cazas NO tenían y que los dos bancos con cuelgues SÍ.
36#   No es prueba —no se puede reconstruir el estado de aquellas dos ventanas— pero es la primera
37#   evidencia que apunta a algo concreto: **cazar bajo presión de memoria, no con la máquina
38#   ociosa**. Se puede forzar corriendo el cazador mientras algo grande compila.
39#
40# ╔══════════════════════════════════════════════════════════════════════════════════════════════╗
41# ║ RESUELTO 2026-09-08 — la causa NO era la presión de memoria, y el cuelgue era la punta       ║
42# ║ visible de un defecto que estaba en el 100 % de las corridas sin que nadie lo viera.         ║
43# ╚══════════════════════════════════════════════════════════════════════════════════════════════╝
44#
45# LO QUE SE REPRODUJO Y LO QUE SE REFUTÓ (ver cazar-cuelgue-bajo-presion.sh para el arnés):
46#
47#   nivel de presión   disponible   PSI    ok   COLGADAS
48#   control (sin hog)    4402 MiB   0,02   20      0
49#   2000                 1944 MiB   0,00   18      2      ← los dos cuelgues, con PSI CERO
50#   1200                 1184 MiB   0,54   20      0
51#    700                  673 MiB   2,31   20      0
52#
53# Reproducido por primera vez en 282 corridas (2 de 80 = 2,5 %, compatible con el 3,5 % original),
54# PERO SIN DOSIS-RESPUESTA: los cuelgues cayeron en el nivel con MENOS presión medida y los dos
55# niveles realmente apretados dieron cero. La presión no es la causa. Y dos hipótesis más, muertas
56# con su medición al lado:
57#   · OOM killer se lleva al hijo → REFUTADA: `oom_kill` de /proc/vmstat no se movió (delta=0) en
58#     ninguna de las dos colgadas, y los `Web Content` estaban VIVOS.
59#   · el fork server de Gecko → REFUTADA con A/B del pref `dom.ipc.forkserver.enable`: con el pref
60#     en false el proceso `forkserver` desaparece del censo (35 muestras → 0, o sea que el pref SÍ
61#     hizo efecto) y siguen los mismos 2 segfaults.
62#
63# LA CAUSA, con la cadena entera medida. El log de una corrida COLGADA contra el de una BUENA se
64# separan perfectamente (6 buenas: 0 · 2 colgadas: 7 fallos de lanzamiento de pestaña + 6
65# `messageManager is null`). Tirando de ahí apareció lo importante: **en las 6 corridas BUENAS
66# también revientan 2 procesos hijos con SIGSEGV**, siempre después de escribir el PNG, o sea sin
67# que nadie lo note. Muestreando /proc dentro del sandbox, esos dos se llaman `Sandbox Forked`: son
68# los ayudantes que el sandbox PROPIO de Firefox forkea para montar su user-namespace. Y no puede:
69# ya estamos dentro del user-namespace de bwrap, así que falla con `writing /proc/self/uid_map:
70# EPERM` — y el ayudante muere en la salida de error.
71#
72# Las tres cantidades se mueven JUNTAS, que es la forma de una cadena causal y no de una coincidencia:
73#
74#   configuración                          segv   screenshot   uid_map:EPERM   'Sandbox Forked'
75#   sandbox de Firefox COMPLETO              7        NO             9              725
76#   security.sandbox.content.level=0         2        sí             2               68
77#   los 3 prefs .level a 0                   1        sí             1               33
78#   las 5 MOZ_DISABLE_*_SANDBOX              0        sí             0                0
79#
80# ⚠ Y el sandbox completo CUELGA DETERMINISTA: rc=124 a los 180 s, sin PNG. Comparte familia con el
81#   cuelgue intermitente (espera infinita, sin screenshot, ayudantes reventados) pero **no está
82#   probado que sean el mismo**: al determinista le faltan los mensajes `Failed to launch`. Lo que
83#   sí está probado es que el arnés viejo dejaba 2 ayudantes reventando en CADA corrida y que
84#   apagando los cinco sandboxes no queda ninguno.
85#
86# LA VERIFICACIÓN A/B (50 pares intercalados bajo presión, verificar-arreglo-sandbox.sh):
87#
88#   arnés viejo (sólo CONTENT apagado) ...... 98 segfaults · 1 COLGADA · 49/50 screenshots
89#   arnés nuevo (los cinco apagados) ........  0 segfaults · 0 colgadas · 50/50 screenshots
90#
91# ⚠ QUÉ PRUEBA Y QUÉ NO, porque la tentación de leer de más acá es grande:
92#   · El MECANISMO está eliminado y eso sí es concluyente: 98 → 0, exactamente 2 por corrida, en el
93#     100 % de las corridas. Un efecto determinista se refuta con pocas muestras.
94#   · El CUELGUE no. 1 contra 0 en 50 pares no es significativo por sí solo: con la tasa medida
95#     (3 colgadas en 130 corridas del arnés viejo ≈ 2,3 %), ver cero en 50 corridas tiene ~31 % de
96#     probabilidad AUNQUE NADA HUBIERA CAMBIADO. Para un negativo convincente harían falta ~200.
97#   Lo que sí se puede decir: las TRES colgadas observadas tienen la misma huella exacta —7 fallos
98#   de lanzamiento de pestaña, 1 de rdd, 6 `messageManager is null`, cero screenshot— y las tres
99#   cayeron en configuraciones donde los ayudantes revientan. Ninguna apareció sin el mecanismo.
100#   Evidencia en docs/evidencia/cuelgue-headless/.
101#
102# EL ARREGLO, para cualquier firefox headless de la distro dentro de bwrap — el sandbox de Gecko no
103# aporta nada acá, porque bwrap YA es la jaula, y lo único que hace es fallar y dejar cadáveres:
104#
105#   MOZ_DISABLE_CONTENT_SANDBOX=1 MOZ_DISABLE_GMP_SANDBOX=1 MOZ_DISABLE_RDD_SANDBOX=1 \
106#   MOZ_DISABLE_SOCKET_PROCESS_SANDBOX=1 MOZ_DISABLE_UTILITY_SANDBOX=1
107#
108# ⚠ LECCIÓN DE MÉTODO, la segunda del día con el mismo disfraz: el A/B del fork server dio "0 segv"
109#   en las dos ramas y parecía un resultado. No lo era — el arnés estaba roto (`bwrap: Can't create
110#   file at /user.js: Read-only file system`, el punto de montaje tiene que EXISTIR en un root de
111#   sólo lectura) y NINGUNA rama sacaba screenshot. Lo delató la rama de control, que también dio
112#   cero cuando tenía que dar dos. **Un instrumento muerto y un experimento exitoso se ven idénticos
113#   desde afuera; sólo los distingue un control que TIENE que dar distinto.**
114#
115# ══ PARA QUÉ SIRVE ENTONCES ════════════════════════════════════════════════════════════════════
116# Para que la PRÓXIMA vez que aparezca, se capture en el acto en vez de empezar de cero: `wchan`,
117# `syscall` y `state` de cada proceso (distinguen espera de disco de espera de lock), memoria y
118# carga del host en ese instante, y el log del navegador de esa corrida —que en las buenas se tira—.
119#
120# Uso:  N=56 UMBRAL=45 scripts/diagnostico/cazar-cuelgue-headless.sh
121#       (necesita el rootfs hidratado en store/.rootfs/sway-v2 y las páginas en el scratchpad;
122#        ajustar las rutas de arriba si se mueven)
123set -u
124RAIZ=/mnt/vvv/takana; R=$RAIZ/store/.rootfs/sway-v2
125S=/tmp/claude-1001/-mnt-vvv-hammer/9d639cb7-92ff-4a26-bd7f-ec9988b972cf/scratchpad
126N=${N:-56}; UMBRAL=${UMBRAL:-45}
127FF=($(ls -d $RAIZ/store/8116bdec*-firefox $RAIZ/store/1d730334*-firefox \
128        $RAIZ/store/3d199174*-firefox $RAIZ/store/bb33fd56*-firefox))
129PAGS=(fuera-del-corpus.html del-corpus.html)   # LAS DEL BANCO: es donde ocurrieron las 2 colgadas
130colgadas=0
131for i in $(seq 1 $N); do
132  art=${FF[$(( i % 4 ))]}; pag=${PAGS[$(( i % 2 ))]}
133  # ⚠ `setsid` + matar el GRUPO, no el bwrap: `kill $BW` mata el bwrap EXTERIOR, que no es el init
134  # del namespace de PID, y el firefox de dentro SOBREVIVE. Si el arnés corre bajo `flock`, ese
135  # superviviente hereda el fd del lock y deja a la granja sin compilar hasta que alguien lo note
136  # (pasó dos veces el 2026-09-08, hora y media la primera). Medido: matando sólo el bwrap queda
137  # 1 firefox vivo; matando el grupo, 0.
138  setsid bwrap --ro-bind "$R" / --ro-bind /usr/lib/musl/lib/libc.so /lib/ld-musl-x86_64.so.1 \
139    --ro-bind "$art/usr/lib/firefox" /usr/lib/firefox \
140    --dev /dev --proc /proc --tmpfs /run --tmpfs /tmp --unshare-pid \
141    --ro-bind "$S/bench" /bench --bind "$S/caza4" /salida --unshare-user --uid 0 --gid 0 \
142    --setenv PATH /usr/bin:/bin \
143    /bin/sh -c 'export HOME=/tmp/h MOZ_HEADLESS=1 MOZ_DISABLE_CONTENT_SANDBOX=1 LIBGL_ALWAYS_SOFTWARE=1 LANG=C.UTF-8; mkdir -p $HOME
144      LD_LIBRARY_PATH=/usr/lib/firefox /usr/lib/firefox/firefox --headless --screenshot /salida/x.png "file:///bench/'"$pag"'"' \
145    >/dev/null 2>&1 &
146  BW=$!
147  t=0
148  while kill -0 $BW 2>/dev/null && [ $t -lt $UMBRAL ]; do sleep 1; t=$((t+1)); done
149  if kill -0 $BW 2>/dev/null; then
150    colgadas=$((colgadas+1))
151    D=$S/caza4/colgada-$i; mkdir -p "$D"
152    { echo "corrida $i · $(basename $art | cut -c1-12) · $pag · colgada a los ${t}s"
153      echo "--- memoria del host ---"; head -4 /proc/meminfo
154      echo "--- carga ---"; cat /proc/loadavg; } > "$D/resumen.txt"
155    ps -eo pid,ppid,stat,rss,wchan:24,comm 2>/dev/null | grep -iE 'bwrap|firefox|Web Con|Isolated|PID' > "$D/ps.txt"
156    for p in $(pgrep -P $BW 2>/dev/null; pgrep -f 'usr/lib/firefox/firefox' 2>/dev/null | head -8); do
157      { echo "--- pid $p comm=$(cat /proc/$p/comm 2>/dev/null) state=$(awk '/^State:/{print $2,$3}' /proc/$p/status 2>/dev/null)"
158        echo "    wchan=$(cat /proc/$p/wchan 2>/dev/null)"
159        echo "    syscall=$(cut -c1-70 /proc/$p/syscall 2>/dev/null)"
160        echo "    rss=$(awk '/^VmRSS:/{print $2,$3}' /proc/$p/status 2>/dev/null)"; } >> "$D/procesos.txt" 2>/dev/null
161    done
162    kill -TERM -$BW 2>/dev/null; sleep 3; kill -KILL -$BW 2>/dev/null   # el GRUPO
163    printf 'X'
164  else printf '.'; fi
165done
166echo ""; echo "== colgadas: $colgadas de $N"