1use crate::error::{NucleusError, Result};
2use std::fs;
3use std::path::{Path, PathBuf};
4use tracing::{debug, info, warn};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct SubidRange {
12 pub start: u32,
13 pub count: u32,
14}
15
16impl SubidRange {
17 pub fn end(&self) -> u32 {
19 self.start.saturating_add(self.count)
20 }
21
22 pub fn contains_host(&self, host_id: u32) -> bool {
24 host_id >= self.start && host_id < self.end()
25 }
26}
27
28pub fn subid_range_for(path: &str, username: &str, uid: u32) -> Option<SubidRange> {
38 let contents = fs::read_to_string(path).ok()?;
39 let uid_str = uid.to_string();
40 for by in [username, uid_str.as_str()] {
42 for line in contents.lines() {
43 let mut it = line.split(':');
44 match (it.next(), it.next(), it.next()) {
45 (Some(name), Some(start), Some(count))
46 if name == by && !name.is_empty() =>
47 {
48 if let (Ok(start), Ok(count)) =
49 (start.parse::<u32>(), count.parse::<u32>())
50 {
51 if count > 0 {
52 return Some(SubidRange { start, count });
53 }
54 }
55 }
56 _ => {}
57 }
58 }
59 }
60 None
61}
62
63pub fn subuid_range_for_user(username: &str, uid: u32) -> Option<SubidRange> {
65 subid_range_for("/etc/subuid", username, uid)
66}
67
68pub fn subgid_range_for_user(username: &str, uid: u32) -> Option<SubidRange> {
70 subid_range_for("/etc/subgid", username, uid)
71}
72
73pub fn current_userns_container_ranges() -> Option<Vec<(u32, u32)>> {
85 let contents = fs::read_to_string("/proc/self/uid_map").ok()?;
86 let mut ranges = Vec::new();
87 let mut identity_root = false;
88 for line in contents.lines() {
89 let mut it = line.split_whitespace();
90 let (Some(c), Some(h), Some(n)) = (it.next(), it.next(), it.next()) else {
91 continue;
92 };
93 let (Ok(c), Ok(h), Ok(n)) = (c.parse::<u32>(), h.parse::<u32>(), n.parse::<u32>()) else {
94 continue;
95 };
96 if c == 0 && h == 0 {
98 identity_root = true;
99 }
100 ranges.push((c, n));
101 }
102 if identity_root && ranges.len() == 1 {
103 return None;
104 }
105 if ranges.is_empty() {
106 None
107 } else {
108 Some(ranges)
109 }
110}
111
112pub fn in_nontrivial_userns() -> bool {
116 current_userns_container_ranges().is_some()
117}
118
119pub fn uid_is_mapped_in_current_userns(uid: u32) -> bool {
123 current_userns_container_ranges()
124 .map(|ranges| {
125 ranges
126 .iter()
127 .any(|(start, count)| uid >= *start && uid < start.saturating_add(*count))
128 })
129 .unwrap_or(false)
130}
131
132pub fn current_username() -> Option<String> {
134 let uid = nix::unistd::getuid().as_raw();
136 if let Some(cname) = nix::unistd::User::from_uid(nix::unistd::Uid::from_raw(uid)).ok().flatten()
137 {
138 return Some(cname.name);
139 }
140 std::env::var("USER").or_else(|_| std::env::var("LOGNAME")).ok()
141}
142
143fn parse_map_triplet(s: &str) -> Option<IdMapping> {
146 let mut it = s.split(':');
147 let container_id = it.next()?.trim().parse::<u32>().ok()?;
148 let host_id = it.next()?.trim().parse::<u32>().ok()?;
149 let count = it.next()?.trim().parse::<u32>().ok()?;
150 if it.next().is_some() {
151 return None;
152 }
153 Some(IdMapping::new(container_id, host_id, count))
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
160pub struct IdMapping {
161 pub container_id: u32,
163 pub host_id: u32,
165 pub count: u32,
167}
168
169impl IdMapping {
170 pub fn new(container_id: u32, host_id: u32, count: u32) -> Self {
172 Self {
173 container_id,
174 host_id,
175 count,
176 }
177 }
178
179 pub fn validate(&self, allow_host_root: bool) -> crate::error::Result<()> {
184 if self.count == 0 {
185 return Err(NucleusError::ConfigError(
186 "ID mapping count must be non-zero".to_string(),
187 ));
188 }
189
190 if self.count > 65_536 {
192 return Err(NucleusError::ConfigError(format!(
193 "ID mapping count {} exceeds maximum 65536",
194 self.count
195 )));
196 }
197
198 if self.container_id.checked_add(self.count).is_none() {
200 return Err(NucleusError::ConfigError(format!(
201 "ID mapping overflow: container_id {} + count {} exceeds u32",
202 self.container_id, self.count
203 )));
204 }
205
206 if self.host_id.checked_add(self.count).is_none() {
208 return Err(NucleusError::ConfigError(format!(
209 "ID mapping overflow: host_id {} + count {} exceeds u32",
210 self.host_id, self.count
211 )));
212 }
213
214 if !allow_host_root && self.host_id == 0 && self.count > 0 {
216 return Err(NucleusError::ConfigError(
217 "ID mapping includes host UID/GID 0; use root-remapped mode if intentional"
218 .to_string(),
219 ));
220 }
221
222 Ok(())
223 }
224
225 pub fn rootless() -> Self {
227 let uid = nix::unistd::getuid().as_raw();
228 Self::new(0, uid, 1)
229 }
230
231 fn format(&self) -> String {
233 format!("{} {} {}\n", self.container_id, self.host_id, self.count)
234 }
235}
236
237#[derive(Debug, Clone)]
239pub struct UserNamespaceConfig {
240 pub uid_mappings: Vec<IdMapping>,
242 pub gid_mappings: Vec<IdMapping>,
244}
245
246impl UserNamespaceConfig {
247 pub fn rootless() -> Self {
255 let uid = nix::unistd::getuid().as_raw();
256 let gid = nix::unistd::getgid().as_raw();
257
258 Self {
259 uid_mappings: vec![IdMapping::new(0, uid, 1)],
260 gid_mappings: vec![IdMapping::new(0, gid, 1)],
261 }
262 }
263
264 pub fn keep_id() -> Result<Self> {
275 let uid = nix::unistd::getuid().as_raw();
276 let gid = nix::unistd::getgid().as_raw();
277 let (uname, urange, grange) = Self::current_subid_ranges()?;
278
279 let uid_mappings = vec![
280 IdMapping::new(0, urange.start, 1),
281 IdMapping::new(uid, uid, 1),
282 ];
283 let gid_mappings = vec![
284 IdMapping::new(0, grange.start, 1),
285 IdMapping::new(gid, gid, 1),
286 ];
287 info!(
288 "keep-id userns: container root -> host {} (subuid), container {} -> host {} (self)",
289 urange.start, uid, uid
290 );
291 let _ = uname; Ok(Self {
293 uid_mappings,
294 gid_mappings,
295 })
296 }
297
298 pub fn subuid_auto() -> Result<Self> {
307 let uid = nix::unistd::getuid().as_raw();
308 let gid = nix::unistd::getgid().as_raw();
309 let (_uname, urange, grange) = Self::current_subid_ranges()?;
310
311 let ucount = urange.count.min(65_536);
314 let gcount = grange.count.min(65_536);
315 let uid_mappings = vec![IdMapping::new(0, uid, 1), IdMapping::new(1, urange.start, ucount)];
316 let gid_mappings = vec![IdMapping::new(0, gid, 1), IdMapping::new(1, grange.start, gcount)];
317 info!(
318 "auto userns: container 0 -> host {}, container 1..{} -> host {}..{}",
319 uid,
320 ucount,
321 urange.start,
322 urange.start + ucount
323 );
324 Ok(Self {
325 uid_mappings,
326 gid_mappings,
327 })
328 }
329
330 pub fn from_map_specs(uid_specs: &[String], gid_specs: &[String]) -> Result<Self> {
333 let parse = |specs: &[String], label: &str| -> Result<Vec<IdMapping>> {
334 specs
335 .iter()
336 .map(|s| {
337 parse_map_triplet(s).ok_or_else(|| {
338 NucleusError::ConfigError(format!(
339 "Invalid {} mapping '{}': expected container:host:size",
340 label, s
341 ))
342 })
343 })
344 .collect()
345 };
346 let uid_mappings = parse(uid_specs, "--uidmap")?;
347 let gid_mappings = parse(gid_specs, "--gidmap")?;
348 if uid_mappings.is_empty() || gid_mappings.is_empty() {
349 return Err(NucleusError::ConfigError(
350 "--uidmap/--gidmap require at least one mapping each".to_string(),
351 ));
352 }
353 Self::custom(uid_mappings, gid_mappings)
354 }
355
356 pub fn for_unprivileged_rootless(workload_uid: Option<u32>) -> Self {
365 match workload_uid {
366 Some(uid) if uid != 0 => match Self::keep_id() {
367 Ok(c) => c,
368 Err(e) => {
369 warn!(
370 "Could not build keep-id mapping for requested workload uid {}: {}; \
371 falling back to nomap (container uid {} will be unmappable). Configure \
372 /etc/subuid + /etc/subgid to enable rootless non-root workloads.",
373 uid, e, uid
374 );
375 Self::rootless()
376 }
377 },
378 _ => Self::rootless(),
379 }
380 }
381
382 fn current_subid_ranges() -> Result<(String, SubidRange, SubidRange)> {
385 let uid = nix::unistd::getuid().as_raw();
386 let uname = current_username().unwrap_or_else(|| format!("uid:{}", uid));
387 let urange = subuid_range_for_user(&uname, uid).ok_or_else(|| {
388 NucleusError::ConfigError(format!(
389 "rootless subuid mode requires an entry for '{}' in /etc/subuid \
390 (configure with `useradd -v ...` or `podman system migrate`); \
391 falling back requires --userns=nomap",
392 uname
393 ))
394 })?;
395 let grange = subgid_range_for_user(&uname, uid).ok_or_else(|| {
396 NucleusError::ConfigError(format!(
397 "rootless subuid mode requires an entry for '{}' in /etc/subgid",
398 uname
399 ))
400 })?;
401 Ok((uname, urange, grange))
402 }
403
404 pub fn needs_setuid_helper(&self) -> bool {
409 let uid = nix::unistd::getuid().as_raw();
410 let gid = nix::unistd::getgid().as_raw();
411 let trivial_uid = self.uid_mappings.len() == 1
412 && self.uid_mappings[0] == IdMapping::new(0, uid, 1);
413 let trivial_gid = self.gid_mappings.len() == 1
414 && self.gid_mappings[0] == IdMapping::new(0, gid, 1);
415 !(trivial_uid && trivial_gid)
416 }
417
418 pub fn root_remapped() -> Self {
423 Self {
424 uid_mappings: vec![IdMapping::new(0, 100_000, 65_536)],
425 gid_mappings: vec![IdMapping::new(0, 100_000, 65_536)],
426 }
427 }
428
429 pub fn custom(
431 uid_mappings: Vec<IdMapping>,
432 gid_mappings: Vec<IdMapping>,
433 ) -> crate::error::Result<Self> {
434 let allow_host_root = nix::unistd::Uid::effective().is_root();
435 for mapping in &uid_mappings {
436 mapping.validate(allow_host_root)?;
437 }
438 for mapping in &gid_mappings {
439 mapping.validate(allow_host_root)?;
440 }
441 Ok(Self {
442 uid_mappings,
443 gid_mappings,
444 })
445 }
446}
447
448pub struct UserNamespaceMapper {
452 config: UserNamespaceConfig,
453}
454
455impl UserNamespaceMapper {
456 pub fn new(config: UserNamespaceConfig) -> Self {
457 Self { config }
458 }
459
460 pub fn setup_mappings(&self) -> Result<()> {
465 if !self.can_self_map_current_process() {
466 return Err(NucleusError::NamespaceError(
467 "This user namespace mapping must be written from a process outside the new \
468 user namespace; use write_mappings_for_pid() from the parent after fork"
469 .to_string(),
470 ));
471 }
472
473 self.write_mappings_for_pid(std::process::id())
474 }
475
476 pub fn write_mappings_for_pid(&self, pid: u32) -> Result<()> {
485 info!("Setting up user namespace mappings for pid {}", pid);
486
487 let is_root = nix::unistd::Uid::effective().is_root();
488 let use_helper = !is_root && self.config.needs_setuid_helper();
489
490 if use_helper {
491 self.write_mappings_via_setuid_helper(pid)?;
495 } else {
496 if self.should_deny_setgroups() {
497 self.write_setgroups_deny(pid)?;
498 }
499 self.write_uid_map(pid)?;
500 self.write_gid_map(pid)?;
501 }
502
503 info!(
504 "Successfully configured user namespace mappings for pid {}",
505 pid
506 );
507 Ok(())
508 }
509
510 fn write_mappings_via_setuid_helper(&self, pid: u32) -> Result<()> {
516 let pid_str = pid.to_string();
517
518 let mut uid_args: Vec<String> = Vec::new();
520 for m in &self.config.uid_mappings {
521 uid_args.push(m.container_id.to_string());
522 uid_args.push(m.host_id.to_string());
523 uid_args.push(m.count.to_string());
524 }
525 let mut gid_args: Vec<String> = Vec::new();
526 for m in &self.config.gid_mappings {
527 gid_args.push(m.container_id.to_string());
528 gid_args.push(m.host_id.to_string());
529 gid_args.push(m.count.to_string());
530 }
531
532 Self::run_idmap_helper("newgidmap", &pid_str, &gid_args)?;
533 Self::run_idmap_helper("newuidmap", &pid_str, &uid_args)?;
534 Ok(())
535 }
536
537 fn run_idmap_helper(prog: &str, pid_str: &str, map_args: &[String]) -> Result<()> {
538 let path = Self::find_setuid_helper(prog)?;
539 debug!(
540 "Invoking idmap helper {:?} for pid {} with args {:?}",
541 path, pid_str, map_args
542 );
543 let status = std::process::Command::new(&path)
544 .arg(pid_str)
545 .args(map_args)
546 .stdin(std::process::Stdio::null())
547 .stdout(std::process::Stdio::null())
548 .stderr(std::process::Stdio::piped())
549 .output()
550 .map_err(|e| {
551 NucleusError::NamespaceError(format!("Failed to execute {}: {}", path.display(), e))
552 })?;
553 if !status.status.success() {
554 let stderr = String::from_utf8_lossy(&status.stderr);
555 return Err(NucleusError::NamespaceError(format!(
556 "{} failed (exit {:?}): {}",
557 prog,
558 status.status.code(),
559 stderr.trim()
560 )));
561 }
562 Ok(())
563 }
564
565 fn find_setuid_helper(prog: &str) -> Result<PathBuf> {
567 const CANDIDATE_DIRS: &[&str] = &[
569 "/run/wrappers/bin",
570 "/usr/bin",
571 "/usr/sbin",
572 "/bin",
573 "/usr/local/bin",
574 ];
575 for dir in CANDIDATE_DIRS {
576 let p = Path::new(dir).join(prog);
577 if p.is_file() {
578 return Ok(p);
579 }
580 }
581 if let Ok(out) = std::process::Command::new("which").arg(prog).output() {
583 if out.status.success() {
584 let s = String::from_utf8_lossy(&out.stdout);
585 let trimmed = s.trim();
586 if !trimmed.is_empty() {
587 return Ok(PathBuf::from(trimmed));
588 }
589 }
590 }
591 warn!(
592 "Could not find {}; install shadow-utils (or its setuid wrappers) for rootless subuid mode",
593 prog
594 );
595 Err(NucleusError::NamespaceError(format!(
596 "Required setuid helper '{}' not found in PATH or standard wrapper directories",
597 prog
598 )))
599 }
600
601 fn can_self_map_current_process(&self) -> bool {
602 let uid = nix::unistd::getuid().as_raw();
603 let gid = nix::unistd::getgid().as_raw();
604
605 self.config.uid_mappings.len() == 1
606 && self.config.gid_mappings.len() == 1
607 && self.config.uid_mappings[0] == IdMapping::new(0, uid, 1)
608 && self.config.gid_mappings[0] == IdMapping::new(0, gid, 1)
609 }
610
611 fn should_deny_setgroups(&self) -> bool {
612 self.config.gid_mappings.len() == 1 && self.config.gid_mappings[0].count == 1
613 }
614
615 fn write_setgroups_deny(&self, pid: u32) -> Result<()> {
619 let path = format!("/proc/{}/setgroups", pid);
620 debug!("Writing 'deny' to {}", path);
621
622 fs::write(&path, "deny\n").map_err(|e| {
623 NucleusError::NamespaceError(format!("Failed to write to {}: {}", path, e))
624 })?;
625
626 Ok(())
627 }
628
629 fn write_uid_map(&self, pid: u32) -> Result<()> {
631 let path = format!("/proc/{}/uid_map", pid);
632 let mut content = String::new();
633
634 for mapping in &self.config.uid_mappings {
635 content.push_str(&mapping.format());
636 }
637
638 debug!("Writing UID mappings to {}: {}", path, content.trim());
639
640 fs::write(&path, &content).map_err(|e| {
641 NucleusError::NamespaceError(format!("Failed to write UID mappings: {}", e))
642 })?;
643
644 Ok(())
645 }
646
647 fn write_gid_map(&self, pid: u32) -> Result<()> {
649 let path = format!("/proc/{}/gid_map", pid);
650 let mut content = String::new();
651
652 for mapping in &self.config.gid_mappings {
653 content.push_str(&mapping.format());
654 }
655
656 debug!("Writing GID mappings to {}: {}", path, content.trim());
657
658 fs::write(&path, &content).map_err(|e| {
659 NucleusError::NamespaceError(format!("Failed to write GID mappings: {}", e))
660 })?;
661
662 Ok(())
663 }
664
665 pub fn config(&self) -> &UserNamespaceConfig {
667 &self.config
668 }
669}
670
671#[cfg(test)]
672mod tests {
673 use super::*;
674
675 #[test]
676 fn test_id_mapping_format() {
677 let mapping = IdMapping::new(0, 1000, 1);
678 assert_eq!(mapping.format(), "0 1000 1\n");
679
680 let mapping = IdMapping::new(1000, 2000, 100);
681 assert_eq!(mapping.format(), "1000 2000 100\n");
682 }
683
684 #[test]
685 fn test_id_mapping_rootless() {
686 let mapping = IdMapping::rootless();
687 assert_eq!(mapping.container_id, 0);
688 assert_eq!(mapping.count, 1);
689 }
691
692 #[test]
693 fn test_user_namespace_config_rootless() {
694 let config = UserNamespaceConfig::rootless();
695 assert_eq!(config.uid_mappings.len(), 1);
696 assert_eq!(config.gid_mappings.len(), 1);
697 assert_eq!(config.uid_mappings[0].container_id, 0);
698 assert_eq!(config.gid_mappings[0].container_id, 0);
699 }
700
701 #[test]
702 fn test_user_namespace_config_custom() {
703 let uid_mappings = vec![IdMapping::new(0, 1000, 1), IdMapping::new(1000, 2000, 100)];
704 let gid_mappings = vec![IdMapping::new(0, 1000, 1)];
705
706 let config =
707 UserNamespaceConfig::custom(uid_mappings.clone(), gid_mappings.clone()).unwrap();
708 assert_eq!(config.uid_mappings, uid_mappings);
709 assert_eq!(config.gid_mappings, gid_mappings);
710 }
711
712 #[test]
713 fn test_rootless_mapping_can_self_map_current_process() {
714 let mapper = UserNamespaceMapper::new(UserNamespaceConfig::rootless());
715 assert!(mapper.can_self_map_current_process());
716 assert!(mapper.should_deny_setgroups());
717 }
718
719 #[test]
720 fn test_root_remapped_requires_external_writer() {
721 let mapper = UserNamespaceMapper::new(UserNamespaceConfig::root_remapped());
722 assert!(!mapper.can_self_map_current_process());
723 assert!(!mapper.should_deny_setgroups());
724 assert!(mapper.setup_mappings().is_err());
725 }
726
727 #[test]
728 fn test_parse_map_triplet_podman_syntax() {
729 assert_eq!(
730 parse_map_triplet("0:1000:1"),
731 Some(IdMapping::new(0, 1000, 1))
732 );
733 assert_eq!(
734 parse_map_triplet(" 1 : 100000 : 65536 "),
735 Some(IdMapping::new(1, 100_000, 65_536))
736 );
737 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); }
742
743 #[test]
744 fn test_subid_range_for_parses_name_and_uid_entries() {
745 let dir = std::env::temp_dir();
746 let path = dir.join(format!("nucleus-subuid-test-{}", std::process::id()));
747 std::fs::write(
748 &path,
749 "root:100000:65536\nalice:200000:65536\n1000:300000:65536\n",
750 )
751 .unwrap();
752 let p = path.to_str().unwrap();
753 assert_eq!(
755 subid_range_for(p, "alice", 1000),
756 Some(SubidRange { start: 200_000, count: 65_536 })
757 );
758 assert_eq!(
760 subid_range_for(p, "nobody", 1000),
761 Some(SubidRange { start: 300_000, count: 65_536 })
762 );
763 assert_eq!(subid_range_for(p, "ghost", 4242), None);
765 let _ = std::fs::remove_file(&path);
766 }
767
768 #[test]
769 fn test_from_map_specs_validates() {
770 let cfg = UserNamespaceConfig::from_map_specs(
771 &["0:1000:1".to_string(), "1:100000:65536".to_string()],
772 &["0:100:1".to_string()],
773 )
774 .unwrap();
775 assert_eq!(cfg.uid_mappings.len(), 2);
776 assert_eq!(cfg.uid_mappings[1], IdMapping::new(1, 100_000, 65_536));
777 assert!(UserNamespaceConfig::from_map_specs(
779 &["0:1000:1".to_string()],
780 &[],
781 )
782 .is_err());
783 assert!(UserNamespaceConfig::from_map_specs(
785 &["bogus".to_string()],
786 &["0:100:1".to_string()],
787 )
788 .is_err());
789 }
790
791 #[test]
792 fn test_needs_setuid_helper_classification() {
793 assert!(!UserNamespaceConfig::rootless().needs_setuid_helper());
795 assert!(UserNamespaceConfig::root_remapped().needs_setuid_helper());
798 }
799
800 #[test]
801 fn test_keep_id_mapping_is_disjoint() {
802 let uid = nix::unistd::getuid().as_raw();
806 let gid = nix::unistd::getgid().as_raw();
807 let cfg = UserNamespaceConfig::custom(
808 vec![IdMapping::new(0, 100_000, 1), IdMapping::new(uid, uid, 1)],
809 vec![IdMapping::new(0, 100_000, 1), IdMapping::new(gid, gid, 1)],
810 )
811 .unwrap();
812 assert!(cfg.needs_setuid_helper());
813 assert_eq!(cfg.uid_mappings.len(), 2);
814 assert_eq!(cfg.uid_mappings[1].host_id, uid);
816 assert_eq!(cfg.uid_mappings[1].container_id, uid);
817 }
818
819 }