Skip to main content

proot/fs/
glue.rs

1//! Mount points for bindings, created where the guest would list them.
2//!
3//! A binding answers every lookup of its guest path, but a directory listing
4//! comes from the kernel, which only sees what is on disk under the guest
5//! path's parent.  A file bound into a directory of the rootfs, such as an
6//! NVIDIA vendor file in `/usr/share/glvnd/egl_vendor.d`, is then found by
7//! name and missing from the listing that libglvnd, the Vulkan loader and the
8//! OpenCL ICD loader take their drivers from.  bubblewrap and the kernel's
9//! mounts create the mount point; PRoot creates an empty placeholder and
10//! removes it on exit.  This does the same, with PRoot's
11//! `PROOT_DONT_POLLUTE_ROOTFS` to opt out.
12//!
13//! A placeholder is only created through real directories: a symlink along
14//! the way stops the walk, so nothing is written outside the directory a
15//! component lexically names.  A component under a read-only binding is
16//! never created, as bubblewrap cannot create one there either.
17//!
18//! Each placeholder carries the `user.proot-next.placeholder` extended
19//! attribute.  One left behind by a supervisor that was killed is found by
20//! it on the next launch and removed with that launch's own, while a file
21//! the rootfs shipped is never mistaken for one.  On a filesystem without
22//! user attributes an orphan stays until removed by hand, as with PRoot.
23
24use 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
33/// Environment variable that turns placeholder creation off, as in PRoot.
34pub const DONT_POLLUTE_VAR: &str = "PROOT_DONT_POLLUTE_ROOTFS";
35
36/// Extended attribute marking a placeholder.
37const MARK: &std::ffi::CStr = c"user.proot-next.placeholder";
38
39/// One mount point created by [`create`], removed again by [`remove`].
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct Placeholder {
42    /// Host path of the created file or directory.
43    pub path: PathBuf,
44    /// Whether it is a directory rather than an empty regular file.
45    pub dir: bool,
46}
47
48/// Creates the missing mount points of every binding of a view.
49///
50/// For each binding whose host side exists, the guest path is walked from
51/// `/` through the view without that binding (and without the bindings
52/// nested under it), and every missing component is created: directories
53/// along the way, then an empty file or a directory for the binding itself,
54/// following the type of its host side.
55///
56/// # Arguments
57/// * `fs` - The view whose bindings need mount points.
58///
59/// # Returns
60/// The placeholders created or adopted, in creation order; empty when
61/// `PROOT_DONT_POLLUTE_ROOTFS` is set or the view is the identity.
62pub 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
73/// Removes placeholders created by [`create`], newest first.
74///
75/// A file is only removed while it is still empty, and a directory only
76/// while it is empty, so anything written there in the meantime is kept.
77///
78/// # Arguments
79/// * `placeholders` - The list [`create`] returned.
80pub 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
161/// The host path `guest` names in the view without `binding` and the
162/// bindings under it, and whether that path is under a read-only binding.
163fn 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}