gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Sutichaykuna

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

refactor(naming): A1 — ente→arje, vista→revista, pluma→fana b83d40a83 · sergio ·

1//! `ente-incarnate` — rutina extraída del Init para encarnar Cards en
2//! procesos aislados (clone(2) + namespaces + cgroup + rlimits + cpu affinity).
3//!
4//! El núcleo histórico vivía en `ente-soma` con globals dependientes de PID 1.
5//! Este crate elimina esos globals: se construye un [`Incarnator`] por
6//! supervisor (Init, shuma, etc.), cada uno con su propio bus socket y su
7//! propia política de capacidades.
8//!
9//! ## Limitaciones que NO desaparecen al extraer
10//!
11//! 1. `mount/pid/net/uts/ipc/cgroup` namespaces requieren `CAP_SYS_ADMIN`
12//!    o estar combinados con `CLONE_NEWUSER` en el mismo `clone(2)`.
13//! 2. `user` namespace puede estar bloqueado por
14//!    `kernel.unprivileged_userns_clone=0` o por LSM (apparmor/selinux).
15//! 3. cgroups v2 requieren delegación (sistemas modernos: systemd
16//!    `Delegate=yes`). Sin delegación, escribir en `/sys/fs/cgroup` falla.
17//! 4. El primer proceso de un PID namespace es PID 1 *de ese ns*; si muere
18//!    el kernel mata el namespace entero.
19//!
20//! [`CapabilitySet::detect`] reporta lo que está disponible runtime;
21//! [`Incarnator::dry_run`] valida un [`Card`] antes de ejecutar.
22
23#![doc(html_no_source)]
24
25pub mod caps;
26pub mod cgroup;
27pub mod child;
28pub mod env;
29pub mod error;
30pub mod namespaced;
31pub mod plain;
32pub mod pre_exec;
33
34pub use brahman_card::Card;
35pub use caps::{CapabilitySet, CgroupStatus, NsKind, UserNsStatus};
36pub use env::{EnvSpec, ENV_BUS_SOCK, ENV_ENTE_ID};
37pub use error::{Degradation, IncarnateError};
38pub use pre_exec::{ChildPreExec, ChildSetup};
39
40use std::os::fd::RawFd;
41
42/// Redirección declarativa de stdio del hijo. Cada `Some(fd)` se `dup2`-ea
43/// como stdin/stdout/stderr en el hijo.
44///
45/// **Contrato de ownership**: el caller transfiere ownership de los FDs al
46/// `Incarnator` (igual que pasaría a `Command::stdio(Stdio::from_raw_fd)`).
47/// `Incarnator` se encarga de cerrarlos en el padre tras `incarnate` (path
48/// namespaced) o de dejar que `std::process::Command` los absorba (path
49/// plain). **No los cierres en el caller** — habría doble-close.
50///
51/// Útil para conectar pipes entre procesos del pipeline de shuma sin
52/// romper la regla async-signal-safe del callback de clone(2).
53#[derive(Debug, Clone, Copy, Default)]
54pub struct ChildStdio {
55    pub stdin_fd: Option<RawFd>,
56    pub stdout_fd: Option<RawFd>,
57    pub stderr_fd: Option<RawFd>,
58}
59
60impl ChildStdio {
61    pub fn is_some(&self) -> bool {
62        self.stdin_fd.is_some() || self.stdout_fd.is_some() || self.stderr_fd.is_some()
63    }
64}
65
66use nix::unistd::Pid;
67use std::path::PathBuf;
68
69#[derive(Debug, Clone, Default)]
70pub struct IncarnatorConfig {
71    /// Path del Unix socket del bus interno (se inyecta como `ENTE_BUS_SOCK`).
72    /// `None` = no inyectar.
73    pub bus_sock: Option<PathBuf>,
74
75    /// Inyectar `NOTIFY_SOCKET` (legacy sd_notify). Default `None`.
76    /// `ente-zero` lo pasa = `Some("/run/systemd/notify")`.
77    pub notify_socket: Option<PathBuf>,
78
79    /// Vars adicionales que el caller fuerza en cada hijo.
80    pub extra_env: Vec<(String, String)>,
81
82    /// Si `true`, falta de capacidades aborta `incarnate()` con error.
83    /// Si `false`, se reportan como `Degradation` y la encarnación continúa
84    /// con menos aislamiento (semántica histórica del Init).
85    pub strict_caps: bool,
86}
87
88pub struct Incarnator {
89    cfg: IncarnatorConfig,
90    caps: CapabilitySet,
91}
92
93#[derive(Debug, Clone)]
94pub struct IncarnateOutcome {
95    pub pid: Pid,
96    pub degradations: Vec<Degradation>,
97}
98
99#[derive(Debug, Default, Clone)]
100pub struct ValidationReport {
101    pub will_work: bool,
102    pub blocking: Vec<String>,
103    pub warnings: Vec<String>,
104}
105
106impl Incarnator {
107    pub fn new(cfg: IncarnatorConfig) -> Self {
108        Self {
109            caps: CapabilitySet::detect(),
110            cfg,
111        }
112    }
113
114    /// Constructor para testing/inyección de capacidades pre-calculadas.
115    pub fn with_caps(cfg: IncarnatorConfig, caps: CapabilitySet) -> Self {
116        Self { cfg, caps }
117    }
118
119    pub fn capabilities(&self) -> &CapabilitySet {
120        &self.caps
121    }
122
123    pub fn config(&self) -> &IncarnatorConfig {
124        &self.cfg
125    }
126
127    /// Valida una Card sin ejecutar nada. Útil para que el caller (shuma,
128    /// admin, tests) sepa de antemano si va a poder encarnar tal cual o si
129    /// va a tener que aflojar el SomaSpec.
130    pub fn dry_run(&self, card: &Card) -> ValidationReport {
131        let mut r = ValidationReport {
132            will_work: true,
133            ..Default::default()
134        };
135        let ns = &card.soma.namespaces;
136
137        // Si user_ns está pedido, evaluar su disponibilidad.
138        if ns.user {
139            match self.caps.user_ns {
140                UserNsStatus::DisabledBySysctl => {
141                    r.blocking.push(
142                        "user namespace requested but kernel.unprivileged_userns_clone=0".into(),
143                    );
144                    r.will_work = false;
145                }
146                UserNsStatus::RestrictedByLsm => {
147                    r.blocking.push(
148                        "user namespace restricted by LSM (apparmor/selinux)".into(),
149                    );
150                    r.will_work = false;
151                }
152                _ => {}
153            }
154        }
155
156        // El resto de namespaces necesitan CAP_SYS_ADMIN o user ns.
157        let needs_priv = [
158            (ns.mount, NsKind::Mount),
159            (ns.pid, NsKind::Pid),
160            (ns.net, NsKind::Net),
161            (ns.uts, NsKind::Uts),
162            (ns.ipc, NsKind::Ipc),
163            (ns.cgroup, NsKind::Cgroup),
164        ];
165        for (wanted, kind) in needs_priv {
166            if wanted && !self.caps.can_create_ns(kind) {
167                r.blocking.push(format!(
168                    "{} namespace requires CAP_SYS_ADMIN or user ns (neither available)",
169                    kind.name()
170                ));
171                r.will_work = false;
172            }
173        }
174
175        // Cgroup: si el card pide path, chequear que tengamos delegación.
176        if !card.soma.cgroup.path.is_empty() && !self.caps.cgroup_delegated {
177            r.warnings.push(format!(
178                "cgroup `{}` requested but our cgroup is not writable (delegation missing)",
179                card.soma.cgroup.path
180            ));
181        }
182
183        // Payload ejecutable.
184        use brahman_card::Payload;
185        if !matches!(card.payload, Payload::Native { .. } | Payload::Legacy { .. }) {
186            r.blocking
187                .push("payload is not Native/Legacy (use ente-wasm for Wasm)".into());
188            r.will_work = false;
189        }
190
191        r
192    }
193
194    /// Encarna la Card. Si `strict_caps`, valida primero y aborta ante
195    /// blocking. Si no, ejecuta y deja que las degradaciones se acumulen.
196    pub fn incarnate(&self, card: &Card) -> Result<IncarnateOutcome, IncarnateError> {
197        self.incarnate_with(card, ChildStdio::default())
198    }
199
200    /// Variante con redirección de stdio declarativa. Útil para conectar
201    /// pipes entre procesos (caso: pipeline aislado).
202    pub fn incarnate_with(
203        &self,
204        card: &Card,
205        stdio: ChildStdio,
206    ) -> Result<IncarnateOutcome, IncarnateError> {
207        self.incarnate_full(card, stdio, ChildSetup::default())
208    }
209
210    /// Variante full: stdio + setup pre-execve.
211    pub fn incarnate_full(
212        &self,
213        card: &Card,
214        stdio: ChildStdio,
215        setup: ChildSetup,
216    ) -> Result<IncarnateOutcome, IncarnateError> {
217        if self.cfg.strict_caps {
218            let v = self.dry_run(card);
219            if !v.will_work {
220                // Mapeamos el primer blocking a IncarnateError tipado.
221                if let Some(first) = v.blocking.first() {
222                    if first.contains("unprivileged_userns_clone") {
223                        return Err(IncarnateError::UserNsDisabledBySysctl);
224                    }
225                    if first.contains("LSM") {
226                        return Err(IncarnateError::UserNsRestrictedByLsm);
227                    }
228                    if let Some(ns) = which_ns_blocking(first) {
229                        return Err(IncarnateError::NamespaceCapMissing { ns });
230                    }
231                    if first.contains("payload") {
232                        return Err(IncarnateError::NonExecutablePayload);
233                    }
234                }
235            }
236        }
237
238        let env_spec = EnvSpec {
239            bus_sock: self.cfg.bus_sock.clone(),
240            notify_socket: self.cfg.notify_socket.clone(),
241            extra: self.cfg.extra_env.clone(),
242        };
243
244        let mut degradations = Vec::new();
245        let pid = if namespaced::needs_namespacing(&card.soma.namespaces) {
246            namespaced::incarnate_namespaced(card, &env_spec, &stdio, &setup, &mut degradations)?
247        } else {
248            plain::incarnate_plain(card, &env_spec, &stdio, &setup)?
249        };
250        Ok(IncarnateOutcome { pid, degradations })
251    }
252}
253
254fn which_ns_blocking(msg: &str) -> Option<&'static str> {
255    for n in ["mount", "pid", "net", "uts", "ipc", "user", "cgroup"] {
256        if msg.starts_with(n) {
257            return Some(match n {
258                "mount" => "mount",
259                "pid" => "pid",
260                "net" => "net",
261                "uts" => "uts",
262                "ipc" => "ipc",
263                "user" => "user",
264                "cgroup" => "cgroup",
265                _ => unreachable!(),
266            });
267        }
268    }
269    None
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275    use brahman_card::{Card, NamespaceSet, Payload};
276
277    fn make_card(payload: Payload, ns: NamespaceSet) -> Card {
278        let mut c = Card::new("test");
279        c.payload = payload;
280        c.soma.namespaces = ns;
281        c
282    }
283
284    #[test]
285    fn dry_run_native_no_ns_works() {
286        let inc = Incarnator::new(IncarnatorConfig::default());
287        let card = make_card(
288            Payload::Native {
289                exec: "/bin/true".into(),
290                argv: vec![],
291                envp: vec![],
292            },
293            NamespaceSet::default(),
294        );
295        let r = inc.dry_run(&card);
296        assert!(r.will_work, "{:?}", r);
297    }
298
299    #[test]
300    fn dry_run_wasm_payload_blocks() {
301        let inc = Incarnator::new(IncarnatorConfig::default());
302        let card = make_card(
303            Payload::Wasm {
304                module_sha256: [0u8; 32],
305                entry: "main".into(),
306            },
307            NamespaceSet::default(),
308        );
309        let r = inc.dry_run(&card);
310        assert!(!r.will_work);
311        assert!(r.blocking.iter().any(|m| m.contains("payload")));
312    }
313
314    /// Smoke: redirección stdout via ChildStdio en path plain.
315    /// Lanza /bin/echo con stdout conectado a un pipe que leemos.
316    #[test]
317    fn incarnate_with_stdout_redirection_captures_output() {
318        use nix::fcntl::OFlag;
319        use nix::unistd::{pipe2, read};
320        use std::os::fd::{AsRawFd, IntoRawFd};
321
322        let inc = Incarnator::new(IncarnatorConfig::default());
323        let card = make_card(
324            Payload::Native {
325                exec: "/bin/echo".into(),
326                argv: vec!["shuma-stdio".into()],
327                envp: vec![],
328            },
329            NamespaceSet::default(),
330        );
331
332        let (r, w) = pipe2(OFlag::empty()).expect("pipe");
333        let w_raw = w.into_raw_fd();
334        let r_raw = r.as_raw_fd();
335
336        let stdio = ChildStdio {
337            stdin_fd: None,
338            stdout_fd: Some(w_raw),
339            stderr_fd: None,
340        };
341        let out = inc.incarnate_with(&card, stdio).expect("incarnate");
342
343        // Cerramos nuestro extremo de write (el hijo tiene su dup2).
344        // Plain path: Command toma ownership y cierra al spawn.
345        // Namespaced path: el padre todavía tiene una copia... pero en plain
346        // no aplica. Para este test usamos plain (NamespaceSet vacío).
347
348        // Cosechamos para no zombi.
349        let _ = nix::sys::wait::waitpid(out.pid, None);
350
351        // Leemos la salida.
352        let mut buf = [0u8; 64];
353        let n = read(r_raw, &mut buf).expect("read");
354        assert!(n > 0);
355        let s = std::str::from_utf8(&buf[..n]).unwrap();
356        assert!(s.contains("shuma-stdio"), "got: {s:?}");
357        // r se cierra al drop del OwnedFd.
358    }
359
360    /// child_pre_exec aplica chdir + NoNewPrivs en path plain.
361    #[test]
362    fn child_pre_exec_chdir_changes_pwd() {
363        use crate::{ChildPreExec, ChildSetup};
364        use nix::fcntl::OFlag;
365        use nix::unistd::{pipe2, read};
366        use std::ffi::CString;
367        use std::os::fd::{AsRawFd, IntoRawFd};
368
369        let inc = Incarnator::new(IncarnatorConfig::default());
370        // Comando: /bin/pwd. Si chdir funciona, output = /tmp.
371        let card = make_card(
372            Payload::Native {
373                exec: "/bin/pwd".into(),
374                argv: vec![],
375                envp: vec![],
376            },
377            NamespaceSet::default(),
378        );
379
380        let (r, w) = pipe2(OFlag::empty()).expect("pipe");
381        let w_raw = w.into_raw_fd();
382        let r_raw = r.as_raw_fd();
383
384        let stdio = ChildStdio {
385            stdin_fd: None,
386            stdout_fd: Some(w_raw),
387            stderr_fd: None,
388        };
389        let setup = ChildSetup::new()
390            .with(ChildPreExec::Chdir(CString::new("/tmp").unwrap()))
391            .with(ChildPreExec::NoNewPrivs);
392        let out = inc.incarnate_full(&card, stdio, setup).expect("incarnate");
393
394        let _ = nix::sys::wait::waitpid(out.pid, None);
395
396        let mut buf = [0u8; 64];
397        let n = read(r_raw, &mut buf).expect("read");
398        let s = std::str::from_utf8(&buf[..n]).unwrap();
399        assert!(s.starts_with("/tmp"), "pwd output was: {s:?}");
400    }
401
402    /// Smoke: encarnar /bin/true sin ns. No requiere root.
403    #[test]
404    fn incarnate_plain_true_succeeds() {
405        let inc = Incarnator::new(IncarnatorConfig::default());
406        let card = make_card(
407            Payload::Native {
408                exec: "/bin/true".into(),
409                argv: vec![],
410                envp: vec![],
411            },
412            NamespaceSet::default(),
413        );
414        let out = inc.incarnate(&card).expect("plain incarnation");
415        assert!(out.pid.as_raw() > 0);
416        // Cosechamos para no dejar zombi.
417        let _ = nix::sys::wait::waitpid(out.pid, None);
418    }
419}