🩺 EFI: el initramfs perdía la carrera contra la enumeración USB — y en silencio

Síntoma en metal ajeno: la pantalla se congela justo tras `sdc: sdc1 sdc2 sdc3 sdc4`
/ `Attached SCSI removable disk`. No estaba colgado.

Prueba de que el root nunca se montó: con `ignore_loglevel` un montaje ext4 imprime
`EXT4-fs (...): mounted filesystem` en pantalla sí o sí, y no aparecía.

Dos causas encadenadas:

1. CARRERA. El rdinit arranca en cuanto se desempaqueta el initramfs, pero el bus USB
   enumera asíncrono y tarda segundos (reset de hub, settling de 1s por dispositivo,
   scan SCSI). Los 5 reintentos de 1s no alcanzaban. En QEMU el disco es virtio y está
   desde el instante cero ⇒ la carrera NUNCA se veía en validación. Es el `rootwait`
   que no podemos usar porque no hay root= en el cmdline. Ahora espera 60s.

2. CEGUERA. El cmdline horneado es `console=tty0 console=ttyS0,115200` y /dev/console
   = la ÚLTIMA `console=` ⇒ el serial. El `exec sh` de rescate nacía invisible. Es el
   MISMO bug que costó el 1er viaje físico de KDE, una capa más temprano: allá se
   arregló para después del switch_root (getty en tty1) y el initramfs quedó ciego.
   Ahora el log del pivote va a /dev/tty0, el marcador INIT-OK también, y el rescate
   abre shell en tty1 listando los bloques visibles.

+ `timeout 15` a `hammer boot menu`: corre ANTES del exec de arje-zero ⇒ colgarse ahí
  deja un arranque sin PID1 ni pantalla, indistinguible de un kernel muerto. El
  `|| true` protegía del fallo, no del bloqueo.

