censar: cuatro binarios que YA NO EXISTEN en disco — y el paso de rescate, que caduca

`/proc/<pid>/exe` termina en « (deleted)» para cuatro servicios de gioser: el fichero ya no está en
disco, sólo vive el inodo que sostiene su proceso. Y en tres de los cuatro hay AHORA otro fichero en
la misma ruta, de distinto tamaño:

    tejido          corre 12750368 B · en su ruta hay 13815864 B
    shuma-gateway   corre  8431896 B · en su ruta hay 10363504 B
    pacha-secretos  corre  8634240 B · en su ruta hay  8647456 B
    puerta-f6e393ff corre 197262440 B · en su ruta NO HAY NADA

Copiar la ruta NO FALLA: muda otra cosa, y el servicio nuevo no es el que estaba andando. Todo verde,
todo distinto — el modo de fallo más caro que hay en este frente.

Se recuperan leyendo `/proc/<pid>/exe`, y SÓLO mientras el proceso viva. En una mudanza que termina
BORRANDO el origen, un reinicio de gioser antes de este paso los pierde para siempre. Por eso:

· clase propia en `origen_binario` (`borrado`), no una coletilla dentro del texto de `suelto`: no es
  «hay que llevarlo», es «se pierde en el próximo reinicio y el que está en su ruta no es el mismo».
  El motivo trae el comando literal de rescate con su pid.
· paso `rescate` en el plan, ANTES del preflight, porque es el único paso que puede volverse
  IMPOSIBLE mientras se piensa el resto.
· su verificación COMPARA TAMAÑOS contra el que corre. No es celo: un `cat` de un `/proc` que ya no
  existe crea un fichero VACÍO y devuelve 0, así que sin comparar el rescate «pasa». Probado en los
  dos sentidos — el paso real sale 0, y con un fichero vacío a propósito dice `FALTA tejido` y sale 1.

Los cuatro ya están rescatados en `work/mudanza/rescate/` (gitignored), byte a byte iguales a los que
corren, con su SHA256SUMS.

