tawasuyu/arje
> Standalone repository, extracted from the tawasuyu monorepo at tag v0.3.3. The domain's code lives under 03ukupacha/arje/; the foundational crates it shares with the rest of the suite come in as git dependencies pinned to that tag. cargo…
⎇ d6b8f18b43c9bd9bf7268e48b61392a2d62bb229
crates/ente-soma/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
Pausa: 11 crates del fractal Ente #0 con cerebro completo
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 env
61}
62
63pub fn incarnate(card: &EntityCard) -> anyhow::Result<Pid> {
64 if needs_namespacing(&card.soma.namespaces) {
65 incarnate_namespaced(card)
66 } else {
67 incarnate_plain(card)
68 }
69}
70
71fn needs_namespacing(ns: &NamespaceSet) -> bool {
72 ns.mount || ns.pid || ns.net || ns.uts || ns.ipc || ns.user || ns.cgroup
73}
74
75/// Path simple: para Entes que no requieren aislamiento. Útil para Entes-shim
76/// que conviven con el host (e.g. compat-logind) y para dev mode.
77fn incarnate_plain(card: &EntityCard) -> anyhow::Result<Pid> {
78 let (exec, argv, base_envp) = match &card.payload {
79 Payload::Native { exec, argv, envp } => (exec.clone(), argv.clone(), envp.clone()),
80 Payload::Legacy { exec, argv, .. } => (exec.clone(), argv.clone(), Vec::new()),
81 _ => anyhow::bail!("incarnate_plain: payload no ejecutable"),
82 };
83 let env = build_env(card, &base_envp);
84 let mut cmd = Command::new(&exec);
85 cmd.args(&argv);
86 cmd.env_clear();
87 for (k, v) in &env {
88 cmd.env(k, v);
89 }
90 let child = cmd.spawn().map_err(|e| anyhow::anyhow!("spawn {exec}: {e}"))?;
91 Ok(Pid::from_raw(child.id() as i32))
92}
93
94/// Path namespaced: clone(2) + sync pipe + setup post-clone en padre + finalize en hijo.
95fn incarnate_namespaced(card: &EntityCard) -> anyhow::Result<Pid> {
96 let flags = build_clone_flags(&card.soma.namespaces);
97 info!(label = %card.label, ?flags, "namespaced incarnation");
98
99 let (exec, argv, base_envp) = match &card.payload {
100 Payload::Native { exec, argv, envp } => (exec.clone(), argv.clone(), envp.clone()),
101 Payload::Legacy { exec, argv, .. } => (exec.clone(), argv.clone(), Vec::new()),
102 _ => anyhow::bail!("incarnate_namespaced: payload no ejecutable"),
103 };
104
105 // Pipe O_CLOEXEC: el read del lado hijo es lo que hace race-free el setup.
106 // O_CLOEXEC garantiza que el fd se cierra automáticamente en execve, así
107 // no contamina el binario final.
108 let (sync_r, sync_w) = pipe2(OFlag::O_CLOEXEC)?;
109 let sync_r_raw: RawFd = sync_r.into_raw_fd();
110 let sync_w_raw: RawFd = sync_w.into_raw_fd();
111
112 let exec_c = CString::new(exec.clone())?;
113 let argv_c: Vec<CString> = std::iter::once(exec_c.clone())
114 .chain(argv.iter().filter_map(|s| CString::new(s.as_str()).ok()))
115 .collect();
116 let argv_ptrs: Vec<*const libc::c_char> = argv_c.iter()
117 .map(|c| c.as_ptr())
118 .chain(std::iter::once(std::ptr::null()))
119 .collect();
120
121 // envp construido pre-clone: padre y hijo comparten el COW. Tras execve
122 // el kernel reemplaza el address space, así que las CStrings sólo viven
123 // hasta el syscall.
124 let env_pairs = build_env(card, &base_envp);
125 let envp_c: Vec<CString> = env_pairs.iter()
126 .filter_map(|(k, v)| CString::new(format!("{k}={v}")).ok())
127 .collect();
128 let envp_ptrs: Vec<*const libc::c_char> = envp_c.iter()
129 .map(|c| c.as_ptr())
130 .chain(std::iter::once(std::ptr::null()))
131 .collect();
132
133 let rlimits = card.soma.rlimits.clone();
134 let mount_ns_enabled = card.soma.namespaces.mount;
135
136 // SAFETY: la clausura corre en stack nuevo dentro de un proceso recién
137 // clonado, COW del padre. Reglas inviolables:
138 // - sólo syscalls async-signal-safe
139 // - no `println!`/`tracing!`/cualquier I/O del runtime
140 // - no allocator (vec/box/string)
141 // - no Drop con efectos
142 // - capturar sólo Copy o datos pre-construidos
143 let cb = Box::new(move || -> isize {
144 // 1) Cerrar el extremo de escritura: pertenece al padre.
145 unsafe { libc::close(sync_w_raw); }
146
147 // 2) Bloquear hasta que el padre termine el setup (uid_map, cgroup, etc).
148 let mut byte = [0u8; 1];
149 let n = unsafe {
150 libc::read(sync_r_raw, byte.as_mut_ptr() as *mut _, 1)
151 };
152 if n != 1 { unsafe { libc::_exit(101); } }
153 unsafe { libc::close(sync_r_raw); }
154
155 // 3) Aplicar rlimits dentro del nuevo namespace.
156 unsafe { apply_rlimits_unchecked(&rlimits); }
157
158 // 4) Si tenemos mount ns, marcar / como privado recursivamente para
159 // que mounts del Ente no se filtren al host (es la trampa más
160 // típica al delegar mount ns).
161 if mount_ns_enabled {
162 unsafe {
163 libc::mount(
164 std::ptr::null(),
165 b"/\0".as_ptr() as *const _,
166 std::ptr::null(),
167 libc::MS_PRIVATE | libc::MS_REC,
168 std::ptr::null(),
169 );
170 }
171 }
172
173 // 5) execve. Si retorna, falló.
174 unsafe {
175 libc::execve(exec_c.as_ptr(), argv_ptrs.as_ptr(), envp_ptrs.as_ptr());
176 libc::_exit(102);
177 }
178 });
179
180 let mut stack = vec![0u8; 1024 * 1024];
181
182 #[allow(deprecated)]
183 let pid = unsafe {
184 nix::sched::clone(cb, &mut stack, flags, Some(libc::SIGCHLD))
185 }.map_err(|e| {
186 unsafe { libc::close(sync_r_raw); libc::close(sync_w_raw); }
187 anyhow::anyhow!("clone failed: {e}")
188 })?;
189
190 // Padre: cerrar el extremo de lectura.
191 unsafe { libc::close(sync_r_raw); }
192
193 // Setup post-clone en padre. Errores aquí no son fatales — registramos y
194 // continuamos. Si algo crítico falla, el hijo execve seguirá adelante con
195 // configuración degradada y el supervisor decidirá qué hacer.
196 if let Err(e) = configure_child(pid, card) {
197 warn!(?e, ?pid, "configure_child errores no-fatales");
198 }
199
200 // Despertar al hijo.
201 let signal_byte = [b'x'];
202 let written = unsafe {
203 libc::write(sync_w_raw, signal_byte.as_ptr() as *const _, 1)
204 };
205 unsafe { libc::close(sync_w_raw); }
206 if written != 1 {
207 warn!(?pid, "no se pudo señalizar al hijo (write devolvió {})", written);
208 }
209
210 if matches!(&card.payload, Payload::Legacy { fakes, .. } if !fakes.is_empty()) {
211 // TODO: facades viven en un Ente-shim aparte que se inyecta vía
212 // bind-mount sobre /run/systemd/notify, /run/dbus/system_bus_socket,
213 // etc. Cuando exista, registrarlas aquí.
214 warn!("legacy facades declaradas pero shim post-clone no implementado");
215 }
216
217 Ok(pid)
218}
219
220/// Setup que requiere capacidades del padre: uid_map, gid_map, cgroup move.
221/// Estos archivos en /proc/<pid>/* tienen reglas de propiedad que sólo el
222/// padre puede satisfacer mientras el hijo está suspendido en el sync pipe.
223fn configure_child(pid: Pid, card: &EntityCard) -> anyhow::Result<()> {
224 if card.soma.namespaces.user {
225 // Desde kernel 3.19 se debe escribir "deny" a setgroups antes de
226 // poder escribir gid_map sin CAP_SETGID. Ignorar errores: en kernels
227 // antiguos el archivo no existe y no es problema.
228 let _ = std::fs::write(format!("/proc/{}/setgroups", pid.as_raw()), "deny");
229
230 let uid = nix::unistd::getuid().as_raw();
231 let gid = nix::unistd::getgid().as_raw();
232 std::fs::write(
233 format!("/proc/{}/uid_map", pid.as_raw()),
234 format!("0 {uid} 1"),
235 ).map_err(|e| anyhow::anyhow!("write uid_map: {e}"))?;
236 std::fs::write(
237 format!("/proc/{}/gid_map", pid.as_raw()),
238 format!("0 {gid} 1"),
239 ).map_err(|e| anyhow::anyhow!("write gid_map: {e}"))?;
240 }
241
242 if !card.soma.cgroup.path.is_empty() {
243 match ensure_cgroup(&card.soma.cgroup) {
244 Ok(abs_path) => {
245 let procs = format!("{abs_path}/cgroup.procs");
246 if let Err(e) = std::fs::write(&procs, format!("{}\n", pid.as_raw())) {
247 warn!(?e, path = %procs, "cgroup move falló");
248 }
249 }
250 Err(e) => warn!(?e, path = %card.soma.cgroup.path, "ensure_cgroup falló"),
251 }
252 }
253
254 if let Some(cpus) = &card.soma.cpu_affinity {
255 if let Err(e) = set_cpu_affinity(pid, cpus) {
256 warn!(?e, ?pid, "sched_setaffinity falló");
257 }
258 }
259
260 Ok(())
261}
262
263fn set_cpu_affinity(pid: Pid, cpus: &[u32]) -> anyhow::Result<()> {
264 let mut set: libc::cpu_set_t = unsafe { std::mem::zeroed() };
265 unsafe { libc::CPU_ZERO(&mut set); }
266 for &c in cpus {
267 unsafe { libc::CPU_SET(c as usize, &mut set); }
268 }
269 let r = unsafe {
270 libc::sched_setaffinity(pid.as_raw(), std::mem::size_of::<libc::cpu_set_t>(), &set)
271 };
272 if r != 0 {
273 anyhow::bail!("sched_setaffinity: {}", std::io::Error::last_os_error());
274 }
275 Ok(())
276}
277
278/// SAFETY: invocada en el hijo post-clone, sólo libc, no Rust I/O.
279unsafe fn apply_rlimits_unchecked(rl: &ResourceLimits) {
280 if let Some(mem) = rl.mem_bytes {
281 let lim = libc::rlimit { rlim_cur: mem, rlim_max: mem };
282 libc::setrlimit(libc::RLIMIT_AS, &lim);
283 }
284 if let Some(np) = rl.nproc {
285 let lim = libc::rlimit { rlim_cur: np as u64, rlim_max: np as u64 };
286 libc::setrlimit(libc::RLIMIT_NPROC, &lim);
287 }
288 if let Some(nf) = rl.nofile {
289 let lim = libc::rlimit { rlim_cur: nf as u64, rlim_max: nf as u64 };
290 libc::setrlimit(libc::RLIMIT_NOFILE, &lim);
291 }
292}
293
294/// Cgroup actual del proceso PID 1 (o ente-zero en dev). Lo usamos como
295/// prefijo para paths declarados relativos en CgroupSpec.path. En prod (PID 1
296/// como child del kernel) será `/`. En dev bajo systemd-user será algo como
297/// `/user.slice/user-1001.slice/user@1001.service/...`.
298fn current_cgroup() -> Option<String> {
299 let s = std::fs::read_to_string("/proc/self/cgroup").ok()?;
300 // Formato unified (cgroup v2): "0::/user.slice/..."
301 s.lines()
302 .find_map(|l| l.strip_prefix("0::"))
303 .map(|s| s.trim().to_string())
304}
305
306/// Resuelve un path declarado en CgroupSpec contra la jerarquía real.
307/// - path absoluto (empieza con `/`): respetar tal cual
308/// - path relativo: prefijar con cgroup actual de PID 1
309fn resolve_cgroup_path(spec_path: &str) -> String {
310 if spec_path.is_empty() { return String::new(); }
311 if spec_path.starts_with('/') {
312 return spec_path.to_string();
313 }
314 let trimmed = spec_path.trim_start_matches('/');
315 if let Some(cg) = current_cgroup() {
316 let base = if cg == "/" { String::new() } else { cg.trim_end_matches('/').to_string() };
317 format!("{base}/{trimmed}")
318 } else {
319 format!("/{trimmed}")
320 }
321}
322
323/// Crea el cgroup declarado, aplica weights. Devuelve el path absoluto
324/// resultante bajo /sys/fs/cgroup.
325fn ensure_cgroup(spec: &CgroupSpec) -> anyhow::Result<String> {
326 let rel = resolve_cgroup_path(&spec.path);
327 if rel.is_empty() {
328 anyhow::bail!("cgroup path vacío");
329 }
330 let abs = format!("/sys/fs/cgroup{}", rel);
331 std::fs::create_dir_all(&abs)
332 .map_err(|e| anyhow::anyhow!("mkdir {}: {e}", abs))?;
333 if let Some(w) = spec.cpu_weight {
334 let _ = std::fs::write(format!("{abs}/cpu.weight"), format!("{w}\n"));
335 }
336 if let Some(w) = spec.io_weight {
337 // io.weight requiere el formato "default <n>" en cgroup v2.
338 let _ = std::fs::write(format!("{abs}/io.weight"), format!("default {w}\n"));
339 }
340 Ok(abs)
341}
342
343fn build_clone_flags(ns: &NamespaceSet) -> CloneFlags {
344 let mut f = CloneFlags::empty();
345 if ns.mount { f |= CloneFlags::CLONE_NEWNS; }
346 if ns.pid { f |= CloneFlags::CLONE_NEWPID; }
347 if ns.net { f |= CloneFlags::CLONE_NEWNET; }
348 if ns.uts { f |= CloneFlags::CLONE_NEWUTS; }
349 if ns.ipc { f |= CloneFlags::CLONE_NEWIPC; }
350 if ns.user { f |= CloneFlags::CLONE_NEWUSER; }
351 if ns.cgroup { f |= CloneFlags::CLONE_NEWCGROUP; }
352 f
353}
354
355// AsRawFd unused but keep the import alive — soma may grow more fd handling.
356#[allow(dead_code)]
357fn _keep_imports(_: &dyn AsRawFd) {}