Validado arrancando la imagen COMO USB (qemu-xhci + usb-storage, -serial null, con
pantalla): marcadores del pivote visibles, motd y prompt `/ #`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-05 13:54:23 -04:00
co-authored by Claude Opus 5
parent 166355d1fb
commit 705a70c33a
2 changed files with 43 additions and 12 deletions
+1 -1
View File
@@ -89,7 +89,7 @@ install -Dm755 scripts/cosmic/cosmic-start-qemu.sh "$MERGED/usr/bin/cosmic-start
#
# Se arregla acá y no en el cmdline: un getty sobre tty1 no depende del cmdline (en metal siempre hay
# VT) y no obliga a re-sellar el kernel. El getty de `console` se conserva para debug por serie.
echo "==> añadiendo getty en tty1 (el prompt de `console` va al SERIAL, invisible en metal)"
echo '==> añadiendo getty en tty1 (el prompt de `console` va al SERIAL, invisible en metal)'
cat > "$MERGED/usr/bin/console-login" <<'LOGIN'
#!/bin/sh
# arje-zero lanza los getty con envp VACÍO ⇒ sin PATH no se resuelve ni `cat` ni `cosmic-start`.
+42 -11
View File
@@ -76,7 +76,7 @@ ESP_IMG="$STAGE/esp.img"; IRD="$STAGE/initramfs.cpio.gz"
echo "==> initramfs de pivote (busybox estático + /init switch_root a hammer-root)"
mkdir -p "$IRD_TREE/bin" "$IRD_TREE/dev" "$IRD_TREE/proc" "$IRD_TREE/sys" "$IRD_TREE/newroot"
cp "$BUSYBOX" "$IRD_TREE/bin/busybox"; chmod 0755 "$IRD_TREE/bin/busybox"
for a in sh mount umount findfs switch_root mkdir echo sleep; do ln -sf busybox "$IRD_TREE/bin/$a"; done
for a in sh mount umount findfs switch_root mkdir echo sleep ls cat setsid dmesg blkid; do ln -sf busybox "$IRD_TREE/bin/$a"; done
cat > "$IRD_TREE/init" <<'INIT'
#!/bin/busybox sh
# initramfs de pivote (EFI-stub → aquí). Monta las pseudo-fs, resuelve la ext4 real por LABEL, monta
@@ -86,20 +86,44 @@ export PATH=/bin
mount -t proc proc /proc 2>/dev/null || true
mount -t sysfs sys /sys 2>/dev/null || true
mount -t devtmpfs dev /dev 2>/dev/null || true
# El cmdline horneado del kernel pone console=tty0 al final ⇒ el stdout de userspace va a la PANTALLA,
# no al serial. Para que el pivote sea visible en un OVMF headless (y para depurar en metal), tee'amos
# los marcadores también a /dev/ttyS0.
log() { echo "$*"; echo "$*" > /dev/ttyS0 2>/dev/null || true; }
# ⚠ EL CMDLINE HORNEADO ES "console=tty0 console=ttyS0,115200 …" Y EL KERNEL HACE /dev/console = LA
# ÚLTIMA `console=` ⇒ **ttyS0**. O sea que el stdout de este script va al SERIAL, no a la pantalla: en
# un metal sin puerto serie TODO lo que pase acá es INVISIBLE y el arranque parece congelado en el
# último printk del kernel. Es el mismo bug que costó el 1er viaje físico de KDE, una capa más
# temprano: allá se arregló para DESPUÉS del switch_root (getty en tty1) y acá seguía sin arreglar.
# Por eso el log va explícitamente a **/dev/tty0** (la VT en primer plano = la pantalla) además del
# serial, y nunca se confía en el stdout heredado.
log() {
echo "$*"
for _d in /dev/tty0 /dev/ttyS0; do echo "$*" > "$_d" 2>/dev/null || true; done
}
# Shell de rescate VISIBLE: si algo falla, un `exec sh` a secas nace en el serial y el usuario ve una
# pantalla muerta sin manera de averiguar nada. Se abre uno en tty1 (pantalla) y se conserva el serial.
rescate() {
log "HAMMER-EFI-FALLO: $*"
log " bloques visibles: $(ls /dev/sd* /dev/nvme* /dev/vd* /dev/mmcblk* 2>/dev/null | tr '\n' ' ')"
log " abriendo shell de rescate en tty1 — probá: blkid ; dmesg | tail -30"
setsid -c sh -c 'exec /bin/sh </dev/tty1 >/dev/tty1 2>&1' >/dev/null 2>&1 &
exec sh
}
log "HAMMER-EFI: initramfs de pivote — resolviendo hammer-root por LABEL"
# ⏱ LA ESPERA TIENE QUE SER LARGA, Y ÉSTE ES EL MOTIVO: el rdinit arranca en cuanto se desempaqueta el
# initramfs, pero el bus USB enumera de forma ASÍNCRONA y tarda SEGUNDOS (reset del hub, settling de 1s
# por dispositivo, scan SCSI). En QEMU el disco es virtio y está desde el instante cero, así que la
# carrera NUNCA se ve en validación: sólo aparece al bootear de un USB en metal, y con 5 reintentos de
# 1s se pierde. Es el equivalente de `rootwait`, que acá no aplica porque no hay root= en el cmdline.
ROOTDEV=""
for _try in 1 2 3 4 5; do
_try=0
while [ "$_try" -lt 60 ]; do
ROOTDEV="$(findfs LABEL=hammer-root 2>/dev/null || true)"
[ -n "$ROOTDEV" ] && break
log "HAMMER-EFI: hammer-root aún no visible, reintento $_try…"; sleep 1
_try=$(( _try + 1 ))
[ $(( _try % 5 )) -eq 0 ] && log "HAMMER-EFI: esperando hammer-root… ${_try}s (el USB tarda en enumerar)"
sleep 1
done
[ -n "$ROOTDEV" ] || { log "HAMMER-EFI-FALLO: no encuentro LABEL=hammer-root"; exec sh; }
[ -n "$ROOTDEV" ] || rescate "no encuentro LABEL=hammer-root tras ${_try}s"
log "HAMMER-EFI: hammer-root = $ROOTDEV → montando en /newroot"
mount -t ext4 "$ROOTDEV" /newroot || { log "HAMMER-EFI-FALLO: no pude montar $ROOTDEV"; exec sh; }
mount -t ext4 "$ROOTDEV" /newroot || rescate "no pude montar $ROOTDEV"
# /store y /var/lib/hammer por LABEL (no bloqueante: el wrapper del root también reintenta).
STOREDEV="$(findfs LABEL=hammer-store 2>/dev/null || true)"
STATEDEV="$(findfs LABEL=hammer-state 2>/dev/null || true)"
@@ -136,12 +160,19 @@ cat > "$ROOT_TREE/sbin/init" <<'RINIT'
# busybox switch_root NO mueve /dev al nuevo root ⇒ montamos devtmpfs antes del marcador (si no,
# /dev/ttyS0 no existe todavía y el echo se pierde). arje-zero re-monta lo que necesite, es idempotente.
/bin/busybox mount -t devtmpfs dev /dev 2>/dev/null || true
echo "HAMMER-EFI-INIT-OK: arje-zero PID1" > /dev/ttyS0 2>/dev/null || true
# A la PANTALLA (tty0) además del serial: en metal sin puerto serie éste es el único indicio de que el
# pivote llegó al init real. Ver el comentario largo del initramfs.
for _d in /dev/tty0 /dev/ttyS0; do
echo "HAMMER-EFI-INIT-OK: arje-zero PID1" > "$_d" 2>/dev/null || true
done
# Menú de arranque por grafo (ADR 0010): emite /run/hammer/boot-graph.json y, si hay un compositor
# (mirada), lo pinta sobre KMS para elegir un nodo. Graceful: sin compositor, emite el grafo y sigue.
# Salida al serial (no pinta tty0 ⇒ respeta el cero-parpadeo). No bloquea el arranque si falla.
# El `timeout` no es paranoia decorativa: esto corre ANTES del exec de arje-zero, o sea que si se
# cuelga no hay PID1, no hay getty y no hay pantalla — un arranque muerto e indistinguible de un kernel
# colgado. El `|| true` protege del fallo, no del bloqueo.
if [ -x /usr/bin/hammer ]; then
/usr/bin/hammer boot menu > /dev/ttyS0 2>&1 || true
/bin/busybox timeout 15 /usr/bin/hammer boot menu > /dev/ttyS0 2>&1 || true
fi
exec /usr/bin/arje-zero
RINIT