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}