cap_primitives/fs/manually/
open.rs

1//! Manual path resolution, one component at a time, with manual symlink
2//! resolution, in order to enforce sandboxing.
3
4use super::{read_link_one, CanonicalPath, CowComponent};
5use crate::fs::{
6    dir_options, errors, open_unchecked, path_has_trailing_dot, path_has_trailing_slash,
7    stat_unchecked, FollowSymlinks, MaybeOwnedFile, Metadata, OpenOptions, OpenUncheckedError,
8};
9#[cfg(any(target_os = "android", target_os = "linux", target_os = "freebsd"))]
10use rustix::fs::OFlags;
11use std::borrow::Cow;
12use std::ffi::OsStr;
13use std::path::{Component, Path, PathBuf};
14use std::{fs, io, mem};
15#[cfg(windows)]
16use {
17    crate::fs::{open_dir_unchecked, path_really_has_trailing_dot, SymlinkKind},
18    windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY,
19};
20
21/// Implement `open` by breaking up the path into components, resolving each
22/// component individually, and resolving symbolic links manually.
23pub(crate) fn open(start: &fs::File, path: &Path, options: &OpenOptions) -> io::Result<fs::File> {
24    let mut symlink_count = 0;
25    let start = MaybeOwnedFile::borrowed(start);
26    let maybe_owned = internal_open(start, path, options, &mut symlink_count, None)?;
27    maybe_owned.into_file(options)
28}
29
30/// Context for performing manual component-at-a-time path resolution.
31struct Context<'start> {
32    /// The current base directory handle for path lookups.
33    base: MaybeOwnedFile<'start>,
34
35    /// The stack of directory handles below the base.
36    dirs: Vec<MaybeOwnedFile<'start>>,
37
38    /// The current worklist stack of path components to process.
39    components: Vec<CowComponent<'start>>,
40
41    /// If requested, the canonical path is constructed here.
42    canonical_path: CanonicalPath<'start>,
43
44    /// Does the path end in `/` or similar, so it requires a directory?
45    dir_required: bool,
46
47    /// Are we requesting write permissions, so we can't open a directory?
48    dir_precluded: bool,
49
50    /// Where there a trailing slash on the path?
51    trailing_slash: bool,
52
53    /// If a path ends in `.`, `..`, or `/`, including after expanding
54    /// symlinks, we need to follow path resolution by opening `.` so that we
55    /// obtain a full `dir_options` file descriptor and confirm that we have
56    /// search rights in the last component.
57    follow_with_dot: bool,
58
59    /// A `PathBuf` that we reuse for calling `read_link_one` to minimize
60    /// allocations.
61    reuse: PathBuf,
62
63    #[cfg(racy_asserts)]
64    start_clone: MaybeOwnedFile<'start>,
65}
66
67impl<'start> Context<'start> {
68    /// Construct a new instance of `Self`.
69    fn new(
70        start: MaybeOwnedFile<'start>,
71        path: &'start Path,
72        _options: &OpenOptions,
73        canonical_path: Option<&'start mut PathBuf>,
74    ) -> Self {
75        let trailing_slash = path_has_trailing_slash(path);
76        let trailing_dot = path_has_trailing_dot(path);
77        let trailing_dotdot = path.ends_with(Component::ParentDir);
78
79        let mut components: Vec<CowComponent> = Vec::new();
80
81        #[cfg(windows)]
82        {
83            // Windows resolves `..` before doing filesystem lookups.
84            for component in path.components().map(CowComponent::borrowed) {
85                match component {
86                    CowComponent::ParentDir
87                        if !components.is_empty() && components.last().unwrap().is_normal() =>
88                    {
89                        let _ = components.pop();
90                    }
91                    _ => components.push(component),
92                }
93            }
94            components.reverse();
95        }
96
97        #[cfg(not(windows))]
98        {
99            // Add the path components to the worklist. Rust's `Path`
100            // normalizes away `.` components, however a trailing `.` affects
101            // path lookup, so special-case it here.
102            if trailing_dot {
103                components.push(CowComponent::CurDir);
104            }
105            components.extend(path.components().rev().map(CowComponent::borrowed));
106        }
107
108        #[cfg(racy_asserts)]
109        let start_clone = MaybeOwnedFile::owned(start.try_clone().unwrap());
110
111        Self {
112            base: start,
113            dirs: Vec::with_capacity(components.len()),
114            components,
115            canonical_path: CanonicalPath::new(canonical_path),
116            dir_required: trailing_slash,
117
118            #[cfg(not(windows))]
119            dir_precluded: _options.write || _options.append,
120
121            #[cfg(windows)]
122            dir_precluded: false,
123
124            trailing_slash,
125
126            follow_with_dot: trailing_dot | trailing_dotdot,
127
128            reuse: PathBuf::new(),
129
130            #[cfg(racy_asserts)]
131            start_clone,
132        }
133    }
134
135    fn check_dot_access(&self) -> io::Result<()> {
136        // Manually check that we have permissions to search `self.base` to
137        // search for `.` in it, since we otherwise resolve `.` and `..`
138        // ourselves by just manipulating the `dirs` stack.
139        #[cfg(not(windows))]
140        {
141            // Use `faccess` with `AT_EACCESS`. `AT_EACCESS` is not often the
142            // right tool for the job; in POSIX, it's better to ask for errno
143            // than to ask for permission. But we use `check_dot_access` to
144            // check access for opening `.` and `..` in situations where we
145            // already have open handles to them, and now we're accessing them
146            // through different paths, and we need to check whether these
147            // paths allow us access.
148            //
149            // Android and Emscripten lack `AT_EACCESS`.
150            // <https://android.googlesource.com/platform/bionic/+/master/libc/bionic/faccessat.cpp>
151            #[cfg(any(target_os = "emscripten", target_os = "android"))]
152            let at_flags = rustix::fs::AtFlags::empty();
153            #[cfg(not(any(target_os = "emscripten", target_os = "android")))]
154            let at_flags = rustix::fs::AtFlags::EACCESS;
155
156            // Always use `CurDir`, even though this code is used to check
157            // permissions for both `.` and `..`, because in both cases we
158            // already know we can access the referenced directory, and we
159            // just need to check for the ability to search for `.` or `..`
160            // within `self.base`, which should always be the same. And
161            // using `.` means we avoid asking the OS to access a `..` path
162            // for us.
163            Ok(rustix::fs::accessat(
164                &*self.base,
165                Component::CurDir.as_os_str(),
166                rustix::fs::Access::EXEC_OK,
167                at_flags,
168            )?)
169        }
170        #[cfg(windows)]
171        open_dir_unchecked(&self.base, Component::CurDir.as_ref()).map(|_| ())
172    }
173
174    /// Handle a "." path component.
175    fn cur_dir(&mut self) -> io::Result<()> {
176        // This is a no-op. If this occurs at the end of the path, it does
177        // imply that we need search access to the directory, and it requires
178        // we open a directory, however we'll handle that in the
179        // `follow_with_dot` check.
180        Ok(())
181    }
182
183    /// Handle a ".." path component.
184    fn parent_dir(&mut self) -> io::Result<()> {
185        #[cfg(racy_asserts)]
186        if !self.dirs.is_empty() {
187            assert_different_file!(&self.start_clone, &self.base);
188        }
189
190        // We hold onto all the parent directory descriptors so that we
191        // don't have to re-open anything when we encounter a `..`. This
192        // way, even if the directory is concurrently moved, we don't have
193        // to worry about `..` leaving the sandbox.
194        match self.dirs.pop() {
195            Some(dir) => {
196                // Check that we have permission to look up `..`.
197                self.check_dot_access()?;
198
199                // Looks good.
200                self.base = dir;
201            }
202            None => return Err(errors::escape_attempt()),
203        }
204        assert!(self.canonical_path.pop());
205
206        Ok(())
207    }
208
209    /// Handle a "normal" path component.
210    fn normal(
211        &mut self,
212        one: &OsStr,
213        options: &OpenOptions,
214        symlink_count: &mut u8,
215    ) -> io::Result<()> {
216        // If there are more named components left, this will be a base
217        // directory from which to open subsequent components, so use "path"
218        // options (`O_PATH` on Linux).
219        let use_options = if self.components.is_empty() {
220            options.clone()
221        } else {
222            dir_options()
223        };
224
225        // If the last path component ended in a slash, re-add the slash,
226        // as Rust's `Path` will have removed it, and we need it to get the
227        // same behavior from the OS.
228        let use_path: Cow<OsStr> = if self.components.is_empty() && self.trailing_slash {
229            let mut p = one.to_os_string();
230            p.push("/");
231            Cow::Owned(p)
232        } else {
233            Cow::Borrowed(one)
234        };
235
236        let dir_required = self.dir_required || use_options.dir_required;
237
238        #[allow(clippy::redundant_clone)]
239        match open_unchecked(
240            &self.base,
241            use_path.as_ref(),
242            use_options
243                .clone()
244                .follow(FollowSymlinks::No)
245                .dir_required(dir_required),
246        ) {
247            Ok(file) => {
248                // Emulate `O_PATH` + `FollowSymlinks::Yes` on Linux. If `file`
249                // is a symlink, follow it.
250                #[cfg(any(target_os = "android", target_os = "linux", target_os = "freebsd"))]
251                if should_emulate_o_path(&use_options) {
252                    match read_link_one(
253                        &file,
254                        Default::default(),
255                        symlink_count,
256                        mem::take(&mut self.reuse),
257                    ) {
258                        Ok(destination) => {
259                            return self.push_symlink_destination(destination);
260                        }
261                        // If it isn't a symlink, handle it as normal.
262                        // `readlinkat` returns `ENOENT` if the file isn't a
263                        // symlink in this situation.
264                        Err(err) if err.kind() == io::ErrorKind::NotFound => (),
265                        // If `readlinkat` fails any other way, pass it on.
266                        Err(err) => return Err(err),
267                    }
268                }
269
270                // Normal case
271                let prev_base = self.base.descend_to(MaybeOwnedFile::owned(file));
272                self.dirs.push(prev_base);
273                self.canonical_path.push(one);
274
275                Ok(())
276            }
277            #[cfg(not(windows))]
278            Err(OpenUncheckedError::Symlink(err, ())) => {
279                self.maybe_last_component_symlink(one, symlink_count, options.follow, err)
280            }
281            #[cfg(windows)]
282            Err(OpenUncheckedError::Symlink(err, SymlinkKind::Dir)) => {
283                // If this is a Windows directory symlink, require a directory.
284                self.dir_required |= self.components.is_empty();
285                self.maybe_last_component_symlink(one, symlink_count, options.follow, err)
286            }
287            #[cfg(windows)]
288            Err(OpenUncheckedError::Symlink(err, SymlinkKind::File)) => {
289                // If this is a Windows file symlink, preclude a directory.
290                self.dir_precluded = true;
291                self.maybe_last_component_symlink(one, symlink_count, options.follow, err)
292            }
293            Err(OpenUncheckedError::NotFound(err)) => Err(err),
294            Err(OpenUncheckedError::Other(err)) => {
295                // An error occurred. If this was the last component, and the
296                // error wasn't due to invalid inputs (eg. the path has an
297                // embedded NUL), record it as the last component of the
298                // canonical path, even if we couldn't open it.
299                if self.components.is_empty() && err.kind() != io::ErrorKind::InvalidInput {
300                    self.canonical_path.push(one);
301                    self.canonical_path.complete();
302                }
303                Err(err)
304            }
305        }
306    }
307
308    /// Dereference one symlink level.
309    fn symlink(&mut self, one: &OsStr, symlink_count: &mut u8) -> io::Result<()> {
310        let destination =
311            read_link_one(&self.base, one, symlink_count, mem::take(&mut self.reuse))?;
312        self.push_symlink_destination(destination)
313    }
314
315    /// Push the components of `destination` onto the worklist stack.
316    fn push_symlink_destination(&mut self, destination: PathBuf) -> io::Result<()> {
317        let at_end = self.components.is_empty();
318        let trailing_slash = path_has_trailing_slash(&destination);
319        let trailing_dot = path_has_trailing_dot(&destination);
320        let trailing_dotdot = destination.ends_with(Component::ParentDir);
321
322        #[cfg(windows)]
323        {
324            // `path_has_trailing_dot` returns false so that we don't open `.`
325            // at the end of path resolution. But for determining the Windows
326            // symlink restrictions, we need to know whether the path really
327            // ends in a `.`.
328            let trailing_dot_really = path_really_has_trailing_dot(&destination);
329
330            // Windows appears to disallow symlinks to paths with trailing
331            // slashes, slashdots, or slashdotdots.
332            if trailing_slash
333                || (trailing_dot_really && destination.as_os_str() != Component::CurDir.as_os_str())
334                || (trailing_dotdot && destination.as_os_str() != Component::ParentDir.as_os_str())
335            {
336                return Err(io::Error::from_raw_os_error(123));
337            }
338
339            // Windows resolves `..` before doing filesystem lookups.
340            let mut components: Vec<CowComponent> = Vec::new();
341            for component in destination.components().map(CowComponent::owned) {
342                match component {
343                    CowComponent::ParentDir
344                        if !components.is_empty() && components.last().unwrap().is_normal() =>
345                    {
346                        let _ = components.pop();
347                    }
348                    _ => components.push(component),
349                }
350            }
351            self.components.extend(components.into_iter().rev());
352        }
353
354        #[cfg(not(windows))]
355        {
356            // Rust's `Path` hides a trailing dot, so handle it manually.
357            if trailing_dot {
358                self.components.push(CowComponent::CurDir);
359            }
360            self.components
361                .extend(destination.components().rev().map(CowComponent::owned));
362        }
363
364        // Record whether the new components ended with a path that implies
365        // an open of `.` at the end of path resolution.
366        if at_end {
367            self.follow_with_dot |= trailing_dot | trailing_dotdot;
368            self.trailing_slash |= trailing_slash;
369            self.dir_required |= trailing_slash;
370        }
371
372        // As an optimization, hold onto the `PathBuf` buffer for later reuse.
373        self.reuse = destination;
374
375        Ok(())
376    }
377
378    /// Check whether this is the last component and we don't need
379    /// to dereference; otherwise call `Self::symlink`.
380    fn maybe_last_component_symlink(
381        &mut self,
382        one: &OsStr,
383        symlink_count: &mut u8,
384        follow: FollowSymlinks,
385        err: io::Error,
386    ) -> io::Result<()> {
387        if follow == FollowSymlinks::No && !self.trailing_slash && self.components.is_empty() {
388            self.canonical_path.push(one);
389            self.canonical_path.complete();
390            return Err(err);
391        }
392
393        self.symlink(one, symlink_count)
394    }
395}
396
397/// Internal implementation of manual `open`, exposing some additional
398/// parameters.
399///
400/// Callers can request the canonical path by passing `Some` to
401/// `canonical_path`. If the complete canonical path is processed, it will be
402/// stored in the provided `&mut PathBuf`, even if the actual open fails. If
403/// a failure occurs before the complete canonical path is processed, the
404/// provided `&mut PathBuf` is cleared to empty.
405///
406/// A note on lifetimes: `path` and `canonical_path` here don't strictly
407/// need `'start`, but using them makes it easier to store them in the
408/// `Context` struct.
409pub(super) fn internal_open<'start>(
410    start: MaybeOwnedFile<'start>,
411    path: &'start Path,
412    options: &OpenOptions,
413    symlink_count: &mut u8,
414    canonical_path: Option<&'start mut PathBuf>,
415) -> io::Result<MaybeOwnedFile<'start>> {
416    // POSIX returns `ENOENT` on an empty path. TODO: On Windows, we should
417    // be compatible with what Windows does instead.
418    if path.as_os_str().is_empty() {
419        return Err(errors::no_such_file_or_directory());
420    }
421
422    let mut ctx = Context::new(start, path, options, canonical_path);
423
424    while let Some(c) = ctx.components.pop() {
425        match c {
426            CowComponent::PrefixOrRootDir => return Err(errors::escape_attempt()),
427            CowComponent::CurDir => ctx.cur_dir()?,
428            CowComponent::ParentDir => ctx.parent_dir()?,
429            CowComponent::Normal(one) => ctx.normal(&one, options, symlink_count)?,
430        }
431    }
432
433    // We've now finished all the path components other than any trailing `.`s,
434    // so we have the complete canonical path.
435    ctx.canonical_path.complete();
436
437    // If the path ended in `.` (explicit or implied) or `..`, we may have
438    // opened the directory with eg. `O_PATH` on Linux, or we may have skipped
439    // checking for search access to `.`, so re-open it.
440    if ctx.follow_with_dot {
441        if ctx.dir_precluded {
442            return Err(errors::is_directory());
443        }
444        ctx.base = MaybeOwnedFile::owned(open_unchecked(
445            &ctx.base,
446            Component::CurDir.as_ref(),
447            options,
448        )?);
449    }
450
451    #[cfg(racy_asserts)]
452    check_internal_open(&ctx, path, options);
453
454    Ok(ctx.base)
455}
456
457/// Implement manual `stat` in a similar manner as manual `open`.
458pub(crate) fn stat(start: &fs::File, path: &Path, follow: FollowSymlinks) -> io::Result<Metadata> {
459    // POSIX returns `ENOENT` on an empty path. TODO: On Windows, we should
460    // be compatible with what Windows does instead.
461    if path.as_os_str().is_empty() {
462        return Err(errors::no_such_file_or_directory());
463    }
464
465    let mut options = OpenOptions::new();
466    options.follow(follow);
467    let mut symlink_count = 0;
468    let mut ctx = Context::new(MaybeOwnedFile::borrowed(start), path, &options, None);
469    assert!(!ctx.dir_precluded);
470
471    while let Some(c) = ctx.components.pop() {
472        match c {
473            CowComponent::PrefixOrRootDir => return Err(errors::escape_attempt()),
474            CowComponent::CurDir => ctx.cur_dir()?,
475            CowComponent::ParentDir => ctx.parent_dir()?,
476            CowComponent::Normal(one) => {
477                if ctx.components.is_empty() {
478                    // If this is the last component, do a non-following
479                    // `stat_unchecked` on it.
480                    let stat = stat_unchecked(&ctx.base, one.as_ref(), FollowSymlinks::No)?;
481
482                    // If we weren't asked to follow symlinks, or it wasn't a
483                    // symlink, we're done.
484                    if options.follow == FollowSymlinks::No || !stat.file_type().is_symlink() {
485                        if stat.is_dir() {
486                            if ctx.dir_precluded {
487                                return Err(errors::is_directory());
488                            }
489                        } else if ctx.dir_required {
490                            return Err(errors::is_not_directory());
491                        }
492                        return Ok(stat);
493                    }
494
495                    // On Windows, symlinks know whether they are a file or
496                    // directory.
497                    #[cfg(windows)]
498                    if stat.file_attributes() & FILE_ATTRIBUTE_DIRECTORY != 0 {
499                        ctx.dir_required = true;
500                    } else {
501                        ctx.dir_precluded = true;
502                    }
503
504                    // If it was a symlink and we're asked to follow symlinks,
505                    // dereference it.
506                    ctx.symlink(&one, &mut symlink_count)?
507                } else {
508                    // Otherwise open the path component normally.
509                    ctx.normal(&one, &options, &mut symlink_count)?
510                }
511            }
512        }
513    }
514
515    // If the path ended in `.` (explicit or implied) or `..`, we may have
516    // opened the directory with eg. `O_PATH` on Linux, or we may have skipped
517    // checking for search access to `.`, so re-check it.
518    if ctx.follow_with_dot {
519        if ctx.dir_precluded {
520            return Err(errors::is_directory());
521        }
522
523        ctx.check_dot_access()?;
524    }
525
526    // If the path ended in `.` or `..`, we already have it open, so just do
527    // `.metadata()` on it.
528    Metadata::from_file(&ctx.base)
529}
530
531/// Test whether the given options imply that we should treat an open file as
532/// potentially being a symlink we need to follow, due to use of `O_PATH`.
533#[cfg(any(target_os = "android", target_os = "linux", target_os = "freebsd"))]
534fn should_emulate_o_path(use_options: &OpenOptions) -> bool {
535    (use_options.ext.custom_flags & (OFlags::PATH.bits() as i32)) == (OFlags::PATH.bits() as i32)
536        && use_options.follow == FollowSymlinks::Yes
537}
538
539#[cfg(racy_asserts)]
540fn check_internal_open(ctx: &Context, path: &Path, options: &OpenOptions) {
541    match open_unchecked(
542        &ctx.start_clone,
543        ctx.canonical_path.debug.as_ref(),
544        options
545            .clone()
546            .create(false)
547            .create_new(false)
548            .truncate(false),
549    ) {
550        Ok(unchecked_file) => {
551            assert_same_file!(
552                &ctx.base,
553                &unchecked_file,
554                "path resolution inconsistency: start='{:?}', path='{}'; canonical_path='{}'",
555                ctx.start_clone,
556                path.display(),
557                ctx.canonical_path.debug.display(),
558            );
559        }
560        Err(_unchecked_error) => {
561            /* TODO: Check error messages.
562            panic!(
563                "unexpected success opening result={:?} start='{:?}', path='{}'; canonical_path='{}'; \
564                 expected {:?}",
565                ctx.base,
566                ctx.start_clone,
567                path.display(),
568                ctx.canonical_path.debug.display(),
569                unchecked_error,
570            */
571        }
572    }
573}