tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 83JSON 198635 Definiciones

feat(arje-incarnate): A5 — pivot_root + OverlayFS

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 <noreply@anthropic.com>

f8a2547b4516bdb95a3198855202f4d0ca56ed2b · por sergio · (hace 5 meses) · padre 545dd59c7 · ver el árbol en este commit

Delta semántico · 10

crates/init/arje-incarnate/src/error.rs

crates/init/arje-incarnate/src/pre_exec.rs

3 archivos · +194 −3

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<Self, IncarnateError> {
+        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<Self, IncarnateError> {
+        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<CString, IncarnateError> {
+    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 { .. }));
+    }
+}