use crate::error::{NucleusError, Result};
use std::fs;
use std::path::{Path, PathBuf};
use tracing::{debug, info, warn};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubidRange {
pub start: u32,
pub count: u32,
}
impl SubidRange {
pub fn end(&self) -> u32 {
self.start.saturating_add(self.count)
}
pub fn contains_host(&self, host_id: u32) -> bool {
host_id >= self.start && host_id < self.end()
}
}
pub fn subid_range_for(path: &str, username: &str, uid: u32) -> Option<SubidRange> {
let contents = fs::read_to_string(path).ok()?;
let uid_str = uid.to_string();
for by in [username, uid_str.as_str()] {
for line in contents.lines() {
let mut it = line.split(':');
match (it.next(), it.next(), it.next()) {
(Some(name), Some(start), Some(count))
if name == by && !name.is_empty() =>
{
if let (Ok(start), Ok(count)) =
(start.parse::<u32>(), count.parse::<u32>())
{
if count > 0 {
return Some(SubidRange { start, count });
}
}
}
_ => {}
}
}
}
None
}
pub fn subuid_range_for_user(username: &str, uid: u32) -> Option<SubidRange> {
subid_range_for("/etc/subuid", username, uid)
}
pub fn subgid_range_for_user(username: &str, uid: u32) -> Option<SubidRange> {
subid_range_for("/etc/subgid", username, uid)
}
pub fn current_userns_container_ranges() -> Option<Vec<(u32, u32)>> {
let contents = fs::read_to_string("/proc/self/uid_map").ok()?;
let mut ranges = Vec::new();
let mut identity_root = false;
for line in contents.lines() {
let mut it = line.split_whitespace();
let (Some(c), Some(h), Some(n)) = (it.next(), it.next(), it.next()) else {
continue;
};
let (Ok(c), Ok(h), Ok(n)) = (c.parse::<u32>(), h.parse::<u32>(), n.parse::<u32>()) else {
continue;
};
if c == 0 && h == 0 {
identity_root = true;
}
ranges.push((c, n));
}
if identity_root && ranges.len() == 1 {
return None;
}
if ranges.is_empty() {
None
} else {
Some(ranges)
}
}
pub fn in_nontrivial_userns() -> bool {
current_userns_container_ranges().is_some()
}
pub fn uid_is_mapped_in_current_userns(uid: u32) -> bool {
current_userns_container_ranges()
.map(|ranges| {
ranges
.iter()
.any(|(start, count)| uid >= *start && uid < start.saturating_add(*count))
})
.unwrap_or(false)
}
pub fn current_username() -> Option<String> {
let uid = nix::unistd::getuid().as_raw();
if let Some(cname) = nix::unistd::User::from_uid(nix::unistd::Uid::from_raw(uid)).ok().flatten()
{
return Some(cname.name);
}
std::env::var("USER").or_else(|_| std::env::var("LOGNAME")).ok()
}
fn parse_map_triplet(s: &str) -> Option<IdMapping> {
let mut it = s.split(':');
let container_id = it.next()?.trim().parse::<u32>().ok()?;
let host_id = it.next()?.trim().parse::<u32>().ok()?;
let count = it.next()?.trim().parse::<u32>().ok()?;
if it.next().is_some() {
return None;
}
Some(IdMapping::new(container_id, host_id, count))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IdMapping {
pub container_id: u32,
pub host_id: u32,
pub count: u32,
}
impl IdMapping {
pub fn new(container_id: u32, host_id: u32, count: u32) -> Self {
Self {
container_id,
host_id,
count,
}
}
pub fn validate(&self, allow_host_root: bool) -> crate::error::Result<()> {
if self.count == 0 {
return Err(NucleusError::ConfigError(
"ID mapping count must be non-zero".to_string(),
));
}
if self.count > 65_536 {
return Err(NucleusError::ConfigError(format!(
"ID mapping count {} exceeds maximum 65536",
self.count
)));
}
if self.container_id.checked_add(self.count).is_none() {
return Err(NucleusError::ConfigError(format!(
"ID mapping overflow: container_id {} + count {} exceeds u32",
self.container_id, self.count
)));
}
if self.host_id.checked_add(self.count).is_none() {
return Err(NucleusError::ConfigError(format!(
"ID mapping overflow: host_id {} + count {} exceeds u32",
self.host_id, self.count
)));
}
if !allow_host_root && self.host_id == 0 && self.count > 0 {
return Err(NucleusError::ConfigError(
"ID mapping includes host UID/GID 0; use root-remapped mode if intentional"
.to_string(),
));
}
Ok(())
}
pub fn rootless() -> Self {
let uid = nix::unistd::getuid().as_raw();
Self::new(0, uid, 1)
}
fn format(&self) -> String {
format!("{} {} {}\n", self.container_id, self.host_id, self.count)
}
}
#[derive(Debug, Clone)]
pub struct UserNamespaceConfig {
pub uid_mappings: Vec<IdMapping>,
pub gid_mappings: Vec<IdMapping>,
}
impl UserNamespaceConfig {
pub fn rootless() -> Self {
let uid = nix::unistd::getuid().as_raw();
let gid = nix::unistd::getgid().as_raw();
Self {
uid_mappings: vec![IdMapping::new(0, uid, 1)],
gid_mappings: vec![IdMapping::new(0, gid, 1)],
}
}
pub fn keep_id() -> Result<Self> {
let uid = nix::unistd::getuid().as_raw();
let gid = nix::unistd::getgid().as_raw();
let (uname, urange, grange) = Self::current_subid_ranges()?;
let uid_mappings = vec![
IdMapping::new(0, urange.start, 1),
IdMapping::new(uid, uid, 1),
];
let gid_mappings = vec![
IdMapping::new(0, grange.start, 1),
IdMapping::new(gid, gid, 1),
];
info!(
"keep-id userns: container root -> host {} (subuid), container {} -> host {} (self)",
urange.start, uid, uid
);
let _ = uname; Ok(Self {
uid_mappings,
gid_mappings,
})
}
pub fn subuid_auto() -> Result<Self> {
let uid = nix::unistd::getuid().as_raw();
let gid = nix::unistd::getgid().as_raw();
let (_uname, urange, grange) = Self::current_subid_ranges()?;
let ucount = urange.count.min(65_536);
let gcount = grange.count.min(65_536);
let uid_mappings = vec![IdMapping::new(0, uid, 1), IdMapping::new(1, urange.start, ucount)];
let gid_mappings = vec![IdMapping::new(0, gid, 1), IdMapping::new(1, grange.start, gcount)];
info!(
"auto userns: container 0 -> host {}, container 1..{} -> host {}..{}",
uid,
ucount,
urange.start,
urange.start + ucount
);
Ok(Self {
uid_mappings,
gid_mappings,
})
}
pub fn from_map_specs(uid_specs: &[String], gid_specs: &[String]) -> Result<Self> {
let parse = |specs: &[String], label: &str| -> Result<Vec<IdMapping>> {
specs
.iter()
.map(|s| {
parse_map_triplet(s).ok_or_else(|| {
NucleusError::ConfigError(format!(
"Invalid {} mapping '{}': expected container:host:size",
label, s
))
})
})
.collect()
};
let uid_mappings = parse(uid_specs, "--uidmap")?;
let gid_mappings = parse(gid_specs, "--gidmap")?;
if uid_mappings.is_empty() || gid_mappings.is_empty() {
return Err(NucleusError::ConfigError(
"--uidmap/--gidmap require at least one mapping each".to_string(),
));
}
Self::custom(uid_mappings, gid_mappings)
}
pub fn for_unprivileged_rootless(workload_uid: Option<u32>) -> Self {
match workload_uid {
Some(uid) if uid != 0 => match Self::keep_id() {
Ok(c) => c,
Err(e) => {
warn!(
"Could not build keep-id mapping for requested workload uid {}: {}; \
falling back to nomap (container uid {} will be unmappable). Configure \
/etc/subuid + /etc/subgid to enable rootless non-root workloads.",
uid, e, uid
);
Self::rootless()
}
},
_ => Self::rootless(),
}
}
fn current_subid_ranges() -> Result<(String, SubidRange, SubidRange)> {
let uid = nix::unistd::getuid().as_raw();
let uname = current_username().unwrap_or_else(|| format!("uid:{}", uid));
let urange = subuid_range_for_user(&uname, uid).ok_or_else(|| {
NucleusError::ConfigError(format!(
"rootless subuid mode requires an entry for '{}' in /etc/subuid \
(configure with `useradd -v ...` or `podman system migrate`); \
falling back requires --userns=nomap",
uname
))
})?;
let grange = subgid_range_for_user(&uname, uid).ok_or_else(|| {
NucleusError::ConfigError(format!(
"rootless subuid mode requires an entry for '{}' in /etc/subgid",
uname
))
})?;
Ok((uname, urange, grange))
}
pub fn needs_setuid_helper(&self) -> bool {
let uid = nix::unistd::getuid().as_raw();
let gid = nix::unistd::getgid().as_raw();
let trivial_uid = self.uid_mappings.len() == 1
&& self.uid_mappings[0] == IdMapping::new(0, uid, 1);
let trivial_gid = self.gid_mappings.len() == 1
&& self.gid_mappings[0] == IdMapping::new(0, gid, 1);
!(trivial_uid && trivial_gid)
}
pub fn root_remapped() -> Self {
Self {
uid_mappings: vec![IdMapping::new(0, 100_000, 65_536)],
gid_mappings: vec![IdMapping::new(0, 100_000, 65_536)],
}
}
pub fn custom(
uid_mappings: Vec<IdMapping>,
gid_mappings: Vec<IdMapping>,
) -> crate::error::Result<Self> {
let allow_host_root = nix::unistd::Uid::effective().is_root();
for mapping in &uid_mappings {
mapping.validate(allow_host_root)?;
}
for mapping in &gid_mappings {
mapping.validate(allow_host_root)?;
}
Ok(Self {
uid_mappings,
gid_mappings,
})
}
}
pub struct UserNamespaceMapper {
config: UserNamespaceConfig,
}
impl UserNamespaceMapper {
pub fn new(config: UserNamespaceConfig) -> Self {
Self { config }
}
pub fn setup_mappings(&self) -> Result<()> {
if !self.can_self_map_current_process() {
return Err(NucleusError::NamespaceError(
"This user namespace mapping must be written from a process outside the new \
user namespace; use write_mappings_for_pid() from the parent after fork"
.to_string(),
));
}
self.write_mappings_for_pid(std::process::id())
}
pub fn write_mappings_for_pid(&self, pid: u32) -> Result<()> {
info!("Setting up user namespace mappings for pid {}", pid);
let is_root = nix::unistd::Uid::effective().is_root();
let use_helper = !is_root && self.config.needs_setuid_helper();
if use_helper {
self.write_mappings_via_setuid_helper(pid)?;
} else {
if self.should_deny_setgroups() {
self.write_setgroups_deny(pid)?;
}
self.write_uid_map(pid)?;
self.write_gid_map(pid)?;
}
info!(
"Successfully configured user namespace mappings for pid {}",
pid
);
Ok(())
}
fn write_mappings_via_setuid_helper(&self, pid: u32) -> Result<()> {
let pid_str = pid.to_string();
let mut uid_args: Vec<String> = Vec::new();
for m in &self.config.uid_mappings {
uid_args.push(m.container_id.to_string());
uid_args.push(m.host_id.to_string());
uid_args.push(m.count.to_string());
}
let mut gid_args: Vec<String> = Vec::new();
for m in &self.config.gid_mappings {
gid_args.push(m.container_id.to_string());
gid_args.push(m.host_id.to_string());
gid_args.push(m.count.to_string());
}
Self::run_idmap_helper("newgidmap", &pid_str, &gid_args)?;
Self::run_idmap_helper("newuidmap", &pid_str, &uid_args)?;
Ok(())
}
fn run_idmap_helper(prog: &str, pid_str: &str, map_args: &[String]) -> Result<()> {
let path = Self::find_setuid_helper(prog)?;
debug!(
"Invoking idmap helper {:?} for pid {} with args {:?}",
path, pid_str, map_args
);
let status = std::process::Command::new(&path)
.arg(pid_str)
.args(map_args)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::piped())
.output()
.map_err(|e| {
NucleusError::NamespaceError(format!("Failed to execute {}: {}", path.display(), e))
})?;
if !status.status.success() {
let stderr = String::from_utf8_lossy(&status.stderr);
return Err(NucleusError::NamespaceError(format!(
"{} failed (exit {:?}): {}",
prog,
status.status.code(),
stderr.trim()
)));
}
Ok(())
}
fn find_setuid_helper(prog: &str) -> Result<PathBuf> {
const CANDIDATE_DIRS: &[&str] = &[
"/run/wrappers/bin",
"/usr/bin",
"/usr/sbin",
"/bin",
"/usr/local/bin",
];
for dir in CANDIDATE_DIRS {
let p = Path::new(dir).join(prog);
if p.is_file() {
return Ok(p);
}
}
if let Ok(out) = std::process::Command::new("which").arg(prog).output() {
if out.status.success() {
let s = String::from_utf8_lossy(&out.stdout);
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(PathBuf::from(trimmed));
}
}
}
warn!(
"Could not find {}; install shadow-utils (or its setuid wrappers) for rootless subuid mode",
prog
);
Err(NucleusError::NamespaceError(format!(
"Required setuid helper '{}' not found in PATH or standard wrapper directories",
prog
)))
}
fn can_self_map_current_process(&self) -> bool {
let uid = nix::unistd::getuid().as_raw();
let gid = nix::unistd::getgid().as_raw();
self.config.uid_mappings.len() == 1
&& self.config.gid_mappings.len() == 1
&& self.config.uid_mappings[0] == IdMapping::new(0, uid, 1)
&& self.config.gid_mappings[0] == IdMapping::new(0, gid, 1)
}
fn should_deny_setgroups(&self) -> bool {
self.config.gid_mappings.len() == 1 && self.config.gid_mappings[0].count == 1
}
fn write_setgroups_deny(&self, pid: u32) -> Result<()> {
let path = format!("/proc/{}/setgroups", pid);
debug!("Writing 'deny' to {}", path);
fs::write(&path, "deny\n").map_err(|e| {
NucleusError::NamespaceError(format!("Failed to write to {}: {}", path, e))
})?;
Ok(())
}
fn write_uid_map(&self, pid: u32) -> Result<()> {
let path = format!("/proc/{}/uid_map", pid);
let mut content = String::new();
for mapping in &self.config.uid_mappings {
content.push_str(&mapping.format());
}
debug!("Writing UID mappings to {}: {}", path, content.trim());
fs::write(&path, &content).map_err(|e| {
NucleusError::NamespaceError(format!("Failed to write UID mappings: {}", e))
})?;
Ok(())
}
fn write_gid_map(&self, pid: u32) -> Result<()> {
let path = format!("/proc/{}/gid_map", pid);
let mut content = String::new();
for mapping in &self.config.gid_mappings {
content.push_str(&mapping.format());
}
debug!("Writing GID mappings to {}: {}", path, content.trim());
fs::write(&path, &content).map_err(|e| {
NucleusError::NamespaceError(format!("Failed to write GID mappings: {}", e))
})?;
Ok(())
}
pub fn config(&self) -> &UserNamespaceConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_id_mapping_format() {
let mapping = IdMapping::new(0, 1000, 1);
assert_eq!(mapping.format(), "0 1000 1\n");
let mapping = IdMapping::new(1000, 2000, 100);
assert_eq!(mapping.format(), "1000 2000 100\n");
}
#[test]
fn test_id_mapping_rootless() {
let mapping = IdMapping::rootless();
assert_eq!(mapping.container_id, 0);
assert_eq!(mapping.count, 1);
}
#[test]
fn test_user_namespace_config_rootless() {
let config = UserNamespaceConfig::rootless();
assert_eq!(config.uid_mappings.len(), 1);
assert_eq!(config.gid_mappings.len(), 1);
assert_eq!(config.uid_mappings[0].container_id, 0);
assert_eq!(config.gid_mappings[0].container_id, 0);
}
#[test]
fn test_user_namespace_config_custom() {
let uid_mappings = vec![IdMapping::new(0, 1000, 1), IdMapping::new(1000, 2000, 100)];
let gid_mappings = vec![IdMapping::new(0, 1000, 1)];
let config =
UserNamespaceConfig::custom(uid_mappings.clone(), gid_mappings.clone()).unwrap();
assert_eq!(config.uid_mappings, uid_mappings);
assert_eq!(config.gid_mappings, gid_mappings);
}
#[test]
fn test_rootless_mapping_can_self_map_current_process() {
let mapper = UserNamespaceMapper::new(UserNamespaceConfig::rootless());
assert!(mapper.can_self_map_current_process());
assert!(mapper.should_deny_setgroups());
}
#[test]
fn test_root_remapped_requires_external_writer() {
let mapper = UserNamespaceMapper::new(UserNamespaceConfig::root_remapped());
assert!(!mapper.can_self_map_current_process());
assert!(!mapper.should_deny_setgroups());
assert!(mapper.setup_mappings().is_err());
}
#[test]
fn test_parse_map_triplet_podman_syntax() {
assert_eq!(
parse_map_triplet("0:1000:1"),
Some(IdMapping::new(0, 1000, 1))
);
assert_eq!(
parse_map_triplet(" 1 : 100000 : 65536 "),
Some(IdMapping::new(1, 100_000, 65_536))
);
assert_eq!(parse_map_triplet("0:1000"), None); assert_eq!(parse_map_triplet("0:1000:1:9"), None); assert_eq!(parse_map_triplet("x:1000:1"), None); }
#[test]
fn test_subid_range_for_parses_name_and_uid_entries() {
let dir = std::env::temp_dir();
let path = dir.join(format!("nucleus-subuid-test-{}", std::process::id()));
std::fs::write(
&path,
"root:100000:65536\nalice:200000:65536\n1000:300000:65536\n",
)
.unwrap();
let p = path.to_str().unwrap();
assert_eq!(
subid_range_for(p, "alice", 1000),
Some(SubidRange { start: 200_000, count: 65_536 })
);
assert_eq!(
subid_range_for(p, "nobody", 1000),
Some(SubidRange { start: 300_000, count: 65_536 })
);
assert_eq!(subid_range_for(p, "ghost", 4242), None);
let _ = std::fs::remove_file(&path);
}
#[test]
fn test_from_map_specs_validates() {
let cfg = UserNamespaceConfig::from_map_specs(
&["0:1000:1".to_string(), "1:100000:65536".to_string()],
&["0:100:1".to_string()],
)
.unwrap();
assert_eq!(cfg.uid_mappings.len(), 2);
assert_eq!(cfg.uid_mappings[1], IdMapping::new(1, 100_000, 65_536));
assert!(UserNamespaceConfig::from_map_specs(
&["0:1000:1".to_string()],
&[],
)
.is_err());
assert!(UserNamespaceConfig::from_map_specs(
&["bogus".to_string()],
&["0:100:1".to_string()],
)
.is_err());
}
#[test]
fn test_needs_setuid_helper_classification() {
assert!(!UserNamespaceConfig::rootless().needs_setuid_helper());
assert!(UserNamespaceConfig::root_remapped().needs_setuid_helper());
}
#[test]
fn test_keep_id_mapping_is_disjoint() {
let uid = nix::unistd::getuid().as_raw();
let gid = nix::unistd::getgid().as_raw();
let cfg = UserNamespaceConfig::custom(
vec![IdMapping::new(0, 100_000, 1), IdMapping::new(uid, uid, 1)],
vec![IdMapping::new(0, 100_000, 1), IdMapping::new(gid, gid, 1)],
)
.unwrap();
assert!(cfg.needs_setuid_helper());
assert_eq!(cfg.uid_mappings.len(), 2);
assert_eq!(cfg.uid_mappings[1].host_id, uid);
assert_eq!(cfg.uid_mappings[1].container_id, uid);
}
}