instalador en RAM: tres bugs que decían «instalado» sin haber instalado nada
Primera corrida del initramfs-instalador en QEMU. Los tres fallos son de la misma familia: el síntoma no señalaba la causa, y dos de ellos terminaban en un ÉXITO FALSO — el modo de fallo más caro que hay acá, porque el usuario reinicia confiando y se queda sin sistema. 1) EL ESTADO DE UN PIPE ES EL DEL ÚLTIMO COMANDO. El /init hacía 'if sh instalador… | tee /dev/tty0 > /dev/ttyS0; then OK; else rescate; fi' ⇒ el if evaluaba el 'tee', que sale 0 siempre. El instalador falló, no formateó NADA, y la pantalla dijo «TAKANA-INSTALADOR-OK: instalado». Ahora el estado se guarda aparte y el pipe no decide nada. 2) FALTABA EL APPLET 'id' Y EL SCRIPT ELIGIÓ LA RAMA EQUIVOCADA. Sin 'id -u' la comparación daba vacío ⇒ «no soy root» ⇒ se fue a 'unshare', que tampoco estaba ⇒ siguió sin formatear. Un if cuyo dato de entrada no existe elige una rama igual, y elige mal. Ahora: si no hay 'id', aborta; si no hay 'unshare' y no somos root, aborta; y DESPUÉS del mke2fs se comprueba con blkid que la LABEL está (regla 3 del CLAUDE.md: el ausente falla ruidoso, el vacío llega hasta el final). 3) 'ls /dev/nvme* /dev/sd* /dev/vd*' SALE ≠0 SI CUALQUIERA NO EXISTE, y los globs sin coincidencia quedan literales ⇒ la espera de discos agotaba los 60 s con /dev/vda presente desde el segundo uno. En metal eso se lee como un arranque colgado. Y una cuarta que no era un bug sino una falta: el /init no montaba efivarfs, así que el instalador habría concluido «esta máquina no arrancó por UEFI» y NO habría registrado la entrada NVRAM — en una máquina que sí. Mismo diagnóstico falso que ya costó una vuelta en el /sbin/init del rootfs. El runbook recoge la política del §5 bis con su diagrama: destino = raíz viva → kexec → espacio libre; destino cualquier otro → flujo normal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Delta semántico
Ninguna definición cambió: el cambio es de texto, documentación o datos.
3 archivos · +96 −6
diff --git a/docs/runbooks/instalador-laptop-tigerlake.md b/docs/runbooks/instalador-laptop-tigerlake.md
index 52edca0a..ab5002d1 100644
--- a/docs/runbooks/instalador-laptop-tigerlake.md
+++ b/docs/runbooks/instalador-laptop-tigerlake.md
@@ -169,6 +169,52 @@ Hoy el instalador hace lo contrario de eso, y conviene verlo exacto antes de cam
5. **`backup` de la NVRAM y de la ESP antes de escribir**, ofrecido por defecto. Ya existe el
subcomando; falta que el instalador lo llame.
+## 5 bis. La política del «saltito»: qué pasa si el destino ES el sistema que corre
+
+> **Decisión del usuario, 2026-09-23:** *«que se utilice primero [kexec], y si falla por las
+> condiciones que dices, que use el método del espacio libre. Pero eso, cuando la partición elegida
+> sea la actual; si no, normal.»* Implementada en `--on-active-root` (`auto` por defecto).
+
+```
+ destino = la raíz viva ?
+ │
+ ├── no → flujo normal: formatear esa partición y nada más ← el §5.1
+ │
+ └── sí → 1. ¿puede esta máquina kexec? (takana kernel kexec --probe)
+ │
+ ├── sí → instalador en RAM + salto. El sistema viejo muere en el salto
+ │ ⇒ su disco queda libre ⇒ su raíz SÍ se puede formatear
+ │
+ └── no → instalar AL LADO, en espacio sin asignar. La raíz viva no se toca
+```
+
+**Por qué no se puede «desde adentro» y sí «desde el salto».** Un filesystem montado y en uso no se
+formatea — eso lo impone el kernel, no una política nuestra. Pero se puede *salir* de adentro sin
+pasar por el firmware: `kexec_file_load` (ADR 0017 §1) carga otro kernel y salta. Lo que corre
+después es un **initramfs-instalador que vive en RAM** (`scripts/instalador-initramfs.sh`), y para él
+el disco entero es ajeno y está libre.
+
+**Las condiciones que pueden cerrar el camino, y cómo se ven:**
+
+| condición | cómo se comprueba | si falla |
+|---|---|---|
+| el kernel que corre implementa `kexec_file_load` | `takana kernel kexec --probe` | espacio libre |
+| somos root (`CAP_SYS_BOOT`) | idem | correr con `sudo` |
+| el kernel no está en *lockdown* | idem (lee `/sys/kernel/security/lockdown`) | Secure Boot: sólo acepta kernels firmados ⇒ espacio libre |
+| el payload entra en la RAM | lo mide `instalador-initramfs.sh` (aborta >40 %) | instalar desde un medio |
+
+⚠ **El sondeo no es concluyente sin root, y esto está medido sobre el MISMO kernel (2026-09-23):**
+como uid 1001 dijo «el syscall existe» y como root dijo `ENOSYS`. El kernel contesta `EPERM` antes de
+llegar a decir si implementa la llamada. Un «sólo te falta ser root» puede estar tapando un «y además
+tu kernel no lo trae» ⇒ **repetir el probe como root antes de creerle**.
+
+En la laptop hay que medir dos cosas, y son dos comandos:
+
+```sh
+sudo takana kernel kexec --probe # el veredicto completo
+cat /sys/kernel/security/lockdown # [none] = Secure Boot no está estorbando
+```
+
## 6. Instalar desde OTRO sistema operativo, sin medio live
La pregunta del usuario (2026-09-23) es si se puede romper con la instalación tradicional y tener un
diff --git a/scripts/instalador-initramfs.sh b/scripts/instalador-initramfs.sh
index 401fd489..6616f203 100755
--- a/scripts/instalador-initramfs.sh
+++ b/scripts/instalador-initramfs.sh
@@ -66,8 +66,12 @@ rm -rf "$IRD_TREE"; mkdir -p "$IRD_TREE/bin" "$IRD_TREE/dev" "$IRD_TREE/proc" "$
BUSYBOX="${BUSYBOX:-$STAGE/root/bin/busybox}"
[ -r "$BUSYBOX" ] || { echo "no encuentro busybox estático en $BUSYBOX" >&2; exit 1; }
cp "$BUSYBOX" "$IRD_TREE/bin/busybox"; chmod 0755 "$IRD_TREE/bin/busybox"
+# ⚠ `id` estaba fuera de esta lista y el instalador eligió la rama EQUIVOCADA por eso: sin `id -u`
+# concluyó «no soy root» y se fue a `unshare`, que tampoco existía. Los applets que el instalador usa
+# se enumeran acá y su ausencia ya no es silenciosa (el script aborta si le falta `id`).
for a in sh mount umount mkdir echo sleep ls cat sed awk grep cut tr basename dirname readlink \
- find sync reboot dmesg blkid setsid tee du wc head tail sort printf test sha256sum; do
+ find sync reboot dmesg blkid setsid tee du wc head tail sort printf test sha256sum \
+ id sh chmod rm cp mv stat; do
ln -sf busybox "$IRD_TREE/bin/$a"
done
# Las herramientas de disco NO salen de busybox: son las SELLADAS del corpus, estáticas musl. Dentro
@@ -97,7 +101,9 @@ mount -t sysfs sys /sys 2>/dev/null || true
mount -t devtmpfs dev /dev 2>/dev/null || true
# ⚠ El log va a tty0 **y** al serial, por lo mismo que el initramfs de pivote: el cmdline horneado
# deja /dev/console en ttyS0 y en un metal sin puerto serie todo esto sería invisible.
-log() { echo "$*"; for d in /dev/tty0 /dev/ttyS0; do echo "$*" > "$d" 2>/dev/null || true; done; }
+# El log va SÓLO a los devices: el stdout de PID 1 ya es /dev/console (= ttyS0 por el cmdline
+# horneado), así que un `echo` suelto duplicaba cada línea en el serial.
+log() { for d in /dev/tty0 /dev/ttyS0; do echo "$*" > "$d" 2>/dev/null || true; done; }
rescate() {
log "TAKANA-INSTALADOR-FALLO: $*"
log " bloques visibles: $(ls /dev/sd* /dev/nvme* /dev/vd* /dev/mmcblk* 2>/dev/null | tr '\n' ' ')"
@@ -105,13 +111,31 @@ rescate() {
setsid -c sh -c 'exec /bin/sh </dev/tty1 >/dev/tty1 2>&1' >/dev/null 2>&1 &
exec sh
}
+# efivarfs: sin esto el instalador concluye «esta máquina no arrancó por UEFI» y NO registra la
+# entrada de arranque — en una máquina que sí arrancó por UEFI. Es el mismo diagnóstico falso que ya
+# costó una vuelta en el `/sbin/init` del rootfs: `switch_root` y un initramfs fresco no arrastran
+# /sys, y sin sysfs el directorio no existe. Tras un kexec los servicios EFI de runtime siguen ahí,
+# así que acá esto es lo único que falta.
+if [ -d /sys/firmware/efi ]; then
+ mkdir -p /sys/firmware/efi/efivars 2>/dev/null || true
+ mount -t efivarfs efivarfs /sys/firmware/efi/efivars 2>/dev/null || true
+fi
log "TAKANA-INSTALADOR: arrancado en RAM. El sistema viejo YA NO CORRE: su disco está libre."
+[ -d /sys/firmware/efi/efivars ] && log "TAKANA-INSTALADOR: firmware UEFI visible (efivarfs montado)" \
+ || log "TAKANA-INSTALADOR: sin efivarfs ⇒ no se registrará entrada NVRAM (BIOS, o kernel sin EFI)"
# Los discos tardan en enumerar (USB y NVMe sobre todo). Misma espera larga que el pivote, y por el
# mismo motivo: en QEMU aparecen al instante y la carrera sólo se ve en metal.
+# ⚠ Esto era `ls /dev/nvme* /dev/sd* /dev/vd* … && break` y **esperaba los 60 s completos incluso
+# con el disco presente**: `ls` sale ≠ 0 si CUALQUIERA de sus argumentos no existe, y los globs sin
+# coincidencia quedan literales. O sea que bastaba con no tener NVMe para que un disco virtio ya
+# enumerado no contara. Medido en QEMU: 60 s de «esperando discos» con /dev/vda ahí desde el
+# segundo uno. En metal eso se lee como un arranque colgado.
i=0
while [ "$i" -lt 60 ]; do
- ls /dev/nvme* /dev/sd* /dev/vd* /dev/mmcblk* >/dev/null 2>&1 && break
+ hay=""
+ for d in /dev/vd? /dev/sd? /dev/nvme?n? /dev/mmcblk?; do [ -b "$d" ] && hay=1; done
+ [ -n "$hay" ] && break
i=$(( i + 1 )); [ $(( i % 5 )) -eq 0 ] && log "TAKANA-INSTALADOR: esperando discos… ${i}s"
sleep 1
done
@@ -124,8 +148,14 @@ log "TAKANA-INSTALADOR: takana-install-partition.sh $ARGS"
export TOOL_mke2fs=/bin/mke2fs TOOL_sfdisk=/bin/sfdisk TOOL_mmd=/bin/mmd \
TOOL_mcopy=/bin/mcopy TOOL_mdir=/bin/mdir
export STAGE=/payload/stage STAGE_LISTO=1 KERNEL=/payload/bzImage HAMMER=/bin/takana
+# ⚠ **El estado de un PIPE es el del ÚLTIMO comando, no el del primero.** Esto era
+# `if sh instalador… | tee …; then OK else rescate fi` y el `if` evaluaba el `tee`, que siempre sale
+# 0 ⇒ **el instalador falló, no formateó NADA, y esta pantalla dijo «instalado»**. Medido en QEMU el
+# 2026-09-23, y es el modo de fallo más caro posible: el usuario reinicia confiando y se queda sin
+# sistema. El estado del instalador se guarda aparte y se lee después; el pipe ya no decide nada.
# shellcheck disable=SC2086
-if sh /instalador/takana-install-partition.sh $ARGS --yes 2>&1 | tee /dev/tty0 > /dev/ttyS0; then
+{ sh /instalador/takana-install-partition.sh $ARGS --yes; echo $? > /rc; } 2>&1 | tee /dev/tty0 > /dev/ttyS0
+if [ "$(cat /rc 2>/dev/null || echo 1)" = "0" ]; then
log "TAKANA-INSTALADOR-OK: instalado. Reiniciando en 10 s (sacá el medio si lo hubiera)."
sync; sleep 10; reboot -f
else
diff --git a/scripts/takana-install-partition.sh b/scripts/takana-install-partition.sh
index a5bb4e0c..dc6ce57e 100755
--- a/scripts/takana-install-partition.sh
+++ b/scripts/takana-install-partition.sh
@@ -320,12 +320,26 @@ ROOT_TREE="$STAGE/root"; IRD="$STAGE/initramfs.cpio.gz"
# montaje no es elegancia: un `mount` mal apuntado en un disco ajeno es justamente el accidente que
# este script existe para no tener. Si no somos root, `unshare -r` da los dueños root sin sudo.
echo "==> ext4 LABEL=hammer-root en $DEST (única escritura destructiva de todo el script)"
-if [ "$(id -u)" = "0" ]; then
+# ⚠ La detección de root NO puede fallar en silencio. Medido dentro del initramfs el 2026-09-23: sin
+# el applet `id` el `$(id -u)` quedaba VACÍO, la comparación daba falso, el script se iba a la rama
+# de `unshare` —que tampoco estaba— y **siguió adelante sin formatear nada**. Un `if` cuyo dato de
+# entrada no existe elige una rama igual, y elige mal.
+_uid=$(id -u 2>/dev/null || echo "")
+[ -n "$_uid" ] || { echo "takana-install-partition: no hay 'id' en este entorno y no puedo saber si soy root" >&2; exit 1; }
+if [ "$_uid" = "0" ]; then
"$MKE2FS" -q -t ext4 -L hammer-root -d "$ROOT_TREE" "$DEST"
else
+ command -v unshare >/dev/null 2>&1 || {
+ echo "takana-install-partition: no soy root (uid $_uid) y no hay 'unshare' para suplirlo" >&2; exit 1; }
unshare -r "$MKE2FS" -q -t ext4 -L hammer-root -d "$ROOT_TREE" "$DEST"
fi
-echo " ✓ $DEST formateada y poblada"
+# Y no se da por hecho: se mira. Regla 3 del CLAUDE.md — un ausente falla ruidosamente, un vacío
+# llega hasta el final diciendo que todo fue bien. Acá el vacío sería un destino sin filesystem.
+_lbl_post=$(blkid -s LABEL -o value "$DEST" 2>/dev/null || true)
+[ "$_lbl_post" = "hammer-root" ] || {
+ echo "takana-install-partition: $DEST NO quedó con LABEL=hammer-root (blkid dice '${_lbl_post:-nada}')." >&2
+ echo " El formateo no se completó: no sigo." >&2; exit 1; }
+echo " ✓ $DEST formateada y poblada (LABEL=hammer-root verificada)"
# ── 3) La ESP: sólo nuestra carpeta de vendor, con mtools y SIN montar ─────────────────────────
if [ -n "$ESP" ]; then