> 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…
fix(qorpa): el EBUSY del overlay preguntaba por otra `run` y el culpable era un nieto fugado
Hoy el usuario no pudo abrir una sesión: el overlay de `claude-sergio-7` estaba tomado y el mensaje
decía «¿Hay otra `hammer qorpa run claude-sergio-7` viva?». **No había ninguna.** Lo retenía un
`minga host` —demonio del espejo de tawasuyu— que alguien arrancó DENTRO de la jaula, que sobrevivió
a su sesión reparentado a pid 1, y que al seguir vivo mantiene vivo el **namespace de montaje** de la
jaula y con él el overlay. La pregunta del mensaje mandaba a buscar donde no estaba.
Es el mismo patrón que el `flock` de CLAUDE.md: **el recurso lo retiene algo que nadie asocia con el
recurso**. Ahí la respuesta fue `fuser -v <lock>`; acá no sirve —el montaje vive en OTRO namespace y
`/proc/mounts` del anfitrión no lo ve— así que se recorre `/proc/*/mountinfo`, que sí lo ve.
Ahora el error no adivina, NOMBRA:
```
Error: el overlay de `claude-sergio-8` sigue tomado tras 6 intentos.
Dos overlays con el mismo `upper` corromperían la capa mutable, y por eso el
kernel lo niega.
lo retiene(n), con el overlay en SU namespace de montaje:
pid 20043 sleep sleep 300
Suele ser un NIETO FUGADO: un demonio arrancado dentro de la jaula
que sobrevivió a su sesión y mantiene vivo su namespace de montaje.
Si no lo necesitás, `kill <pid>` libera el overlay. Matalo POR PID,
nunca con `pkill -f`, que se lleva tu propia shell por delante.
```
En el incidente real habría impreso `pid 21764 minga …/minga --repo … host …` y el arreglo habría
sido una línea en vez de veinte minutos de `/proc`.
Y si de verdad no hay nadie, lo dice así en vez de culpar a un proceso inexistente: entonces el
EBUSY es otra cosa y el mensaje orienta hacia el `upper`/`work`.
Probado con un control sintético que reproduce el nieto fugado —`unshare -m` + mount del overlay de
una instancia LIBRE + sleep— sin tocar ninguna de las sesiones vivas del usuario: el mensaje nombra
al retenedor, y tras matarlo la instancia corre (`DENTRO-OK`).
⚠ Detalle de forma que costó dos intentos: las líneas del aviso se concatenan sueltas, no con `\`
de continuación — el `\` arrastra la sangría del FUENTE al mensaje y el texto salía escalonado.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/crates/takana-cli/src/qorpa.rs b/crates/takana-cli/src/qorpa.rs
index 0ae14708..19b4259e 100644
--- a/crates/takana-cli/src/qorpa.rs
+++ b/crates/takana-cli/src/qorpa.rs
@@ -1516,6 +1516,64 @@ struct Ajustes {
evidencia: bool,
}
+/// Quién tiene el overlay de esta instancia montado **en su propio namespace**.
+///
+/// ══ POR QUÉ HACE FALTA, Y QUÉ COSTÓ NO TENERLO ════════════════════════════════════════════════
+/// El mensaje de abajo preguntaba «¿Hay otra `qorpa run <id>` viva?», y el 2026-10-13 la respuesta
+/// fue **NO** y el overlay estaba tomado igual. Lo tenía un **nieto fugado**: un `minga host`
+/// —demonio del espejo de tawasuyu— que alguien arrancó DENTRO de la jaula, que sobrevivió a su
+/// sesión reparentado a pid 1, y que al seguir vivo mantiene vivo el **namespace de montaje** de la
+/// jaula… y con él el overlay. El `qorpa run` que lo creó ya no existe, así que la pregunta del
+/// mensaje llevaba a buscar donde no estaba.
+///
+/// Es el mismo patrón que el `flock` de `CLAUDE.md`: *el recurso lo retiene algo que nadie asocia
+/// con el recurso*. Ahí la respuesta fue `fuser -v <lock>`; acá no hay `fuser` que valga —el mount
+/// vive en otro namespace y `/proc/mounts` del anfitrión no lo ve— así que se recorre
+/// `/proc/*/mountinfo`, que sí.
+///
+/// Devuelve `(pid, comm, cmdline recortada)`. Vacío significa que de verdad no hay nadie: entonces
+/// el EBUSY es otra cosa y el mensaje lo dice así en vez de culpar a un proceso inexistente.
+fn quien_retiene_overlay(id: &str) -> Vec<(String, String, String)> {
+ let yo = std::process::id().to_string();
+ let mut fuera = Vec::new();
+ let Ok(entradas) = std::fs::read_dir("/proc") else {
+ return fuera;
+ };
+ for e in entradas.flatten() {
+ let pid = e.file_name().to_string_lossy().to_string();
+ if pid == yo || !pid.chars().all(|c| c.is_ascii_digit()) {
+ continue;
+ }
+ // `mountinfo` y no `mounts`: el primero es por-proceso y muestra SU namespace, que es
+ // justo donde se esconde el montaje que el anfitrión no ve.
+ let Ok(mi) = std::fs::read_to_string(format!("/proc/{pid}/mountinfo")) else {
+ continue;
+ };
+ if !mi.contains(id) {
+ continue;
+ }
+ let comm = std::fs::read_to_string(format!("/proc/{pid}/comm"))
+ .unwrap_or_default()
+ .trim()
+ .to_string();
+ let cmd = std::fs::read("/proc/".to_string() + &pid + "/cmdline")
+ .map(|b| {
+ b.split(|&c| c == 0)
+ .filter(|s| !s.is_empty())
+ .map(|s| String::from_utf8_lossy(s).to_string())
+ .collect::<Vec<_>>()
+ .join(" ")
+ })
+ .unwrap_or_default();
+ let cmd: String = cmd.chars().take(100).collect();
+ fuera.push((pid, comm, cmd));
+ if fuera.len() >= 6 {
+ break;
+ }
+ }
+ fuera
+}
+
fn run_instance(
root: &Path, id: &str, cmd: &[String], aj: Ajustes,
) -> Result<()> {
@@ -1813,8 +1871,35 @@ fn run_instance(
break;
}
if intento == INTENTOS {
+ // NO se adivina quién lo tiene: se mira. El mensaje viejo preguntaba por otra
+ // `qorpa run` y el culpable medido fue un demonio nieto en el namespace de la jaula
+ // muerta, así que la pregunta mandaba a buscar donde no estaba.
+ let culpables = quien_retiene_overlay(&id);
+ let detalle = if culpables.is_empty() {
+ // Cada línea suelta, sin `\` de continuación: el `\` arrastra la sangría del
+ // FUENTE al mensaje y el aviso salía escalonado en la terminal.
+ [
+ " y NADIE lo tiene montado: ningún proceso tiene este overlay en su",
+ " `mountinfo`. Entonces el EBUSY no es otra corrida — mirá si el `upper`",
+ " o el `work` quedaron en un estado raro, o si el namespace que lo montó",
+ " acaba de morir y el kernel todavía no lo soltó.",
+ ]
+ .join("\n")
+ } else {
+ let mut t = String::from(" lo retiene(n), con el overlay en SU namespace de montaje:\n");
+ for (pid, comm, cmd) in &culpables {
+ t.push_str(&format!(" pid {pid:<8} {comm:<16} {cmd}\n"));
+ }
+ t.push_str(" Suele ser un NIETO FUGADO: un demonio arrancado dentro de la jaula\n");
+ t.push_str(" que sobrevivió a su sesión y mantiene vivo su namespace de montaje.\n");
+ t.push_str(" Si no lo necesitás, `kill <pid>` libera el overlay. Matalo POR PID,\n");
+ t.push_str(" nunca con `pkill -f`, que se lleva tu propia shell por delante.");
+ t
+ };
bail!(
- "el overlay de `{id}` sigue tomado tras {INTENTOS} intentos.\n ¿Hay otra `hammer qorpa run {id}` viva? Dos overlays con el mismo `upper` \n corromperían la capa mutable, y por eso el kernel lo niega."
+ "el overlay de `{id}` sigue tomado tras {INTENTOS} intentos.\n\
+ Dos overlays con el mismo `upper` corromperían la capa mutable, y por eso el\n\
+ kernel lo niega.\n{detalle}"
);
}
eprintln!(" el overlay lo tiene todavía la corrida anterior — reintento {intento}/{INTENTOS}");