Skip to main content

proot/fs/
binding.rs

1//! Binding tables: longest-prefix substitution between guest and host paths,
2//! and the descriptors the path walk starts from.
3
4use crate::config::{Binding, Config, trim_slashes};
5use crate::procs::ProcTable;
6use std::collections::HashMap;
7use std::ffi::CString;
8use std::os::fd::{FromRawFd, OwnedFd};
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::{Arc, OnceLock, RwLock};
11
12/// A cache entry's key: the view's id and the canonical guest directory.
13type CacheKey = (u64, Vec<u8>);
14
15/// Directory descriptors kept in the cache before it is cleared wholesale.
16///
17/// Each entry holds an open descriptor, so the cap bounds the supervisor's
18/// descriptor table rather than its memory.
19const DIR_CACHE_MAX: usize = 512;
20
21/// `O_PATH` descriptors of canonical guest directories already walked,
22/// shared by every view of one supervisor.
23///
24/// Lets the walk skip the `openat` of an intermediate component, and pins
25/// the directory the component named when it was walked: a later lookup
26/// through the entry lands in that directory whatever the name now points
27/// to.  Entries are keyed by the view they were resolved in, since the same
28/// guest path names different host directories in different views, and the
29/// whole cache is flushed by [`DirCache::flush`] after any syscall that may
30/// replace a directory, whichever view made it: the host directories are
31/// shared.
32#[derive(Debug)]
33pub struct DirCache {
34    entries: RwLock<HashMap<CacheKey, Arc<OwnedFd>>>,
35    /// False when `PROOT_NO_PATH_CACHE` is set.
36    enabled: bool,
37}
38
39impl DirCache {
40    /// Creates an empty cache, disabled when `PROOT_NO_PATH_CACHE` is set.
41    pub fn new() -> DirCache {
42        DirCache {
43            entries: RwLock::new(HashMap::new()),
44            enabled: std::env::var_os("PROOT_NO_PATH_CACHE").is_none(),
45        }
46    }
47
48    /// Forgets every cached directory of every view.
49    ///
50    /// Call after `mkdir`, `rmdir`, `rename`, `unlink`, `symlink`, `link`,
51    /// `mknod` or any other syscall that can turn a directory into something
52    /// else (or the reverse); changes made from outside PRoot are not seen
53    /// until then, and a lookup through a stale entry stays in the directory
54    /// it pinned.
55    pub fn flush(&self) {
56        if let Ok(mut c) = self.entries.write() {
57            c.clear();
58        }
59    }
60}
61
62/// The host side of the root or of a binding, held open once.
63///
64/// Every walk starts from one of these descriptors rather than from the
65/// textual host prefix, so a rename of the prefix's parents on the host (the
66/// rootfs under a bound `$HOME`, say) cannot redirect it.  A host that does
67/// not exist yet is opened on the first lookup that finds it there.
68#[derive(Debug)]
69pub struct Pin {
70    host: Vec<u8>,
71    fd: OnceLock<Arc<OwnedFd>>,
72}
73
74impl Pin {
75    fn new(host: Vec<u8>) -> Pin {
76        Pin {
77            host,
78            fd: OnceLock::new(),
79        }
80    }
81
82    /// The descriptor, opening the host side on first use; `None` while it
83    /// does not exist (a symlink is never a host side: `resolve_host` ran).
84    pub fn get(&self) -> Option<Arc<OwnedFd>> {
85        if let Some(fd) = self.fd.get() {
86            return Some(fd.clone());
87        }
88        let fd = open_path(&self.host, libc::O_NOFOLLOW)?;
89        let _ = self.fd.set(Arc::new(fd));
90        self.fd.get().cloned()
91    }
92}
93
94/// Opens a host path as an `O_PATH|O_CLOEXEC` descriptor with extra `flags`.
95fn open_path(host: &[u8], flags: i32) -> Option<OwnedFd> {
96    let c = CString::new(host.to_vec()).ok()?;
97    let fd = unsafe { libc::open(c.as_ptr(), libc::O_PATH | libc::O_CLOEXEC | flags) };
98    (fd >= 0).then(|| unsafe { OwnedFd::from_raw_fd(fd) })
99}
100
101impl Default for DirCache {
102    fn default() -> Self {
103        DirCache::new()
104    }
105}
106
107/// Source of view identifiers, unique for the life of the supervisor.
108static NEXT_VIEW_ID: AtomicU64 = AtomicU64::new(1);
109
110/// The guest filesystem view: a root directory plus bind mounts.
111#[derive(Debug)]
112pub struct Fs {
113    /// Host directory serving as guest `/`, no trailing slash.
114    pub root: Vec<u8>,
115    /// Bindings sorted so that longer guest prefixes come first.
116    pub bindings: Vec<Binding>,
117    /// True when root is `/` and there are no bindings: translation is a no-op.
118    pub identity: bool,
119    /// True when guest `/proc` is the host's: its magic links mean what they
120    /// mean on the host.
121    pub proc_is_host: bool,
122    /// The root's host side, held open.
123    root_pin: Pin,
124    /// Each binding's host side, held open; same order as `bindings`.
125    pins: Vec<Pin>,
126    /// Identifies this view's entries in the shared directory cache.
127    id: u64,
128    /// Directory cache shared with every other view of the supervisor.
129    cache: Arc<DirCache>,
130    /// Per-process records: the program each process runs, since the kernel's
131    /// own `/proc/<pid>/exe` names the dynamic loader after an exec rewrite,
132    /// and without ptrace the working directory too.
133    pub procs: Arc<ProcTable>,
134}
135
136impl Fs {
137    /// Builds the view from the parsed configuration with its own process
138    /// table and directory cache.
139    ///
140    /// # Arguments
141    /// * `config` - Parsed command line.  Bindings are normalized and sorted.
142    pub fn new(config: &Config) -> Fs {
143        Fs::with_procs(config, Arc::new(ProcTable::new(false)))
144    }
145
146    /// Builds the view from the parsed configuration over a shared process
147    /// table, with a directory cache of its own.
148    ///
149    /// # Arguments
150    /// * `config` - Parsed command line.  Bindings are normalized and sorted.
151    /// * `procs` - Process table shared with the supervisor.
152    pub fn with_procs(config: &Config, procs: Arc<ProcTable>) -> Fs {
153        Fs::with_cache(config, procs, Arc::new(DirCache::new()))
154    }
155
156    /// Builds the view from the parsed configuration over a shared process
157    /// table and a shared directory cache.
158    ///
159    /// The root is made absolute, since it is used from the alias directory
160    /// as well (loader aliases, the exec shim's records).
161    ///
162    /// # Arguments
163    /// * `config` - Parsed command line.  Bindings are normalized and sorted.
164    /// * `procs` - Process table shared with the supervisor.
165    /// * `cache` - Directory cache shared with the supervisor's other views.
166    pub fn with_cache(config: &Config, procs: Arc<ProcTable>, cache: Arc<DirCache>) -> Fs {
167        let root = resolve_host(&config.root);
168        let mut bindings: Vec<Binding> = config
169            .bindings
170            .iter()
171            .map(|b| Binding {
172                host: resolve_host(&b.host),
173                guest: trim_slashes(b.guest.clone()),
174                read_only: b.read_only,
175            })
176            .collect();
177        // Stable sort: longer guest prefix first, later command-line entries first on ties.
178        bindings.reverse();
179        bindings.sort_by_key(|b| std::cmp::Reverse(b.guest.len()));
180        let identity = root == b"/" && bindings.iter().all(|b| b.host == b.guest);
181        let pins = bindings.iter().map(|b| Pin::new(b.host.clone())).collect();
182        let mut fs = Fs {
183            root_pin: Pin::new(root.clone()),
184            root,
185            bindings,
186            identity,
187            proc_is_host: false,
188            pins,
189            id: NEXT_VIEW_ID.fetch_add(1, Ordering::Relaxed),
190            cache,
191            procs,
192        };
193        fs.canonicalize_bindings();
194        fs.proc_is_host = fs.substitute_guest_to_host(b"/proc") == b"/proc";
195        fs
196    }
197
198    /// Rewrites every binding's guest path to the canonical form lookups use.
199    ///
200    /// A lookup is canonicalized through the view before the binding table is
201    /// consulted, so a guest path given through a symlink of the rootfs, such
202    /// as `/lib/ld-linux.so.2` on a merged-usr host where `/lib` points to
203    /// `usr/lib`, would never match unless the table holds the
204    /// `/usr/lib/ld-linux.so.2` the lookup resolves to.  The final component
205    /// is kept as given, and a path whose intermediate components are missing
206    /// stays lexical, as the walk stops there.  The table is re-sorted and the
207    /// identity flag recomputed when anything changed.
208    fn canonicalize_bindings(&mut self) {
209        let canonical: Vec<Vec<u8>> = self
210            .bindings
211            .iter()
212            .map(|b| match super::canon::canonicalize(self, 0, &b.guest, false) {
213                Ok(c) => trim_slashes(c),
214                Err(_) => b.guest.clone(),
215            })
216            .collect();
217        if self.bindings.iter().zip(&canonical).all(|(b, c)| b.guest == *c) {
218            return;
219        }
220        for (b, c) in self.bindings.iter_mut().zip(canonical) {
221            b.guest = c;
222        }
223        let mut pairs: Vec<(Binding, Pin)> = std::mem::take(&mut self.bindings)
224            .into_iter()
225            .zip(std::mem::take(&mut self.pins))
226            .collect();
227        pairs.sort_by_key(|(b, _)| std::cmp::Reverse(b.guest.len()));
228        (self.bindings, self.pins) = pairs.into_iter().unzip();
229        self.identity = self.root == b"/" && self.bindings.iter().all(|b| b.host == b.guest);
230    }
231
232    /// The pinned directory of a canonical guest directory the cache has seen in this view.
233    ///
234    /// # Arguments
235    /// * `guest` - Canonical absolute guest path.
236    pub fn cache_get(&self, guest: &[u8]) -> Option<Arc<OwnedFd>> {
237        if !self.cache.enabled {
238            return None;
239        }
240        self.cache
241            .entries
242            .read()
243            .ok()
244            .and_then(|c| c.get(&(self.id, guest.to_vec())).cloned())
245    }
246
247    /// Records the directory `guest` resolved to in this view.
248    ///
249    /// # Arguments
250    /// * `guest` - Canonical absolute guest path.
251    /// * `fd` - Its `O_PATH` descriptor.
252    pub fn cache_put(&self, guest: &[u8], fd: Arc<OwnedFd>) {
253        if !self.cache.enabled {
254            return;
255        }
256        if let Ok(mut c) = self.cache.entries.write() {
257            if c.len() >= DIR_CACHE_MAX {
258                c.clear();
259            }
260            c.insert((self.id, guest.to_vec()), fd);
261        }
262    }
263
264    /// Whether `guest` is a canonical directory the cache has seen in this view.
265    #[cfg(test)]
266    pub fn cache_has_dir(&self, guest: &[u8]) -> bool {
267        self.cache_get(guest).is_some()
268    }
269
270    /// The pinned host side of the binding whose guest path is exactly
271    /// `guest`, or of the root for `/`.
272    ///
273    /// # Returns
274    /// `None` when `guest` is no mount point of this view; `Some(None)` when
275    /// it is one whose host side does not exist.
276    pub fn mount_pin(&self, guest: &[u8]) -> Option<Option<Arc<OwnedFd>>> {
277        if let Some(i) = self.bindings.iter().position(|b| b.guest == guest) {
278            return Some(self.pins[i].get());
279        }
280        (guest == b"/").then(|| self.root_pin.get())
281    }
282
283    /// Whether a binding lies below `guest`, which then stands in the view as a
284    /// directory even where the host side has nothing under the name: in a
285    /// rootfs the placeholders of `glue` could not be made in, say.
286    ///
287    /// # Arguments
288    /// * `guest` - Canonical absolute guest path.
289    pub fn binding_below(&self, guest: &[u8]) -> bool {
290        self.bindings
291            .iter()
292            .any(|b| b.guest.len() > guest.len() && is_prefix(guest, &b.guest))
293    }
294
295    /// The pinned mount point under which `guest` lies, and the path from there.
296    ///
297    /// # Arguments
298    /// * `guest` - Canonical absolute guest path.
299    ///
300    /// # Returns
301    /// The binding's (or root's) descriptor, `None` when its host side does
302    /// not exist, and the remainder of `guest` below it with its leading
303    /// slash (empty for the mount point itself).
304    pub fn pin_for_guest<'a>(&self, guest: &'a [u8]) -> (Option<Arc<OwnedFd>>, &'a [u8]) {
305        match self.bindings.iter().position(|b| is_prefix(&b.guest, guest)) {
306            Some(i) => (self.pins[i].get(), rest_after(&self.bindings[i].guest, guest)),
307            None => (self.root_pin.get(), rest_after(b"/", guest)),
308        }
309    }
310
311    /// Forgets every cached directory of every view sharing the cache.
312    pub fn flush_cache(&self) {
313        self.cache.flush();
314    }
315
316    /// The recorded guest program of thread `tid`, if any.
317    ///
318    /// # Arguments
319    /// * `tid` - Thread or process id as it appears in a `/proc/<n>` path.
320    pub fn exe_of(&self, tid: i32) -> Option<Vec<u8>> {
321        self.procs.exe_of(tid)
322    }
323
324    /// The virtual working directory of thread `tid`, when cwd is virtual (no ptrace).
325    ///
326    /// # Arguments
327    /// * `tid` - Thread or process id.
328    pub fn virtual_cwd(&self, tid: i32) -> Option<Vec<u8>> {
329        if self.procs.lazy {
330            self.procs.cwd_of(tid)
331        } else {
332            None
333        }
334    }
335
336    /// Finds the binding whose guest prefix covers `guest`, if any.
337    ///
338    /// # Arguments
339    /// * `guest` - Absolute, lexically clean guest path.
340    pub fn binding_for_guest(&self, guest: &[u8]) -> Option<&Binding> {
341        self.bindings.iter().find(|b| is_prefix(&b.guest, guest))
342    }
343
344    /// Finds the binding whose host prefix covers `host`, preferring the longest.
345    ///
346    /// # Arguments
347    /// * `host` - Absolute host path.
348    pub fn binding_for_host(&self, host: &[u8]) -> Option<&Binding> {
349        self.bindings
350            .iter()
351            .filter(|b| is_prefix(&b.host, host))
352            .max_by_key(|b| b.host.len())
353    }
354
355    /// Substitutes the guest prefix of an absolute guest path with its host prefix.
356    ///
357    /// This is purely lexical: no symlink is followed.  Use
358    /// [`crate::fs::translate::translate_path`] for a real translation.
359    ///
360    /// # Arguments
361    /// * `guest` - Absolute guest path.
362    ///
363    /// # Returns
364    /// The host path the guest path maps to.
365    pub fn substitute_guest_to_host(&self, guest: &[u8]) -> Vec<u8> {
366        if self.identity {
367            return guest.to_vec();
368        }
369        match self.binding_for_guest(guest) {
370            Some(b) => join_prefix(&b.host, rest_after(&b.guest, guest)),
371            None => join_prefix(&self.root, rest_after(b"/", guest)),
372        }
373    }
374
375    /// Substitutes the host prefix of a host path with its guest prefix.
376    ///
377    /// # Arguments
378    /// * `host` - Absolute host path.
379    ///
380    /// # Returns
381    /// The guest path, or `None` when the host path is not visible in the guest.
382    pub fn substitute_host_to_guest(&self, host: &[u8]) -> Option<Vec<u8>> {
383        if self.identity {
384            return Some(host.to_vec());
385        }
386        // The root is just the lowest-priority binding of `/`: the longest host
387        // prefix wins, and a binding beats the root on equal length.  This matters
388        // when the rootfs itself lives under a bound directory (e.g. $HOME).
389        let binding = self.binding_for_host(host);
390        let root_hit = is_prefix(&self.root, host);
391        match binding {
392            Some(b) if !root_hit || b.host.len() >= self.root.len() => {
393                Some(join_prefix(&b.guest, rest_after(&b.host, host)))
394            }
395            _ if root_hit => Some(join_prefix(b"/", rest_after(&self.root, host))),
396            _ => None,
397        }
398    }
399}
400
401/// The real host path behind a binding or the root, symlinks resolved.
402///
403/// A binding names a host object, not a symlink to it: `/etc/resolv.conf`
404/// pointing into `/run/systemd/resolve/` must bind the resolver file itself,
405/// or the guest would follow the link inside its own rootfs and find nothing.
406/// Same as PRoot's `realpath` on every binding.  A relative path becomes
407/// absolute the same way.  A path that does not exist (yet) is kept as given.
408fn resolve_host(host: &[u8]) -> Vec<u8> {
409    use std::os::unix::ffi::OsStrExt;
410    let trimmed = trim_slashes(host.to_vec());
411    match std::fs::canonicalize(std::ffi::OsStr::from_bytes(&trimmed)) {
412        Ok(real) => trim_slashes(real.as_os_str().as_bytes().to_vec()),
413        Err(_) => trimmed,
414    }
415}
416
417/// Whether `prefix` is `path` itself or a directory ancestor of it.
418///
419/// A prefix of `/` matches every absolute path.
420pub fn is_prefix(prefix: &[u8], path: &[u8]) -> bool {
421    if prefix == b"/" {
422        return path.first() == Some(&b'/');
423    }
424    path.starts_with(prefix) && (path.len() == prefix.len() || path[prefix.len()] == b'/')
425}
426
427/// The part of `path` after `prefix`, keeping its leading slash.
428///
429/// A prefix of `/` consumes nothing, so the whole path is the remainder.
430pub fn rest_after<'a>(prefix: &[u8], path: &'a [u8]) -> &'a [u8] {
431    if prefix == b"/" {
432        path
433    } else {
434        &path[prefix.len()..]
435    }
436}
437
438/// Concatenates a prefix and a `/`-leading (or empty) remainder without doubling slashes.
439pub fn join_prefix(prefix: &[u8], rest: &[u8]) -> Vec<u8> {
440    let mut out = if prefix == b"/" {
441        Vec::new()
442    } else {
443        prefix.to_vec()
444    };
445    out.extend_from_slice(rest);
446    if out.is_empty() {
447        out.push(b'/');
448    }
449    out
450}
451
452#[cfg(test)]
453mod tests {
454    use super::*;
455
456    fn fs(root: &str, binds: &[(&str, &str)]) -> Fs {
457        let mut c = Config {
458            root: root.as_bytes().to_vec(),
459            ..Config::default()
460        };
461        for (h, g) in binds {
462            c.bindings.push(Binding {
463                host: h.as_bytes().to_vec(),
464                guest: g.as_bytes().to_vec(),
465                read_only: false,
466            });
467        }
468        Fs::new(&c)
469    }
470
471    #[test]
472    fn substitutes_longest_prefix() {
473        let f = fs("/rootfs", &[("/tmp", "/tmp"), ("/host/pulse", "/var/run/pulse")]);
474        assert_eq!(
475            f.substitute_guest_to_host(b"/usr/bin/ls"),
476            b"/rootfs/usr/bin/ls".to_vec()
477        );
478        assert_eq!(f.substitute_guest_to_host(b"/tmp/x"), b"/tmp/x".to_vec());
479        assert_eq!(
480            f.substitute_guest_to_host(b"/var/run/pulse/native"),
481            b"/host/pulse/native".to_vec()
482        );
483        assert_eq!(
484            f.substitute_guest_to_host(b"/var/run/pulsex"),
485            b"/rootfs/var/run/pulsex".to_vec()
486        );
487        assert_eq!(f.substitute_host_to_guest(b"/rootfs/etc"), Some(b"/etc".to_vec()));
488        assert_eq!(
489            f.substitute_host_to_guest(b"/host/pulse/native"),
490            Some(b"/var/run/pulse/native".to_vec())
491        );
492        assert_eq!(f.substitute_host_to_guest(b"/elsewhere"), None);
493        assert_eq!(f.substitute_host_to_guest(b"/rootfs"), Some(b"/".to_vec()));
494    }
495
496    #[test]
497    fn slash_root_with_bindings_keeps_leading_slash() {
498        let f = fs("/", &[("/data/x", "/pn-bind")]);
499        assert!(!f.identity);
500        assert_eq!(
501            f.substitute_host_to_guest(b"/usr/bin/cat"),
502            Some(b"/usr/bin/cat".to_vec())
503        );
504        assert_eq!(
505            f.substitute_guest_to_host(b"/usr/bin/cat"),
506            b"/usr/bin/cat".to_vec()
507        );
508        assert_eq!(f.substitute_guest_to_host(b"/pn-bind/f"), b"/data/x/f".to_vec());
509        assert_eq!(
510            f.substitute_host_to_guest(b"/data/x/f"),
511            Some(b"/pn-bind/f".to_vec())
512        );
513        let g = fs("/rootfs", &[("/", "/host")]);
514        assert_eq!(g.substitute_guest_to_host(b"/host/etc"), b"/etc".to_vec());
515    }
516
517    #[test]
518    fn binding_host_side_is_the_real_path() {
519        let d = std::env::temp_dir().join(format!("pn-bind-link-{}", std::process::id()));
520        let _ = std::fs::remove_dir_all(&d);
521        std::fs::create_dir_all(d.join("real")).unwrap();
522        std::fs::write(d.join("real/conf"), "x").unwrap();
523        std::os::unix::fs::symlink("real/conf", d.join("link")).unwrap();
524        let link = d.join("link").as_os_str().as_encoded_bytes().to_vec();
525        let f = fs("/rootfs", &[(std::str::from_utf8(&link).unwrap(), "/etc/conf")]);
526        let real = std::fs::canonicalize(d.join("real/conf")).unwrap();
527        assert_eq!(
528            f.substitute_guest_to_host(b"/etc/conf"),
529            real.as_os_str().as_encoded_bytes().to_vec()
530        );
531        let _ = std::fs::remove_dir_all(&d);
532    }
533
534    #[test]
535    fn rootfs_under_a_bound_directory_detranslates_to_root() {
536        // proot-apps: the rootfs lives in $HOME and -R binds $HOME.
537        let f = fs(
538            "/home/me/proot-apps/app",
539            &[("/home/me", "/home/me"), ("/tmp", "/tmp")],
540        );
541        assert_eq!(
542            f.substitute_host_to_guest(b"/home/me/proot-apps/app/usr/share"),
543            Some(b"/usr/share".to_vec())
544        );
545        assert_eq!(
546            f.substitute_host_to_guest(b"/home/me/Documents"),
547            Some(b"/home/me/Documents".to_vec())
548        );
549        assert_eq!(
550            f.substitute_host_to_guest(b"/home/me/proot-apps/app"),
551            Some(b"/".to_vec())
552        );
553        assert_eq!(f.substitute_host_to_guest(b"/tmp/x"), Some(b"/tmp/x".to_vec()));
554    }
555}