//! Real subuid unpack test. It runs `forge-runner __sandbox-unpack` with a map that uses this //! user's `/etc/subuid` and `/etc/subgid` ranges, and checks that a file owned by 1000:1000 in the //! layer ends up owned by `start + 999` on the host. The test skips, with a message, when the host //! cannot create a mapped user namespace or has no subordinate id range for this user. use std::fs; use std::io::Write; use std::os::unix::fs::MetadataExt; use std::path::Path; use std::process::{Command, Output, Stdio}; use forge_runner::oci::UnpackStats; use forge_runner::sandbox::idmap::{IdMap, UsernsMode}; /// The subuid map for this user, or None when the test should skip. fn subuid_map() -> Option { let probe = Command::new("unshare") .args(["--map-auto", "--map-root-user", "true"]) .status(); if !matches!(probe, Ok(status) if status.success()) { eprintln!("skipping: unshare --map-auto --map-root-user does not work here"); return None; } match IdMap::resolve(UsernsMode::Subuid) { Ok(map) if map.is_subuid() => Some(map), Ok(map) => { eprintln!("skipping: resolved to {}", map.describe()); None } Err(err) => { eprintln!("skipping: no subuid map: {err}"); None } } } /// A plain tar with `srv/` and `srv/owned` owned by 1000:1000. fn write_layer(path: &Path) { let mut builder = tar::Builder::new(Vec::new()); let mut dir = tar::Header::new_gnu(); dir.set_entry_type(tar::EntryType::Directory); dir.set_mode(0o755); dir.set_size(0); dir.set_uid(1000); dir.set_gid(1000); builder .append_data(&mut dir, "srv/", std::io::empty()) .expect("append dir"); let mut file = tar::Header::new_gnu(); file.set_entry_type(tar::EntryType::Regular); file.set_mode(0o644); file.set_size(1); file.set_uid(1000); file.set_gid(1000); builder .append_data(&mut file, "srv/owned", &b"x"[..]) .expect("append file"); let bytes = builder.into_inner().expect("finish tar"); fs::write(path, bytes).expect("write layer"); } /// Runs `forge-runner __sandbox-unpack` for one layer into `rootfs`. fn run_unpack(map: &IdMap, rootfs: &Path, layer: &Path) -> Output { let req = serde_json::json!({ "map": map, "rootfs": rootfs, "layers": [layer], "max_bytes": 1u64 << 30, }); let mut child = Command::new(env!("CARGO_BIN_EXE_forge-runner")) .arg("__sandbox-unpack") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .expect("spawn helper"); child .stdin .take() .expect("helper stdin") .write_all(&serde_json::to_vec(&req).expect("serialize request")) .expect("write request"); child.wait_with_output().expect("wait for helper") } /// Removes a tree that holds subordinate-owned files, through `forge-runner __sandbox-rm`. fn remove_with_helper(map: &IdMap, path: &Path) { let mut child = Command::new(env!("CARGO_BIN_EXE_forge-runner")) .arg("__sandbox-rm") .arg(path) .stdin(Stdio::piped()) .spawn() .expect("spawn rm helper"); if let Some(mut stdin) = child.stdin.take() { let _ = stdin.write_all(&serde_json::to_vec(map).expect("serialize map")); } match child.wait() { Ok(status) if status.success() => {} other => eprintln!("warning: could not remove {}: {other:?}", path.display()), } } #[test] fn subuid_unpack_gives_files_their_subordinate_owner() { let Some(map) = subuid_map() else { return; }; let (uid_start, _) = map.sub_uid.expect("subuid map has a uid range"); let (gid_start, _) = map.sub_gid.expect("subuid map has a gid range"); let dir = tempfile::tempdir().expect("tempdir"); let layer = dir.path().join("layer.tar"); write_layer(&layer); let rootfs = dir.path().join("rootfs"); fs::create_dir(&rootfs).expect("rootfs"); let out = run_unpack(&map, &rootfs, &layer); assert!( out.status.success(), "helper failed: {}", String::from_utf8_lossy(&out.stderr) ); let stats: UnpackStats = serde_json::from_slice(&out.stdout).expect("stats JSON"); assert_eq!(stats.files, 1); assert_eq!(stats.remapped_ids, 0); let file = fs::symlink_metadata(rootfs.join("srv/owned")).expect("file metadata"); // Container id 1000 is container id 1 + 999, which maps to start + 999 on the host. assert_eq!(file.uid(), uid_start + 999, "host uid of srv/owned"); assert_eq!(file.gid(), gid_start + 999, "host gid of srv/owned"); remove_with_helper(&map, &rootfs); }