tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Sutichaykuna

fix(init): el reboot-loop de Fedora — remount rw + /run tmpfs + shell de rescate

Diagnóstico: en el VPS Fedora arje-zero caía como PID 1 y el cmdline
traía `panic=10`, así que el kernel rebooteaba cada 10 s. Tres causas
encadenadas, todas arregladas:

1) **Cmdline `ro` + sin `/run` tmpfs.** El menuentry montaba `/` como
   sólo lectura (systemd lo remonta rw temprano; arje no). Sin eso, el
   socket del bus interno se intenta crear sobre un FS de sólo lectura
   y falla con EROFS → spawn_bus devuelve Err → PID 1 sale → kernel
   panic. arje-kernel ahora remonta `/` rw en el bootstrap y monta
   `/run`, `/tmp`, `/dev/pts`, `/dev/shm` como tmpfs — superficies
   escribibles aunque la raíz quede ro.

2) **PID 1 saliendo en cualquier `?`.** Doctrina dura nueva: PID 1
   NUNCA puede salir. Cualquier error de arranque ahora cae a una
   `emergency_shell()` que imprime el diagnóstico en `/dev/console`,
   abre `/bin/sh` y, si la shell muere, la reabre — así el operador
   puede reparar en vez de mirar la máquina reiniciarse en bucle.

3) **El script no conocía grub2 (Fedora).** `install-arje-as-init.sh`
   sólo probaba `update-grub` (Debian) y `grub-mkconfig` (Arch). Ahora
   detecta `grub2-mkconfig` y resuelve el `grub.cfg` correcto
   (UEFI/BIOS, fedora/redhat/centos/almalinux/rocky). El menuentry
   también pasa de `ro` a `rw` — el remount es belt-and-suspenders.
   Mismo arreglo en `uninstall-arje.sh`.

Renaser intacto: estos cambios son Linux-side puro (arje-kernel y
arje-zero usan nix/libc/tracing); renaser sólo comparte mirada-layout y
formato, ninguno tocado.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

d1b700eb2bda9fa5f6f913d36f6ce1fb1e6d290b · ruwaq sergio · (5 killa ñawpaq) · tayta 922ad1f86 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 6

crates/init/arje-kernel/src/surface.rs

crates/init/arje-zero/src/main.rs

4 qillqakuna · +185 −32

diff --git a/crates/init/arje-kernel/src/surface.rs b/crates/init/arje-kernel/src/surface.rs
index 0f50bb3..ab395a3 100644
--- a/crates/init/arje-kernel/src/surface.rs
+++ b/crates/init/arje-kernel/src/surface.rs
@@ -1,32 +1,65 @@
-//! Bootstrap del entorno kernel para PID 1: monta procfs/sysfs/devtmpfs/cgroup2
-//! y registra al proceso como subreaper para adoptar huérfanos.
+//! Bootstrap del entorno kernel para PID 1: remonta `/` rw, monta
+//! procfs/sysfs/devtmpfs/cgroup2 y las superficies escribibles volátiles
+//! (`/run`, `/tmp`, `/dev/pts`, `/dev/shm`), y registra al proceso como
+//! subreaper para adoptar huérfanos.
 //!
-//! Idempotente: si los puntos de montaje ya existen (initramfs los montó),
-//! el segundo mount falla con EBUSY y simplemente lo ignoramos.
+//! Idempotente: si un punto de montaje ya existe (lo montó el initramfs),
+//! el segundo mount falla con EBUSY y simplemente se ignora.
+//!
+//! **Por qué importa `/run`:** el cmdline de arranque suele traer `ro`
+//! (systemd remonta rw temprano; nosotros también debemos). Sin remontar
+//! `/` y sin `/run` como tmpfs, crear el socket del bus interno falla con
+//! EROFS — y PID 1 moriría, provocando un kernel panic. Esta función es
+//! infalible a propósito: devuelve `Ok` siempre y sólo loggea los fallos.
 
 use nix::mount::{mount, MsFlags};
-use tracing::debug;
+use tracing::{debug, warn};
 
