1use crate::spec::{IdmapMode, IdmapSpec};
21
22pub 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#[derive(Debug, Clone, Default)]
45pub struct SubIds {
46 pub subuid: String,
47 pub subgid: String,
48 pub caller_owned: bool,
53}
54
55pub 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
69pub 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 const TITAN: &str = "stephan:100000:65536\nroot:1000000:1000000000\nroot:1000:1\n";
112 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 #[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 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}