tawasuyu
⎇ 227715a464b872a0ecd918a069c667e05c95ec27 crates/ente-soma/src/lib.rs Rust · 362 lines · 14.0 KB History Who touched what raw

compat-systemd1, NOTIFY_SOCKET, binfmt y timer 227715a46 · Sergio ·

1//! Encarnación del Soma: traducción de SomaSpec a syscalls.
2//!
3//! Esta capa es la única parte de PID 1 que toca syscalls de namespacing —
4//! todo lo demás opera sobre tipos de alto nivel. La complejidad vive aquí
5//! por diseño: encapsulada, auditable, y con un único punto de entrada.
6//!
7//! ## Protocolo padre↔hijo en el path namespaced
8//!
9//! ```text
10//!   parent               child
11//!     |                    |
12//!     |--- clone() ------->|   (child empieza dentro de los nuevos NS)
13//!     |                    |
14//!     |                    |---- read(sync_r, 1) ----  (bloquea)
15//!     |                    |
16//!     |  write uid_map    |
17//!     |  write gid_map    |
18//!     |  cgroup move       |
19//!     |  cpu affinity      |
20//!     |                    |
21//!     |--- write(sync_w) ->|
22//!     |                    |---- setrlimit
23//!     |                    |---- mount(/, MS_PRIVATE | MS_REC)
24//!     |                    |---- execve()
25//! ```
26
27use ente_card::{CgroupSpec, EntityCard, NamespaceSet, Payload, ResourceLimits};
28use nix::fcntl::OFlag;
29use nix::sched::CloneFlags;
30use nix::unistd::{pipe2, Pid};
31use std::ffi::CString;
32use std::os::fd::{AsRawFd, IntoRawFd, RawFd};
33use std::process::Command;
34use std::sync::OnceLock;
35use tracing::{info, warn};
36
37/// Path del socket del bus interno. Se establece una sola vez al arrancar
38/// PID 1 (después de que el listener bind exitoso). Cada hijo encarnado
39/// recibe este path en `ENTE_BUS_SOCK`.
40static BUS_SOCK_PATH: OnceLock<String> = OnceLock::new();
41
42pub fn set_bus_sock(path: String) {
43    let _ = BUS_SOCK_PATH.set(path);
44}
45
46fn build_env(card: &EntityCard, base_envp: &[(String, String)]) -> Vec<(String, String)> {
47    // Heredamos parent env, sobreescribimos con el envp explícito de la Card,
48    // y al final inyectamos las vars del fractal (no negociables).
49    let mut env: Vec<(String, String)> = std::env::vars().collect();
50    for (k, v) in base_envp {
51        env.retain(|(ek, _)| ek != k);
52        env.push((k.clone(), v.clone()));
53    }
54    if let Some(p) = BUS_SOCK_PATH.get() {
55        env.retain(|(k, _)| k != ente_bus::ENV_BUS_SOCK);
56        env.push((ente_bus::ENV_BUS_SOCK.into(), p.clone()));
57    }
58    env.retain(|(k, _)| k != ente_bus::ENV_ENTE_ID);
59    env.push((ente_bus::ENV_ENTE_ID.into(), card.id.to_string()));
60    // Apps `Type=notify` (sd_notify) leen NOTIFY_SOCKET. Apuntamos al path
61    // canónico de systemd; si ente-notify-compat no está corriendo, apps
62    // sólo verán que sd_notify falla y siguen sin "ready" signal — no es fatal.
63    env.retain(|(k, _)| k != "NOTIFY_SOCKET");
64    env.push(("NOTIFY_SOCKET".into(), "/run/systemd/notify".into()));
65    env
66}
67
68pub fn incarnate(card: &EntityCard) -> anyhow::Result<Pid> {
69    if needs_namespacing(&card.soma.namespaces) {
70        incarnate_namespaced(card)
71    } else {
72        incarnate_plain(card)
73    }
74}
75
76fn needs_namespacing(ns: &NamespaceSet) -> bool {
77    ns.mount || ns.pid || ns.net || ns.uts || ns.ipc || ns.user || ns.cgroup
78}
79
80/// Path simple: para Entes que no requieren aislamiento. Útil para Entes-shim
81/// que conviven con el host (e.g. compat-logind) y para dev mode.
82fn incarnate_plain(card: &EntityCard) -> anyhow::Result<Pid> {
83    let (exec, argv, base_envp) = match &card.payload {
84        Payload::Native { exec, argv, envp } => (exec.clone(), argv.clone(), envp.clone()),
85        Payload::Legacy { exec, argv, .. } => (exec.clone(), argv.clone(), Vec::new()),
86        _ => anyhow::bail!("incarnate_plain: payload no ejecutable"),
87    };
88    let env = build_env(card, &base_envp);
89    let mut cmd = Command::new(&exec);
90    cmd.args(&argv);
91    cmd.env_clear();
92    for (k, v) in &env {
93        cmd.env(k, v);
94    }
95    let child = cmd.spawn().map_err(|e| anyhow::anyhow!("spawn {exec}: {e}"))?;
96    Ok(Pid::from_raw(child.id() as i32))
97}
98
99/// Path namespaced: clone(2) + sync pipe + setup post-clone en padre + finalize en hijo.
100fn incarnate_namespaced(card: &EntityCard) -> anyhow::Result<Pid> {
101    let flags = build_clone_flags(&card.soma.namespaces);
102    info!(label = %card.label, ?flags, "namespaced incarnation");
103
104    let (exec, argv, base_envp) = match &card.payload {
105        Payload::Native { exec, argv, envp } => (exec.clone(), argv.clone(), envp.clone()),
106        Payload::Legacy { exec, argv, .. } => (exec.clone(), argv.clone(), Vec::new()),
107        _ => anyhow::bail!("incarnate_namespaced: payload no ejecutable"),
108    };
109
110    // Pipe O_CLOEXEC: el read del lado hijo es lo que hace race-free el setup.
111    // O_CLOEXEC garantiza que el fd se cierra automáticamente en execve, así
112    // no contamina el binario final.
113    let (sync_r, sync_w) = pipe2(OFlag::O_CLOEXEC)?;
114    let sync_r_raw: RawFd = sync_r.into_raw_fd();
115    let sync_w_raw: RawFd = sync_w.into_raw_fd();
116
117    let exec_c = CString::new(exec.clone())?;
118    let argv_c: Vec<CString> = std::iter::once(exec_c.clone())
119        .chain(argv.iter().filter_map(|s| CString::new(s.as_str()).ok()))
120        .collect();
121    let argv_ptrs: Vec<*const libc::c_char> = argv_c.iter()
122        .map(|c| c.as_ptr())
123        .chain(std::iter::once(std::ptr::null()))
124        .collect();
125
126    // envp construido pre-clone: padre y hijo comparten el COW. Tras execve
127    // el kernel reemplaza el address space, así que las CStrings sólo viven
128    // hasta el syscall.
129    let env_pairs = build_env(card, &base_envp);
130    let envp_c: Vec<CString> = env_pairs.iter()
131        .filter_map(|(k, v)| CString::new(format!("{k}={v}")).ok())
132        .collect();
133    let envp_ptrs: Vec<*const libc::c_char> = envp_c.iter()
134        .map(|c| c.as_ptr())
135        .chain(std::iter::once(std::ptr::null()))
136        .collect();
137
138    let rlimits = card.soma.rlimits.clone();
139    let mount_ns_enabled = card.soma.namespaces.mount;
140
141    // SAFETY: la clausura corre en stack nuevo dentro de un proceso recién
142    // clonado, COW del padre. Reglas inviolables:
143    //   - sólo syscalls async-signal-safe
144    //   - no `println!`/`tracing!`/cualquier I/O del runtime
145    //   - no allocator (vec/box/string)
146    //   - no Drop con efectos
147    //   - capturar sólo Copy o datos pre-construidos
148    let cb = Box::new(move || -> isize {
149        // 1) Cerrar el extremo de escritura: pertenece al padre.
150        unsafe { libc::close(sync_w_raw); }
151
152        // 2) Bloquear hasta que el padre termine el setup (uid_map, cgroup, etc).
153        let mut byte = [0u8; 1];
154        let n = unsafe {
155            libc::read(sync_r_raw, byte.as_mut_ptr() as *mut _, 1)
156        };
157        if n != 1 { unsafe { libc::_exit(101); } }
158        unsafe { libc::close(sync_r_raw); }
159
160        // 3) Aplicar rlimits dentro del nuevo namespace.
161        unsafe { apply_rlimits_unchecked(&rlimits); }
162
163        // 4) Si tenemos mount ns, marcar / como privado recursivamente para
164        //    que mounts del Ente no se filtren al host (es la trampa más
165        //    típica al delegar mount ns).
166        if mount_ns_enabled {
167            unsafe {
168                libc::mount(
169                    std::ptr::null(),
170                    b"/\0".as_ptr() as *const _,
171                    std::ptr::null(),
172                    libc::MS_PRIVATE | libc::MS_REC,
173                    std::ptr::null(),
174                );
175            }
176        }
177
178        // 5) execve. Si retorna, falló.
179        unsafe {
180            libc::execve(exec_c.as_ptr(), argv_ptrs.as_ptr(), envp_ptrs.as_ptr());
181            libc::_exit(102);
182        }
183    });
184
185    let mut stack = vec![0u8; 1024 * 1024];
186
187    #[allow(deprecated)]
188    let pid = unsafe {
189        nix::sched::clone(cb, &mut stack, flags, Some(libc::SIGCHLD))
190    }.map_err(|e| {
191        unsafe { libc::close(sync_r_raw); libc::close(sync_w_raw); }
192        anyhow::anyhow!("clone failed: {e}")
193    })?;
194
195    // Padre: cerrar el extremo de lectura.
196    unsafe { libc::close(sync_r_raw); }
197
198    // Setup post-clone en padre. Errores aquí no son fatales — registramos y
199    // continuamos. Si algo crítico falla, el hijo execve seguirá adelante con
200    // configuración degradada y el supervisor decidirá qué hacer.
201    if let Err(e) = configure_child(pid, card) {
202        warn!(?e, ?pid, "configure_child errores no-fatales");
203    }
204
205    // Despertar al hijo.
206    let signal_byte = [b'x'];
207    let written = unsafe {
208        libc::write(sync_w_raw, signal_byte.as_ptr() as *const _, 1)
209    };
210    unsafe { libc::close(sync_w_raw); }
211    if written != 1 {
212        warn!(?pid, "no se pudo señalizar al hijo (write devolvió {})", written);
213    }
214
215    if matches!(&card.payload, Payload::Legacy { fakes, .. } if !fakes.is_empty()) {
216        // TODO: facades viven en un Ente-shim aparte que se inyecta vía
217        // bind-mount sobre /run/systemd/notify, /run/dbus/system_bus_socket,
218        // etc. Cuando exista, registrarlas aquí.
219        warn!("legacy facades declaradas pero shim post-clone no implementado");
220    }
221
222    Ok(pid)
223}
224
225/// Setup que requiere capacidades del padre: uid_map, gid_map, cgroup move.
226/// Estos archivos en /proc/<pid>/* tienen reglas de propiedad que sólo el
227/// padre puede satisfacer mientras el hijo está suspendido en el sync pipe.
228fn configure_child(pid: Pid, card: &EntityCard) -> anyhow::Result<()> {
229    if card.soma.namespaces.user {
230        // Desde kernel 3.19 se debe escribir "deny" a setgroups antes de
231        // poder escribir gid_map sin CAP_SETGID. Ignorar errores: en kernels
232        // antiguos el archivo no existe y no es problema.
233        let _ = std::fs::write(format!("/proc/{}/setgroups", pid.as_raw()), "deny");
234
235        let uid = nix::unistd::getuid().as_raw();
236        let gid = nix::unistd::getgid().as_raw();
237        std::fs::write(
238            format!("/proc/{}/uid_map", pid.as_raw()),
239            format!("0 {uid} 1"),
240        ).map_err(|e| anyhow::anyhow!("write uid_map: {e}"))?;
241        std::fs::write(
242            format!("/proc/{}/gid_map", pid.as_raw()),
243            format!("0 {gid} 1"),
244        ).map_err(|e| anyhow::anyhow!("write gid_map: {e}"))?;
245    }
246
247    if !card.soma.cgroup.path.is_empty() {
248        match ensure_cgroup(&card.soma.cgroup) {
249            Ok(abs_path) => {
250                let procs = format!("{abs_path}/cgroup.procs");
251                if let Err(e) = std::fs::write(&procs, format!("{}\n", pid.as_raw())) {
252                    warn!(?e, path = %procs, "cgroup move falló");
253                }
254            }
255            Err(e) => warn!(?e, path = %card.soma.cgroup.path, "ensure_cgroup falló"),
256        }
257    }
258
259    if let Some(cpus) = &card.soma.cpu_affinity {
260        if let Err(e) = set_cpu_affinity(pid, cpus) {
261            warn!(?e, ?pid, "sched_setaffinity falló");
262        }
263    }
264
265    Ok(())
266}
267
268fn set_cpu_affinity(pid: Pid, cpus: &[u32]) -> anyhow::Result<()> {
269    let mut set: libc::cpu_set_t = unsafe { std::mem::zeroed() };
270    unsafe { libc::CPU_ZERO(&mut set); }
271    for &c in cpus {
272        unsafe { libc::CPU_SET(c as usize, &mut set); }
273    }
274    let r = unsafe {
275        libc::sched_setaffinity(pid.as_raw(), std::mem::size_of::<libc::cpu_set_t>(), &set)
276    };
277    if r != 0 {
278        anyhow::bail!("sched_setaffinity: {}", std::io::Error::last_os_error());
279    }
280    Ok(())
281}
282
283/// SAFETY: invocada en el hijo post-clone, sólo libc, no Rust I/O.
284unsafe fn apply_rlimits_unchecked(rl: &ResourceLimits) {
285    if let Some(mem) = rl.mem_bytes {
286        let lim = libc::rlimit { rlim_cur: mem, rlim_max: mem };
287        libc::setrlimit(libc::RLIMIT_AS, &lim);
288    }
289    if let Some(np) = rl.nproc {
290        let lim = libc::rlimit { rlim_cur: np as u64, rlim_max: np as u64 };
291        libc::setrlimit(libc::RLIMIT_NPROC, &lim);
292    }
293    if let Some(nf) = rl.nofile {
294        let lim = libc::rlimit { rlim_cur: nf as u64, rlim_max: nf as u64 };
295        libc::setrlimit(libc::RLIMIT_NOFILE, &lim);
296    }
297}
298
299/// Cgroup actual del proceso PID 1 (o ente-zero en dev). Lo usamos como
300/// prefijo para paths declarados relativos en CgroupSpec.path. En prod (PID 1
301/// como child del kernel) será `/`. En dev bajo systemd-user será algo como
302/// `/user.slice/user-1001.slice/user@1001.service/...`.
303fn current_cgroup() -> Option<String> {
304    let s = std::fs::read_to_string("/proc/self/cgroup").ok()?;
305    // Formato unified (cgroup v2): "0::/user.slice/..."
306    s.lines()
307        .find_map(|l| l.strip_prefix("0::"))
308        .map(|s| s.trim().to_string())
309}
310
311/// Resuelve un path declarado en CgroupSpec contra la jerarquía real.
312/// - path absoluto (empieza con `/`): respetar tal cual
313/// - path relativo: prefijar con cgroup actual de PID 1
314fn resolve_cgroup_path(spec_path: &str) -> String {
315    if spec_path.is_empty() { return String::new(); }
316    if spec_path.starts_with('/') {
317        return spec_path.to_string();
318    }
319    let trimmed = spec_path.trim_start_matches('/');
320    if let Some(cg) = current_cgroup() {
321        let base = if cg == "/" { String::new() } else { cg.trim_end_matches('/').to_string() };
322        format!("{base}/{trimmed}")
323    } else {
324        format!("/{trimmed}")
325    }
326}
327
328/// Crea el cgroup declarado, aplica weights. Devuelve el path absoluto
329/// resultante bajo /sys/fs/cgroup.
330fn ensure_cgroup(spec: &CgroupSpec) -> anyhow::Result<String> {
331    let rel = resolve_cgroup_path(&spec.path);
332    if rel.is_empty() {
333        anyhow::bail!("cgroup path vacío");
334    }
335    let abs = format!("/sys/fs/cgroup{}", rel);
336    std::fs::create_dir_all(&abs)
337        .map_err(|e| anyhow::anyhow!("mkdir {}: {e}", abs))?;
338    if let Some(w) = spec.cpu_weight {
339        let _ = std::fs::write(format!("{abs}/cpu.weight"), format!("{w}\n"));
340    }
341    if let Some(w) = spec.io_weight {
342        // io.weight requiere el formato "default <n>" en cgroup v2.
343        let _ = std::fs::write(format!("{abs}/io.weight"), format!("default {w}\n"));
344    }
345    Ok(abs)
346}
347
348fn build_clone_flags(ns: &NamespaceSet) -> CloneFlags {
349    let mut f = CloneFlags::empty();
350    if ns.mount  { f |= CloneFlags::CLONE_NEWNS; }
351    if ns.pid    { f |= CloneFlags::CLONE_NEWPID; }
352    if ns.net    { f |= CloneFlags::CLONE_NEWNET; }
353    if ns.uts    { f |= CloneFlags::CLONE_NEWUTS; }
354    if ns.ipc    { f |= CloneFlags::CLONE_NEWIPC; }
355    if ns.user   { f |= CloneFlags::CLONE_NEWUSER; }
356    if ns.cgroup { f |= CloneFlags::CLONE_NEWCGROUP; }
357    f
358}
359
360// AsRawFd unused but keep the import alive — soma may grow more fd handling.
361#[allow(dead_code)]
362fn _keep_imports(_: &dyn AsRawFd) {}