Y el empalme se hizo comprobando que el marcador fuera ÚNICO antes de cortar, que es la lección del
`def pasos` duplicado de ayer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RomoxEGZUhaT4pob1QSX5x
This commit is contained in:
Sergio
2026-09-12 02:28:37 +00:00
co-authored by Claude Opus 5
parent c78c5ff91f
commit edc95325d9
3 changed files with 100 additions and 1 deletions
+29
View File
@@ -301,6 +301,35 @@ reconstruyen el servidor desde cero**:
Ninguno de los tres es un log de lo que se hizo: los tres son entradas que se vuelven a ejecutar.
Ésa es la diferencia entre mudar un servidor y poder mudarlo otra vez.
### 4.0 undecies · Cuatro binarios que ya no existen — el paso que CADUCA
El más urgente de todo el censo, y lo encontró la lista de lo que el perfil no puede cubrir:
`/proc/<pid>/exe` termina en `(deleted)` para **cuatro servicios de gioser**. El fichero ya no está
en disco; sólo vive el inodo que sostiene su proceso.
Y en tres de los cuatro hay **ahora otro fichero en la misma ruta, de distinto tamaño**:
| servicio | corriendo | en su ruta hoy |
|---|---|---|
| `tejido` | 12 750 368 B | 13 815 864 B |
| `shuma-gateway` | 8 431 896 B | 10 363 504 B |
| `pacha-secretos` | 8 634 240 B | 8 647 456 B |
| `puerta-f6e393ff…` | 197 262 440 B | *no hay nada* |
Copiar la ruta **no falla**: muda otra cosa, y el servicio nuevo no es el que estaba andando. Es el
modo de fallo más caro que hay en este documento — todo verde, todo distinto.
Se recuperan leyendo `/proc/<pid>/exe`, **y sólo mientras el proceso viva**. En una mudanza que
termina *borrando el origen*, un reinicio de gioser antes de este paso pierde esos binarios para
siempre. Por eso el rescate va **antes del preflight**: es el único paso del plan que puede volverse
imposible mientras se piensa el resto.
Su verificación **compara tamaños** contra el que corre, y no por celo: un `cat` de un `/proc` que ya
no existe crea un fichero **vacío** y devuelve 0. Probado en los dos sentidos — el paso real pasa, y
con un fichero vacío a propósito el guardián dice `FALTA tejido` y sale 1.
Los cuatro ya están rescatados en `work/mudanza/rescate/`, con su `SHA256SUMS`.
### 4.0 decies · Leer la config del servidor: un fósil se detecta SIN tocar la red
La sonda DNS dice si un dominio resuelve; no si el servidor tiene algo que servirle. Eso lo dice el
+31 -1
View File
@@ -101,6 +101,24 @@ def invocaciones(t, pids):
# quedaban en «no se pudo leer su binario» por esto, y no por ser realmente desconocidos.
exe, _ = t.run(f"readlink /proc/{pid}/exe 2>/dev/null")
cmd, cwd, exe = cmd.strip(), cwd.strip(), exe.strip()
# ⚠⚠ `/proc/<pid>/exe` termina en « (deleted)» cuando el fichero YA NO ESTÁ en disco: el
# proceso sostiene el inodo y nadie más lo puede abrir por su ruta. Medido en gioser: CUATRO
# servicios están así, y en dos de ellos hay AHORA otro fichero en la misma ruta, de distinto
# tamaño — `tejido` corre 12 750 368 bytes y en su ruta hay 13 815 864. Copiar la ruta muda
# OTRA COSA, en silencio, y el servicio nuevo no es el que estaba andando.
#
# Se recupera leyendo `/proc/<pid>/exe`, y SÓLO mientras el proceso viva: al reiniciar gioser
# —o al morirse ese pid— ese binario desaparece para siempre. En una mudanza que termina
# BORRANDO el origen, esto es lo más urgente del censo.
borrado = exe.endswith(" (deleted)")
if borrado:
exe = exe[:-len(" (deleted)")]
vivo_b = disco_b = ""
if borrado:
v, _ = t.run(f"stat -L -c %s /proc/{pid}/exe 2>/dev/null")
d, _ = t.run(f"stat -c %s {shlex.quote(exe)} 2>/dev/null")
vivo_b = v.strip() if v.strip().isdigit() else ""
disco_b = d.strip() if d.strip().isdigit() else ""
for v in ("cmd", "cwd", "exe"):
if locals()[v].startswith("(sonda falló"):
cmd, cwd, exe = (("" if v == "cmd" else cmd), ("" if v == "cwd" else cwd),
@@ -111,7 +129,9 @@ def invocaciones(t, pids):
if cmd or cwd or exe:
out[nombre] = {"cmdline": cmd, "cwd": cwd,
"exe": exe if exe.startswith("/") else "",
"usuario": (sup or {}).get("usuario", "")}
"usuario": (sup or {}).get("usuario", ""),
"exe_borrado": borrado, "pid": pid,
"bytes_vivo": vivo_b, "bytes_disco": disco_b}
return out
@@ -642,6 +662,11 @@ def emitir_toml(c):
L.append(f"exec = {toml_escape(s.get('exec',''))}")
if s.get('usuario'):
L.append(f"usuario = {toml_escape(s['usuario'])}")
if s.get('exec_borrado'):
L.append("exec_borrado = true")
L.append(f"pid_vivo = {int(s.get('pid_vivo') or 0)}")
L.append(f"bytes_vivo = {toml_escape(s.get('bytes_vivo',''))}")
L.append(f"bytes_disco = {toml_escape(s.get('bytes_disco',''))}")
if s.get('exec_motivo'):
L.append(f"exec_motivo = {toml_escape(s['exec_motivo'])}")
L.append(f"paquete = {toml_escape(s.get('paquete',''))}")
@@ -750,6 +775,11 @@ def main():
e['cmdline'] = i.get('cmdline', '')
e['cwd'] = i.get('cwd', '')
e['exe_real'] = i.get('exe', '')
if i.get('exe_borrado'):
e['exec_borrado'] = True
e['pid_vivo'] = i.get('pid', 0)
e['bytes_vivo'] = i.get('bytes_vivo', '')
e['bytes_disco'] = i.get('bytes_disco', '')
if i.get('usuario'):
e['usuario'] = i['usuario']
# `/proc/<pid>/exe` MANDA sobre `argv[0]`: es el binario de verdad y no lo puede reescribir el
+40
View File
@@ -297,6 +297,19 @@ def origen_binario(e, repo=None):
return ("paquete-ajeno",
f"lo provee el gestor del origen: paquete `{pkg}`. En el destino hace falta una "
f"receta takana, o `qorpa` si se acepta una jaula con la distro ajena (ADR 0015)")
if exe and e.get("exec_borrado"):
# Su propia clase, y la más urgente del censo: el fichero YA NO EXISTE. No es «hay que
# llevarlo», es «se pierde en el próximo reinicio y el que está en su ruta NO es el mismo».
pid = e.get("pid_vivo") or (e.get("pids") or [0])[0]
bv, bd = e.get("bytes_vivo", ""), e.get("bytes_disco", "")
dif = (f" El que hay AHORA en esa ruta pesa {bd} bytes y el que corre {bv}: NO SON EL MISMO."
if bv and bd and bv != bd else
(" En esa ruta ya no hay nada." if not bd else ""))
return ("borrado",
f"⚠ el binario que CORRE fue borrado o reemplazado: `{exe}` ya no existe en disco."
f"{dif} Sólo vive mientras viva el pid {pid}; al reiniciar el origen se pierde PARA "
f"SIEMPRE. Rescatalo ya: sudo cat /proc/{pid}/exe > {os.path.basename(exe)} "
f"&& chmod +x {os.path.basename(exe)}")
if exe:
return ("suelto",
f"NADIE lo provee: binario puesto a mano en {exe}. Hay que LLEVARLO (con lo que "
@@ -544,6 +557,33 @@ def pasos(censo, target, key):
muere = [(t, n, e) for t, n, e in entradas(censo) if e.get("decision") == "muere"]
contenido = censo.get("contenido", [])
# ── 0 bis. RESCATE, antes que nada ──────────────────────────────────────────────────────────
# Va PRIMERO, antes del preflight, porque es el único paso del plan que puede volverse
# IMPOSIBLE mientras se piensa el resto: estos binarios ya no existen en disco y sólo viven en el
# inodo que sostiene su proceso. Un reinicio del origen —o que se caiga ese pid— y se pierden
# para siempre. Y en dos de los cuatro hay AHORA otro fichero en la misma ruta, de distinto
# tamaño: copiar la ruta no falla, muda otra cosa.
borrados = [s for s in censo.get("servicio", [])
if s.get("exec_borrado") and s.get("decision") == "muda"]
if borrados:
lineas, ver = [], []
for s in borrados:
pid = s.get("pid_vivo") or (s.get("pids") or [0])[0]
nom = os.path.basename(s.get("exec") or s["name"])
lineas.append(f"sudo cat /proc/{pid}/exe > rescate/{shlex.quote(nom)} && "
f"chmod +x rescate/{shlex.quote(nom)}")
if s.get("bytes_vivo"):
# La verificación COMPARA contra el tamaño del que CORRE. Un `cat` de un /proc que ya
# no existe crea un fichero VACÍO y devuelve 0: sin comparar, el rescate «pasa».
ver.append(f"[ \"$(stat -c %s rescate/{shlex.quote(nom)} 2>/dev/null)\" = "
f"\"{s['bytes_vivo']}\" ] || {{ echo 'FALTA {nom}'; exit 1; }}")
paso("rescate", f"rescatar {len(borrados)} binario(s) BORRADOS del disco (sólo mientras corran)",
"mkdir -p rescate\n" + "\n".join(lineas),
verifica="\n".join(ver) + "\necho ' ✓ los binarios rescatados coinciden con los que corren'",
nota="⚠ ESTE PASO CADUCA. `/proc/<pid>/exe` deja de existir cuando muere el proceso, y "
"estos ficheros ya no están en su ruta. Si el origen se reinicia antes de correr "
"esto, el binario que estaba andando NO se puede recuperar de ninguna manera.")
# ── 0. preflight ────────────────────────────────────────────────────────────────────────────
# ⚠ El sitio se mide POR SISTEMA DE FICHEROS, no contra `/`. Medido en la caja takana: `/` tiene
# 3,5 G libres, `/store` 19 G y `/work` 66 G — un preflight que sume 33,6 G y los compare contra