tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definiciones

⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc crates/shared/ente-incarnate/src/caps.rs Rust · 214 líneas · 6.2 KB Historia Quién tocó qué crudo

shell c22d2480b · sergio ·

1//! Detección runtime de capacidades del kernel/proceso para aislamiento.
2//!
3//! Esto NO se cachea entre instancias — sysctls pueden cambiar entre boot, y
4//! cgroup delegation depende del proceso concreto. Cada `Incarnator::new`
5//! hace su detección al construirse.
6
7use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone)]
10pub struct CapabilitySet {
11    pub kernel_version: (u32, u32, u32),
12    pub has_cap_sys_admin: bool,
13    pub user_ns: UserNsStatus,
14    pub cgroup_v2: CgroupStatus,
15    pub cgroup_delegated: bool,
16    pub max_user_namespaces: Option<u64>,
17    pub our_cgroup: Option<PathBuf>,
18}
19
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum UserNsStatus {
22    Allowed,
23    DisabledBySysctl,
24    RestrictedByLsm,
25    Unknown,
26}
27
28impl UserNsStatus {
29    pub fn is_allowed(&self) -> bool {
30        matches!(self, UserNsStatus::Allowed)
31    }
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum CgroupStatus {
36    Unified,
37    Hybrid,
38    Legacy,
39    NotMounted,
40}
41
42impl CapabilitySet {
43    pub fn detect() -> Self {
44        Self {
45            kernel_version: detect_kernel_version().unwrap_or((0, 0, 0)),
46            has_cap_sys_admin: detect_cap_sys_admin(),
47            user_ns: detect_user_ns(),
48            cgroup_v2: detect_cgroup_status(),
49            cgroup_delegated: detect_cgroup_delegated(),
50            max_user_namespaces: read_u64("/proc/sys/user/max_user_namespaces"),
51            our_cgroup: detect_our_cgroup(),
52        }
53    }
54
55    /// ¿Podemos crear el namespace `ns`?
56    /// Reglas:
57    /// - user → necesita user_ns Allowed (o ya tener CAP_SYS_ADMIN, en cuyo caso no se crea uno nuevo).
58    /// - resto → CAP_SYS_ADMIN, o crearlos junto con user ns nuevo.
59    pub fn can_create_ns(&self, kind: NsKind) -> bool {
60        match kind {
61            NsKind::User => self.user_ns.is_allowed() || self.has_cap_sys_admin,
62            _ => {
63                self.has_cap_sys_admin
64                    || (self.user_ns.is_allowed() && self.max_user_namespaces.unwrap_or(0) > 0)
65            }
66        }
67    }
68}
69
70#[derive(Debug, Clone, Copy)]
71pub enum NsKind {
72    Mount,
73    Pid,
74    Net,
75    Uts,
76    Ipc,
77    User,
78    Cgroup,
79}
80
81impl NsKind {
82    pub fn name(self) -> &'static str {
83        match self {
84            NsKind::Mount => "mount",
85            NsKind::Pid => "pid",
86            NsKind::Net => "net",
87            NsKind::Uts => "uts",
88            NsKind::Ipc => "ipc",
89            NsKind::User => "user",
90            NsKind::Cgroup => "cgroup",
91        }
92    }
93}
94
95fn detect_kernel_version() -> Option<(u32, u32, u32)> {
96    let s = std::fs::read_to_string("/proc/sys/kernel/osrelease").ok()?;
97    let head = s.split(|c: char| !c.is_ascii_digit() && c != '.').next()?;
98    let mut it = head.split('.');
99    let a = it.next()?.parse().ok()?;
100    let b = it.next()?.parse().ok()?;
101    let c = it.next().and_then(|x| x.parse().ok()).unwrap_or(0);
102    Some((a, b, c))
103}
104
105fn detect_cap_sys_admin() -> bool {
106    // euid 0 implica caps por default. Modo simple: si euid==0, asumimos CAP_SYS_ADMIN.
107    // Podríamos parsear /proc/self/status > CapEff, pero para nuestros usos el
108    // discriminador útil es root vs no-root.
109    nix::unistd::geteuid().is_root()
110}
111
112fn detect_user_ns() -> UserNsStatus {
113    // Sysctl tradicional Debian/Ubuntu pre-24.
114    if let Some(v) = read_u64("/proc/sys/kernel/unprivileged_userns_clone") {
115        if v == 0 {
116            return UserNsStatus::DisabledBySysctl;
117        }
118    }
119    // AppArmor restriction (Ubuntu 24+). 1 = restringido, 2 = restricción aplicada.
120    if let Some(v) = read_u64("/proc/sys/kernel/apparmor_restrict_unprivileged_userns") {
121        if v >= 1 {
122            return UserNsStatus::RestrictedByLsm;
123        }
124    }
125    if let Some(0) = read_u64("/proc/sys/user/max_user_namespaces") {
126        return UserNsStatus::DisabledBySysctl;
127    }
128    UserNsStatus::Allowed
129}
130
131fn detect_cgroup_status() -> CgroupStatus {
132    // /sys/fs/cgroup montado como cgroup2 → unified.
133    let mounts = match std::fs::read_to_string("/proc/self/mountinfo") {
134        Ok(s) => s,
135        Err(_) => return CgroupStatus::NotMounted,
136    };
137    let mut has_v2 = false;
138    let mut has_v1 = false;
139    for line in mounts.lines() {
140        // formato: ... - <fstype> <source> <opts>
141        let parts: Vec<&str> = line.split(" - ").collect();
142        if parts.len() < 2 {
143            continue;
144        }
145        let tail = parts[1];
146        let fields: Vec<&str> = tail.split_whitespace().collect();
147        if fields.is_empty() {
148            continue;
149        }
150        match fields[0] {
151            "cgroup2" => has_v2 = true,
152            "cgroup" => has_v1 = true,
153            _ => {}
154        }
155    }
156    match (has_v2, has_v1) {
157        (true, false) => CgroupStatus::Unified,
158        (true, true) => CgroupStatus::Hybrid,
159        (false, true) => CgroupStatus::Legacy,
160        (false, false) => CgroupStatus::NotMounted,
161    }
162}
163
164fn detect_our_cgroup() -> Option<PathBuf> {
165    let s = std::fs::read_to_string("/proc/self/cgroup").ok()?;
166    let rel = s.lines().find_map(|l| l.strip_prefix("0::"))?.trim();
167    let abs = if rel == "/" {
168        PathBuf::from("/sys/fs/cgroup")
169    } else {
170        PathBuf::from(format!("/sys/fs/cgroup{rel}"))
171    };
172    Some(abs)
173}
174
175fn detect_cgroup_delegated() -> bool {
176    // Heurística: ¿podemos escribir cgroup.subtree_control en nuestro cgroup
177    // o crear subdirectorios? En cgroup v2 con Delegate=yes, el dueño es el uid
178    // del usuario y `access(W_OK)` sobre el directorio devuelve OK.
179    let Some(p) = detect_our_cgroup() else { return false };
180    use nix::unistd::{access, AccessFlags};
181    access(&p, AccessFlags::W_OK).is_ok()
182}
183
184fn read_u64(path: &str) -> Option<u64> {
185    let s = std::fs::read_to_string(Path::new(path)).ok()?;
186    s.trim().parse().ok()
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn detect_does_not_panic() {
195        let _ = CapabilitySet::detect();
196    }
197
198    #[test]
199    fn ns_kind_names_unique() {
200        let names = [
201            NsKind::Mount.name(),
202            NsKind::Pid.name(),
203            NsKind::Net.name(),
204            NsKind::Uts.name(),
205            NsKind::Ipc.name(),
206            NsKind::User.name(),
207            NsKind::Cgroup.name(),
208        ];
209        let mut sorted = names.to_vec();
210        sorted.sort();
211        sorted.dedup();
212        assert_eq!(sorted.len(), names.len());
213    }
214}