forge
ExploreLog in

Tahinli/forge

public
0 watching
★ 0
4.7 KiB
1//! Real subuid unpack test. It runs `forge-runner __sandbox-unpack` with a map that uses this
2//! user's `/etc/subuid` and `/etc/subgid` ranges, and checks that a file owned by 1000:1000 in the
3//! layer ends up owned by `start + 999` on the host. The test skips, with a message, when the host
4//! cannot create a mapped user namespace or has no subordinate id range for this user.
5
6use std::fs;
7use std::io::Write;
8use std::os::unix::fs::MetadataExt;
9use std::path::Path;
10use std::process::{Command, Output, Stdio};
11
12use forge_runner::oci::UnpackStats;
13use forge_runner::sandbox::idmap::{IdMap, UsernsMode};
14
15/// The subuid map for this user, or None when the test should skip.
16fn subuid_map() -> Option<IdMap> {
17 let probe = Command::new("unshare")
18 .args(["--map-auto", "--map-root-user", "true"])
19 .status();
20 if !matches!(probe, Ok(status) if status.success()) {
21 eprintln!("skipping: unshare --map-auto --map-root-user does not work here");
22 return None;
23 }
24 match IdMap::resolve(UsernsMode::Subuid) {
25 Ok(map) if map.is_subuid() => Some(map),
26 Ok(map) => {
27 eprintln!("skipping: resolved to {}", map.describe());
28 None
29 }
30 Err(err) => {
31 eprintln!("skipping: no subuid map: {err}");
32 None
33 }
34 }
35}
36
37/// A plain tar with `srv/` and `srv/owned` owned by 1000:1000.
38fn write_layer(path: &Path) {
39 let mut builder = tar::Builder::new(Vec::new());
40
41 let mut dir = tar::Header::new_gnu();
42 dir.set_entry_type(tar::EntryType::Directory);
43 dir.set_mode(0o755);
44 dir.set_size(0);
45 dir.set_uid(1000);
46 dir.set_gid(1000);
47 builder
48 .append_data(&mut dir, "srv/", std::io::empty())
49 .expect("append dir");
50
51 let mut file = tar::Header::new_gnu();
52 file.set_entry_type(tar::EntryType::Regular);
53 file.set_mode(0o644);
54 file.set_size(1);
55 file.set_uid(1000);
56 file.set_gid(1000);
57 builder
58 .append_data(&mut file, "srv/owned", &b"x"[..])
59 .expect("append file");
60
61 let bytes = builder.into_inner().expect("finish tar");
62 fs::write(path, bytes).expect("write layer");
63}
64
65/// Runs `forge-runner __sandbox-unpack` for one layer into `rootfs`.
66fn run_unpack(map: &IdMap, rootfs: &Path, layer: &Path) -> Output {
67 let req = serde_json::json!({
68 "map": map,
69 "rootfs": rootfs,
70 "layers": [layer],
71 "max_bytes": 1u64 << 30,
72 });
73 let mut child = Command::new(env!("CARGO_BIN_EXE_forge-runner"))
74 .arg("__sandbox-unpack")
75 .stdin(Stdio::piped())
76 .stdout(Stdio::piped())
77 .stderr(Stdio::piped())
78 .spawn()
79 .expect("spawn helper");
80 child
81 .stdin
82 .take()
83 .expect("helper stdin")
84 .write_all(&serde_json::to_vec(&req).expect("serialize request"))
85 .expect("write request");
86 child.wait_with_output().expect("wait for helper")
87}
88
89/// Removes a tree that holds subordinate-owned files, through `forge-runner __sandbox-rm`.
90fn remove_with_helper(map: &IdMap, path: &Path) {
91 let mut child = Command::new(env!("CARGO_BIN_EXE_forge-runner"))
92 .arg("__sandbox-rm")
93 .arg(path)
94 .stdin(Stdio::piped())
95 .spawn()
96 .expect("spawn rm helper");
97 if let Some(mut stdin) = child.stdin.take() {
98 let _ = stdin.write_all(&serde_json::to_vec(map).expect("serialize map"));
99 }
100 match child.wait() {
101 Ok(status) if status.success() => {}
102 other => eprintln!("warning: could not remove {}: {other:?}", path.display()),
103 }
104}
105
106#[test]
107fn subuid_unpack_gives_files_their_subordinate_owner() {
108 let Some(map) = subuid_map() else {
109 return;
110 };
111 let (uid_start, _) = map.sub_uid.expect("subuid map has a uid range");
112 let (gid_start, _) = map.sub_gid.expect("subuid map has a gid range");
113
114 let dir = tempfile::tempdir().expect("tempdir");
115 let layer = dir.path().join("layer.tar");
116 write_layer(&layer);
117 let rootfs = dir.path().join("rootfs");
118 fs::create_dir(&rootfs).expect("rootfs");
119
120 let out = run_unpack(&map, &rootfs, &layer);
121 assert!(
122 out.status.success(),
123 "helper failed: {}",
124 String::from_utf8_lossy(&out.stderr)
125 );
126 let stats: UnpackStats = serde_json::from_slice(&out.stdout).expect("stats JSON");
127 assert_eq!(stats.files, 1);
128 assert_eq!(stats.remapped_ids, 0);
129
130 let file = fs::symlink_metadata(rootfs.join("srv/owned")).expect("file metadata");
131 // Container id 1000 is container id 1 + 999, which maps to start + 999 on the host.
132 assert_eq!(file.uid(), uid_start + 999, "host uid of srv/owned");
133 assert_eq!(file.gid(), gid_start + 999, "host gid of srv/owned");
134
135 remove_with_helper(&map, &rootfs);
136}