1use std::path::{Path, PathBuf};
2use std::process::Command;
3use std::{env, fs};
4
5use nix::unistd::Pid;
6use oci_spec::runtime::{Linux, LinuxIdMapping, LinuxNamespace, LinuxNamespaceType, Mount, Spec};
7
8use crate::error::MissingSpecError;
9use crate::namespaces::{NamespaceError, Namespaces};
10use crate::syscall::syscall::{Syscall, create_syscall};
11use crate::utils;
12#[derive(Debug, Clone)]
16pub struct UserNamespaceIDMapper {
17 base_path: PathBuf,
18}
19
20impl Default for UserNamespaceIDMapper {
21 fn default() -> Self {
22 Self {
23 base_path: PathBuf::from("/proc"),
27 }
28 }
29}
30
31impl UserNamespaceIDMapper {
32 pub fn new() -> Self {
35 Default::default()
36 }
37
38 pub fn get_uid_path(&self, pid: &Pid) -> PathBuf {
39 self.base_path.join(pid.to_string()).join("uid_map")
40 }
41 pub fn get_gid_path(&self, pid: &Pid) -> PathBuf {
42 self.base_path.join(pid.to_string()).join("gid_map")
43 }
44
45 #[cfg(test)]
46 pub fn ensure_uid_path(&self, pid: &Pid) -> std::result::Result<(), std::io::Error> {
47 std::fs::create_dir_all(self.get_uid_path(pid).parent().unwrap())?;
48
49 Ok(())
50 }
51
52 #[cfg(test)]
53 pub fn ensure_gid_path(&self, pid: &Pid) -> std::result::Result<(), std::io::Error> {
54 std::fs::create_dir_all(self.get_gid_path(pid).parent().unwrap())?;
55
56 Ok(())
57 }
58
59 #[cfg(test)]
60 pub fn new_test(path: PathBuf) -> Self {
62 Self { base_path: path }
63 }
64}
65
66#[derive(Debug, thiserror::Error)]
67pub enum UserNamespaceError {
68 #[error(transparent)]
69 MissingSpec(#[from] crate::error::MissingSpecError),
70 #[error("user namespace definition is invalid")]
71 NoUserNamespace,
72 #[error("invalid spec for new user namespace container")]
73 InvalidSpec(#[from] ValidateSpecError),
74 #[error("failed to read unprivileged userns clone")]
75 ReadUnprivilegedUsernsClone(#[source] std::io::Error),
76 #[error("failed to parse unprivileged userns clone")]
77 ParseUnprivilegedUsernsClone(#[source] std::num::ParseIntError),
78 #[error("unknown userns clone value")]
79 UnknownUnprivilegedUsernsClone(u8),
80 #[error(transparent)]
81 IDMapping(#[from] MappingError),
82 #[error(transparent)]
83 OtherIO(#[from] std::io::Error),
84}
85
86type Result<T> = std::result::Result<T, UserNamespaceError>;
87
88#[derive(Debug, thiserror::Error)]
89pub enum ValidateSpecError {
90 #[error(transparent)]
91 MissingSpec(#[from] crate::error::MissingSpecError),
92 #[error("new user namespace requires valid uid mappings")]
93 NoUIDMappings,
94 #[error("new user namespace requires valid gid mappings")]
95 NoGIDMapping,
96 #[error("no mount in spec")]
97 NoMountSpec,
98 #[error("unprivileged user can't set supplementary groups")]
99 UnprivilegedUser,
100 #[error("supplementary group needs to be mapped in the gid mappings")]
101 GidNotMapped(u32),
102 #[error("failed to parse ID")]
103 ParseID(#[source] std::num::ParseIntError),
104 #[error("mount options require mapping valid uid inside the container with new user namespace")]
105 MountGidMapping(u32),
106 #[error("mount options require mapping valid gid inside the container with new user namespace")]
107 MountUidMapping(u32),
108 #[error(transparent)]
109 Namespaces(#[from] NamespaceError),
110 #[error(transparent)]
111 OtherIO(#[from] std::io::Error),
112}
113
114#[derive(Debug, thiserror::Error)]
115pub enum MappingError {
116 #[error("newuidmap/newgidmap binaries could not be found in path")]
117 BinaryNotFound,
118 #[error("could not find PATH")]
119 NoPathEnv,
120 #[error("failed to execute newuidmap/newgidmap")]
121 Execute(#[source] std::io::Error),
122 #[error("at least one id mapping needs to be defined")]
123 NoIDMapping,
124 #[error("failed to write id mapping")]
125 WriteIDMapping(#[source] std::io::Error),
126}
127
128#[derive(Debug, Clone, Default)]
129pub struct UserNamespaceConfig {
130 pub newuidmap: Option<PathBuf>,
132 pub newgidmap: Option<PathBuf>,
134 pub(crate) uid_mappings: Option<Vec<LinuxIdMapping>>,
136 pub(crate) gid_mappings: Option<Vec<LinuxIdMapping>>,
138 pub user_namespace: Option<LinuxNamespace>,
140 pub privileged: bool,
142 pub id_mapper: UserNamespaceIDMapper,
144}
145
146impl UserNamespaceConfig {
147 pub fn new(spec: &Spec) -> Result<Option<Self>> {
148 let syscall = create_syscall();
149 let linux = spec.linux().as_ref().ok_or(MissingSpecError::Linux)?;
150 let namespaces = Namespaces::try_from(linux.namespaces().as_ref())
151 .map_err(ValidateSpecError::Namespaces)?;
152 let user_namespace = namespaces
153 .get(LinuxNamespaceType::User)
154 .map_err(ValidateSpecError::Namespaces)?;
155
156 if user_namespace.is_some() && user_namespace.unwrap().path().is_none() {
157 tracing::debug!("container with new user namespace should be created");
158
159 validate_spec_for_new_user_ns(spec, &*syscall).map_err(|err| {
160 tracing::error!("failed to validate spec for new user namespace: {}", err);
161 err
162 })?;
163 let mut user_ns_config = UserNamespaceConfig::try_from(linux)?;
164 if let Some((uid_binary, gid_binary)) = lookup_map_binaries(linux)? {
165 user_ns_config.newuidmap = Some(uid_binary);
166 user_ns_config.newgidmap = Some(gid_binary);
167 }
168
169 Ok(Some(user_ns_config))
170 } else {
171 tracing::debug!("this container does NOT create a new user namespace");
172 Ok(None)
173 }
174 }
175
176 pub fn write_uid_mapping(&self, target_pid: Pid) -> Result<()> {
177 tracing::debug!("write UID mapping for {:?}", target_pid);
178 if let Some(uid_mappings) = self.uid_mappings.as_ref() {
179 write_id_mapping(
180 target_pid,
181 self.id_mapper.get_uid_path(&target_pid).as_path(),
182 uid_mappings,
183 self.newuidmap.as_deref(),
184 )?;
185 }
186 Ok(())
187 }
188
189 pub fn write_gid_mapping(&self, target_pid: Pid) -> Result<()> {
190 tracing::debug!("write GID mapping for {:?}", target_pid);
191 if let Some(gid_mappings) = self.gid_mappings.as_ref() {
192 write_id_mapping(
193 target_pid,
194 self.id_mapper.get_gid_path(&target_pid).as_path(),
195 gid_mappings,
196 self.newgidmap.as_deref(),
197 )?;
198 }
199 Ok(())
200 }
201
202 pub fn with_id_mapper(&mut self, mapper: UserNamespaceIDMapper) {
203 self.id_mapper = mapper
204 }
205}
206
207impl TryFrom<&Linux> for UserNamespaceConfig {
208 type Error = UserNamespaceError;
209
210 fn try_from(linux: &Linux) -> Result<Self> {
211 let namespaces = Namespaces::try_from(linux.namespaces().as_ref())
212 .map_err(ValidateSpecError::Namespaces)?;
213 let user_namespace = namespaces
214 .get(LinuxNamespaceType::User)
215 .map_err(ValidateSpecError::Namespaces)?;
216 let syscall = create_syscall();
217 Ok(Self {
218 newuidmap: None,
219 newgidmap: None,
220 uid_mappings: linux.uid_mappings().to_owned(),
221 gid_mappings: linux.gid_mappings().to_owned(),
222 user_namespace: user_namespace.cloned(),
223 privileged: !utils::rootless_required(&*syscall)?,
224 id_mapper: UserNamespaceIDMapper::new(),
225 })
226 }
227}
228
229pub fn unprivileged_user_ns_enabled() -> Result<bool> {
230 let user_ns_sysctl = Path::new("/proc/sys/kernel/unprivileged_userns_clone");
231 if !user_ns_sysctl.exists() {
232 return Ok(true);
233 }
234
235 let content = fs::read_to_string(user_ns_sysctl)
236 .map_err(UserNamespaceError::ReadUnprivilegedUsernsClone)?;
237
238 match content
239 .trim()
240 .parse::<u8>()
241 .map_err(UserNamespaceError::ParseUnprivilegedUsernsClone)?
242 {
243 0 => Ok(false),
244 1 => Ok(true),
245 v => Err(UserNamespaceError::UnknownUnprivilegedUsernsClone(v)),
246 }
247}
248
249fn validate_spec_for_new_user_ns(
252 spec: &Spec,
253 syscall: &dyn Syscall,
254) -> std::result::Result<(), ValidateSpecError> {
255 tracing::debug!(
256 ?spec,
257 "validating spec for container with new user namespace"
258 );
259 let linux = spec.linux().as_ref().ok_or(MissingSpecError::Linux)?;
260
261 let gid_mappings = linux
262 .gid_mappings()
263 .as_ref()
264 .ok_or(ValidateSpecError::NoGIDMapping)?;
265 let uid_mappings = linux
266 .uid_mappings()
267 .as_ref()
268 .ok_or(ValidateSpecError::NoUIDMappings)?;
269
270 if uid_mappings.is_empty() {
271 return Err(ValidateSpecError::NoUIDMappings);
272 }
273 if gid_mappings.is_empty() {
274 return Err(ValidateSpecError::NoGIDMapping);
275 }
276
277 validate_mounts_for_new_user_ns(
278 spec.mounts()
279 .as_ref()
280 .ok_or(ValidateSpecError::NoMountSpec)?,
281 uid_mappings,
282 gid_mappings,
283 )?;
284
285 if let Some(additional_gids) = spec
286 .process()
287 .as_ref()
288 .and_then(|process| process.user().additional_gids().as_ref())
289 {
290 let privileged = !utils::rootless_required(syscall)?;
291
292 match (privileged, additional_gids.is_empty()) {
293 (true, false) => {
294 for gid in additional_gids {
295 if !is_id_mapped(*gid, gid_mappings) {
296 tracing::error!(
297 ?gid,
298 "gid is specified as supplementary group, but is not mapped in the user namespace"
299 );
300 return Err(ValidateSpecError::GidNotMapped(*gid));
301 }
302 }
303 }
304 (false, false) => {
305 tracing::error!(
306 user = ?syscall.get_euid(),
307 "user is unprivileged. Supplementary groups cannot be set in \
308 a rootless container for this user due to CVE-2014-8989",
309 );
310 return Err(ValidateSpecError::UnprivilegedUser);
311 }
312 _ => {}
313 }
314 }
315
316 Ok(())
317}
318
319fn validate_mounts_for_new_user_ns(
320 mounts: &[Mount],
321 uid_mappings: &[LinuxIdMapping],
322 gid_mappings: &[LinuxIdMapping],
323) -> std::result::Result<(), ValidateSpecError> {
324 for mount in mounts {
325 if let Some(options) = mount.options() {
326 for opt in options {
327 if opt.starts_with("uid=")
328 && !is_id_mapped(
329 opt[4..].parse().map_err(ValidateSpecError::ParseID)?,
330 uid_mappings,
331 )
332 {
333 tracing::error!(
334 ?mount,
335 ?opt,
336 "mount specifies option which is not mapped inside the container with new user namespace"
337 );
338 return Err(ValidateSpecError::MountUidMapping(
339 opt[4..].parse().map_err(ValidateSpecError::ParseID)?,
340 ));
341 }
342
343 if opt.starts_with("gid=")
344 && !is_id_mapped(
345 opt[4..].parse().map_err(ValidateSpecError::ParseID)?,
346 gid_mappings,
347 )
348 {
349 tracing::error!(
350 ?mount,
351 ?opt,
352 "mount specifies option which is not mapped inside the container with new user namespace"
353 );
354 return Err(ValidateSpecError::MountGidMapping(
355 opt[4..].parse().map_err(ValidateSpecError::ParseID)?,
356 ));
357 }
358 }
359 }
360 }
361
362 Ok(())
363}
364
365fn is_id_mapped(id: u32, mappings: &[LinuxIdMapping]) -> bool {
366 mappings
367 .iter()
368 .any(|m| id >= m.container_id() && id <= m.container_id() + m.size())
369}
370
371pub fn lookup_map_binaries(
374 spec: &Linux,
375) -> std::result::Result<Option<(PathBuf, PathBuf)>, MappingError> {
376 if let Some(uid_mappings) = spec.uid_mappings() {
377 let gid_len = spec.gid_mappings().as_ref().map(|g| g.len()).unwrap_or(0);
378 if uid_mappings.len() == 1 && gid_len == 1 {
379 return Ok(None);
380 }
381
382 let uidmap = lookup_map_binary("newuidmap")?;
383 let gidmap = lookup_map_binary("newgidmap")?;
384
385 match (uidmap, gidmap) {
386 (Some(newuidmap), Some(newgidmap)) => Ok(Some((newuidmap, newgidmap))),
387 _ => Err(MappingError::BinaryNotFound),
388 }
389 } else {
390 Ok(None)
391 }
392}
393
394fn lookup_map_binary(binary: &str) -> std::result::Result<Option<PathBuf>, MappingError> {
395 let paths = env::var("PATH").map_err(|_| MappingError::NoPathEnv)?;
396 Ok(paths
397 .split_terminator(':')
398 .map(|p| Path::new(p).join(binary))
399 .find(|p| p.exists()))
400}
401
402fn write_id_mapping(
403 pid: Pid,
404 map_file: &Path,
405 mappings: &[LinuxIdMapping],
406 map_binary: Option<&Path>,
407) -> std::result::Result<(), MappingError> {
408 tracing::debug!("Write ID mapping: {:?}", mappings);
409
410 match mappings.len() {
411 0 => return Err(MappingError::NoIDMapping),
412 1 => {
413 let mapping = mappings
414 .first()
415 .and_then(|m| format!("{} {} {}", m.container_id(), m.host_id(), m.size()).into())
416 .unwrap();
417 std::fs::write(map_file, &mapping).map_err(|err| {
418 tracing::error!(?err, ?map_file, ?mapping, "failed to write uid/gid mapping");
419 MappingError::WriteIDMapping(err)
420 })?;
421 }
422 _ => {
423 let args: Vec<String> = mappings
424 .iter()
425 .flat_map(|m| {
426 [
427 m.container_id().to_string(),
428 m.host_id().to_string(),
429 m.size().to_string(),
430 ]
431 })
432 .collect();
433
434 Command::new(map_binary.unwrap())
438 .arg(pid.to_string())
439 .args(args)
440 .output()
441 .map_err(|err| {
442 tracing::error!(?err, ?map_binary, "failed to execute newuidmap/newgidmap");
443 MappingError::Execute(err)
444 })?;
445 }
446 }
447
448 Ok(())
449}
450
451#[cfg(test)]
452mod tests {
453 use std::fs;
454
455 use anyhow::Result;
456 use nix::unistd::getpid;
457 use oci_spec::runtime::{
458 LinuxBuilder, LinuxIdMappingBuilder, LinuxNamespaceBuilder, SpecBuilder,
459 };
460 use rand::RngExt;
461 use serial_test::serial;
462
463 use super::*;
464
465 fn gen_u32() -> u32 {
466 rand::rng().random()
467 }
468
469 #[test]
470 fn test_validate_ok() -> Result<()> {
471 let syscall = create_syscall();
472 let userns = LinuxNamespaceBuilder::default()
473 .typ(LinuxNamespaceType::User)
474 .build()?;
475 let uid_mappings = vec![
476 LinuxIdMappingBuilder::default()
477 .host_id(gen_u32())
478 .container_id(0_u32)
479 .size(10_u32)
480 .build()?,
481 ];
482 let gid_mappings = vec![
483 LinuxIdMappingBuilder::default()
484 .host_id(gen_u32())
485 .container_id(0_u32)
486 .size(10_u32)
487 .build()?,
488 ];
489 let linux = LinuxBuilder::default()
490 .namespaces(vec![userns])
491 .uid_mappings(uid_mappings)
492 .gid_mappings(gid_mappings)
493 .build()?;
494 let spec = SpecBuilder::default().linux(linux).build()?;
495 assert!(validate_spec_for_new_user_ns(&spec, &*syscall).is_ok());
496 Ok(())
497 }
498
499 #[test]
500 fn test_validate_err() -> Result<()> {
501 let syscall = create_syscall();
502 let userns = LinuxNamespaceBuilder::default()
503 .typ(LinuxNamespaceType::User)
504 .build()?;
505 let uid_mappings = vec![
506 LinuxIdMappingBuilder::default()
507 .host_id(gen_u32())
508 .container_id(0_u32)
509 .size(10_u32)
510 .build()?,
511 ];
512 let gid_mappings = vec![
513 LinuxIdMappingBuilder::default()
514 .host_id(gen_u32())
515 .container_id(0_u32)
516 .size(10_u32)
517 .build()?,
518 ];
519
520 let linux_uid_empty = LinuxBuilder::default()
521 .namespaces(vec![userns.clone()])
522 .uid_mappings(vec![])
523 .gid_mappings(gid_mappings.clone())
524 .build()?;
525 assert!(
526 validate_spec_for_new_user_ns(
527 &SpecBuilder::default()
528 .linux(linux_uid_empty)
529 .build()
530 .unwrap(),
531 &*syscall
532 )
533 .is_err()
534 );
535
536 let linux_gid_empty = LinuxBuilder::default()
537 .namespaces(vec![userns.clone()])
538 .uid_mappings(uid_mappings.clone())
539 .gid_mappings(vec![])
540 .build()?;
541 assert!(
542 validate_spec_for_new_user_ns(
543 &SpecBuilder::default()
544 .linux(linux_gid_empty)
545 .build()
546 .unwrap(),
547 &*syscall
548 )
549 .is_err()
550 );
551
552 let linux_uid_none = LinuxBuilder::default()
553 .namespaces(vec![userns.clone()])
554 .gid_mappings(gid_mappings)
555 .build()?;
556 assert!(
557 validate_spec_for_new_user_ns(
558 &SpecBuilder::default()
559 .linux(linux_uid_none)
560 .build()
561 .unwrap(),
562 &*syscall
563 )
564 .is_err()
565 );
566
567 let linux_gid_none = LinuxBuilder::default()
568 .namespaces(vec![userns])
569 .uid_mappings(uid_mappings)
570 .build()?;
571 assert!(
572 validate_spec_for_new_user_ns(
573 &SpecBuilder::default()
574 .linux(linux_gid_none)
575 .build()
576 .unwrap(),
577 &*syscall
578 )
579 .is_err()
580 );
581
582 Ok(())
583 }
584
585 #[test]
586 #[serial]
587 fn test_write_uid_mapping() -> Result<()> {
588 let userns = LinuxNamespaceBuilder::default()
589 .typ(LinuxNamespaceType::User)
590 .build()?;
591 let host_uid = gen_u32();
592 let host_gid = gen_u32();
593 let container_id = 0_u32;
594 let size = 10_u32;
595 let uid_mappings = vec![
596 LinuxIdMappingBuilder::default()
597 .host_id(host_uid)
598 .container_id(container_id)
599 .size(size)
600 .build()?,
601 ];
602 let gid_mappings = vec![
603 LinuxIdMappingBuilder::default()
604 .host_id(host_gid)
605 .container_id(container_id)
606 .size(size)
607 .build()?,
608 ];
609 let linux = LinuxBuilder::default()
610 .namespaces(vec![userns])
611 .uid_mappings(uid_mappings)
612 .gid_mappings(gid_mappings)
613 .build()?;
614 let spec = SpecBuilder::default().linux(linux).build()?;
615
616 let pid = getpid();
617 let tmp = tempfile::tempdir()?;
618 let id_mapper = UserNamespaceIDMapper {
619 base_path: tmp.path().to_path_buf(),
620 };
621 id_mapper.ensure_uid_path(&pid)?;
622
623 let mut config = UserNamespaceConfig::new(&spec)?.unwrap();
624 config.with_id_mapper(id_mapper.clone());
625 config.write_uid_mapping(pid)?;
626 assert_eq!(
627 format!("{container_id} {host_uid} {size}"),
628 fs::read_to_string(id_mapper.get_uid_path(&pid))?
629 );
630 config.write_gid_mapping(pid)?;
631 Ok(())
632 }
633
634 #[test]
635 #[serial]
636 fn test_write_gid_mapping() -> Result<()> {
637 let userns = LinuxNamespaceBuilder::default()
638 .typ(LinuxNamespaceType::User)
639 .build()?;
640 let host_uid = gen_u32();
641 let host_gid = gen_u32();
642 let container_id = 0_u32;
643 let size = 10_u32;
644 let uid_mappings = vec![
645 LinuxIdMappingBuilder::default()
646 .host_id(host_uid)
647 .container_id(container_id)
648 .size(size)
649 .build()?,
650 ];
651 let gid_mappings = vec![
652 LinuxIdMappingBuilder::default()
653 .host_id(host_gid)
654 .container_id(container_id)
655 .size(size)
656 .build()?,
657 ];
658 let linux = LinuxBuilder::default()
659 .namespaces(vec![userns])
660 .uid_mappings(uid_mappings)
661 .gid_mappings(gid_mappings)
662 .build()?;
663 let spec = SpecBuilder::default().linux(linux).build()?;
664
665 let pid = getpid();
666 let tmp = tempfile::tempdir()?;
667 let id_mapper = UserNamespaceIDMapper {
668 base_path: tmp.path().to_path_buf(),
669 };
670 id_mapper.ensure_gid_path(&pid)?;
671
672 let mut config = UserNamespaceConfig::new(&spec)?.unwrap();
673 config.with_id_mapper(id_mapper.clone());
674 config.write_gid_mapping(pid)?;
675 assert_eq!(
676 format!("{container_id} {host_gid} {size}"),
677 fs::read_to_string(id_mapper.get_gid_path(&pid))?
678 );
679 Ok(())
680 }
681}