1use crate::error::{Error, Result};
21use crate::spec::{IdmapMode, IdmapSpec, MountType, SandboxSpec};
22
23pub fn in_subid_range(content: &str, owner: &str, id: u32) -> bool {
26 content.lines().any(|line| {
27 let mut parts = line.trim().split(':');
28 let (Some(who), Some(start), Some(count)) = (parts.next(), parts.next(), parts.next())
29 else {
30 return false;
31 };
32 if who != owner && !(owner == "root" && who == "0") {
33 return false;
34 }
35 let (Ok(start), Ok(count)) = (start.trim().parse::<u64>(), count.trim().parse::<u64>())
36 else {
37 return false;
38 };
39 count > 1 && (id as u64) >= start && (id as u64) < start + count
40 })
41}
42
43#[derive(Debug, Clone, Default)]
46pub struct SubIds {
47 pub subuid: String,
48 pub subgid: String,
49 pub caller_owned: bool,
54}
55
56pub const CALLER_OWNED_ENV: &str = "ISB_BIND_CALLER_OWNED";
58
59impl SubIds {
60 pub fn read_host() -> Self {
61 SubIds {
62 subuid: std::fs::read_to_string("/etc/subuid").unwrap_or_default(),
63 subgid: std::fs::read_to_string("/etc/subgid").unwrap_or_default(),
64 caller_owned: cfg!(target_os = "macos")
65 || std::env::var(CALLER_OWNED_ENV).is_ok_and(|v| v == "1"),
66 }
67 }
68}
69
70pub fn resolve(spec: &IdmapSpec, host: &SubIds) -> Option<String> {
72 let (mode, hu, hg, gu, gg) = match spec {
73 IdmapSpec::Raw(r) => return Some(r.raw.clone()),
74 IdmapSpec::Mode(m) => (*m, 1000, 1000, 1000, 1000),
75 IdmapSpec::Map(m) => (m.mode, m.host_uid, m.host_gid, m.guest_uid, m.guest_gid),
76 };
77 let (need_uid, need_gid) = match mode {
78 IdmapMode::None => return None,
79 IdmapMode::Always => (true, true),
80 IdmapMode::Auto if host.caller_owned => return None,
81 IdmapMode::Auto => (
82 !in_subid_range(&host.subuid, "root", hu),
83 !in_subid_range(&host.subgid, "root", hg),
84 ),
85 };
86 render(need_uid.then_some((hu, gu)), need_gid.then_some((hg, gg)))
87}
88
89pub const VM_IDMAP_MIN_INCUS: (u32, u32) = (7, 5);
94
95pub fn incus_translates_vm_shares(server_version: Option<&str>) -> bool {
98 let Some(v) = server_version else {
99 return false;
100 };
101 let mut parts = v
102 .split(|c: char| !c.is_ascii_digit())
103 .filter(|p| !p.is_empty());
104 let (Some(major), Some(minor)) = (
105 parts.next().and_then(|p| p.parse::<u32>().ok()),
106 parts.next().and_then(|p| p.parse::<u32>().ok()),
107 ) else {
108 return false;
109 };
110 (major, minor) >= VM_IDMAP_MIN_INCUS
111}
112
113pub fn invoking_ids() -> (u32, u32) {
115 (
116 rustix::process::getuid().as_raw(),
117 rustix::process::getgid().as_raw(),
118 )
119}
120
121pub fn plan_container(
124 spec: Option<&IdmapSpec>,
125 host: &SubIds,
126) -> (Option<IdmapMode>, Option<String>) {
127 let Some(i) = spec else {
128 return (None, None);
129 };
130 let mode = match i {
131 IdmapSpec::Mode(m) => Some(*m),
132 IdmapSpec::Map(m) => Some(m.mode),
133 IdmapSpec::Raw(_) => None,
134 };
135 (mode, resolve(i, host))
136}
137
138pub fn plan_vm(
143 name: &str,
144 spec: &SandboxSpec,
145 incus_version: Option<&str>,
146 invoking: (u32, u32),
147) -> Result<(Option<IdmapMode>, Option<String>)> {
148 if !spec.volumes.iter().any(|v| v.mount_type == MountType::Bind) {
149 return Ok((None, None));
150 }
151 let mode = match &spec.idmap {
152 Some(IdmapSpec::Mode(m)) => Some(*m),
153 Some(IdmapSpec::Map(m)) => Some(m.mode),
154 Some(IdmapSpec::Raw(_)) => None,
155 None => Some(IdmapMode::Auto),
156 };
157 let guest = vm_service_ids(spec.user.as_deref());
158 let Some(value) = resolve_vm(spec.idmap.as_ref(), invoking, guest) else {
159 eprintln!(
160 "isb: warning: {name}: idmap: none shares host bind mounts into a VM untranslated: \
161 guest root creates root-owned files, and setuid binaries, on the host"
162 );
163 return Ok((mode, None));
164 };
165 if !incus_translates_vm_shares(incus_version) {
166 return Err(Error::invalid(format!(
167 "{name}: this VM bind-mounts host directories, and incus {} is not known to \
168 translate their ids (needs {}.{} or later): guest root would own files on \
169 the host and could plant setuid binaries. Upgrade incus, or set \
170 `idmap: none` to share them untranslated (unsafe for untrusted code)",
171 incus_version.unwrap_or("(unknown version)"),
172 VM_IDMAP_MIN_INCUS.0,
173 VM_IDMAP_MIN_INCUS.1,
174 )));
175 }
176 Ok((mode, Some(value)))
177}
178
179pub fn vm_service_ids(user: Option<&str>) -> (u32, u32) {
183 let Some(user) = user.map(str::trim).filter(|u| !u.is_empty()) else {
184 return (0, 0);
185 };
186 let (u, g) = user.split_once(':').unwrap_or((user, ""));
187 let uid = match u {
188 "root" => 0,
189 n => n.parse().unwrap_or(1000),
190 };
191 let gid = match g {
192 "" => uid,
193 "root" => 0,
194 n => n.parse().unwrap_or(uid),
195 };
196 (uid, gid)
197}
198
199pub fn resolve_vm(
210 spec: Option<&IdmapSpec>,
211 invoking: (u32, u32),
212 guest: (u32, u32),
213) -> Option<String> {
214 let (hu, hg, gu, gg) = match spec {
215 Some(IdmapSpec::Raw(r)) => return Some(r.raw.clone()),
216 Some(IdmapSpec::Mode(IdmapMode::None)) => return None,
217 Some(IdmapSpec::Map(m)) if m.mode == IdmapMode::None => return None,
218 Some(IdmapSpec::Map(m)) => (m.host_uid, m.host_gid, m.guest_uid, m.guest_gid),
219 Some(IdmapSpec::Mode(_)) | None => (invoking.0, invoking.1, guest.0, guest.1),
220 };
221 render(Some((hu, gu)), Some((hg, gg)))
222}
223
224fn render(uid: Option<(u32, u32)>, gid: Option<(u32, u32)>) -> Option<String> {
225 match (uid, gid) {
226 (None, None) => None,
227 (Some(u), Some(g)) if u == g => Some(format!("both {} {}", u.0, u.1)),
228 (u, g) => {
229 let mut lines = Vec::new();
230 if let Some((h, c)) = u {
231 lines.push(format!("uid {h} {c}"));
232 }
233 if let Some((h, c)) = g {
234 lines.push(format!("gid {h} {c}"));
235 }
236 Some(lines.join("\n"))
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244 use crate::spec::{IdmapMap, IdmapRaw};
245
246 const TITAN: &str = "stephan:100000:65536\nroot:1000000:1000000000\nroot:1000:1\n";
248 const BOX: &str = "root:0:1000000000\n";
250
251 fn host(s: &str) -> SubIds {
252 SubIds {
253 subuid: s.into(),
254 subgid: s.into(),
255 caller_owned: false,
256 }
257 }
258
259 #[test]
261 fn auto_maps_nothing_on_caller_owned_binds() {
262 let mut h = host(TITAN);
263 h.caller_owned = true;
264 assert_eq!(resolve(&IdmapSpec::Mode(IdmapMode::Auto), &h), None);
265 assert_eq!(
266 resolve(&IdmapSpec::Mode(IdmapMode::Always), &h).as_deref(),
267 Some("both 1000 1000")
268 );
269 }
270
271 #[test]
272 fn vm_default_maps_the_service_user_to_the_invoker() {
273 assert_eq!(
274 resolve_vm(None, (1001, 1002), (1000, 1000)).as_deref(),
275 Some("uid 1001 1000\ngid 1002 1000")
276 );
277 assert_eq!(
278 resolve_vm(
279 Some(&IdmapSpec::Mode(IdmapMode::Auto)),
280 (1000, 1000),
281 (1000, 1000)
282 )
283 .as_deref(),
284 Some("both 1000 1000")
285 );
286 assert_eq!(
287 resolve_vm(
288 Some(&IdmapSpec::Mode(IdmapMode::None)),
289 (1000, 1000),
290 (0, 0)
291 ),
292 None
293 );
294 let root = IdmapSpec::Map(IdmapMap {
295 mode: IdmapMode::Always,
296 host_uid: 1000,
297 host_gid: 1000,
298 guest_uid: 0,
299 guest_gid: 0,
300 });
301 assert_eq!(
302 resolve_vm(Some(&root), (5, 5), (0, 0)).as_deref(),
303 Some("both 1000 0")
304 );
305 }
306
307 #[test]
308 fn vm_guest_id_follows_the_service_user() {
309 assert_eq!(vm_service_ids(None), (0, 0));
310 assert_eq!(vm_service_ids(Some("root")), (0, 0));
311 assert_eq!(vm_service_ids(Some("dev")), (1000, 1000));
312 assert_eq!(vm_service_ids(Some("1001")), (1001, 1001));
313 assert_eq!(vm_service_ids(Some("1001:50")), (1001, 50));
314 }
315
316 #[test]
317 fn vm_translation_needs_incus_7_5() {
318 assert!(incus_translates_vm_shares(Some("7.5.1")));
319 assert!(incus_translates_vm_shares(Some("7.10.0")));
320 assert!(incus_translates_vm_shares(Some("8.0")));
321 assert!(!incus_translates_vm_shares(Some("7.4.9")));
322 assert!(!incus_translates_vm_shares(Some("6.0.4")));
323 assert!(!incus_translates_vm_shares(Some("garbage")));
324 assert!(!incus_translates_vm_shares(None));
325 }
326
327 #[test]
328 fn delegation_line_is_not_a_range() {
329 assert!(!in_subid_range(TITAN, "root", 1000));
330 assert!(in_subid_range(BOX, "root", 1000));
331 assert!(in_subid_range("0:0:65536\n", "root", 1000));
332 assert!(!in_subid_range("", "root", 1000));
333 assert!(!in_subid_range("garbage\nroot:x:y\n", "root", 1000));
334 assert!(!in_subid_range("root:0:1000\n", "root", 1000));
336 }
337
338 #[test]
339 fn auto_on_titan_maps_both() {
340 let s = IdmapSpec::Mode(IdmapMode::Auto);
341 assert_eq!(resolve(&s, &host(TITAN)).as_deref(), Some("both 1000 1000"));
342 }
343
344 #[test]
345 fn auto_in_box_sets_nothing() {
346 let s = IdmapSpec::Mode(IdmapMode::Auto);
347 assert_eq!(resolve(&s, &host(BOX)), None);
348 }
349
350 #[test]
351 fn auto_per_id() {
352 let s = IdmapSpec::Mode(IdmapMode::Auto);
353 let h = SubIds {
354 subuid: BOX.into(),
355 subgid: TITAN.into(),
356 caller_owned: false,
357 };
358 assert_eq!(resolve(&s, &h).as_deref(), Some("gid 1000 1000"));
359 }
360
361 #[test]
362 fn explicit_modes() {
363 assert_eq!(
364 resolve(&IdmapSpec::Mode(IdmapMode::None), &host(TITAN)),
365 None
366 );
367 assert_eq!(
368 resolve(&IdmapSpec::Mode(IdmapMode::Always), &host(BOX)).as_deref(),
369 Some("both 1000 1000")
370 );
371 let raw = IdmapSpec::Raw(IdmapRaw {
372 raw: "uid 5 6".into(),
373 });
374 assert_eq!(resolve(&raw, &host(BOX)).as_deref(), Some("uid 5 6"));
375 let m = IdmapSpec::Map(IdmapMap {
376 mode: IdmapMode::Always,
377 host_uid: 1001,
378 host_gid: 1002,
379 guest_uid: 1000,
380 guest_gid: 1000,
381 });
382 assert_eq!(
383 resolve(&m, &host(BOX)).as_deref(),
384 Some("uid 1001 1000\ngid 1002 1000")
385 );
386 }
387}