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
57 lines
2.6 KiB
Python
Executable File
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
|