use crate::{
error::{Error, ErrorExt, ErrorImpl},
flags::{OpenFlags, ResolverFlags},
procfs::ProcfsHandle,
resolvers::{opath::SymlinkStack, PartialLookup, MAX_SYMLINK_TRAVERSALS},
syscalls,
utils::{self, FdExt, PathIterExt},
Handle,
};
use std::{
collections::VecDeque,
ffi::{OsStr, OsString},
io::Error as IOError,
iter,
os::unix::{
ffi::OsStrExt,
fs::MetadataExt,
io::{AsFd, OwnedFd},
},
path::{Path, PathBuf},
rc::Rc,
};
use itertools::Itertools;
use once_cell::sync::Lazy;
fn check_current(
procfs: &ProcfsHandle,
current: impl AsFd,
root: impl AsFd,
expected: impl AsRef<Path>,
) -> Result<(), Error> {
let root_path = root
.as_unsafe_path(procfs)
.wrap("get root path to construct expected path")?;
let full_path: PathBuf = root_path.join(
iter::once(OsStr::from_bytes(b"."))
.chain(expected.as_ref().raw_components())
.collect::<PathBuf>(),
);
let current_path = current
.as_unsafe_path(procfs)
.wrap("check fd against expected path")?;
if current_path != full_path {
Err(ErrorImpl::SafetyViolation {
description: format!(
"fd doesn't match expected path ({} != {})",
current_path.display(),
full_path.display()
)
.into(),
})?
}
let new_root_path = root
.as_unsafe_path(procfs)
.wrap("get root path to double-check it hasn't moved")?;
if root_path != new_root_path {
Err(ErrorImpl::SafetyViolation {
description: "root moved during lookup".into(),
})?
}
Ok(())
}
static PROTECTED_SYMLINKS_SYSCTL: Lazy<u32> = Lazy::new(|| {
let procfs = ProcfsHandle::new().expect("should be able to get a procfs handle");
utils::sysctl_read_parse(&procfs, "fs.protected_symlinks")
.expect("should be able to parse fs.protected_symlinks")
});
fn may_follow_link(dir: impl AsFd, link: impl AsFd) -> Result<(), Error> {
let link = link.as_fd();
#[allow(non_snake_case)]
let ST_NOSYMFOLLOW = 0x2000;
let link_statfs = syscalls::fstatfs(link).map_err(|err| ErrorImpl::RawOsError {
operation: "fetch mount flags of symlink".into(),
source: err,
})?;
if link_statfs.f_flags & ST_NOSYMFOLLOW == ST_NOSYMFOLLOW {
Err(ErrorImpl::OsError {
operation: "emulated MS_NOSYMFOLLOW".into(),
source: IOError::from_raw_os_error(libc::ELOOP),
})?
}
let fsuid = syscalls::geteuid();
let dir_meta = dir.metadata().wrap("fetch directory metadata")?;
let link_meta = link.metadata().wrap("fetch symlink metadata")?;
const STICKY_WRITABLE: libc::mode_t = libc::S_ISVTX | libc::S_IWOTH;
if *PROTECTED_SYMLINKS_SYSCTL == 0 ||
link_meta.uid() == fsuid ||
dir_meta.mode() & STICKY_WRITABLE != STICKY_WRITABLE ||
link_meta.uid() == dir_meta.uid()
{
Ok(())
} else {
Err(ErrorImpl::OsError {
operation: "emulated fs.protected_symlinks".into(),
source: IOError::from_raw_os_error(libc::EACCES),
}
.into())
}
}
fn do_resolve(
root: impl AsFd,
path: impl AsRef<Path>,
flags: ResolverFlags,
no_follow_trailing: bool,
mut symlink_stack: Option<&mut SymlinkStack<OwnedFd>>,
) -> Result<PartialLookup<Rc<OwnedFd>>, Error> {
let procfs = ProcfsHandle::new()?;
let mut expected_path = PathBuf::from("/");
let root = Rc::new(
root.as_fd()
.try_clone_to_owned()
.map_err(|err| ErrorImpl::OsError {
operation: "dup root handle as starting point of resolution".into(),
source: err,
})?,
);
let mut current = Rc::clone(&root);
let mut remaining_components = path
.raw_components()
.map(|p| p.to_os_string())
.collect::<VecDeque<_>>();
let mut symlink_traversals = 0;
while let Some(part) = remaining_components.pop_front() {
let remaining: PathBuf = Itertools::intersperse(
iter::once(&part)
.chain(remaining_components.iter())
.map(OsString::as_os_str),
OsStr::new("/"),
)
.collect::<OsString>()
.into();
let part = match part.as_bytes() {
b"" => ".".into(),
b"." => part,
b".." => {
if !expected_path.pop() {
if let Some(ref mut stack) = symlink_stack {
stack
.pop_part(&part)
.map_err(|err| ErrorImpl::BadSymlinkStackError {
description: "walking into component".into(),
source: err,
})?;
}
current = Rc::clone(&root);
continue;
}
part
}
_ => {
expected_path.push(&part);
if part.as_bytes().contains(&b'/') {
Err(ErrorImpl::SafetyViolation {
description: "component of path resolution contains '/'".into(),
})?
}
part
}
};
match syscalls::openat(
&*current,
&part,
OpenFlags::O_PATH | OpenFlags::O_NOFOLLOW,
0,
)
.map_err(|err| {
ErrorImpl::RawOsError {
operation: "open next component of resolution".into(),
source: err,
}
.into()
}) {
Err(err) => {
return Ok(PartialLookup::Partial {
handle: current,
remaining,
last_error: err,
});
}
Ok(next) => {
if part.as_bytes() == b".." {
check_current(&procfs, &next, &*root, &expected_path)
.wrap("check next '..' component didn't escape")?;
}
if !next
.metadata()
.wrap("fstat of next component")?
.is_symlink()
{
if let Some(ref mut stack) = symlink_stack {
stack
.pop_part(&part)
.map_err(|err| ErrorImpl::BadSymlinkStackError {
description: "walking into component".into(),
source: err,
})?;
}
current = next.into();
continue;
} else {
if remaining_components.is_empty() && no_follow_trailing {
current = next.into();
break;
}
if flags.contains(ResolverFlags::NO_SYMLINKS) {
return Ok(PartialLookup::Partial {
handle: current,
remaining,
last_error: ErrorImpl::OsError {
operation: "emulated symlink resolution".into(),
source: IOError::from_raw_os_error(libc::ELOOP),
}
.wrap(format!(
"component {part:?} is a symlink but symlink resolution is disabled",
))
.into(),
});
}
if let Err(err) = may_follow_link(&*current, &next) {
return Ok(PartialLookup::Partial {
handle: current,
remaining,
last_error: err
.wrap(format!("component {part:?} is a symlink we cannot follow")),
});
}
symlink_traversals += 1;
if symlink_traversals >= MAX_SYMLINK_TRAVERSALS {
return Ok(PartialLookup::Partial {
handle: current,
remaining,
last_error: ErrorImpl::OsError {
operation: "emulated symlink resolution".into(),
source: IOError::from_raw_os_error(libc::ELOOP),
}
.wrap("exceeded symlink limit")
.into(),
});
}
let link_target =
syscalls::readlinkat(&next, "").map_err(|err| ErrorImpl::RawOsError {
operation: "readlink next symlink component".into(),
source: err,
})?;
if link_target.is_absolute()
&& next
.is_magiclink_filesystem()
.wrap("check if next is on a dangerous filesystem")?
{
Err(ErrorImpl::OsError {
operation: "emulated RESOLVE_NO_MAGICLINKS".into(),
source: IOError::from_raw_os_error(libc::ELOOP),
})
.wrap("walked into a potential magic-link")?
}
if let Some(ref mut stack) = symlink_stack {
stack
.swap_link(&part, (¤t, remaining), link_target.clone())
.map_err(|err| ErrorImpl::BadSymlinkStackError {
description: "walking into symlink".into(),
source: err,
})?;
}
expected_path.pop();
link_target
.raw_components()
.prepend(&mut remaining_components);
if link_target.is_absolute() {
current = Rc::clone(&root);
expected_path = PathBuf::from("/");
}
}
}
}
}
check_current(&procfs, &*current, &*root, &expected_path)
.wrap("check final handle didn't escape")?;
Ok(PartialLookup::Complete(current))
}
pub(crate) fn resolve_partial(
root: impl AsFd,
path: impl AsRef<Path>,
flags: ResolverFlags,
no_follow_trailing: bool,
) -> Result<PartialLookup<Rc<OwnedFd>>, Error> {
let mut symlink_stack = SymlinkStack::new();
match do_resolve(
root,
path,
flags,
no_follow_trailing,
Some(&mut symlink_stack),
) {
ret @ Ok(PartialLookup::Complete(_)) => ret,
err @ Err(_) => err,
Ok(PartialLookup::Partial {
handle,
remaining,
last_error,
}) => match symlink_stack.pop_top_symlink() {
Some((handle, remaining)) => Ok(PartialLookup::Partial {
handle,
remaining,
last_error,
}),
None => Ok(PartialLookup::Partial {
handle,
remaining,
last_error,
}),
},
}
}
pub(crate) fn resolve(
root: impl AsFd,
path: impl AsRef<Path>,
flags: ResolverFlags,
no_follow_trailing: bool,
) -> Result<Handle, Error> {
do_resolve(root, path, flags, no_follow_trailing, None).and_then(TryInto::try_into)
}