Files
SergioandClaude Opus 5 b405fc77df cuelgue headless RESUELTO: el sandbox de Firefox no puede montar su userns dentro de bwrap
No era la presión de memoria. La caza bajo presión (4 niveles × 20 corridas, con el nivel de
presión MEDIDO por MemAvailable y PSI) reprodujo el cuelgue por primera vez en 282 corridas
—2 de 80, 2,5%, compatible con el 3,5% original— pero SIN dosis-respuesta: los dos cuelgues
cayeron en el nivel con PSI 0,00 y los dos niveles apretados dieron cero.

Tres hipótesis muertas, cada una con su medición:
  · OOM se lleva al hijo   → oom_kill de /proc/vmstat no se movió (delta=0) y los hijos vivían
  · presión de memoria     → sin dosis-respuesta
  · fork server de Gecko   → A/B del pref: el proceso forkserver desaparece (35 muestras → 0,
                             o sea que el pref hizo efecto) y siguen los mismos 2 segfaults

La causa apareció comparando el log de una colgada con el de una BUENA, que es lo que faltaba:
en las 6 buenas TAMBIÉN revientan 2 hijos con SIGSEGV, después de escribir el PNG y por eso
invisibles. Muestreando /proc adentro, se llaman 'Sandbox Forked': los ayudantes que el sandbox
propio de Firefox forkea para montar su user-namespace, que no puede montar porque ya estamos
dentro del de bwrap ('writing /proc/self/uid_map: EPERM').

Las tres cantidades bajan juntas hasta cero, que es forma de cadena causal:

  sandbox completo            segv=7  SIN screenshot  EPERM=9  SandboxForked=725
  content.level=0             segv=2  con screenshot  EPERM=2  SandboxForked=68
  los 3 prefs .level a 0      segv=1  con screenshot  EPERM=1  SandboxForked=33
  las 5 MOZ_DISABLE_*         segv=0  con screenshot  EPERM=0  SandboxForked=0

Y el sandbox completo cuelga DETERMINISTA (rc=124 a los 180 s). Comparte familia con el
intermitente pero no está probado que sean el mismo: al determinista le faltan los 'Failed to
launch'. Queda dicho como lo que es.

Arreglo para cualquier firefox headless en bwrap (donde el sandbox de Gecko no aporta nada,
porque bwrap ya es la jaula):
  MOZ_DISABLE_{CONTENT,GMP,RDD,SOCKET_PROCESS,UTILITY}_SANDBOX=1

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QPJteswQvP1L7zSBrQQe2
2026-09-08 15:59:23 +00:00

57 lines
2.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""presion-memoria.py — generador de presión de memoria ACOTADO y con seguro.
No apunta a un tamaño de hog sino a un SETPOINT de MemAvailable: crece hasta dejar la máquina con
los MiB pedidos disponibles y ahí se queda, corrigiendo si el resto del sistema se mueve. La
variable controlada es la que importa (memoria disponible), no la que es fácil de fijar.
TRES SEGUROS, porque esta máquina es compartida con la granja y con otros agentes:
1. oom_score_adj = 1000 ⇒ si el kernel tiene que matar a alguien, el PRIMERO soy yo, nunca un
build de 4 h ni el trabajo de otro agente. Subirlo no necesita privilegios; bajarlo sí.
2. suelo duro: no crece por debajo de PISO_MIB de disponible, pase lo que pase.
3. reloj: se muere solo a los SEGUNDOS, aunque quien lo lanzó desaparezca.
⚠ El relleno es ALEATORIO A PROPÓSITO. El swap de esta máquina es zram (comprimido EN RAM): un hog
de ceros se comprime a nada y no genera ni un byte de presión real — mediría cero creyendo medir
mucho. Un bloque aleatorio de 4 KiB repetido es incompresible página a página.
"""
import os, sys, time, signal
SETPOINT = int(sys.argv[1]) if len(sys.argv) > 1 else 800 # MiB disponibles objetivo
SEGUNDOS = int(sys.argv[2]) if len(sys.argv) > 2 else 600
PISO_MIB = 350
TROZO = 64 * 1024 * 1024
def disponible_mib():
with open('/proc/meminfo') as f:
for l in f:
if l.startswith('MemAvailable:'):
return int(l.split()[1]) // 1024
return 0
try:
with open('/proc/self/oom_score_adj', 'w') as f:
f.write('1000') # seguro 1: víctima designada
except OSError as e:
print(f"presion: NO pude marcarme como víctima del OOM ({e}) — abortando por seguridad")
sys.exit(1)
signal.alarm(SEGUNDOS) # seguro 3: reloj de pared
bloque = os.urandom(4096) * (TROZO // 4096) # incompresible para zram
lastre = []
print(f"presion: objetivo {SETPOINT} MiB disponibles, piso {PISO_MIB}, {SEGUNDOS}s, "
f"disponible ahora {disponible_mib()}", flush=True)
try:
while True:
d = disponible_mib()
if d > SETPOINT and d > PISO_MIB: # seguro 2: suelo duro
lastre.append(bytearray(bloque)) # bytearray: memoria propia, tocada de verdad
elif d < SETPOINT - 128 and lastre:
lastre.pop() # el sistema pidió; le devuelvo
else:
time.sleep(0.25)
if len(lastre) % 8 == 0:
print(f"presion: lastre={len(lastre)*64} MiB disponible={d}", flush=True)
except (MemoryError, KeyboardInterrupt):
pass