From f8a2547b4516bdb95a3198855202f4d0ca56ed2b Mon Sep 17 00:00:00 2001 From: sergio Date: Wed, 20 May 2026 00:35:31 +0000 Subject: [PATCH] =?UTF-8?q?feat(arje-incarnate):=20A5=20=E2=80=94=20pivot?= =?UTF-8?q?=5Froot=20+=20OverlayFS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dos ChildPreExec nuevos en el hook declarativo pre-execve: - MountOverlay { target, options } — monta OverlayFS (capa base RO + capa de sesión RW + workdir). - PivotRoot { new_root, put_old, old_root_after } — bind-mount de new_root sobre sí mismo + pivot_root + chdir("/") + umount2 lazy (MNT_DETACH) del root viejo. Builders ergonómicos en ChildSetup: - with_overlay(lower, upper, work, merged) - with_pivot_root(new_root, put_old_name) Ambas ops corren en el hijo post-clone, dentro del mount namespace, async-signal-safe (solo libc, sin allocator). Las consumirán mirada (compositor Wayland) y matilda Ghost para rootfs aislados. 19 tests arje-incarnate verdes (3 nuevos: builders overlay/pivot). cargo check --workspace verde. Pendiente: integration test en entorno con namespaces reales. Co-Authored-By: Claude Opus 4.7 --- crates/init/SDD.md | 20 ++- crates/init/arje-incarnate/src/error.rs | 3 + crates/init/arje-incarnate/src/pre_exec.rs | 174 +++++++++++++++++++++ 3 files changed, 194 insertions(+), 3 deletions(-) diff --git a/crates/init/SDD.md b/crates/init/SDD.md index 43d9a44..a617d7f 100644 --- a/crates/init/SDD.md +++ b/crates/init/SDD.md @@ -12,7 +12,8 @@ cgroups, y snapshot/restore del grafo. | `arje-zero` | binario | PID 1: reap + handshake server + bus dispatcher | | `arje-kernel` | lib | `bootstrap_kernel_surface`, subreaper, SIGCHLD/uevent | | `arje-soma` | lib | Wrapper 44 LOC sobre `arje-incarnate` (compat API) | -| `arje-incarnate` | lib | `clone(2) + namespaces + cgroup + rlimits + cpu` | +| `arje-incarnate` | lib | `clone(2) + namespaces + cgroup + rlimits + cpu` | +| | | + `pivot_root` + OverlayFS (capa base RO + sesión RW)| | `arje-snapshot` | lib | `FractalSnapshot` JSON: checkpoint del grafo Cards | ## Dependencias @@ -29,8 +30,21 @@ cgroups, y snapshot/restore del grafo. 4. Para cada `genesis` child Card: `incarnate(card)` → spawn aislado. 5. Reap loop atiende SIGCHLD; bus loop atiende anuncios/invokes. +## Encarnación de rootfs aislado + +`arje-incarnate` expone vía `ChildSetup` (hook pre-execve declarativo): +- `with_overlay(lower, upper, work, merged)` → monta OverlayFS: + capa base RO + capa de sesión RW + workdir. +- `with_pivot_root(new_root, put_old_name)` → `pivot_root` + chdir + + desmontaje lazy (`MNT_DETACH`) del root viejo. + +Ambas corren en el hijo, post-clone, dentro del mount namespace, +async-signal-safe. Las consumen `mirada` (compositor) y `matilda` Ghost. + ## Estado Funciona bare metal + QEMU + initramfs (ver `docs/arje-boot.md`). LOC -~2.2K en init core. Pendiente: cobertura de tests sobre snapshot -restore en escenarios con stale fds. +~2.2K en init core. `arje-incarnate`: 19 tests verdes (incluye builders +de overlay/pivot_root). Pendiente: integration testing de pivot_root + +OverlayFS en entorno con namespaces reales; cobertura de snapshot +restore con stale fds. diff --git a/crates/init/arje-incarnate/src/error.rs b/crates/init/arje-incarnate/src/error.rs index bd91a39..114e3db 100644 --- a/crates/init/arje-incarnate/src/error.rs +++ b/crates/init/arje-incarnate/src/error.rs @@ -31,6 +31,9 @@ pub enum IncarnateError { #[error("invalid argv: contains NUL byte")] InvalidArgv, + + #[error("rootfs path contains NUL byte (pivot_root / overlayfs)")] + InvalidRootfsPath, } /// Cuando `strict_caps = false`, errores no-fatales se reportan como diff --git a/crates/init/arje-incarnate/src/pre_exec.rs b/crates/init/arje-incarnate/src/pre_exec.rs index 2b07277..763512b 100644 --- a/crates/init/arje-incarnate/src/pre_exec.rs +++ b/crates/init/arje-incarnate/src/pre_exec.rs @@ -5,7 +5,9 @@ //! - sin allocator (los CStrings ya están construidos por el padre). //! - sin Drop con efectos. +use crate::error::IncarnateError; use std::ffi::CString; +use std::path::Path; /// Operaciones declarativas aplicables pre-execve. #[derive(Debug, Clone)] @@ -25,6 +27,19 @@ pub enum ChildPreExec { Chdir(CString), /// `umask(mode)` — fijar umask (octal, e.g. 0o022). Umask(libc::mode_t), + /// Monta un OverlayFS en `target`. `options` es la cadena + /// `lowerdir=...,upperdir=...,workdir=...` pre-construida por el padre. + /// Requiere mount namespace (CLONE_NEWNS) ya establecido. + MountOverlay { target: CString, options: CString }, + /// `pivot_root`: `new_root` pasa a ser `/`. El root viejo va a + /// `put_old` (dir existente dentro de new_root) y se desmonta lazy + /// (`MNT_DETACH`) tras pivotar. `old_root_after` es la ruta del root + /// viejo YA pivotado (e.g. `/.oldroot`). Requiere mount namespace. + PivotRoot { + new_root: CString, + put_old: CString, + old_root_after: CString, + }, } /// Setup completo del hijo. Default = sin ops. @@ -51,6 +66,59 @@ impl ChildSetup { pub fn is_empty(&self) -> bool { self.ops.is_empty() } + + /// Agrega un mount OverlayFS: `lower` = capa base RO, `upper` = capa + /// de sesión RW (persiste cambios), `work` = scratch interno de + /// overlayfs, `merged` = mountpoint resultante. + /// + /// El padre debe garantizar que `upper`, `work` y `merged` existen + /// antes de encarnar. Pensado para encadenarse antes de `with_pivot_root`. + pub fn with_overlay( + mut self, + lower: &Path, + upper: &Path, + work: &Path, + merged: &Path, + ) -> Result { + let opts = format!( + "lowerdir={},upperdir={},workdir={}", + lower.display(), + upper.display(), + work.display(), + ); + let target = path_cstring(merged)?; + let options = + CString::new(opts).map_err(|_| IncarnateError::InvalidRootfsPath)?; + self.ops.push(ChildPreExec::MountOverlay { target, options }); + Ok(self) + } + + /// Agrega un `pivot_root` a `new_root`. `put_old_name` es el nombre + /// del subdirectorio (dentro de new_root, debe existir) que recibe el + /// root viejo; tras pivotar se desmonta lazy. + pub fn with_pivot_root( + mut self, + new_root: &Path, + put_old_name: &str, + ) -> Result { + let new_root_c = path_cstring(new_root)?; + let put_old_c = path_cstring(&new_root.join(put_old_name))?; + let old_root_after = CString::new(format!("/{put_old_name}")) + .map_err(|_| IncarnateError::InvalidRootfsPath)?; + self.ops.push(ChildPreExec::PivotRoot { + new_root: new_root_c, + put_old: put_old_c, + old_root_after, + }); + Ok(self) + } +} + +/// Convierte un `Path` a `CString` (rechaza NUL bytes interiores). +fn path_cstring(p: &Path) -> Result { + use std::os::unix::ffi::OsStrExt; + CString::new(p.as_os_str().as_bytes()) + .map_err(|_| IncarnateError::InvalidRootfsPath) } /// Aplica las ops en orden. SAFETY: ejecuta en el hijo, post-fork, @@ -97,7 +165,113 @@ pub unsafe fn apply_unchecked(ops: &[ChildPreExec]) -> i32 { ChildPreExec::Umask(mode) => { unsafe { libc::umask(*mode) }; } + ChildPreExec::MountOverlay { target, options } => { + let r = unsafe { + libc::mount( + b"overlay\0".as_ptr() as *const libc::c_char, + target.as_ptr(), + b"overlay\0".as_ptr() as *const libc::c_char, + 0, + options.as_ptr() as *const libc::c_void, + ) + }; + if r != 0 { + return 115; + } + } + ChildPreExec::PivotRoot { new_root, put_old, old_root_after } => { + // pivot_root exige que new_root sea un mount point: + // bind-mount recursivo sobre sí mismo lo garantiza. + let r = unsafe { + libc::mount( + new_root.as_ptr(), + new_root.as_ptr(), + std::ptr::null(), + libc::MS_BIND | libc::MS_REC, + std::ptr::null(), + ) + }; + if r != 0 { + return 116; + } + let r = unsafe { + libc::syscall(libc::SYS_pivot_root, new_root.as_ptr(), put_old.as_ptr()) + }; + if r != 0 { + return 117; + } + let r = unsafe { libc::chdir(b"/\0".as_ptr() as *const libc::c_char) }; + if r != 0 { + return 118; + } + // Desmontaje lazy del root viejo: se desliga del árbol ya; + // los fds abiertos contra él siguen válidos hasta cerrarse. + let r = unsafe { + libc::umount2(old_root_after.as_ptr(), libc::MNT_DETACH) + }; + if r != 0 { + return 119; + } + } } } 0 } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn overlay_builds_correct_options() { + let s = ChildSetup::new() + .with_overlay( + Path::new("/base"), + Path::new("/sess"), + Path::new("/work"), + Path::new("/merged"), + ) + .expect("overlay"); + match &s.ops[0] { + ChildPreExec::MountOverlay { target, options } => { + assert_eq!(target.to_str().unwrap(), "/merged"); + assert_eq!( + options.to_str().unwrap(), + "lowerdir=/base,upperdir=/sess,workdir=/work" + ); + } + other => panic!("esperaba MountOverlay, fue {other:?}"), + } + } + + #[test] + fn pivot_root_builds_paths() { + let s = ChildSetup::new() + .with_pivot_root(Path::new("/newroot"), ".oldroot") + .expect("pivot"); + match &s.ops[0] { + ChildPreExec::PivotRoot { new_root, put_old, old_root_after } => { + assert_eq!(new_root.to_str().unwrap(), "/newroot"); + assert_eq!(put_old.to_str().unwrap(), "/newroot/.oldroot"); + assert_eq!(old_root_after.to_str().unwrap(), "/.oldroot"); + } + other => panic!("esperaba PivotRoot, fue {other:?}"), + } + } + + #[test] + fn overlay_then_pivot_preserves_order() { + let s = ChildSetup::new() + .with_overlay( + Path::new("/b"), + Path::new("/u"), + Path::new("/w"), + Path::new("/m"), + ) + .unwrap() + .with_pivot_root(Path::new("/m"), ".oldroot") + .unwrap(); + assert!(matches!(s.ops[0], ChildPreExec::MountOverlay { .. })); + assert!(matches!(s.ops[1], ChildPreExec::PivotRoot { .. })); + } +}