diff --git a/contrib/bindings/python/pathrs.py b/contrib/bindings/python/pathrs.py index 78439c40..f7357445 100644 --- a/contrib/bindings/python/pathrs.py +++ b/contrib/bindings/python/pathrs.py @@ -227,6 +227,7 @@ def proc_open_raw(base, path, flags): return WrappedFd(fd) def proc_readlink(base, path): + # TODO: See if we can merge this with Root.readlink. path = _cstr(path) linkbuf_size = 128 while True: @@ -284,13 +285,34 @@ def open(cls, path): def from_file(cls, file): return cls(file) - def resolve(self, path): + def resolve(self, path, follow_trailing=True): path = _cstr(path) - fd = libpathrs_so.pathrs_resolve(self.fileno(), path) + if follow_trailing: + fd = libpathrs_so.pathrs_resolve(self.fileno(), path) + else: + fd = libpathrs_so.pathrs_resolve_nofollow(self.fileno(), path) if fd < 0: raise Error._fetch(fd) or INTERNAL_ERROR return Handle(fd) + def readlink(self, path): + path = _cstr(path) + linkbuf_size = 128 + while True: + linkbuf = _cbuffer(linkbuf_size) + n = libpathrs_so.pathrs_readlink(self.fileno(), path, linkbuf, linkbuf_size) + if n < 0: + raise Error._fetch(n) or INTERNAL_ERROR + elif n <= linkbuf_size: + return ffi.buffer(linkbuf, linkbuf_size)[:n].decode("latin1") + else: + # The contents were truncated. Unlike readlinkat, pathrs returns + # the size of the link when it checked. So use the returned size + # as a basis for the reallocated size (but in order to avoid a DoS + # where a magic-link is growing by a single byte each iteration, + # make sure we are a fair bit larger). + linkbuf_size += n + def creat(self, path, filemode, mode="r", extra_flags=0): path = _cstr(path) flags = convert_mode(mode) | extra_flags diff --git a/go-pathrs/libpathrs_linux.go b/go-pathrs/libpathrs_linux.go index d9a01876..d79e5a93 100644 --- a/go-pathrs/libpathrs_linux.go +++ b/go-pathrs/libpathrs_linux.go @@ -71,6 +71,38 @@ func pathrsResolve(rootFd uintptr, path string) (uintptr, error) { return uintptr(fd), fetchError(fd) } +func pathrsResolveNoFollow(rootFd uintptr, path string) (uintptr, error) { + cPath := C.CString(path) + defer C.free(unsafe.Pointer(cPath)) + + fd := C.pathrs_resolve_nofollow(C.int(rootFd), cPath) + return uintptr(fd), fetchError(fd) +} + +func pathrsReadlink(rootFd uintptr, path string) (string, error) { + cPath := C.CString(path) + defer C.free(unsafe.Pointer(cPath)) + + size := 128 + for { + linkBuf := make([]byte, size) + n := C.pathrs_readlink(C.int(rootFd), cPath, C.cast_ptr(unsafe.Pointer(&linkBuf[0])), C.ulong(len(linkBuf))) + switch { + case int(n) < 0: + return "", fetchError(n) + case int(n) <= len(linkBuf): + return string(linkBuf[:int(n)]), nil + default: + // The contents were truncated. Unlike readlinkat, pathrs returns + // the size of the link when it checked. So use the returned size + // as a basis for the reallocated size (but in order to avoid a DoS + // where a magic-link is growing by a single byte each iteration, + // make sure we are a fair bit larger). + size += int(n) + } + } +} + func pathrsCreat(rootFd uintptr, path string, flags int, mode uint32) (uintptr, error) { cPath := C.CString(path) defer C.free(unsafe.Pointer(cPath)) @@ -146,6 +178,8 @@ func pathrsProcOpen(base pathrsProcBase, path string, flags int) (uintptr, error } func pathrsProcReadlink(base pathrsProcBase, path string) (string, error) { + // TODO: See if we can unify this code with pathrsReadlink. + cBase := C.pathrs_proc_base_t(base) cPath := C.CString(path) diff --git a/go-pathrs/root_linux.go b/go-pathrs/root_linux.go index 30e80ad8..fe7ed861 100644 --- a/go-pathrs/root_linux.go +++ b/go-pathrs/root_linux.go @@ -62,6 +62,10 @@ func RootFromFile(file *os.File) (*Root, error) { // Resolve resolves the given path within the Root's directory tree, and return // a Handle to the resolved path. The path must already exist, otherwise an // error will occur. +// +// All symlinks (including trailing symlinks) are followed, but they are +// resolved within the rootfs. If you wish to open a handle to the symlink +// itself, use ResolveNoFollow. func (r *Root) Resolve(path string) (*Handle, error) { // TODO: Get the actual name of the handle through /proc/self/fd/... fakeName, err := randName(32) @@ -81,6 +85,29 @@ func (r *Root) Resolve(path string) (*Handle, error) { }) } +// ResolveNoFollow is effectively an O_NOFOLLOW version of Resolve. Their +// behaviour is identical, except that *trailing* symlinks will not be +// followed. If the final component is a trailing symlink, an O_PATH|O_NOFOLLOW +// handle to the symlink itself is returned. +func (r *Root) ResolveNoFollow(path string) (*Handle, error) { + // TODO: Get the actual name of the handle through /proc/self/fd/... + fakeName, err := randName(32) + if err != nil { + return nil, err + } + // Prefix the root. + fakeName = r.inner.Name() + fakeName + + return withFileFd(r.inner, func(rootFd uintptr) (*Handle, error) { + handleFd, err := pathrsResolveNoFollow(rootFd, path) + if err != nil { + return nil, err + } + handleFile := os.NewFile(uintptr(handleFd), fakeName) + return &Handle{inner: handleFile}, nil + }) +} + // Create creates a file within the Root's directory tree at the given path, // and returns a handle to the file. The provided mode is used for the new file // (the process's umask applies). diff --git a/include/pathrs.h b/include/pathrs.h index 507248e7..c0e2f8bf 100644 --- a/include/pathrs.h +++ b/include/pathrs.h @@ -133,6 +133,10 @@ int pathrs_reopen(int fd, int flags); * Resolve the given path within the rootfs referenced by root_fd. The path * *must already exist*, otherwise an error will occur. * + * All symlinks (including trailing symlinks) are followed, but they are + * resolved within the rootfs. If you wish to open a handle to the symlink + * itself, use pathrs_resolve_nofollow(). + * * # Return Value * * On success, this function returns an O_PATH file descriptor referencing the @@ -145,6 +149,66 @@ int pathrs_reopen(int fd, int flags); */ int pathrs_resolve(int root_fd, const char *path); +/** + * pathrs_resolve_nofollow() is effectively an O_NOFOLLOW version of + * pathrs_resolve(). Their behaviour is identical, except that *trailing* + * symlinks will not be followed. If the final component is a trailing symlink, + * an O_PATH|O_NOFOLLOW handle to the symlink itself is returned. + * + * # Return Value + * + * On success, this function returns an O_PATH file descriptor referencing the + * resolved path. + * + * If an error occurs, this function will return a negative error code. To + * retrieve information about the error (such as a string describing the error, + * the system errno(7) value associated with the error, etc), use + * pathrs_errorinfo(). + */ +int pathrs_resolve_nofollow(int root_fd, const char *path); + +/** + * Get the target of a symlink within the rootfs referenced by root_fd. + * + * NOTE: The returned path is not modified to be "safe" outside of the + * root. You should not use this path for doing further path lookups -- use + * pathrs_resolve() instead. + * + * This method is just shorthand for: + * + * ```c + * int linkfd = pathrs_resolve_nofollow(rootfd, path); + * if (linkfd < 0) { + * liberr = fd; // for use with pathrs_errorinfo() + * goto err; + * } + * copied = readlinkat(linkfd, "", linkbuf, linkbuf_size); + * close(linkfd); + * ``` + * + * # Return Value + * + * On success, this function copies the symlink contents to `linkbuf` (up to + * `linkbuf_size` bytes) and returns the full size of the symlink path buffer. + * This function will not copy the trailing NUL byte, and the return size does + * not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are + * treated as zero-size buffers. + * + * NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to + * contain the symlink contents, pathrs_readlink() will return *the number of + * bytes it would have copied if the buffer was large enough*. This matches the + * behaviour of pathrs_proc_readlink(). + * + * If an error occurs, this function will return a negative error code. To + * retrieve information about the error (such as a string describing the error, + * the system errno(7) value associated with the error, etc), use + * pathrs_errorinfo(). + */ +int pathrs_readlink(int root_fd, + const char *path, + char *linkbuf, + size_t linkbuf_size); + /** * Rename a path within the rootfs referenced by root_fd. The flags argument is * identical to the renameat2(2) flags that are supported on the system. @@ -306,7 +370,7 @@ int pathrs_proc_open(pathrs_proc_base_t base, const char *path, int flags); * This function is effectively shorthand for * * ```c - * fd = pathrs_proc_readlink(base, path, O_PATH|O_NOFOLLOW); + * fd = pathrs_proc_open(base, path, O_PATH|O_NOFOLLOW); * if (fd < 0) { * liberr = fd; // for use with pathrs_errorinfo() * goto err; @@ -323,9 +387,10 @@ int pathrs_proc_open(pathrs_proc_base_t base, const char *path, int flags); * not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are * treated as zero-size buffers. * - * NOTE: Unlike `readlinkat(2)`, in the case where `linkbuf` is too small to - * contain the symlink contents, `pathrs_proc_readlink` will return *the number - * of bytes it would have copied if the buffer was large enough*. + * NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to + * contain the symlink contents, pathrs_proc_readlink() will return *the number + * of bytes it would have copied if the buffer was large enough*. This matches + * the behaviour of pathrs_readlink(). * * If an error occurs, this function will return a negative error code. To * retrieve information about the error (such as a string describing the error, diff --git a/src/capi/core.rs b/src/capi/core.rs index 119c9c15..af15872f 100644 --- a/src/capi/core.rs +++ b/src/capi/core.rs @@ -37,7 +37,7 @@ use std::{ }, }; -use libc::{c_char, c_int, c_uint, dev_t}; +use libc::{c_char, c_int, c_uint, dev_t, size_t}; use snafu::ResultExt; /// Open a root handle. @@ -106,6 +106,10 @@ pub extern "C" fn pathrs_reopen(fd: RawFd, flags: c_int) -> RawFd { /// Resolve the given path within the rootfs referenced by root_fd. The path /// *must already exist*, otherwise an error will occur. /// +/// All symlinks (including trailing symlinks) are followed, but they are +/// resolved within the rootfs. If you wish to open a handle to the symlink +/// itself, use pathrs_resolve_nofollow(). +/// /// # Return Value /// /// On success, this function returns an O_PATH file descriptor referencing the @@ -122,6 +126,75 @@ pub extern "C" fn pathrs_resolve(root_fd: RawFd, path: *const c_char) -> RawFd { }) } +/// pathrs_resolve_nofollow() is effectively an O_NOFOLLOW version of +/// pathrs_resolve(). Their behaviour is identical, except that *trailing* +/// symlinks will not be followed. If the final component is a trailing symlink, +/// an O_PATH|O_NOFOLLOW handle to the symlink itself is returned. +/// +/// # Return Value +/// +/// On success, this function returns an O_PATH file descriptor referencing the +/// resolved path. +/// +/// If an error occurs, this function will return a negative error code. To +/// retrieve information about the error (such as a string describing the error, +/// the system errno(7) value associated with the error, etc), use +/// pathrs_errorinfo(). +#[no_mangle] +pub extern "C" fn pathrs_resolve_nofollow(root_fd: RawFd, path: *const c_char) -> RawFd { + ret::with_fd(root_fd, |root: &mut Root| { + root.resolve_nofollow(utils::parse_path(path)?) + }) +} + +/// Get the target of a symlink within the rootfs referenced by root_fd. +/// +/// NOTE: The returned path is not modified to be "safe" outside of the +/// root. You should not use this path for doing further path lookups -- use +/// pathrs_resolve() instead. +/// +/// This method is just shorthand for: +/// +/// ```c +/// int linkfd = pathrs_resolve_nofollow(rootfd, path); +/// if (linkfd < 0) { +/// liberr = fd; // for use with pathrs_errorinfo() +/// goto err; +/// } +/// copied = readlinkat(linkfd, "", linkbuf, linkbuf_size); +/// close(linkfd); +/// ``` +/// +/// # Return Value +/// +/// On success, this function copies the symlink contents to `linkbuf` (up to +/// `linkbuf_size` bytes) and returns the full size of the symlink path buffer. +/// This function will not copy the trailing NUL byte, and the return size does +/// not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are +/// treated as zero-size buffers. +/// +/// NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to +/// contain the symlink contents, pathrs_readlink() will return *the number of +/// bytes it would have copied if the buffer was large enough*. This matches the +/// behaviour of pathrs_proc_readlink(). +/// +/// If an error occurs, this function will return a negative error code. To +/// retrieve information about the error (such as a string describing the error, +/// the system errno(7) value associated with the error, etc), use +/// pathrs_errorinfo(). +#[no_mangle] +pub extern "C" fn pathrs_readlink( + root_fd: RawFd, + path: *const c_char, + linkbuf: *mut c_char, + linkbuf_size: size_t, +) -> RawFd { + ret::with_fd(root_fd, |root: &mut Root| { + let link_target = root.readlink(utils::parse_path(path)?)?; + utils::copy_path_into_buffer(link_target, linkbuf, linkbuf_size) + }) +} + /// Rename a path within the rootfs referenced by root_fd. The flags argument is /// identical to the renameat2(2) flags that are supported on the system. /// diff --git a/src/capi/procfs.rs b/src/capi/procfs.rs index a94959b6..85b102b6 100644 --- a/src/capi/procfs.rs +++ b/src/capi/procfs.rs @@ -19,16 +19,11 @@ use crate::{ capi::{ret::IntoCReturn, utils}, error::Error, + flags::OpenFlags, procfs::{ProcfsBase, PROCFS_HANDLE}, - OpenFlags, }; -use std::{ - cmp, - ffi::CString, - os::{fd::RawFd, unix::ffi::OsStringExt}, - ptr, -}; +use std::os::fd::RawFd; use libc::{c_char, c_int, size_t}; @@ -127,7 +122,7 @@ pub extern "C" fn pathrs_proc_open(base: CProcfsBase, path: *const c_char, flags /// This function is effectively shorthand for /// /// ```c -/// fd = pathrs_proc_readlink(base, path, O_PATH|O_NOFOLLOW); +/// fd = pathrs_proc_open(base, path, O_PATH|O_NOFOLLOW); /// if (fd < 0) { /// liberr = fd; // for use with pathrs_errorinfo() /// goto err; @@ -144,9 +139,10 @@ pub extern "C" fn pathrs_proc_open(base: CProcfsBase, path: *const c_char, flags /// not include the NUL byte. A `NULL` `linkbuf` or invalid `linkbuf_size` are /// treated as zero-size buffers. /// -/// NOTE: Unlike `readlinkat(2)`, in the case where `linkbuf` is too small to -/// contain the symlink contents, `pathrs_proc_readlink` will return *the number -/// of bytes it would have copied if the buffer was large enough*. +/// NOTE: Unlike readlinkat(2), in the case where linkbuf is too small to +/// contain the symlink contents, pathrs_proc_readlink() will return *the number +/// of bytes it would have copied if the buffer was large enough*. This matches +/// the behaviour of pathrs_readlink(). /// /// If an error occurs, this function will return a negative error code. To /// retrieve information about the error (such as a string describing the error, @@ -162,26 +158,9 @@ pub extern "C" fn pathrs_proc_readlink( || -> Result<_, Error> { let path = utils::parse_path(path)?; - let link_target = CString::new( - PROCFS_HANDLE - .readlink(base.into(), path)? - .into_os_string() - .into_vec(), - ) - .expect("link from readlink should not contain any nulls"); - - // If the linkbuf is null, we just return the number of bytes we - // would've written. - if !linkbuf.is_null() && linkbuf_size > 0 { - let to_copy = cmp::min(link_target.count_bytes(), linkbuf_size); - // SAFETY: The C caller guarantees that linkbuf is safe to write to - // up to linkbuf_size bytes. - unsafe { - ptr::copy_nonoverlapping(link_target.as_ptr(), linkbuf, to_copy); - } - } + let link_target = PROCFS_HANDLE.readlink(base.into(), path)?; - Ok(link_target.count_bytes() as c_int) + utils::copy_path_into_buffer(link_target, linkbuf, linkbuf_size) }() .into_c_return() } diff --git a/src/capi/utils.rs b/src/capi/utils.rs index c8399c0e..bfa338f0 100644 --- a/src/capi/utils.rs +++ b/src/capi/utils.rs @@ -19,6 +19,7 @@ use crate::error::{self, Error}; use std::{ + cmp, convert::TryInto, ffi::{CStr, CString, OsStr}, io::Error as IOError, @@ -27,7 +28,7 @@ use std::{ ptr, }; -use libc::c_char; +use libc::{c_char, c_int, size_t}; pub(crate) fn parse_path<'a>(path: *const c_char) -> Result<&'a Path, Error> { ensure!( @@ -42,6 +43,28 @@ pub(crate) fn parse_path<'a>(path: *const c_char) -> Result<&'a Path, Error> { Ok(OsStr::from_bytes(bytes).as_ref()) } +pub(crate) fn copy_path_into_buffer>( + path: P, + buf: *mut c_char, + bufsize: size_t, +) -> Result { + let path = CString::new(path.as_ref().as_os_str().as_bytes()) + .expect("link from readlink should not contain any nulls"); + + // If the linkbuf is null, we just return the number of bytes we + // would've written. + if !buf.is_null() && bufsize > 0 { + let to_copy = cmp::min(path.count_bytes(), bufsize); + // SAFETY: The C caller guarantees that buf is safe to write to + // up to bufsize bytes. + unsafe { + ptr::copy_nonoverlapping(path.as_ptr(), buf, to_copy); + } + } + + Ok(path.count_bytes() as c_int) +} + pub(crate) trait Leakable { /// Leak a structure such that it can be passed through C-FFI. fn leak(self) -> &'static mut Self; diff --git a/src/flags.rs b/src/flags.rs index c0176795..0432bc83 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -262,3 +262,14 @@ mod tests { ); } } + +bitflags! { + /// Optional flags to modify the resolution of paths inside a [`Root`]. + /// + /// [`Root`]: struct.Root.html + #[derive(Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] + pub struct ResolverFlags: u64 { + // TODO: We should probably have our own bits... + const NO_SYMLINKS = libc::RESOLVE_NO_SYMLINKS; + } +} diff --git a/src/lib.rs b/src/lib.rs index f7db9d2a..edb07ca6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,7 +48,7 @@ //! //! ``` //! # extern crate libc; -//! # use pathrs::{error::Error, Root, OpenFlags}; +//! # use pathrs::{error::Error, flags::OpenFlags, Root}; //! # fn main() -> Result<(), Error> { //! let (root_path, unsafe_path) = ("/path/to/root", "/etc/passwd"); //! # let root_path = "/"; @@ -137,9 +137,6 @@ extern crate snafu; /// Bit-flags for controlling various operations. pub mod flags; -// TODO: Remove this. -#[doc(inline)] -pub use flags::{OpenFlags, RenameFlags}; // `Handle` implementation. mod handle; @@ -157,7 +154,7 @@ pub mod error; // Backend resolver implementations. mod resolvers; #[doc(inline)] -pub use resolvers::{Resolver, ResolverBackend, ResolverFlags}; +pub use resolvers::{Resolver, ResolverBackend}; /// Safe procfs handles. pub mod procfs; diff --git a/src/procfs.rs b/src/procfs.rs index a50b09fc..0dc89a51 100644 --- a/src/procfs.rs +++ b/src/procfs.rs @@ -20,9 +20,10 @@ use crate::{ error::{self, Error}, - resolvers::{procfs::ProcfsResolver, ResolverFlags}, + flags::{OpenFlags, ResolverFlags}, + resolvers::procfs::ProcfsResolver, syscalls::{self, FsmountFlags, FsopenFlags, OpenTreeFlags}, - utils, OpenFlags, + utils, }; use std::{ @@ -59,7 +60,6 @@ lazy_static! { /// [`ProcfsHandle`]: struct.ProcfsHandle.html #[non_exhaustive] #[derive(Debug, Clone, Copy)] -#[allow(dead_code)] // TODO: Remove when we export this properly. pub enum ProcfsBase { /// Use `/proc/self`. For most programs, this is the standard choice. ProcSelf, @@ -424,9 +424,9 @@ impl ProcfsHandle { // Do a basic lookup. let base = self.open_base(base)?; - let file = - self.resolver - .resolve(&base, subpath, flags, ResolverFlags::NO_FOLLOW_TRAILING)?; + let file = self + .resolver + .resolve(&base, subpath, flags, ResolverFlags::empty())?; // Detect if the file we landed is in a bind-mount. self.check_mnt_id(&file, "")?; diff --git a/src/resolvers/mod.rs b/src/resolvers/mod.rs index 7165ec0b..a0637dbc 100644 --- a/src/resolvers/mod.rs +++ b/src/resolvers/mod.rs @@ -18,7 +18,7 @@ #![forbid(unsafe_code)] -use crate::{error::Error, syscalls, Handle}; +use crate::{error::Error, flags::ResolverFlags, syscalls, Handle}; use std::{fs::File, path::Path}; @@ -32,28 +32,6 @@ pub(crate) mod procfs; /// Maximum number of symlink traversals we will accept. const MAX_SYMLINK_TRAVERSALS: usize = 128; -bitflags! { - /// Optional flags to modify the resolution of paths inside a [`Root`]. - /// - /// [`Root`]: struct.Root.html - #[derive(Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] - pub struct ResolverFlags: u64 { - // TODO: We should probably have our own bits... - const NO_FOLLOW_TRAILING = libc::O_NOFOLLOW as u64; - const NO_SYMLINKS = libc::RESOLVE_NO_SYMLINKS; - } -} - -impl ResolverFlags { - pub(crate) fn openat2_flag_bits(self) -> u64 { - self.intersection(ResolverFlags::NO_FOLLOW_TRAILING).bits() - } - - pub(crate) fn openat2_resolve_bits(self) -> u64 { - self.intersection(ResolverFlags::NO_SYMLINKS).bits() - } -} - /// The backend used for path resolution within a [`Root`] to get a [`Handle`]. /// /// We don't generally recommend specifying this, since libpathrs will @@ -116,10 +94,19 @@ pub struct Resolver { impl Resolver { /// Internal dispatcher to the relevant backend. #[inline] - pub(crate) fn resolve>(&self, root: &File, path: P) -> Result { + pub(crate) fn resolve>( + &self, + root: &File, + path: P, + no_follow_trailing: bool, + ) -> Result { match self.backend { - ResolverBackend::KernelOpenat2 => openat2::resolve(root, path, self.flags), - ResolverBackend::EmulatedOpath => opath::resolve(root, path, self.flags), + ResolverBackend::KernelOpenat2 => { + openat2::resolve(root, path, self.flags, no_follow_trailing) + } + ResolverBackend::EmulatedOpath => { + opath::resolve(root, path, self.flags, no_follow_trailing) + } } } } diff --git a/src/resolvers/opath.rs b/src/resolvers/opath.rs index a0f7b8fe..42c571c3 100644 --- a/src/resolvers/opath.rs +++ b/src/resolvers/opath.rs @@ -37,8 +37,9 @@ use crate::{ error::{self, Error, ErrorExt}, + flags::ResolverFlags, procfs::PROCFS_HANDLE, - resolvers::{ResolverFlags, MAX_SYMLINK_TRAVERSALS}, + resolvers::MAX_SYMLINK_TRAVERSALS, syscalls, utils::{RawComponentsIter, RawFdExt}, Handle, @@ -165,6 +166,7 @@ pub(crate) fn resolve>( root: &File, path: P, flags: ResolverFlags, + no_follow_trailing: bool, ) -> Result { let path = path.as_ref(); @@ -276,7 +278,7 @@ pub(crate) fn resolve>( // If we hit the last component and we were told to not follow the // trailing symlink, just return the link we have. // TODO: Is this behaviour correct for "foo/" cases? - if remaining_components.is_empty() && flags.contains(ResolverFlags::NO_FOLLOW_TRAILING) { + if remaining_components.is_empty() && no_follow_trailing { current = next.into(); break; } diff --git a/src/resolvers/openat2.rs b/src/resolvers/openat2.rs index 2e0f2373..fa045612 100644 --- a/src/resolvers/openat2.rs +++ b/src/resolvers/openat2.rs @@ -18,7 +18,7 @@ use crate::{ error::{self, Error}, - resolvers::ResolverFlags, + flags::{OpenFlags, ResolverFlags}, syscalls::{self, OpenHow}, Handle, }; @@ -32,6 +32,7 @@ pub(crate) fn resolve>( root: &File, path: P, rflags: ResolverFlags, + no_follow_trailing: bool, ) -> Result { ensure!( *syscalls::OPENAT2_IS_SUPPORTED, @@ -39,12 +40,14 @@ pub(crate) fn resolve>( ); // Copy the O_NOFOLLOW and RESOLVE_NO_SYMLINKS bits from flags. - let oflags = libc::O_PATH as u64 | rflags.openat2_flag_bits(); - let rflags = - libc::RESOLVE_IN_ROOT | libc::RESOLVE_NO_MAGICLINKS | rflags.openat2_resolve_bits(); + let mut oflags = OpenFlags::O_PATH; + if no_follow_trailing { + oflags.insert(OpenFlags::O_NOFOLLOW); + } + let rflags = libc::RESOLVE_IN_ROOT | libc::RESOLVE_NO_MAGICLINKS | rflags.bits(); let how = OpenHow { - flags: oflags, + flags: oflags.bits() as u64, resolve: rflags, ..Default::default() }; diff --git a/src/resolvers/procfs.rs b/src/resolvers/procfs.rs index b4d2580b..72801072 100644 --- a/src/resolvers/procfs.rs +++ b/src/resolvers/procfs.rs @@ -31,8 +31,8 @@ use crate::{ error::{self, Error, ErrorExt}, - flags::OpenFlags, - resolvers::{ResolverFlags, MAX_SYMLINK_TRAVERSALS}, + flags::{OpenFlags, ResolverFlags}, + resolvers::MAX_SYMLINK_TRAVERSALS, syscalls::{self, OpenHow}, utils::{self, RawComponentsIter}, }; @@ -67,8 +67,8 @@ impl ProcfsResolver { &self, root: &File, path: P, - mut oflags: OpenFlags, - mut rflags: ResolverFlags, + oflags: OpenFlags, + rflags: ResolverFlags, ) -> Result { // These flags don't make sense for procfs and will just result in // confusing errors during lookup. O_TMPFILE contains multiple flags @@ -85,14 +85,6 @@ impl ProcfsResolver { }, ); - // If O_NOFOLLOW was specified, convert it to NO_FOLLOW_TRAILING. - if oflags.contains(OpenFlags::O_NOFOLLOW) { - rflags.insert(ResolverFlags::NO_FOLLOW_TRAILING); - } - if rflags.contains(ResolverFlags::NO_FOLLOW_TRAILING) { - oflags.insert(OpenFlags::O_NOFOLLOW); - } - match *self { Self::Openat2 => openat2_resolve(root, path, oflags, rflags), Self::RestrictedOpath => opath_resolve(root, path, oflags, rflags), @@ -112,11 +104,9 @@ fn openat2_resolve>( ); // Copy the O_NOFOLLOW and RESOLVE_NO_SYMLINKS bits from rflags. - let oflags = oflags.bits() as u64 | rflags.openat2_flag_bits(); - let rflags = libc::RESOLVE_BENEATH - | libc::RESOLVE_NO_MAGICLINKS - | libc::RESOLVE_NO_XDEV - | rflags.openat2_resolve_bits(); + let oflags = oflags.bits() as u64; + let rflags = + libc::RESOLVE_BENEATH | libc::RESOLVE_NO_MAGICLINKS | libc::RESOLVE_NO_XDEV | rflags.bits(); syscalls::openat2( root.as_raw_fd(), @@ -376,11 +366,11 @@ fn opath_resolve>( mod tests { use crate::{ error::{Error as PathrsError, ErrorKind}, + flags::{OpenFlags, ResolverFlags}, resolvers::procfs::ProcfsResolver, syscalls, tests::common as tests_common, utils::RawFdExt, - OpenFlags, ResolverFlags, }; use std::{fs::File, path::PathBuf}; diff --git a/src/root.rs b/src/root.rs index 11cacc88..337bded3 100644 --- a/src/root.rs +++ b/src/root.rs @@ -234,23 +234,57 @@ impl Root { } /// Within the given [`Root`]'s tree, resolve `path` and return a - /// [`Handle`]. All symlink path components are scoped to [`Root`]. + /// [`Handle`]. All symlink path components are scoped to [`Root`]. Trailing + /// symlinks *are* followed, if you want to get a handle to a symlink use + /// [`Root::resolve_nofollow`]. /// /// # Errors /// /// If `path` doesn't exist, or an attack was detected during resolution, a - /// corresponding Error will be returned. If no error is returned, then the - /// path is guaranteed to have been reachable from the root of the directory - /// tree and thus have been inside the root at one point in the resolution. + /// corresponding [`Error`] will be returned. If no error is returned, then + /// the path is guaranteed to have been reachable from the root of the + /// directory tree and thus have been inside the root at one point in the + /// resolution. /// /// [`Root`]: struct.Root.html /// [`Handle`]: trait.Handle.html + /// [`Error`]: error/struct.Error.html + /// [`Root::resolve_nofollow`]: struct.Root.html#method.resolve_nofollow #[inline] pub fn resolve>(&self, path: P) -> Result { - self.resolver.resolve(&self.inner, path) + self.resolver.resolve(&self.inner, path, false) } - // TODO: readlink (need to move ResolverFlags out of Resolver) + /// Identical to [`Root::resolve`], except that *trailing* symlinks are + /// *not* followed and if the trailing component is a symlink + /// `Root::resolve_nofollow` will return a handle to the symlink itself. + /// + /// [`Root::resolve`]: struct.Root.html#method.resolve + #[inline] + pub fn resolve_nofollow>(&self, path: P) -> Result { + self.resolver.resolve(&self.inner, path, true) + } + + /// Get the target of a symlink within a [`Root`]. + /// + /// **NOTE**: The returned path is not modified to be "safe" outside of the + /// root. You should not use this path for doing further path lookups -- use + /// [`Root::resolve`] instead. + /// + /// This method is just shorthand for calling `readlinkat(2)` on the handle + /// returned by [`Root::resolve_nofollow`]. + /// + /// [`Root`]: struct.Root.html + /// [`Root::resolve`]: struct.Root.html#method.resolve + /// [`Root::resolve_nofollow`]: struct.Root.html#method.resolve_nofollow + pub fn readlink>(&self, path: P) -> Result { + let link = self + .resolve_nofollow(path) + .wrap("resolve symlink O_NOFOLLOW for readlink")?; + syscalls::readlinkat(link.as_file().as_raw_fd(), "").context(error::RawOsSnafu { + operation: "readlink resolve symlink", + }) + } /// Within the [`Root`]'s tree, create an inode at `path` as specified by /// `inode_type`. diff --git a/src/syscalls.rs b/src/syscalls.rs index 730273c8..b9f7132f 100644 --- a/src/syscalls.rs +++ b/src/syscalls.rs @@ -20,8 +20,8 @@ #![allow(unsafe_code)] use crate::{ + flags::OpenFlags, utils::{RawFdExt, ToCString}, - OpenFlags, }; use std::{ diff --git a/src/tests/test_procfs.rs b/src/tests/test_procfs.rs index 9f5932fd..6163572f 100644 --- a/src/tests/test_procfs.rs +++ b/src/tests/test_procfs.rs @@ -18,10 +18,10 @@ use crate::{ error::ErrorKind, + flags::OpenFlags, procfs::{ProcfsBase, ProcfsHandle}, resolvers::procfs::ProcfsResolver, syscalls::{self, OpenTreeFlags}, - OpenFlags, }; use utils::ExpectedResult; diff --git a/src/tests/test_resolve.rs b/src/tests/test_resolve.rs index 63010358..1358ee4a 100644 --- a/src/tests/test_resolve.rs +++ b/src/tests/test_resolve.rs @@ -19,21 +19,25 @@ use std::{fs::File, path::Path}; use crate::{ + flags::ResolverFlags, resolvers::{opath, openat2}, syscalls, tests::common as tests_common, - Handle, ResolverBackend, ResolverFlags, Root, + Handle, ResolverBackend, Root, }; use utils::ExpectedResult; use anyhow::Error; +// TODO: See if we can adjust the logic from test_root_ops or test_procs to make +// this macro soup a little nicer. + macro_rules! resolve_tests { // resolve_tests! { // abc("foo") => ExpectedResult::Err(..); // xyz("baz") => ExpectedResult::Ok{..}; // } - ($(let $root_dir_var:ident = $root_dir:expr => @reopen_tests:$reopen_tests:ident { $($test_name:ident ( $unsafe_path:expr, $rflags:expr ) => $expected:expr);+ $(;)? });* $(;)?) => { + ($(let $root_dir_var:ident = $root_dir:expr => @reopen_tests:$reopen_tests:ident { $($test_name:ident ( $unsafe_path:expr, $rflags:expr, $no_follow_trailing:expr ) => $expected:expr);+ $(;)? });* $(;)?) => { paste::paste! { $( $( #[test] @@ -46,6 +50,7 @@ macro_rules! resolve_tests { utils::check_resolve_in_root( &root, $unsafe_path, + $no_follow_trailing, &expected, reopen_tests, )?; @@ -68,6 +73,7 @@ macro_rules! resolve_tests { utils::check_resolve_in_root( &root, $unsafe_path, + $no_follow_trailing, &expected, reopen_tests, )?; @@ -95,6 +101,7 @@ macro_rules! resolve_tests { utils::check_resolve_in_root( &root, $unsafe_path, + $no_follow_trailing, &expected, reopen_tests, )?; @@ -115,7 +122,7 @@ macro_rules! resolve_tests { let reopen_tests: bool = $reopen_tests; utils::check_resolve_fn( |file, subpath| { - opath::resolve(file, subpath, rflags) + opath::resolve(file, subpath, rflags, $no_follow_trailing) .map(Handle::into_file) }, &root, @@ -145,7 +152,7 @@ macro_rules! resolve_tests { let reopen_tests: bool = $reopen_tests; utils::check_resolve_fn( |file, subpath| { - openat2::resolve(file, subpath, rflags) + openat2::resolve(file, subpath, rflags, $no_follow_trailing) .map(Handle::into_file) }, &root, @@ -166,171 +173,171 @@ macro_rules! resolve_tests { resolve_tests! { let proc_root_dir = Path::new("/proc") => @reopen_tests:false { - proc_pseudo_magiclink("self/status", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: &format!("{}/status", syscalls::getpid()), file_type: libc::S_IFREG }; - proc_pseudo_magiclink_nosym1("self", ResolverFlags::NO_SYMLINKS) => ExpectedResult::Err(libc::ELOOP); - proc_pseudo_magiclink_nosym2("self/status", ResolverFlags::NO_SYMLINKS) => ExpectedResult::Err(libc::ELOOP); - proc_pseudo_magiclink_nofollow1("self", ResolverFlags::NO_FOLLOW_TRAILING) => ExpectedResult::Ok { real_path: "self", file_type: libc::S_IFLNK }; - proc_pseudo_magiclink_nofollow2("self/status", ResolverFlags::NO_FOLLOW_TRAILING) => ExpectedResult::Ok { real_path: &format!("{}/status", syscalls::getpid()), file_type: libc::S_IFREG }; - - proc_magiclink("self/exe", ResolverFlags::empty()) => ExpectedResult::Err(libc::ELOOP); - proc_magiclink_nofollow("self/exe", ResolverFlags::NO_FOLLOW_TRAILING) => ExpectedResult::Ok { real_path: &format!("{}/exe", syscalls::getpid()), file_type: libc::S_IFLNK }; - proc_magiclink_component_nofollow("self/root/etc/passwd", ResolverFlags::NO_FOLLOW_TRAILING) => ExpectedResult::Err(libc::ELOOP); + proc_pseudo_magiclink("self/status", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: &format!("{}/status", syscalls::getpid()), file_type: libc::S_IFREG }; + proc_pseudo_magiclink_nosym1("self", ResolverFlags::NO_SYMLINKS, false) => ExpectedResult::Err(libc::ELOOP); + proc_pseudo_magiclink_nosym2("self/status", ResolverFlags::NO_SYMLINKS, false) => ExpectedResult::Err(libc::ELOOP); + proc_pseudo_magiclink_nofollow1("self", ResolverFlags::empty(), true) => ExpectedResult::Ok { real_path: "self", file_type: libc::S_IFLNK }; + proc_pseudo_magiclink_nofollow2("self/status", ResolverFlags::empty(), true) => ExpectedResult::Ok { real_path: &format!("{}/status", syscalls::getpid()), file_type: libc::S_IFREG }; + + proc_magiclink("self/exe", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ELOOP); + proc_magiclink_nofollow("self/exe", ResolverFlags::empty(), true) => ExpectedResult::Ok { real_path: &format!("{}/exe", syscalls::getpid()), file_type: libc::S_IFLNK }; + proc_magiclink_component_nofollow("self/root/etc/passwd", ResolverFlags::empty(), true) => ExpectedResult::Err(libc::ELOOP); }; // Complete lookups. let root_dir = tests_common::create_basic_tree()? => @reopen_tests:true { - complete_root1("/", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/", file_type: libc::S_IFDIR }; - complete_root2("/../../../../../..", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/", file_type: libc::S_IFDIR }; - complete_root_link1("root-link1", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/", file_type: libc::S_IFDIR }; - complete_root_link2("root-link2", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/", file_type: libc::S_IFDIR }; - complete_root_link3("root-link3", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/", file_type: libc::S_IFDIR }; - complete_dir1("a", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/a", file_type: libc::S_IFDIR }; - complete_dir2("b/c/d/e/f", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/b/c/d/e/f", file_type: libc::S_IFDIR }; - complete_dir3("b///././c////.//d/./././///e////.//./f//././././", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/b/c/d/e/f", file_type: libc::S_IFDIR }; - complete_file("b/c/file", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/b/c/file", file_type: libc::S_IFREG }; - complete_file_link("b-file", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/b/c/file", file_type: libc::S_IFREG }; - complete_fifo("b/fifo", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/b/fifo", file_type: libc::S_IFIFO }; - complete_sock("b/sock", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/b/sock", file_type: libc::S_IFSOCK }; + complete_root1("/", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/", file_type: libc::S_IFDIR }; + complete_root2("/../../../../../..", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/", file_type: libc::S_IFDIR }; + complete_root_link1("root-link1", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/", file_type: libc::S_IFDIR }; + complete_root_link2("root-link2", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/", file_type: libc::S_IFDIR }; + complete_root_link3("root-link3", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/", file_type: libc::S_IFDIR }; + complete_dir1("a", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/a", file_type: libc::S_IFDIR }; + complete_dir2("b/c/d/e/f", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/b/c/d/e/f", file_type: libc::S_IFDIR }; + complete_dir3("b///././c////.//d/./././///e////.//./f//././././", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/b/c/d/e/f", file_type: libc::S_IFDIR }; + complete_file("b/c/file", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/b/c/file", file_type: libc::S_IFREG }; + complete_file_link("b-file", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/b/c/file", file_type: libc::S_IFREG }; + complete_fifo("b/fifo", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/b/fifo", file_type: libc::S_IFIFO }; + complete_sock("b/sock", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/b/sock", file_type: libc::S_IFSOCK }; // Partial lookups. - partial_dir_basic("a/b/c/d/e/f/g/h", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - partial_dir_dotdot("a/foo/../bar/baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); + partial_dir_basic("a/b/c/d/e/f/g/h", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + partial_dir_dotdot("a/foo/../bar/baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); // Complete lookups of non_lexical symlinks. - nonlexical_basic_complete("target", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_basic_complete1("target/", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_basic_complete2("target//", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_basic_partial("target/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_basic_partial_dotdot("target/../target/foo/bar/../baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level1_abs_complete1("link1/target_abs", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level1_abs_complete2("link1/target_abs/", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level1_abs_complete3("link1/target_abs//", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level1_abs_partial("link1/target_abs/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level1_abs_partial_dotdot("link1/target_abs/../target/foo/bar/../baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level1_rel_complete1("link1/target_rel", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level1_rel_complete2("link1/target_rel/", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level1_rel_complete3("link1/target_rel//", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level1_rel_partial("link1/target_rel/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level1_rel_partial_dotdot("link1/target_rel/../target/foo/bar/../baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level2_abs_abs_complete1("link2/link1_abs/target_abs", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_abs_abs_complete2("link2/link1_abs/target_abs/", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_abs_abs_complete3("link2/link1_abs/target_abs//", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_abs_abs_partial("link2/link1_abs/target_abs/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level2_abs_abs_partial_dotdot("link2/link1_abs/target_abs/../target/foo/bar/../baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level2_abs_rel_complete1("link2/link1_abs/target_rel", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_abs_rel_complete2("link2/link1_abs/target_rel/", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_abs_rel_complete3("link2/link1_abs/target_rel//", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_abs_rel_partial("link2/link1_abs/target_rel/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level2_abs_rel_partial_dotdot("link2/link1_abs/target_rel/../target/foo/bar/../baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level2_abs_open_complete1("link2/link1_abs/../target", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_abs_open_complete2("link2/link1_abs/../target/", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_abs_open_complete3("link2/link1_abs/../target//", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_abs_open_partial("link2/link1_abs/../target/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level2_abs_open_partial_dotdot("link2/link1_abs/../target/../target/foo/bar/../baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level2_rel_abs_complete1("link2/link1_rel/target_abs", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_rel_abs_complete2("link2/link1_rel/target_abs/", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_rel_abs_complete3("link2/link1_rel/target_abs//", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_rel_abs_partial("link2/link1_rel/target_abs/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level2_rel_abs_partial_dotdot("link2/link1_rel/target_abs/../target/foo/bar/../baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level2_rel_rel_complete1("link2/link1_rel/target_rel", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_rel_rel_complete2("link2/link1_rel/target_rel/", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_rel_rel_complete3("link2/link1_rel/target_rel//", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_rel_rel_partial("link2/link1_rel/target_rel/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level2_rel_rel_partial_dotdot("link2/link1_rel/target_rel/../target/foo/bar/../baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level2_rel_open_complete1("link2/link1_rel/../target", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_rel_open_complete2("link2/link1_rel/../target/", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_rel_open_complete3("link2/link1_rel/../target//", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level2_rel_open_partial("link2/link1_rel/../target/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level2_rel_open_partial_dotdot("link2/link1_rel/../target/../target/foo/bar/../baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level3_abs_complete1("link3/target_abs", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level3_abs_complete2("link3/target_abs/", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level3_abs_complete3("link3/target_abs//", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level3_abs_partial("link3/target_abs/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level3_abs_partial_dotdot("link3/target_abs/../target/foo/bar/../baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level3_rel_complete("link3/target_rel", ResolverFlags::empty()) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; - nonlexical_level3_rel_partial("link3/target_rel/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - nonlexical_level3_rel_partial_dotdot("link3/target_rel/../target/foo/bar/../baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); + nonlexical_basic_complete("target", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_basic_complete1("target/", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_basic_complete2("target//", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_basic_partial("target/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_basic_partial_dotdot("target/../target/foo/bar/../baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level1_abs_complete1("link1/target_abs", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level1_abs_complete2("link1/target_abs/", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level1_abs_complete3("link1/target_abs//", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level1_abs_partial("link1/target_abs/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level1_abs_partial_dotdot("link1/target_abs/../target/foo/bar/../baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level1_rel_complete1("link1/target_rel", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level1_rel_complete2("link1/target_rel/", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level1_rel_complete3("link1/target_rel//", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level1_rel_partial("link1/target_rel/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level1_rel_partial_dotdot("link1/target_rel/../target/foo/bar/../baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level2_abs_abs_complete1("link2/link1_abs/target_abs", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_abs_abs_complete2("link2/link1_abs/target_abs/", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_abs_abs_complete3("link2/link1_abs/target_abs//", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_abs_abs_partial("link2/link1_abs/target_abs/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level2_abs_abs_partial_dotdot("link2/link1_abs/target_abs/../target/foo/bar/../baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level2_abs_rel_complete1("link2/link1_abs/target_rel", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_abs_rel_complete2("link2/link1_abs/target_rel/", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_abs_rel_complete3("link2/link1_abs/target_rel//", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_abs_rel_partial("link2/link1_abs/target_rel/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level2_abs_rel_partial_dotdot("link2/link1_abs/target_rel/../target/foo/bar/../baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level2_abs_open_complete1("link2/link1_abs/../target", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_abs_open_complete2("link2/link1_abs/../target/", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_abs_open_complete3("link2/link1_abs/../target//", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_abs_open_partial("link2/link1_abs/../target/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level2_abs_open_partial_dotdot("link2/link1_abs/../target/../target/foo/bar/../baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level2_rel_abs_complete1("link2/link1_rel/target_abs", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_rel_abs_complete2("link2/link1_rel/target_abs/", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_rel_abs_complete3("link2/link1_rel/target_abs//", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_rel_abs_partial("link2/link1_rel/target_abs/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level2_rel_abs_partial_dotdot("link2/link1_rel/target_abs/../target/foo/bar/../baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level2_rel_rel_complete1("link2/link1_rel/target_rel", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_rel_rel_complete2("link2/link1_rel/target_rel/", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_rel_rel_complete3("link2/link1_rel/target_rel//", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_rel_rel_partial("link2/link1_rel/target_rel/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level2_rel_rel_partial_dotdot("link2/link1_rel/target_rel/../target/foo/bar/../baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level2_rel_open_complete1("link2/link1_rel/../target", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_rel_open_complete2("link2/link1_rel/../target/", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_rel_open_complete3("link2/link1_rel/../target//", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level2_rel_open_partial("link2/link1_rel/../target/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level2_rel_open_partial_dotdot("link2/link1_rel/../target/../target/foo/bar/../baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level3_abs_complete1("link3/target_abs", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level3_abs_complete2("link3/target_abs/", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level3_abs_complete3("link3/target_abs//", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level3_abs_partial("link3/target_abs/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level3_abs_partial_dotdot("link3/target_abs/../target/foo/bar/../baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level3_rel_complete("link3/target_rel", ResolverFlags::empty(), false) => ExpectedResult::Ok { real_path: "/target", file_type: libc::S_IFDIR }; + nonlexical_level3_rel_partial("link3/target_rel/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + nonlexical_level3_rel_partial_dotdot("link3/target_rel/../target/foo/bar/../baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); // Partial lookups due to hitting a non_directory. - partial_nondir_slash1("b/c/file/", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_nondir_slash2("b/c/file//", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_nondir_dot("b/c/file/.", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_nondir_dotdot1("b/c/file/..", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_nondir_dotdot2("b/c/file/../foo/bar", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_nondir_symlink_slash1("b-file/", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_nondir_symlink_slash2("b-file//", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_nondir_symlink_dot("b-file/.", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_nondir_symlink_dotdot1("b-file/..", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_nondir_symlink_dotdot2("b-file/../foo/bar", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_fifo_slash1("b/fifo/", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_fifo_slash2("b/fifo//", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_fifo_dot("b/fifo/.", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_fifo_dotdot1("b/fifo/..", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_fifo_dotdot2("b/fifo/../foo/bar", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_sock_slash1("b/sock/", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_sock_slash2("b/sock//", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_sock_dot("b/sock/.", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_sock_dotdot1("b/sock/..", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - partial_sock_dotdot2("b/sock/../foo/bar", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); + partial_nondir_slash1("b/c/file/", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_nondir_slash2("b/c/file//", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_nondir_dot("b/c/file/.", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_nondir_dotdot1("b/c/file/..", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_nondir_dotdot2("b/c/file/../foo/bar", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_nondir_symlink_slash1("b-file/", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_nondir_symlink_slash2("b-file//", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_nondir_symlink_dot("b-file/.", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_nondir_symlink_dotdot1("b-file/..", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_nondir_symlink_dotdot2("b-file/../foo/bar", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_fifo_slash1("b/fifo/", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_fifo_slash2("b/fifo//", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_fifo_dot("b/fifo/.", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_fifo_dotdot1("b/fifo/..", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_fifo_dotdot2("b/fifo/../foo/bar", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_sock_slash1("b/sock/", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_sock_slash2("b/sock//", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_sock_dot("b/sock/.", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_sock_dotdot1("b/sock/..", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + partial_sock_dotdot2("b/sock/../foo/bar", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); // Dangling symlinks are treated as though they are non_existent. - dangling1_inroot_trailing("a-fake1", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling1_inroot_partial("a-fake1/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling1_inroot_partial_dotdot("a-fake1/../bar/baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling1_sub_trailing("c/a-fake1", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling1_sub_partial("c/a-fake1/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling1_sub_partial_dotdot("c/a-fake1/../bar/baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling2_inroot_trailing("a-fake2", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling2_inroot_partial("a-fake2/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling2_inroot_partial_dotdot("a-fake2/../bar/baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling2_sub_trailing("c/a-fake2", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling2_sub_partial("c/a-fake2/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling2_sub_partial_dotdot("c/a-fake2/../bar/baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling3_inroot_trailing("a-fake3", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling3_inroot_partial("a-fake3/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling3_inroot_partial_dotdot("a-fake3/../bar/baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling3_sub_trailing("c/a-fake3", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling3_sub_partial("c/a-fake3/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling3_sub_partial_dotdot("c/a-fake3/../bar/baz", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); + dangling1_inroot_trailing("a-fake1", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling1_inroot_partial("a-fake1/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling1_inroot_partial_dotdot("a-fake1/../bar/baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling1_sub_trailing("c/a-fake1", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling1_sub_partial("c/a-fake1/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling1_sub_partial_dotdot("c/a-fake1/../bar/baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling2_inroot_trailing("a-fake2", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling2_inroot_partial("a-fake2/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling2_inroot_partial_dotdot("a-fake2/../bar/baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling2_sub_trailing("c/a-fake2", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling2_sub_partial("c/a-fake2/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling2_sub_partial_dotdot("c/a-fake2/../bar/baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling3_inroot_trailing("a-fake3", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling3_inroot_partial("a-fake3/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling3_inroot_partial_dotdot("a-fake3/../bar/baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling3_sub_trailing("c/a-fake3", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling3_sub_partial("c/a-fake3/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling3_sub_partial_dotdot("c/a-fake3/../bar/baz", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); // Tricky dangling symlinks. - dangling_tricky1_trailing("link3/deep_dangling1", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling_tricky1_partial("link3/deep_dangling1/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling_tricky1_partial_dotdot("link3/deep_dangling1/..", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling_tricky2_trailing("link3/deep_dangling2", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling_tricky2_partial("link3/deep_dangling2/foo", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - dangling_tricky2_partial_dotdot("link3/deep_dangling2/..", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); + dangling_tricky1_trailing("link3/deep_dangling1", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling_tricky1_partial("link3/deep_dangling1/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling_tricky1_partial_dotdot("link3/deep_dangling1/..", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling_tricky2_trailing("link3/deep_dangling2", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling_tricky2_partial("link3/deep_dangling2/foo", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + dangling_tricky2_partial_dotdot("link3/deep_dangling2/..", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); // Really deep dangling links. - deep_dangling1("dangling/a", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - deep_dangling2("dangling/b/c", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - deep_dangling3("dangling/c", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - deep_dangling4("dangling/d/e", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - deep_dangling5("dangling/e", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - deep_dangling6("dangling/g", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOENT); - deep_dangling_fileasdir1("dangling-file/a", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - deep_dangling_fileasdir2("dangling-file/b/c", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - deep_dangling_fileasdir3("dangling-file/c", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - deep_dangling_fileasdir4("dangling-file/d/e", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - deep_dangling_fileasdir5("dangling-file/e", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); - deep_dangling_fileasdir6("dangling-file/g", ResolverFlags::empty()) => ExpectedResult::Err(libc::ENOTDIR); + deep_dangling1("dangling/a", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + deep_dangling2("dangling/b/c", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + deep_dangling3("dangling/c", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + deep_dangling4("dangling/d/e", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + deep_dangling5("dangling/e", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + deep_dangling6("dangling/g", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOENT); + deep_dangling_fileasdir1("dangling-file/a", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + deep_dangling_fileasdir2("dangling-file/b/c", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + deep_dangling_fileasdir3("dangling-file/c", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + deep_dangling_fileasdir4("dangling-file/d/e", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + deep_dangling_fileasdir5("dangling-file/e", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); + deep_dangling_fileasdir6("dangling-file/g", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ENOTDIR); // Symlink loops. - loop1("loop/link", ResolverFlags::empty()) => ExpectedResult::Err(libc::ELOOP); - loop_basic1("loop/basic-loop1", ResolverFlags::empty()) => ExpectedResult::Err(libc::ELOOP); - loop_basic2("loop/basic-loop2", ResolverFlags::empty()) => ExpectedResult::Err(libc::ELOOP); - loop_basic3("loop/basic-loop3", ResolverFlags::empty()) => ExpectedResult::Err(libc::ELOOP); + loop1("loop/link", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ELOOP); + loop_basic1("loop/basic-loop1", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ELOOP); + loop_basic2("loop/basic-loop2", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ELOOP); + loop_basic3("loop/basic-loop3", ResolverFlags::empty(), false) => ExpectedResult::Err(libc::ELOOP); // NO_FOLLOW. - symlink_nofollow("link3/target_abs", ResolverFlags::NO_FOLLOW_TRAILING) => ExpectedResult::Ok { real_path: "link3/target_abs", file_type: libc::S_IFLNK }; - symlink_component_nofollow1("e/f", ResolverFlags::NO_FOLLOW_TRAILING) => ExpectedResult::Ok { real_path: "b/c/d/e/f", file_type: libc::S_IFDIR }; - symlink_component_nofollow2("link2/link1_abs/target_rel", ResolverFlags::NO_FOLLOW_TRAILING) => ExpectedResult::Ok { real_path: "link1/target_rel", file_type: libc::S_IFLNK }; - loop_nofollow("loop/link", ResolverFlags::NO_FOLLOW_TRAILING) => ExpectedResult::Ok { real_path: "loop/link", file_type: libc::S_IFLNK }; + symlink_nofollow("link3/target_abs", ResolverFlags::empty(), true) => ExpectedResult::Ok { real_path: "link3/target_abs", file_type: libc::S_IFLNK }; + symlink_component_nofollow1("e/f", ResolverFlags::empty(), true) => ExpectedResult::Ok { real_path: "b/c/d/e/f", file_type: libc::S_IFDIR }; + symlink_component_nofollow2("link2/link1_abs/target_rel", ResolverFlags::empty(), true) => ExpectedResult::Ok { real_path: "link1/target_rel", file_type: libc::S_IFLNK }; + loop_nofollow("loop/link", ResolverFlags::empty(), true) => ExpectedResult::Ok { real_path: "loop/link", file_type: libc::S_IFLNK }; // RESOLVE_NO_SYMLINKS. - dir_nosym("b/c/d/e", ResolverFlags::NO_SYMLINKS) => ExpectedResult::Ok { real_path: "b/c/d/e", file_type: libc::S_IFDIR }; - symlink_nosym("link3/target_abs", ResolverFlags::NO_SYMLINKS) => ExpectedResult::Err(libc::ELOOP); - symlink_component_nosym1("e/f", ResolverFlags::NO_SYMLINKS) => ExpectedResult::Err(libc::ELOOP); - symlink_component_nosym2("link2/link1_abs/target_rel", ResolverFlags::NO_SYMLINKS) => ExpectedResult::Err(libc::ELOOP); - loop_nosym("loop/link", ResolverFlags::NO_SYMLINKS) => ExpectedResult::Err(libc::ELOOP); + dir_nosym("b/c/d/e", ResolverFlags::NO_SYMLINKS, false) => ExpectedResult::Ok { real_path: "b/c/d/e", file_type: libc::S_IFDIR }; + symlink_nosym("link3/target_abs", ResolverFlags::NO_SYMLINKS, false) => ExpectedResult::Err(libc::ELOOP); + symlink_component_nosym1("e/f", ResolverFlags::NO_SYMLINKS, false) => ExpectedResult::Err(libc::ELOOP); + symlink_component_nosym2("link2/link1_abs/target_rel", ResolverFlags::NO_SYMLINKS, false) => ExpectedResult::Err(libc::ELOOP); + loop_nosym("loop/link", ResolverFlags::NO_SYMLINKS, false) => ExpectedResult::Err(libc::ELOOP); } } mod utils { use std::{fs::File, io, os::linux::fs::MetadataExt, path::Path}; - use crate::{error::Error as PathrsError, utils::RawFdExt, Handle, OpenFlags, Root}; + use crate::{error::Error as PathrsError, flags::OpenFlags, utils::RawFdExt, Handle, Root}; use anyhow::Error; use errno::Errno; @@ -509,6 +516,7 @@ mod utils { pub(super) fn check_resolve_in_root

