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}