use crate::{
error::{self, Error, ErrorExt},
resolvers::ResolverFlags,
syscalls,
utils::{FileExt, RawFdExt},
Handle,
};
use std::{
collections::VecDeque,
fs::File,
io::Error as IOError,
os::unix::{ffi::OsStrExt, io::AsRawFd},
path::{Component, Path, PathBuf},
};
use snafu::ResultExt;
const MAX_SYMLINK_TRAVERSALS: usize = 128;
fn check_current<P: AsRef<Path>>(current: &File, root: &File, expected: P) -> Result<(), Error> {
let root_path = root
.as_unsafe_path()
.wrap("get root path to construct expected path")?;
let full_path: PathBuf = root_path.join(
expected
.as_ref()
.components()
.filter(|c| match c {
Component::Normal(_) => true,
_ => false,
})
.collect::<PathBuf>(),
);
let current_path = current
.as_unsafe_path()
.wrap("check fd against expected path")?;
ensure!(
current_path == full_path,
error::SafetyViolation {
description: "fd doesn't match expected path"
}
);
let new_root_path = root
.as_unsafe_path()
.wrap("get root path to double-check it hasn't moved")?;
ensure!(
root_path == new_root_path,
error::SafetyViolation {
description: "root moved during lookup"
}
);
Ok(())
}
pub(crate) fn resolve<P: AsRef<Path>>(
root: &File,
path: P,
flags: ResolverFlags,
) -> Result<Handle, Error> {
let path = path.as_ref();
let mut expected_path = PathBuf::from(Component::RootDir.as_os_str());
let mut current = root.try_clone_hotfix().wrap("dup root as starting point")?;
let mut components: VecDeque<_> = path
.components()
.map(|p| PathBuf::from(p.as_os_str()))
.collect();
let mut symlink_traversals = 0;
while let Some(part) = components.pop_front() {
let part = part
.components()
.next()
.expect("components should have one entry");
match part {
Component::Normal(part) => {
expected_path.push(part);
ensure!(
!part.as_bytes().contains(&b'/'),
error::SafetyViolation {
description: "component of path resolution contains '/'",
}
);
}
Component::ParentDir => {
if !expected_path.pop() {
continue;
}
}
_ => continue,
};
let next = syscalls::openat(current.as_raw_fd(), part, libc::O_PATH, 0).context(
error::RawOsError {
operation: "open next component of resolution",
},
)?;
if part == Component::ParentDir {
check_current(&next, root, &expected_path)
.wrap("check next '..' component didn't escape")?;
}
let next_type = next
.metadata()
.context(error::OsError {
operation: "fstat of next component",
})?
.file_type();
if !next_type.is_symlink() {
current = next;
continue;
}
if flags.contains(ResolverFlags::NO_SYMLINKS) {
return error::SafetyViolation {
description: "next is a symlink and symlink resolution disabled",
}
.fail();
}
if next
.is_dangerous()
.wrap("check if next is on a dangerous filesystem")?
{
return error::SafetyViolation {
description: "next is a symlink on a dangerous filesystem",
}
.fail();
}
symlink_traversals += 1;
if symlink_traversals >= MAX_SYMLINK_TRAVERSALS {
return Err(IOError::from_raw_os_error(libc::ELOOP)).context(error::OsError {
operation: "emulated symlink resolution",
})?;
}
let contents =
syscalls::readlinkat(current.as_raw_fd(), part).context(error::RawOsError {
operation: "readlink next symlink component",
})?;
contents
.components()
.map(|p| PathBuf::from(p.as_os_str()))
.rev()
.for_each(|p| components.push_front(p));
expected_path.pop();
if contents.is_absolute() {
current = root.try_clone_hotfix().wrap("dup root as next current")?;
}
}
check_current(¤t, root, &expected_path).wrap("check final handle didn't escape")?;
Ok(Handle::from_file_unchecked(current))
}