cap_primitives/fs/
canonicalize.rs

1//! Sandboxed path canonicalization.
2
3use crate::fs::canonicalize_impl;
4#[cfg(racy_asserts)]
5use crate::fs::{file_path, open, OpenOptions};
6use std::path::{Path, PathBuf};
7use std::{fs, io};
8
9/// Canonicalize the given path, ensuring that the resolution of the path never
10/// escapes the directory tree rooted at `start`.
11#[cfg_attr(not(racy_asserts), allow(clippy::let_and_return))]
12#[inline]
13pub fn canonicalize(start: &fs::File, path: &Path) -> io::Result<PathBuf> {
14    // Call the underlying implementation.
15    let result = canonicalize_impl(start, path);
16
17    #[cfg(racy_asserts)]
18    check_canonicalize(start, path, &result);
19
20    result
21}
22
23#[cfg(racy_asserts)]
24fn check_canonicalize(start: &fs::File, path: &Path, result: &io::Result<PathBuf>) {
25    if let Ok(canonical_path) = result {
26        let path_result = open(start, path, OpenOptions::new().read(true));
27        let canonical_result = open(start, canonical_path, OpenOptions::new().read(true));
28        match (path_result, canonical_result) {
29            (Ok(path_file), Ok(canonical_file)) => assert_same_file!(
30                &path_file,
31                &canonical_file,
32                "we should be able to stat paths that we just canonicalized"
33            ),
34            (Err(path_err), Err(canonical_err)) => {
35                assert_eq!(path_err.to_string(), canonical_err.to_string())
36            }
37            other => {
38                // TODO: Checking in the case it does end with ".".
39                if !path.to_string_lossy().ends_with("/.") {
40                    panic!("inconsistent canonicalize checks: {:?}", other);
41                }
42            }
43        }
44
45        // On operating systems which can tell us the path of a file descriptor,
46        // assert that the path we computed canonicalizes to the same thing as
47        // the input canonicalizes too.
48        if let Some(start_abspath) = file_path(start) {
49            let check_abspath = start_abspath.join(path);
50            let result_abspath = start_abspath.join(canonical_path);
51            if let Ok(check_abspath) = fs::canonicalize(check_abspath) {
52                let result_abspath =
53                    fs::canonicalize(result_abspath).expect("we already canonicalized this");
54                assert_eq!(
55                    check_abspath,
56                    result_abspath,
57                    "incorrect canonicalization: start='{:?}' path='{}' result='{}'",
58                    start,
59                    path.display(),
60                    canonical_path.display()
61                );
62                // TODO: When porting to Windows, check whether `start_abspath` not being
63                // a canonicalized path leads to `\\?\` extended path prefix differences.
64                assert!(
65                    result_abspath.starts_with(start_abspath),
66                    "sandbox escape: start='{:?}' path='{}' result='{}'",
67                    start,
68                    path.display(),
69                    canonical_path.display()
70                );
71            }
72        }
73    }
74}