Skip to main content

isb_core/
idmap.rs

1//! Deciding whether a sandbox needs `raw.idmap`.
2//!
3//! The requirement is only ever that a host uid/gid lands on a guest uid/gid so a
4//! bind mount is writable. Three hosts, three answers:
5//!
6//! - A host whose `/etc/subuid` gives root a range that does NOT contain the uid
7//!   (plus a `root:1000:1` delegation): the default map puts the container
8//!   elsewhere, and `raw.idmap` is what pulls the id through.
9//! - A nested box where root's range starts at 0: the default map is already the
10//!   identity, and asking for `raw.idmap` is refused ("Host ID is in the range of
11//!   subids").
12//! - macOS and its `isb machine` VM: bind sources are the Mac home over Apple's
13//!   virtiofs, which reports every file as owned by whoever asks and writes as
14//!   the Mac user, so every guest uid can already write them.
15//!
16//! Only a real RANGE (count > 1) counts. A `root:1000:1` line is the delegation
17//! that permits `raw.idmap` to map 1000 at all, not a range the default map draws
18//! from; treating it as one answers "not needed" on exactly the host that needs it.
19
20use crate::spec::{IdmapMode, IdmapSpec};
21
22/// Whether `id` falls inside a subordinate id RANGE (count > 1) owned by `owner`
23/// (`root` or `0`) in subuid/subgid file content.
24pub fn in_subid_range(content: &str, owner: &str, id: u32) -> bool {
25    content.lines().any(|line| {
26        let mut parts = line.trim().split(':');
27        let (Some(who), Some(start), Some(count)) = (parts.next(), parts.next(), parts.next())
28        else {
29            return false;
30        };
31        if who != owner && !(owner == "root" && who == "0") {
32            return false;
33        }
34        let (Ok(start), Ok(count)) = (start.trim().parse::<u64>(), count.trim().parse::<u64>())
35        else {
36            return false;
37        };
38        count > 1 && (id as u64) >= start && (id as u64) < start + count
39    })
40}
41
42/// Host facts that decide the idmap. Read from `/etc/subuid` and `/etc/subgid`
43/// (empty where those do not exist).
44#[derive(Debug, Clone, Default)]
45pub struct SubIds {
46    pub subuid: String,
47    pub subgid: String,
48    /// Bind sources live on a filesystem that reports every file as owned by
49    /// whoever asks and writes as one fixed user: the `isb machine`'s macOS
50    /// home over virtiofs. Any guest user can then read and write them, so
51    /// `auto` maps nothing.
52    pub caller_owned: bool,
53}
54
55/// Set in the `isb machine` VM, whose bind sources are the shared Mac home.
56pub const CALLER_OWNED_ENV: &str = "ISB_BIND_CALLER_OWNED";
57
58impl SubIds {
59    pub fn read_host() -> Self {
60        SubIds {
61            subuid: std::fs::read_to_string("/etc/subuid").unwrap_or_default(),
62            subgid: std::fs::read_to_string("/etc/subgid").unwrap_or_default(),
63            caller_owned: cfg!(target_os = "macos")
64                || std::env::var(CALLER_OWNED_ENV).is_ok_and(|v| v == "1"),
65        }
66    }
67}
68
69/// The `raw.idmap` value for a spec on this host, or `None` if it should not be set.
70pub fn resolve(spec: &IdmapSpec, host: &SubIds) -> Option<String> {
71    let (mode, hu, hg, gu, gg) = match spec {
72        IdmapSpec::Raw(r) => return Some(r.raw.clone()),
73        IdmapSpec::Mode(m) => (*m, 1000, 1000, 1000, 1000),
74        IdmapSpec::Map(m) => (m.mode, m.host_uid, m.host_gid, m.guest_uid, m.guest_gid),
75    };
76    let (need_uid, need_gid) = match mode {
77        IdmapMode::None => return None,
78        IdmapMode::Always => (true, true),
79        IdmapMode::Auto if host.caller_owned => return None,
80        IdmapMode::Auto => (
81            !in_subid_range(&host.subuid, "root", hu),
82            !in_subid_range(&host.subgid, "root", hg),
83        ),
84    };
85    render(need_uid.then_some((hu, gu)), need_gid.then_some((hg, gg)))
86}
87
88fn render(uid: Option<(u32, u32)>, gid: Option<(u32, u32)>) -> Option<String> {
89    match (uid, gid) {
90        (None, None) => None,
91        (Some(u), Some(g)) if u == g => Some(format!("both {} {}", u.0, u.1)),
92        (u, g) => {
93            let mut lines = Vec::new();
94            if let Some((h, c)) = u {
95                lines.push(format!("uid {h} {c}"));
96            }
97            if let Some((h, c)) = g {
98                lines.push(format!("gid {h} {c}"));
99            }
100            Some(lines.join("\n"))
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::spec::{IdmapMap, IdmapRaw};
109
110    // titan: root's range starts at 1000000, plus the root:1000:1 delegation.
111    const TITAN: &str = "stephan:100000:65536\nroot:1000000:1000000000\nroot:1000:1\n";
112    // A nested workspace box: root's range starts at 0 (identity map).
113    const BOX: &str = "root:0:1000000000\n";
114
115    fn host(s: &str) -> SubIds {
116        SubIds {
117            subuid: s.into(),
118            subgid: s.into(),
119            caller_owned: false,
120        }
121    }
122
123    // The isb machine: the shared home answers every caller as its owner.
124    #[test]
125    fn auto_maps_nothing_on_caller_owned_binds() {
126        let mut h = host(TITAN);
127        h.caller_owned = true;
128        assert_eq!(resolve(&IdmapSpec::Mode(IdmapMode::Auto), &h), None);
129        assert_eq!(
130            resolve(&IdmapSpec::Mode(IdmapMode::Always), &h).as_deref(),
131            Some("both 1000 1000")
132        );
133    }
134
135    #[test]
136    fn delegation_line_is_not_a_range() {
137        assert!(!in_subid_range(TITAN, "root", 1000));
138        assert!(in_subid_range(BOX, "root", 1000));
139        assert!(in_subid_range("0:0:65536\n", "root", 1000));
140        assert!(!in_subid_range("", "root", 1000));
141        assert!(!in_subid_range("garbage\nroot:x:y\n", "root", 1000));
142        // Range end is exclusive.
143        assert!(!in_subid_range("root:0:1000\n", "root", 1000));
144    }
145
146    #[test]
147    fn auto_on_titan_maps_both() {
148        let s = IdmapSpec::Mode(IdmapMode::Auto);
149        assert_eq!(resolve(&s, &host(TITAN)).as_deref(), Some("both 1000 1000"));
150    }
151
152    #[test]
153    fn auto_in_box_sets_nothing() {
154        let s = IdmapSpec::Mode(IdmapMode::Auto);
155        assert_eq!(resolve(&s, &host(BOX)), None);
156    }
157
158    #[test]
159    fn auto_per_id() {
160        let s = IdmapSpec::Mode(IdmapMode::Auto);
161        let h = SubIds {
162            subuid: BOX.into(),
163            subgid: TITAN.into(),
164            caller_owned: false,
165        };
166        assert_eq!(resolve(&s, &h).as_deref(), Some("gid 1000 1000"));
167    }
168
169    #[test]
170    fn explicit_modes() {
171        assert_eq!(
172            resolve(&IdmapSpec::Mode(IdmapMode::None), &host(TITAN)),
173            None
174        );
175        assert_eq!(
176            resolve(&IdmapSpec::Mode(IdmapMode::Always), &host(BOX)).as_deref(),
177            Some("both 1000 1000")
178        );
179        let raw = IdmapSpec::Raw(IdmapRaw {
180            raw: "uid 5 6".into(),
181        });
182        assert_eq!(resolve(&raw, &host(BOX)).as_deref(), Some("uid 5 6"));
183        let m = IdmapSpec::Map(IdmapMap {
184            mode: IdmapMode::Always,
185            host_uid: 1001,
186            host_gid: 1002,
187            guest_uid: 1000,
188            guest_gid: 1000,
189        });
190        assert_eq!(
191            resolve(&m, &host(BOX)).as_deref(),
192            Some("uid 1001 1000\ngid 1002 1000")
193        );
194    }
195}