use std::cell::Cell;
use std::ffi::{CStr, CString, OsStr, OsString};
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Component, Path, PathBuf};
const RESOLVE_NO_MAGICLINKS: u64 = 0x02;
const RESOLVE_BENEATH: u64 = 0x08;
#[repr(C)]
struct OpenHow {
flags: u64,
mode: u64,
resolve: u64,
}
pub(crate) fn cstr(s: &OsStr) -> io::Result<CString> {
CString::new(s.as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "a NUL in a name"))
}
pub(crate) fn cvt(r: libc::c_int) -> io::Result<libc::c_int> {
if r < 0 {
Err(io::Error::last_os_error())
} else {
Ok(r)
}
}
pub(crate) fn escape() -> io::Error {
io::Error::from_raw_os_error(libc::EXDEV)
}
pub(crate) fn is_escape(e: &io::Error) -> bool {
e.raw_os_error() == Some(libc::EXDEV)
}
pub(crate) fn openat(
dir: RawFd,
name: &CStr,
flags: libc::c_int,
mode: libc::mode_t,
) -> io::Result<OwnedFd> {
let fd = cvt(unsafe {
libc::openat(
dir,
name.as_ptr(),
flags | libc::O_CLOEXEC,
mode as libc::c_uint,
)
})?;
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
fn openat2(dir: RawFd, path: &CStr, flags: libc::c_int, resolve: u64) -> io::Result<OwnedFd> {
let how = OpenHow {
flags: (flags | libc::O_CLOEXEC) as u64,
mode: 0,
resolve,
};
let r = unsafe {
libc::syscall(
libc::SYS_openat2,
dir,
path.as_ptr(),
&how as *const OpenHow,
std::mem::size_of::<OpenHow>(),
)
};
if r < 0 {
return Err(io::Error::last_os_error());
}
Ok(unsafe { OwnedFd::from_raw_fd(r as RawFd) })
}
pub(crate) fn fstatat(dir: RawFd, name: &CStr) -> io::Result<Option<libc::stat>> {
let mut st: libc::stat = unsafe { std::mem::zeroed() };
match cvt(unsafe { libc::fstatat(dir, name.as_ptr(), &mut st, libc::AT_SYMLINK_NOFOLLOW) }) {
Ok(_) => Ok(Some(st)),
Err(e) if e.raw_os_error() == Some(libc::ENOENT) => Ok(None),
Err(e) => Err(e),
}
}
pub(crate) fn fstat(fd: RawFd) -> io::Result<libc::stat> {
let mut st: libc::stat = unsafe { std::mem::zeroed() };
cvt(unsafe { libc::fstat(fd, &mut st) })?;
Ok(st)
}
pub(crate) fn is(st: &libc::stat, kind: libc::mode_t) -> bool {
st.st_mode & libc::S_IFMT == kind
}
fn readlinkat(dir: RawFd, name: &CStr) -> io::Result<PathBuf> {
let mut buf = vec![0u8; libc::PATH_MAX as usize];
let n = unsafe { libc::readlinkat(dir, name.as_ptr(), buf.as_mut_ptr().cast(), buf.len()) };
if n < 0 {
return Err(io::Error::last_os_error());
}
buf.truncate(n as usize);
Ok(PathBuf::from(OsString::from_vec(buf)))
}
pub(crate) fn list(dir: RawFd) -> io::Result<Vec<CString>> {
let d = openat(dir, c".", libc::O_RDONLY | libc::O_DIRECTORY, 0)?;
let raw = d.as_raw_fd();
let dp = unsafe { libc::fdopendir(raw) };
if dp.is_null() {
return Err(io::Error::last_os_error());
}
std::mem::forget(d);
let mut out = Vec::new();
loop {
let ent = unsafe { libc::readdir(dp) };
if ent.is_null() {
break;
}
let name = unsafe { CStr::from_ptr((*ent).d_name.as_ptr()) };
if name.to_bytes() != b"." && name.to_bytes() != b".." {
out.push(name.to_owned());
}
}
unsafe { libc::closedir(dp) };
Ok(out)
}
pub(crate) fn remove_at(dir: RawFd, name: &CStr) -> io::Result<()> {
let Some(st) = fstatat(dir, name)? else {
return Ok(());
};
if is(&st, libc::S_IFDIR) {
let d = openat(
dir,
name,
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_NOFOLLOW,
0,
)?;
for child in list(d.as_raw_fd())? {
remove_at(d.as_raw_fd(), &child)?;
}
cvt(unsafe { libc::unlinkat(dir, name.as_ptr(), libc::AT_REMOVEDIR) })?;
} else {
cvt(unsafe { libc::unlinkat(dir, name.as_ptr(), 0) })?;
}
Ok(())
}
pub(crate) struct Root {
fd: OwnedFd,
openat2: Cell<bool>,
}
impl Root {
pub(crate) fn open(path: &Path) -> io::Result<Root> {
let c = cstr(path.as_os_str())?;
let fd = openat(libc::AT_FDCWD, &c, libc::O_PATH | libc::O_DIRECTORY, 0)?;
Ok(Root {
fd,
openat2: Cell::new(true),
})
}
#[cfg(test)]
fn without_openat2(self) -> Self {
self.openat2.set(false);
self
}
fn dup(&self) -> io::Result<OwnedFd> {
self.fd.try_clone()
}
pub(crate) fn dir(&self, rel: &Path, create: bool) -> io::Result<OwnedFd> {
if rel.as_os_str().is_empty() {
return self.dup();
}
if self.openat2.get() {
let c = cstr(rel.as_os_str())?;
match openat2(
self.fd.as_raw_fd(),
&c,
libc::O_PATH | libc::O_DIRECTORY,
RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS,
) {
Ok(fd) => return Ok(fd),
Err(e)
if matches!(
e.raw_os_error(),
Some(libc::ENOSYS | libc::EPERM | libc::E2BIG)
) =>
{
self.openat2.set(false)
}
Err(e) if e.raw_os_error() == Some(libc::ENOENT) && create => {}
Err(e) => return Err(e),
}
}
self.walk(rel, create)
}
fn walk(&self, rel: &Path, create: bool) -> io::Result<OwnedFd> {
let mut stack: Vec<OwnedFd> = Vec::new();
let mut todo: Vec<OsString> = Vec::new();
push_components(&mut todo, rel)?;
let mut hops = 0;
while let Some(c) = todo.pop() {
if c == ".." {
if stack.pop().is_none() {
return Err(escape());
}
continue;
}
let cur = stack.last().map_or(self.fd.as_raw_fd(), |f| f.as_raw_fd());
let name = cstr(&c)?;
match openat(cur, &name, libc::O_PATH | libc::O_NOFOLLOW, 0) {
Ok(fd) => {
let st = fstat(fd.as_raw_fd())?;
if is(&st, libc::S_IFDIR) {
stack.push(fd);
} else if is(&st, libc::S_IFLNK) {
hops += 1;
if hops > 40 {
return Err(io::Error::from_raw_os_error(libc::ELOOP));
}
let target = readlinkat(cur, &name)?;
if target.is_absolute() {
return Err(escape());
}
push_components(&mut todo, &target)?;
} else {
return Err(io::Error::from_raw_os_error(libc::ENOTDIR));
}
}
Err(e) if e.raw_os_error() == Some(libc::ENOENT) && create => {
match cvt(unsafe { libc::mkdirat(cur, name.as_ptr(), 0o755) }) {
Ok(_) => {}
Err(e) if e.raw_os_error() == Some(libc::EEXIST) => {}
Err(e) => return Err(e),
}
let fd = openat(
cur,
&name,
libc::O_PATH | libc::O_NOFOLLOW | libc::O_DIRECTORY,
0,
)?;
stack.push(fd);
}
Err(e) => return Err(e),
}
}
match stack.pop() {
Some(f) => Ok(f),
None => self.dup(),
}
}
}
fn push_components(todo: &mut Vec<OsString>, p: &Path) -> io::Result<()> {
let mut v: Vec<OsString> = Vec::new();
for c in p.components() {
match c {
Component::Normal(n) => v.push(n.to_os_string()),
Component::ParentDir => v.push(OsString::from("..")),
Component::CurDir | Component::RootDir => {}
Component::Prefix(_) => return Err(escape()),
}
}
todo.extend(v.into_iter().rev());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_component_walk_refuses_what_leaves_the_root_like_openat2_does() {
let base = tempfile::tempdir().unwrap();
let root = base.path().join("r");
std::fs::create_dir_all(root.join("usr/lib")).unwrap();
std::os::unix::fs::symlink("usr/lib", root.join("lib")).unwrap();
std::os::unix::fs::symlink("/usr/lib", root.join("abs")).unwrap();
std::os::unix::fs::symlink("../..", root.join("up")).unwrap();
for fast in [true, false] {
let r = Root::open(&root).unwrap();
let r = if fast { r } else { r.without_openat2() };
assert!(r.dir(Path::new("lib"), false).is_ok(), "fast={fast}");
assert!(r.dir(Path::new("usr/../lib"), false).is_ok(), "fast={fast}");
for bad in ["abs", "up", "..", "usr/../.."] {
let e = r.dir(Path::new(bad), true).unwrap_err();
assert!(is_escape(&e), "{bad} fast={fast}: {e}");
}
r.dir(Path::new("lib/new/deeper"), true).unwrap();
assert!(root.join("usr/lib/new/deeper").is_dir());
std::fs::remove_dir_all(root.join("usr/lib/new")).unwrap();
}
assert!(!base.path().join("usr").exists());
}
}