gioser

tawasuyu/brahman

Rust 483TOML 191Markdown 50JSON 176777 Sutichaykuna

⎇ f8a2547b4516bdb95a3198855202f4d0ca56ed2b crates/init/arje-incarnate/src/pre_exec.rs Rust · 277 siq'ikuna · 9.6 KB Ñawpa willay Pi imata llamkarqan ch'uya

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

1//! Hook declarativo pre-execve para el hijo.
2//!
3//! Las ops corren EN EL HIJO, post-fork/clone, pre-execve. Reglas:
4//! - sólo syscalls async-signal-safe.
5//! - sin allocator (los CStrings ya están construidos por el padre).
6//! - sin Drop con efectos.
7
8use crate::error::IncarnateError;
9use std::ffi::CString;
10use std::path::Path;
11
12/// Operaciones declarativas aplicables pre-execve.
13#[derive(Debug, Clone)]
14pub enum ChildPreExec {
15    /// `PR_SET_NO_NEW_PRIVS = 1` — bloquea escaladas futuras
16    /// (suid bits, file caps, AT_SECURE). Recomendado en sandboxes.
17    NoNewPrivs,
18    /// `PR_SET_PDEATHSIG = sig` — el child recibe esta señal cuando su
19    /// padre (PID 1 del namespace, o el que sea) muere. Útil para
20    /// auto-cleanup de procesos huérfanos.
21    ParentDeathSig(i32),
22    /// `PR_SET_DUMPABLE` — controla si el proceso permite core dump.
23    Dumpable(bool),
24    /// `setsid()` — nuevo session/group leader (desconecta del controlling tty).
25    NewSession,
26    /// `chdir(path)` — cambiar working dir. Path pre-allocado.
27    Chdir(CString),
28    /// `umask(mode)` — fijar umask (octal, e.g. 0o022).
29    Umask(libc::mode_t),
30    /// Monta un OverlayFS en `target`. `options` es la cadena
31    /// `lowerdir=...,upperdir=...,workdir=...` pre-construida por el padre.
32    /// Requiere mount namespace (CLONE_NEWNS) ya establecido.
33    MountOverlay { target: CString, options: CString },
34    /// `pivot_root`: `new_root` pasa a ser `/`. El root viejo va a
35    /// `put_old` (dir existente dentro de new_root) y se desmonta lazy
36    /// (`MNT_DETACH`) tras pivotar. `old_root_after` es la ruta del root
37    /// viejo YA pivotado (e.g. `/.oldroot`). Requiere mount namespace.
38    PivotRoot {
39        new_root: CString,
40        put_old: CString,
41        old_root_after: CString,
42    },
43}
44
45/// Setup completo del hijo. Default = sin ops.
46#[derive(Debug, Clone, Default)]
47pub struct ChildSetup {
48    pub ops: Vec<ChildPreExec>,
49}
50
51impl ChildSetup {
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    pub fn push(&mut self, op: ChildPreExec) -> &mut Self {
57        self.ops.push(op);
58        self
59    }
60
61    pub fn with(mut self, op: ChildPreExec) -> Self {
62        self.ops.push(op);
63        self
64    }
65
66    pub fn is_empty(&self) -> bool {
67        self.ops.is_empty()
68    }
69
70    /// Agrega un mount OverlayFS: `lower` = capa base RO, `upper` = capa
71    /// de sesión RW (persiste cambios), `work` = scratch interno de
72    /// overlayfs, `merged` = mountpoint resultante.
73    ///
74    /// El padre debe garantizar que `upper`, `work` y `merged` existen
75    /// antes de encarnar. Pensado para encadenarse antes de `with_pivot_root`.
76    pub fn with_overlay(
77        mut self,
78        lower: &Path,
79        upper: &Path,
80        work: &Path,
81        merged: &Path,
82    ) -> Result<Self, IncarnateError> {
83        let opts = format!(
84            "lowerdir={},upperdir={},workdir={}",
85            lower.display(),
86            upper.display(),
87            work.display(),
88        );
89        let target = path_cstring(merged)?;
90        let options =
91            CString::new(opts).map_err(|_| IncarnateError::InvalidRootfsPath)?;
92        self.ops.push(ChildPreExec::MountOverlay { target, options });
93        Ok(self)
94    }
95
96    /// Agrega un `pivot_root` a `new_root`. `put_old_name` es el nombre
97    /// del subdirectorio (dentro de new_root, debe existir) que recibe el
98    /// root viejo; tras pivotar se desmonta lazy.
99    pub fn with_pivot_root(
100        mut self,
101        new_root: &Path,
102        put_old_name: &str,
103    ) -> Result<Self, IncarnateError> {
104        let new_root_c = path_cstring(new_root)?;
105        let put_old_c = path_cstring(&new_root.join(put_old_name))?;
106        let old_root_after = CString::new(format!("/{put_old_name}"))
107            .map_err(|_| IncarnateError::InvalidRootfsPath)?;
108        self.ops.push(ChildPreExec::PivotRoot {
109            new_root: new_root_c,
110            put_old: put_old_c,
111            old_root_after,
112        });
113        Ok(self)
114    }
115}
116
117/// Convierte un `Path` a `CString` (rechaza NUL bytes interiores).
118fn path_cstring(p: &Path) -> Result<CString, IncarnateError> {
119    use std::os::unix::ffi::OsStrExt;
120    CString::new(p.as_os_str().as_bytes())
121        .map_err(|_| IncarnateError::InvalidRootfsPath)
122}
123
124/// Aplica las ops en orden. SAFETY: ejecuta en el hijo, post-fork,
125/// pre-execve. Sólo libc, sin allocator, sin Drop.
126///
127/// En caso de error, retorna el código de exit que el caller usará para
128/// abortar el child (igual semántica que el resto de la closure de clone).
129/// 0 = todo OK.
130pub unsafe fn apply_unchecked(ops: &[ChildPreExec]) -> i32 {
131    for op in ops {
132        match op {
133            ChildPreExec::NoNewPrivs => {
134                // PR_SET_NO_NEW_PRIVS = 38 en Linux.
135                let r = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1u64, 0u64, 0u64, 0u64) };
136                if r != 0 {
137                    return 110;
138                }
139            }
140            ChildPreExec::ParentDeathSig(sig) => {
141                let r = unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, *sig as u64, 0u64, 0u64, 0u64) };
142                if r != 0 {
143                    return 111;
144                }
145            }
146            ChildPreExec::Dumpable(yes) => {
147                let v: u64 = if *yes { 1 } else { 0 };
148                let r = unsafe { libc::prctl(libc::PR_SET_DUMPABLE, v, 0u64, 0u64, 0u64) };
149                if r != 0 {
150                    return 112;
151                }
152            }
153            ChildPreExec::NewSession => {
154                let r = unsafe { libc::setsid() };
155                if r < 0 {
156                    return 113;
157                }
158            }
159            ChildPreExec::Chdir(path) => {
160                let r = unsafe { libc::chdir(path.as_ptr()) };
161                if r != 0 {
162                    return 114;
163                }
164            }
165            ChildPreExec::Umask(mode) => {
166                unsafe { libc::umask(*mode) };
167            }
168            ChildPreExec::MountOverlay { target, options } => {
169                let r = unsafe {
170                    libc::mount(
171                        b"overlay\0".as_ptr() as *const libc::c_char,
172                        target.as_ptr(),
173                        b"overlay\0".as_ptr() as *const libc::c_char,
174                        0,
175                        options.as_ptr() as *const libc::c_void,
176                    )
177                };
178                if r != 0 {
179                    return 115;
180                }
181            }
182            ChildPreExec::PivotRoot { new_root, put_old, old_root_after } => {
183                // pivot_root exige que new_root sea un mount point:
184                // bind-mount recursivo sobre sí mismo lo garantiza.
185                let r = unsafe {
186                    libc::mount(
187                        new_root.as_ptr(),
188                        new_root.as_ptr(),
189                        std::ptr::null(),
190                        libc::MS_BIND | libc::MS_REC,
191                        std::ptr::null(),
192                    )
193                };
194                if r != 0 {
195                    return 116;
196                }
197                let r = unsafe {
198                    libc::syscall(libc::SYS_pivot_root, new_root.as_ptr(), put_old.as_ptr())
199                };
200                if r != 0 {
201                    return 117;
202                }
203                let r = unsafe { libc::chdir(b"/\0".as_ptr() as *const libc::c_char) };
204                if r != 0 {
205                    return 118;
206                }
207                // Desmontaje lazy del root viejo: se desliga del árbol ya;
208                // los fds abiertos contra él siguen válidos hasta cerrarse.
209                let r = unsafe {
210                    libc::umount2(old_root_after.as_ptr(), libc::MNT_DETACH)
211                };
212                if r != 0 {
213                    return 119;
214                }
215            }
216        }
217    }
218    0
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn overlay_builds_correct_options() {
227        let s = ChildSetup::new()
228            .with_overlay(
229                Path::new("/base"),
230                Path::new("/sess"),
231                Path::new("/work"),
232                Path::new("/merged"),
233            )
234            .expect("overlay");
235        match &s.ops[0] {
236            ChildPreExec::MountOverlay { target, options } => {
237                assert_eq!(target.to_str().unwrap(), "/merged");
238                assert_eq!(
239                    options.to_str().unwrap(),
240                    "lowerdir=/base,upperdir=/sess,workdir=/work"
241                );
242            }
243            other => panic!("esperaba MountOverlay, fue {other:?}"),
244        }
245    }
246
247    #[test]
248    fn pivot_root_builds_paths() {
249        let s = ChildSetup::new()
250            .with_pivot_root(Path::new("/newroot"), ".oldroot")
251            .expect("pivot");
252        match &s.ops[0] {
253            ChildPreExec::PivotRoot { new_root, put_old, old_root_after } => {
254                assert_eq!(new_root.to_str().unwrap(), "/newroot");
255                assert_eq!(put_old.to_str().unwrap(), "/newroot/.oldroot");
256                assert_eq!(old_root_after.to_str().unwrap(), "/.oldroot");
257            }
258            other => panic!("esperaba PivotRoot, fue {other:?}"),
259        }
260    }
261
262    #[test]
263    fn overlay_then_pivot_preserves_order() {
264        let s = ChildSetup::new()
265            .with_overlay(
266                Path::new("/b"),
267                Path::new("/u"),
268                Path::new("/w"),
269                Path::new("/m"),
270            )
271            .unwrap()
272            .with_pivot_root(Path::new("/m"), ".oldroot")
273            .unwrap();
274        assert!(matches!(s.ops[0], ChildPreExec::MountOverlay { .. }));
275        assert!(matches!(s.ops[1], ChildPreExec::PivotRoot { .. }));
276    }
277}