1use super::binding::{Fs, is_prefix, join_prefix, rest_after};
25use crate::config::Binding;
26use std::ffi::OsStr;
27use std::fs;
28use std::os::unix::ffi::OsStrExt;
29use std::os::unix::fs::DirBuilderExt;
30use std::os::unix::fs::OpenOptionsExt;
31use std::path::PathBuf;
32
33pub const DONT_POLLUTE_VAR: &str = "PROOT_DONT_POLLUTE_ROOTFS";
35
36const MARK: &std::ffi::CStr = c"user.proot-next.placeholder";
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Placeholder {
42 pub path: PathBuf,
44 pub dir: bool,
46}
47
48pub fn create(fs: &Fs) -> Vec<Placeholder> {
63 if fs.identity || std::env::var_os(DONT_POLLUTE_VAR).is_some() {
64 return Vec::new();
65 }
66 let mut created = Vec::new();
67 for binding in &fs.bindings {
68 create_for(fs, binding, &mut created);
69 }
70 created
71}
72
73pub fn remove(placeholders: &[Placeholder]) {
81 for p in placeholders.iter().rev() {
82 if p.dir {
83 let _ = fs::remove_dir(&p.path);
84 } else if fs::symlink_metadata(&p.path).is_ok_and(|m| m.is_file() && m.len() == 0) {
85 let _ = fs::remove_file(&p.path);
86 }
87 }
88}
89
90fn create_for(fs: &Fs, binding: &Binding, created: &mut Vec<Placeholder>) {
91 if binding.guest == b"/" {
92 return;
93 }
94 let Ok(target) = fs::metadata(OsStr::from_bytes(&binding.host)) else {
95 return;
96 };
97 let mut guest = Vec::with_capacity(binding.guest.len());
98 let components: Vec<&[u8]> = binding
99 .guest
100 .split(|&c| c == b'/')
101 .filter(|c| !c.is_empty())
102 .collect();
103 for (i, component) in components.iter().enumerate() {
104 guest.push(b'/');
105 guest.extend_from_slice(component);
106 let last = i + 1 == components.len();
107 let (host, read_only) = underlying(fs, binding, &guest);
108 let path = PathBuf::from(OsStr::from_bytes(&host));
109 match fs::symlink_metadata(&path) {
110 Ok(m) if !m.is_dir() && !m.is_file() => return,
111 Ok(m) => {
112 if marked(&path) && !created.iter().any(|p: &Placeholder| p.path == path) {
113 created.push(Placeholder {
114 path,
115 dir: m.is_dir(),
116 });
117 }
118 if m.is_dir() {
119 continue;
120 }
121 return;
122 }
123 Err(e) if e.kind() != std::io::ErrorKind::NotFound => return,
124 Err(_) => {}
125 }
126 if read_only {
127 return;
128 }
129 let dir = !last || target.is_dir();
130 let made = if dir {
131 fs::DirBuilder::new().mode(0o755).create(&path).is_ok()
132 } else {
133 fs::OpenOptions::new()
134 .write(true)
135 .create_new(true)
136 .mode(0o644)
137 .open(&path)
138 .is_ok()
139 };
140 if !made {
141 return;
142 }
143 mark(&path);
144 created.push(Placeholder { path, dir });
145 }
146}
147
148fn mark(path: &std::path::Path) {
149 if let Ok(c) = std::ffi::CString::new(path.as_os_str().as_bytes()) {
150 unsafe { libc::lsetxattr(c.as_ptr(), MARK.as_ptr(), std::ptr::null(), 0, 0) };
151 }
152}
153
154fn marked(path: &std::path::Path) -> bool {
155 let Ok(c) = std::ffi::CString::new(path.as_os_str().as_bytes()) else {
156 return false;
157 };
158 unsafe { libc::lgetxattr(c.as_ptr(), MARK.as_ptr(), std::ptr::null_mut(), 0) >= 0 }
159}
160
161fn underlying(fs: &Fs, binding: &Binding, guest: &[u8]) -> (Vec<u8>, bool) {
164 match fs
165 .bindings
166 .iter()
167 .find(|b| is_prefix(&b.guest, guest) && !is_prefix(&binding.guest, &b.guest))
168 {
169 Some(b) => (join_prefix(&b.host, rest_after(&b.guest, guest)), b.read_only),
170 None => (join_prefix(&fs.root, rest_after(b"/", guest)), false),
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::config::Config;
178
179 fn scratch(name: &str) -> PathBuf {
180 let d = std::env::temp_dir().join(format!("proot-glue-{name}-{}", std::process::id()));
181 let _ = fs::remove_dir_all(&d);
182 fs::create_dir_all(&d).unwrap();
183 d
184 }
185
186 fn bytes(p: &std::path::Path) -> Vec<u8> {
187 p.as_os_str().as_bytes().to_vec()
188 }
189
190 fn view(root: &std::path::Path, bindings: &[(&std::path::Path, &str, bool)]) -> Fs {
191 let config = Config {
192 root: bytes(root),
193 bindings: bindings
194 .iter()
195 .map(|(host, guest, read_only)| Binding {
196 host: bytes(host),
197 guest: guest.as_bytes().to_vec(),
198 read_only: *read_only,
199 })
200 .collect(),
201 ..Config::default()
202 };
203 Fs::new(&config)
204 }
205
206 #[test]
207 fn creates_file_and_directory_mount_points_and_removes_them() {
208 let d = scratch("basic");
209 let root = d.join("root");
210 fs::create_dir_all(root.join("etc/vk")).unwrap();
211 fs::write(d.join("icd.json"), b"{}").unwrap();
212 fs::create_dir(d.join("srcdir")).unwrap();
213 let fs_view = view(
214 &root,
215 &[
216 (&d.join("icd.json"), "/etc/vk/icd.json", true),
217 (&d.join("srcdir"), "/opt/a/b", false),
218 ],
219 );
220 let made = create(&fs_view);
221 assert!(root.join("etc/vk/icd.json").is_file());
222 assert!(root.join("opt/a/b").is_dir());
223 assert_eq!(made.len(), 4);
224 remove(&made);
225 assert!(!root.join("etc/vk/icd.json").exists());
226 assert!(!root.join("opt").exists());
227 assert!(root.join("etc/vk").is_dir());
228 let _ = fs::remove_dir_all(&d);
229 }
230
231 #[test]
232 fn leaves_existing_paths_symlinks_and_read_only_parents_alone() {
233 let d = scratch("guards");
234 let root = d.join("root");
235 let outside = d.join("outside");
236 fs::create_dir_all(root.join("etc")).unwrap();
237 fs::create_dir_all(&outside).unwrap();
238 fs::write(root.join("etc/hosts"), b"real").unwrap();
239 std::os::unix::fs::symlink(&outside, root.join("link")).unwrap();
240 fs::write(d.join("f"), b"x").unwrap();
241 let ro = d.join("ro");
242 fs::create_dir(&ro).unwrap();
243 let fs_view = view(
244 &root,
245 &[
246 (&d.join("f"), "/etc/hosts", false),
247 (&d.join("f"), "/link/escape", false),
248 (&ro, "/ro", true),
249 (&d.join("f"), "/ro/inner", false),
250 (&d.join("missing"), "/etc/missing", false),
251 ],
252 );
253 let made = create(&fs_view);
254 assert!(made.contains(&Placeholder {
255 path: root.join("ro"),
256 dir: true
257 }));
258 assert!(made.iter().all(|p| p.path.starts_with(&root)));
259 assert!(!outside.join("escape").exists());
260 assert!(
261 root.join(outside.strip_prefix("/").unwrap())
262 .join("escape")
263 .is_file()
264 );
265 assert!(!ro.join("inner").exists());
266 assert!(!root.join("etc/missing").exists());
267 assert_eq!(fs::read(root.join("etc/hosts")).unwrap(), b"real");
268 remove(&made);
269 assert!(!root.join("tmp").exists());
270 let _ = fs::remove_dir_all(&d);
271 }
272
273 #[test]
274 fn adopts_an_orphan_left_by_a_killed_supervisor() {
275 let d = scratch("orphan");
276 let root = d.join("root");
277 fs::create_dir_all(root.join("etc")).unwrap();
278 fs::write(d.join("f"), b"x").unwrap();
279 fs::write(root.join("etc/shipped"), b"").unwrap();
280 let fs_view = view(
281 &root,
282 &[
283 (&d.join("f"), "/etc/new", false),
284 (&d.join("f"), "/etc/shipped", false),
285 ],
286 );
287 let first = create(&fs_view);
288 if first.is_empty() || !marked(&root.join("etc/new")) {
289 eprintln!("skipping: no user extended attributes on {}", d.display());
290 let _ = fs::remove_dir_all(&d);
291 return;
292 }
293 let second = create(&fs_view);
294 assert_eq!(second, first);
295 remove(&second);
296 assert!(!root.join("etc/new").exists());
297 assert!(root.join("etc/shipped").exists());
298 let _ = fs::remove_dir_all(&d);
299 }
300
301 #[test]
302 fn keeps_a_placeholder_that_was_written_to() {
303 let d = scratch("written");
304 let p = d.join("file");
305 fs::write(&p, b"data").unwrap();
306 remove(&[Placeholder {
307 path: p.clone(),
308 dir: false,
309 }]);
310 assert!(p.exists());
311 let _ = fs::remove_dir_all(&d);
312 }
313}