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>
This commit is contained in:
sergio
2026-05-20 00:35:31 +00:00
parent 545dd59c72
commit f8a2547b45
3 changed files with 194 additions and 3 deletions
+3
View File
@@ -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
+174
View File
@@ -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 { .. }));
}
}