( root: &Root, unsafe_path: P, + no_follow_trailing: bool, expected: &ExpectedResult, reopen_tests: bool, ) -> Result<(), Error> @@ -516,7 +524,13 @@ mod utils { P: AsRef, { check_resolve( - |root: &Root, unsafe_path: P| root.resolve(unsafe_path).map_err(Into::into), + |root: &Root, unsafe_path: P| { + if no_follow_trailing { + root.resolve_nofollow(unsafe_path).map_err(Into::into) + } else { + root.resolve(unsafe_path).map_err(Into::into) + } + }, root, root.as_file().as_unsafe_path_unchecked()?, unsafe_path, diff --git a/src/tests/test_root_ops.rs b/src/tests/test_root_ops.rs index bb254d57..09e9e0df 100644 --- a/src/tests/test_root_ops.rs +++ b/src/tests/test_root_ops.rs @@ -20,7 +20,7 @@ use crate::{ error::ErrorKind, flags::{OpenFlags, RenameFlags}, tests::common as tests_common, - InodeType, ResolverBackend, ResolverFlags, Root, + InodeType, ResolverBackend, Root, }; use std::{fs::Permissions, os::unix::fs::PermissionsExt}; @@ -34,8 +34,7 @@ macro_rules! root_op_tests { #[test] fn []() -> Result<(), Error> { let root_dir = tests_common::create_basic_tree()?; - let mut $root_var = Root::open(&root_dir)?; - $root_var.resolver.flags = ResolverFlags::NO_FOLLOW_TRAILING; + let $root_var = Root::open(&root_dir)?; $body } @@ -45,7 +44,6 @@ macro_rules! root_op_tests { fn []() -> Result<(), Error> { let root_dir = tests_common::create_basic_tree()?; let mut $root_var = Root::open(&root_dir)?; - $root_var.resolver.flags = ResolverFlags::NO_FOLLOW_TRAILING; $root_var.resolver.backend = ResolverBackend::KernelOpenat2; $body @@ -56,7 +54,6 @@ macro_rules! root_op_tests { fn []() -> Result<(), Error> { let root_dir = tests_common::create_basic_tree()?; let mut $root_var = Root::open(&root_dir)?; - $root_var.resolver.flags = ResolverFlags::NO_FOLLOW_TRAILING; $root_var.resolver.backend = ResolverBackend::EmulatedOpath; $body @@ -281,7 +278,7 @@ mod utils { Err(err) => assert_eq!(Err(err.kind()), expected_result, "unexpected error {err:?}",), Ok(_) => { let root = root_roundtrip(root)?; - let created = root.resolve(path)?; + let created = root.resolve_nofollow(path)?; let meta = created.as_file().metadata()?; let actual_path = created.as_file().as_unsafe_path_unchecked()?; @@ -301,7 +298,7 @@ mod utils { } // Check hardlink is the same inode. InodeType::Hardlink(target) => { - let target_meta = root.resolve(target)?.as_file().metadata()?; + let target_meta = root.resolve_nofollow(target)?.as_file().metadata()?; assert_eq!( meta.ino(), target_meta.ino(), @@ -310,12 +307,19 @@ mod utils { } // Check symlink is correct. InodeType::Symlink(target) => { + // Check using the a resolved handle. let actual_target = syscalls::readlinkat(created.as_file().as_raw_fd(), "")?; assert_eq!( target, actual_target, "readlinkat(handle) link target mismatch" ); + // Double-check with Root::readlink. + let actual_target = root.readlink(path)?; + assert_eq!( + target, actual_target, + "root.readlink(path) link target mismatch" + ); } } } @@ -336,7 +340,7 @@ mod utils { let _ = unsafe { libc::umask(0) }; // Get a handle to the original path if it existed beforehand. - let pre_create_handle = root.resolve(path); // do not unwrap + let pre_create_handle = root.resolve_nofollow(path); // do not unwrap // Update the expected path to have the rootdir as a prefix. let root_dir = root.as_file().as_unsafe_path_unchecked()?; @@ -354,7 +358,7 @@ mod utils { let root = root_roundtrip(root)?; let new_lookup = root - .resolve(path) + .resolve_nofollow(path) .wrap("re-open created file using original path")?; assert_eq!( @@ -399,7 +403,7 @@ mod utils { // Get a handle before we remove the path, to make sure the actual inode // was unlinked. - let handle = root.resolve(path); // do not unwrap + let handle = root.resolve_nofollow(path); // do not unwrap let res = root.remove(path); assert_eq!( @@ -415,7 +419,7 @@ mod utils { assert_eq!(meta.nlink(), 0, "deleted file should have a 0 nlink"); let root = root_roundtrip(root)?; - let new_lookup = root.resolve(path); + let new_lookup = root.resolve_nofollow(path); assert_eq!( new_lookup.as_ref().map_err(PathrsError::kind).err(), Some(ErrorKind::OsError(Some(libc::ENOENT))), @@ -437,8 +441,8 @@ mod utils { // Get a handle before we move the paths, to make sure the right inodes // were moved. - let src_handle = root.resolve(src_path)?; - let dst_handle = root.resolve(dst_path); // do not unwrap this here! + let src_handle = root.resolve_nofollow(src_path)?; + let dst_handle = root.resolve_nofollow(dst_path); // do not unwrap this here! // Keep track of the original paths, pre-rename. let src_real_path = src_handle.as_file().as_unsafe_path_unchecked()?; @@ -487,7 +491,7 @@ mod utils { // Verify that there is a whiteout entry where the soure // used to be. let new_lookup = root - .resolve(src_path) + .resolve_nofollow(src_path) .wrap("expected source to exist with RENAME_WHITEOUT")?; let meta = new_lookup.as_file().metadata()?; diff --git a/src/utils.rs b/src/utils.rs index 0c1c7177..aa2f0220 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -20,8 +20,9 @@ use crate::{ error::{self, Error}, + flags::OpenFlags, procfs::{ProcfsBase, ProcfsHandle}, - syscalls, OpenFlags, + syscalls, }; use std::{