#[cfg(feature = "capi")]
use crate::tests::capi::{CapiProcfsHandle, CapiProcfsHandleFd};
use crate::{
error::ErrorKind,
flags::OpenFlags,
procfs::{ProcfsBase, ProcfsHandle, ProcfsHandleBuilder},
resolvers::procfs::ProcfsResolver,
syscalls,
};
use utils::ExpectedResult;
use anyhow::Error;
use rustix::mount::OpenTreeFlags;
macro_rules! procfs_tests {
($(#[$meta:meta])* @rust-fn [<$func_prefix:ident $test_name:ident>] $procfs_inst:block . $procfs_op:ident ($($args:expr),*) => (over_mounts: $over_mounts:expr, error: $expect_error:expr) ;) => {
paste::paste! {
#[test]
$(#[$meta])*
fn [<procfs_overmounts_ $func_prefix $test_name>]() -> Result<(), Error> {
utils::[<check_proc_ $procfs_op>](
|| $procfs_inst,
$($args,)*
$over_mounts,
ExpectedResult::$expect_error,
)
}
#[test]
$(#[$meta])*
fn [<procfs_overmounts_ $func_prefix openat2_ $test_name>]() -> Result<(), Error> {
utils::[<check_proc_ $procfs_op>](
|| {
let mut proc = $procfs_inst ?;
proc.resolver = ProcfsResolver::Openat2;
Ok(proc)
},
$($args,)*
$over_mounts,
ExpectedResult::$expect_error,
)
}
#[test]
$(#[$meta])*
fn [<procfs_overmounts_ $func_prefix opath_ $test_name>]() -> Result<(), Error> {
if syscalls::openat2::openat2_is_not_supported() {
return Ok(());
}
utils::[<check_proc_ $procfs_op>](
|| {
let mut proc = $procfs_inst ?;
proc.resolver = ProcfsResolver::RestrictedOpath;
Ok(proc)
},
$($args,)*
$over_mounts,
ExpectedResult::$expect_error,
)
}
}
};
($(#[$meta:meta])* @capi-fn [<$func_prefix:ident $test_name:ident>] $procfs_inst:block . $procfs_op:ident ($($args:expr),*) => (over_mounts: $over_mounts:expr, error: $expect_error:expr) ;) => {
paste::paste! {
#[test]
#[cfg(feature = "capi")]
$(#[$meta])*
fn [<procfs_overmounts_ $func_prefix $test_name>]() -> Result<(), Error> {
utils::[<check_proc_ $procfs_op>](
|| $procfs_inst,
$($args,)*
$over_mounts,
ExpectedResult::$expect_error,
)
}
}
};
($(#[$meta:meta])* @impl $test_name:ident $procfs_var:ident . $procfs_op:ident ($($args:tt)*) => ($($tt:tt)*) ;) => {
procfs_tests! {
$(#[$meta])*
@rust-fn [<new_ $test_name>]
{ ProcfsHandle::new() }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*);
}
procfs_tests! {
$(#[$meta])*
@rust-fn [<new_unmasked_ $test_name>]
{
ProcfsHandleBuilder::new()
.unmasked()
.build()
}.$procfs_op($($args)*) => (over_mounts: false, $($tt)*);
}
procfs_tests! {
$(#[$meta])*
#[cfg_attr(not(feature = "_test_as_root"), ignore, allow(unused_attributes))]
@rust-fn [<new_fsopen_ $test_name>]
{ ProcfsHandle::new_fsopen(false) }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*);
}
procfs_tests! {
$(#[$meta])*
#[cfg_attr(not(feature = "_test_as_root"), ignore, allow(unused_attributes))]
@rust-fn [<new_fsopen_subset_ $test_name>]
{ ProcfsHandle::new_fsopen(true) }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*);
}
procfs_tests! {
$(#[$meta])*
#[cfg_attr(not(feature = "_test_as_root"), ignore, allow(unused_attributes))]
@rust-fn [<new_open_tree_ $test_name>]
{
ProcfsHandle::new_open_tree(OpenTreeFlags::OPEN_TREE_CLONE)
}.$procfs_op($($args)*) => (over_mounts: false, $($tt)*);
}
procfs_tests! {
$(#[$meta])*
#[cfg_attr(not(feature = "_test_as_root"), ignore, allow(unused_attributes))]
@rust-fn [<new_open_tree_recursive_ $test_name>]
{
ProcfsHandle::new_open_tree(OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::AT_RECURSIVE)
}.$procfs_op($($args)*) => (over_mounts: true, $($tt)*);
}
procfs_tests! {
$(#[$meta])*
@rust-fn [<new_unsafe_open_ $test_name>]
{ ProcfsHandle::new_unsafe_open() }.$procfs_op($($args)*) => (over_mounts: true, $($tt)*);
}
procfs_tests! {
$(#[$meta])*
@capi-fn [<capi_ $test_name>]
{ Ok(CapiProcfsHandle) }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*);
}
procfs_tests! {
$(#[$meta])*
@capi-fn [<capi_unmasked_ $test_name>]
{ CapiProcfsHandleFd::new_unmasked() }.$procfs_op($($args)*) => (over_mounts: false, $($tt)*);
}
};
($(#[cfg($ignore_meta:meta)])* $test_name:ident : readlink (ProcfsBase::$base:ident $(($pid:literal))?, $path:expr ) => ($($tt:tt)*)) => {
paste::paste! {
procfs_tests! {
$(#[cfg_attr(not($ignore_meta), ignore, allow(unused_attributes))])*
@impl [<$base:lower $($pid)* _readlink_ $test_name>]
procfs.readlink(ProcfsBase::$base $(($pid))*, $path) => ($($tt)*);
}
}
};
($(#[cfg($ignore_meta:meta)])* $test_name:ident : open (ProcfsBase::$base:ident $(($pid:literal))?, $path:expr, $($flag:ident)|* ) => ($($tt:tt)*)) => {
paste::paste! {
procfs_tests! {
$(#[cfg_attr(not($ignore_meta), ignore, allow(unused_attributes))])*
@impl [<$base:lower $($pid)* _open_ $test_name>]
procfs.open(ProcfsBase::$base $(($pid))*, $path, $(OpenFlags::$flag)|*) => ($($tt)*);
}
}
};
($(#[cfg($ignore_meta:meta)])* $test_name:ident : open_follow (ProcfsBase::$base:ident $(($pid:literal))?, $path:expr, $($flag:ident)|* ) => ($($tt:tt)*)) => {
paste::paste! {
procfs_tests! {
$(#[cfg_attr(not($ignore_meta), ignore, allow(unused_attributes))])*
@impl [<$base:lower $($pid)* _open_follow_ $test_name>]
procfs.open_follow(ProcfsBase::$base $(($pid))*, $path, $(OpenFlags::$flag)|*) => ($($tt)*);
}
}
};
($(#[$meta:meta])* $test_name:ident : $func:ident (self, $($args:tt)*) => ($($tt:tt)*)) => {
paste::paste! {
procfs_tests! {
$(#[$meta])*
$test_name : $func (ProcfsBase::ProcSelf, $($args)*) => ($($tt)*);
}
procfs_tests! {
$(#[$meta])*
$test_name : $func (ProcfsBase::ProcThreadSelf, $($args)*) => ($($tt)*);
}
}
};
($($(#[$meta:meta])* $test_name:ident : $func:ident ($($args:tt)*) => ($($res:tt)*) );* $(;)?) => {
paste::paste! {
$(
$(#[$meta])*
procfs_tests!{$test_name : $func ( $($args)* ) => ($($res)*) }
)*
}
}
}
procfs_tests! {
tmpfs_dir: open(self, "net", O_DIRECTORY) => (error: ErrOvermount("/proc/self/net", ErrorKind::OsError(Some(libc::EXDEV))));
tmpfs_dir: open_follow(self, "net", O_DIRECTORY) => (error: ErrOvermount("/proc/self/net", ErrorKind::OsError(Some(libc::EXDEV))));
nomount: open(self, "attr/current", O_RDONLY) => (error: Ok);
nomount: open_follow(self, "attr/current", O_RDONLY) => (error: Ok);
nomount_dir: open(self, "attr", O_RDONLY) => (error: Ok);
nomount_dir: open_follow(self, "attr", O_RDONLY) => (error: Ok);
nomount_dir_odir: open(self, "attr", O_DIRECTORY|O_RDONLY) => (error: Ok);
nomount_dir_odir: open_follow(self, "attr", O_DIRECTORY|O_RDONLY) => (error: Ok);
nomount_dir_trailing_slash: open(self, "attr/", O_RDONLY) => (error: Ok);
nomount_dir_trailing_slash: open_follow(self, "attr/", O_RDONLY) => (error: Ok);
global_nomount: open(ProcfsBase::ProcRoot, "filesystems", O_RDONLY) => (error: Ok);
global_nomount: readlink(ProcfsBase::ProcRoot, "mounts") => (error: Ok);
pid1_nomount: open(ProcfsBase::ProcPid(1), "stat", O_RDONLY) => (error: Ok);
pid1_nomount: open_follow(ProcfsBase::ProcPid(1), "stat", O_RDONLY) => (error: Ok);
#[cfg(feature = "_test_as_root")]
pid1_nomount: readlink(ProcfsBase::ProcPid(1), "cwd") => (error: Ok);
#[cfg(not(feature = "_test_as_root"))]
pid1_nomount: readlink(ProcfsBase::ProcPid(1), "cwd") => (error: Err(ErrorKind::OsError(Some(libc::EACCES))));
proc_file_wr: open(self, "attr/exec", O_WRONLY) => (error: ErrOvermount("/proc/self/attr/exec", ErrorKind::OsError(Some(libc::EXDEV))));
proc_file_wr: open_follow(self, "attr/exec", O_WRONLY) => (error: ErrOvermount("/proc/self/attr/exec", ErrorKind::OsError(Some(libc::EXDEV))));
proc_file_rd: open(self, "mountinfo", O_RDONLY) => (error: ErrOvermount("/proc/self/mountinfo", ErrorKind::OsError(Some(libc::EXDEV))));
proc_file_rd: open_follow(self, "mountinfo", O_RDONLY) => (error: ErrOvermount("/proc/self/mountinfo", ErrorKind::OsError(Some(libc::EXDEV))));
global_cpuinfo_rd: open(ProcfsBase::ProcRoot, "cpuinfo", O_RDONLY) => (error: ErrOvermount("/proc/cpuinfo", ErrorKind::OsError(Some(libc::EXDEV))));
global_meminfo_rd: open(ProcfsBase::ProcRoot, "meminfo", O_RDONLY) => (error: ErrOvermount("/proc/meminfo", ErrorKind::OsError(Some(libc::EXDEV))));
global_fs_dir: open(ProcfsBase::ProcRoot, "fs", O_RDONLY|O_DIRECTORY) => (error: ErrOvermount("/proc/fs", ErrorKind::OsError(Some(libc::EXDEV))));
symlink: open(ProcfsBase::ProcRoot, "mounts", O_PATH) => (error: Ok);
symlink: open_follow(ProcfsBase::ProcRoot, "mounts", O_RDONLY) => (error: Ok);
symlink: readlink(ProcfsBase::ProcRoot, "mounts") => (error: Ok);
symlink_parentdir: open(ProcfsBase::ProcRoot, "self/mounts", O_PATH) => (error: Ok);
symlink_parentdir: open_follow(ProcfsBase::ProcRoot, "self/mounts", O_RDONLY) => (error: Ok);
symlink_parentdir: readlink(ProcfsBase::ProcRoot, "self/cwd") => (error: Ok);
symlink_overmount: open(ProcfsBase::ProcRoot, "net", O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ELOOP))));
symlink_overmount: open_follow(ProcfsBase::ProcRoot, "net", O_PATH|O_DIRECTORY) => (error: ErrOvermount("/proc/self/net", ErrorKind::OsError(Some(libc::EXDEV))));
symlink_overmount: readlink(ProcfsBase::ProcRoot, "net") => (error: Ok);
symlink_parentdir_overmount: open(ProcfsBase::ProcRoot, "net/unix", O_RDONLY) => (error: ErrOvermount("/proc/self/net", ErrorKind::OsError(Some(libc::EXDEV))));
symlink_parentdir_overmount: open_follow(ProcfsBase::ProcRoot, "net/unix", O_RDONLY) => (error: ErrOvermount("/proc/self/net", ErrorKind::OsError(Some(libc::EXDEV))));
magiclink_nomount: open(self, "cwd", O_PATH) => (error: Ok);
magiclink_nomount: open_follow(self, "cwd", O_RDONLY) => (error: Ok);
magiclink_nomount: readlink(self, "cwd") => (error: Ok);
magiclink_nomount_fd1: readlink(self, "fd/1") => (error: Ok);
magiclink_nomount_fd2: readlink(self, "fd/2") => (error: Ok);
magiclink_exe: open(self, "exe", O_PATH) => (error: ErrOvermount("/proc/self/exe", ErrorKind::OsError(Some(libc::EXDEV))));
magiclink_exe: open_follow(self, "exe", O_RDONLY) => (error: ErrOvermount("/proc/self/exe", ErrorKind::OsError(Some(libc::EXDEV))));
magiclink_exe: readlink(self, "exe") => (error: ErrOvermount("/proc/self/exe", ErrorKind::OsError(Some(libc::EXDEV))));
magiclink_fd0: open(self, "fd/0", O_PATH) => (error: ErrOvermount("/proc/self/fd/0", ErrorKind::OsError(Some(libc::EXDEV))));
magiclink_fd0: open_follow(self, "fd/0", O_RDONLY) => (error: ErrOvermount("/proc/self/fd/0", ErrorKind::OsError(Some(libc::EXDEV))));
magiclink_fd0: readlink(self, "fd/0") => (error: ErrOvermount("/proc/self/fd/0", ErrorKind::OsError(Some(libc::EXDEV))));
nondir_odir: open_follow(self, "environ", O_DIRECTORY|O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ENOTDIR))));
nondir_trailing_slash: open_follow(self, "environ/", O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ENOTDIR))));
proc_cwd_odir: open_follow(self, "cwd", O_DIRECTORY|O_RDONLY) => (error: Ok);
proc_cwd_trailing_slash: open_follow(self, "cwd/", O_RDONLY) => (error: Ok);
proc_fdlink_odir: open_follow(self, "fd//1", O_DIRECTORY|O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ENOTDIR))));
proc_fdlink_trailing_slash: open_follow(self, "fd//1/", O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ENOTDIR))));
proc_dotdot_escape: open_follow(self, "../..", O_PATH) => (error: Err(ErrorKind::OsError(Some(libc::EXDEV))));
proc_magic_component: open(self, "root/etc/passwd", O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ELOOP))));
proc_magic_component: open_follow(self, "root/etc/passwd", O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ELOOP))));
proc_magic_component: readlink(self, "root/etc/passwd") => (error: Err(ErrorKind::OsError(Some(libc::ELOOP))));
proc_sym_onofollow: open(self, "fd/1", O_RDONLY) => (error: Err(ErrorKind::OsError(Some(libc::ELOOP))));
proc_sym_opath_onofollow: open(self, "fd/1", O_PATH) => (error: Ok);
proc_sym_odir_opath_onofollow: open(self, "fd/1", O_DIRECTORY|O_PATH) => (error: Err(ErrorKind::OsError(Some(libc::ENOTDIR))));
proc_dir_odir_opath_onofollow: open(self, "fd", O_DIRECTORY|O_PATH) => (error: Ok);
}
mod utils {
use std::{
collections::HashSet,
fmt::Debug,
path::{Path, PathBuf},
};
use crate::{
error::ErrorKind,
flags::OpenFlags,
procfs::ProcfsBase,
syscalls,
tests::{
common::{self as tests_common, MountType},
traits::{ErrorImpl, ProcfsHandleImpl},
},
utils,
};
use anyhow::{Context, Error};
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(super) enum ExpectedResult {
Ok,
Err(ErrorKind),
ErrOvermount(&'static str, ErrorKind),
}
fn check_proc_error<T: Debug, E: ErrorImpl>(
res: Result<T, E>,
over_mounts: &HashSet<PathBuf>,
expected: ExpectedResult,
) -> Result<(), Error> {
let want_error = match expected {
ExpectedResult::Ok => Ok(()),
ExpectedResult::Err(kind) => Err(kind),
ExpectedResult::ErrOvermount(path, kind) => {
if over_mounts.contains(Path::new(path)) {
Err(kind)
} else {
Ok(())
}
}
};
tests_common::check_err(&res, &want_error)
.with_context(|| format!("unexpected result for overmounts={over_mounts:?}"))?;
Ok(())
}
fn in_host_mnt_ns<T, E, F>(expected: ExpectedResult, func: F) -> Result<(), Error>
where
T: Debug,
E: ErrorImpl,
F: FnOnce() -> Result<T, E>,
{
let over_mounts = HashSet::new();
let res = func();
check_proc_error(res, &over_mounts, expected)?;
Ok(())
}
const PROCFS_MAYBE_UNMOUNTABLE: &[&str] = &[
"/proc/self/map_files/",
"/proc/thread-self/map_files/",
"/proc/self/fd/",
"/proc/thread-self/fd/",
"/proc/self/fdinfo/",
"/proc/thread-self/fdinfo/",
];
fn try_mount(
over_mounts: &mut HashSet<PathBuf>,
dst: impl AsRef<Path>,
ty: MountType,
) -> Result<(), Error> {
let dst = dst.as_ref();
let might_fail = {
let dst = dst.to_str().expect("our path strings are valid utf8");
PROCFS_MAYBE_UNMOUNTABLE
.iter()
.any(|prefix| dst.starts_with(prefix))
};
match (tests_common::mount(dst, ty), might_fail) {
(Ok(_), _) => {
over_mounts.insert(dst.to_path_buf());
}
(Err(_), true) => (),
(Err(err), false) => Err(err)?,
};
Ok(())
}
fn in_mnt_ns_with_overmounts<T, E, F>(
are_over_mounts_visible: bool,
expected: ExpectedResult,
func: F,
) -> Result<(), Error>
where
T: Debug,
E: ErrorImpl,
F: FnOnce() -> Result<T, E>,
{
tests_common::in_mnt_ns(|| {
let mut over_mounts = HashSet::new();
try_mount(
&mut over_mounts,
"/proc/fs",
MountType::Tmpfs,
)?;
try_mount(
&mut over_mounts,
"/proc/meminfo",
MountType::Bind {
src: "/dev/null".into(),
},
)?;
try_mount(
&mut over_mounts,
"/proc/cpuinfo",
MountType::Bind {
src: "/proc/1/environ".into(),
},
)?;
for prefix in ["/proc/self", "/proc/thread-self"] {
let prefix = PathBuf::from(prefix);
try_mount(
&mut over_mounts,
prefix.join("net"),
MountType::Tmpfs,
)?;
try_mount(
&mut over_mounts,
prefix.join("attr/exec"),
MountType::Bind {
src: "/proc/1/sched".into(),
},
)?;
try_mount(
&mut over_mounts,
prefix.join("mountinfo"),
MountType::Bind {
src: "/proc/1/environ".into(),
},
)?;
try_mount(
&mut over_mounts,
prefix.join("exe"),
MountType::Bind {
src: "/proc/1/fd/0".into(),
},
)?;
try_mount(
&mut over_mounts,
prefix.join("fd/0"),
MountType::Bind {
src: "/proc/1/exe".into(),
},
)?;
}
if !are_over_mounts_visible {
over_mounts.clear();
}
let res = func();
check_proc_error(res, &over_mounts, expected)?;
Ok(())
})
}
fn check_func<T, E, F>(
are_over_mounts_visible: bool,
expected: ExpectedResult,
func: F,
) -> Result<(), Error>
where
T: Debug,
E: ErrorImpl,
F: FnOnce() -> Result<T, E>,
{
if syscalls::geteuid() == 0 {
in_mnt_ns_with_overmounts(are_over_mounts_visible, expected, func)
} else {
in_host_mnt_ns(expected, func)
}
}
pub(super) fn check_proc_open<Proc, ProcFn>(
proc_fn: ProcFn,
base: ProcfsBase,
path: impl AsRef<Path>,
oflags: impl Into<OpenFlags>,
are_over_mounts_visible: bool,
expected: ExpectedResult,
) -> Result<(), Error>
where
Proc: ProcfsHandleImpl,
ProcFn: FnOnce() -> Result<Proc, Proc::Error>,
{
check_func(
are_over_mounts_visible,
expected,
|| -> Result<_, Proc::Error> {
let oflags = oflags.into();
let proc = proc_fn()?;
let f = proc.open(base, path, oflags)?;
let mut want_oflags = oflags;
want_oflags.insert(OpenFlags::O_NOFOLLOW);
tests_common::check_oflags(&f, want_oflags).expect("check oflags");
Ok(f)
},
)
}
pub(super) fn check_proc_open_follow<Proc, ProcFn>(
proc_fn: ProcFn,
base: ProcfsBase,
path: impl AsRef<Path>,
oflags: impl Into<OpenFlags>,
are_over_mounts_visible: bool,
expected: ExpectedResult,
) -> Result<(), Error>
where
Proc: ProcfsHandleImpl,
ProcFn: FnOnce() -> Result<Proc, Proc::Error>,
{
check_func(
are_over_mounts_visible,
expected,
|| -> Result<_, Proc::Error> {
let path = path.as_ref();
let oflags = oflags.into();
let proc = proc_fn()?;
let f = proc.open_follow(base, path, oflags)?;
let mut want_oflags = oflags;
let (_, trailing_slash) = utils::path_strip_trailing_slash(path);
if trailing_slash {
want_oflags.insert(OpenFlags::O_DIRECTORY);
}
tests_common::check_oflags(&f, want_oflags).expect("check oflags");
Ok(f)
},
)
}
pub(super) fn check_proc_readlink<Proc, ProcFn>(
proc_fn: ProcFn,
base: ProcfsBase,
path: impl AsRef<Path>,
are_over_mounts_visible: bool,
expected: ExpectedResult,
) -> Result<(), Error>
where
Proc: ProcfsHandleImpl,
ProcFn: FnOnce() -> Result<Proc, Proc::Error>,
{
check_func(are_over_mounts_visible, expected, || {
proc_fn()?.readlink(base, path)
})
}
}