-/// Monta los pseudo-filesystems esenciales. Errores benignos (ya montados)
-/// se ignoran; errores serios se propagan.
+/// Prepara el entorno del kernel para PID 1. Nunca falla de forma dura:
+/// cada paso es best-effort y los problemas se loggean, porque un `Err`
+/// que llegue hasta `main` terminaría PID 1.
 pub fn bootstrap_kernel_surface() -> anyhow::Result<()> {
-    // Cada uno con sus flags estándar — NOSUID/NOEXEC/NODEV donde aplica.
-    mount::<str, str, str, str>(
-        Some("proc"), "/proc", Some("proc"),
-        MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV, None,
-    ).ok();
-    mount::<str, str, str, str>(
-        Some("sysfs"), "/sys", Some("sysfs"),
-        MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV, None,
-    ).ok();
-    mount::<str, str, str, str>(
-        Some("devtmpfs"), "/dev", Some("devtmpfs"),
-        MsFlags::MS_NOSUID, None,
-    ).ok();
-    mount::<str, str, str, str>(
-        Some("cgroup2"), "/sys/fs/cgroup", Some("cgroup2"),
-        MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV, None,
-    ).ok();
+    // 1) Remontar `/` lectura-escritura. El cmdline casi siempre trae
+    //    `ro`; sin esto el resto del sistema queda de sólo lectura.
+    if let Err(e) = mount::<str, str, str, str>(
+        None, "/", None, MsFlags::MS_REMOUNT, None,
+    ) {
+        warn!(?e, "remount / rw falló — el sistema puede quedar de sólo lectura");
+    }
+
+    // 2) Pseudo-filesystems del kernel. NOSUID/NOEXEC/NODEV donde aplica.
+    let pseudo: [(&str, &str, &str, MsFlags); 4] = [
+        ("proc", "/proc", "proc",
+            MsFlags::MS_NOSUID.union(MsFlags::MS_NOEXEC).union(MsFlags::MS_NODEV)),
+        ("sysfs", "/sys", "sysfs",
+            MsFlags::MS_NOSUID.union(MsFlags::MS_NOEXEC).union(MsFlags::MS_NODEV)),
+        ("devtmpfs", "/dev", "devtmpfs", MsFlags::MS_NOSUID),
+        ("cgroup2", "/sys/fs/cgroup", "cgroup2",
+            MsFlags::MS_NOSUID.union(MsFlags::MS_NOEXEC).union(MsFlags::MS_NODEV)),
+    ];
+    for (src, dst, fstype, flags) in pseudo {
+        let _ = mount::<str, str, str, str>(Some(src), dst, Some(fstype), flags, None);
+    }
+
+    // 3) Superficies escribibles volátiles. `/run` como tmpfs es lo que
+    //    permite crear el socket del bus interno aun con `/` de sólo
+    //    lectura. `mkdir` best-effort antes de cada montaje.
+    let volatile: [(&str, &str, &str, MsFlags, &str); 4] = [
+        ("tmpfs", "/run", "tmpfs",
+            MsFlags::MS_NOSUID.union(MsFlags::MS_NODEV), "mode=0755"),
+        ("tmpfs", "/tmp", "tmpfs",
+            MsFlags::MS_NOSUID.union(MsFlags::MS_NODEV), "mode=1777"),
+        ("devpts", "/dev/pts", "devpts",
+            MsFlags::MS_NOSUID.union(MsFlags::MS_NOEXEC), "mode=0620,gid=5"),
+        ("tmpfs", "/dev/shm", "tmpfs",
+            MsFlags::MS_NOSUID.union(MsFlags::MS_NODEV), "mode=1777"),
+    ];
+    for (src, dst, fstype, flags, data) in volatile {
+        let _ = std::fs::create_dir_all(dst);
+        let _ = mount::<str, str, str, str>(Some(src), dst, Some(fstype), flags, Some(data));
+    }
+    let _ = std::fs::create_dir_all("/run/lock");
+
     debug!("kernel surface bootstrap completo");
     Ok(())
 }
