#![forbid(unsafe_code)]
use crate::{
error::{Error, ErrorExt, ErrorImpl, ErrorKind},
flags::{OpenFlags, ResolverFlags},
resolvers::procfs::ProcfsResolver,
syscalls,
utils::{self, kernel_version, FdExt, MaybeOwnedFd, RawProcfsRoot},
};
use std::{
fs::File,
io::Error as IOError,
os::unix::{
fs::MetadataExt,
io::{AsFd, BorrowedFd, OwnedFd},
},
path::{Path, PathBuf},
};
use once_cell::sync::{Lazy, OnceCell as OnceLock};
use rustix::{
fs::{self as rustix_fs, Access, AtFlags},
mount::{FsMountFlags, FsOpenFlags, MountAttrFlags, OpenTreeFlags},
};
#[doc(alias = "pathrs_proc_base_t")]
#[derive(Eq, PartialEq, Debug, Clone, Copy)]
#[non_exhaustive]
pub enum ProcfsBase {
ProcRoot,
ProcPid(u32),
ProcSelf,
ProcThreadSelf,
}
impl ProcfsBase {
pub(crate) fn into_path(self, proc_rootfd: RawProcfsRoot<'_>) -> PathBuf {
match self {
Self::ProcRoot => PathBuf::from("."),
Self::ProcSelf => PathBuf::from("self"),
Self::ProcPid(pid) => PathBuf::from(pid.to_string()),
Self::ProcThreadSelf => [
"thread-self".into(),
format!("self/task/{}", syscalls::gettid()).into(),
"self".into(),
]
.into_iter()
.find(|base| proc_rootfd.exists_unchecked(base).is_ok())
.expect("at least one candidate /proc/thread-self path should work"),
}
}
}
#[derive(Clone, Debug)]
pub struct ProcfsHandleBuilder {
subset_pid: bool,
}
impl Default for ProcfsHandleBuilder {
fn default() -> Self {
Self::new()
}
}
impl ProcfsHandleBuilder {
#[inline]
pub fn new() -> Self {
Self { subset_pid: true }
}
#[inline]
pub fn subset_pid(mut self, subset_pid: bool) -> Self {
self.set_subset_pid(subset_pid);
self
}
#[inline]
pub fn set_subset_pid(&mut self, subset_pid: bool) -> &mut Self {
self.subset_pid = subset_pid;
self
}
#[inline]
pub fn unmasked(mut self) -> Self {
self.set_unmasked();
self
}
#[inline]
pub fn set_unmasked(&mut self) -> &mut Self {
self.subset_pid = false;
self
}
#[inline]
fn is_cache_friendly(&self) -> bool {
self.subset_pid
}
pub fn build(self) -> Result<ProcfsHandle, Error> {
static CACHED_PROCFS_HANDLE: OnceLock<OwnedFd> = OnceLock::new();
if self.is_cache_friendly() {
if let Some(fd) = CACHED_PROCFS_HANDLE.get() {
let procfs = ProcfsHandle::try_from_borrowed_fd(fd.as_fd())
.expect("cached procfs handle should be valid");
debug_assert!(
procfs.is_subset && procfs.is_detached,
"cached procfs handle should be subset=pid and detached"
);
return Ok(procfs);
}
}
let procfs = ProcfsHandle::new_fsopen(self.subset_pid)
.or_else(|_| ProcfsHandle::new_open_tree(OpenTreeFlags::empty()))
.or_else(|_| ProcfsHandle::new_open_tree(OpenTreeFlags::AT_RECURSIVE))
.or_else(|_| ProcfsHandle::new_unsafe_open())
.wrap("get safe procfs handle")?;
match procfs {
ProcfsHandle {
inner: MaybeOwnedFd::OwnedFd(inner),
is_subset: true, is_detached: true, ..
} => {
let cached_inner = match CACHED_PROCFS_HANDLE.try_insert(inner) {
Ok(inner) => MaybeOwnedFd::BorrowedFd(inner.as_fd()),
Err((inner, _)) => MaybeOwnedFd::BorrowedFd(inner.as_fd()),
};
Ok(ProcfsHandle::try_from_maybe_owned_fd(cached_inner)
.expect("cached procfs handle should be valid"))
}
procfs => Ok(procfs),
}
}
}
#[derive(Debug)]
pub struct ProcfsHandleRef<'fd> {
inner: MaybeOwnedFd<'fd, OwnedFd>,
mnt_id: Option<u64>,
is_subset: bool,
is_detached: bool,
pub(crate) resolver: ProcfsResolver,
}
impl<'fd> AsFd for ProcfsHandleRef<'fd> {
fn as_fd(&self) -> BorrowedFd<'_> {
self.inner.as_fd()
}
}
impl<'fd> ProcfsHandleRef<'fd> {
const PROC_ROOT_INO: u64 = 1;
pub fn into_owned_fd(self) -> Option<OwnedFd> {
self.inner.into_owned()
}
pub(crate) fn as_raw_procfs(&self) -> RawProcfsRoot<'_> {
RawProcfsRoot::UnsafeFd(self.as_fd())
}
fn openat_raw(
&self,
dirfd: BorrowedFd<'_>,
subpath: &Path,
oflags: OpenFlags,
) -> Result<OwnedFd, Error> {
let fd = self.resolver.resolve(
self.as_raw_procfs(),
dirfd,
subpath,
oflags,
ResolverFlags::empty(),
)?;
self.verify_same_procfs_mnt(&fd).with_wrap(|| {
format!(
"validate that procfs subpath fd {} is on the same procfs mount",
syscalls::FrozenFd::from(&fd),
)
})?;
Ok(fd)
}
fn open_base(&self, base: ProcfsBase) -> Result<OwnedFd, Error> {
self.openat_raw(
self.as_fd(),
&base.into_path(self.as_raw_procfs()),
OpenFlags::O_PATH | OpenFlags::O_DIRECTORY,
)
}
#[doc(alias = "pathrs_proc_open")]
pub fn open_follow(
&self,
base: ProcfsBase,
subpath: impl AsRef<Path>,
oflags: impl Into<OpenFlags>,
) -> Result<File, Error> {
let subpath = subpath.as_ref();
let mut oflags = oflags.into();
let (subpath, trailing_slash) = utils::path_strip_trailing_slash(subpath);
if trailing_slash {
oflags.insert(OpenFlags::O_DIRECTORY);
}
match self.openat_raw(self.open_base(base)?.as_fd(), subpath, oflags) {
Ok(file) => return Ok(file.into()),
Err(err) => {
if self.is_subset && err.kind() == ErrorKind::OsError(Some(libc::ENOENT)) {
return ProcfsHandleBuilder::new()
.unmasked()
.build()
.or(Err(err))?
.open_follow(base, subpath, oflags);
}
if err.kind() != ErrorKind::OsError(Some(libc::ELOOP)) {
return Err(err)?;
}
}
}
let (parent, trailing) = utils::path_split(subpath)?;
let trailing = trailing.ok_or_else(|| ErrorImpl::InvalidArgument {
name: "path".into(),
description: "proc_open_follow path has trailing slash".into(),
})?;
let parentdir = self.openat_raw(
self.open_base(base)?.as_fd(),
parent,
OpenFlags::O_PATH | OpenFlags::O_DIRECTORY,
)?;
let parent_mnt_id =
utils::fetch_mnt_id(self.as_raw_procfs(), &parentdir, "").with_wrap(|| {
format!(
"get mount id of procfs fd {}",
syscalls::FrozenFd::from(&parentdir)
)
})?;
verify_same_mnt(self.as_raw_procfs(), parent_mnt_id, &parentdir, trailing).with_wrap(
|| {
format!(
"check that parent dir {} and {trailing:?} are on the same procfs mount",
syscalls::FrozenFd::from(&parentdir)
)
},
)?;
syscalls::openat_follow(parentdir, trailing, oflags, 0)
.map(File::from)
.map_err(|err| {
ErrorImpl::RawOsError {
operation: "open final magiclink component".into(),
source: err,
}
.into()
})
}
#[doc(alias = "pathrs_proc_open")]
pub fn open(
&self,
base: ProcfsBase,
subpath: impl AsRef<Path>,
oflags: impl Into<OpenFlags>,
) -> Result<File, Error> {
let mut oflags = oflags.into();
oflags.insert(OpenFlags::O_NOFOLLOW);
let subpath = subpath.as_ref();
let fd = self
.openat_raw(self.open_base(base)?.as_fd(), subpath, oflags)
.or_else(|err| {
if self.is_subset && err.kind() == ErrorKind::OsError(Some(libc::ENOENT)) {
ProcfsHandleBuilder::new()
.unmasked()
.build()
.or(Err(err))?
.open(base, subpath, oflags)
.map(OwnedFd::from)
} else {
Err(err)
}
})?;
Ok(fd.into())
}
#[doc(alias = "pathrs_proc_readlink")]
pub fn readlink(&self, base: ProcfsBase, subpath: impl AsRef<Path>) -> Result<PathBuf, Error> {
let link = self.open(base, subpath, OpenFlags::O_PATH)?;
syscalls::readlinkat(link, "").map_err(|err| {
ErrorImpl::RawOsError {
operation: "read procfs magiclink".into(),
source: err,
}
.into()
})
}
#[inline]
fn verify_same_procfs_mnt(&self, fd: impl AsFd) -> Result<(), Error> {
verify_same_procfs_mnt(self.as_raw_procfs(), self.mnt_id, fd)
}
pub fn try_from_borrowed_fd<Fd: Into<BorrowedFd<'fd>>>(inner: Fd) -> Result<Self, Error> {
Self::try_from_maybe_owned_fd(inner.into().into())
}
fn try_from_maybe_owned_fd(inner: MaybeOwnedFd<'fd, OwnedFd>) -> Result<Self, Error> {
let inner_fd = inner.as_fd();
verify_is_procfs_root(inner_fd).with_wrap(|| {
format!(
"check if candidate procfs root fd {} is a procfs root",
syscalls::FrozenFd::from(inner_fd)
)
})?;
let proc_rootfd = RawProcfsRoot::UnsafeFd(inner_fd);
let mnt_id = utils::fetch_mnt_id(proc_rootfd, inner_fd, "").with_wrap(|| {
format!(
"get mount id for candidate procfs root fd {}",
syscalls::FrozenFd::from(inner_fd)
)
})?;
let resolver = ProcfsResolver::default();
let is_subset = [ "stat", "1"]
.iter()
.any(|&subpath| {
syscalls::accessat(inner_fd, subpath, Access::EXISTS, AtFlags::SYMLINK_NOFOLLOW)
.is_err()
});
let is_detached = verify_same_mnt(proc_rootfd, mnt_id, inner_fd, "..")
.and_then(|_| {
verify_is_procfs_root(
syscalls::openat(
inner_fd,
"..",
OpenFlags::O_PATH | OpenFlags::O_DIRECTORY,
0,
)
.map_err(|err| ErrorImpl::RawOsError {
operation: "get parent directory of procfs handle".into(),
source: err,
})?,
)
})
.is_ok();
Ok(Self {
inner,
mnt_id,
is_subset,
is_detached,
resolver,
})
}
}
pub type ProcfsHandle = ProcfsHandleRef<'static>;
static HAS_UNBROKEN_MOUNT_API: Lazy<bool> = Lazy::new(|| kernel_version::is_gte!(5, 2));
impl ProcfsHandle {
pub(crate) fn new_fsopen(subset: bool) -> Result<Self, Error> {
if !*HAS_UNBROKEN_MOUNT_API {
Err(ErrorImpl::NotSupported {
feature: "fsopen".into(),
})?
}
let sfd = syscalls::fsopen("proc", FsOpenFlags::FSOPEN_CLOEXEC).map_err(|err| {
ErrorImpl::RawOsError {
operation: "create procfs suberblock".into(),
source: err,
}
})?;
if subset {
let _ = syscalls::fsconfig_set_string(&sfd, "hidepid", "ptraceable");
let _ = syscalls::fsconfig_set_string(&sfd, "subset", "pid");
}
syscalls::fsconfig_create(&sfd).map_err(|err| ErrorImpl::RawOsError {
operation: "instantiate procfs superblock".into(),
source: err,
})?;
syscalls::fsmount(
&sfd,
FsMountFlags::FSMOUNT_CLOEXEC,
MountAttrFlags::MOUNT_ATTR_NODEV
| MountAttrFlags::MOUNT_ATTR_NOEXEC
| MountAttrFlags::MOUNT_ATTR_NOSUID,
)
.map_err(|err| {
ErrorImpl::RawOsError {
operation: "mount new private procfs".into(),
source: err,
}
.into()
})
.and_then(Self::try_from_fd)
}
pub(crate) fn new_open_tree(flags: OpenTreeFlags) -> Result<Self, Error> {
if !*HAS_UNBROKEN_MOUNT_API {
Err(ErrorImpl::NotSupported {
feature: "open_tree".into(),
})?
}
syscalls::open_tree(
syscalls::BADFD,
"/proc",
OpenTreeFlags::OPEN_TREE_CLONE | flags,
)
.map_err(|err| {
ErrorImpl::RawOsError {
operation: "create private /proc bind-mount".into(),
source: err,
}
.into()
})
.and_then(Self::try_from_fd)
}
pub(crate) fn new_unsafe_open() -> Result<Self, Error> {
syscalls::openat(
syscalls::BADFD,
"/proc",
OpenFlags::O_PATH | OpenFlags::O_DIRECTORY,
0,
)
.map_err(|err| {
ErrorImpl::RawOsError {
operation: "open /proc handle".into(),
source: err,
}
.into()
})
.and_then(Self::try_from_fd)
}
pub fn new() -> Result<Self, Error> {
ProcfsHandleBuilder::new().subset_pid(true).build()
}
pub fn try_from_fd<Fd: Into<OwnedFd>>(inner: Fd) -> Result<Self, Error> {
Self::try_from_maybe_owned_fd(inner.into().into())
}
}
pub(crate) fn verify_is_procfs(fd: impl AsFd) -> Result<(), Error> {
let fs_type = syscalls::fstatfs(fd)
.map_err(|err| ErrorImpl::RawOsError {
operation: "fstatfs proc handle".into(),
source: err,
})?
.f_type;
if fs_type != rustix_fs::PROC_SUPER_MAGIC {
Err(ErrorImpl::OsError {
operation: "verify fd is from procfs".into(),
source: IOError::from_raw_os_error(libc::EXDEV),
})
.wrap(format!(
"fstype mismatch in restricted procfs resolver (f_type is 0x{fs_type:X}, not 0x{:X})",
rustix_fs::PROC_SUPER_MAGIC,
))?
}
Ok(())
}
pub(crate) fn verify_is_procfs_root(fd: impl AsFd) -> Result<(), Error> {
let fd = fd.as_fd();
verify_is_procfs(fd)?;
let ino = fd.metadata().expect("fstat(/proc) should work").ino();
if ino != ProcfsHandle::PROC_ROOT_INO {
Err(ErrorImpl::SafetyViolation {
description: format!(
"/proc is not root of a procfs mount (ino is 0x{ino:X}, not 0x{:X})",
ProcfsHandle::PROC_ROOT_INO,
)
.into(),
})?;
}
Ok(())
}
pub(crate) fn verify_same_mnt(
proc_rootfd: RawProcfsRoot<'_>,
root_mnt_id: Option<u64>,
dirfd: impl AsFd,
path: impl AsRef<Path>,
) -> Result<(), Error> {
let mnt_id = utils::fetch_mnt_id(proc_rootfd, dirfd, path)?;
if root_mnt_id != mnt_id {
Err(ErrorImpl::OsError {
operation: "verify lookup is still in the same mount".into(),
source: IOError::from_raw_os_error(libc::EXDEV),
})
.wrap(format!(
"mount id mismatch in restricted procfs resolver (mnt_id is {mnt_id:?}, not procfs {root_mnt_id:?})",
))?
}
Ok(())
}
pub(crate) fn verify_same_procfs_mnt(
proc_rootfd: RawProcfsRoot<'_>,
root_mnt_id: Option<u64>,
fd: impl AsFd,
) -> Result<(), Error> {
let fd = fd.as_fd();
verify_same_mnt(proc_rootfd, root_mnt_id, fd, "")?;
verify_is_procfs(fd)
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fs::File, os::unix::io::AsRawFd};
use pretty_assertions::assert_eq;
#[test]
fn bad_root() {
let file = File::open("/").expect("open root");
let procfs = ProcfsHandle::try_from_fd(file);
assert!(
procfs.is_err(),
"creating a procfs handle from the wrong filesystem should return an error"
);
}
#[test]
fn bad_tmpfs() {
let file = File::open("/tmp").expect("open tmpfs");
let procfs = ProcfsHandle::try_from_fd(file);
assert!(
procfs.is_err(),
"creating a procfs handle from the wrong filesystem should return an error"
);
}
#[test]
fn bad_proc_nonroot() {
let file = File::open("/proc/tty").expect("open tmpfs");
let procfs = ProcfsHandle::try_from_fd(file);
assert!(
procfs.is_err(),
"creating a procfs handle from non-root of procfs should return an error"
);
}
#[test]
fn builder_props() {
assert_eq!(
ProcfsHandleBuilder::new().subset_pid,
true,
"ProcfsHandleBuilder::new() should have subset_pid = true"
);
assert_eq!(
ProcfsHandleBuilder::default().subset_pid,
true,
"ProcfsHandleBuilder::default() should have subset_pid = true"
);
assert_eq!(
ProcfsHandleBuilder::new().subset_pid(true).subset_pid,
true,
"ProcfsHandleBuilder::subset_pid(true) should give subset_pid = true"
);
let mut builder = ProcfsHandleBuilder::new();
builder.set_subset_pid(true);
assert_eq!(
builder.subset_pid, true,
"ProcfsHandleBuilder::set_subset_pid(true) should give subset_pid = true"
);
assert_eq!(
ProcfsHandleBuilder::new().subset_pid(false).subset_pid,
false,
"ProcfsHandleBuilder::subset_pid(true) should give subset_pid = false"
);
let mut builder = ProcfsHandleBuilder::new();
builder.set_subset_pid(false);
assert_eq!(
builder.subset_pid, false,
"ProcfsHandleBuilder::set_subset_pid(false) should give subset_pid = false"
);
assert_eq!(
ProcfsHandleBuilder::new().unmasked().subset_pid,
false,
"ProcfsHandleBuilder::unmasked() should have subset_pid = false"
);
let mut builder = ProcfsHandleBuilder::new();
builder.set_unmasked();
assert_eq!(
builder.subset_pid, false,
"ProcfsHandleBuilder::set_unmasked() should have subset_pid = false"
);
}
#[test]
fn new() {
let procfs = ProcfsHandle::new();
assert!(
procfs.is_ok(),
"new procfs handle should succeed, got {procfs:?}",
);
}
#[test]
fn builder_build() {
let procfs = ProcfsHandleBuilder::new().build();
assert!(
procfs.is_ok(),
"new procfs handle should succeed, got {procfs:?}",
);
}
#[test]
fn builder_unmasked_build() {
let procfs = ProcfsHandleBuilder::new()
.unmasked()
.build()
.expect("should be able to get unmasked procfs handle");
assert!(
!procfs.is_subset,
"new unmasked procfs handle should have !subset=pid",
);
}
#[test]
fn builder_unmasked_build_not_cached() {
let procfs1 = ProcfsHandleBuilder::new()
.unmasked()
.build()
.expect("should be able to get unmasked procfs handle");
let procfs2 = ProcfsHandleBuilder::new()
.unmasked()
.build()
.expect("should be able to get unmasked procfs handle");
assert!(
!procfs1.is_subset,
"new unmasked procfs handle should have !subset=pid",
);
assert!(
!procfs2.is_subset,
"new unmasked procfs handle should have !subset=pid",
);
assert_eq!(
procfs1.is_detached, procfs2.is_detached,
"is_detached should be the same for both handles"
);
assert_ne!(
procfs1.as_fd().as_raw_fd(),
procfs2.as_fd().as_raw_fd(),
"unmasked procfs handles should NOT be cached and thus have different fds"
);
}
#[test]
fn new_fsopen() {
if let Ok(procfs) = ProcfsHandle::new_fsopen(false) {
assert!(
!procfs.is_subset,
"ProcfsHandle::new_fsopen(false) should be !subset=pid"
);
assert!(
procfs.is_detached,
"ProcfsHandle::new_fsopen(false) should be detached"
);
}
}
#[test]
fn new_fsopen_subset() {
if let Ok(procfs) = ProcfsHandle::new_fsopen(true) {
assert!(
procfs.is_subset,
"ProcfsHandle::new_fsopen(true) should be subset=pid"
);
assert!(
procfs.is_detached,
"ProcfsHandle::new_fsopen(true) should be detached"
);
}
}
#[test]
fn new_open_tree() {
if let Ok(procfs) = ProcfsHandle::new_open_tree(OpenTreeFlags::empty()) {
assert!(
!procfs.is_subset,
"ProcfsHandle::new_open_tree() should be !subset=pid (same as host)"
);
assert!(
procfs.is_detached,
"ProcfsHandle::new_open_tree() should be detached"
);
}
if let Ok(procfs) = ProcfsHandle::new_open_tree(OpenTreeFlags::AT_RECURSIVE) {
assert!(
!procfs.is_subset,
"ProcfsHandle::new_open_tree(AT_RECURSIVE) should be !subset=pid (same as host)"
);
assert!(
procfs.is_detached,
"ProcfsHandle::new_open_tree(AT_RECURSIVE) should be detached"
);
}
}
#[test]
fn new_unsafe_open() {
let procfs = ProcfsHandle::new_unsafe_open()
.expect("ProcfsHandle::new_unsafe_open should always work");
assert!(
!procfs.is_subset,
"ProcfsHandle::new_unsafe_open() should be !subset=pid"
);
assert!(
!procfs.is_detached,
"ProcfsHandle::new_unsafe_open() should not be detached"
);
}
#[test]
fn new_cached() {
std::thread::spawn(|| {
let _ = ProcfsHandle::new().expect("should be able to get ProcfsHandle in thread");
})
.join()
.expect("ProcfsHandle::new thread should succeed");
let procfs1 = ProcfsHandle::new().expect("get procfs handle");
let procfs2 = ProcfsHandle::new().expect("get procfs handle");
assert_eq!(
procfs1.is_subset, procfs2.is_subset,
"subset=pid should be the same for both handles"
);
assert_eq!(
procfs1.is_detached, procfs2.is_detached,
"is_detached should be the same for both handles"
);
if procfs1.is_subset && procfs1.is_detached {
assert_eq!(
procfs1.as_fd().as_raw_fd(),
procfs2.as_fd().as_raw_fd(),
"subset=pid handles should be cached and thus have the same fd"
);
} else {
assert_ne!(
procfs1.as_fd().as_raw_fd(),
procfs2.as_fd().as_raw_fd(),
"!subset=pid handles should NOT be cached and thus have different fds"
);
}
}
#[test]
fn builder_build_cached() {
std::thread::spawn(|| {
let _ = ProcfsHandleBuilder::new()
.build()
.expect("should be able to get ProcfsHandle in thread");
})
.join()
.expect("ProcfsHandle::new thread should succeed");
let procfs1 = ProcfsHandleBuilder::new()
.build()
.expect("get procfs handle");
let procfs2 = ProcfsHandleBuilder::new()
.build()
.expect("get procfs handle");
assert_eq!(
procfs1.is_subset, procfs2.is_subset,
"subset=pid should be the same for both handles"
);
assert_eq!(
procfs1.is_detached, procfs2.is_detached,
"is_detached should be the same for both handles"
);
if procfs1.is_subset && procfs1.is_detached {
assert_eq!(
procfs1.as_fd().as_raw_fd(),
procfs2.as_fd().as_raw_fd(),
"subset=pid handles should be cached and thus have the same fd"
);
} else {
assert_ne!(
procfs1.as_fd().as_raw_fd(),
procfs2.as_fd().as_raw_fd(),
"!subset=pid handles should NOT be cached and thus have different fds"
);
}
}
}