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::( - Some("proc"), "/proc", Some("proc"), - MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV, None, - ).ok(); - mount::( - Some("sysfs"), "/sys", Some("sysfs"), - MsFlags::MS_NOSUID | MsFlags::MS_NOEXEC | MsFlags::MS_NODEV, None, - ).ok(); - mount::( - Some("devtmpfs"), "/dev", Some("devtmpfs"), - MsFlags::MS_NOSUID, None, - ).ok(); - mount::( - 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::( + 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::(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::(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 { + 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// (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 </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