diff --git a/crates/init/arje-zero/src/main.rs b/crates/init/arje-zero/src/main.rs
index d4f7932..18c3d3e 100644
--- a/crates/init/arje-zero/src/main.rs
+++ b/crates/init/arje-zero/src/main.rs
@@ -83,8 +83,27 @@ fn main() -> anyhow::Result<()> {
 
     if dev_mode {
         warn!(?pid, "ente-zero corriendo en DEV MODE (no PID 1) — kernel surface no se monta");
-    } else {
-        info!("ente-zero despierta como PID 1");
+        return run(cli, true);
+    }
+
+    info!("ente-zero despierta como PID 1");
+    // Doctrina dura: PID 1 NUNCA puede salir — el kernel haría panic
+    // ("Attempted to kill init") y, con `panic=N` en el cmdline, la
+    // máquina cae en un reboot-loop. Por eso cualquier fallo de arranque
+    // se desvía a una shell de rescate: deja diagnosticar y reparar en
+    // vez de reiniciar a ciegas cada diez segundos.
+    match run(cli, false) {
+        Ok(()) => emergency_shell(
+            "el bucle primordial terminó — el fractal pidió shutdown",
+        ),
+        Err(e) => emergency_shell(&format!("{e:#}")),
+    }
+}
+
+/// Arranque + bucle primordial. En PID 1, cualquier `Err` que devuelva
+/// lo intercepta `main` y lo convierte en shell de rescate.
+fn run(cli: CliArgs, dev_mode: bool) -> anyhow::Result<()> {
+    if !dev_mode {
         bootstrap_kernel_surface().context("bootstrap kernel surface")?;
         become_child_subreaper().context("PR_SET_CHILD_SUBREAPER")?;
     }
@@ -105,6 +124,75 @@ fn main() -> anyhow::Result<()> {
     ))
 }
 
