f8a2547b45
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>
278 lines
9.6 KiB
Rust
278 lines
9.6 KiB
Rust
//! Hook declarativo pre-execve para el hijo.
|
|
//!
|
|
//! Las ops corren EN EL HIJO, post-fork/clone, pre-execve. Reglas:
|
|
//! - sólo syscalls async-signal-safe.
|
|
//! - 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)]
|
|
pub enum ChildPreExec {
|
|
/// `PR_SET_NO_NEW_PRIVS = 1` — bloquea escaladas futuras
|
|
/// (suid bits, file caps, AT_SECURE). Recomendado en sandboxes.
|
|
NoNewPrivs,
|
|
/// `PR_SET_PDEATHSIG = sig` — el child recibe esta señal cuando su
|
|
/// padre (PID 1 del namespace, o el que sea) muere. Útil para
|
|
/// auto-cleanup de procesos huérfanos.
|
|
ParentDeathSig(i32),
|
|
/// `PR_SET_DUMPABLE` — controla si el proceso permite core dump.
|
|
Dumpable(bool),
|
|
/// `setsid()` — nuevo session/group leader (desconecta del controlling tty).
|
|
NewSession,
|
|
/// `chdir(path)` — cambiar working dir. Path pre-allocado.
|
|
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.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct ChildSetup {
|
|
pub ops: Vec<ChildPreExec>,
|
|
}
|
|
|
|
impl ChildSetup {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn push(&mut self, op: ChildPreExec) -> &mut Self {
|
|
self.ops.push(op);
|
|
self
|
|
}
|
|
|
|
pub fn with(mut self, op: ChildPreExec) -> Self {
|
|
self.ops.push(op);
|
|
self
|
|
}
|
|
|
|
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,
|
|
/// pre-execve. Sólo libc, sin allocator, sin Drop.
|
|
///
|
|
/// En caso de error, retorna el código de exit que el caller usará para
|
|
/// abortar el child (igual semántica que el resto de la closure de clone).
|
|
/// 0 = todo OK.
|
|
pub unsafe fn apply_unchecked(ops: &[ChildPreExec]) -> i32 {
|
|
for op in ops {
|
|
match op {
|
|
ChildPreExec::NoNewPrivs => {
|
|
// PR_SET_NO_NEW_PRIVS = 38 en Linux.
|
|
let r = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1u64, 0u64, 0u64, 0u64) };
|
|
if r != 0 {
|
|
return 110;
|
|
}
|
|
}
|
|
ChildPreExec::ParentDeathSig(sig) => {
|
|
let r = unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, *sig as u64, 0u64, 0u64, 0u64) };
|
|
if r != 0 {
|
|
return 111;
|
|
}
|
|
}
|
|
ChildPreExec::Dumpable(yes) => {
|
|
let v: u64 = if *yes { 1 } else { 0 };
|
|
let r = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, v, 0u64, 0u64, 0u64) };
|
|
if r != 0 {
|
|
return 112;
|
|
}
|
|
}
|
|
ChildPreExec::NewSession => {
|
|
let r = unsafe { libc::setsid() };
|
|
if r < 0 {
|
|
return 113;
|
|
}
|
|
}
|
|
ChildPreExec::Chdir(path) => {
|
|
let r = unsafe { libc::chdir(path.as_ptr()) };
|
|
if r != 0 {
|
|
return 114;
|
|
}
|
|
}
|
|
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 { .. }));
|
|
}
|
|
}
|