Skip to main content

ferroday_cage/
idmap.rs

1//! Identity maps: how the sandbox's user namespace presents uids and gids,
2//! and the delegate seam through which range maps are established.
3//!
4//! The default map is [`IdentityMap::Single`]: root inside the sandbox is the
5//! calling user outside, and no other id exists. The kernel permits an
6//! unprivileged process to write exactly that map and nothing more — one
7//! extent, of one id, naming the writer's own effective id — so any richer
8//! map must be written from outside the new namespace by a party that holds
9//! `CAP_SETUID`/`CAP_SETGID` over its parent. [`IdMapper`] is that party's
10//! seam: [`CageBuilder::id_mapper`] accepts any implementation, and the
11//! library bundles two.
12//!
13//! [`DirectMapper`] writes the map itself, in process. It succeeds when the
14//! caller already holds the needed capabilities over the parent user
15//! namespace — running as root, or running inside another range-mapped
16//! namespace.
17//!
18//! `SubidMapper` (behind the `subid` feature) delegates to the shadow
19//! suite's privileged `newuidmap`/`newgidmap` helpers, which apply the
20//! subordinate-id allocation an administrator granted in `/etc/subuid` and
21//! `/etc/subgid`. It is the only route to a range map for an ordinary
22//! unprivileged caller, and the one feature of this crate that executes an
23//! external binary.
24//!
25//! Without a configured mapper, [`CageBuilder::build`] resolves a range
26//! request through the bundled delegates in that order. The fallback is
27//! between delegates, never between tiers: a range request no delegate can
28//! satisfy is a configuration error, not a quiet downgrade to the
29//! single-identity map.
30//!
31//! [`CageBuilder::id_mapper`]: crate::CageBuilder::id_mapper
32//! [`CageBuilder::build`]: crate::CageBuilder::build
33
34use std::fmt;
35use std::io;
36
37use rustix::fs::{Mode, OFlags};
38use rustix::io::Errno;
39
40use crate::error::ConfigError;
41
42/// The kernel's ceiling on identity-map extents, `UID_GID_MAP_MAX_EXTENTS`.
43///
44/// A map file accepts at most this many lines (five before Linux 4.16; this
45/// crate requires 5.6 or later, where the limit is 340).
46pub(crate) const MAX_EXTENTS: usize = 340;
47
48/// How the sandbox's user namespace maps identities to the host's.
49///
50/// Selected with [`CageBuilder::identity_map`]. The single-identity map is
51/// established by the sandbox itself; every range map is established from
52/// outside the new namespace by an [`IdMapper`], because the kernel does not
53/// permit a process to write its own multi-id map — not even a process that
54/// was privileged before it unshared.
55///
56/// [`CageBuilder::identity_map`]: crate::CageBuilder::identity_map
57#[derive(Debug, Clone, PartialEq, Eq, Default)]
58#[non_exhaustive]
59#[cfg_attr(
60    feature = "serde",
61    derive(serde::Serialize, serde::Deserialize),
62    serde(rename_all = "kebab-case", try_from = "profile::IdentityMapWire")
63)]
64pub enum IdentityMap {
65    /// Root inside is the calling user outside, and no other id exists.
66    /// This is the default.
67    ///
68    /// Reads of host ids outside the map present the overflow id (`nobody`),
69    /// and changing a file's ownership to any other id fails with `EINVAL`,
70    /// because the namespace cannot represent the id at all.
71    #[default]
72    Single,
73    /// Explicit ranges, written verbatim.
74    ///
75    /// Both lists must be non-empty, map inside-id 0, stay within the
76    /// kernel's 340-extent ceiling, and contain no overlapping extents.
77    /// The configured [`IdMapper`] decides whether the outside ids are
78    /// available to it.
79    ///
80    /// Construct this variant with [`IdentityMap::ranges`]. It is
81    /// `#[non_exhaustive]` so that a further extent list — the kernel has
82    /// `projid_map` alongside `uid_map` and `gid_map` — can be added without
83    /// breaking callers; match it with a trailing `..`.
84    #[non_exhaustive]
85    Ranges {
86        /// The uid extents.
87        uid: Vec<IdRange>,
88        /// The gid extents.
89        gid: Vec<IdRange>,
90    },
91    /// Root plus the caller's whole subordinate allocation, as the delegate
92    /// reports it.
93    ///
94    /// The composed map is `0` to the caller's own id, then inside ids from
95    /// `1` onward covering each allocated range in order. Requires a
96    /// delegate that can report an allocation; the bundled one is
97    /// `SubidMapper`, behind the `subid` feature.
98    Subordinate,
99}
100
101impl IdentityMap {
102    /// Returns an explicit range map of the given uid and gid extents.
103    ///
104    /// The constructor for [`IdentityMap::Ranges`], which is
105    /// `#[non_exhaustive]` so it can gain a further extent list later.
106    ///
107    /// # Example
108    ///
109    /// ```
110    /// use ferroday_cage::{IdRange, IdentityMap};
111    ///
112    /// let root = IdRange { inside: 0, outside: 1000, count: 1 };
113    /// let rest = IdRange { inside: 1, outside: 100_000, count: 65_536 };
114    /// let map = IdentityMap::ranges(vec![root, rest], vec![root, rest]);
115    /// ```
116    pub fn ranges(uid: Vec<IdRange>, gid: Vec<IdRange>) -> IdentityMap {
117        IdentityMap::Ranges { uid, gid }
118    }
119}
120
121/// One contiguous mapping: `count` ids starting at `inside` within the
122/// sandbox correspond to `count` ids starting at `outside` on the host.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
124#[cfg_attr(
125    feature = "serde",
126    derive(serde::Serialize, serde::Deserialize),
127    serde(rename_all = "kebab-case", deny_unknown_fields)
128)]
129pub struct IdRange {
130    /// The first id inside the sandbox.
131    pub inside: u32,
132    /// The first id on the host.
133    pub outside: u32,
134    /// How many consecutive ids the extent covers.
135    pub count: u32,
136}
137
138/// The identity the command executes as inside the sandbox.
139///
140/// Configured with [`CageBuilder::run_as`]; the default is root inside. The
141/// switch sits within the hardening layer, after securebits,
142/// no-new-privileges, and Landlock and before the capability drop and the
143/// seccomp filter, so it is the identity the command holds from its first
144/// instruction. Every id must be mapped:
145/// [`CageBuilder::build`](crate::CageBuilder::build) rejects an identity the
146/// configured [`IdentityMap`] cannot represent.
147///
148/// # Example
149///
150/// ```
151/// use ferroday_cage::Identity;
152///
153/// let identity = Identity::new(250, 250).groups([250, 100]);
154/// ```
155///
156/// [`CageBuilder::run_as`]: crate::CageBuilder::run_as
157#[derive(Debug, Clone, PartialEq, Eq)]
158#[cfg_attr(
159    feature = "serde",
160    derive(serde::Serialize, serde::Deserialize),
161    serde(rename_all = "kebab-case", deny_unknown_fields)
162)]
163pub struct Identity {
164    /// The uid the command runs as, a sandbox-inside id.
165    pub(crate) uid: u32,
166    /// The gid the command runs as, a sandbox-inside id.
167    pub(crate) gid: u32,
168    /// The supplementary groups, sandbox-inside ids. Requires a range gid
169    /// map: the single-identity map denies `setgroups`.
170    #[cfg_attr(
171        feature = "serde",
172        serde(default, skip_serializing_if = "Vec::is_empty")
173    )]
174    pub(crate) groups: Vec<u32>,
175}
176
177impl Identity {
178    /// Returns an identity of the given uid and gid, with no supplementary
179    /// groups.
180    pub fn new(uid: u32, gid: u32) -> Identity {
181        Identity {
182            uid,
183            gid,
184            groups: Vec::new(),
185        }
186    }
187
188    /// Sets the supplementary groups.
189    ///
190    /// Supplementary groups require a range gid map; under the
191    /// single-identity map `setgroups` is denied and any groups are rejected
192    /// at build time.
193    pub fn groups<I: IntoIterator<Item = u32>>(mut self, groups: I) -> Identity {
194        self.groups = groups.into_iter().collect();
195        self
196    }
197}
198
199/// A resolved identity map: the extents a delegate will establish, known at
200/// build time.
201///
202/// Produced by [`IdMapper::resolve`] and consumed by [`IdMapper::apply`].
203/// Resolution happens in [`CageBuilder::build`], so the ranges are available
204/// for validating [`run_as`](crate::CageBuilder::run_as) before any launch.
205///
206/// A delegate constructs one with [`new`](Self::new) and the crate reads it
207/// back through [`uid`](Self::uid) and [`gid`](Self::gid). The fields are
208/// private, and the struct `#[non_exhaustive]`, so a further extent list can
209/// be added without breaking the delegates that build one.
210///
211/// [`CageBuilder::build`]: crate::CageBuilder::build
212#[derive(Debug, Clone, PartialEq, Eq)]
213#[non_exhaustive]
214pub struct ResolvedMap {
215    /// The uid extents, in the order they are written.
216    uid: Vec<IdRange>,
217    /// The gid extents, in the order they are written.
218    gid: Vec<IdRange>,
219}
220
221impl ResolvedMap {
222    /// Returns a resolved map of the given uid and gid extents, in the order
223    /// they are to be written.
224    pub fn new(uid: Vec<IdRange>, gid: Vec<IdRange>) -> ResolvedMap {
225        ResolvedMap { uid, gid }
226    }
227
228    /// The uid extents, in the order they are written.
229    pub fn uid(&self) -> &[IdRange] {
230        &self.uid
231    }
232
233    /// The gid extents, in the order they are written.
234    pub fn gid(&self) -> &[IdRange] {
235        &self.gid
236    }
237}
238
239/// Establishes a user namespace's identity map on behalf of a caller that
240/// cannot write one itself.
241///
242/// The kernel permits a multi-extent map to be written only from outside the
243/// namespace, by a process holding `CAP_SETUID`/`CAP_SETGID` over its
244/// parent. Implementations of this trait are that outside party: the
245/// launch holds the freshly-unshared sandbox at an internal gate, calls
246/// [`apply`](Self::apply) against its pid, and releases it once the map is
247/// in place.
248///
249/// The bundled implementations are [`DirectMapper`] and, behind the `subid`
250/// feature, `SubidMapper`. A caller with site-specific machinery — a
251/// privileged broker, a different helper — supplies its own through
252/// [`CageBuilder::id_mapper`].
253///
254/// # Stability
255///
256/// Every method added to this trait in a later release will carry a default
257/// body, so an existing implementation keeps compiling. The two existing
258/// methods take their inputs as loose parameters because those inputs are
259/// closed: a resolution is a request, and an application is a process and the
260/// extents to write it. Anything richer arrives as a new defaulted method.
261///
262/// [`CageBuilder::id_mapper`]: crate::CageBuilder::id_mapper
263pub trait IdMapper: fmt::Debug + Send + Sync {
264    /// The ranges this delegate can provide for the request.
265    ///
266    /// Called from [`CageBuilder::build`], so a map that cannot be satisfied
267    /// is a configuration error rather than a launch failure. The returned
268    /// map is validated against the same rules as an explicit
269    /// [`IdentityMap::Ranges`].
270    ///
271    /// [`CageBuilder::build`]: crate::CageBuilder::build
272    fn resolve(&self, request: &IdentityMap) -> Result<ResolvedMap, IdMapError>;
273
274    /// Applies the resolved map to a process sitting in a fresh, unmapped
275    /// user namespace.
276    ///
277    /// `pid` names a process that has unshared its user namespace and is
278    /// blocked waiting for the map; the launch machinery guarantees it stays
279    /// blocked until this call returns. The map files are write-once, so the
280    /// whole map must land in one write per file.
281    fn apply(&self, pid: u32, map: &ResolvedMap) -> Result<(), IdMapError>;
282}
283
284/// An identity-map delegate failure.
285///
286/// Returned by [`IdMapper`] implementations. A `resolve` failure surfaces
287/// from [`CageBuilder::build`] as
288/// [`ConfigError::IdentityMapUnavailable`](crate::ConfigError::IdentityMapUnavailable);
289/// an `apply` failure surfaces from the launch as
290/// [`Error::IdentityMap`](crate::Error::IdentityMap).
291///
292/// [`CageBuilder::build`]: crate::CageBuilder::build
293#[derive(Debug)]
294#[non_exhaustive]
295pub enum IdMapError {
296    /// The delegate cannot satisfy the request, and says why.
297    #[non_exhaustive]
298    Unsatisfiable {
299        /// What the delegate is missing: privilege, an allocation, a
300        /// queryable source.
301        reason: String,
302    },
303    /// A helper binary could not be executed.
304    #[non_exhaustive]
305    HelperSpawn {
306        /// The helper that failed to start.
307        helper: String,
308        /// The underlying error, absent where the spawn failed with none.
309        source: Option<io::Error>,
310    },
311    /// A helper binary ran and refused the map.
312    #[non_exhaustive]
313    HelperFailed {
314        /// The helper that refused.
315        helper: String,
316        /// What the helper reported on standard error, trimmed.
317        detail: String,
318    },
319    /// Writing a map file failed.
320    #[non_exhaustive]
321    MapWrite {
322        /// The file that could not be written: `"uid_map"` or `"gid_map"`.
323        which: &'static str,
324        /// The underlying error.
325        source: io::Error,
326    },
327}
328
329impl IdMapError {
330    /// The delegate cannot satisfy the request, and says why.
331    ///
332    /// The refusal every delegate needs: `reason` names what is missing —
333    /// privilege, an allocation, a queryable source — and surfaces verbatim in
334    /// the configuration error the build reports.
335    pub fn unsatisfiable(reason: impl Into<String>) -> IdMapError {
336        IdMapError::Unsatisfiable {
337            reason: reason.into(),
338        }
339    }
340
341    /// A helper binary could not be executed.
342    ///
343    /// `source` is `None` where the failure carried no OS error at all, which
344    /// is a fact rather than an errno of zero: rendering zero would read
345    /// "Success (os error 0)".
346    pub fn helper_spawn(helper: impl Into<String>, source: Option<io::Error>) -> IdMapError {
347        IdMapError::HelperSpawn {
348            helper: helper.into(),
349            source,
350        }
351    }
352
353    /// A helper binary ran and refused the map.
354    pub fn helper_failed(helper: impl Into<String>, detail: impl Into<String>) -> IdMapError {
355        IdMapError::HelperFailed {
356            helper: helper.into(),
357            detail: detail.into(),
358        }
359    }
360
361    /// Writing a map file failed. `which` is `"uid_map"` or `"gid_map"`.
362    pub fn map_write(which: &'static str, source: io::Error) -> IdMapError {
363        IdMapError::MapWrite { which, source }
364    }
365}
366
367impl fmt::Display for IdMapError {
368    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
369        match self {
370            IdMapError::Unsatisfiable { reason } => f.write_str(reason),
371            IdMapError::HelperSpawn {
372                helper,
373                source: None,
374            } => write!(f, "could not execute {helper}"),
375            IdMapError::HelperSpawn {
376                helper,
377                source: Some(source),
378            } => write!(f, "could not execute {helper}: {source}"),
379            IdMapError::HelperFailed { helper, detail } => {
380                if detail.is_empty() {
381                    write!(f, "{helper} refused the map")
382                } else {
383                    write!(f, "{helper} refused the map: {detail}")
384                }
385            }
386            IdMapError::MapWrite { which, source } => {
387                write!(f, "could not write {which}: {source}")
388            }
389        }
390    }
391}
392
393impl std::error::Error for IdMapError {
394    /// The OS failure underneath, where there is one.
395    ///
396    /// [`Unsatisfiable`](Self::Unsatisfiable) and
397    /// [`HelperFailed`](Self::HelperFailed) carry no inner error: the first is
398    /// a refusal this crate composed, and the second is what a helper wrote to
399    /// its standard error, which is text rather than an error value.
400    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
401        match self {
402            IdMapError::HelperSpawn { source, .. } => source
403                .as_ref()
404                .map(|source| source as &(dyn std::error::Error + 'static)),
405            IdMapError::MapWrite { source, .. } => Some(source),
406            _ => None,
407        }
408    }
409}
410
411/// Whether `id` falls inside one of the extents' inside ranges.
412pub(crate) fn maps_inside_id(ranges: &[IdRange], id: u32) -> bool {
413    ranges
414        .iter()
415        .any(|range| id >= range.inside && id - range.inside < range.count)
416}
417
418/// Validates one extent list: non-empty, positive counts, no arithmetic
419/// wrap, inside-id 0 present, at most [`MAX_EXTENTS`] extents, and no
420/// overlap on either side of the mapping.
421///
422/// `which` names the list (`"uid"` or `"gid"`) in the reason.
423fn validate_ranges(which: &str, ranges: &[IdRange]) -> Result<(), String> {
424    if ranges.is_empty() {
425        return Err(format!("the {which} range list is empty"));
426    }
427    if ranges.len() > MAX_EXTENTS {
428        return Err(format!(
429            "the {which} range list has {} extents, over the kernel maximum of {MAX_EXTENTS}",
430            ranges.len(),
431        ));
432    }
433    for range in ranges {
434        if range.count == 0 {
435            return Err(format!(
436                "the {which} extent at inside id {} has a zero count",
437                range.inside,
438            ));
439        }
440        // `start + count` rather than `start + count - 1`: the kernel rejects
441        // the id `(u32)-1`, so an extent ending exactly at `u32::MAX` is one the
442        // map cannot carry. Catching it here keeps the promise that an
443        // unrepresentable extent is a typed build error rather than a failure
444        // at apply time.
445        if range.inside.checked_add(range.count).is_none()
446            || range.outside.checked_add(range.count).is_none()
447        {
448            return Err(format!(
449                "the {which} extent at inside id {} runs past the last usable id",
450                range.inside,
451            ));
452        }
453    }
454    if !maps_inside_id(ranges, 0) {
455        return Err(format!(
456            "the {which} map does not contain inside id 0; the sandbox is built as root inside",
457        ));
458    }
459    check_overlap(which, "inside", ranges, |range| range.inside)?;
460    check_overlap(which, "outside", ranges, |range| range.outside)
461}
462
463/// Rejects overlapping extents on one side of a mapping, `key` selecting
464/// which side is examined.
465///
466/// The arithmetic is checked rather than plain, though both preconditions that
467/// would make it wrap hold at the only call site: `validate_ranges` has already
468/// refused a zero count and an extent running past the last usable id. This is
469/// the identity map's validator and a release build wraps rather than panics,
470/// so a wrapped comparison here would accept the overlap the function exists to
471/// refuse — a cost out of all proportion to the two operations it saves.
472fn check_overlap(
473    which: &str,
474    side: &str,
475    ranges: &[IdRange],
476    key: fn(&IdRange) -> u32,
477) -> Result<(), String> {
478    let mut sorted: Vec<&IdRange> = ranges.iter().collect();
479    sorted.sort_by_key(|range| key(range));
480    for pair in sorted.windows(2) {
481        let (a, b) = (pair[0], pair[1]);
482        let last = key(a).saturating_add(a.count.saturating_sub(1));
483        if last >= key(b) {
484            return Err(format!(
485                "the {which} extents at {side} ids {} and {} overlap",
486                key(a),
487                key(b),
488            ));
489        }
490    }
491    Ok(())
492}
493
494/// Validates a requested identity map's structure.
495///
496/// Single and subordinate maps carry no ranges to validate; explicit ranges
497/// are checked against the kernel's rules so a map the kernel would refuse
498/// is a typed configuration error instead of a launch failure.
499pub(crate) fn validate_request(map: &IdentityMap) -> Result<(), ConfigError> {
500    match map {
501        IdentityMap::Single | IdentityMap::Subordinate => Ok(()),
502        IdentityMap::Ranges { uid, gid } => {
503            for (which, ranges) in [("uid", uid), ("gid", gid)] {
504                validate_ranges(which, ranges)
505                    .map_err(|reason| ConfigError::IdentityMapInvalid { reason })?;
506            }
507            Ok(())
508        }
509    }
510}
511
512/// Validates the map a delegate resolved, with the same rules as an explicit
513/// range request.
514///
515/// A delegate is caller-supplied code; a malformed resolution is reported as
516/// the delegate's defect rather than passed to the kernel.
517pub(crate) fn validate_resolved(map: &ResolvedMap) -> Result<(), ConfigError> {
518    for (which, ranges) in [("uid", map.uid()), ("gid", map.gid())] {
519        validate_ranges(which, ranges).map_err(|reason| ConfigError::IdentityMapInvalid {
520            reason: format!("the identity-map delegate resolved an invalid map: {reason}"),
521        })?;
522    }
523    Ok(())
524}
525
526/// Formats an extent list as the kernel map-file text: one `inside outside
527/// count` line per extent.
528pub(crate) fn format_map(ranges: &[IdRange]) -> Vec<u8> {
529    let mut text = String::new();
530    for range in ranges {
531        text.push_str(&format!(
532            "{} {} {}\n",
533            range.inside, range.outside, range.count
534        ));
535    }
536    text.into_bytes()
537}
538
539/// The map a namespace nested inside one these extents describe must carry to
540/// leave the inside view unchanged: each extent reproduced as an identity
541/// mapping of its own inside ids.
542///
543/// A nested user namespace maps against its parent's id space, not the host's,
544/// so the parent's outside ids are not the ids to name. Reproducing each
545/// extent as `inside inside count` gives the nested namespace exactly the ids
546/// the outer one has, under the same names, and a process inside sees no
547/// change at all.
548///
549/// The extent count is preserved, so a map within the kernel's
550/// [`MAX_EXTENTS`] ceiling reflects to one that is too.
551pub(crate) fn reflect_inside(ranges: &[IdRange]) -> Vec<IdRange> {
552    ranges
553        .iter()
554        .map(|range| IdRange {
555            inside: range.inside,
556            outside: range.inside,
557            count: range.count,
558        })
559        .collect()
560}
561
562/// Opens `path` and writes `data` in a single `write`, as the map files
563/// require: they accept the whole map in one write and nothing else.
564fn write_map_file(path: &str, which: &'static str, data: &[u8]) -> Result<(), IdMapError> {
565    let map_write = |errno: Errno| IdMapError::map_write(which, errno.into());
566    let fd = rustix::fs::open(path, OFlags::WRONLY | OFlags::CLOEXEC, Mode::empty())
567        .map_err(map_write)?;
568    match rustix::io::write(&fd, data) {
569        Ok(n) if n == data.len() => Ok(()),
570        // The kernel takes a map file's contents whole or not at all, so a
571        // short write is not an errno the kernel reported. Fabricating one
572        // would render as "Input/output error" and send a reader looking for a
573        // device fault; `EMSGSIZE` says what happened — the write did not
574        // carry the whole map.
575        Ok(_) => Err(map_write(Errno::MSGSIZE)),
576        Err(errno) => Err(map_write(errno)),
577    }
578}
579
580/// The in-process delegate: the caller writes the sandbox's map itself.
581///
582/// Writing another process's map requires `CAP_SETUID` and `CAP_SETGID`
583/// over the user namespace the new one is a child of — the caller's own
584/// namespace. A process running as root holds them; so does a process
585/// running inside a range-mapped user namespace, over that namespace, which
586/// is what lets a cage nested inside a range-mapped cage (or a rootless
587/// container) establish a range map on its own.
588///
589/// [`resolve`](IdMapper::resolve) checks the capabilities and, where the
590/// caller's own namespace has a readable map, that the requested outside ids
591/// exist in it, so an unsatisfiable request fails at build time with the
592/// reason. `DirectMapper` cannot serve [`IdentityMap::Subordinate`]:
593/// reporting an allocation is the subordinate delegate's business.
594#[derive(Debug, Clone, Copy, Default)]
595#[non_exhaustive]
596pub struct DirectMapper;
597
598impl DirectMapper {
599    /// Returns the direct delegate.
600    pub fn new() -> DirectMapper {
601        DirectMapper
602    }
603}
604
605impl IdMapper for DirectMapper {
606    fn resolve(&self, request: &IdentityMap) -> Result<ResolvedMap, IdMapError> {
607        let (uid, gid) = match request {
608            IdentityMap::Single => {
609                return Err(IdMapError::unsatisfiable(
610                    "the single-identity map is established in process and needs no \
611                             delegate"
612                        .to_string(),
613                ));
614            }
615            IdentityMap::Subordinate => {
616                return Err(IdMapError::unsatisfiable(
617                    "the direct mapper cannot report a subordinate allocation".to_string(),
618                ));
619            }
620            IdentityMap::Ranges { uid, gid } => (uid.clone(), gid.clone()),
621        };
622
623        let effective = rustix::thread::capabilities(None)
624            .map(|sets| sets.effective)
625            .unwrap_or_else(|_| rustix::thread::CapabilitySet::empty());
626        for (capability, name) in [
627            (rustix::thread::CapabilitySet::SETUID, "CAP_SETUID"),
628            (rustix::thread::CapabilitySet::SETGID, "CAP_SETGID"),
629        ] {
630            if !effective.contains(capability) {
631                return Err(IdMapError::unsatisfiable(format!(
632                    "writing a range map directly requires {name} over the current user \
633                         namespace, which the caller does not hold",
634                )));
635            }
636        }
637
638        // Where the caller's own map is readable, require the requested
639        // outside ids to exist in this namespace: the kernel would refuse
640        // them at apply time, and the refusal is clearer here. An unreadable
641        // map skips the check and leaves the kernel as the arbiter.
642        for (which, path, ranges) in [
643            ("uid", "/proc/self/uid_map", &uid),
644            ("gid", "/proc/self/gid_map", &gid),
645        ] {
646            let Some(own) = read_own_map(path) else {
647                continue;
648            };
649            for range in ranges {
650                let last = range.outside.checked_add(range.count.saturating_sub(1));
651                let covered = last.is_some_and(|last| {
652                    own.iter()
653                        .any(|&(inside, count)| range.outside >= inside && last - inside < count)
654                });
655                if !covered {
656                    return Err(IdMapError::unsatisfiable(format!(
657                        "the {which} extent {} {} {} names outside ids that do not exist \
658                             in the caller's user namespace",
659                        range.inside, range.outside, range.count,
660                    )));
661                }
662            }
663        }
664
665        Ok(ResolvedMap::new(uid, gid))
666    }
667
668    fn apply(&self, pid: u32, map: &ResolvedMap) -> Result<(), IdMapError> {
669        // A privileged writer needs no setgroups denial, and none is
670        // performed: a range gid map deliberately leaves `setgroups` at
671        // `allow` so supplementary groups work inside.
672        write_map_file(
673            &format!("/proc/{pid}/uid_map"),
674            "uid_map",
675            &format_map(map.uid()),
676        )?;
677        write_map_file(
678            &format!("/proc/{pid}/gid_map"),
679            "gid_map",
680            &format_map(map.gid()),
681        )
682    }
683}
684
685/// Reads an identity-map file into `(inside, count)` pairs, or `None` when
686/// it cannot be read or parsed. The outside column is not needed: the
687/// question is which ids exist in the namespace the file describes.
688fn read_own_map(path: &str) -> Option<Vec<(u32, u32)>> {
689    let text = std::fs::read_to_string(path).ok()?;
690    let mut ranges = Vec::new();
691    for line in text.lines() {
692        // A line this does not recognize is skipped, not taken as grounds to
693        // abandon the file: "unreadable" is what leaves the kernel as the
694        // arbiter, and one unexpected line is a different thing. Giving up here
695        // would drop the check for both id spaces and turn a named extent at
696        // build time into a bare EPERM at apply time.
697        let mut fields = line.split_whitespace();
698        let (Some(inside), Some(outside), Some(count)) =
699            (fields.next(), fields.next(), fields.next())
700        else {
701            continue;
702        };
703        let (Ok(inside), Ok(_outside), Ok(count)) = (
704            inside.parse::<u32>(),
705            outside.parse::<u64>(),
706            count.parse::<u32>(),
707        ) else {
708            continue;
709        };
710        ranges.push((inside, count));
711    }
712    if ranges.is_empty() {
713        None
714    } else {
715        Some(ranges)
716    }
717}
718
719/// The serde profile forms.
720///
721/// Deserialization runs through a mirror type so that a build without the
722/// `subid` feature refuses a profile requesting the subordinate map at load
723/// time, with a message naming the feature — an identity posture is never
724/// silently downgraded by a library that cannot establish it.
725#[cfg(feature = "serde")]
726mod profile {
727    use super::{IdRange, IdentityMap};
728
729    /// The wire shape of [`IdentityMap`], deserialized then converted.
730    #[derive(serde::Deserialize)]
731    #[serde(rename_all = "kebab-case", deny_unknown_fields)]
732    pub(crate) enum IdentityMapWire {
733        Single,
734        Ranges {
735            uid: Vec<IdRange>,
736            gid: Vec<IdRange>,
737        },
738        Subordinate,
739    }
740
741    impl TryFrom<IdentityMapWire> for IdentityMap {
742        type Error = String;
743
744        fn try_from(wire: IdentityMapWire) -> Result<IdentityMap, String> {
745            Ok(match wire {
746                IdentityMapWire::Single => IdentityMap::Single,
747                IdentityMapWire::Ranges { uid, gid } => IdentityMap::Ranges { uid, gid },
748                #[cfg(feature = "subid")]
749                IdentityMapWire::Subordinate => IdentityMap::Subordinate,
750                #[cfg(not(feature = "subid"))]
751                IdentityMapWire::Subordinate => {
752                    return Err(
753                        "this profile requests the subordinate identity map, but the library \
754                         was built without the `subid` feature"
755                            .to_string(),
756                    );
757                }
758            })
759        }
760    }
761}
762
763#[cfg(feature = "subid")]
764pub use subid::SubidMapper;
765#[cfg(feature = "subid")]
766pub(crate) use subid::{AllocationUnusable, probe_allocation};
767
768/// The subordinate-id delegate: `newuidmap` and `newgidmap`.
769#[cfg(feature = "subid")]
770mod subid {
771    use std::process::Command;
772
773    use super::{IdMapError, IdMapper, IdRange, IdentityMap, ResolvedMap, maps_outside_range};
774
775    /// The delegated mapper: establishes range maps through the shadow
776    /// suite's privileged `newuidmap` and `newgidmap` helpers.
777    ///
778    /// The helpers apply the subordinate-id allocation an administrator
779    /// granted the calling user in `/etc/subuid` and `/etc/subgid` (or a
780    /// site's NSS `subid` source). They are the standard mechanism every
781    /// rootless container runtime uses, and the only route to a range map
782    /// for an unprivileged caller on an ordinary host. This is the one
783    /// delegate that executes an external binary, which is why it lives
784    /// behind the `subid` feature.
785    ///
786    /// [`resolve`](IdMapper::resolve) determines the caller's allocation
787    /// with the NSS-aware `getsubids` query where available, falling back to
788    /// reading `/etc/subuid` and `/etc/subgid` only on hosts whose
789    /// `nsswitch.conf` names no other `subid` source — the same rule the
790    /// helpers themselves follow, so the fallback can never invent an
791    /// allocation an NSS module would deny. [`apply`](IdMapper::apply)
792    /// executes the helpers against the gated sandbox process.
793    ///
794    /// *Which* helpers is decided by the ambient `PATH`, which is worth knowing
795    /// for a consumer running this from a daemon with an inherited environment.
796    /// They are looked up by name among the absolute `PATH` entries — a relative
797    /// entry is skipped rather than resolved against the process's working
798    /// directory — and the resolved absolute path is what executes.
799    /// [`host::range_map_blocker`](crate::host::range_map_blocker) judges a
800    /// helper's privilege through the same lookup, so the two agree on the same
801    /// `PATH`; a `PATH` that changes between the probe and the launch is the
802    /// caller's to keep still. The two auxiliary tools [`resolve`](IdMapper::resolve)
803    /// consults — `id` for the caller's user name, and `getsubids` for the
804    /// allocation itself — are resolved by the same rule.
805    ///
806    /// [`host::range_map_blocker`](crate::host::range_map_blocker) explains
807    /// a host on which this delegate cannot work.
808    #[derive(Debug, Clone, Default)]
809    #[non_exhaustive]
810    pub struct SubidMapper;
811
812    impl SubidMapper {
813        /// Returns the subordinate-id delegate.
814        pub fn new() -> SubidMapper {
815            SubidMapper
816        }
817    }
818
819    impl IdMapper for SubidMapper {
820        fn resolve(&self, request: &IdentityMap) -> Result<ResolvedMap, IdMapError> {
821            match request {
822                IdentityMap::Single => Err(IdMapError::unsatisfiable(
823                    "the single-identity map is established in process and needs no \
824                             delegate"
825                        .to_string(),
826                )),
827                IdentityMap::Subordinate => {
828                    let euid = rustix::process::geteuid().as_raw();
829                    let egid = rustix::process::getegid().as_raw();
830                    let uid_alloc = subordinate_ranges(Kind::Uid)?;
831                    let gid_alloc = subordinate_ranges(Kind::Gid)?;
832                    Ok(ResolvedMap::new(
833                        compose_subordinate(euid, &uid_alloc),
834                        compose_subordinate(egid, &gid_alloc),
835                    ))
836                }
837                IdentityMap::Ranges { uid, gid } => {
838                    // The helpers accept an outside range only when the
839                    // caller's allocation (or the caller's own single id)
840                    // covers it; check here so the refusal happens at build
841                    // time with the offending extent named.
842                    let euid = rustix::process::geteuid().as_raw();
843                    let egid = rustix::process::getegid().as_raw();
844                    for (which, own, ranges, alloc) in [
845                        ("uid", euid, uid, subordinate_ranges(Kind::Uid)?),
846                        ("gid", egid, gid, subordinate_ranges(Kind::Gid)?),
847                    ] {
848                        for range in ranges {
849                            let is_own = range.count == 1 && range.outside == own;
850                            if !is_own && !maps_outside_range(&alloc, range) {
851                                return Err(IdMapError::unsatisfiable(format!(
852                                    "the {which} extent {} {} {} is outside the caller's \
853                                         subordinate allocation",
854                                    range.inside, range.outside, range.count,
855                                )));
856                            }
857                        }
858                    }
859                    Ok(ResolvedMap::new(uid.clone(), gid.clone()))
860                }
861            }
862        }
863
864        fn apply(&self, pid: u32, map: &ResolvedMap) -> Result<(), IdMapError> {
865            run_helper("newuidmap", pid, map.uid())?;
866            run_helper("newgidmap", pid, map.gid())
867        }
868    }
869
870    /// Executes one helper with the extent triples for one map file.
871    ///
872    /// The helper is resolved to an absolute path first, by the same lookup
873    /// [`host::range_map_blocker`](crate::host::range_map_blocker) uses to judge
874    /// its privilege, rather than handed to `Command` as a bare name for it to
875    /// resolve again on its own terms. The two would otherwise be free to differ
876    /// — `Command` searches every `PATH` entry, this lookup only the absolute
877    /// ones — and the probe's verdict would describe a binary that is not the
878    /// one that runs.
879    fn run_helper(helper: &'static str, pid: u32, ranges: &[IdRange]) -> Result<(), IdMapError> {
880        let resolved = crate::host::find_on_path(helper).ok_or_else(|| {
881            IdMapError::helper_spawn(helper, Some(rustix::io::Errno::NOENT.into()))
882        })?;
883        let mut command = Command::new(resolved);
884        command.arg(pid.to_string());
885        for range in ranges {
886            command
887                .arg(range.inside.to_string())
888                .arg(range.outside.to_string())
889                .arg(range.count.to_string());
890        }
891        let output = command.output().map_err(|err| {
892            // A spawn failure with no OS error behind it is reported as
893            // having none, rather than as errno zero.
894            let carried = err.raw_os_error().is_some().then_some(err);
895            IdMapError::helper_spawn(helper, carried)
896        })?;
897        if output.status.success() {
898            Ok(())
899        } else {
900            Err(IdMapError::helper_failed(
901                helper,
902                String::from_utf8_lossy(&output.stderr).trim(),
903            ))
904        }
905    }
906
907    /// Which id space an allocation query concerns.
908    #[derive(Clone, Copy, PartialEq, Eq)]
909    pub(crate) enum Kind {
910        Uid,
911        Gid,
912    }
913
914    impl Kind {
915        fn noun(self) -> &'static str {
916            match self {
917                Kind::Uid => "uid",
918                Kind::Gid => "gid",
919            }
920        }
921
922        fn file(self) -> &'static str {
923            match self {
924                Kind::Uid => "/etc/subuid",
925                Kind::Gid => "/etc/subgid",
926            }
927        }
928    }
929
930    /// The caller's subordinate allocation for one id space, as `(start,
931    /// count)` pairs in the order granted.
932    ///
933    /// The query prefers `getsubids`, which consults the same NSS `subid`
934    /// sources the helpers do. When `getsubids` is absent, the subordinate
935    /// files are read directly only if `nsswitch.conf` names no `subid`
936    /// module beyond `files` — on any other host the files are not the
937    /// authority and reading them could silently miss (or invent) an
938    /// allocation, so the query fails naming the missing tool instead.
939    pub(crate) fn subordinate_ranges(kind: Kind) -> Result<Vec<(u32, u32)>, IdMapError> {
940        allocation(kind).map_err(|unusable| IdMapError::unsatisfiable(unusable.to_string(kind)))
941    }
942
943    /// Why the caller's subordinate allocation cannot be used.
944    ///
945    /// The two are different diagnoses with different remedies, and the host
946    /// probe reports whichever applies rather than collapsing both into "no
947    /// allocation": on a host whose `nsswitch.conf` sources subordinate ids
948    /// from a directory service, the user may well have an allocation, and
949    /// adding `/etc/subuid` entries would not help because the files are not
950    /// the authority there.
951    pub(crate) enum AllocationUnusable {
952        /// The query answered, and the answer is that there is no allocation.
953        Empty,
954        /// The query could not answer at all; the reason names what to do.
955        Unknown(String),
956    }
957
958    impl AllocationUnusable {
959        /// The reason as a message, `kind` naming the id space.
960        pub(crate) fn to_string(&self, kind: Kind) -> String {
961            match self {
962                AllocationUnusable::Empty => format!(
963                    "the calling user has no subordinate {} allocation; grant one with \
964                     usermod --add-sub{}s or an entry in {}",
965                    kind.noun(),
966                    kind.noun(),
967                    kind.file(),
968                ),
969                AllocationUnusable::Unknown(reason) => reason.clone(),
970            }
971        }
972    }
973
974    /// The caller's allocation for one id space, or why it could not be had.
975    pub(crate) fn allocation(kind: Kind) -> Result<Vec<(u32, u32)>, AllocationUnusable> {
976        // The helpers match a subordinate entry by user name or by literal
977        // uid string, so the query tries both. The name lookup is delegated
978        // to `id`, whose NSS-aware resolution covers directory-service
979        // users that /etc/passwd does not list.
980        //
981        // Resolved through `find_on_path` rather than by bare name, for the
982        // reason that lookup exists: `Command`'s own search is `execvp`'s, which
983        // honours a relative or empty `PATH` entry and so would run whatever
984        // binary of that name the process's working directory happens to hold.
985        let mut keys: Vec<String> = Vec::new();
986        if let Some(id) = crate::host::find_on_path("id")
987            && let Ok(output) = Command::new(id).arg("-un").output()
988            && output.status.success()
989            && let Ok(name) = String::from_utf8(output.stdout)
990        {
991            let name = name.trim().to_string();
992            if !name.is_empty() {
993                keys.push(name);
994            }
995        }
996        keys.push(rustix::process::geteuid().as_raw().to_string());
997
998        match query_getsubids(kind, &keys) {
999            Ok(ranges) => return finish(ranges),
1000            Err(GetsubidsUnusable::Absent) => {}
1001            Err(GetsubidsUnusable::Failed(detail)) => {
1002                return Err(AllocationUnusable::Unknown(format!(
1003                    "getsubids could not report the allocation: {detail}"
1004                )));
1005            }
1006        }
1007
1008        if let Some(modules) = non_files_subid_modules() {
1009            return Err(AllocationUnusable::Unknown(format!(
1010                "nsswitch.conf sources subordinate ids from `{modules}` and getsubids is \
1011                     not installed, so the allocation cannot be determined; install getsubids \
1012                     (shadow 4.10 or later)",
1013            )));
1014        }
1015
1016        finish(parse_subid_file(kind.file(), &keys))
1017    }
1018
1019    /// Wraps an allocation query result, turning an empty allocation into
1020    /// the typed refusal.
1021    fn finish(ranges: Vec<(u32, u32)>) -> Result<Vec<(u32, u32)>, AllocationUnusable> {
1022        if ranges.is_empty() {
1023            Err(AllocationUnusable::Empty)
1024        } else {
1025            Ok(ranges)
1026        }
1027    }
1028
1029    /// Why a `getsubids` query produced no answer.
1030    enum GetsubidsUnusable {
1031        /// The tool is not installed; the files may still be authoritative.
1032        Absent,
1033        /// The tool exists but failed; its answer is not safely replaceable
1034        /// by a file read.
1035        Failed(String),
1036    }
1037
1038    /// Queries `getsubids` for each key until one answers with ranges.
1039    ///
1040    /// Output lines have the shape `index: owner start count`.
1041    ///
1042    /// The tool is resolved through
1043    /// [`find_on_path`](crate::host::find_on_path), the same lookup the helpers
1044    /// themselves go through, so only absolute `PATH` entries are searched: what
1045    /// this query answers decides what allocation the delegate believes the
1046    /// caller has, and it is not a question to put to a binary found in whatever
1047    /// directory the process happens to be in. A lookup that finds nothing is
1048    /// the tool being absent, which is the case the files may still answer.
1049    fn query_getsubids(kind: Kind, keys: &[String]) -> Result<Vec<(u32, u32)>, GetsubidsUnusable> {
1050        let Some(getsubids) = crate::host::find_on_path("getsubids") else {
1051            return Err(GetsubidsUnusable::Absent);
1052        };
1053        let mut ranges = Vec::new();
1054        for key in keys {
1055            let mut command = Command::new(&getsubids);
1056            if kind == Kind::Gid {
1057                command.arg("-g");
1058            }
1059            let output = command.arg(key).output().map_err(|err| {
1060                if err.kind() == std::io::ErrorKind::NotFound {
1061                    GetsubidsUnusable::Absent
1062                } else {
1063                    GetsubidsUnusable::Failed(err.to_string())
1064                }
1065            })?;
1066            // A key with no allocation makes getsubids exit non-zero; that
1067            // is an empty answer for this key, not a failure of the tool.
1068            if !output.status.success() {
1069                continue;
1070            }
1071            for line in String::from_utf8_lossy(&output.stdout).lines() {
1072                let mut fields = line.split_whitespace();
1073                let (_index, _owner) = (fields.next(), fields.next());
1074                if let (Some(start), Some(count)) = (fields.next(), fields.next())
1075                    && let (Ok(start), Ok(count)) = (start.parse(), count.parse())
1076                {
1077                    ranges.push((start, count));
1078                }
1079            }
1080            if !ranges.is_empty() {
1081                break;
1082            }
1083        }
1084        Ok(ranges)
1085    }
1086
1087    /// The `subid` modules `nsswitch.conf` names beyond `files`, joined, or
1088    /// `None` when the files are the authority (no `subid` line, or a line
1089    /// naming only `files`).
1090    fn non_files_subid_modules() -> Option<String> {
1091        let text = std::fs::read_to_string("/etc/nsswitch.conf").ok()?;
1092        for line in text.lines() {
1093            let line = line.split('#').next().unwrap_or("").trim();
1094            let Some((database, modules)) = line.split_once(':') else {
1095                continue;
1096            };
1097            if database.trim() != "subid" {
1098                continue;
1099            }
1100            let foreign: Vec<&str> = modules
1101                .split_whitespace()
1102                .filter(|module| *module != "files")
1103                .collect();
1104            if foreign.is_empty() {
1105                return None;
1106            }
1107            return Some(foreign.join(" "));
1108        }
1109        None
1110    }
1111
1112    /// Reads a subordinate file directly, collecting the ranges of every
1113    /// entry whose owner field matches one of the keys.
1114    ///
1115    /// Only reached when the files are the host's authority; see
1116    /// [`subordinate_ranges`].
1117    fn parse_subid_file(path: &str, keys: &[String]) -> Vec<(u32, u32)> {
1118        let Ok(text) = std::fs::read_to_string(path) else {
1119            return Vec::new();
1120        };
1121        let mut ranges = Vec::new();
1122        // The key the file turned out to be written under, fixed by the first
1123        // entry that yielded a range.
1124        let mut matching: Option<&String> = None;
1125        for line in text.lines() {
1126            let line = line.trim();
1127            if line.is_empty() || line.starts_with('#') {
1128                continue;
1129            }
1130            let mut fields = line.split(':');
1131            let Some(owner) = fields.next() else { continue };
1132            // The first key that answers owns the file, as `query_getsubids`
1133            // has it: a host granting the same block under both the user name
1134            // and the literal uid would otherwise yield each range twice, and
1135            // the duplicate overlaps itself and fails the build with a message
1136            // blaming a perfectly legal configuration.
1137            let Some(answering) = keys.iter().find(|key| *key == owner) else {
1138                continue;
1139            };
1140            if matching.is_some_and(|key| key != answering) {
1141                continue;
1142            }
1143            if let (Some(start), Some(count)) = (fields.next(), fields.next())
1144                && let (Ok(start), Ok(count)) = (start.parse(), count.parse())
1145            {
1146                matching = Some(answering);
1147                ranges.push((start, count));
1148            }
1149        }
1150        ranges
1151    }
1152
1153    /// Why the caller's subordinate allocation cannot be used, for the host
1154    /// probe, or `None` when both id spaces answer with one.
1155    ///
1156    /// The reason is carried rather than reduced to "no allocation": the probe
1157    /// reports conditions, and the condition it reports has to be the one whose
1158    /// remedy is right. See [`AllocationUnusable`].
1159    pub(crate) fn probe_allocation() -> Option<(Kind, AllocationUnusable)> {
1160        for kind in [Kind::Uid, Kind::Gid] {
1161            if let Err(unusable) = allocation(kind) {
1162                return Some((kind, unusable));
1163            }
1164        }
1165        None
1166    }
1167
1168    /// Composes the subordinate map: the caller's own id as inside id 0,
1169    /// then inside ids from 1 onward covering each allocated range in order.
1170    fn compose_subordinate(own: u32, allocation: &[(u32, u32)]) -> Vec<IdRange> {
1171        let mut ranges = vec![IdRange {
1172            inside: 0,
1173            outside: own,
1174            count: 1,
1175        }];
1176        let mut inside = 1u32;
1177        for &(start, count) in allocation {
1178            // A zero-count entry contributes nothing but is a legal line in a
1179            // subordinate file; skipping it keeps the entries after it, which
1180            // stopping would silently drop.
1181            if count == 0 {
1182                continue;
1183            }
1184            // An allocation vast enough to exhaust the 32-bit inside space is
1185            // truncated at the boundary; ids beyond it are unaddressable, and
1186            // nothing after this entry can be addressed either.
1187            let count = count.min(u32::MAX - inside);
1188            if count == 0 {
1189                break;
1190            }
1191            // A map file carries at most MAX_EXTENTS lines, and an
1192            // administrator is free to grant more separate ranges than that.
1193            // Truncating keeps a legal host configuration working with a
1194            // smaller map, where emitting them all would fail the build with a
1195            // message blaming the delegate for the host's allocation.
1196            if ranges.len() == super::MAX_EXTENTS {
1197                break;
1198            }
1199            ranges.push(IdRange {
1200                inside,
1201                outside: start,
1202                count,
1203            });
1204            inside += count;
1205        }
1206        ranges
1207    }
1208
1209    #[cfg(test)]
1210    mod tests {
1211        use super::*;
1212
1213        #[test]
1214        fn a_subordinate_map_is_root_then_the_allocation() {
1215            let map = compose_subordinate(1000, &[(100000, 65536), (300000, 1000)]);
1216            assert_eq!(
1217                map,
1218                [
1219                    IdRange {
1220                        inside: 0,
1221                        outside: 1000,
1222                        count: 1
1223                    },
1224                    IdRange {
1225                        inside: 1,
1226                        outside: 100000,
1227                        count: 65536
1228                    },
1229                    IdRange {
1230                        inside: 65537,
1231                        outside: 300000,
1232                        count: 1000
1233                    },
1234                ]
1235            );
1236        }
1237
1238        #[test]
1239        fn an_allocation_beyond_the_extent_ceiling_is_truncated_to_it() {
1240            // A user granted more separate subordinate ranges than a map file
1241            // can carry is unusual but entirely legal. Emitting them all would
1242            // fail `validate_resolved` with a message blaming the bundled
1243            // delegate for the host's allocation; truncating keeps the
1244            // configuration working with a smaller map, as the u32-exhaustion
1245            // case above it already does.
1246            let allocation: Vec<(u32, u32)> = (0..super::super::MAX_EXTENTS + 20)
1247                .map(|i| (100_000 + (i as u32) * 10, 5))
1248                .collect();
1249            let map = compose_subordinate(1000, &allocation);
1250            assert_eq!(map.len(), super::super::MAX_EXTENTS);
1251            // Still a map the kernel accepts, with the caller's own id at
1252            // inside id 0 where every map this library builds has it.
1253            let resolved = ResolvedMap::new(map.clone(), map);
1254            crate::idmap::validate_resolved(&resolved)
1255                .expect("a truncated map is still a valid one");
1256        }
1257
1258        #[test]
1259        fn a_zero_count_allocation_entry_does_not_truncate_the_map() {
1260            // `user:100000:0` parses fine and grants nothing. Stopping there
1261            // would silently drop every range after it, leaving a map far
1262            // smaller than the host allocated and a sandbox unable to reach
1263            // ids it was granted.
1264            let map = compose_subordinate(1000, &[(100000, 0), (300000, 1000)]);
1265            assert_eq!(
1266                map,
1267                [
1268                    IdRange {
1269                        inside: 0,
1270                        outside: 1000,
1271                        count: 1
1272                    },
1273                    IdRange {
1274                        inside: 1,
1275                        outside: 300000,
1276                        count: 1000
1277                    },
1278                ]
1279            );
1280        }
1281    }
1282}
1283
1284/// Resolves a range request through the bundled delegate chain:
1285/// [`DirectMapper`] first, then the subordinate delegate when the `subid`
1286/// feature is enabled. On failure the reason collects each delegate's
1287/// refusal and the host probe's diagnosis, so the error names what to fix.
1288///
1289/// Shared by [`CageBuilder::build`](crate::CageBuilder::build) (when no
1290/// mapper is configured) and by the map-assisted removal in
1291/// [`provision::remove`](crate::provision::remove).
1292pub(crate) fn resolve_default_chain(
1293    request: &IdentityMap,
1294) -> Result<(std::sync::Arc<dyn IdMapper>, ResolvedMap), String> {
1295    let mut reasons: Vec<String> = Vec::new();
1296
1297    let direct: std::sync::Arc<dyn IdMapper> = std::sync::Arc::new(DirectMapper::new());
1298    match direct.resolve(request) {
1299        Ok(map) => return Ok((direct, map)),
1300        Err(err) => reasons.push(format!("direct: {err}")),
1301    }
1302
1303    #[cfg(feature = "subid")]
1304    {
1305        let subid: std::sync::Arc<dyn IdMapper> = std::sync::Arc::new(SubidMapper::new());
1306        match subid.resolve(request) {
1307            Ok(map) => return Ok((subid, map)),
1308            Err(err) => reasons.push(format!("subid: {err}")),
1309        }
1310    }
1311
1312    if let Some(blocker) = crate::host::range_map_blocker() {
1313        reasons.push(blocker.to_string());
1314    }
1315
1316    Err(reasons.join("; "))
1317}
1318
1319/// Whether the allocation covers the extent's whole outside range.
1320///
1321/// Adjoining entries count as one span, which is what shadow's own `have_range`
1322/// does: a caller granted `100000:65536` and `165536:65536` may legally ask for
1323/// a single extent across both, and refusing it would reject a map the delegate
1324/// would have applied. (The kernel's own requirement, which [`DirectMapper`]
1325/// checks against the parent's map, is different and genuinely per-extent.)
1326#[cfg(feature = "subid")]
1327fn maps_outside_range(allocation: &[(u32, u32)], range: &IdRange) -> bool {
1328    let Some(last) = range.outside.checked_add(range.count.saturating_sub(1)) else {
1329        return false;
1330    };
1331    coalesce(allocation)
1332        .into_iter()
1333        .any(|(start, end)| range.outside >= start && last <= end)
1334}
1335
1336/// The allocation as inclusive `(start, end)` spans, with adjoining and
1337/// overlapping entries merged.
1338#[cfg(feature = "subid")]
1339fn coalesce(allocation: &[(u32, u32)]) -> Vec<(u32, u32)> {
1340    let mut spans: Vec<(u32, u32)> = allocation
1341        .iter()
1342        .filter(|&&(_, count)| count > 0)
1343        .map(|&(start, count)| (start, start.saturating_add(count - 1)))
1344        .collect();
1345    spans.sort_unstable();
1346    let mut merged: Vec<(u32, u32)> = Vec::with_capacity(spans.len());
1347    for (start, end) in spans {
1348        match merged.last_mut() {
1349            // `start - 1` is safe: a span starting at 0 sorts first and cannot
1350            // follow another.
1351            Some(last) if start <= last.1 || start - 1 == last.1 => last.1 = last.1.max(end),
1352            _ => merged.push((start, end)),
1353        }
1354    }
1355    merged
1356}
1357
1358#[cfg(test)]
1359mod tests {
1360    use super::*;
1361    use crate::scratch::Scratch;
1362
1363    #[test]
1364    fn an_unparsable_map_line_is_skipped_rather_than_abandoning_the_file() {
1365        // The coverage check exists so that a requested outside id the caller's
1366        // namespace does not hold is named at build time instead of arriving as
1367        // a bare EPERM from the kernel at apply time. Abandoning the file on one
1368        // line the parser did not expect would drop that check silently — and
1369        // drop it for both id spaces, since the failure is per-file.
1370        let dir = Scratch::new("own-map");
1371        let path = dir.join("uid_map");
1372        std::fs::write(
1373            &path,
1374            b"         0       1000          1\n\
1375              this line is not a map entry\n\
1376              1 100000 65536\n",
1377        )
1378        .unwrap();
1379        assert_eq!(
1380            read_own_map(path.to_str().unwrap()),
1381            Some(vec![(0, 1), (1, 65536)]),
1382            "the entries around an unparsable line must survive it"
1383        );
1384
1385        // A file with nothing parsable in it is still "no map to check
1386        // against", which leaves the kernel as the arbiter.
1387        std::fs::write(&path, b"not a map at all\n").unwrap();
1388        assert_eq!(read_own_map(path.to_str().unwrap()), None);
1389        // As is a file that cannot be read.
1390        assert_eq!(read_own_map("/proc/self/no-such-map"), None);
1391    }
1392
1393    fn range(inside: u32, outside: u32, count: u32) -> IdRange {
1394        IdRange {
1395            inside,
1396            outside,
1397            count,
1398        }
1399    }
1400
1401    fn ranges_map(uid: Vec<IdRange>, gid: Vec<IdRange>) -> IdentityMap {
1402        IdentityMap::Ranges { uid, gid }
1403    }
1404
1405    #[test]
1406    fn the_single_and_subordinate_requests_have_nothing_to_validate() {
1407        validate_request(&IdentityMap::Single).unwrap();
1408        validate_request(&IdentityMap::Subordinate).unwrap();
1409    }
1410
1411    #[test]
1412    fn a_plain_range_map_validates() {
1413        validate_request(&ranges_map(
1414            vec![range(0, 1000, 1), range(1, 100000, 65536)],
1415            vec![range(0, 1000, 1), range(1, 100000, 65536)],
1416        ))
1417        .unwrap();
1418    }
1419
1420    #[test]
1421    fn an_empty_range_list_is_rejected() {
1422        let err = validate_request(&ranges_map(vec![], vec![range(0, 1000, 1)])).unwrap_err();
1423        assert!(matches!(
1424            err,
1425            ConfigError::IdentityMapInvalid { reason } if reason.contains("uid range list is empty")
1426        ));
1427    }
1428
1429    #[test]
1430    fn a_zero_count_extent_is_rejected() {
1431        let err = validate_request(&ranges_map(
1432            vec![range(0, 1000, 1), range(1, 100000, 0)],
1433            vec![range(0, 1000, 1)],
1434        ))
1435        .unwrap_err();
1436        assert!(matches!(
1437            err,
1438            ConfigError::IdentityMapInvalid { reason } if reason.contains("zero count")
1439        ));
1440    }
1441
1442    #[test]
1443    fn a_map_without_root_inside_is_rejected() {
1444        let err = validate_request(&ranges_map(
1445            vec![range(1, 100000, 65536)],
1446            vec![range(0, 1000, 1)],
1447        ))
1448        .unwrap_err();
1449        assert!(matches!(
1450            err,
1451            ConfigError::IdentityMapInvalid { reason } if reason.contains("inside id 0")
1452        ));
1453    }
1454
1455    #[test]
1456    fn overlapping_inside_extents_are_rejected() {
1457        let err = validate_request(&ranges_map(
1458            vec![
1459                range(0, 1000, 1),
1460                range(1, 100000, 100),
1461                range(50, 300000, 10),
1462            ],
1463            vec![range(0, 1000, 1)],
1464        ))
1465        .unwrap_err();
1466        assert!(matches!(
1467            err,
1468            ConfigError::IdentityMapInvalid { reason } if reason.contains("overlap")
1469        ));
1470    }
1471
1472    #[test]
1473    fn overlapping_outside_extents_are_rejected() {
1474        let err = validate_request(&ranges_map(
1475            vec![
1476                range(0, 1000, 1),
1477                range(1, 100000, 100),
1478                range(101, 100050, 100),
1479            ],
1480            vec![range(0, 1000, 1)],
1481        ))
1482        .unwrap_err();
1483        assert!(matches!(
1484            err,
1485            ConfigError::IdentityMapInvalid { reason } if reason.contains("overlap")
1486        ));
1487    }
1488
1489    #[test]
1490    fn an_extent_running_past_the_last_usable_id_is_rejected() {
1491        let err = validate_request(&ranges_map(
1492            vec![range(0, 1000, 1), range(u32::MAX, 100000, 2)],
1493            vec![range(0, 1000, 1)],
1494        ))
1495        .unwrap_err();
1496        assert!(matches!(
1497            err,
1498            ConfigError::IdentityMapInvalid { reason } if reason.contains("past the last usable id")
1499        ));
1500
1501        // The bound is the kernel's, which reserves `(u32)-1`: an extent ending
1502        // exactly there is refused here rather than at apply time, where the
1503        // build error the docs promise is no longer available.
1504        let ends_at_max = validate_request(&ranges_map(
1505            vec![range(0, 1000, 1), range(1, u32::MAX - 1, 2)],
1506            vec![range(0, 1000, 1)],
1507        ))
1508        .unwrap_err();
1509        assert!(matches!(
1510            ends_at_max,
1511            ConfigError::IdentityMapInvalid { reason } if reason.contains("past the last usable id")
1512        ));
1513        // One id short of it is fine.
1514        validate_request(&ranges_map(
1515            vec![range(0, 1000, 1), range(1, u32::MAX - 2, 2)],
1516            vec![range(0, 1000, 1)],
1517        ))
1518        .expect("an extent ending one short of the reserved id is representable");
1519    }
1520
1521    #[test]
1522    fn more_extents_than_the_kernel_accepts_are_rejected() {
1523        let mut uid = vec![range(0, 1000, 1)];
1524        for i in 0..MAX_EXTENTS as u32 {
1525            uid.push(range(1 + i * 2, 100000 + i * 2, 1));
1526        }
1527        let err = validate_request(&ranges_map(uid, vec![range(0, 1000, 1)])).unwrap_err();
1528        assert!(matches!(
1529            err,
1530            ConfigError::IdentityMapInvalid { reason } if reason.contains("340")
1531        ));
1532    }
1533
1534    #[test]
1535    fn maps_inside_id_matches_extent_boundaries() {
1536        let ranges = [range(0, 1000, 1), range(1, 100000, 65536)];
1537        assert!(maps_inside_id(&ranges, 0));
1538        assert!(maps_inside_id(&ranges, 1));
1539        assert!(maps_inside_id(&ranges, 65536));
1540        assert!(!maps_inside_id(&ranges, 65537));
1541    }
1542
1543    #[test]
1544    fn a_resolved_map_is_validated_like_a_request() {
1545        let err = validate_resolved(&ResolvedMap::new(
1546            vec![range(1, 100000, 10)],
1547            vec![range(0, 1000, 1)],
1548        ))
1549        .unwrap_err();
1550        assert!(matches!(
1551            err,
1552            ConfigError::IdentityMapInvalid { reason }
1553                if reason.contains("delegate resolved an invalid map")
1554        ));
1555    }
1556
1557    #[test]
1558    fn format_map_emits_one_line_per_extent() {
1559        let text = format_map(&[range(0, 1000, 1), range(1, 100000, 65536)]);
1560        assert_eq!(text, b"0 1000 1\n1 100000 65536\n");
1561    }
1562
1563    #[test]
1564    fn a_reflected_map_names_the_inside_ids_on_both_sides() {
1565        // The nested namespace maps against the outer one's id space, so the
1566        // map that leaves the inside view unchanged names each extent's inside
1567        // ids twice. The outside column of the outer map — the host's ids — has
1568        // no meaning one namespace deeper.
1569        let outer = [range(0, 1000, 1), range(1, 100000, 65536)];
1570        assert_eq!(reflect_inside(&outer), [range(0, 0, 1), range(1, 1, 65536)],);
1571        assert_eq!(format_map(&reflect_inside(&outer)), b"0 0 1\n1 1 65536\n",);
1572
1573        // The reflection preserves the extent count, so a map the kernel
1574        // accepts reflects to one it accepts. Checked at the ceiling, where a
1575        // reflection that split or merged extents would show.
1576        let ceiling: Vec<IdRange> = (0..MAX_EXTENTS as u32)
1577            .map(|i| range(i * 2, 100_000 + i * 2, 1))
1578            .collect();
1579        let reflected = reflect_inside(&ceiling);
1580        assert_eq!(reflected.len(), MAX_EXTENTS);
1581        validate_resolved(&ResolvedMap::new(reflected.clone(), reflected))
1582            .expect("a reflected map at the ceiling is still a valid one");
1583    }
1584
1585    #[test]
1586    fn the_direct_mapper_refuses_single_and_subordinate() {
1587        let mapper = DirectMapper::new();
1588        assert!(matches!(
1589            mapper.resolve(&IdentityMap::Single),
1590            Err(IdMapError::Unsatisfiable { .. })
1591        ));
1592        assert!(matches!(
1593            mapper.resolve(&IdentityMap::Subordinate),
1594            Err(IdMapError::Unsatisfiable { .. })
1595        ));
1596    }
1597
1598    #[cfg(feature = "subid")]
1599    #[test]
1600    fn an_extent_is_checked_against_the_whole_allocation() {
1601        // The containment check behind the `Ranges` refusal. An extent inside
1602        // one entry passes; one running past every entry does not.
1603        let allocation = [(100_000, 65_536), (300_000, 1_000)];
1604        assert!(maps_outside_range(&allocation, &range(1, 100_000, 65_536)));
1605        assert!(maps_outside_range(&allocation, &range(1, 165_535, 1)));
1606        assert!(!maps_outside_range(&allocation, &range(1, 165_536, 1)));
1607        assert!(!maps_outside_range(&allocation, &range(1, 99_999, 2)));
1608        assert!(!maps_outside_range(&allocation, &range(1, 300_000, 1_001)));
1609        // An extent spanning adjoining entries is legal — shadow's own
1610        // `have_range` accepts it — so refusing it would reject a map the
1611        // delegate would have applied.
1612        let adjoining = [(100_000, 65_536), (165_536, 65_536)];
1613        assert!(maps_outside_range(&adjoining, &range(1, 100_000, 131_072)));
1614        // A gap of one id is still a gap.
1615        let gapped = [(100_000, 65_536), (165_537, 65_536)];
1616        assert!(!maps_outside_range(&gapped, &range(1, 100_000, 131_072)));
1617        // An empty allocation covers nothing, and a zero-count entry is empty.
1618        assert!(!maps_outside_range(&[], &range(1, 100_000, 1)));
1619        assert!(!maps_outside_range(&[(100_000, 0)], &range(1, 100_000, 1)));
1620    }
1621
1622    #[test]
1623    fn an_identity_carries_its_groups() {
1624        let identity = Identity::new(250, 250).groups([250, 100]);
1625        assert_eq!(identity.uid, 250);
1626        assert_eq!(identity.gid, 250);
1627        assert_eq!(identity.groups, [250, 100]);
1628    }
1629}