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}