use std::ffi::{CStr, CString};
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::os::unix::ffi::OsStrExt as _;
use std::path::Path;
pub(super) enum Node<'a> {
Directory {
fd: &'a OwnedFd,
stat: &'a libc::stat,
},
Entry {
parent: RawFd,
name: &'a CStr,
stat: &'a libc::stat,
},
}
impl Node<'_> {
pub(super) const fn stat(&self) -> &libc::stat {
match self {
Self::Directory { stat, .. } | Self::Entry { stat, .. } => stat,
}
}
pub(super) fn chown(&self, uid: u32, gid: u32) -> io::Result<()> {
check(unsafe {
match self {
Self::Directory { fd, .. } => libc::fchown(fd.as_raw_fd(), uid, gid),
Self::Entry { parent, name, .. } => {
libc::fchownat(*parent, name.as_ptr(), uid, gid, libc::AT_SYMLINK_NOFOLLOW)
}
}
})
}
pub(super) fn chmod(&self, mode: u32) -> io::Result<()> {
if is_link(self.stat()) {
return Ok(());
}
let mode = libc::mode_t::try_from(mode).map_err(|_| io::ErrorKind::InvalidInput)?;
let status = unsafe {
match self {
Self::Directory { fd, .. } => libc::fchmod(fd.as_raw_fd(), mode),
Self::Entry { parent, name, .. } => {
libc::fchmodat(*parent, name.as_ptr(), mode, libc::AT_SYMLINK_NOFOLLOW)
}
}
};
match check(status) {
Err(error) if error.raw_os_error() == Some(libc::EOPNOTSUPP) => Ok(()),
other => other,
}
}
}
pub(super) fn walk(
root: &Path,
visit: &mut dyn FnMut(&Node<'_>) -> io::Result<bool>,
) -> io::Result<()> {
let path = CString::new(root.as_os_str().as_bytes())
.map_err(|_| io::Error::from(io::ErrorKind::InvalidInput))?;
match open_directory(libc::AT_FDCWD, &path) {
Ok(fd) => {
let stat = fstat(&fd)?;
if visit(&Node::Directory {
fd: &fd,
stat: &stat,
})? {
walk_below(&fd, visit)?;
}
Ok(())
}
Err(error) if matches!(error.raw_os_error(), Some(libc::ENOTDIR | libc::ELOOP)) => {
let stat = lstat_at(libc::AT_FDCWD, &path)?;
visit(&Node::Entry {
parent: libc::AT_FDCWD,
name: &path,
stat: &stat,
})
.map(|_| ())
}
Err(error) => Err(error),
}
}
fn walk_below(
directory: &OwnedFd,
visit: &mut dyn FnMut(&Node<'_>) -> io::Result<bool>,
) -> io::Result<()> {
for name in names(directory)? {
let parent = directory.as_raw_fd();
let stat = match lstat_at(parent, &name) {
Ok(stat) => stat,
Err(error) if error.kind() == io::ErrorKind::NotFound => continue,
Err(error) => return Err(error),
};
if stat.st_mode & libc::S_IFMT != libc::S_IFDIR {
visit(&Node::Entry {
parent,
name: &name,
stat: &stat,
})?;
continue;
}
let child = match open_directory(parent, &name) {
Ok(child) => child,
Err(error)
if matches!(
error.raw_os_error(),
Some(libc::ENOENT | libc::ENOTDIR | libc::ELOOP)
) =>
{
continue;
}
Err(error) => return Err(error),
};
let stat = fstat(&child)?;
if visit(&Node::Directory {
fd: &child,
stat: &stat,
})? {
walk_below(&child, visit)?;
}
}
Ok(())
}
pub(super) const fn is_link(stat: &libc::stat) -> bool {
stat.st_mode & libc::S_IFMT == libc::S_IFLNK
}
fn check(status: libc::c_int) -> io::Result<()> {
if status == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
fn open_directory(parent: RawFd, name: &CStr) -> io::Result<OwnedFd> {
let fd = unsafe {
libc::openat(
parent,
name.as_ptr(),
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC,
)
};
if fd < 0 {
Err(io::Error::last_os_error())
} else {
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
}
fn fstat(fd: &OwnedFd) -> io::Result<libc::stat> {
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
check(unsafe { libc::fstat(fd.as_raw_fd(), &mut stat) })?;
Ok(stat)
}
fn lstat_at(parent: RawFd, name: &CStr) -> io::Result<libc::stat> {
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
check(unsafe { libc::fstatat(parent, name.as_ptr(), &mut stat, libc::AT_SYMLINK_NOFOLLOW) })?;
Ok(stat)
}
fn names(directory: &OwnedFd) -> io::Result<Vec<CString>> {
let duplicate = unsafe { libc::fcntl(directory.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 0) };
if duplicate < 0 {
return Err(io::Error::last_os_error());
}
let stream = unsafe { libc::fdopendir(duplicate) };
if stream.is_null() {
let error = io::Error::last_os_error();
unsafe { libc::close(duplicate) };
return Err(error);
}
let mut names = Vec::new();
loop {
let entry = unsafe { libc::readdir(stream) };
if entry.is_null() {
break;
}
let name = unsafe { CStr::from_ptr((*entry).d_name.as_ptr()) };
if !matches!(name.to_bytes(), b"." | b"..") {
names.push(name.to_owned());
}
}
unsafe { libc::closedir(stream) };
Ok(names)
}
#[cfg(target_os = "linux")]
pub(super) fn set_attribute(fd: &OwnedFd, name: &CStr, value: &[u8]) -> io::Result<()> {
check(unsafe {
libc::fsetxattr(
fd.as_raw_fd(),
name.as_ptr(),
value.as_ptr().cast(),
value.len(),
0,
)
})
}
#[cfg(target_os = "linux")]
pub(super) fn attribute(fd: &OwnedFd, name: &CStr, value: &mut [u8]) -> io::Result<usize> {
let length = unsafe {
libc::fgetxattr(
fd.as_raw_fd(),
name.as_ptr(),
value.as_mut_ptr().cast(),
value.len(),
)
};
usize::try_from(length).map_err(|_| io::Error::last_os_error())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_link_swapped_in_anywhere_is_never_followed() {
let root = tempfile::tempdir().unwrap();
let outside = tempfile::tempdir().unwrap();
std::fs::write(outside.path().join("secret"), b"x").unwrap();
let tree = root.path().join("tree");
std::fs::create_dir_all(tree.join("a/b")).unwrap();
std::fs::write(tree.join("a/b/file"), b"x").unwrap();
std::os::unix::fs::symlink(outside.path(), tree.join("a/link")).unwrap();
std::os::unix::fs::symlink(outside.path(), root.path().join("root-link")).unwrap();
let mut seen = Vec::new();
walk(&tree, &mut |node| {
seen.push((matches!(node, Node::Directory { .. }), is_link(node.stat())));
node.chmod(0o750)?;
Ok(true)
})
.unwrap();
assert_eq!(seen.len(), 5);
assert_eq!(seen.iter().filter(|(_, link)| *link).count(), 1);
use std::os::unix::fs::PermissionsExt as _;
let mode = |path: &Path| std::fs::metadata(path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode(&tree.join("a/b/file")), 0o750);
assert_ne!(mode(&outside.path().join("secret")), 0o750);
let mut entered = false;
walk(&root.path().join("root-link"), &mut |node| {
entered |= matches!(node, Node::Directory { .. });
Ok(true)
})
.unwrap();
assert!(!entered);
}
}