chore: monorepo inicial con arje + minga + yahweh absorbidos
Workspace en 4 ejes (core/modules/apps/shared):
- core/: 24 crates de arje (Init systemd-compatible: ente-card, ente-zero,
ente-kernel, ente-bus, ente-cas, ente-soma, ente-wasm, ente-snapshot,
ente-brain, ente-echo, ente-policy-provider, + 12 crates *-compat)
- modules/semantic_dht/: 5 crates de minga (minga-core con AST/CAS/MST,
minga-p2p con libp2p Kad, minga-store, minga-vfs, minga-cli)
- modules/ui_engine/: 11 crates de yahweh (libs/{core,theme,bus,providers},
widgets/{tree,splitter,tabs,tiled,container_core,text_input})
- apps/: 5 crates de yahweh (file_explorer, database_explorer, text_viewer,
image_viewer, yahweh-shell)
- shared_wit/protocol.wit: handshake/lifecycle inicial
Cargo.toml unificado: thiserror bumped a 2 (transparente para arje), tokio
"full", paths intra-workspace de yahweh redirigidos a su nueva ubicación.
cargo check --workspace: 0 errores, 17 warnings (dead code preexistente).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "ente-soma"
|
||||
version = "0.0.1"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ente-card = { path = "../ente-card" }
|
||||
ente-bus = { path = "../ente-bus" }
|
||||
nix = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
@@ -0,0 +1,362 @@
|
||||
//! Encarnación del Soma: traducción de SomaSpec a syscalls.
|
||||
//!
|
||||
//! Esta capa es la única parte de PID 1 que toca syscalls de namespacing —
|
||||
//! todo lo demás opera sobre tipos de alto nivel. La complejidad vive aquí
|
||||
//! por diseño: encapsulada, auditable, y con un único punto de entrada.
|
||||
//!
|
||||
//! ## Protocolo padre↔hijo en el path namespaced
|
||||
//!
|
||||
//! ```text
|
||||
//! parent child
|
||||
//! | |
|
||||
//! |--- clone() ------->| (child empieza dentro de los nuevos NS)
|
||||
//! | |
|
||||
//! | |---- read(sync_r, 1) ---- (bloquea)
|
||||
//! | |
|
||||
//! | write uid_map |
|
||||
//! | write gid_map |
|
||||
//! | cgroup move |
|
||||
//! | cpu affinity |
|
||||
//! | |
|
||||
//! |--- write(sync_w) ->|
|
||||
//! | |---- setrlimit
|
||||
//! | |---- mount(/, MS_PRIVATE | MS_REC)
|
||||
//! | |---- execve()
|
||||
//! ```
|
||||
|
||||
use ente_card::{CgroupSpec, EntityCard, NamespaceSet, Payload, ResourceLimits};
|
||||
use nix::fcntl::OFlag;
|
||||
use nix::sched::CloneFlags;
|
||||
use nix::unistd::{pipe2, Pid};
|
||||
use std::ffi::CString;
|
||||
use std::os::fd::{AsRawFd, IntoRawFd, RawFd};
|
||||
use std::process::Command;
|
||||
use std::sync::OnceLock;
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// Path del socket del bus interno. Se establece una sola vez al arrancar
|
||||
/// PID 1 (después de que el listener bind exitoso). Cada hijo encarnado
|
||||
/// recibe este path en `ENTE_BUS_SOCK`.
|
||||
static BUS_SOCK_PATH: OnceLock<String> = OnceLock::new();
|
||||
|
||||
pub fn set_bus_sock(path: String) {
|
||||
let _ = BUS_SOCK_PATH.set(path);
|
||||
}
|
||||
|
||||
fn build_env(card: &EntityCard, base_envp: &[(String, String)]) -> Vec<(String, String)> {
|
||||
// Heredamos parent env, sobreescribimos con el envp explícito de la Card,
|
||||
// y al final inyectamos las vars del fractal (no negociables).
|
||||
let mut env: Vec<(String, String)> = std::env::vars().collect();
|
||||
for (k, v) in base_envp {
|
||||
env.retain(|(ek, _)| ek != k);
|
||||
env.push((k.clone(), v.clone()));
|
||||
}
|
||||
if let Some(p) = BUS_SOCK_PATH.get() {
|
||||
env.retain(|(k, _)| k != ente_bus::ENV_BUS_SOCK);
|
||||
env.push((ente_bus::ENV_BUS_SOCK.into(), p.clone()));
|
||||
}
|
||||
env.retain(|(k, _)| k != ente_bus::ENV_ENTE_ID);
|
||||
env.push((ente_bus::ENV_ENTE_ID.into(), card.id.to_string()));
|
||||
// Apps `Type=notify` (sd_notify) leen NOTIFY_SOCKET. Apuntamos al path
|
||||
// canónico de systemd; si ente-notify-compat no está corriendo, apps
|
||||
// sólo verán que sd_notify falla y siguen sin "ready" signal — no es fatal.
|
||||
env.retain(|(k, _)| k != "NOTIFY_SOCKET");
|
||||
env.push(("NOTIFY_SOCKET".into(), "/run/systemd/notify".into()));
|
||||
env
|
||||
}
|
||||
|
||||
pub fn incarnate(card: &EntityCard) -> anyhow::Result<Pid> {
|
||||
if needs_namespacing(&card.soma.namespaces) {
|
||||
incarnate_namespaced(card)
|
||||
} else {
|
||||
incarnate_plain(card)
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_namespacing(ns: &NamespaceSet) -> bool {
|
||||
ns.mount || ns.pid || ns.net || ns.uts || ns.ipc || ns.user || ns.cgroup
|
||||
}
|
||||
|
||||
/// Path simple: para Entes que no requieren aislamiento. Útil para Entes-shim
|
||||
/// que conviven con el host (e.g. compat-logind) y para dev mode.
|
||||
fn incarnate_plain(card: &EntityCard) -> anyhow::Result<Pid> {
|
||||
let (exec, argv, base_envp) = match &card.payload {
|
||||
Payload::Native { exec, argv, envp } => (exec.clone(), argv.clone(), envp.clone()),
|
||||
Payload::Legacy { exec, argv, .. } => (exec.clone(), argv.clone(), Vec::new()),
|
||||
_ => anyhow::bail!("incarnate_plain: payload no ejecutable"),
|
||||
};
|
||||
let env = build_env(card, &base_envp);
|
||||
let mut cmd = Command::new(&exec);
|
||||
cmd.args(&argv);
|
||||
cmd.env_clear();
|
||||
for (k, v) in &env {
|
||||
cmd.env(k, v);
|
||||
}
|
||||
let child = cmd.spawn().map_err(|e| anyhow::anyhow!("spawn {exec}: {e}"))?;
|
||||
Ok(Pid::from_raw(child.id() as i32))
|
||||
}
|
||||
|
||||
/// Path namespaced: clone(2) + sync pipe + setup post-clone en padre + finalize en hijo.
|
||||
fn incarnate_namespaced(card: &EntityCard) -> anyhow::Result<Pid> {
|
||||
let flags = build_clone_flags(&card.soma.namespaces);
|
||||
info!(label = %card.label, ?flags, "namespaced incarnation");
|
||||
|
||||
let (exec, argv, base_envp) = match &card.payload {
|
||||
Payload::Native { exec, argv, envp } => (exec.clone(), argv.clone(), envp.clone()),
|
||||
Payload::Legacy { exec, argv, .. } => (exec.clone(), argv.clone(), Vec::new()),
|
||||
_ => anyhow::bail!("incarnate_namespaced: payload no ejecutable"),
|
||||
};
|
||||
|
||||
// Pipe O_CLOEXEC: el read del lado hijo es lo que hace race-free el setup.
|
||||
// O_CLOEXEC garantiza que el fd se cierra automáticamente en execve, así
|
||||
// no contamina el binario final.
|
||||
let (sync_r, sync_w) = pipe2(OFlag::O_CLOEXEC)?;
|
||||
let sync_r_raw: RawFd = sync_r.into_raw_fd();
|
||||
let sync_w_raw: RawFd = sync_w.into_raw_fd();
|
||||
|
||||
let exec_c = CString::new(exec.clone())?;
|
||||
let argv_c: Vec<CString> = std::iter::once(exec_c.clone())
|
||||
.chain(argv.iter().filter_map(|s| CString::new(s.as_str()).ok()))
|
||||
.collect();
|
||||
let argv_ptrs: Vec<*const libc::c_char> = argv_c.iter()
|
||||
.map(|c| c.as_ptr())
|
||||
.chain(std::iter::once(std::ptr::null()))
|
||||
.collect();
|
||||
|
||||
// envp construido pre-clone: padre y hijo comparten el COW. Tras execve
|
||||
// el kernel reemplaza el address space, así que las CStrings sólo viven
|
||||
// hasta el syscall.
|
||||
let env_pairs = build_env(card, &base_envp);
|
||||
let envp_c: Vec<CString> = env_pairs.iter()
|
||||
.filter_map(|(k, v)| CString::new(format!("{k}={v}")).ok())
|
||||
.collect();
|
||||
let envp_ptrs: Vec<*const libc::c_char> = envp_c.iter()
|
||||
.map(|c| c.as_ptr())
|
||||
.chain(std::iter::once(std::ptr::null()))
|
||||
.collect();
|
||||
|
||||
let rlimits = card.soma.rlimits.clone();
|
||||
let mount_ns_enabled = card.soma.namespaces.mount;
|
||||
|
||||
// SAFETY: la clausura corre en stack nuevo dentro de un proceso recién
|
||||
// clonado, COW del padre. Reglas inviolables:
|
||||
// - sólo syscalls async-signal-safe
|
||||
// - no `println!`/`tracing!`/cualquier I/O del runtime
|
||||
// - no allocator (vec/box/string)
|
||||
// - no Drop con efectos
|
||||
// - capturar sólo Copy o datos pre-construidos
|
||||
let cb = Box::new(move || -> isize {
|
||||
// 1) Cerrar el extremo de escritura: pertenece al padre.
|
||||
unsafe { libc::close(sync_w_raw); }
|
||||
|
||||
// 2) Bloquear hasta que el padre termine el setup (uid_map, cgroup, etc).
|
||||
let mut byte = [0u8; 1];
|
||||
let n = unsafe {
|
||||
libc::read(sync_r_raw, byte.as_mut_ptr() as *mut _, 1)
|
||||
};
|
||||
if n != 1 { unsafe { libc::_exit(101); } }
|
||||
unsafe { libc::close(sync_r_raw); }
|
||||
|
||||
// 3) Aplicar rlimits dentro del nuevo namespace.
|
||||
unsafe { apply_rlimits_unchecked(&rlimits); }
|
||||
|
||||
// 4) Si tenemos mount ns, marcar / como privado recursivamente para
|
||||
// que mounts del Ente no se filtren al host (es la trampa más
|
||||
// típica al delegar mount ns).
|
||||
if mount_ns_enabled {
|
||||
unsafe {
|
||||
libc::mount(
|
||||
std::ptr::null(),
|
||||
b"/\0".as_ptr() as *const _,
|
||||
std::ptr::null(),
|
||||
libc::MS_PRIVATE | libc::MS_REC,
|
||||
std::ptr::null(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 5) execve. Si retorna, falló.
|
||||
unsafe {
|
||||
libc::execve(exec_c.as_ptr(), argv_ptrs.as_ptr(), envp_ptrs.as_ptr());
|
||||
libc::_exit(102);
|
||||
}
|
||||
});
|
||||
|
||||
let mut stack = vec![0u8; 1024 * 1024];
|
||||
|
||||
#[allow(deprecated)]
|
||||
let pid = unsafe {
|
||||
nix::sched::clone(cb, &mut stack, flags, Some(libc::SIGCHLD))
|
||||
}.map_err(|e| {
|
||||
unsafe { libc::close(sync_r_raw); libc::close(sync_w_raw); }
|
||||
anyhow::anyhow!("clone failed: {e}")
|
||||
})?;
|
||||
|
||||
// Padre: cerrar el extremo de lectura.
|
||||
unsafe { libc::close(sync_r_raw); }
|
||||
|
||||
// Setup post-clone en padre. Errores aquí no son fatales — registramos y
|
||||
// continuamos. Si algo crítico falla, el hijo execve seguirá adelante con
|
||||
// configuración degradada y el supervisor decidirá qué hacer.
|
||||
if let Err(e) = configure_child(pid, card) {
|
||||
warn!(?e, ?pid, "configure_child errores no-fatales");
|
||||
}
|
||||
|
||||
// Despertar al hijo.
|
||||
let signal_byte = [b'x'];
|
||||
let written = unsafe {
|
||||
libc::write(sync_w_raw, signal_byte.as_ptr() as *const _, 1)
|
||||
};
|
||||
unsafe { libc::close(sync_w_raw); }
|
||||
if written != 1 {
|
||||
warn!(?pid, "no se pudo señalizar al hijo (write devolvió {})", written);
|
||||
}
|
||||
|
||||
if matches!(&card.payload, Payload::Legacy { fakes, .. } if !fakes.is_empty()) {
|
||||
// TODO: facades viven en un Ente-shim aparte que se inyecta vía
|
||||
// bind-mount sobre /run/systemd/notify, /run/dbus/system_bus_socket,
|
||||
// etc. Cuando exista, registrarlas aquí.
|
||||
warn!("legacy facades declaradas pero shim post-clone no implementado");
|
||||
}
|
||||
|
||||
Ok(pid)
|
||||
}
|
||||
|
||||
/// Setup que requiere capacidades del padre: uid_map, gid_map, cgroup move.
|
||||
/// Estos archivos en /proc/<pid>/* tienen reglas de propiedad que sólo el
|
||||
/// padre puede satisfacer mientras el hijo está suspendido en el sync pipe.
|
||||
fn configure_child(pid: Pid, card: &EntityCard) -> anyhow::Result<()> {
|
||||
if card.soma.namespaces.user {
|
||||
// Desde kernel 3.19 se debe escribir "deny" a setgroups antes de
|
||||
// poder escribir gid_map sin CAP_SETGID. Ignorar errores: en kernels
|
||||
// antiguos el archivo no existe y no es problema.
|
||||
let _ = std::fs::write(format!("/proc/{}/setgroups", pid.as_raw()), "deny");
|
||||
|
||||
let uid = nix::unistd::getuid().as_raw();
|
||||
let gid = nix::unistd::getgid().as_raw();
|
||||
std::fs::write(
|
||||
format!("/proc/{}/uid_map", pid.as_raw()),
|
||||
format!("0 {uid} 1"),
|
||||
).map_err(|e| anyhow::anyhow!("write uid_map: {e}"))?;
|
||||
std::fs::write(
|
||||
format!("/proc/{}/gid_map", pid.as_raw()),
|
||||
format!("0 {gid} 1"),
|
||||
).map_err(|e| anyhow::anyhow!("write gid_map: {e}"))?;
|
||||
}
|
||||
|
||||
if !card.soma.cgroup.path.is_empty() {
|
||||
match ensure_cgroup(&card.soma.cgroup) {
|
||||
Ok(abs_path) => {
|
||||
let procs = format!("{abs_path}/cgroup.procs");
|
||||
if let Err(e) = std::fs::write(&procs, format!("{}\n", pid.as_raw())) {
|
||||
warn!(?e, path = %procs, "cgroup move falló");
|
||||
}
|
||||
}
|
||||
Err(e) => warn!(?e, path = %card.soma.cgroup.path, "ensure_cgroup falló"),
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cpus) = &card.soma.cpu_affinity {
|
||||
if let Err(e) = set_cpu_affinity(pid, cpus) {
|
||||
warn!(?e, ?pid, "sched_setaffinity falló");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_cpu_affinity(pid: Pid, cpus: &[u32]) -> anyhow::Result<()> {
|
||||
let mut set: libc::cpu_set_t = unsafe { std::mem::zeroed() };
|
||||
unsafe { libc::CPU_ZERO(&mut set); }
|
||||
for &c in cpus {
|
||||
unsafe { libc::CPU_SET(c as usize, &mut set); }
|
||||
}
|
||||
let r = unsafe {
|
||||
libc::sched_setaffinity(pid.as_raw(), std::mem::size_of::<libc::cpu_set_t>(), &set)
|
||||
};
|
||||
if r != 0 {
|
||||
anyhow::bail!("sched_setaffinity: {}", std::io::Error::last_os_error());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// SAFETY: invocada en el hijo post-clone, sólo libc, no Rust I/O.
|
||||
unsafe fn apply_rlimits_unchecked(rl: &ResourceLimits) {
|
||||
if let Some(mem) = rl.mem_bytes {
|
||||
let lim = libc::rlimit { rlim_cur: mem, rlim_max: mem };
|
||||
libc::setrlimit(libc::RLIMIT_AS, &lim);
|
||||
}
|
||||
if let Some(np) = rl.nproc {
|
||||
let lim = libc::rlimit { rlim_cur: np as u64, rlim_max: np as u64 };
|
||||
libc::setrlimit(libc::RLIMIT_NPROC, &lim);
|
||||
}
|
||||
if let Some(nf) = rl.nofile {
|
||||
let lim = libc::rlimit { rlim_cur: nf as u64, rlim_max: nf as u64 };
|
||||
libc::setrlimit(libc::RLIMIT_NOFILE, &lim);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cgroup actual del proceso PID 1 (o ente-zero en dev). Lo usamos como
|
||||
/// prefijo para paths declarados relativos en CgroupSpec.path. En prod (PID 1
|
||||
/// como child del kernel) será `/`. En dev bajo systemd-user será algo como
|
||||
/// `/user.slice/user-1001.slice/user@1001.service/...`.
|
||||
fn current_cgroup() -> Option<String> {
|
||||
let s = std::fs::read_to_string("/proc/self/cgroup").ok()?;
|
||||
// Formato unified (cgroup v2): "0::/user.slice/..."
|
||||
s.lines()
|
||||
.find_map(|l| l.strip_prefix("0::"))
|
||||
.map(|s| s.trim().to_string())
|
||||
}
|
||||
|
||||
/// Resuelve un path declarado en CgroupSpec contra la jerarquía real.
|
||||
/// - path absoluto (empieza con `/`): respetar tal cual
|
||||
/// - path relativo: prefijar con cgroup actual de PID 1
|
||||
fn resolve_cgroup_path(spec_path: &str) -> String {
|
||||
if spec_path.is_empty() { return String::new(); }
|
||||
if spec_path.starts_with('/') {
|
||||
return spec_path.to_string();
|
||||
}
|
||||
let trimmed = spec_path.trim_start_matches('/');
|
||||
if let Some(cg) = current_cgroup() {
|
||||
let base = if cg == "/" { String::new() } else { cg.trim_end_matches('/').to_string() };
|
||||
format!("{base}/{trimmed}")
|
||||
} else {
|
||||
format!("/{trimmed}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Crea el cgroup declarado, aplica weights. Devuelve el path absoluto
|
||||
/// resultante bajo /sys/fs/cgroup.
|
||||
fn ensure_cgroup(spec: &CgroupSpec) -> anyhow::Result<String> {
|
||||
let rel = resolve_cgroup_path(&spec.path);
|
||||
if rel.is_empty() {
|
||||
anyhow::bail!("cgroup path vacío");
|
||||
}
|
||||
let abs = format!("/sys/fs/cgroup{}", rel);
|
||||
std::fs::create_dir_all(&abs)
|
||||
.map_err(|e| anyhow::anyhow!("mkdir {}: {e}", abs))?;
|
||||
if let Some(w) = spec.cpu_weight {
|
||||
let _ = std::fs::write(format!("{abs}/cpu.weight"), format!("{w}\n"));
|
||||
}
|
||||
if let Some(w) = spec.io_weight {
|
||||
// io.weight requiere el formato "default <n>" en cgroup v2.
|
||||
let _ = std::fs::write(format!("{abs}/io.weight"), format!("default {w}\n"));
|
||||
}
|
||||
Ok(abs)
|
||||
}
|
||||
|
||||
fn build_clone_flags(ns: &NamespaceSet) -> CloneFlags {
|
||||
let mut f = CloneFlags::empty();
|
||||
if ns.mount { f |= CloneFlags::CLONE_NEWNS; }
|
||||
if ns.pid { f |= CloneFlags::CLONE_NEWPID; }
|
||||
if ns.net { f |= CloneFlags::CLONE_NEWNET; }
|
||||
if ns.uts { f |= CloneFlags::CLONE_NEWUTS; }
|
||||
if ns.ipc { f |= CloneFlags::CLONE_NEWIPC; }
|
||||
if ns.user { f |= CloneFlags::CLONE_NEWUSER; }
|
||||
if ns.cgroup { f |= CloneFlags::CLONE_NEWCGROUP; }
|
||||
f
|
||||
}
|
||||
|
||||
// AsRawFd unused but keep the import alive — soma may grow more fd handling.
|
||||
#[allow(dead_code)]
|
||||
fn _keep_imports(_: &dyn AsRawFd) {}
|
||||
Reference in New Issue
Block a user