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        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            // we can be certain here that map_binary will not be None,
435            // as in the lookup_map_binaries function, we return error
436            // if there are mappings.len() > 1 and binaries are not present
437            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}