unix_fd/
chroot.rs

1//! Userspace `chroot` implementation
2extern crate libc;
3extern crate error_chain;
4
5use std::fmt;
6use std::path::{Path, PathBuf};
7use std::ffi::OsString;
8
9use crate::fd::*;
10use crate::dir::*;
11
12use crate::errors::*;
13
14const MAX_LOOP_CNT: u32 = 256;
15
16struct ChdirLoopEnv {
17    counter: u32,
18    root_stat: Option<libc::stat>,
19}
20
21impl ChdirLoopEnv {
22    fn new() -> ChdirLoopEnv {
23        ChdirLoopEnv {
24            counter: MAX_LOOP_CNT,
25            root_stat: None,
26        }
27    }
28}
29
30impl fmt::Debug for ChdirLoopEnv {
31    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
32        write!(f, "counter={:?}, root_stat={:?}",
33               self.counter, self.root_stat.map(|_| "..."))
34    }
35}
36
37struct DirInfo {
38    is_root: bool,
39    stat: libc::stat,
40}
41
42/// Userspace `chroot` environment
43///
44/// All symlinks below a root directory are resolved relative this
45/// directory.  E.g. when having a directory tree like
46///
47/// ```text
48/// /
49/// |-- etc/
50/// |   `-- passwd
51/// `-- srv/
52///     `-- www/
53///         |-- etc/
54///         |   `-- passwd
55///         |-- tmp -> /etc/
56///         |-- passwd -> /etc/passwd
57///         `-- test -> ../../../etc/passwd
58/// ```
59///
60/// All the `open()` statements in code like
61///
62/// ```
63/// # extern crate libc;
64/// # extern crate unix_fd;
65/// #
66/// # use std::ffi::OsString;
67/// # use std::path::Path;
68/// #
69/// # type Chroot = unix_fd::chroot::Chroot;
70/// #
71/// let chroot = Chroot::new(&OsString::from("/srv/www"));
72///
73/// let fd = chroot.open(&Path::new("/etc/passwd"), libc::O_RDONLY);
74/// let fd = chroot.open(&Path::new("/tmp/passwd"), libc::O_RDONLY);
75/// let fd = chroot.open(&Path::new("/test"), libc::O_RDONLY);
76/// let fd = chroot.open(&Path::new("/passwd"), libc::O_RDONLY);
77/// ```
78///
79/// will access `/srv/www/etc/passwd` instead of `/etc/passwd`.
80#[derive(Debug)]
81pub struct Chroot {
82    root: PathBuf
83}
84
85impl Chroot {
86    pub fn new<T: AsRef<Path>>(root: &T) -> Self {
87        Chroot {
88            root: root.as_ref().to_path_buf(),
89        }
90    }
91
92    /// Opens the top level directory of the chroot directory and
93    /// returns the filedescriptor.
94    ///
95    /// The directory will be opened with `O_CLOEXEC` flag being set.
96    pub fn root_fdraw(&self) -> Result<FdRaw> {
97        let open_flags = libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_RDONLY;
98
99        FdRaw::open(&self.root, open_flags)
100    }
101
102    pub fn root_fd(&self) -> Result<Fd> {
103        let open_flags = libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_RDONLY;
104
105        Fd::open(&self.root, open_flags)
106    }
107
108    fn dir_info(&self, dir_fd: &Fd, env: &mut ChdirLoopEnv) -> Result<DirInfo> {
109        if env.root_stat.is_none() {
110            env.root_stat = Some(Fd::cwd().fstatat(&self.root, true)?);
111        }
112
113        let root_stat = env.root_stat.as_ref().unwrap();
114
115        let stat = dir_fd.fstatat(&".", false)?;
116        let is_root =
117            (stat.st_dev == root_stat.st_dev) &&
118            (stat.st_ino == root_stat.st_ino);
119
120        Ok(DirInfo {
121            stat: stat,
122            is_root: is_root,
123        })
124    }
125
126    /// Opens the directory at `path` within the chroot.
127    ///
128    /// Every intermediate symlinks will be resolved relative to to
129    /// the chroot.
130    ///
131    /// Restrictions: `path` must be absolute.
132    pub fn chdir<T>(&self, path: &T) -> Result<Fd>
133    where
134        T: AsRef<Path>,
135    {
136        let path : &Path = path.as_ref();
137
138        ensure!(path.is_absolute(), "path '{:?}' not absolute", path);
139
140        let mut env: ChdirLoopEnv = ChdirLoopEnv::new();
141
142        self.chdir_internal(Fd::cwd(), path, &mut env)
143    }
144
145    /// Opens a directory `path` in the chroot environment relative
146    /// to `fd`.
147    ///
148    /// Behaviour is unspecified if `fd` lies outside the chroot.
149    /// `path` can be relative.
150    pub fn chdirat<T>(&self, dir_fd: &Fd, path:  &T) -> Result<Fd>
151    where
152        T: AsRef<Path>,
153    {
154        let mut env: ChdirLoopEnv = ChdirLoopEnv::new();
155
156        self.chdir_internal(dir_fd.clone(), path.as_ref(), &mut env)
157    }
158
159    fn open_component(&self, dir_fd: Fd,
160                      path: std::path::Component,
161                      env: &mut ChdirLoopEnv) -> Result<Fd>
162    {
163	#[allow(clippy::identity_op)]
164        let open_flags = 0
165            | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_RDONLY
166            | libc::O_NOFOLLOW;
167
168        match path {
169            std::path::Component::Prefix(_) => {
170                unreachable!();
171            },
172
173            std::path::Component::ParentDir => {
174                let info = self.dir_info(&dir_fd, env)?;
175
176                if info.is_root {
177                    Ok(dir_fd)
178                } else {
179                    dir_fd.openat(&"..", open_flags)
180                }
181            },
182
183            std::path::Component::RootDir => {
184                self.root_fd()
185            },
186
187            std::path::Component::CurDir => {
188                Ok(dir_fd)
189            },
190
191            std::path::Component::Normal(p) => {
192                dir_fd.openat(&p, open_flags)
193            },
194        }
195    }
196
197    fn chdir_internal(&self, dir_fd: Fd, path: &Path,
198                      env: &mut ChdirLoopEnv) -> Result<Fd>
199    {
200        let mut dir_fd = dir_fd;
201
202        for p in path.components() {
203            use std::path::Component;
204
205            dir_fd = match p {
206                Component::Prefix(_) |
207                Component::RootDir |
208                Component::CurDir |
209                Component::ParentDir =>
210                    self.open_component(dir_fd, p, env)?,
211
212                Component::Normal(path_name) => {
213                    let tmp = Path::new(path_name);
214
215                    if !dir_fd.is_lnkat(&tmp) {
216                        self.open_component(dir_fd, p, env)?
217                    } else if env.counter == 0 {
218                        bail!("too much loops while resolving symbolic link '{:?}'",
219                              path);
220                    } else {
221                        let new_path = dir_fd.readlinkat(&tmp)?;
222                        let link = Path::new(&new_path);
223
224                        env.counter -= 1;
225                        let res = self.chdir_internal(dir_fd, link, env);
226                        env.counter += 1;
227
228                        res?
229                    }
230                }
231            };
232        }
233
234        Ok(dir_fd)
235    }
236
237    fn opendir_internal(&self, dir_fd: &Fd, path: &Path, env: &mut ChdirLoopEnv)
238                 -> Result<(Fd, OsString)>
239    {
240        let current_dir = OsString::from(".");
241        let fdrc = dir_fd.clone();
242
243        match path.parent() {
244            None =>
245                Ok((self.chdir_internal(fdrc, path, env)?, current_dir)),
246
247            Some(p) => {
248                Ok((
249                    self.chdir_internal(fdrc, p, env)?,
250                    path.file_name()
251                        .unwrap_or_else(|| current_dir.as_os_str())
252                        .to_os_string()))
253            }
254        }
255    }
256
257    /// Opens a file in the chroot relative to an open directory `fd`.
258    ///
259    /// Method first opens the directory containing `path` as described
260    /// by `Self::chdirat()` and calls `openat()` with `O_NOFOLLOW
261    /// being set there.
262    pub fn openat<T>(&self, dir_fd: &Fd, path: &T, flags: libc::c_int)
263                     -> Result<Fd>
264    where
265        T: AsRef<Path>,
266    {
267
268        let mut env = ChdirLoopEnv::new();
269        let mut path = path.as_ref().to_owned();
270        let mut num_loops = MAX_LOOP_CNT;
271
272        while num_loops > 0 {
273            let (dir_fd, comp) =
274                self.opendir_internal(dir_fd, &path, &mut env)?;
275
276            assert_eq!(env.counter, MAX_LOOP_CNT);
277
278            if !dir_fd.is_lnkat(&comp) {
279                return dir_fd.openat(&comp, flags | libc::O_NOFOLLOW);
280            }
281
282            path = Path::new(&dir_fd.readlinkat(&comp)?).to_owned();
283
284            num_loops -= 1;
285        }
286
287        bail!("too much loops while resolving symbolic link '{:?}'",
288              path);
289    }
290
291    /// Opens a file in the chroot environment.
292    ///
293    /// Method first opens the directory containing `path` as described
294    /// by `Self::chdir()` and calls `openat()` with `O_NOFOLLOW being
295    /// set there.
296    pub fn open<T>(&self, path: &T, flags: libc::c_int)
297                     -> Result<Fd>
298    where
299        T: AsRef<Path>,
300    {
301        self.openat(&self.root_fd()?, path, flags)
302    }
303
304    /// Checks whether path is a symlink
305    ///
306    /// Method returns when errors occurred while performing the
307    /// lookup.
308    pub fn is_lnkat<T>(&self, dir_fd: &Fd, path: &T) -> bool
309    where
310        T: AsRef<Path>,
311    {
312        let mut env = ChdirLoopEnv::new();
313
314        self.opendir_internal(dir_fd, path.as_ref(), &mut env)
315            .map(|(dir_fd, comp)| dir_fd.is_lnkat(&comp))
316            .unwrap_or(false)
317    }
318
319    /// Checks whether path is a directory
320    ///
321    /// Method returns when errors occurred while performing the
322    /// lookup.
323    pub fn is_dirat<T>(&self, dir_fd: &Fd, path: &T) -> bool
324    where
325        T: AsRef<Path>,
326    {
327        let mut env = ChdirLoopEnv::new();
328
329        self.opendir_internal(dir_fd, path.as_ref(), &mut env)
330            .map(|(dir_fd, comp)| dir_fd.is_dirat(&comp))
331            .unwrap_or(false)
332    }
333
334    /// Checks whether path is a regular file
335    ///
336    /// Method returns when errors occurred while performing the
337    /// lookup.
338    pub fn is_regat<T>(&self, dir_fd: &Fd, path: &T) -> bool
339    where
340        T: AsRef<Path>,
341    {
342        let mut env = ChdirLoopEnv::new();
343
344        self.opendir_internal(dir_fd, path.as_ref(), &mut env)
345            .map(|(dir_fd, comp)| dir_fd.is_regat(&comp))
346            .unwrap_or(false)
347    }
348
349    /// Returns fstat information
350    pub fn fstatat<T>(&self, dir_fd: &Fd, fname: &T) -> Result<libc::stat>
351    where
352        T: AsRef<Path>,
353    {
354        let do_follow = false;
355
356        let mut env = ChdirLoopEnv::new();
357
358        self.opendir_internal(dir_fd, fname.as_ref(), &mut env)
359            .map(|(dir_fd, comp)| dir_fd.fstatat(&comp, do_follow))?
360    }
361
362    fn check_and_get_entry(dir_fd: &Fd, entry: &DirEntry,
363                           info: &DirInfo) -> Result<Option<OsString>> {
364        //const DT_UNKNOWN: u8 = libc::DT_UNKNOWN;
365        const DT_UNKNOWN: u8 = 0;
366        const DT_DIR: u8 = libc::DT_DIR;
367
368        if entry.d_ino != info.stat.st_ino {
369            return Ok(None);
370        }
371
372        if entry.d_type != DT_DIR && entry.d_type != DT_UNKNOWN {
373            return Ok(None);
374        }
375
376        let name = OsString::from(entry.name());
377        let stat = dir_fd.fstatat(&name, false)?;
378
379        if  ((stat.st_mode & libc::S_IFMT) != libc::S_IFDIR) ||
380            stat.st_ino != info.stat.st_ino ||
381            stat.st_dev != info.stat.st_dev {
382            return Ok(None);
383        }
384
385        Ok(Some(name))
386    }
387
388    /// Transforms `fd` into an absolute path relative to the chroot
389    /// and appends `fname` optionally.
390    ///
391    /// Note: this operation is expensive because it recurses into the
392    /// parent directories of `fd` and iterates over their contents to
393    /// look for a matching subdirectory.
394    pub fn full_path<T>(&self, dir_fd: &Fd, fname: Option<&T>)
395                        -> Result<OsString>
396    where
397        T: AsRef<Path>,
398    {
399        let parent_dir = Path::new("..");
400        let mut res = Vec::new();
401        let mut dir_fd = dir_fd.clone();
402        let mut env: ChdirLoopEnv = ChdirLoopEnv::new();
403        let mut total_size = 0;
404
405        loop {
406            let info = self.dir_info(&dir_fd, &mut env)?;
407
408            assert_eq!(env.counter, MAX_LOOP_CNT);
409
410            if info.is_root {
411                break;
412            }
413
414            dir_fd = dir_fd.openat(&parent_dir,
415                                   libc::O_CLOEXEC | libc::O_RDONLY |
416                                   libc::O_DIRECTORY)?;
417
418            let dir = Dir::fdopendir(&dir_fd)?;
419
420            for e in ReadDir::new(dir) {
421                let e_name = Self::check_and_get_entry(&dir_fd, &e?, &info)?;
422
423                if let Some(name) = e_name {
424                    total_size += name.len() + 1;
425                    res.push(name);
426
427                    break;
428                }
429            }
430
431            if res.is_empty() {
432                bail!("full_path(): no entry found");
433            }
434        }
435
436        res.reverse();
437        let mut path = OsString::with_capacity(total_size);
438
439        if res.is_empty() && fname.is_none() {
440            path.push("/");
441        }
442
443        for p in res {
444            path.push("/");
445            path.push(p);
446        }
447
448        match fname {
449            None => {}
450            Some(f) => {
451                path.push("/");
452                path.push(f.as_ref().as_os_str());
453            }
454        }
455
456        Ok(path)
457    }
458}
459
460#[cfg(test)]
461#[path="tests/chroot-data.inc.rs"]
462mod testdata;
463
464#[cfg(test)]
465#[path="tests/chroot.inc.rs"]
466mod test;