⎇ f8a2547b4516bdb95a3198855202f4d0ca56ed2b
crates/init/arje-incarnate/src/pre_exec.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(arje-incarnate): A5 — pivot_root + OverlayFS
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}