safe_path/
pinned_path_buf.rs

1// Copyright (c) 2022 Alibaba Cloud
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5
6use std::ffi::{CString, OsStr};
7use std::fs::{self, File, Metadata, OpenOptions};
8use std::io::{Error, ErrorKind, Result};
9use std::ops::Deref;
10use std::os::unix::ffi::OsStrExt;
11use std::os::unix::fs::OpenOptionsExt;
12use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
13use std::path::{Component, Path, PathBuf};
14
15use crate::scoped_join;
16
17/// A safe version of [`PathBuf`] pinned to an underlying filesystem object to protect from
18/// `TOCTTOU` style of attacks.
19///
20/// A [`PinnedPathBuf`] is a resolved path buffer pinned to an underlying filesystem object, which
21/// guarantees:
22/// - the value of [`PinnedPathBuf::as_path()`] never changes.
23/// - the path returned by [`PinnedPathBuf::as_path()`] is always a symlink.
24/// - the filesystem object referenced by the symlink [`PinnedPathBuf::as_path()`] never changes.
25/// - the value of [`PinnedPathBuf::target()`] never changes.
26///
27/// Note:
28/// - Though the filesystem object referenced by the symlink [`PinnedPathBuf::as_path()`] never
29///   changes, the value of `fs::read_link(PinnedPathBuf::as_path())` may change due to filesystem
30///   operations.
31/// - The value of [`PinnedPathBuf::target()`] is a cached version of
32///   `fs::read_link(PinnedPathBuf::as_path())` generated when creating the `PinnedPathBuf` object.
33/// - It's a sign of possible attacks if `[PinnedPathBuf::target()]` doesn't match
34///   `fs::read_link(PinnedPathBuf::as_path())`.
35/// - Once the [`PinnedPathBuf`] object gets dropped, the [`Path`] returned by
36///   [`PinnedPathBuf::as_path()`] becomes invalid.
37///
38/// With normal [`PathBuf`], there's a race window for attackers between time to validate a path and
39/// time to use the path. An attacker may maliciously change filesystem object referenced by the
40/// path by using symlinks to compose an attack.
41///
42/// The [`PinnedPathBuf`] is introduced to protect from such attacks, by using the
43/// `/proc/self/fd/xxx` files on Linux. The `/proc/self/fd/xxx` file on Linux is a symlink to the
44/// real target corresponding to the process's file descriptor `xxx`. And the target filesystem
45/// object referenced by the symlink will be kept stable until the file descriptor has been closed.
46/// Combined with `O_PATH`, a safe version of `PathBuf` could be built by:
47/// - Generate a safe path from `root` and `path` by using [`crate::scoped_join()`].
48/// - Open the safe path with O_PATH | O_CLOEXEC flags, say the fd number is `fd_num`.
49/// - Read the symlink target of `/proc/self/fd/fd_num`.
50/// - Compare the symlink target with the safe path, it's safe if these two paths equal.
51/// - Use the proc file path as a safe version of [`PathBuf`].
52/// - Close the `fd_num` when dropping the [`PinnedPathBuf`] object.
53#[derive(Debug)]
54pub struct PinnedPathBuf {
55    handle: File,
56    path: PathBuf,
57    target: PathBuf,
58}
59
60impl PinnedPathBuf {
61    /// Create a [`PinnedPathBuf`] object from `root` and `path`.
62    ///
63    /// The `path` must be a subdirectory of `root`, otherwise error will be returned.
64    pub fn new<R: AsRef<Path>, U: AsRef<Path>>(root: R, path: U) -> Result<Self> {
65        let path = scoped_join(root, path)?;
66        Self::from_path(path)
67    }
68
69    /// Create a `PinnedPathBuf` from `path`.
70    ///
71    /// If the resolved value of `path` doesn't equal to `path`, an error will be returned.
72    pub fn from_path<P: AsRef<Path>>(orig_path: P) -> Result<Self> {
73        let orig_path = orig_path.as_ref();
74        let handle = Self::open_by_path(orig_path)?;
75        Self::new_from_file(handle, orig_path)
76    }
77
78    /// Try to clone the [`PinnedPathBuf`] object.
79    pub fn try_clone(&self) -> Result<Self> {
80        let fd = unsafe { libc::dup(self.path_fd()) };
81        if fd < 0 {
82            Err(Error::last_os_error())
83        } else {
84            Ok(Self {
85                handle: unsafe { File::from_raw_fd(fd) },
86                path: Self::get_proc_path(fd),
87                target: self.target.clone(),
88            })
89        }
90    }
91
92    /// Return the underlying file descriptor representing the pinned path.
93    ///
94    /// Following operations are supported by the returned `RawFd`:
95    /// - fchdir
96    /// - fstat/fstatfs
97    /// - openat/linkat/fchownat/fstatat/readlinkat/mkdirat/*at
98    /// - fcntl(F_GETFD, F_SETFD, F_GETFL)
99    pub fn path_fd(&self) -> RawFd {
100        self.handle.as_raw_fd()
101    }
102
103    /// Get the symlink path referring the target filesystem object.
104    pub fn as_path(&self) -> &Path {
105        self.path.as_path()
106    }
107
108    /// Get the cached real path of the target filesystem object.
109    ///
110    /// The target path is cached version of `fs::read_link(PinnedPathBuf::as_path())` generated
111    /// when creating the `PinnedPathBuf` object. On the other hand, the value of
112    /// `fs::read_link(PinnedPathBuf::as_path())` may change due to underlying filesystem operations.
113    /// So it's a sign of possible attacks if `PinnedPathBuf::target()` does not match
114    /// `fs::read_link(PinnedPathBuf::as_path())`.
115    pub fn target(&self) -> &Path {
116        &self.target
117    }
118
119    /// Get [`Metadata`] about the path handle.
120    pub fn metadata(&self) -> Result<Metadata> {
121        self.handle.metadata()
122    }
123
124    /// Open a direct child of the filesystem objected referenced by the `PinnedPathBuf` object.
125    pub fn open_child(&self, path_comp: &OsStr) -> Result<Self> {
126        let name = Self::prepare_path_component(path_comp)?;
127        let oflags = libc::O_PATH | libc::O_CLOEXEC;
128        let res = unsafe { libc::openat(self.path_fd(), name.as_ptr(), oflags, 0) };
129        if res < 0 {
130            Err(Error::last_os_error())
131        } else {
132            let handle = unsafe { File::from_raw_fd(res) };
133            Self::new_from_file(handle, self.target.join(path_comp))
134        }
135    }
136
137    /// Create or open a child directory if current object is a directory.
138    pub fn mkdir(&self, path_comp: &OsStr, mode: libc::mode_t) -> Result<Self> {
139        let path_name = Self::prepare_path_component(path_comp)?;
140        let res = unsafe { libc::mkdirat(self.handle.as_raw_fd(), path_name.as_ptr(), mode) };
141        if res < 0 {
142            Err(Error::last_os_error())
143        } else {
144            self.open_child(path_comp)
145        }
146    }
147
148    /// Open a directory/file by path.
149    ///
150    /// Obtain a file descriptor that can be used for two purposes:
151    /// - indicate a location in the filesystem tree
152    /// - perform operations that act purely at the file descriptor level
153    fn open_by_path<P: AsRef<Path>>(path: P) -> Result<File> {
154        // When O_PATH is specified in flags, flag bits other than O_CLOEXEC, O_DIRECTORY, and
155        // O_NOFOLLOW are ignored.
156        let o_flags = libc::O_PATH | libc::O_CLOEXEC;
157        OpenOptions::new()
158            .read(true)
159            .custom_flags(o_flags)
160            .open(path.as_ref())
161    }
162
163    fn get_proc_path<F: AsRawFd>(file: F) -> PathBuf {
164        PathBuf::from(format!("/proc/self/fd/{}", file.as_raw_fd()))
165    }
166
167    fn new_from_file<P: AsRef<Path>>(handle: File, orig_path: P) -> Result<Self> {
168        let path = Self::get_proc_path(handle.as_raw_fd());
169        let link_path = fs::read_link(path.as_path())?;
170        if link_path != orig_path.as_ref() {
171            Err(Error::new(
172                ErrorKind::Other,
173                format!(
174                    "Path changed from {} to {} on open, possible attack",
175                    orig_path.as_ref().display(),
176                    link_path.display()
177                ),
178            ))
179        } else {
180            Ok(PinnedPathBuf {
181                handle,
182                path,
183                target: link_path,
184            })
185        }
186    }
187
188    #[inline]
189    fn prepare_path_component(path_comp: &OsStr) -> Result<CString> {
190        let path = Path::new(path_comp);
191        let mut comps = path.components();
192        let name = comps.next();
193        if !matches!(name, Some(Component::Normal(_))) || comps.next().is_some() {
194            return Err(Error::new(
195                ErrorKind::Other,
196                format!("Path component {} is invalid", path_comp.to_string_lossy()),
197            ));
198        }
199        let name = name.unwrap();
200        if name.as_os_str() != path_comp {
201            return Err(Error::new(
202                ErrorKind::Other,
203                format!("Path component {} is invalid", path_comp.to_string_lossy()),
204            ));
205        }
206
207        CString::new(path_comp.as_bytes()).map_err(|_e| {
208            Error::new(
209                ErrorKind::Other,
210                format!("Path component {} is invalid", path_comp.to_string_lossy()),
211            )
212        })
213    }
214}
215
216impl Deref for PinnedPathBuf {
217    type Target = PathBuf;
218
219    fn deref(&self) -> &Self::Target {
220        &self.path
221    }
222}
223
224impl AsRef<Path> for PinnedPathBuf {
225    fn as_ref(&self) -> &Path {
226        self.path.as_path()
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use std::ffi::OsString;
234    use std::fs::DirBuilder;
235    use std::io::Write;
236    use std::os::unix::fs::{symlink, MetadataExt};
237    use std::sync::{Arc, Barrier};
238    use std::thread;
239
240    #[test]
241    fn test_pinned_path_buf() {
242        // Create a root directory, which itself contains symlinks.
243        let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
244        DirBuilder::new()
245            .create(rootfs_dir.path().join("b"))
246            .unwrap();
247        symlink(rootfs_dir.path().join("b"), rootfs_dir.path().join("a")).unwrap();
248        let rootfs_path = &rootfs_dir.path().join("a");
249
250        // Create a file and a symlink to it.
251        fs::create_dir(rootfs_path.join("symlink_dir")).unwrap();
252        symlink("/endpoint", rootfs_path.join("symlink_dir/endpoint")).unwrap();
253        fs::write(rootfs_path.join("endpoint"), "test").unwrap();
254
255        // Pin the target and validate the path/content.
256        let path = PinnedPathBuf::new(rootfs_path.to_path_buf(), "symlink_dir/endpoint").unwrap();
257        assert!(!path.is_dir());
258        let path_ref = path.deref();
259        let target = fs::read_link(path_ref).unwrap();
260        assert_eq!(target, rootfs_path.join("endpoint").canonicalize().unwrap());
261        let content = fs::read_to_string(&path).unwrap();
262        assert_eq!(&content, "test");
263
264        // Remove the target file and validate that we could still read data from the pinned path.
265        fs::remove_file(&target).unwrap();
266        fs::read_to_string(&target).unwrap_err();
267        let content = fs::read_to_string(&path).unwrap();
268        assert_eq!(&content, "test");
269    }
270
271    #[test]
272    fn test_pinned_path_buf_race() {
273        let root_dir = tempfile::tempdir().expect("failed to create tmpdir");
274        let root_path = root_dir.path();
275        let barrier = Arc::new(Barrier::new(2));
276
277        fs::write(root_path.join("a"), b"a").unwrap();
278        fs::write(root_path.join("b"), b"b").unwrap();
279        fs::write(root_path.join("c"), b"c").unwrap();
280        symlink("a", root_path.join("s")).unwrap();
281
282        let root_path2 = root_path.to_path_buf();
283        let barrier2 = barrier.clone();
284        let thread = thread::spawn(move || {
285            // step 1
286            barrier2.wait();
287            fs::remove_file(root_path2.join("a")).unwrap();
288            symlink("b", root_path2.join("a")).unwrap();
289            barrier2.wait();
290
291            // step 2
292            barrier2.wait();
293            fs::remove_file(root_path2.join("b")).unwrap();
294            symlink("c", root_path2.join("b")).unwrap();
295            barrier2.wait();
296        });
297
298        let path = scoped_join(&root_path, "s").unwrap();
299        let data = fs::read_to_string(&path).unwrap();
300        assert_eq!(&data, "a");
301        assert!(path.is_file());
302        barrier.wait();
303        barrier.wait();
304        // Verify the target has been redirected.
305        let data = fs::read_to_string(&path).unwrap();
306        assert_eq!(&data, "b");
307        PinnedPathBuf::from_path(&path).unwrap_err();
308
309        let pinned_path = PinnedPathBuf::new(&root_path, "s").unwrap();
310        let data = fs::read_to_string(&pinned_path).unwrap();
311        assert_eq!(&data, "b");
312
313        // step2
314        barrier.wait();
315        barrier.wait();
316        // Verify it still points to the old target.
317        let data = fs::read_to_string(&pinned_path).unwrap();
318        assert_eq!(&data, "b");
319
320        thread.join().unwrap();
321    }
322
323    #[test]
324    fn test_new_pinned_path_buf() {
325        let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
326        let rootfs_path = rootfs_dir.path();
327        let path = PinnedPathBuf::from_path(rootfs_path).unwrap();
328        let _ = OpenOptions::new().read(true).open(&path).unwrap();
329    }
330
331    #[test]
332    fn test_pinned_path_try_clone() {
333        let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
334        let rootfs_path = rootfs_dir.path();
335        let path = PinnedPathBuf::from_path(rootfs_path).unwrap();
336        let path2 = path.try_clone().unwrap();
337        assert_ne!(path.as_path(), path2.as_path());
338    }
339
340    #[test]
341    fn test_new_pinned_path_buf_from_nonexist_file() {
342        let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
343        let rootfs_path = rootfs_dir.path();
344        PinnedPathBuf::new(rootfs_path, "does_not_exist").unwrap_err();
345    }
346
347    #[test]
348    fn test_new_pinned_path_buf_without_read_perm() {
349        let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
350        let rootfs_path = rootfs_dir.path();
351        let path = rootfs_path.join("write_only_file");
352
353        let mut file = OpenOptions::new()
354            .read(false)
355            .write(true)
356            .create(true)
357            .mode(0o200)
358            .open(&path)
359            .unwrap();
360        file.write_all(&[0xa5u8]).unwrap();
361        let md = fs::metadata(&path).unwrap();
362        let umask = unsafe { libc::umask(0022) };
363        unsafe { libc::umask(umask) };
364        assert_eq!(md.mode() & 0o700, 0o200 & !umask);
365        PinnedPathBuf::from_path(&path).unwrap();
366    }
367
368    #[test]
369    fn test_pinned_path_buf_path_fd() {
370        let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
371        let rootfs_path = rootfs_dir.path();
372        let path = rootfs_path.join("write_only_file");
373
374        let mut file = OpenOptions::new()
375            .read(false)
376            .write(true)
377            .create(true)
378            .mode(0o200)
379            .open(&path)
380            .unwrap();
381        file.write_all(&[0xa5u8]).unwrap();
382        let handle = PinnedPathBuf::from_path(&path).unwrap();
383        // Check that `fstat()` etc works with the fd returned by `path_fd()`.
384        let fd = handle.path_fd();
385        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
386        let res = unsafe { libc::fstat(fd, &mut stat as *mut _) };
387        assert_eq!(res, 0);
388    }
389
390    #[test]
391    fn test_pinned_path_buf_open_child() {
392        let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
393        let rootfs_path = rootfs_dir.path();
394        let path = PinnedPathBuf::from_path(rootfs_path).unwrap();
395
396        fs::write(path.join("child"), "test").unwrap();
397        let path = path.open_child(OsStr::new("child")).unwrap();
398        let content = fs::read_to_string(&path).unwrap();
399        assert_eq!(&content, "test");
400
401        path.open_child(&OsString::from("__does_not_exist__"))
402            .unwrap_err();
403        path.open_child(&OsString::from("test/a")).unwrap_err();
404    }
405
406    #[test]
407    fn test_prepare_path_component() {
408        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("")).is_err());
409        assert!(PinnedPathBuf::prepare_path_component(&OsString::from(".")).is_err());
410        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("..")).is_err());
411        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("/")).is_err());
412        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("//")).is_err());
413        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/b")).is_err());
414        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("./b")).is_err());
415        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/.")).is_err());
416        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/..")).is_err());
417        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/./")).is_err());
418        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/../")).is_err());
419        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/./a")).is_err());
420        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a/../a")).is_err());
421
422        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a")).is_ok());
423        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a.b")).is_ok());
424        assert!(PinnedPathBuf::prepare_path_component(&OsString::from("a..b")).is_ok());
425    }
426
427    #[test]
428    fn test_target_fs_object_changed() {
429        let rootfs_dir = tempfile::tempdir().expect("failed to create tmpdir");
430        let rootfs_path = rootfs_dir.path();
431        let file = rootfs_path.join("child");
432        fs::write(&file, "test").unwrap();
433
434        let path = PinnedPathBuf::from_path(&file).unwrap();
435        let path3 = fs::read_link(path.as_path()).unwrap();
436        assert_eq!(&path3, path.target());
437        fs::rename(file, rootfs_path.join("child2")).unwrap();
438        let path4 = fs::read_link(path.as_path()).unwrap();
439        assert_ne!(&path4, path.target());
440        fs::remove_file(rootfs_path.join("child2")).unwrap();
441        let path5 = fs::read_link(path.as_path()).unwrap();
442        assert_ne!(&path4, &path5);
443    }
444}