use std::collections::BTreeSet; use std::fmt::Write as _; use std::io; use std::path::Path; use rustix::process::{getgid, getuid}; use serde::{Deserialize, Serialize}; pub const REQUIRED_SUBORDINATE_IDS: u32 = 65_536; #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct UserNamespaceIdRange { pub container_id: u32, pub parent_id: u32, pub size: u32, } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct UserNamespaceMap { pub uid: Vec, pub gid: Vec, } impl UserNamespaceMap { /// Resolves the invoking identity and the lowest sufficient configured subordinate ranges. /// /// # Errors /// Refuses missing, malformed, overlapping, or insufficient `/etc/subuid` and `/etc/subgid` /// allocations. pub fn resolve_current() -> io::Result { let uid = getuid().as_raw(); let gid = getgid().as_raw(); let names = account_names(uid)?; let subordinate_user_start = select_subordinate_range(Path::new("/etc/subuid"), &names)?; let subordinate_group_start = select_subordinate_range(Path::new("/etc/subgid"), &names)?; let map = Self { uid: vec![ UserNamespaceIdRange { container_id: 0, parent_id: uid, size: 1, }, UserNamespaceIdRange { container_id: 1, parent_id: subordinate_user_start, size: REQUIRED_SUBORDINATE_IDS, }, ], gid: vec![ UserNamespaceIdRange { container_id: 0, parent_id: gid, size: 1, }, UserNamespaceIdRange { container_id: 1, parent_id: subordinate_group_start, size: REQUIRED_SUBORDINATE_IDS, }, ], }; map.validate()?; Ok(map) } /// # Errors /// Refuses empty, zero-sized, overflowing, overlapping, or rootless-incompatible maps. pub fn validate(&self) -> io::Result<()> { validate_ranges("UID", &self.uid, getuid().as_raw())?; validate_ranges("GID", &self.gid, getgid().as_raw()) } #[must_use] pub fn uid_helper_arguments(&self, pid: u32) -> Vec { helper_arguments(pid, &self.uid) } #[must_use] pub fn gid_helper_arguments(&self, pid: u32) -> Vec { helper_arguments(pid, &self.gid) } #[must_use] pub fn uid_proc_text(&self) -> String { proc_text(&self.uid) } #[must_use] pub fn gid_proc_text(&self) -> String { proc_text(&self.gid) } /// # Errors /// Refuses an unreadable process or a live UID/GID map that differs from this map. pub fn verify_process(&self, pid: u32) -> io::Result<()> { verify_proc_map(&format!("/proc/{pid}/uid_map"), &self.uid, "UID")?; verify_proc_map(&format!("/proc/{pid}/gid_map"), &self.gid, "GID") } } fn account_names(uid: u32) -> io::Result> { let mut names = BTreeSet::from([uid.to_string()]); for line in std::fs::read_to_string("/etc/passwd")?.lines() { let fields = line.split(':').collect::>(); if fields.len() >= 3 && fields[2].parse::().ok() == Some(uid) { names.insert(fields[0].to_owned()); } } Ok(names) } fn select_subordinate_range(path: &Path, names: &BTreeSet) -> io::Result { select_subordinate_range_contents( &path.display().to_string(), &std::fs::read_to_string(path)?, names, ) } fn select_subordinate_range_contents( label: &str, contents: &str, names: &BTreeSet, ) -> io::Result { let mut candidates = Vec::new(); for (line_number, line) in contents.lines().enumerate() { if line.trim().is_empty() { continue; } let fields = line.split(':').collect::>(); if fields.len() != 3 { return Err(io::Error::other(format!( "{} row {} is malformed", label, line_number + 1 ))); } if !names.contains(fields[0]) { continue; } let start = fields[1].parse::().map_err(|_| { io::Error::other(format!( "{} row {} has an invalid start", label, line_number + 1 )) })?; let count = fields[2].parse::().map_err(|_| { io::Error::other(format!( "{} row {} has an invalid count", label, line_number + 1 )) })?; if count >= REQUIRED_SUBORDINATE_IDS && start.checked_add(REQUIRED_SUBORDINATE_IDS - 1).is_some() { candidates.push(start); } } candidates.into_iter().min().ok_or_else(|| { io::Error::other(format!( "{label} has no contiguous {REQUIRED_SUBORDINATE_IDS}-ID allocation for the invoking user" )) }) } fn validate_ranges( name: &str, ranges: &[UserNamespaceIdRange], invoking_id: u32, ) -> io::Result<()> { if ranges.len() != 2 || ranges[0] != (UserNamespaceIdRange { container_id: 0, parent_id: invoking_id, size: 1, }) || ranges[1].container_id != 1 || ranges[1].size != REQUIRED_SUBORDINATE_IDS { return Err(io::Error::other(format!( "{name} map does not match the canonical rootless shape" ))); } for range in ranges { range .container_id .checked_add(range.size - 1) .and_then(|_| range.parent_id.checked_add(range.size - 1)) .ok_or_else(|| io::Error::other(format!("{name} map overflows")))?; } let subordinate = ranges[1]; if invoking_id >= subordinate.parent_id && invoking_id - subordinate.parent_id < subordinate.size { return Err(io::Error::other(format!( "{name} subordinate range contains the invoking identity" ))); } Ok(()) } fn helper_arguments(pid: u32, ranges: &[UserNamespaceIdRange]) -> Vec { std::iter::once(pid.to_string()) .chain(ranges.iter().flat_map(|range| { [ range.container_id.to_string(), range.parent_id.to_string(), range.size.to_string(), ] })) .collect() } fn proc_text(ranges: &[UserNamespaceIdRange]) -> String { ranges.iter().fold(String::new(), |mut text, range| { writeln!( text, "{} {} {}", range.container_id, range.parent_id, range.size ) .expect("writing to String cannot fail"); text }) } fn verify_proc_map(path: &str, expected: &[UserNamespaceIdRange], name: &str) -> io::Result<()> { let observed = std::fs::read_to_string(path)? .lines() .map(|line| { let fields = line.split_whitespace().collect::>(); if fields.len() != 3 { return Err(io::Error::other(format!("{name} map row is malformed"))); } Ok(UserNamespaceIdRange { container_id: fields[0] .parse() .map_err(|_| io::Error::other(format!("{name} container ID is invalid")))?, parent_id: fields[1] .parse() .map_err(|_| io::Error::other(format!("{name} parent ID is invalid")))?, size: fields[2] .parse() .map_err(|_| io::Error::other(format!("{name} range size is invalid")))?, }) }) .collect::>>()?; if observed != expected { return Err(io::Error::other(format!( "live {name} map differs from the canonical map" ))); } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn helper_arguments_preserve_canonical_range_order() { let map = UserNamespaceMap { uid: vec![ UserNamespaceIdRange { container_id: 0, parent_id: 1000, size: 1, }, UserNamespaceIdRange { container_id: 1, parent_id: 524_288, size: 65_536, }, ], gid: Vec::new(), }; assert_eq!( map.uid_helper_arguments(42), ["42", "0", "1000", "1", "1", "524288", "65536"] ); assert_eq!(map.uid_proc_text(), "0 1000 1\n1 524288 65536\n"); } #[test] fn subordinate_selection_uses_the_lowest_complete_allocation() { let names = BTreeSet::from(["1000".to_owned(), "developer".to_owned()]); assert_eq!( select_subordinate_range_contents( "/etc/subuid", "other:100000:65536\ndeveloper:700000:131072\n1000:524288:65536\n", &names, ) .unwrap(), 524_288 ); } #[test] fn subordinate_selection_refuses_partial_and_overflowing_allocations() { let names = BTreeSet::from(["developer".to_owned()]); let error = select_subordinate_range_contents( "/etc/subgid", "developer:524288:65535\ndeveloper:4294967295:65536\n", &names, ) .unwrap_err(); assert!( error .to_string() .contains("no contiguous 65536-ID allocation") ); } }