Skip to main content

libcontainer/
user_ns.rs

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// Wrap the uid/gid path function into a struct for dependency injection. This
13// allows us to mock the id mapping logic in unit tests by using a different
14// base path other than `/proc`.
15#[derive(Debug, Clone)]
16pub struct UserNamespaceIDMapper {
17    base_path: PathBuf,
18}
19
20impl Default for UserNamespaceIDMapper {
21    fn default() -> Self {
22        Self {
23            // By default, the `uid_map` and `gid_map` files are located in the
24            // `/proc` directory. In the production code, we can use the
25            // default.
26            base_path: PathBuf::from("/proc"),
27        }
28    }
29}
30
31impl UserNamespaceIDMapper {
32    // In production code, we can direct use the `new` function without the
33    // need to worry about the default.
34    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    // In test, we need to fake the base path to a temporary directory.
61    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    /// Location of the newuidmap binary
131    pub newuidmap: Option<PathBuf>,
132    /// Location of the newgidmap binary
133    pub newgidmap: Option<PathBuf>,
134    /// Mappings for user ids
135    pub(crate) uid_mappings: Option<Vec<LinuxIdMapping>>,
136    /// Mappings for group ids
137    pub(crate) gid_mappings: Option<Vec<LinuxIdMapping>>,
138    /// Info on the user namespaces
139    pub user_namespace: Option<LinuxNamespace>,
140    /// Is the container requested by a privileged user
141    pub privileged: bool,
142    /// Path to the id mappings
143    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
249/// Validates that the spec contains the required information for
250/// creating a new user namespace
251fn 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
371/// Looks up the location of the newuidmap and newgidmap binaries which
372/// are required to write multiple user/group mappings
373pub 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        if uid_mappings.len() == 1 && uid_mappings.len() == 1 {
378            return Ok(None);
379        }
380
381        let uidmap = lookup_map_binary("newuidmap")?;
382        let gidmap = lookup_map_binary("newgidmap")?;
383
384        match (uidmap, gidmap) {
385            (Some(newuidmap), Some(newgidmap)) => Ok(Some((newuidmap, newgidmap))),
386            _ => Err(MappingError::BinaryNotFound),
387        }
388    } else {
389        Ok(None)
390    }
391}
392
393fn lookup_map_binary(binary: &str) -> std::result::Result<Option<PathBuf>, MappingError> {
394    let paths = env::var("PATH").map_err(|_| MappingError::NoPathEnv)?;
395    Ok(paths
396        .split_terminator(':')
397        .map(|p| Path::new(p).join(binary))
398        .find(|p| p.exists()))
399}
400
401fn write_id_mapping(
402    pid: Pid,
403    map_file: &Path,
404    mappings: &[LinuxIdMapping],
405    map_binary: Option<&Path>,
406) -> std::result::Result<(), MappingError> {
407    tracing::debug!("Write ID mapping: {:?}", mappings);
408
409    match mappings.len() {
410        0 => return Err(MappingError::NoIDMapping),
411        1 => {
412            let mapping = mappings
413                .first()
414                .and_then(|m| format!("{} {} {}", m.container_id(), m.host_id(), m.size()).into())
415                .unwrap();
416            std::fs::write(map_file, &mapping).map_err(|err| {
417                tracing::error!(?err, ?map_file, ?mapping, "failed to write uid/gid mapping");
418                MappingError::WriteIDMapping(err)
419            })?;
420        }
421        _ => {
422            let args: Vec<String> = mappings
423                .iter()
424                .flat_map(|m| {
425                    [
426                        m.container_id().to_string(),
427                        m.host_id().to_string(),
428                        m.size().to_string(),
429                    ]
430                })
431                .collect();
432
433            // we can be certain here that map_binary will not be None,
434            // as in the lookup_map_binaries function, we return error
435            // if there are mappings.len() > 1 and binaries are not present
436            Command::new(map_binary.unwrap())
437                .arg(pid.to_string())
438                .args(args)
439                .output()
440                .map_err(|err| {
441                    tracing::error!(?err, ?map_binary, "failed to execute newuidmap/newgidmap");
442                    MappingError::Execute(err)
443                })?;
444        }
445    }
446
447    Ok(())
448}
449
450#[cfg(test)]
451mod tests {
452    use std::fs;
453
454    use anyhow::Result;
455    use nix::unistd::getpid;
456    use oci_spec::runtime::{
457        LinuxBuilder, LinuxIdMappingBuilder, LinuxNamespaceBuilder, SpecBuilder,
458    };
459    use rand::RngExt;
460    use serial_test::serial;
461
462    use super::*;
463
464    fn gen_u32() -> u32 {
465        rand::rng().random()
466    }
467
468    #[test]
469    fn test_validate_ok() -> Result<()> {
470        let syscall = create_syscall();
471        let userns = LinuxNamespaceBuilder::default()
472            .typ(LinuxNamespaceType::User)
473            .build()?;
474        let uid_mappings = vec![
475            LinuxIdMappingBuilder::default()
476                .host_id(gen_u32())
477                .container_id(0_u32)
478                .size(10_u32)
479                .build()?,
480        ];
481        let gid_mappings = vec![
482            LinuxIdMappingBuilder::default()
483                .host_id(gen_u32())
484                .container_id(0_u32)
485                .size(10_u32)
486                .build()?,
487        ];
488        let linux = LinuxBuilder::default()
489            .namespaces(vec![userns])
490            .uid_mappings(uid_mappings)
491            .gid_mappings(gid_mappings)
492            .build()?;
493        let spec = SpecBuilder::default().linux(linux).build()?;
494        assert!(validate_spec_for_new_user_ns(&spec, &*syscall).is_ok());
495        Ok(())
496    }
497
498    #[test]
499    fn test_validate_err() -> Result<()> {
500        let syscall = create_syscall();
501        let userns = LinuxNamespaceBuilder::default()
502            .typ(LinuxNamespaceType::User)
503            .build()?;
504        let uid_mappings = vec![
505            LinuxIdMappingBuilder::default()
506                .host_id(gen_u32())
507                .container_id(0_u32)
508                .size(10_u32)
509                .build()?,
510        ];
511        let gid_mappings = vec![
512            LinuxIdMappingBuilder::default()
513                .host_id(gen_u32())
514                .container_id(0_u32)
515                .size(10_u32)
516                .build()?,
517        ];
518
519        let linux_uid_empty = LinuxBuilder::default()
520            .namespaces(vec![userns.clone()])
521            .uid_mappings(vec![])
522            .gid_mappings(gid_mappings.clone())
523            .build()?;
524        assert!(
525            validate_spec_for_new_user_ns(
526                &SpecBuilder::default()
527                    .linux(linux_uid_empty)
528                    .build()
529                    .unwrap(),
530                &*syscall
531            )
532            .is_err()
533        );
534
535        let linux_gid_empty = LinuxBuilder::default()
536            .namespaces(vec![userns.clone()])
537            .uid_mappings(uid_mappings.clone())
538            .gid_mappings(vec![])
539            .build()?;
540        assert!(
541            validate_spec_for_new_user_ns(
542                &SpecBuilder::default()
543                    .linux(linux_gid_empty)
544                    .build()
545                    .unwrap(),
546                &*syscall
547            )
548            .is_err()
549        );
550
551        let linux_uid_none = LinuxBuilder::default()
552            .namespaces(vec![userns.clone()])
553            .gid_mappings(gid_mappings)
554            .build()?;
555        assert!(
556            validate_spec_for_new_user_ns(
557                &SpecBuilder::default()
558                    .linux(linux_uid_none)
559                    .build()
560                    .unwrap(),
561                &*syscall
562            )
563            .is_err()
564        );
565
566        let linux_gid_none = LinuxBuilder::default()
567            .namespaces(vec![userns])
568            .uid_mappings(uid_mappings)
569            .build()?;
570        assert!(
571            validate_spec_for_new_user_ns(
572                &SpecBuilder::default()
573                    .linux(linux_gid_none)
574                    .build()
575                    .unwrap(),
576                &*syscall
577            )
578            .is_err()
579        );
580
581        Ok(())
582    }
583
584    #[test]
585    #[serial]
586    fn test_write_uid_mapping() -> Result<()> {
587        let userns = LinuxNamespaceBuilder::default()
588            .typ(LinuxNamespaceType::User)
589            .build()?;
590        let host_uid = gen_u32();
591        let host_gid = gen_u32();
592        let container_id = 0_u32;
593        let size = 10_u32;
594        let uid_mappings = vec![
595            LinuxIdMappingBuilder::default()
596                .host_id(host_uid)
597                .container_id(container_id)
598                .size(size)
599                .build()?,
600        ];
601        let gid_mappings = vec![
602            LinuxIdMappingBuilder::default()
603                .host_id(host_gid)
604                .container_id(container_id)
605                .size(size)
606                .build()?,
607        ];
608        let linux = LinuxBuilder::default()
609            .namespaces(vec![userns])
610            .uid_mappings(uid_mappings)
611            .gid_mappings(gid_mappings)
612            .build()?;
613        let spec = SpecBuilder::default().linux(linux).build()?;
614
615        let pid = getpid();
616        let tmp = tempfile::tempdir()?;
617        let id_mapper = UserNamespaceIDMapper {
618            base_path: tmp.path().to_path_buf(),
619        };
620        id_mapper.ensure_uid_path(&pid)?;
621
622        let mut config = UserNamespaceConfig::new(&spec)?.unwrap();
623        config.with_id_mapper(id_mapper.clone());
624        config.write_uid_mapping(pid)?;
625        assert_eq!(
626            format!("{container_id} {host_uid} {size}"),
627            fs::read_to_string(id_mapper.get_uid_path(&pid))?
628        );
629        config.write_gid_mapping(pid)?;
630        Ok(())
631    }
632
633    #[test]
634    #[serial]
635    fn test_write_gid_mapping() -> Result<()> {
636        let userns = LinuxNamespaceBuilder::default()
637            .typ(LinuxNamespaceType::User)
638            .build()?;
639        let host_uid = gen_u32();
640        let host_gid = gen_u32();
641        let container_id = 0_u32;
642        let size = 10_u32;
643        let uid_mappings = vec![
644            LinuxIdMappingBuilder::default()
645                .host_id(host_uid)
646                .container_id(container_id)
647                .size(size)
648                .build()?,
649        ];
650        let gid_mappings = vec![
651            LinuxIdMappingBuilder::default()
652                .host_id(host_gid)
653                .container_id(container_id)
654                .size(size)
655                .build()?,
656        ];
657        let linux = LinuxBuilder::default()
658            .namespaces(vec![userns])
659            .uid_mappings(uid_mappings)
660            .gid_mappings(gid_mappings)
661            .build()?;
662        let spec = SpecBuilder::default().linux(linux).build()?;
663
664        let pid = getpid();
665        let tmp = tempfile::tempdir()?;
666        let id_mapper = UserNamespaceIDMapper {
667            base_path: tmp.path().to_path_buf(),
668        };
669        id_mapper.ensure_gid_path(&pid)?;
670
671        let mut config = UserNamespaceConfig::new(&spec)?.unwrap();
672        config.with_id_mapper(id_mapper.clone());
673        config.write_gid_mapping(pid)?;
674        assert_eq!(
675            format!("{container_id} {host_gid} {size}"),
676            fs::read_to_string(id_mapper.get_gid_path(&pid))?
677        );
678        Ok(())
679    }
680}