tawasuyu

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 1389Shell 137Markdown 80Rust 802602 Definiciones

⎇ b405fc77dfee7904f5f78f838cd6479854419522 scripts/diagnostico/cazar-cuelgue-headless.sh Shell · 145 líneas · 10.2 KB Historia crudo

cuelgue headless RESUELTO: el sandbox de Firefox no puede montar su userns dentro de bwrap b405fc77d · 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# EL ARREGLO, para cualquier firefox headless de la distro dentro de bwrap — el sandbox de Gecko no
87# aporta nada acá, porque bwrap YA es la jaula, y lo único que hace es fallar y dejar cadáveres:
88#
89#   MOZ_DISABLE_CONTENT_SANDBOX=1 MOZ_DISABLE_GMP_SANDBOX=1 MOZ_DISABLE_RDD_SANDBOX=1 \
90#   MOZ_DISABLE_SOCKET_PROCESS_SANDBOX=1 MOZ_DISABLE_UTILITY_SANDBOX=1
91#
92# ⚠ LECCIÓN DE MÉTODO, la segunda del día con el mismo disfraz: el A/B del fork server dio "0 segv"
93#   en las dos ramas y parecía un resultado. No lo era — el arnés estaba roto (`bwrap: Can't create
94#   file at /user.js: Read-only file system`, el punto de montaje tiene que EXISTIR en un root de
95#   sólo lectura) y NINGUNA rama sacaba screenshot. Lo delató la rama de control, que también dio
96#   cero cuando tenía que dar dos. **Un instrumento muerto y un experimento exitoso se ven idénticos
97#   desde afuera; sólo los distingue un control que TIENE que dar distinto.**
98#
99# ══ PARA QUÉ SIRVE ENTONCES ════════════════════════════════════════════════════════════════════
100# Para que la PRÓXIMA vez que aparezca, se capture en el acto en vez de empezar de cero: `wchan`,
101# `syscall` y `state` de cada proceso (distinguen espera de disco de espera de lock), memoria y
102# carga del host en ese instante, y el log del navegador de esa corrida —que en las buenas se tira—.
103#
104# Uso:  N=56 UMBRAL=45 scripts/diagnostico/cazar-cuelgue-headless.sh
105#       (necesita el rootfs hidratado en store/.rootfs/sway-v2 y las páginas en el scratchpad;
106#        ajustar las rutas de arriba si se mueven)
107set -u
108RAIZ=/mnt/vvv/hammer; R=$RAIZ/store/.rootfs/sway-v2
109S=/tmp/claude-1001/-mnt-vvv-hammer/9d639cb7-92ff-4a26-bd7f-ec9988b972cf/scratchpad
110N=${N:-56}; UMBRAL=${UMBRAL:-45}
111FF=($(ls -d $RAIZ/store/8116bdec*-firefox $RAIZ/store/1d730334*-firefox \
112        $RAIZ/store/3d199174*-firefox $RAIZ/store/bb33fd56*-firefox))
113PAGS=(fuera-del-corpus.html del-corpus.html)   # LAS DEL BANCO: es donde ocurrieron las 2 colgadas
114colgadas=0
115for i in $(seq 1 $N); do
116  art=${FF[$(( i % 4 ))]}; pag=${PAGS[$(( i % 2 ))]}
117  bwrap --ro-bind "$R" / --ro-bind /usr/lib/musl/lib/libc.so /lib/ld-musl-x86_64.so.1 \
118    --ro-bind "$art/usr/lib/firefox" /usr/lib/firefox \
119    --dev /dev --proc /proc --tmpfs /run --tmpfs /tmp --unshare-pid \
120    --ro-bind "$S/bench" /bench --bind "$S/caza4" /salida --unshare-user --uid 0 --gid 0 \
121    --setenv PATH /usr/bin:/bin \
122    /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
123      LD_LIBRARY_PATH=/usr/lib/firefox /usr/lib/firefox/firefox --headless --screenshot /salida/x.png "file:///bench/'"$pag"'"' \
124    >/dev/null 2>&1 &
125  BW=$!
126  t=0
127  while kill -0 $BW 2>/dev/null && [ $t -lt $UMBRAL ]; do sleep 1; t=$((t+1)); done
128  if kill -0 $BW 2>/dev/null; then
129    colgadas=$((colgadas+1))
130    D=$S/caza4/colgada-$i; mkdir -p "$D"
131    { echo "corrida $i · $(basename $art | cut -c1-12) · $pag · colgada a los ${t}s"
132      echo "--- memoria del host ---"; head -4 /proc/meminfo
133      echo "--- carga ---"; cat /proc/loadavg; } > "$D/resumen.txt"
134    ps -eo pid,ppid,stat,rss,wchan:24,comm 2>/dev/null | grep -iE 'bwrap|firefox|Web Con|Isolated|PID' > "$D/ps.txt"
135    for p in $(pgrep -P $BW 2>/dev/null; pgrep -f 'usr/lib/firefox/firefox' 2>/dev/null | head -8); do
136      { echo "--- pid $p comm=$(cat /proc/$p/comm 2>/dev/null) state=$(awk '/^State:/{print $2,$3}' /proc/$p/status 2>/dev/null)"
137        echo "    wchan=$(cat /proc/$p/wchan 2>/dev/null)"
138        echo "    syscall=$(cut -c1-70 /proc/$p/syscall 2>/dev/null)"
139        echo "    rss=$(awk '/^VmRSS:/{print $2,$3}' /proc/$p/status 2>/dev/null)"; } >> "$D/procesos.txt" 2>/dev/null
140    done
141    kill -TERM $BW 2>/dev/null; sleep 3; kill -KILL $BW 2>/dev/null
142    printf 'X'
143  else printf '.'; fi
144done
145echo ""; echo "== colgadas: $colgadas de $N"