safe_path/
scoped_dir_builder.rs

1// Copyright (c) 2022 Alibaba Cloud
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5
6use std::io::{Error, ErrorKind, Result};
7use std::path::Path;
8
9use crate::{scoped_join, scoped_resolve, PinnedPathBuf};
10
11const DIRECTORY_MODE_DEFAULT: u32 = 0o777;
12const DIRECTORY_MODE_MASK: u32 = 0o777;
13
14/// Safe version of `DirBuilder` to protect from TOCTOU style of attacks.
15///
16/// The `ScopedDirBuilder` is a counterpart for `DirBuilder`, with safety enhancements of:
17/// - ensuring the new directories are created under a specified `root` directory.
18/// - ensuring all created directories are still scoped under `root` even under symlink based
19///   attacks.
20/// - returning a [PinnedPathBuf] for the last level of directory, so it could be used for other
21///   operations safely.
22#[derive(Debug)]
23pub struct ScopedDirBuilder {
24    root: PinnedPathBuf,
25    mode: u32,
26    recursive: bool,
27}
28
29impl ScopedDirBuilder {
30    /// Create a new instance of `ScopedDirBuilder` with with default mode/security settings.
31    pub fn new<P: AsRef<Path>>(root: P) -> Result<Self> {
32        let root = root.as_ref().canonicalize()?;
33        let root = PinnedPathBuf::from_path(root)?;
34        if !root.metadata()?.is_dir() {
35            return Err(Error::new(
36                ErrorKind::Other,
37                format!("Invalid root path: {}", root.display()),
38            ));
39        }
40
41        Ok(ScopedDirBuilder {
42            root,
43            mode: DIRECTORY_MODE_DEFAULT,
44            recursive: false,
45        })
46    }
47
48    /// Indicates that directories should be created recursively, creating all parent directories.
49    ///
50    /// Parents that do not exist are created with the same security and permissions settings.
51    pub fn recursive(&mut self, recursive: bool) -> &mut Self {
52        self.recursive = recursive;
53        self
54    }
55
56    /// Sets the mode to create new directories with. This option defaults to 0o755.
57    pub fn mode(&mut self, mode: u32) -> &mut Self {
58        self.mode = mode & DIRECTORY_MODE_MASK;
59        self
60    }
61
62    /// Creates the specified directory with the options configured in this builder.
63    ///
64    /// This is a helper to create subdirectory with an absolute path, without stripping off
65    /// `self.root`. So error will be returned if path does start with `self.root`.
66    /// It is considered an error if the directory already exists unless recursive mode is enabled.
67    pub fn create_with_unscoped_path<P: AsRef<Path>>(&self, path: P) -> Result<PinnedPathBuf> {
68        if !path.as_ref().is_absolute() {
69            return Err(Error::new(
70                ErrorKind::Other,
71                format!(
72                    "Expected absolute directory path: {}",
73                    path.as_ref().display()
74                ),
75            ));
76        }
77        // Partially canonicalize `path` so we can strip the `root` part.
78        let scoped_path = scoped_join("/", path)?;
79        let stripped_path = scoped_path.strip_prefix(self.root.target()).map_err(|_| {
80            Error::new(
81                ErrorKind::Other,
82                format!(
83                    "Path {} is not under {}",
84                    scoped_path.display(),
85                    self.root.target().display()
86                ),
87            )
88        })?;
89
90        self.do_mkdir(&stripped_path)
91    }
92
93    /// Creates sub-directory with the options configured in this builder.
94    ///
95    /// It is considered an error if the directory already exists unless recursive mode is enabled.
96    pub fn create<P: AsRef<Path>>(&self, path: P) -> Result<PinnedPathBuf> {
97        let path = scoped_resolve(&self.root, path)?;
98        self.do_mkdir(&path)
99    }
100
101    fn do_mkdir(&self, path: &Path) -> Result<PinnedPathBuf> {
102        assert!(path.is_relative());
103        if path.file_name().is_none() {
104            if !self.recursive {
105                return Err(Error::new(
106                    ErrorKind::AlreadyExists,
107                    "directory already exists",
108                ));
109            } else {
110                return self.root.try_clone();
111            }
112        }
113
114        // Safe because `path` have at least one level.
115        let levels = path.iter().count() - 1;
116        let mut dir = self.root.try_clone()?;
117        for (idx, comp) in path.iter().enumerate() {
118            match dir.open_child(comp) {
119                Ok(v) => {
120                    if !v.metadata()?.is_dir() {
121                        return Err(Error::new(
122                            ErrorKind::Other,
123                            format!("Path {} is not a directory", v.display()),
124                        ));
125                    } else if !self.recursive && idx == levels {
126                        return Err(Error::new(
127                            ErrorKind::AlreadyExists,
128                            "directory already exists",
129                        ));
130                    }
131                    dir = v;
132                }
133                Err(_e) => {
134                    if !self.recursive && idx != levels {
135                        return Err(Error::new(
136                            ErrorKind::NotFound,
137                            format!("parent directory does not exist"),
138                        ));
139                    }
140                    dir = dir.mkdir(comp, self.mode)?;
141                }
142            }
143        }
144
145        Ok(dir)
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use std::fs;
153    use std::fs::DirBuilder;
154    use std::os::unix::fs::{symlink, MetadataExt};
155    use tempfile::tempdir;
156
157    #[test]
158    fn test_scoped_dir_builder() {
159        // create temporary directory to emulate container rootfs with symlink
160        let rootfs_dir = tempdir().expect("failed to create tmpdir");
161        DirBuilder::new()
162            .create(rootfs_dir.path().join("b"))
163            .unwrap();
164        symlink(rootfs_dir.path().join("b"), rootfs_dir.path().join("a")).unwrap();
165        let rootfs_path = &rootfs_dir.path().join("a");
166
167        // root directory doesn't exist
168        ScopedDirBuilder::new(rootfs_path.join("__does_not_exist__")).unwrap_err();
169        ScopedDirBuilder::new("__does_not_exist__").unwrap_err();
170
171        // root is a file
172        fs::write(rootfs_path.join("txt"), "test").unwrap();
173        ScopedDirBuilder::new(rootfs_path.join("txt")).unwrap_err();
174
175        let mut builder = ScopedDirBuilder::new(&rootfs_path).unwrap();
176
177        // file with the same name already exists.
178        builder
179            .create_with_unscoped_path(rootfs_path.join("txt"))
180            .unwrap_err();
181        // parent is a file
182        builder.create("/txt/a").unwrap_err();
183        // Not starting with root
184        builder.create_with_unscoped_path("/txt/a").unwrap_err();
185        // creating "." without recursive mode should fail
186        builder
187            .create_with_unscoped_path(rootfs_path.join("."))
188            .unwrap_err();
189        // parent doesn't exist
190        builder
191            .create_with_unscoped_path(rootfs_path.join("a/b"))
192            .unwrap_err();
193        builder.create("a/b/c").unwrap_err();
194
195        let path = builder.create("a").unwrap();
196        assert!(rootfs_path.join("a").is_dir());
197        assert_eq!(path.target(), rootfs_path.join("a").canonicalize().unwrap());
198
199        // Creating an existing directory without recursive mode should fail.
200        builder
201            .create_with_unscoped_path(rootfs_path.join("a"))
202            .unwrap_err();
203
204        // Creating an existing directory with recursive mode should succeed.
205        builder.recursive(true);
206        let path = builder
207            .create_with_unscoped_path(rootfs_path.join("a"))
208            .unwrap();
209        assert_eq!(path.target(), rootfs_path.join("a").canonicalize().unwrap());
210        let path = builder.create(".").unwrap();
211        assert_eq!(path.target(), rootfs_path.canonicalize().unwrap());
212
213        let umask = unsafe { libc::umask(0022) };
214        unsafe { libc::umask(umask) };
215
216        builder.mode(0o740);
217        let path = builder.create("a/b/c/d").unwrap();
218        assert_eq!(
219            path.target(),
220            rootfs_path.join("a/b/c/d").canonicalize().unwrap()
221        );
222        assert!(rootfs_path.join("a/b/c/d").is_dir());
223        assert_eq!(
224            rootfs_path.join("a").metadata().unwrap().mode() & 0o777,
225            DIRECTORY_MODE_DEFAULT & !umask,
226        );
227        assert_eq!(
228            rootfs_path.join("a/b").metadata().unwrap().mode() & 0o777,
229            0o740 & !umask
230        );
231        assert_eq!(
232            rootfs_path.join("a/b/c").metadata().unwrap().mode() & 0o777,
233            0o740 & !umask
234        );
235        assert_eq!(
236            rootfs_path.join("a/b/c/d").metadata().unwrap().mode() & 0o777,
237            0o740 & !umask
238        );
239
240        // Creating should fail if some components are not directory.
241        builder.create("txt/e/f").unwrap_err();
242        fs::write(rootfs_path.join("a/b/txt"), "test").unwrap();
243        builder.create("a/b/txt/h/i").unwrap_err();
244    }
245
246    #[test]
247    fn test_create_root() {
248        let mut builder = ScopedDirBuilder::new("/").unwrap();
249        builder.recursive(true);
250        builder.create("/").unwrap();
251        builder.create(".").unwrap();
252        builder.create("..").unwrap();
253        builder.create("../../.").unwrap();
254        builder.create("").unwrap();
255        builder.create_with_unscoped_path("/").unwrap();
256        builder.create_with_unscoped_path("/..").unwrap();
257        builder.create_with_unscoped_path("/../.").unwrap();
258    }
259
260    #[test]
261    fn test_create_with_absolute_path() {
262        // create temporary directory to emulate container rootfs with symlink
263        let rootfs_dir = tempdir().expect("failed to create tmpdir");
264        DirBuilder::new()
265            .create(rootfs_dir.path().join("b"))
266            .unwrap();
267        symlink(rootfs_dir.path().join("b"), rootfs_dir.path().join("a")).unwrap();
268        let rootfs_path = &rootfs_dir.path().join("a");
269
270        let mut builder = ScopedDirBuilder::new(&rootfs_path).unwrap();
271        builder.create_with_unscoped_path("/").unwrap_err();
272        builder
273            .create_with_unscoped_path(rootfs_path.join("../__xxxx___xxx__"))
274            .unwrap_err();
275        builder
276            .create_with_unscoped_path(rootfs_path.join("c/d"))
277            .unwrap_err();
278
279        // Return `AlreadyExist` when recursive is false
280        builder.create_with_unscoped_path(&rootfs_path).unwrap_err();
281        builder
282            .create_with_unscoped_path(rootfs_path.join("."))
283            .unwrap_err();
284
285        builder.recursive(true);
286        builder.create_with_unscoped_path(&rootfs_path).unwrap();
287        builder
288            .create_with_unscoped_path(rootfs_path.join("."))
289            .unwrap();
290        builder
291            .create_with_unscoped_path(rootfs_path.join("c/d"))
292            .unwrap();
293    }
294}