+/// Último recurso de PID 1: imprime el diagnóstico en la consola y abre
+/// una shell de rescate. **Nunca retorna** — si lo hiciera, el proceso
+/// saldría y el kernel haría panic.
+fn emergency_shell(reason: &str) -> ! {
+    let banner = format!(
+        "\n\n\
+         ===============  arje-zero — ARRANQUE FALLIDO  ================\n\
+         {reason}\n\
+         ---------------------------------------------------------------\n\
+         Se abre una shell de rescate sobre la consola. Revisá el sistema\n\
+         (p. ej. /ente/seed.card.json y los binarios en /usr/sbin) y\n\
+         reiniciá con `reboot -f`. Salir de la shell la vuelve a abrir.\n\n",
+    );
+    write_to_console(&banner);
+    error!(reason = %reason.replace('\n', " "), "arranque de PID 1 fallido");
+    loop {
+        match spawn_console_shell() {
+            Ok(status) => write_to_console(&format!(
+                "\n[arje-zero] la shell de rescate terminó ({status}) — reabriendo.\n",
+            )),
+            Err(e) => {
+                write_to_console(&format!(
+                    "\n[arje-zero] no hay shell de rescate disponible: {e}\n\
+                     PID 1 queda en espera pasiva — usá la consola del proveedor.\n",
+                ));
+                loop {
+                    std::thread::sleep(Duration::from_secs(3600));
+                }
+            }
+        }
+    }
+}
+
+/// Abre una shell interactiva con stdin/stdout/stderr sobre
+/// `/dev/console` y espera a que termine.
+fn spawn_console_shell() -> std::io::Result<std::process::ExitStatus> {
+    let shell = ["/bin/sh", "/bin/bash", "/usr/bin/sh", "/usr/bin/bash"]
+        .into_iter()
+        .find(|p| std::path::Path::new(p).exists())
+        .ok_or_else(|| {
+            std::io::Error::new(
+                std::io::ErrorKind::NotFound,
+                "ninguna shell en /bin ni /usr/bin",
+            )
+        })?;
+    let console = std::fs::OpenOptions::new()
+        .read(true)
+        .write(true)
+        .open("/dev/console")?;
+    std::process::Command::new(shell)
+        .stdin(console.try_clone()?)
+        .stdout(console.try_clone()?)
+        .stderr(console)
+        .env("HOME", "/root")
+        .env("PATH", "/usr/sbin:/usr/bin:/sbin:/bin")
+        .env("PS1", "arje-rescate# ")
+        .env("TERM", "linux")
+        .status()
+}
+
+/// Escribe un mensaje directo a `/dev/console`, con stderr de respaldo.
+fn write_to_console(msg: &str) {
+    use std::io::Write;
+    if let Ok(mut f) = std::fs::OpenOptions::new().write(true).open("/dev/console") {
+        let _ = f.write_all(msg.as_bytes());
+    }
+    eprint!("{msg}");
+}
+
 async fn primordial_loop(
     seed_card: arje_card::EntityCard,
     dev_mode: bool,
diff --git a/scripts/install-arje-as-init.sh b/scripts/install-arje-as-init.sh
index 5e772d9..fbe0ba7 100755
--- a/scripts/install-arje-as-init.sh
+++ b/scripts/install-arje-as-init.sh
@@ -133,23 +133,44 @@ menuentry "arje (init=/sbin/arje-zero) — kernel $KVER" {
     insmod gzio
     insmod part_msdos
     insmod ext2
-    linux  $VMLINUZ $ROOT_OPT ro init=/sbin/arje-zero console=tty1 console=ttyS0,115200 panic=10
+    linux  $VMLINUZ $ROOT_OPT rw init=/sbin/arje-zero console=tty1 console=ttyS0,115200 panic=10
     initrd $INITRD
 }
 # END ARJE-MENUENTRY
 EOF
 chmod 0755 "$CUSTOM"
 
-# Regenerar grub.cfg.
+# Regenerar grub.cfg. Detectamos la convención de la distro: Debian /
+# Ubuntu usan `update-grub` (wrapper de grub-mkconfig). Fedora / RHEL /
+# CentOS usan `grub2-mkconfig` y el .cfg vive en /boot/grub2/ (BIOS) o
+# /boot/efi/EFI/<distro>/ (UEFI). Arch usa `grub-mkconfig`.
 if command -v update-grub >/dev/null 2>&1; then
-  echo "  ejecutando update-grub"
+  echo "  ejecutando update-grub (Debian/Ubuntu)"
   update-grub
+elif command -v grub2-mkconfig >/dev/null 2>&1; then
+  # Fedora / RHEL: buscar el grub.cfg activo. UEFI primero — su path es
+  # el que arranca realmente; el de BIOS suele ser un symlink al UEFI.
+  GRUB_CFG=""
+  for cand in /boot/efi/EFI/fedora/grub.cfg \
+              /boot/efi/EFI/redhat/grub.cfg \
+              /boot/efi/EFI/centos/grub.cfg \
+              /boot/efi/EFI/almalinux/grub.cfg \
+              /boot/efi/EFI/rocky/grub.cfg \
+              /boot/grub2/grub.cfg; do
+    if [ -f "$cand" ] || [ -L "$cand" ]; then GRUB_CFG="$cand"; break; fi
+  done
+  if [ -z "$GRUB_CFG" ]; then
+    GRUB_CFG="/boot/grub2/grub.cfg"
+    echo "  no detecté grub.cfg existente — usando $GRUB_CFG por default"
+  fi
+  echo "  ejecutando grub2-mkconfig -o $GRUB_CFG (Fedora/RHEL)"
+  grub2-mkconfig -o "$GRUB_CFG"
 elif command -v grub-mkconfig >/dev/null 2>&1; then
-  echo "  ejecutando grub-mkconfig"
+  echo "  ejecutando grub-mkconfig (Arch/otros)"
   grub-mkconfig -o /boot/grub/grub.cfg
 else
-  echo "[install-arje] WARN: no encontré update-grub ni grub-mkconfig." >&2
-  echo "  Regenerá grub.cfg manualmente." >&2
+  echo "[install-arje] WARN: no encontré update-grub, grub2-mkconfig ni grub-mkconfig." >&2
+  echo "  Regenerá la grub.cfg manualmente." >&2
 fi
 
 cat <<EOF
diff --git a/scripts/uninstall-arje.sh b/scripts/uninstall-arje.sh
index 017cb60..8722528 100755
--- a/scripts/uninstall-arje.sh
+++ b/scripts/uninstall-arje.sh
@@ -38,6 +38,17 @@ fi
 
 if command -v update-grub >/dev/null 2>&1; then
   update-grub
+elif command -v grub2-mkconfig >/dev/null 2>&1; then
+  GRUB_CFG=""
+  for cand in /boot/efi/EFI/fedora/grub.cfg \
+              /boot/efi/EFI/redhat/grub.cfg \
+              /boot/efi/EFI/centos/grub.cfg \
+              /boot/efi/EFI/almalinux/grub.cfg \
+              /boot/efi/EFI/rocky/grub.cfg \
+              /boot/grub2/grub.cfg; do
+    if [ -f "$cand" ] || [ -L "$cand" ]; then GRUB_CFG="$cand"; break; fi
+  done
+  grub2-mkconfig -o "${GRUB_CFG:-/boot/grub2/grub.cfg}"
 elif command -v grub-mkconfig >/dev/null 2>&1; then
   grub-mkconfig -o /boot/grub/grub.cfg
 fi