use std::{
collections::VecDeque,
ffi::CStr,
fmt,
fs::{metadata, set_permissions, File},
hash::Hash,
io::{Read, Seek, SeekFrom, Write},
ops::Deref,
os::{
fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd},
unix::{ffi::OsStrExt, fs::PermissionsExt},
},
path::Path,
sync::LazyLock,
};
use ahash::HashMapExt;
use bitflags::bitflags;
use btoi::btoi;
use libc::{
c_char, c_int, c_long, c_uint, c_ulong, c_void, clone, off64_t, openat, seccomp_notif,
seccomp_notif_addfd, seccomp_notif_resp, siginfo_t, size_t, syscall, SYS_close_range,
SYS_execveat, SYS_faccessat2, SYS_ioctl, SYS_kcmp, SYS_pidfd_getfd, SYS_pidfd_open,
SYS_pidfd_send_signal, SYS_tgkill, AT_EMPTY_PATH, AT_SYMLINK_NOFOLLOW, CLONE_PIDFD, EBADF,
O_NONBLOCK, S_ISVTX, S_IWGRP, S_IWOTH, _IO, _IOR, _IOW, _IOWR,
};
use libseccomp::{ScmpFilterContext, ScmpSyscall};
use memchr::{arch::all::is_prefix, memchr};
use nix::{
errno::Errno,
fcntl::{
fcntl, openat2, AtFlags, FallocateFlags, FcntlArg, FdFlag, OFlag, OpenHow, ResolveFlag,
SealFlag, AT_FDCWD,
},
libc::{
mode_t, DT_BLK, DT_CHR, DT_DIR, DT_FIFO, DT_LNK, DT_REG, DT_SOCK, S_IFBLK, S_IFCHR,
S_IFDIR, S_IFIFO, S_IFLNK, S_IFMT, S_IFREG, S_IFSOCK,
},
pty::Winsize,
sched::{CloneCb, CloneFlags},
sys::{
signal::{SigSet, Signal},
socket::{
getsockopt,
sockopt::{PeerCredentials, ReceiveTimeout, SendTimeout},
UnixCredentials,
},
stat::Mode,
time::TimeSpec,
},
unistd::{read, write, AccessFlags, Pid},
NixPath,
};
use serde::{ser::SerializeMap, Serialize, Serializer};
use crate::{
compat::{
fstatfs64, fstatx, getdents64, statx, AddWatchFlags, FileStatx, STATX_BASIC_STATS,
STATX_INO, STATX_MNT_ID, STATX_MNT_ID_UNIQUE, STATX_MODE, STATX_NLINK, STATX_SIZE,
STATX_TYPE,
},
config::*,
confine::CLONE_NEWTIME,
cookie::{
safe_openat2, safe_socket, SECCOMP_IOCTL_NOTIF_ADDFD_COOKIE_ARG3,
SECCOMP_IOCTL_NOTIF_ADDFD_COOKIE_ARG4, SECCOMP_IOCTL_NOTIF_ADDFD_COOKIE_ARG5,
SECCOMP_IOCTL_NOTIF_SEND_COOKIE_ARG3, SECCOMP_IOCTL_NOTIF_SEND_COOKIE_ARG4,
SECCOMP_IOCTL_NOTIF_SEND_COOKIE_ARG5,
},
err::err2no,
error,
hash::{SydHashMap, SydHashSet},
ioctl::{TIOCEXCL, TIOCGEXCL, TIOCGPTPEER, TIOCGWINSZ, TIOCNXCL, TIOCSWINSZ},
kernel::sandbox_path,
magic::ProcMagic,
path::{dotdot_with_nul, XPath, XPathBuf, PATH_MAX},
proc::{proc_tgid, PROCMAP_QUERY},
retry::retry_on_eintr,
sandbox::{Capability, Sandbox},
};
pub const AT_BADFD: BorrowedFd<'static> = unsafe { BorrowedFd::borrow_raw(-EBADF) };
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub struct FileInfo {
pub ino: u64,
pub mnt: u64,
}
impl FileInfo {
pub fn from_cwd() -> Result<Self, Errno> {
statx(AT_FDCWD, XPath::empty(), AT_EMPTY_PATH, Self::mask()).map(Self::from_statx)
}
pub fn from_fd<Fd: AsFd>(fd: Fd) -> Result<Self, Errno> {
fstatx(fd, Self::mask()).map(Self::from_statx)
}
pub fn from_statx(stx: FileStatx) -> Self {
Self {
ino: stx.stx_ino,
mnt: stx.stx_mnt_id,
}
}
pub fn mask() -> c_uint {
let mut mask = STATX_INO;
mask |= if *HAVE_STATX_MNT_ID_UNIQUE {
STATX_MNT_ID_UNIQUE
} else {
STATX_MNT_ID
};
mask
}
}
bitflags! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct FsFlags: u16 {
const MUST_PATH = 1 << 0;
const MISS_LAST = 1 << 1;
const NO_FOLLOW_LAST = 1 << 2;
const RESOLVE_BENEATH = 1 << 3;
const NO_RESOLVE_PATH = 1 << 4;
const NO_RESOLVE_PROC = 1 << 5;
const NO_RESOLVE_XDEV = 1 << 6;
const NO_RESOLVE_DOTDOT = 1 << 7;
const WANT_BASE = 1 << 8;
const WANT_READ = 1 << 9;
}
}
impl Default for FsFlags {
fn default() -> Self {
Self::MUST_PATH
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MissingHandling {
Normal,
Existing,
Missing,
}
impl From<FsFlags> for MissingHandling {
fn from(flag: FsFlags) -> Self {
if flag.contains(FsFlags::MUST_PATH) {
Self::Existing
} else if flag.contains(FsFlags::MISS_LAST) {
Self::Missing
} else {
Self::Normal
}
}
}
impl FsFlags {
pub fn magic_errno(self) -> Errno {
if self.intersects(Self::RESOLVE_BENEATH | Self::NO_RESOLVE_XDEV) {
Errno::EXDEV
} else if self.intersects(Self::NO_RESOLVE_PATH | Self::NO_RESOLVE_PROC) {
Errno::ELOOP
} else {
Errno::EACCES
}
}
pub fn follow_last(self) -> bool {
!self.contains(Self::NO_FOLLOW_LAST)
}
pub fn resolve_path(self) -> bool {
!self.contains(Self::NO_RESOLVE_PATH)
}
pub fn resolve_proc(self) -> bool {
!self.intersects(Self::NO_RESOLVE_PROC | Self::NO_RESOLVE_XDEV | Self::RESOLVE_BENEATH)
}
pub fn must_exist(self) -> bool {
self.contains(Self::MUST_PATH)
}
pub fn want_read(self) -> bool {
self.contains(Self::WANT_READ)
}
pub fn missing(self) -> bool {
self.contains(Self::MISS_LAST)
}
}
impl Serialize for FsFlags {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut flags: Vec<&str> = vec![];
if self.is_empty() {
return serializer.collect_seq(flags);
}
if self.contains(Self::RESOLVE_BENEATH) {
flags.push("resolve-beneath");
}
if self.contains(Self::NO_RESOLVE_PATH) {
flags.push("resolve-no-symlinks");
}
if self.contains(Self::NO_RESOLVE_PROC) {
flags.push("resolve-no-magiclinks");
}
if self.contains(Self::NO_RESOLVE_XDEV) {
flags.push("resolve-no-xdev");
}
if self.contains(Self::NO_RESOLVE_DOTDOT) {
flags.push("resolve-no-dotdot");
}
if self.contains(Self::MUST_PATH) {
flags.push("must-path");
}
if self.contains(Self::MISS_LAST) {
flags.push("miss-last");
}
if self.contains(Self::NO_FOLLOW_LAST) {
flags.push("no-follow");
}
if self.contains(Self::WANT_BASE) {
flags.push("want-base");
}
if self.contains(Self::WANT_READ) {
flags.push("want-read");
}
serializer.collect_seq(flags)
}
}
pub fn seal_memfd<Fd: AsFd>(fd: Fd) -> Result<(), Errno> {
fcntl(
fd,
FcntlArg::F_ADD_SEALS(
SealFlag::F_SEAL_SEAL
| SealFlag::F_SEAL_WRITE
| SealFlag::F_SEAL_SHRINK
| SealFlag::F_SEAL_GROW,
),
)
.map(drop)
}
pub fn set_append<Fd: AsFd>(fd: Fd, state: bool) -> Result<(), Errno> {
let flags = fcntl(&fd, FcntlArg::F_GETFL)?;
let mut new_flags = flags;
if state {
new_flags |= OFlag::O_APPEND.bits();
} else {
new_flags &= !OFlag::O_APPEND.bits();
}
fcntl(&fd, FcntlArg::F_SETFL(OFlag::from_bits_truncate(new_flags))).map(drop)
}
pub fn get_nonblock<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
fcntl(fd, FcntlArg::F_GETFL).map(|flags| flags & O_NONBLOCK != 0)
}
pub fn set_nonblock<Fd: AsFd>(fd: Fd, state: bool) -> Result<(), Errno> {
let flags = fcntl(&fd, FcntlArg::F_GETFL)?;
let mut new_flags = flags;
if state {
new_flags |= OFlag::O_NONBLOCK.bits();
} else {
new_flags &= !OFlag::O_NONBLOCK.bits();
}
fcntl(&fd, FcntlArg::F_SETFL(OFlag::from_bits_truncate(new_flags))).map(drop)
}
pub fn set_cloexec<Fd: AsFd>(fd: Fd, state: bool) -> Result<(), Errno> {
let flags = fcntl(&fd, FcntlArg::F_GETFD)?;
let mut new_flags = flags;
if state {
new_flags |= FdFlag::FD_CLOEXEC.bits();
} else {
new_flags &= !FdFlag::FD_CLOEXEC.bits();
}
fcntl(
&fd,
FcntlArg::F_SETFD(FdFlag::from_bits_truncate(new_flags)),
)
.map(drop)
}
pub fn set_pipemax<Fd: AsFd>(fd: Fd, size: c_int) -> Result<usize, Errno> {
#[expect(clippy::cast_sign_loss)]
fcntl(fd, FcntlArg::F_SETPIPE_SZ(size)).map(|r| r as usize)
}
pub fn get_exclusive<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
let mut set: c_int = 0;
let fd = fd.as_fd().as_raw_fd();
let req = TIOCGEXCL.ok_or(Errno::ENOTTY)?;
Errno::result(unsafe { syscall(SYS_ioctl, fd, req, std::ptr::addr_of_mut!(set)) })
.map(|_| set != 0)
}
pub fn set_exclusive<Fd: AsFd>(fd: Fd, enable: bool) -> Result<(), Errno> {
let fd = fd.as_fd().as_raw_fd();
let req = if enable { *TIOCEXCL } else { *TIOCNXCL }.ok_or(Errno::ENOTTY)?;
Errno::result(unsafe { syscall(SYS_ioctl, fd, req) }).map(drop)
}
pub fn openpts<Fd: AsFd>(fd: Fd, flags: OFlag) -> Result<OwnedFd, Errno> {
let fd = fd.as_fd().as_raw_fd();
let flags = flags.bits();
let req = TIOCGPTPEER.ok_or(Errno::ENOTTY)?;
#[expect(clippy::cast_possible_truncation)]
Errno::result(unsafe { syscall(SYS_ioctl, fd, req, flags) }).map(|fd| {
unsafe { OwnedFd::from_raw_fd(fd as RawFd) }
})
}
const KCMP_FILE: c_long = 0;
pub fn is_open_fd(pid: Pid, fd: RawFd) -> Result<bool, Errno> {
#[expect(clippy::cast_lossless)]
#[expect(clippy::cast_possible_wrap)]
#[expect(clippy::cast_sign_loss)]
match Errno::result(unsafe {
syscall(
SYS_kcmp,
pid.as_raw() as c_long,
pid.as_raw() as c_long,
KCMP_FILE,
fd as c_ulong as c_long,
fd as c_ulong as c_long,
)
}) {
Ok(_) => Ok(true),
Err(Errno::EBADF) => Ok(false),
Err(errno) => Err(errno),
}
}
pub fn is_same_fd(pid1: Pid, pid2: Pid, fd1: RawFd, fd2: RawFd) -> Result<bool, Errno> {
if pid1 == pid2 && fd1 == fd2 {
return Ok(true);
}
#[expect(clippy::cast_lossless)]
#[expect(clippy::cast_possible_wrap)]
#[expect(clippy::cast_sign_loss)]
Ok(Errno::result(unsafe {
syscall(
SYS_kcmp,
pid1.as_raw() as c_long,
pid2.as_raw() as c_long,
KCMP_FILE,
fd1 as c_ulong as c_long,
fd2 as c_ulong as c_long,
)
})? == 0)
}
pub fn is_same_vm(pid1: Pid, pid2: Pid) -> Result<bool, Errno> {
const KCMP_VM: u64 = 1;
Ok(Errno::result(unsafe { syscall(SYS_kcmp, pid1.as_raw(), pid2.as_raw(), KCMP_VM) })? == 0)
}
pub fn is_huge_file<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
fstatfs64(fd.as_fd()).map(|st| st.is_huge_file())
}
pub fn is_proc<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
fstatfs64(fd.as_fd()).map(|st| st.is_proc())
}
pub fn is_dev_null<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
#[expect(clippy::cast_possible_truncation)]
const S_IFCHR: u16 = libc::S_IFCHR as u16;
const DEV_NULL_MAJOR: u32 = 1;
const DEV_NULL_MINOR: u32 = 3;
let statx = fstatx(fd, STATX_BASIC_STATS)?;
Ok(statx.stx_mode & S_IFCHR == S_IFCHR
&& statx.stx_rdev_major == DEV_NULL_MAJOR
&& statx.stx_rdev_minor == DEV_NULL_MINOR)
}
pub fn is_dev_kfd<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
#[expect(clippy::cast_possible_truncation)]
const S_IFCHR: u16 = libc::S_IFCHR as u16;
const KFD_MAJOR: u32 = 238;
const KFD_MINOR: u32 = 0;
let statx = fstatx(fd, STATX_BASIC_STATS)?;
Ok(statx.stx_mode & S_IFCHR == S_IFCHR
&& statx.stx_rdev_major == KFD_MAJOR
&& statx.stx_rdev_minor == KFD_MINOR)
}
pub fn has_send_timeout<F: AsFd>(fd: &F) -> Result<bool, Errno> {
let tv = getsockopt(fd, SendTimeout)?;
Ok(tv.tv_sec() != 0 || tv.tv_usec() != 0)
}
pub fn has_recv_timeout<F: AsFd>(fd: &F) -> Result<bool, Errno> {
let tv = getsockopt(fd, ReceiveTimeout)?;
Ok(tv.tv_sec() != 0 || tv.tv_usec() != 0)
}
pub fn inotify_add_watch<Fd: AsFd, P: ?Sized + NixPath>(
fd: Fd,
path: &P,
mask: AddWatchFlags,
) -> Result<c_int, Errno> {
let res = path.with_nix_path(|cstr| unsafe {
libc::inotify_add_watch(fd.as_fd().as_raw_fd(), cstr.as_ptr(), mask.bits())
})?;
Errno::result(res).map(|wd| wd as c_int)
}
pub fn fallocate64<Fd: AsFd>(
fd: Fd,
mode: FallocateFlags,
off: off64_t,
len: off64_t,
) -> Result<(), Errno> {
Errno::result(unsafe { libc::fallocate64(fd.as_fd().as_raw_fd(), mode.bits(), off, len) })
.map(drop)
}
pub fn truncate64<P: ?Sized + NixPath>(path: &P, len: off64_t) -> Result<(), Errno> {
Errno::result(path.with_nix_path(|cstr| unsafe { libc::truncate64(cstr.as_ptr(), len) })?)
.map(drop)
}
pub fn ftruncate64<Fd: AsFd>(fd: Fd, len: off64_t) -> Result<(), Errno> {
Errno::result(unsafe { libc::ftruncate64(fd.as_fd().as_raw_fd(), len) }).map(drop)
}
pub fn winsize_get<Fd: AsFd>(fd: Fd) -> Result<Winsize, Errno> {
let fd = fd.as_fd().as_raw_fd();
let req = TIOCGWINSZ.ok_or(Errno::ENOTTY)?;
let mut ws = Winsize {
ws_row: 0,
ws_col: 0,
ws_xpixel: 0,
ws_ypixel: 0,
};
Errno::result(unsafe { syscall(SYS_ioctl, fd, req, &mut ws) })?;
Ok(ws)
}
pub fn winsize_set<Fd: AsFd>(fd: Fd, ws: Winsize) -> Result<(), Errno> {
let fd = fd.as_fd().as_raw_fd();
let req = TIOCSWINSZ.ok_or(Errno::ENOTTY)?;
Errno::result(unsafe { syscall(SYS_ioctl, fd, req, &ws) }).map(drop)
}
pub fn read_all<Fd: AsFd>(fd: Fd, buf: &mut [u8]) -> Result<usize, Errno> {
let mut nread = 0;
while nread < buf.len() {
match retry_on_eintr(|| read(&fd, &mut buf[nread..]))? {
0 => break,
n => nread = nread.checked_add(n).ok_or(Errno::EOVERFLOW)?,
}
}
Ok(nread)
}
pub fn write_all<Fd: AsFd>(fd: Fd, data: &[u8]) -> Result<(), Errno> {
let mut nwrite = 0;
while nwrite < data.len() {
match retry_on_eintr(|| write(&fd, &data[nwrite..]))? {
0 => return Err(Errno::EPIPE),
n => nwrite = nwrite.checked_add(n).ok_or(Errno::EOVERFLOW)?,
}
}
Ok(())
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct fiemap_extent {
fe_logical: u64,
fe_physical: u64,
fe_length: u64,
_fe_reserved64: [u64; 2],
fe_flags: u32,
_fe_reserved: [u32; 3],
}
#[repr(C)]
struct fiemap {
fm_start: u64,
fm_length: u64,
fm_flags: u32,
fm_mapped_extents: u32,
fm_extent_count: u32,
_fm_reserved: u32,
fm_extents: [fiemap_extent; 0],
}
pub const FS_IOC_FIEMAP: c_ulong = _IOWR::<fiemap>(b'f' as u32, 11) as c_ulong;
pub const FIGETBSZ: c_ulong = _IO(0x00, 2) as c_ulong;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct file_dedupe_range_info {
dest_fd: i64,
dest_offset: u64,
bytes_deduped: u64,
status: i32,
reserved: u32,
}
#[repr(C)]
struct file_dedupe_range {
src_offset: u64,
src_length: u64,
dest_count: u16,
reserved1: u16,
reserved2: u32,
info: [file_dedupe_range_info; 0],
}
pub const FIDEDUPERANGE: c_ulong = _IOWR::<file_dedupe_range>(0x94, 54) as c_ulong;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct fsuuid2 {
len: u8,
uuid: [u8; 16],
}
pub const FS_IOC_GETFSUUID: c_ulong = _IOR::<fsuuid2>(0x15, 0) as c_ulong;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct fs_sysfs_path {
len: u8,
name: [u8; 128],
}
pub const FS_IOC_GETFSSYSFSPATH: c_ulong = _IOR::<fs_sysfs_path>(0x15, 1) as c_ulong;
pub const FIBMAP: c_ulong = _IO(0x00, 1) as c_ulong;
pub const KDSETKEYCODE: c_ulong = 0x4B4D;
pub const KDSIGACCEPT: c_ulong = 0x4B4E;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct fsxattr {
fsx_xflags: u32,
fsx_extsize: u32,
fsx_nextents: u32,
fsx_projid: u32,
fsx_cowextsize: u32,
fsx_pad: [u8; 8],
}
pub const FS_IOC_FSGETXATTR: c_ulong = _IOR::<fsxattr>(b'X' as u32, 31) as c_ulong;
pub const FS_IOC_FSSETXATTR: c_ulong = _IOW::<fsxattr>(b'X' as u32, 32) as c_ulong;
pub const FS_IOC_SETFLAGS: c_ulong = _IOW::<c_long>(b'f' as u32, 2) as c_ulong;
pub const SECCOMP_IOCTL_MAGIC: u32 = b'!' as u32;
pub const SECCOMP_IOCTL_NOTIF_RECV: c_ulong =
_IOWR::<seccomp_notif>(SECCOMP_IOCTL_MAGIC, 0) as c_ulong;
pub const SECCOMP_IOCTL_NOTIF_SEND: c_ulong =
_IOWR::<seccomp_notif_resp>(SECCOMP_IOCTL_MAGIC, 1) as c_ulong;
pub const SECCOMP_IOCTL_NOTIF_ID_VALID: c_ulong = _IOW::<u64>(SECCOMP_IOCTL_MAGIC, 2) as c_ulong;
pub const SECCOMP_IOCTL_NOTIF_ADDFD: c_ulong =
_IOW::<seccomp_notif_addfd>(SECCOMP_IOCTL_MAGIC, 3) as c_ulong;
pub const SECCOMP_IOCTL_NOTIF_SET_FLAGS: c_ulong = _IOW::<u64>(SECCOMP_IOCTL_MAGIC, 4) as c_ulong;
pub(crate) const SECCOMP_IOCTL_NOTIF_LIST: &[c_ulong] = &[
SECCOMP_IOCTL_NOTIF_RECV,
SECCOMP_IOCTL_NOTIF_SEND,
SECCOMP_IOCTL_NOTIF_ID_VALID,
SECCOMP_IOCTL_NOTIF_ADDFD,
SECCOMP_IOCTL_NOTIF_SET_FLAGS,
];
pub(crate) const SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP: u32 = 1;
pub(crate) fn seccomp_export_pfc(ctx: &ScmpFilterContext) -> Result<String, Errno> {
#[expect(clippy::disallowed_methods)]
let mut file = nix::fcntl::openat(
AT_FDCWD,
"/tmp",
OFlag::O_TMPFILE | OFlag::O_EXCL | OFlag::O_RDWR,
Mode::empty(),
)
.map(File::from)?;
ctx.export_pfc(&mut file).or(Err(Errno::EFAULT))?;
file.seek(SeekFrom::Start(0)).map_err(|err| err2no(&err))?;
let mut buf = Vec::new();
file.read_to_end(&mut buf).map_err(|err| err2no(&err))?;
let mut pfc = String::from_utf8_lossy(&buf).into_owned();
for &(from, to) in &[
("0x7fc00000", "NOTIFY"),
(
&format!("{SECCOMP_IOCTL_NOTIF_RECV}"),
"SECCOMP_IOCTL_NOTIF_RECV",
),
(
&format!("{SECCOMP_IOCTL_NOTIF_SEND}"),
"SECCOMP_IOCTL_NOTIF_SEND",
),
(
&format!("{SECCOMP_IOCTL_NOTIF_ID_VALID}"),
"SECCOMP_IOCTL_NOTIF_ID_VALID",
),
(
&format!("{SECCOMP_IOCTL_NOTIF_ADDFD}"),
"SECCOMP_IOCTL_NOTIF_ADDFD",
),
(
&format!("{SECCOMP_IOCTL_NOTIF_SET_FLAGS}"),
"SECCOMP_IOCTL_NOTIF_SET_FLAGS",
),
(&format!("{PROCMAP_QUERY}"), "PROCMAP_QUERY"),
] {
pfc = pfc.replace(from, to);
}
Ok(pfc)
}
pub(crate) fn seccomp_notify_set_flags(fd: RawFd, flags: u32) -> Result<(), Errno> {
if !*HAVE_SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP {
return Err(Errno::ENOSYS);
}
retry_on_eintr(|| {
Errno::result(unsafe {
syscall(
SYS_ioctl,
fd,
SECCOMP_IOCTL_NOTIF_SET_FLAGS as c_ulong,
flags,
)
})
})
.map(drop)
}
pub(crate) fn seccomp_notify_id_valid(fd: RawFd, id: u64) -> Result<(), Errno> {
retry_on_eintr(|| {
Errno::result(unsafe {
syscall(SYS_ioctl, fd, SECCOMP_IOCTL_NOTIF_ID_VALID as c_ulong, &id)
})
})
.map(drop)
}
pub(crate) fn seccomp_notify_respond(
fd: RawFd,
response: *const seccomp_notif_resp,
) -> Result<(), Errno> {
retry_on_eintr(|| {
Errno::result(unsafe {
syscall(
SYS_ioctl,
fd,
SECCOMP_IOCTL_NOTIF_SEND as c_ulong,
response,
*SECCOMP_IOCTL_NOTIF_SEND_COOKIE_ARG3,
*SECCOMP_IOCTL_NOTIF_SEND_COOKIE_ARG4,
*SECCOMP_IOCTL_NOTIF_SEND_COOKIE_ARG5,
)
})
})
.map(drop)
}
pub(crate) fn seccomp_notify_addfd(
fd: RawFd,
addfd: *const seccomp_notif_addfd,
) -> Result<RawFd, Errno> {
#[expect(clippy::cast_possible_truncation)]
retry_on_eintr(|| {
Errno::result(unsafe {
syscall(
SYS_ioctl,
fd,
SECCOMP_IOCTL_NOTIF_ADDFD as c_ulong,
addfd,
*SECCOMP_IOCTL_NOTIF_ADDFD_COOKIE_ARG3,
*SECCOMP_IOCTL_NOTIF_ADDFD_COOKIE_ARG4,
*SECCOMP_IOCTL_NOTIF_ADDFD_COOKIE_ARG5,
)
})
})
.map(|fd| fd as RawFd)
}
pub fn is_active_fd<Fd: AsFd>(fd: Fd) -> bool {
fcntl(fd, FcntlArg::F_GETFD).is_ok()
}
pub fn is_valid_fd(fd: u64) -> bool {
to_valid_fd(fd).map(|fd| fd >= 0).unwrap_or(false)
}
#[expect(clippy::cast_possible_truncation)]
pub fn to_valid_fd(fd: u64) -> Result<RawFd, Errno> {
let fd = fd as RawFd;
if fd == libc::AT_FDCWD || fd >= 0 {
Ok(fd)
} else {
Err(Errno::EBADF)
}
}
pub fn fd_status_flags<Fd: AsFd>(fd: Fd) -> Result<OFlag, Errno> {
fcntl(fd, FcntlArg::F_GETFL).map(OFlag::from_bits_truncate)
}
pub fn is_writable_fd<Fd: AsFd>(fd: Fd) -> Result<bool, Errno> {
fd_status_flags(fd)
.map(oflag_accmode)
.map(|mode| !mode.is_empty())
}
pub fn oflag_accmode(flags: OFlag) -> OFlag {
flags & (OFlag::O_ACCMODE | OFlag::O_PATH)
}
pub fn oflag_nonblock(flags: OFlag) -> bool {
!(flags & (OFlag::O_NONBLOCK | OFlag::O_NDELAY)).is_empty()
}
pub fn fd_count(pid: Option<Pid>) -> Result<u64, Errno> {
let mut pfd = XPathBuf::from("/proc");
if let Some(pid) = pid {
pfd.push_pid(pid);
} else {
pfd.push(b"thread-self");
}
pfd.push(b"fd");
if *HAVE_PROC_PID_FD_STAT_SIZE {
let stx = statx(AT_BADFD, &pfd, 0, STATX_SIZE)?;
return Ok(stx.stx_size);
}
#[expect(clippy::disallowed_methods)]
let fd = nix::fcntl::openat(
AT_BADFD,
&pfd,
OFlag::O_RDONLY | OFlag::O_DIRECTORY | OFlag::O_CLOEXEC,
Mode::empty(),
)?;
let mut nfds: u64 = 0;
loop {
match getdents64(&fd, DIRENT_BUF_SIZE) {
Ok(entries) => {
nfds = nfds
.checked_add(entries.count() as u64)
.ok_or(Errno::ERANGE)?
}
Err(Errno::ECANCELED) => break, Err(errno) => return Err(errno),
};
}
Ok(nfds.saturating_sub(2))
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum FileType {
Reg,
Dir,
Lnk,
MagicLnk(ProcMagic),
Mfd,
Fifo,
Sock,
Chr,
Blk,
Unk,
}
impl Serialize for FileType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let repr = match self {
Self::Reg => "reg".to_string(),
Self::Dir => "dir".to_string(),
Self::Lnk => "lnk".to_string(),
Self::MagicLnk(magic) => format!("mgc@{}", &magic.link_path()),
Self::Mfd => "memfd".to_string(),
Self::Fifo => "fifo".to_string(),
Self::Sock => "sock".to_string(),
Self::Chr => "chr".to_string(),
Self::Blk => "blk".to_string(),
Self::Unk => "unk".to_string(),
};
serializer.serialize_str(&repr)
}
}
impl FileType {
pub fn is_file(self) -> bool {
self == Self::Reg
}
pub fn is_dir(self) -> bool {
self == Self::Dir
}
pub fn is_symlink(self) -> bool {
self == Self::Lnk
}
pub fn is_magic_link(self) -> bool {
matches!(self, Self::MagicLnk(_))
}
pub fn is_magic_dir(self) -> bool {
matches!(self, Self::MagicLnk(magic) if magic.want_dir())
}
pub fn is_memory_fd(self) -> bool {
self == Self::Mfd
}
pub fn is_block_device(self) -> bool {
self == Self::Blk
}
pub fn is_char_device(self) -> bool {
self == Self::Chr
}
pub fn is_fifo(self) -> bool {
self == Self::Fifo
}
pub fn is_socket(self) -> bool {
self == Self::Sock
}
pub fn is_unknown(self) -> bool {
self == Self::Unk
}
pub fn mode(self) -> Option<mode_t> {
match self {
Self::Blk => Some(S_IFBLK),
Self::Chr => Some(S_IFCHR),
Self::Dir => Some(S_IFDIR),
Self::Fifo => Some(S_IFIFO),
Self::Lnk => Some(S_IFLNK),
Self::Reg => Some(S_IFREG),
Self::Sock => Some(S_IFSOCK),
_ => None,
}
}
}
impl From<u8> for FileType {
fn from(dt_type: u8) -> Self {
match dt_type {
DT_DIR => Self::Dir,
DT_REG => Self::Reg,
DT_LNK => Self::Lnk,
DT_CHR => Self::Chr,
DT_BLK => Self::Blk,
DT_FIFO => Self::Fifo,
DT_SOCK => Self::Sock,
_ => Self::Unk,
}
}
}
impl From<mode_t> for FileType {
fn from(mode: mode_t) -> Self {
match mode & S_IFMT {
S_IFBLK => Self::Blk,
S_IFCHR => Self::Chr,
S_IFDIR => Self::Dir,
S_IFIFO => Self::Fifo,
S_IFLNK => Self::Lnk,
S_IFREG => Self::Reg,
S_IFSOCK => Self::Sock,
_ => Self::Unk,
}
}
}
pub fn file_type<Fd: AsFd>(
fd: Fd,
p: Option<&XPath>,
follow_symlinks: bool,
) -> Result<FileType, Errno> {
if let Some(p) = p {
statx(
fd,
p,
if p.is_empty() {
AT_EMPTY_PATH
} else if follow_symlinks {
0
} else {
AT_SYMLINK_NOFOLLOW
},
STATX_TYPE,
)
} else {
fstatx(fd, STATX_TYPE)
}
.map(|statx| FileType::from(mode_t::from(statx.stx_mode)))
}
pub fn safe_clone(
mut cb: CloneCb,
stack: &mut [u8],
flags: c_int,
signal: Option<c_int>,
) -> Result<OwnedFd, Errno> {
#[expect(clippy::cast_possible_truncation)]
extern "C" fn callback(data: *mut CloneCb) -> c_int {
let cb: &mut CloneCb = unsafe { &mut *data };
(*cb)() as c_int
}
let mut pid_fd: c_int = -1;
let combined: c_int = flags | CLONE_PIDFD | signal.unwrap_or(0);
#[expect(clippy::missing_transmute_annotations)]
let res = unsafe {
let ptr = stack.as_mut_ptr().add(stack.len());
let ptr_aligned = ptr.sub(ptr as usize % 16);
clone(
std::mem::transmute(callback as extern "C" fn(*mut Box<dyn FnMut() -> isize>) -> i32),
ptr_aligned as *mut c_void,
combined,
std::ptr::addr_of_mut!(cb) as *mut c_void,
&mut pid_fd,
)
};
Errno::result(res).map(|_| {
unsafe { OwnedFd::from_raw_fd(pid_fd) }
})
}
pub fn safe_open_file<Fd: AsFd, P: NixPath + ?Sized>(
fd: Fd,
base: &P,
) -> Result<(File, FileStatx), Errno> {
let fd = safe_open_path(fd, base, OFlag::O_NOFOLLOW, ResolveFlag::empty())?;
let statx = retry_on_eintr(|| fstatx(&fd, STATX_INO | STATX_TYPE | STATX_SIZE))?;
let ftype = FileType::from(mode_t::from(statx.stx_mode));
if !ftype.is_file() {
return Err(Errno::ENOEXEC);
}
let pfd = XPathBuf::from_self_fd(fd.as_raw_fd());
let flags = OFlag::O_RDONLY | OFlag::O_NOCTTY | OFlag::O_CLOEXEC;
let file = safe_open_msym(PROC_FILE(), &pfd, flags, ResolveFlag::empty()).map(File::from)?;
Ok((file, statx))
}
pub fn safe_copy_if_exists<F: Write, P: NixPath + ?Sized>(
dst: &mut F,
src: &P,
) -> Result<u64, Errno> {
let how = safe_open_how(OFlag::O_PATH, ResolveFlag::empty());
#[expect(clippy::disallowed_methods)]
let fd = if let Ok(fd) = retry_on_eintr(|| openat2(AT_FDCWD, src, how)) {
fd
} else {
return Ok(0);
};
let statx = retry_on_eintr(|| fstatx(&fd, STATX_TYPE))?;
let ftype = FileType::from(mode_t::from(statx.stx_mode));
if !ftype.is_file() {
return Err(Errno::ENOEXEC);
}
let mut pfd = XPathBuf::from("/proc/thread-self");
pfd.push(b"fd");
pfd.push_fd(fd.as_raw_fd());
let how = safe_open_how(OFlag::O_RDONLY | OFlag::O_NOCTTY, ResolveFlag::empty());
#[expect(clippy::disallowed_methods)]
let mut src = if let Ok(src) = retry_on_eintr(|| openat2(AT_FDCWD, &pfd, how).map(File::from)) {
src
} else {
return Ok(0);
};
std::io::copy(&mut src, dst).map_err(|e| err2no(&e))
}
pub fn safe_open_how(flags: OFlag, rflags: ResolveFlag) -> OpenHow {
let mode = if flags.contains(OFlag::O_CREAT) || flags.contains(OFlag::O_TMPFILE) {
Mode::from_bits_truncate(0o600)
} else {
Mode::empty()
};
OpenHow::new()
.flags(flags | OFlag::O_CLOEXEC | OFlag::O_NOFOLLOW)
.mode(mode)
.resolve(
rflags
| ResolveFlag::RESOLVE_NO_MAGICLINKS
| ResolveFlag::RESOLVE_NO_SYMLINKS
| ResolveFlag::RESOLVE_BENEATH,
)
}
pub fn safe_open_how_abs(flags: OFlag, rflags: ResolveFlag) -> OpenHow {
let mode = if flags.contains(OFlag::O_CREAT) || flags.contains(OFlag::O_TMPFILE) {
Mode::from_bits_truncate(0o600)
} else {
Mode::empty()
};
OpenHow::new()
.flags(flags | OFlag::O_CLOEXEC | OFlag::O_NOFOLLOW)
.mode(mode)
.resolve(rflags | ResolveFlag::RESOLVE_NO_MAGICLINKS | ResolveFlag::RESOLVE_NO_SYMLINKS)
}
pub fn safe_open_how_msym(flags: OFlag, rflags: ResolveFlag) -> OpenHow {
OpenHow::new()
.flags(flags | OFlag::O_CLOEXEC)
.resolve(rflags)
}
pub fn safe_open_path<Fd: AsFd, P: NixPath + ?Sized>(
fd: Fd,
base: &P,
flags: OFlag,
rflags: ResolveFlag,
) -> Result<OwnedFd, Errno> {
safe_open(fd, base, OFlag::O_PATH | flags, rflags)
}
pub fn safe_open<Fd: AsFd, P: NixPath + ?Sized>(
fd: Fd,
base: &P,
flags: OFlag,
rflags: ResolveFlag,
) -> Result<OwnedFd, Errno> {
let how = safe_open_how(flags, rflags);
base.with_nix_path(|cstr| {
let xp = XPath::from_bytes(cstr.to_bytes());
let (fd, base) = if xp.is_relative() {
(fd.as_fd(), cstr)
} else if xp.is_rootfs() {
unreachable!("BUG: Attempt to reopen /");
} else {
let xp = xp.as_bytes();
let n = xp.iter().position(|&b| b != b'/').ok_or(Errno::ENOENT)?;
let xp = &cstr.to_bytes_with_nul()[n..];
let cstr = unsafe { CStr::from_bytes_with_nul_unchecked(xp) };
(ROOT_FILE(), cstr)
};
retry_on_eintr(|| safe_openat2(fd, base, how))
})?
}
pub fn safe_open_path_abs<P: NixPath + ?Sized>(
path: &P,
flags: OFlag,
rflags: ResolveFlag,
) -> Result<OwnedFd, Errno> {
safe_open_abs(path, OFlag::O_PATH | flags, rflags)
}
pub fn safe_open_abs<P: NixPath + ?Sized>(
path: &P,
flags: OFlag,
rflags: ResolveFlag,
) -> Result<OwnedFd, Errno> {
let how = safe_open_how_abs(flags, rflags);
path.with_nix_path(|cstr| {
let xp = XPath::from_bytes(cstr.to_bytes());
if xp.is_relative() {
return Err(Errno::EINVAL);
}
#[expect(clippy::disallowed_methods)]
retry_on_eintr(|| openat2(AT_FDCWD, cstr, how))
})?
}
pub fn safe_open_path_msym<Fd: AsFd, P: NixPath + ?Sized>(
fd: Fd,
base: &P,
flags: OFlag,
rflags: ResolveFlag,
) -> Result<OwnedFd, Errno> {
safe_open_msym(fd, base, OFlag::O_PATH | flags, rflags)
}
pub fn safe_open_msym<Fd: AsFd, P: NixPath + ?Sized>(
fd: Fd,
base: &P,
flags: OFlag,
rflags: ResolveFlag,
) -> Result<OwnedFd, Errno> {
let how = safe_open_how_msym(flags, rflags);
base.with_nix_path(|cstr| {
let xp = XPath::from_bytes(cstr.to_bytes());
let (fd, base) = if xp.is_relative() {
(fd.as_fd(), cstr)
} else if xp.is_rootfs() {
unreachable!("BUG: Attempt to reopen /");
} else {
let xp = xp.as_bytes();
let n = xp.iter().position(|&b| b != b'/').ok_or(Errno::ENOENT)?;
let xp = &cstr.to_bytes_with_nul()[n..];
let cstr = unsafe { CStr::from_bytes_with_nul_unchecked(xp) };
(ROOT_FILE(), cstr)
};
retry_on_eintr(|| safe_openat2(fd, base, how))
})?
}
pub fn fgetxattr<Fd: AsFd, P: ?Sized + NixPath>(
fd: Fd,
name: &P,
value: Option<&mut [u8]>,
) -> Result<usize, Errno> {
let (value, len) = match value {
Some(v) => (v.as_mut_ptr() as *mut c_void, v.len() as size_t),
None => (std::ptr::null_mut(), 0),
};
let res = name.with_nix_path(|name_ptr| unsafe {
libc::fgetxattr(fd.as_fd().as_raw_fd(), name_ptr.as_ptr(), value, len)
})?;
#[expect(clippy::cast_sign_loss)]
Errno::result(res).map(|res| res as usize)
}
pub fn fsetxattr<Fd: AsFd, P: ?Sized + NixPath>(
fd: Fd,
name: &P,
value: &[u8],
flags: i32,
) -> Result<(), Errno> {
let res = name.with_nix_path(|name_ptr| unsafe {
libc::fsetxattr(
fd.as_fd().as_raw_fd(),
name_ptr.as_ptr(),
value.as_ptr() as *const c_void,
value.len() as size_t,
flags as c_int,
)
})?;
Errno::result(res).map(drop)
}
pub fn fremovexattr<Fd: AsFd, P: ?Sized + NixPath>(fd: Fd, name: &P) -> Result<(), Errno> {
let res = name.with_nix_path(|name_ptr| unsafe {
libc::fremovexattr(fd.as_fd().as_raw_fd(), name_ptr.as_ptr())
})?;
Errno::result(res).map(drop)
}
const SEC_XATTR: &[u8] = b"security.";
const SYD_XATTR: &[u8] = b"user.syd.";
const XATTR_SEC: &[&[u8]] = &[SEC_XATTR, SYD_XATTR];
pub unsafe fn denyxattr(name: *const c_char) -> Result<(), Errno> {
if name.is_null() {
return Ok(());
}
if (name as u64) < *MMAP_MIN_ADDR {
return Err(Errno::EFAULT);
}
let name = CStr::from_ptr(name);
let name = name.to_bytes();
for prefix in XATTR_SEC {
if is_prefix(name, prefix) {
return Err(Errno::ENODATA);
}
}
Ok(())
}
pub fn filterxattr(buf: &[u8], n: usize) -> Result<Vec<u8>, Errno> {
let mut soff = 0;
let mut fbuf = Vec::new();
while soff < n {
let end = if let Some(end) = memchr(0, &buf[soff..]) {
end
} else {
break;
};
let eoff = soff
.checked_add(end)
.ok_or(Errno::EOVERFLOW)?
.checked_add(1)
.ok_or(Errno::EOVERFLOW)?;
let name = &buf[soff..eoff];
let cstr = unsafe { CStr::from_bytes_with_nul_unchecked(name) };
let cstr = cstr.to_bytes();
let mut filter = false;
for prefix in XATTR_SEC {
if is_prefix(cstr, prefix) {
filter = true;
break;
}
}
if !filter {
fbuf.try_reserve(name.len()).or(Err(Errno::ENOMEM))?;
fbuf.extend_from_slice(name);
}
soff = eoff;
}
Ok(fbuf)
}
pub fn fdaccess<Fd: AsFd>(fd: Fd, mode: AccessFlags, mut flags: AtFlags) -> Result<(), Errno> {
flags.remove(AtFlags::AT_SYMLINK_NOFOLLOW);
flags.insert(AtFlags::AT_EMPTY_PATH);
Errno::result(unsafe {
syscall(
SYS_faccessat2,
fd.as_fd().as_raw_fd(),
c"".as_ptr(),
mode.bits(),
flags.bits(),
)
})
.map(drop)
}
pub(crate) const AT_EXECVE_CHECK: AtFlags = AtFlags::from_bits_retain(0x10000);
pub fn is_executable<Fd: AsFd>(file: Fd) -> bool {
check_executable(file).is_ok()
}
pub fn check_executable<Fd: AsFd>(file: Fd) -> Result<(), Errno> {
if *HAVE_AT_EXECVE_CHECK {
let argv: [*const c_char; 2] = [c"".as_ptr(), std::ptr::null()];
let envp: [*const c_char; 1] = [std::ptr::null()];
Errno::result(unsafe {
syscall(
SYS_execveat,
file.as_fd().as_raw_fd(),
c"".as_ptr(),
argv.as_ptr(),
envp.as_ptr(),
(AT_EXECVE_CHECK | AtFlags::AT_EMPTY_PATH).bits(),
)
})
.map(drop)
} else {
fdaccess(file, AccessFlags::X_OK, AtFlags::AT_EACCESS)
}
}
#[expect(clippy::cast_sign_loss)]
pub const PIDFD_THREAD: u32 = OFlag::O_EXCL.bits() as u32;
pub fn pidfd_open(pid: Pid, mut flags: u32) -> Result<OwnedFd, Errno> {
let pid = if *HAVE_PIDFD_THREAD || flags & PIDFD_THREAD == 0 {
pid
} else {
flags &= !PIDFD_THREAD;
proc_tgid(pid)?
};
#[expect(clippy::cast_possible_truncation)]
Errno::result(unsafe { syscall(SYS_pidfd_open, pid.as_raw(), flags) }).map(|fd| {
unsafe { OwnedFd::from_raw_fd(fd as RawFd) }
})
}
pub fn pidfd_getfd<Fd: AsFd>(pid_fd: Fd, remote_fd: RawFd) -> Result<OwnedFd, Errno> {
#[expect(clippy::cast_possible_truncation)]
Errno::result(unsafe { syscall(SYS_pidfd_getfd, pid_fd.as_fd().as_raw_fd(), remote_fd, 0) })
.map(|fd| {
unsafe { OwnedFd::from_raw_fd(fd as RawFd) }
})
}
pub fn pidfd_send_signal<Fd: AsFd>(pid_fd: Fd, sig: i32) -> Result<(), Errno> {
Errno::result(unsafe { syscall(SYS_pidfd_send_signal, pid_fd.as_fd().as_raw_fd(), sig, 0, 0) })
.map(drop)
}
pub fn pidfd_is_alive<Fd: AsFd>(pid_fd: Fd) -> Result<(), Errno> {
pidfd_send_signal(pid_fd, 0)
}
static SYS_PROCESS_MRELEASE: LazyLock<Option<c_long>> = LazyLock::new(|| {
match ScmpSyscall::from_name("process_mrelease")
.map(i32::from)
.map(c_long::from)
.ok()
{
Some(n) if n < 0 => None,
Some(n) => Some(n),
None => None,
}
});
pub fn process_mrelease<Fd: AsFd>(pid_fd: Fd) -> Result<(), Errno> {
let sysnum = SYS_PROCESS_MRELEASE.ok_or(Errno::ENOSYS)?;
Errno::result(unsafe { syscall(sysnum, pid_fd.as_fd().as_raw_fd(), 0) }).map(drop)
}
pub fn tgkill(tgid: Pid, tid: Pid, sig: i32) -> Result<(), Errno> {
Errno::result(unsafe { syscall(SYS_tgkill, tgid.as_raw(), tid.as_raw(), sig) }).map(drop)
}
pub fn sigwaitinfo(set: &SigSet, info: Option<&mut siginfo_t>) -> Result<i32, Errno> {
let info = info.map(|si| si as *mut _).unwrap_or(std::ptr::null_mut());
Errno::result(unsafe { libc::sigwaitinfo(set.as_ref(), info) })
}
pub fn sigtimedwait(
set: &SigSet,
info: Option<&mut siginfo_t>,
timeout: TimeSpec,
) -> Result<i32, Errno> {
let info = info.map(|si| si as *mut _).unwrap_or(std::ptr::null_mut());
Errno::result(unsafe { libc::sigtimedwait(set.as_ref(), info, timeout.as_ref()) })
}
pub fn sigtimedpoll(set: &SigSet, info: Option<&mut siginfo_t>) -> Result<i32, Errno> {
sigtimedwait(set, info, TimeSpec::new(0, 0))
}
pub fn block_signal(sig: Signal) -> Result<(), Errno> {
let mut mask = SigSet::empty();
mask.add(sig);
mask.thread_block()
}
pub fn unblock_signal(sig: Signal) -> Result<(), Errno> {
let mut mask = SigSet::empty();
mask.add(sig);
mask.thread_unblock()
}
pub fn close_range(first: c_uint, last: c_uint, flags: c_uint) -> Result<(), Errno> {
Errno::result(unsafe { syscall(SYS_close_range, first, last, flags) }).map(drop)
}
pub fn closefrom(fd: c_uint) -> Result<(), Errno> {
close_range(fd, RawFd::MAX as c_uint, 0)
}
pub fn closeexcept(exceptions: &[c_uint]) -> Result<(), Errno> {
if exceptions.windows(2).any(|w| w[0] >= w[1]) {
return Err(Errno::EINVAL);
}
if exceptions.is_empty() {
return closefrom(0);
}
let mut next: u64 = 0;
#[expect(clippy::arithmetic_side_effects)]
#[expect(clippy::cast_possible_truncation)]
for &ex_fd in exceptions {
let ex_fd = u64::from(ex_fd);
if next < ex_fd {
let first = next as c_uint;
let last = (ex_fd - 1) as c_uint;
close_range(first, last, 0)?;
}
next = ex_fd.saturating_add(1);
}
#[expect(clippy::cast_possible_truncation)]
if next <= RawFd::MAX as u64 {
let first = next as c_uint;
closefrom(first)?;
}
Ok(())
}
pub fn peer_cred<Fd: AsFd>(fd: Fd) -> Result<UnixCredentials, Errno> {
getsockopt(&fd, PeerCredentials)
}
#[expect(clippy::arithmetic_side_effects)]
pub fn nlmsg_align(v: usize) -> usize {
(v + 3) & !3usize
}
#[expect(clippy::arithmetic_side_effects)]
pub fn nla_align(v: usize) -> usize {
(v + 3) & !3usize
}
const SOCK_DIAG_BY_FAMILY: u16 = 20;
#[expect(clippy::cast_possible_truncation)]
const NLMSG_DONE: u16 = libc::NLMSG_DONE as u16;
#[expect(clippy::cast_possible_truncation)]
const NLMSG_ERROR: u16 = libc::NLMSG_ERROR as u16;
const NL_HDR_LEN: usize = 16;
const UD_REQ_LEN: usize = 24;
#[expect(clippy::cast_possible_truncation)]
const NL_MSG_LEN: u32 = (NL_HDR_LEN + UD_REQ_LEN) as u32;
const UNIX_DIAG_VFS: u16 = 1;
const UNIX_DIAG_PEER: u16 = 2;
const UDIAG_SHOW_VFS: u32 = 0x0000_0002;
const UDIAG_SHOW_PEER: u32 = 0x0000_0004;
#[expect(clippy::arithmetic_side_effects)]
#[expect(clippy::cast_possible_truncation)]
pub fn peer_inode<Fd: AsFd>(fd: Fd) -> Result<u64, Errno> {
let stx = fstatx(fd, STATX_INO)?;
let local_ino = stx.stx_ino;
let local_ino32 = (local_ino & 0xffff_ffff) as u32;
let nl = safe_socket(
libc::AF_NETLINK,
libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
libc::NETLINK_SOCK_DIAG,
)?;
let mut req = [0u8; NL_HDR_LEN + UD_REQ_LEN];
let mut p = 0usize;
req[p..p + 4].copy_from_slice(&NL_MSG_LEN.to_ne_bytes()); p += 4;
req[p..p + 2].copy_from_slice(&SOCK_DIAG_BY_FAMILY.to_ne_bytes()); p += 2;
let nl_flags = (libc::NLM_F_REQUEST | libc::NLM_F_ROOT | libc::NLM_F_MATCH) as u16;
req[p..p + 2].copy_from_slice(&nl_flags.to_ne_bytes()); p += 2;
req[p..p + 4].copy_from_slice(&1u32.to_ne_bytes()); p += 4;
req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes()); p += 4;
req[p] = libc::AF_UNIX as u8;
p += 1; req[p] = 0u8;
p += 1; req[p..p + 2].copy_from_slice(&0u16.to_ne_bytes());
p += 2; req[p..p + 4].copy_from_slice(&u32::MAX.to_ne_bytes());
p += 4; req[p..p + 4].copy_from_slice(&local_ino32.to_ne_bytes());
p += 4; req[p..p + 4].copy_from_slice(&UDIAG_SHOW_PEER.to_ne_bytes());
p += 4; req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes());
p += 4; req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes());
p += 4; assert_eq!(p, req.len());
let mut sent_total = 0usize;
while sent_total < req.len() {
let slice = &req[sent_total..];
let sent = retry_on_eintr(|| write(&nl, slice))?;
if sent == 0 {
return Err(Errno::EIO);
}
sent_total = sent_total.saturating_add(sent);
}
let mut rbuf = [0u8; 0x8000];
loop {
let n = retry_on_eintr(|| read(&nl, &mut rbuf))?;
if n == 0 {
return Err(Errno::EIO);
}
let mut off = 0usize;
while off + NL_HDR_LEN <= n {
let nlmsg_len = {
let b: [u8; 4] = rbuf[off..off + 4].try_into().or(Err(Errno::EOVERFLOW))?;
u32::from_ne_bytes(b) as usize
};
if nlmsg_len == 0 || off + nlmsg_len > n {
return Err(Errno::EIO);
}
let nlmsg_type = {
let b: [u8; 2] = rbuf[off + 4..off + 6]
.try_into()
.or(Err(Errno::EOVERFLOW))?;
u16::from_ne_bytes(b)
};
if nlmsg_type == NLMSG_DONE {
return Ok(local_ino);
} else if nlmsg_type == NLMSG_ERROR {
if nlmsg_len >= NL_HDR_LEN + 4 {
let err_b: [u8; 4] = rbuf[off + NL_HDR_LEN..off + NL_HDR_LEN + 4]
.try_into()
.or(Err(Errno::EOVERFLOW))?;
let nl_err = i32::from_ne_bytes(err_b);
return Err(Errno::from_raw(-nl_err));
} else {
return Err(Errno::EIO);
}
} else if nlmsg_type == SOCK_DIAG_BY_FAMILY {
let payload_off = off + NL_HDR_LEN;
let ud_min = 16usize;
if payload_off + ud_min > off + nlmsg_len {
return Err(Errno::EIO);
}
let found_ino32 = {
let b: [u8; 4] = rbuf[payload_off + 4..payload_off + 8]
.try_into()
.or(Err(Errno::EOVERFLOW))?;
u64::from(u32::from_ne_bytes(b))
};
if (found_ino32 & 0xffff_ffff) != (local_ino & 0xffff_ffff) {
off = nlmsg_align(off + nlmsg_len);
continue;
}
let mut attr_off = payload_off + ud_min;
while attr_off + 4 <= off + nlmsg_len {
let nla_len = {
let b: [u8; 2] = rbuf[attr_off..attr_off + 2]
.try_into()
.or(Err(Errno::EOVERFLOW))?;
u16::from_ne_bytes(b) as usize
};
let nla_type = {
let b: [u8; 2] = rbuf[attr_off + 2..attr_off + 4]
.try_into()
.or(Err(Errno::EOVERFLOW))?;
u16::from_ne_bytes(b)
};
if nla_len < 4 {
break;
}
let payload_start = attr_off + 4;
let payload_len = nla_len - 4;
if payload_start + payload_len > off + nlmsg_len {
break;
}
if nla_type == UNIX_DIAG_PEER && payload_len >= 4 {
let peer_b: [u8; 4] = rbuf[payload_start..payload_start + 4]
.try_into()
.or(Err(Errno::EOVERFLOW))?;
let peer_ino = u64::from(u32::from_ne_bytes(peer_b));
return Ok(peer_ino);
}
attr_off = attr_off.saturating_add(nla_align(nla_len));
}
}
off = nlmsg_align(off + nlmsg_len);
}
}
}
#[expect(clippy::arithmetic_side_effects)]
#[expect(clippy::cast_possible_truncation)]
pub fn unix_inodes() -> Result<SydHashSet<u64>, Errno> {
let nl = safe_socket(
libc::AF_NETLINK,
libc::SOCK_DGRAM | libc::SOCK_CLOEXEC,
libc::NETLINK_SOCK_DIAG,
)?;
let mut req = [0u8; NL_HDR_LEN + UD_REQ_LEN];
let mut p = 0usize;
req[p..p + 4].copy_from_slice(&NL_MSG_LEN.to_ne_bytes()); p += 4;
req[p..p + 2].copy_from_slice(&SOCK_DIAG_BY_FAMILY.to_ne_bytes()); p += 2;
let nl_flags = (libc::NLM_F_REQUEST | libc::NLM_F_ROOT | libc::NLM_F_MATCH) as u16;
req[p..p + 2].copy_from_slice(&nl_flags.to_ne_bytes()); p += 2;
req[p..p + 4].copy_from_slice(&1u32.to_ne_bytes()); p += 4;
req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes()); p += 4;
req[p] = libc::AF_UNIX as u8;
p += 1; req[p] = 0u8;
p += 1; req[p..p + 2].copy_from_slice(&0u16.to_ne_bytes());
p += 2; req[p..p + 4].copy_from_slice(&u32::MAX.to_ne_bytes());
p += 4; req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes());
p += 4; req[p..p + 4].copy_from_slice(&UDIAG_SHOW_VFS.to_ne_bytes());
p += 4; req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes());
p += 4; req[p..p + 4].copy_from_slice(&0u32.to_ne_bytes());
p += 4; assert_eq!(p, req.len());
let mut sent_total = 0usize;
while sent_total < req.len() {
let slice = &req[sent_total..];
let sent = retry_on_eintr(|| write(&nl, slice))?;
if sent == 0 {
return Err(Errno::EIO);
}
sent_total = sent_total.saturating_add(sent);
}
let mut rbuf = [0u8; 0x8000];
let mut iset = SydHashSet::default();
'recv: loop {
let n = retry_on_eintr(|| read(&nl, &mut rbuf))?;
if n == 0 {
return Err(Errno::EIO);
}
let mut off = 0usize;
while off + NL_HDR_LEN <= n {
let nlmsg_len = {
let b: [u8; 4] = rbuf[off..off + 4].try_into().or(Err(Errno::EOVERFLOW))?;
u32::from_ne_bytes(b) as usize
};
if nlmsg_len == 0 || off + nlmsg_len > n {
return Err(Errno::EIO);
}
let nlmsg_type = {
let b: [u8; 2] = rbuf[off + 4..off + 6]
.try_into()
.or(Err(Errno::EOVERFLOW))?;
u16::from_ne_bytes(b)
};
if nlmsg_type == NLMSG_DONE {
break 'recv;
} else if nlmsg_type == NLMSG_ERROR {
if nlmsg_len >= NL_HDR_LEN + 4 {
let err_b: [u8; 4] = rbuf[off + NL_HDR_LEN..off + NL_HDR_LEN + 4]
.try_into()
.or(Err(Errno::EOVERFLOW))?;
let nl_err = i32::from_ne_bytes(err_b);
return Err(Errno::from_raw(-nl_err));
} else {
return Err(Errno::EIO);
}
} else if nlmsg_type == SOCK_DIAG_BY_FAMILY {
let payload_off = off + NL_HDR_LEN;
let ud_min = 16usize;
if payload_off + ud_min > off + nlmsg_len {
return Err(Errno::EIO);
}
let ino32 = {
let b: [u8; 4] = rbuf[payload_off + 4..payload_off + 8]
.try_into()
.or(Err(Errno::EOVERFLOW))?;
u32::from_ne_bytes(b)
};
let mut has_vfs = false;
let mut attr_off = payload_off + ud_min;
let attrs_end = off + nlmsg_len;
while attr_off + 4 <= attrs_end {
let nla_len = {
let b: [u8; 2] = rbuf[attr_off..attr_off + 2]
.try_into()
.or(Err(Errno::EOVERFLOW))?;
u16::from_ne_bytes(b) as usize
};
let nla_type = {
let b: [u8; 2] = rbuf[attr_off + 2..attr_off + 4]
.try_into()
.or(Err(Errno::EOVERFLOW))?;
u16::from_ne_bytes(b)
};
if nla_len < 4 {
break;
}
let payload_start = attr_off + 4;
let payload_len = nla_len - 4;
if payload_start > attrs_end || payload_start + payload_len > attrs_end {
break;
}
if nla_type == UNIX_DIAG_VFS {
has_vfs = true;
break;
}
let next = attr_off.saturating_add(nla_align(nla_len));
if next <= attr_off {
break;
} attr_off = next;
}
if has_vfs {
iset.try_reserve(1).or(Err(Errno::ENOMEM))?;
let _ = iset.insert(ino32.into());
}
}
off = nlmsg_align(off + nlmsg_len);
}
}
Ok(iset)
}
pub fn parse_fd(path: &XPath) -> Result<RawFd, Errno> {
btoi::<RawFd>(path.as_bytes()).or(Err(Errno::EBADF))
}
pub fn readlinkat<Fd: AsFd>(fd: Fd, base: &XPath) -> Result<XPathBuf, Errno> {
nix::fcntl::readlinkat(fd, base).map(XPathBuf::from)
}
pub fn readlinkfd<Fd: AsFd>(fd: Fd) -> Result<XPathBuf, Errno> {
let mut target = XPathBuf::with_capacity(PATH_MAX);
#[expect(clippy::cast_sign_loss)]
let n = Errno::result(unsafe {
libc::readlinkat(
fd.as_fd().as_raw_fd(),
c"".as_ptr(),
target.as_mut_ptr().cast(),
target.capacity(),
)
})
.map(|n| n as usize)
.map_err(|errno| {
if errno == Errno::ENOENT {
Errno::EINVAL
} else {
errno
}
})?;
if n >= target.capacity() {
return Err(Errno::ENAMETOOLONG);
}
unsafe { target.set_len(n) };
target.shrink_to_fit();
Ok(target)
}
#[expect(clippy::disallowed_methods)]
pub fn cat<P: AsRef<Path>, T: AsRef<[u8]>>(path: P, content: T) -> std::io::Result<()> {
let mut file = File::create(path)?;
file.write_all(content.as_ref())?;
Ok(())
}
pub fn chmod_x<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
let metadata = metadata(path.as_ref())?;
let mut permissions = metadata.permissions();
permissions.set_mode(0o700); set_permissions(path.as_ref(), permissions)
}
#[derive(Debug, PartialEq)]
enum PathComponent {
ParentDir,
Normal(XPathBuf),
}
impl Serialize for PathComponent {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let comp = match self {
Self::Normal(p) => p.to_owned(),
Self::ParentDir => XPathBuf::from(".."),
};
serializer.serialize_str(&comp.to_string())
}
}
fn path_components(path: &XPath) -> Result<(VecDeque<PathComponent>, bool), Errno> {
let searcher = memchr::arch::all::memchr::One::new(b'/');
let path_bytes = path.as_os_str().as_bytes();
let mut components = VecDeque::new();
let comp_count = searcher.iter(path_bytes).count().min(PATH_MAX_COMP);
components.try_reserve(comp_count).or(Err(Errno::ENOMEM))?;
let mut start = 0;
#[expect(clippy::arithmetic_side_effects)]
for pos in searcher.iter(path_bytes) {
if pos > start {
match &path_bytes[start..pos] {
b"." => {} b".." => components.push_back(PathComponent::ParentDir),
component => {
if component.len() >= PATH_MAX {
return Err(Errno::ENAMETOOLONG);
}
components.push_back(PathComponent::Normal(component.into()));
}
}
}
start = pos + 1; }
#[expect(clippy::arithmetic_side_effects)]
let slash = if start < path_bytes.len() {
match &path_bytes[start..] {
b"." => {
components.is_empty() || (start >= 1 && path_bytes[start - 1] == b'/')
}
b".." => {
components.push_back(PathComponent::ParentDir);
true
}
component => {
if component.len() >= PATH_MAX {
return Err(Errno::ENAMETOOLONG);
}
components.push_back(PathComponent::Normal(component.into()));
false
}
}
} else {
true
};
Ok((components, slash))
}
fn path_components2(path: &XPath, components: &mut VecDeque<PathComponent>) -> Result<(), Errno> {
let path_bytes = path.as_os_str().as_bytes();
let searcher = memchr::arch::all::memchr::One::new(b'/');
let mut last_pos = path_bytes.len();
let mut last_component = true;
#[expect(clippy::arithmetic_side_effects)]
for pos in searcher.iter(path_bytes).rev() {
match &path_bytes[pos + 1..last_pos] {
b"" | b"." => {} b".." => {
components.try_reserve(1).or(Err(Errno::ENOMEM))?;
components.push_front(PathComponent::ParentDir);
}
component => {
let mut component: XPathBuf = component.into();
if last_component && path.ends_with(b"/") {
component.append_byte(b'/');
}
last_component = false;
components.try_reserve(1).or(Err(Errno::ENOMEM))?;
components.push_front(PathComponent::Normal(component));
}
}
last_pos = pos;
}
match &path_bytes[..last_pos] {
b"" | b"." => {} b".." => {
components.try_reserve(1).or(Err(Errno::ENOMEM))?;
components.push_front(PathComponent::ParentDir);
}
component => {
let mut component: XPathBuf = component.into();
if last_component && path.ends_with(b"/") {
component.append_byte(b'/');
}
components.try_reserve(1).or(Err(Errno::ENOMEM))?;
components.push_front(PathComponent::Normal(component));
}
}
Ok(())
}
#[expect(clippy::arithmetic_side_effects)]
pub fn getdir_long<Fd: AsFd>(fd: Fd, max_components: usize) -> Result<XPathBuf, Errno> {
let mut pinfo = FileInfo::from_fd(&fd)?;
let mut dir: Box<dyn AsFd> = Box::new(fd);
let mut cwd = Vec::new();
cwd.try_reserve(PATH_MAX).or(Err(Errno::ENOMEM))?;
let flags = (OFlag::O_RDONLY
| OFlag::O_CLOEXEC
| OFlag::O_DIRECTORY
| OFlag::O_LARGEFILE
| OFlag::O_NOCTTY
| OFlag::O_NOFOLLOW)
.bits();
let mut i = 0;
while i < max_components {
let fd = retry_on_eintr(|| {
Errno::result(unsafe {
openat(
dir.as_fd().as_raw_fd(),
dotdot_with_nul() as *const c_char,
flags,
0,
)
})
})
.map(|fd| {
unsafe { OwnedFd::from_raw_fd(fd) }
})?;
dir = Box::new(fd);
let info = FileInfo::from_fd(&dir)?;
if info == pinfo {
let cwd = if !cwd.is_empty() {
cwd.reverse();
cwd.into()
} else {
XPathBuf::from("/")
};
return Ok(cwd);
}
let mut dot = 0u8;
let mut found = false;
let new_device = info.mnt != pinfo.mnt;
'main: loop {
let mut entries = match getdents64(&dir, DIRENT_BUF_SIZE) {
Ok(entries) => entries,
Err(Errno::ECANCELED) => break, Err(errno) => return Err(errno),
};
for entry in &mut entries {
if dot < 2 && entry.is_dot() {
dot += 1;
continue;
} else if !new_device && entry.ino() != pinfo.ino {
continue;
} else if pinfo
!= statx(
&dir,
entry.as_xpath(),
AT_SYMLINK_NOFOLLOW,
FileInfo::mask(),
)
.map(FileInfo::from_statx)?
{
continue;
}
found = true;
pinfo = info;
cwd.try_reserve(entry.name_bytes().len().saturating_add(1))
.or(Err(Errno::ENOMEM))?;
cwd.extend(entry.name_bytes().iter().rev());
cwd.push(b'/');
break 'main;
}
}
if found {
i += 1;
} else {
return Err(Errno::ENOENT);
}
}
Err(Errno::ERANGE)
}
pub enum MaybeFd {
Owned(OwnedFd),
RawFd(RawFd),
}
impl Clone for MaybeFd {
fn clone(&self) -> Self {
match self {
MaybeFd::Owned(fd) => MaybeFd::RawFd(fd.as_raw_fd()),
MaybeFd::RawFd(fd) => MaybeFd::RawFd(*fd),
}
}
}
impl AsFd for MaybeFd {
fn as_fd(&self) -> BorrowedFd<'_> {
match self {
MaybeFd::Owned(owned) => owned.as_fd(),
MaybeFd::RawFd(fd) => unsafe { BorrowedFd::borrow_raw(*fd) },
}
}
}
impl AsRawFd for MaybeFd {
fn as_raw_fd(&self) -> RawFd {
match self {
MaybeFd::Owned(owned) => owned.as_raw_fd(),
MaybeFd::RawFd(fd) => *fd,
}
}
}
impl From<OwnedFd> for MaybeFd {
fn from(fd: OwnedFd) -> Self {
MaybeFd::Owned(fd)
}
}
impl From<RawFd> for MaybeFd {
fn from(fd: RawFd) -> Self {
MaybeFd::RawFd(fd)
}
}
impl fmt::Debug for MaybeFd {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MaybeFd::Owned(_) => f.debug_tuple("OwnedFd").field(&self.as_raw_fd()).finish(),
MaybeFd::RawFd(_) => f.debug_tuple("RawFd").field(&self.as_raw_fd()).finish(),
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct FileMapEntry {
fd: MaybeFd,
f_type: Option<FileType>,
f_mode: Option<u16>,
mnt_id: Option<u64>,
target: Option<Result<XPathBuf, Errno>>,
nvisit: u8, }
#[derive(Debug, Clone)]
pub(crate) struct FileMap(pub(crate) SydHashMap<XPathBuf, FileMapEntry>);
impl FileMapEntry {
pub(crate) fn new(
fd: MaybeFd,
f_type: Option<FileType>,
f_mode: Option<u16>,
mnt_id: Option<u64>,
target: Option<Result<XPathBuf, Errno>>,
) -> Self {
Self {
fd,
f_type,
f_mode,
mnt_id,
target,
nvisit: 0,
}
}
#[expect(clippy::cognitive_complexity)]
fn from_magic_link(
magic: ProcMagic,
want_dir: bool,
sandbox: Option<&Sandbox>,
) -> Result<(Self, XPathBuf), Errno> {
let remote_sym = magic.link_path();
let flags = if want_dir || magic.want_dir() {
OFlag::O_PATH | OFlag::O_DIRECTORY
} else {
OFlag::O_PATH
};
let fd = safe_open_msym(PROC_FILE(), &remote_sym, flags, ResolveFlag::empty())?;
let sym = XPathBuf::from_self_fd(fd.as_raw_fd());
let (target, is_deleted, is_mfd) = match readlinkat(PROC_FILE(), &sym) {
Ok(mut p) => {
if p.is_relative() {
if magic.want_dir() {
return Err(Errno::EBADF);
}
(Some(p), false, false)
} else if p.ends_with(b" (deleted)") {
if p.starts_with(b"/memfd:") {
p.truncate(
p.len()
.checked_sub(b" (deleted)".len())
.ok_or(Errno::EOVERFLOW)?,
);
p.set(0, b'!');
(Some(p), false, true)
} else {
(Some(p), true, false)
}
} else {
(Some(p), false, false)
}
}
Err(Errno::ENAMETOOLONG) => (None, false, false),
Err(errno) => return Err(errno),
};
let mut sym = XPathBuf::from("/proc");
sym.push(remote_sym.as_bytes());
if let Some(mut target) = target {
if !target.is_root() && target.is_absolute() {
if let Some(sandbox) = sandbox {
sandbox_path(
None,
sandbox,
magic.pid(),
target.deref(),
Capability::CAP_WALK,
false,
"walk",
)?;
}
}
if is_deleted {
let stx = retry_on_eintr(|| fstatx(&fd, STATX_TYPE | STATX_NLINK))?;
let f_type = FileType::from(mode_t::from(stx.stx_mode));
if stx.stx_nlink == 0 {
target.truncate(
target
.len()
.checked_sub(b" (deleted)".len())
.ok_or(Errno::EOVERFLOW)?,
);
}
let entry = Self {
fd: fd.into(),
f_type: Some(f_type),
f_mode: None,
mnt_id: None,
target: Some(Ok(target)),
nvisit: 0,
};
return Ok((entry, sym));
}
let f_type = if is_mfd {
if is_huge_file(&fd).unwrap_or(false) {
target.replace_prefix(MFD_NAME_PREFIX, MFD_HUGETLB_NAME_PREFIX)?;
}
Some(FileType::Mfd)
} else if magic.want_dir() {
Some(FileType::Dir)
} else {
file_type(&fd, None, false).ok()
};
let entry = Self {
fd: fd.into(),
f_type,
f_mode: None,
mnt_id: None,
target: Some(Ok(target)),
nvisit: 0,
};
return Ok((entry, sym));
}
let target = getdir_long(&fd, PATH_MAX_COMP)?;
if !target.is_root() && target.is_absolute() {
if let Some(sandbox) = sandbox {
sandbox_path(
None,
sandbox,
magic.pid(),
target.deref(),
Capability::CAP_WALK,
false,
"walk",
)?;
}
}
let entry = Self {
fd: fd.into(),
f_type: Some(FileType::Dir),
f_mode: None,
mnt_id: None,
target: Some(Ok(target)),
nvisit: 0,
};
Ok((entry, sym))
}
#[expect(clippy::cast_possible_truncation)]
fn get_mode(&mut self) -> Result<u16, Errno> {
if let Some(mode) = self.f_mode {
return Ok(mode);
}
let stx = retry_on_eintr(|| fstatx(&self.fd, STATX_TYPE | STATX_MODE))?;
if !self.is_magic_link() {
let f_type = FileType::from(mode_t::from(stx.stx_mode));
self.f_type = Some(f_type);
}
let mode = stx.stx_mode & !(S_IFMT as u16);
self.f_mode = Some(mode);
Ok(mode)
}
fn is_magic_link(&self) -> bool {
matches!(self.f_type, Some(FileType::MagicLnk(_)))
}
}
impl FileMap {
pub(crate) fn with_capacity(cap: usize) -> Self {
Self(SydHashMap::with_capacity(cap))
}
fn remove(&mut self, path: &XPath) -> Option<FileMapEntry> {
self.0.remove(path)
}
fn get(&self, path: &XPath) -> Option<&FileMapEntry> {
self.0.get(path)
}
fn open(
&mut self,
pid: Pid,
path: &XPath,
options: FsFlags,
filetyp: Option<FileType>,
sandbox: Option<&Sandbox>,
) -> Result<&mut FileMapEntry, Errno> {
if !path.is_root() {
if let Some(sandbox) = sandbox {
sandbox_path(
None,
sandbox,
pid,
path,
Capability::CAP_WALK,
false,
"walk",
)?;
}
}
let (parent, base) = path.split();
let (parent_fd, base) = if let Some(parent_entry) = self.0.get(parent) {
(parent_entry.fd.as_fd(), base)
} else if path.starts_with(b"/proc/") {
let pfd = if let Some(pfd) = self.0.get(parent) {
pfd.fd.as_fd()
} else {
let parent = XPath::from_bytes(&parent.as_bytes()[b"/proc/".len()..]);
let dfd: MaybeFd = if parent.is_empty() {
PROC_FD().into()
} else {
safe_open_path(
PROC_FILE(),
parent,
OFlag::O_DIRECTORY,
ResolveFlag::empty(),
)?
.into()
};
self.0.try_reserve(1).or(Err(Errno::ENOMEM))?;
self.0.insert(
parent.to_owned(),
FileMapEntry::new(dfd, Some(FileType::Dir), None, None, None),
);
#[expect(clippy::disallowed_methods)]
{
self.0.get(parent).unwrap().fd.as_fd()
}
};
(pfd, base)
} else {
let base = XPath::from_bytes(&path.as_bytes()[1..]);
(ROOT_FILE(), base)
};
let rflags = if options.contains(FsFlags::NO_RESOLVE_XDEV) {
ResolveFlag::RESOLVE_NO_XDEV
} else {
ResolveFlag::empty()
};
let fd = safe_open_path(parent_fd, base, OFlag::O_NOFOLLOW, rflags)?;
self.0.try_reserve(1).or(Err(Errno::ENOMEM))?;
let entry = FileMapEntry::new(fd.into(), filetyp, None, None, None);
self.0.insert(path.to_owned(), entry);
self.0.get_mut(path).ok_or(Errno::ENOENT)
}
#[expect(clippy::cognitive_complexity)]
fn readlink(
&mut self,
path: &XPath,
pid: Pid,
options: FsFlags,
filetyp: Option<FileType>,
sandbox: Option<&Sandbox>,
) -> Result<XPathBuf, Errno> {
if path.is_static() {
return Err(Errno::EINVAL);
}
let entry = if let Some(entry) = self.0.get_mut(path) {
match entry.target.as_ref() {
Some(Ok(target)) => {
#[expect(clippy::arithmetic_side_effects)]
return if entry.nvisit > 16 {
Err(Errno::ELOOP)
} else {
entry.nvisit += 1;
Ok(target.to_owned())
};
}
Some(Err(errno)) => {
return Err(*errno);
}
None => entry,
}
} else {
self.open(pid, path, options, filetyp, sandbox)?
};
let target = match readlinkfd(&entry.fd) {
Ok(target) => target,
Err(errno) => {
entry.target = Some(Err(errno));
return Err(errno);
}
};
if !options.resolve_path() {
return Err(Errno::ELOOP);
}
if target.is_absolute() && options.contains(FsFlags::RESOLVE_BENEATH) {
return Err(Errno::EXDEV);
}
entry.target = Some(Ok(target.clone()));
let restrict_symlinks = sandbox
.map(|sb| !sb.flags.allow_unsafe_symlinks())
.unwrap_or(false);
if restrict_symlinks {
if let Some(entry) = self.0.get_mut(path.parent()) {
let mut err = None;
let mode: u32 = entry.get_mode()?.into();
if mode & S_ISVTX != 0 {
err = Some("parent directory has sticky bit set");
} else if mode & S_IWOTH != 0 {
err = Some("parent directory is world writable");
} else if mode & S_IWGRP != 0 {
err = Some("parent directory is group writable");
}
if let Some(msg) = err {
error!("ctx": "trusted_symlinks", "path": path,
"pid": pid.as_raw(), "rflags": options,
"msg": format!("follow for untrusted symlink blocked: {msg}"),
"tip": "fix parent directory permissions or use `trace/allow_unsafe_symlinks:1'");
return Err(Errno::ELOOP);
}
}
}
Ok(target)
}
fn get_mnt_id(
&mut self,
path: &XPath,
pid: Pid,
options: FsFlags,
filetyp: Option<FileType>,
sandbox: Option<&Sandbox>,
) -> Result<u64, Errno> {
let mut mask = STATX_TYPE;
mask |= if *HAVE_STATX_MNT_ID_UNIQUE {
STATX_MNT_ID_UNIQUE
} else {
STATX_MNT_ID
};
let entry = if let Some(entry) = self.0.get_mut(path) {
entry
} else {
self.open(pid, path, options, filetyp, sandbox)?
};
if let Some(mnt_id) = entry.mnt_id {
return Ok(mnt_id);
}
let stx = retry_on_eintr(|| fstatx(&entry.fd, mask))?;
if !entry.is_magic_link() {
let f_type = FileType::from(mode_t::from(stx.stx_mode));
entry.f_type = Some(f_type);
}
let mnt_id = stx.stx_mnt_id;
entry.mnt_id = Some(mnt_id);
Ok(mnt_id)
}
}
impl Default for FileMap {
fn default() -> Self {
FILEMAP.clone()
}
}
#[derive(Debug)]
pub struct CanonicalPath<'a> {
abs: XPathBuf,
pub base: &'a XPath,
pub dir: Option<MaybeFd>,
pub typ: Option<FileType>,
}
impl fmt::Display for CanonicalPath<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.abs())
}
}
impl Serialize for CanonicalPath<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(4))?;
map.serialize_entry("abs", &self.abs)?;
if self.base.is_empty() {
map.serialize_entry("fd", &self.dir.as_ref().map(|fd| fd.as_raw_fd()))?;
} else {
map.serialize_entry("dir", &self.dir.as_ref().map(|fd| fd.as_raw_fd()))?;
map.serialize_entry("base", self.base)?;
}
map.serialize_entry("type", &self.typ)?;
let owned = matches!(self.dir, Some(MaybeFd::Owned(_)));
map.serialize_entry("owned", &owned)?;
map.end()
}
}
impl<'a> CanonicalPath<'a> {
pub fn is_file(&self) -> bool {
self.typ.as_ref().map(|typ| typ.is_file()).unwrap_or(false)
}
pub fn is_dir(&self) -> bool {
self.typ.as_ref().map(|typ| typ.is_dir()).unwrap_or(false)
}
pub fn is_symlink(&self) -> bool {
self.typ
.as_ref()
.map(|typ| typ.is_symlink())
.unwrap_or(false)
}
pub fn is_magic_link(&self) -> bool {
self.typ
.as_ref()
.map(|typ| typ.is_magic_link())
.unwrap_or(false)
}
pub fn is_magic_dir(&self) -> bool {
self.typ
.as_ref()
.map(|typ| typ.is_magic_dir())
.unwrap_or(false)
}
pub fn is_memory_fd(&self) -> bool {
self.typ
.as_ref()
.map(|typ| typ.is_memory_fd())
.unwrap_or(false)
}
pub fn is_syd_memory_fd(&self) -> bool {
self.is_memory_fd() && self.abs().starts_with(b"!memfd:syd-")
}
pub fn is_block_device(&self) -> bool {
self.typ
.as_ref()
.map(|typ| typ.is_block_device())
.unwrap_or(false)
}
pub fn is_char_device(&self) -> bool {
self.typ
.as_ref()
.map(|typ| typ.is_char_device())
.unwrap_or(false)
}
pub fn is_fifo(&self) -> bool {
self.typ.as_ref().map(|typ| typ.is_fifo()).unwrap_or(false)
}
pub fn is_socket(&self) -> bool {
self.typ
.as_ref()
.map(|typ| typ.is_socket())
.unwrap_or(false)
}
pub fn is_unknown(&self) -> bool {
self.typ
.as_ref()
.map(|typ| typ.is_unknown())
.unwrap_or(false)
}
pub fn abs(&self) -> &XPath {
XPath::from_bytes(self.abs.as_bytes())
}
pub fn parent(&self) -> &XPath {
self.abs().split().0
}
pub fn take(self) -> XPathBuf {
self.abs
}
pub fn new(abs: XPathBuf, typ: FileType, options: FsFlags) -> Result<CanonicalPath<'a>, Errno> {
if abs.is_rootfs() {
return Ok(Self::new_root());
} else if abs.is_procfs() {
return Ok(Self::new_proc());
} else if abs.is_equal(b"/dev/null") {
return Ok(Self::new_null());
}
let flags = if options.contains(FsFlags::WANT_READ) {
OFlag::O_RDONLY | OFlag::O_NONBLOCK | OFlag::O_NOCTTY
} else if typ.is_dir() {
OFlag::O_PATH | OFlag::O_DIRECTORY
} else {
OFlag::O_PATH
};
let fd = safe_open(AT_FDCWD, &abs, flags, ResolveFlag::empty())?;
Ok(Self {
abs,
base: XPath::from_bytes(b""),
dir: Some(fd.into()),
typ: Some(typ),
})
}
pub fn new_root() -> CanonicalPath<'a> {
Self {
abs: XPathBuf::from("/"),
base: XPath::from_bytes(b""),
dir: Some(ROOT_FD().into()),
typ: Some(FileType::Dir),
}
}
pub fn new_proc() -> CanonicalPath<'a> {
Self {
abs: XPathBuf::from("/proc"),
base: XPath::from_bytes(b""),
dir: Some(PROC_FD().into()),
typ: Some(FileType::Dir),
}
}
pub fn new_magic(virtual_path: XPathBuf) -> CanonicalPath<'a> {
Self {
abs: virtual_path,
base: XPath::from_bytes(b""),
dir: Some(NULL_FD().into()),
typ: Some(FileType::Chr),
}
}
pub fn new_null() -> CanonicalPath<'a> {
Self {
abs: XPathBuf::from("/dev/null"),
base: XPath::from_bytes(b""),
dir: Some(NULL_FD().into()),
typ: Some(FileType::Chr),
}
}
pub fn new_mask(mask: &XPath, abs: &XPath) -> Result<CanonicalPath<'a>, Errno> {
let base = XPath::from_bytes(&mask.as_bytes()[b"/".len()..]);
let fd = safe_open_path(ROOT_FILE(), base, OFlag::O_NOFOLLOW, ResolveFlag::empty())?;
let typ = file_type(&fd, None, false)?;
Ok(Self {
abs: abs.to_owned(), base: XPath::from_bytes(b""),
dir: Some(fd.into()), typ: Some(typ),
})
}
pub fn new_tty(abs: XPathBuf) -> Result<CanonicalPath<'a>, Errno> {
assert!(
abs.is_dev(),
"BUG: non /dev path passed to CanonicalPath::new_tty!"
);
let base = XPath::from_bytes(&abs.as_bytes()[b"/".len()..]);
let fd = safe_open_path(ROOT_FILE(), base, OFlag::O_NOFOLLOW, ResolveFlag::empty())?;
Ok(Self {
abs,
base: XPath::empty(),
dir: Some(fd.into()),
typ: Some(FileType::Chr),
})
}
pub fn new_crypt(fd: MaybeFd, abs: XPathBuf) -> CanonicalPath<'a> {
Self {
abs,
base: XPath::empty(),
dir: Some(fd),
typ: Some(FileType::Reg),
}
}
pub fn new_fd(mut fd: MaybeFd, pid: Pid) -> Result<CanonicalPath<'a>, Errno> {
let cwd = fd.as_raw_fd() == libc::AT_FDCWD;
if cwd {
let flags = OFlag::O_PATH | OFlag::O_DIRECTORY;
let mut sym = XPathBuf::from_pid(pid);
sym.push(b"cwd");
fd = safe_open_msym(PROC_FILE(), &sym, flags, ResolveFlag::empty())?.into();
}
let sym = XPathBuf::from_self_fd(fd.as_raw_fd());
let (target, is_deleted, is_mfd) = match readlinkat(PROC_FILE(), &sym) {
Ok(mut p) => {
if p.is_relative() {
if cwd {
return Err(Errno::ENOTDIR);
}
let mut sym = XPathBuf::from("/proc");
sym.push_pid(pid);
sym.push(b"fd");
sym.push(p.as_bytes());
(Some(sym), false, false)
} else if p.ends_with(b" (deleted)") {
if p.starts_with(b"/memfd:") {
p.truncate(
p.len()
.checked_sub(b" (deleted)".len())
.ok_or(Errno::EOVERFLOW)?,
);
p.set(0, b'!');
(Some(p), false, true)
} else {
(Some(p), true, false)
}
} else {
(Some(p), false, false)
}
}
Err(Errno::ENOENT) => return Err(Errno::EBADF),
Err(Errno::ENAMETOOLONG) => (None, false, false),
Err(errno) => return Err(errno),
};
if let Some(mut path) = target {
if is_deleted {
let stx = retry_on_eintr(|| fstatx(&fd, STATX_TYPE | STATX_NLINK))?;
let f_type = FileType::from(mode_t::from(stx.stx_mode));
if stx.stx_nlink == 0 {
#[expect(clippy::arithmetic_side_effects)]
path.truncate(path.len() - b" (deleted)".len());
}
return Ok(Self {
abs: path,
base: XPath::from_bytes(b""),
dir: Some(fd),
typ: Some(f_type),
});
}
let file_type = if is_mfd {
if is_huge_file(&fd).unwrap_or(false) {
path.replace_prefix(MFD_NAME_PREFIX, MFD_HUGETLB_NAME_PREFIX)?;
}
Some(FileType::Mfd)
} else if cwd {
Some(FileType::Dir)
} else {
file_type(&fd, None, false).ok()
};
return Ok(Self {
abs: path,
base: XPath::from_bytes(b""),
dir: Some(fd),
typ: file_type,
});
}
let path = getdir_long(&fd, PATH_MAX_COMP)?;
Ok(Self {
abs: path,
base: XPath::from_bytes(b""),
dir: Some(fd),
typ: Some(FileType::Dir),
})
}
#[expect(clippy::cognitive_complexity)]
fn new_map(
mut abs: XPathBuf,
mut typ: Option<FileType>,
options: FsFlags,
mut filemap: FileMap,
restrict_mkbdev: bool,
) -> Result<CanonicalPath<'a>, Errno> {
if abs.is_rootfs() {
return Ok(Self::new_root());
} else if abs.is_procfs() {
return Ok(Self::new_proc());
} else if abs.is_equal(b"/dev/null") {
return Ok(Self::new_null());
}
let follow_last = options.follow_last();
let (mut is_magic_link, magic_base) = match typ {
Some(FileType::MagicLnk(magic @ (ProcMagic::Pid { .. } | ProcMagic::Tid { .. }))) => {
if follow_last {
(false, magic.base())
} else {
(true, None)
}
}
Some(FileType::MagicLnk(ref magic)) => (true, magic.base()),
_ => (false, None),
};
let has_trailing_slash = abs.ends_with_slash();
if has_trailing_slash {
#[expect(clippy::arithmetic_side_effects)]
abs.truncate(abs.len() - 1);
}
let entry = filemap.remove(&abs);
let mut magic_parent: XPathBuf;
let (parent, mut base) = abs.split();
#[expect(clippy::disallowed_methods)]
let (parent_fd, has_parent) = if let Some(ref entry) = entry {
typ = if let Some(f_type) = entry.f_type {
Some(f_type)
} else {
file_type(&entry.fd, None, false).ok()
};
magic_parent = XPathBuf::from_self_fd(entry.fd.as_raw_fd());
base = &magic_parent;
is_magic_link = true;
(PROC_FD().into(), false)
} else if let Some(entry_parent) = filemap.remove(parent) {
if let Some(ref magic_base) = magic_base {
base = magic_base;
} else {
typ = if let Some(Some(f_type)) = entry.as_ref().map(|e| e.f_type) {
Some(f_type)
} else {
file_type(&entry_parent.fd, Some(base), false).ok()
};
}
(entry_parent.fd, true)
} else if let Some(ref magic_base) = magic_base {
magic_parent = parent.strip_prefix(b"/proc").unwrap().to_owned();
magic_parent.push(magic_base.as_bytes());
base = &magic_parent;
(PROC_FD().into(), false)
} else if abs.is_proc() {
base = XPath::from_bytes(&abs.as_bytes()[b"/proc/".len()..]);
if !is_magic_link {
typ = if let Some(Some(f_type)) = entry.as_ref().map(|e| e.f_type) {
Some(f_type)
} else {
file_type(PROC_FILE(), Some(base), false).ok()
};
}
(PROC_FD().into(), false)
} else {
base = XPath::from_bytes(&abs.as_bytes()[1..]);
if !is_magic_link {
typ = if let Some(Some(f_type)) = entry.as_ref().map(|e| e.f_type) {
Some(f_type)
} else {
file_type(ROOT_FILE(), Some(base), false).ok()
};
}
(ROOT_FD().into(), false)
};
crate::debug!("ctx": "resolve_path", "op": "open_last",
"path": &abs,
"base": &base,
"type": &typ,
"options": format!("{options:?}"),
"open_files": filemap.0.len());
if follow_last {
match typ {
None if options.must_exist() => {
return Err(Errno::ENOENT);
}
Some(FileType::Lnk) => {
return Err(Errno::ELOOP);
}
_ => {}
}
} else if has_trailing_slash && typ.is_none() && options.must_exist() {
return Err(Errno::ENOENT);
}
let open_parent = options.intersects(FsFlags::MISS_LAST | FsFlags::WANT_BASE)
|| (typ.is_none() && !options.must_exist());
if open_parent {
let parent_fd = if has_parent {
parent_fd
} else if parent.starts_with(b"/proc") {
let fd: MaybeFd = if parent.len() == b"/proc".len() {
PROC_FD().into()
} else {
let parent_base = XPath::from_bytes(&parent.as_bytes()[b"/proc/".len()..]);
safe_open_path(
PROC_FILE(),
parent_base,
OFlag::O_DIRECTORY,
ResolveFlag::empty(),
)?
.into()
};
fd
} else {
let parent_base = XPath::from_bytes(&parent.as_bytes()[1..]);
let fd: MaybeFd = if parent_base.is_empty() {
ROOT_FD().into()
} else {
safe_open_path(
ROOT_FILE(),
parent_base,
OFlag::O_DIRECTORY,
ResolveFlag::empty(),
)?
.into()
};
fd
};
let parent_len = parent.len();
if has_trailing_slash {
abs.append_byte(b'/');
};
#[expect(clippy::arithmetic_side_effects)]
{
let offset = parent_len + usize::from(parent_len > 1);
base = XPath::from_bytes(&abs.as_bytes()[offset..]);
}
let base = unsafe { std::mem::transmute::<&XPath, &'a XPath>(base) };
return Ok(Self {
abs,
base,
dir: Some(parent_fd),
typ,
});
}
let (mut flags, is_read) = if options.contains(FsFlags::WANT_READ) {
(OFlag::O_RDONLY | OFlag::O_NOCTTY | OFlag::O_NONBLOCK, true)
} else {
(OFlag::O_PATH, false)
};
if has_trailing_slash {
flags.insert(OFlag::O_DIRECTORY);
}
if restrict_mkbdev && is_read && typ.as_ref().map(|t| t.is_block_device()).unwrap_or(false)
{
return Err(Errno::ENOENT);
}
if !is_read {
if let Some(entry) = entry {
if has_trailing_slash {
abs.append_byte(b'/');
}
return Ok(Self {
abs,
base: XPath::from_bytes(b""),
dir: Some(entry.fd),
typ,
});
}
}
if !follow_last && matches!(typ, Some(FileType::Lnk)) {
flags.remove(OFlag::O_RDONLY | OFlag::O_NONBLOCK | OFlag::O_NOCTTY);
flags.insert(OFlag::O_PATH | OFlag::O_NOFOLLOW);
if has_trailing_slash {
flags.insert(OFlag::O_DIRECTORY);
}
}
let fd = if is_magic_link {
if !follow_last {
flags.insert(OFlag::O_NOFOLLOW);
}
safe_open_msym(parent_fd, base, flags, ResolveFlag::empty())
} else {
safe_open(parent_fd, base, flags, ResolveFlag::empty())
}?;
if has_trailing_slash {
abs.append_byte(b'/');
}
Ok(Self {
abs,
base: XPath::from_bytes(b""),
dir: Some(fd.into()),
typ,
})
}
}
#[expect(clippy::cognitive_complexity)]
pub fn safe_canonicalize<'a>(
pid: Pid,
fd: Option<RawFd>,
path: &XPath,
mut options: FsFlags,
sandbox: Option<&Sandbox>,
) -> Result<CanonicalPath<'a>, Errno> {
let flags = sandbox.map(|sb| *sb.flags).unwrap_or_default();
let mut filemap: FileMap = FileMap::default();
let mut file_type = None;
let resolve_beneath = options.contains(FsFlags::RESOLVE_BENEATH);
let (mut result, cwd) = if path.is_relative() {
if let Some(fd) = fd {
let magic = if fd == libc::AT_FDCWD {
ProcMagic::Cwd { pid }
} else if fd < 0 {
return Err(Errno::EBADF);
} else {
ProcMagic::Fd { pid, fd }
};
let (mut entry, sym) =
FileMapEntry::from_magic_link(magic, true, sandbox).map_err(|e| {
if e == Errno::ENOENT {
Errno::EBADF
} else {
e
}
})?;
#[expect(clippy::disallowed_methods)]
let dir = entry.target.clone().unwrap().unwrap();
let entry_sym = FileMapEntry::new(
MaybeFd::RawFd(entry.fd.as_raw_fd()),
Some(FileType::MagicLnk(ProcMagic::Fd { pid, fd })),
entry.f_mode,
entry.mnt_id,
entry.target.take(),
);
entry.f_type = Some(FileType::Dir);
filemap.0.try_reserve(2).or(Err(Errno::ENOMEM))?;
filemap.0.insert(dir.clone(), entry);
filemap.0.insert(sym, entry_sym);
(dir.clone(), Some(dir))
} else if !path.is_empty() {
let magic = ProcMagic::Cwd { pid };
let (mut entry, sym) = FileMapEntry::from_magic_link(magic, true, sandbox)?;
#[expect(clippy::disallowed_methods)]
let dir = entry.target.clone().unwrap().unwrap();
let entry_sym = FileMapEntry::new(
MaybeFd::RawFd(entry.fd.as_raw_fd()),
Some(FileType::MagicLnk(ProcMagic::Cwd { pid })),
entry.f_mode,
entry.mnt_id,
entry.target.take(),
);
entry.f_type = Some(FileType::Dir);
filemap.0.try_reserve(2).or(Err(Errno::ENOMEM))?;
filemap.0.insert(dir.clone(), entry);
filemap.0.insert(sym, entry_sym);
(dir.clone(), Some(dir))
} else {
return Err(Errno::ENOENT);
}
} else if resolve_beneath {
return Err(Errno::EXDEV);
} else if path.is_rootfs() {
return Ok(CanonicalPath::new_root());
} else if path.is_procfs() {
return Ok(CanonicalPath::new_proc());
} else {
(XPathBuf::from("/"), Some(XPathBuf::from("/")))
};
let miss_mode = MissingHandling::from(options);
let resolve_proc = options.resolve_proc();
let resolve_xdev = if flags.force_no_xdev() {
options.insert(FsFlags::NO_RESOLVE_XDEV);
false
} else {
!options.contains(FsFlags::NO_RESOLVE_XDEV)
};
let deny_dotdot = options.contains(FsFlags::NO_RESOLVE_DOTDOT);
let restrict_symlinks = !flags.allow_unsafe_symlinks();
let restrict_magiclinks = !flags.allow_unsafe_magiclinks();
let restrict_mkbdev = !flags.allow_unsafe_mkbdev();
let no_follow_last = !options.follow_last();
let is_split = options.intersects(FsFlags::MISS_LAST | FsFlags::WANT_BASE);
let mut open_flags = if !is_split && options.contains(FsFlags::WANT_READ) {
OFlag::O_RDONLY | OFlag::O_NONBLOCK | OFlag::O_NOCTTY
} else {
OFlag::O_PATH
};
open_flags |= OFlag::O_CLOEXEC;
let (mut parts, mut has_to_be_directory) = path_components(path)?;
if has_to_be_directory {
open_flags.insert(OFlag::O_DIRECTORY);
}
crate::debug!("ctx": "resolve_path", "op": "loop_init",
"pid": pid.as_raw(),
"path": &result,
"root": &cwd,
"is_dir": has_to_be_directory,
"parts": &parts,
"options": format!("{options:?}"),
"flags": format!("{flags:?}"));
let pid_errno = options.magic_errno();
let mnt_id = if resolve_xdev {
None
} else if let Some(ref cwd) = cwd {
Some(filemap.get_mnt_id(cwd, pid, options, file_type, sandbox)?)
} else if result.is_rootfs() {
None
} else {
return Err(Errno::EXDEV);
};
let mut last;
let mut loop_first = true;
let mut no_resolve_symlinks;
while let Some(part) = parts.pop_front() {
if result.len() >= PATH_MAX && parts.len() >= PATH_MAX_COMP {
return Err(Errno::ENAMETOOLONG);
}
last = parts.is_empty();
no_resolve_symlinks = last
&& no_follow_last
&& (!has_to_be_directory || miss_mode == MissingHandling::Missing);
crate::debug!("ctx": "resolve_path", "op": "loop_iter",
"pid": pid.as_raw(),
"path": &result,
"type": &file_type,
"root": &cwd,
"options": format!("{options:?}"),
"part": &part,
"parts": &parts,
"open_files": filemap.0.len(),
"resolve_beneath": resolve_beneath,
"resolve_proc": resolve_proc,
"resolve_xdev": resolve_xdev,
"is_last": last,
"is_dir": has_to_be_directory,
"follow_last": !no_follow_last,
"is_split": is_split,
"oflags": format_oflags(open_flags),
"rflags": options,
"miss_mode": format!("{miss_mode:?}"),
"deny_dotdot": deny_dotdot,
"restrict_symlinks": restrict_symlinks,
"restrict_magiclinks": restrict_magiclinks);
if deny_dotdot && part == PathComponent::ParentDir {
return Err(Errno::EACCES);
}
if !loop_first {
if resolve_beneath {
let cwd = cwd.as_ref().ok_or(Errno::EXDEV)?;
if !result.deref().descendant_of(cwd.as_bytes()) {
return Err(Errno::EXDEV);
}
}
if let Some(mnt_id) = mnt_id {
if !result.is_rootfs() {
let my_mnt_id =
filemap.get_mnt_id(&result, pid, options, file_type, sandbox)?;
if my_mnt_id != mnt_id {
return Err(Errno::EXDEV);
}
}
}
} else {
loop_first = false;
}
match part {
PathComponent::Normal(ref p) => {
result.try_reserve(p.len()).or(Err(Errno::ENAMETOOLONG))?;
result.push(p.as_bytes());
file_type = None;
}
PathComponent::ParentDir => {
file_type = Some(FileType::Dir);
if filemap.get(result.parent()).is_some() {
unsafe { result.pop_unchecked() };
continue;
}
let entry = filemap.0.get_mut(&result).ok_or(Errno::ENOENT)?;
if let Some(f_type) = entry.f_type {
if f_type != FileType::Dir {
return Err(Errno::ENOTDIR);
}
}
unsafe { result.pop_unchecked() };
let how = safe_open_how(OFlag::O_PATH | OFlag::O_DIRECTORY, ResolveFlag::empty())
.resolve(ResolveFlag::RESOLVE_NO_MAGICLINKS | ResolveFlag::RESOLVE_NO_SYMLINKS);
let fd = retry_on_eintr(|| safe_openat2(&entry.fd, "..", how))?;
entry.f_type = Some(FileType::Dir);
let entry = FileMapEntry::new(fd.into(), Some(FileType::Dir), None, None, None);
filemap.0.try_reserve(1).or(Err(Errno::ENOMEM))?;
filemap.0.insert(result.clone(), entry);
continue;
}
}
let result_magic = match ProcMagic::check_link(pid, result.deref(), restrict_magiclinks)? {
Some(_) if pid_errno == Errno::ELOOP && (!last || !no_follow_last) => {
return Err(pid_errno);
}
Some(
magic @ (ProcMagic::Fd { .. }
| ProcMagic::Cwd { .. }
| ProcMagic::Root { .. }
| ProcMagic::Exe { .. }),
) => {
let sym = magic.link_path();
file_type = Some(FileType::MagicLnk(magic));
let target = if let Some(entry) = filemap.get(&sym) {
#[expect(clippy::disallowed_methods)]
entry.target.clone().unwrap().unwrap()
} else {
let (mut entry, _) = FileMapEntry::from_magic_link(magic, !last, sandbox)?;
#[expect(clippy::disallowed_methods)]
let target = entry.target.take().unwrap().unwrap();
let entry_sym = FileMapEntry::new(
MaybeFd::RawFd(entry.fd.as_raw_fd()),
Some(FileType::MagicLnk(magic)),
entry.f_mode,
entry.mnt_id,
Some(Ok(target.clone())),
);
filemap.0.try_reserve(1).or(Err(Errno::ENOMEM))?;
filemap.0.insert(sym, entry_sym);
if target.is_absolute() {
filemap.0.try_reserve(1).or(Err(Errno::ENOMEM))?;
filemap.0.insert(target.clone(), entry);
}
target
};
if last && !no_resolve_symlinks {
if target.is_relative() {
unsafe { result.pop_unchecked() };
result.push(target.as_bytes());
} else {
result = target;
file_type = None;
}
if has_to_be_directory {
result.push(b"");
}
break;
}
Some(Ok(target))
}
Some(magic @ ProcMagic::Ns { .. }) => {
file_type = Some(FileType::MagicLnk(magic));
Some(Err(Errno::EINVAL))
}
Some(magic @ (ProcMagic::Pid { .. } | ProcMagic::Tid { .. })) => {
unreachable!("BUG: ProcMagic::check_link returned invalid magic `{magic:?}'!");
}
None if result.is_proc_self(false) => {
if !no_resolve_symlinks {
file_type = Some(FileType::Dir);
unsafe { result.pop_unchecked() };
result.push_pid(pid);
} else if last {
file_type = Some(FileType::MagicLnk(ProcMagic::Pid { pid }));
}
Some(Err(Errno::EINVAL))
}
None if result.is_proc_self(true) => {
let tgid = proc_tgid(pid)?;
if !no_resolve_symlinks {
file_type = Some(FileType::Dir);
unsafe { result.pop_unchecked() };
result.push_pid(tgid);
result.push(b"task");
result.push_pid(pid);
} else if last {
file_type = Some(FileType::MagicLnk(ProcMagic::Tid { tgid, pid }));
}
Some(Err(Errno::EINVAL))
}
None => None,
};
if no_resolve_symlinks {
if result.ends_with(b"/") {
has_to_be_directory = true;
open_flags.insert(OFlag::O_DIRECTORY);
}
if !file_type
.as_ref()
.map(|typ| typ.is_symlink() || typ.is_magic_link())
.unwrap_or(false)
{
file_type = None;
}
break;
}
let resolve_result = if let Some(result_magic) = result_magic {
result_magic
} else {
filemap.readlink(&result, pid, options, file_type, sandbox)
};
crate::debug!("ctx": "resolve_path", "op": "read_symlink",
"pid": pid.as_raw(),
"ret": format!("{resolve_result:?}"),
"path": &result,
"type": &file_type,
"root": &cwd,
"part": &part,
"parts": &parts,
"options": format!("{options:?}"),
"open_files": filemap.0.len(),
"resolve_beneath": resolve_beneath,
"resolve_proc": resolve_proc,
"resolve_xdev": resolve_xdev,
"is_last": last,
"follow_last": !no_follow_last,
"is_split": is_split,
"oflags": format_oflags(open_flags),
"rflags": options,
"miss_mode": format!("{miss_mode:?}"),
"deny_dotdot": deny_dotdot,
"restrict_symlinks": restrict_symlinks,
"restrict_magiclinks": restrict_magiclinks);
match resolve_result {
Ok(target) => {
file_type = None;
if target.is_relative() {
unsafe { result.pop_unchecked() };
} else {
result.truncate(1);
}
path_components2(target.deref(), &mut parts)?;
}
Err(Errno::EINVAL) => {
if last && !has_to_be_directory && result.ends_with(b"/") {
has_to_be_directory = true;
open_flags.insert(OFlag::O_DIRECTORY);
} else if !file_type.map(|t| t.is_magic_link()).unwrap_or(false) {
file_type = None;
}
}
Err(errno) => match miss_mode {
MissingHandling::Existing => return Err(errno),
MissingHandling::Normal if !parts.is_empty() => return Err(errno),
_ => {
file_type = None;
}
},
}
}
crate::debug!("ctx": "resolve_path", "op": "loop_done",
"pid": pid.as_raw(),
"path": &result,
"type": &file_type,
"root": &cwd,
"options": format!("{options:?}"),
"open_files": filemap.0.len(),
"resolve_beneath": resolve_beneath,
"resolve_proc": resolve_proc,
"resolve_xdev": resolve_xdev,
"is_dir": has_to_be_directory,
"follow_last": !no_follow_last,
"is_split": is_split,
"oflags": format_oflags(open_flags),
"rflags": options,
"miss_mode": format!("{miss_mode:?}"),
"deny_dotdot": deny_dotdot,
"restrict_symlinks": restrict_symlinks,
"restrict_magiclinks": restrict_magiclinks);
if options.follow_last()
&& file_type
.as_ref()
.map(|typ| typ.is_symlink())
.unwrap_or(false)
{
return Err(Errno::ELOOP);
}
if resolve_beneath {
let cwd = cwd.as_ref().ok_or(Errno::EXDEV)?;
if !result.deref().descendant_of(cwd.as_bytes()) {
return Err(Errno::EXDEV);
}
}
if has_to_be_directory {
let dir_ok = match file_type {
Some(FileType::Dir) => true,
Some(FileType::MagicLnk(ProcMagic::Cwd { .. } | ProcMagic::Root { .. })) => true,
Some(FileType::MagicLnk(ProcMagic::Pid { .. } | ProcMagic::Tid { .. })) => true,
Some(FileType::MagicLnk(_)) => false,
None => true,
_ => false,
};
if matches!(
miss_mode,
MissingHandling::Existing | MissingHandling::Normal
) && !dir_ok
{
return Err(Errno::ENOTDIR);
}
result.append_byte(b'/');
}
if let Some(mnt_id) = mnt_id {
let my_mnt_id = match (
filemap.get_mnt_id(&result, pid, options, file_type, sandbox),
miss_mode,
) {
(Ok(mnt_id), _) => mnt_id,
(Err(Errno::ELOOP), _) => {
return Err(Errno::ELOOP);
}
(Err(Errno::ENOENT), MissingHandling::Existing) => {
return Err(Errno::ENOENT);
}
(Err(Errno::ENOTDIR), MissingHandling::Existing | MissingHandling::Normal)
if has_to_be_directory =>
{
return Err(Errno::ENOTDIR);
}
_ => return Err(Errno::EXDEV),
};
if my_mnt_id != mnt_id {
return Err(Errno::EXDEV);
}
}
open_flags |= OFlag::O_NOFOLLOW;
let flen = filemap.0.len();
let path = CanonicalPath::new_map(result, file_type, options, filemap, restrict_mkbdev)?;
crate::debug!("ctx": "resolve_path", "op": "resolve_done",
"pid": pid.as_raw(),
"path": &path,
"root": &cwd,
"options": format!("{options:?}"),
"open_files": flen,
"resolve_beneath": resolve_beneath,
"resolve_proc": resolve_proc,
"resolve_xdev": resolve_xdev,
"is_dir": has_to_be_directory,
"is_split": is_split,
"follow_last": !no_follow_last,
"oflags": format_oflags(open_flags),
"rflags": options,
"miss_mode": format!("{miss_mode:?}"),
"deny_dotdot": deny_dotdot,
"restrict_symlinks": restrict_symlinks,
"restrict_magiclinks": restrict_magiclinks);
Ok(path)
}
pub fn format_oflags(flags: OFlag) -> Vec<String> {
let count = flags.into_iter().count();
if count == 0 {
return vec![];
}
let mut fmt = Vec::with_capacity(count);
for flag in flags.iter() {
fmt.push(format_oflag(flag));
}
fmt
}
pub fn format_oflag(flag: OFlag) -> String {
let flag = format!("{flag:?}");
if !flag.starts_with("OFlag(O_") || !flag.ends_with(')') {
return "?".to_string();
}
#[expect(clippy::arithmetic_side_effects)]
String::from_utf8_lossy(&flag.as_bytes()[8..flag.len() - 1]).to_ascii_lowercase()
}
pub fn format_clone_flags(flags: CloneFlags) -> Vec<&'static str> {
let mut names = vec![];
if flags.is_empty() {
return names;
}
if flags.contains(CloneFlags::CLONE_NEWUSER) {
names.push("user");
}
if flags.contains(CloneFlags::CLONE_NEWNS) {
names.push("mount");
}
if flags.contains(CloneFlags::CLONE_NEWUTS) {
names.push("uts");
}
if flags.contains(CloneFlags::CLONE_NEWIPC) {
names.push("ipc");
}
if flags.contains(CloneFlags::CLONE_NEWPID) {
names.push("pid");
}
if flags.contains(CloneFlags::CLONE_NEWNET) {
names.push("net");
}
if flags.contains(CloneFlags::CLONE_NEWCGROUP) {
names.push("cgroup");
}
if flags.contains(CLONE_NEWTIME) {
names.push("time");
}
names
}
pub fn format_clone_names(clone_names: &[&str]) -> String {
match clone_names.len() {
0 => "no namespaces".to_string(),
1 => format!("{} namespace", clone_names[0]),
2 => format!("{} and {} namespaces", clone_names[0], clone_names[1]),
_ => {
let mut s = clone_names.join(", ");
#[expect(clippy::arithmetic_side_effects)]
if let Some(pos) = s.rfind(", ") {
s.replace_range(pos..pos + 2, ", and ");
}
format!("{s} namespaces")
}
}
}
#[expect(clippy::disallowed_methods)]
pub fn grep(dir: &XPath, name: &[u8]) -> Option<XPathBuf> {
let dir = File::open(dir.as_path()).ok()?;
let name = XPath::from_bytes(name);
loop {
let mut entries = getdents64(&dir, 128).ok()?;
for entry in &mut entries {
let mut path = XPathBuf::from(entry.name_bytes());
if entry.is_dir() {
path.append_byte(b'/');
} else if entry.is_symlink() {
path.append_byte(b'@');
} else if entry.is_block_device() {
path.append_byte(b'!');
} else if entry.is_char_device() {
path.append_byte(b'$');
} else if entry.is_fifo() {
path.append_byte(b'|');
} else if entry.is_socket() {
path.append_byte(b'~');
}
if *path == *name || (name.len() == 1 && path.ends_with(name.as_bytes())) {
return Some(path);
}
}
}
}
#[cfg(test)]
mod tests {
use std::{
fs::{self, OpenOptions},
io::ErrorKind,
os::{
fd::{AsRawFd, IntoRawFd},
unix::{
fs::{symlink, OpenOptionsExt},
net::{UnixListener, UnixStream},
},
},
process::{exit, Command},
sync::mpsc,
thread,
thread::sleep,
time::{Duration, SystemTime},
};
use bitflags::Flags;
use nix::{
fcntl::open,
sched::{unshare, CloneFlags},
sys::{
signal::{kill, Signal},
socket::{
accept, bind, connect, listen, socket, socketpair, AddressFamily, Backlog,
SockFlag, SockType, UnixAddr,
},
stat::Mode,
wait::waitpid,
},
unistd::{chdir, close, dup, fchdir, fork, getcwd, mkdir, pause, pipe, ForkResult},
};
use tempfile::NamedTempFile;
use super::*;
use crate::{confine::check_unix_diag, xpath};
fn setup() -> bool {
let _ = crate::log::log_init_simple(crate::syslog::LogLevel::Warn);
if let Err(error) = crate::config::proc_init() {
eprintln!("Failed to initialize proc: {error:?}");
return false;
}
true
}
fn setup_deep_directory_structure(name: &XPath, depth: usize) -> Result<(), nix::Error> {
unshare(CloneFlags::CLONE_FS).unwrap();
let fd = safe_open_path(AT_FDCWD, ".", OFlag::O_DIRECTORY, ResolveFlag::empty())?;
for _ in 0..depth {
mkdir(name, Mode::S_IRWXU)?;
chdir(name)?;
}
fchdir(fd)?;
Ok(())
}
fn get_atime<P: AsRef<Path>>(path: P) -> SystemTime {
let metadata = fs::metadata(path).expect("Failed to get metadata");
metadata.accessed().expect("Failed to get accessed time")
}
fn assert_atime_unchanged<'a, P: AsRef<Path>, F>(path: P, func: F)
where
F: FnOnce() -> Result<CanonicalPath<'a>, Errno>,
{
let original_atime_f = get_atime(&path);
let original_atime_p = get_atime(path.as_ref().parent().unwrap());
sleep(Duration::from_secs(7));
assert!(
func().is_ok(),
"canonicalize {} failed",
path.as_ref().display()
);
let new_atime_f = get_atime(&path);
let new_atime_p = get_atime(path.as_ref().parent().unwrap());
assert!(
new_atime_f <= original_atime_f + Duration::new(1, 0),
"The atime of the file should not have significantly changed."
);
assert!(
new_atime_p <= original_atime_p + Duration::new(1, 0),
"The atime of the parent dir should not have significantly changed."
);
}
fn remove_dir_all<P: AsRef<Path>>(path: P) -> std::io::Result<()> {
let status = Command::new("rm")
.arg("-rf")
.arg(path.as_ref().to_string_lossy().to_string())
.status()?;
if status.success() {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to remove directory: {}", path.as_ref().display()),
))
}
}
fn setup_symlink_loop(tmp_dir: &XPath, links: &[(&str, &str)]) {
for &(src, dst) in links {
let src_path = tmp_dir.join(src.as_bytes());
let src_parent = src_path.parent();
if !src_parent.exists(false) {
fs::create_dir_all(src_parent.as_path()).unwrap();
}
let dst_path = XPath::from_bytes(&dst.as_bytes());
if src_path.exists(false) {
fs::remove_file(src_path.as_path()).unwrap();
}
let full_dst_path = if dst_path.is_absolute() {
tmp_dir.join(dst_path.strip_prefix(b"/").unwrap().as_bytes())
} else {
src_parent.join(dst_path.as_bytes()).into()
};
let dst_parent = full_dst_path.parent();
if !dst_parent.exists(false) {
fs::create_dir_all(dst_parent.as_path()).unwrap();
}
symlink(full_dst_path.as_path(), src_path.as_path())
.expect(&format!("Unable to symlink {src_path} -> {full_dst_path}",));
}
}
fn tempdir() -> Result<XPathBuf, Box<dyn std::error::Error>> {
let tmp = tempfile::Builder::new()
.disable_cleanup(true)
.tempdir_in(".")?;
let _ = OpenOptions::new()
.write(true)
.create(true)
.mode(0o600)
.open(tmp.path().join("test"))?;
Ok(tmp
.path()
.to_path_buf()
.file_name()
.unwrap()
.as_bytes()
.into())
}
fn chdir_long(dir: &XPath) -> Result<(), Errno> {
let mut path_buf = dir.as_bytes();
let mut current_dir_fd = -2;
loop {
let dir = XPath::from_bytes(path_buf);
if path_buf.is_empty() || chdir(dir).is_ok() {
if current_dir_fd >= 0 {
let _ = close(current_dir_fd);
}
return Ok(());
}
if !matches!(Errno::last(), Errno::ENAMETOOLONG | Errno::ENOMEM)
|| dir.len() < PATH_MAX - 1
{
break;
}
let mut boundary = path_buf.len().min(PATH_MAX - 1);
while boundary > 0 && path_buf[boundary] != b'/' {
boundary -= 1;
}
if boundary == 0 {
break;
}
if current_dir_fd == -2 {
current_dir_fd = open(".", OFlag::O_PATH | OFlag::O_DIRECTORY, Mode::empty())
.map(|fd| fd.into_raw_fd())?;
}
let dir = XPath::from_bytes(&path_buf[..boundary]);
if chdir(dir).is_err() {
break;
}
path_buf = &path_buf[boundary + 1..];
}
let errno = match Errno::last() {
Errno::UnknownErrno => Errno::ENAMETOOLONG,
errno => errno,
};
if current_dir_fd >= 0 {
let result = fchdir(unsafe { BorrowedFd::borrow_raw(current_dir_fd) });
let _ = close(current_dir_fd);
return if result.is_ok() {
Err(errno)
} else {
Err(Errno::ENOTRECOVERABLE)
};
}
Err(if current_dir_fd == -2 {
errno
} else {
Errno::ENOTRECOVERABLE
})
}
#[test]
fn test_canonicalize_empty_path() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let result = safe_canonicalize(
Pid::this(),
None,
&XPath::from_bytes(b""),
FsFlags::empty(),
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ENOENT)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&XPath::from_bytes(b""),
FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ENOENT)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&XPath::from_bytes(b""),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ENOENT)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&XPath::from_bytes(b""),
FsFlags::NO_FOLLOW_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ENOENT)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&XPath::from_bytes(b""),
FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ENOENT)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&XPath::from_bytes(b""),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ENOENT)), "{result:?}");
}
#[test]
fn test_canonicalize_repetitive_root() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let root = safe_canonicalize(
Pid::this(),
None,
&XPath::from_bytes(b"//"),
FsFlags::empty(),
Some(&sandbox),
)
.unwrap();
assert_eq!(root.abs.as_bytes(), b"/");
assert_eq!(root.typ, Some(FileType::Dir));
}
#[test]
fn test_canonicalize_repetitive_slashes() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let result_test = safe_canonicalize(
Pid::this(),
None,
&XPath::from_bytes(b"/etc/passwd"),
FsFlags::empty(),
Some(&sandbox),
)
.unwrap()
.abs;
let paths = vec![
"/etc/passwd",
"/etc//passwd",
"/etc///passwd",
"//etc/passwd",
"//etc//passwd",
"//etc///passwd",
"///etc/passwd",
"///etc//passwd",
"///etc///passwd",
];
for path in &paths {
let path = XPathBuf::from(path.to_string());
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::empty(),
Some(&sandbox),
)
.unwrap()
.abs;
assert_eq!(result, result_test);
}
}
#[test]
fn test_canonicalize_dots_slashes() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let cwd = XPathBuf::from(std::env::current_dir().unwrap());
let tmp = tempdir().unwrap();
let path = xpath!("{tmp}//./..//{tmp}/test");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::empty(),
Some(&sandbox),
);
assert!(result.is_ok(), "{path}->{result:?}");
let result1 = result.unwrap().abs;
let path = xpath!("{cwd}/{tmp}//./..//{tmp}/test");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(result.is_ok(), "{path}->{result:?}");
let result2 = result.unwrap().abs;
assert!(!result1.is_empty(), "result:{result1}");
assert!(!result2.is_empty(), "result:{result2}");
assert_eq!(result1, result2);
}
#[test]
fn test_canonicalize_non_directory_with_slash() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let path = tempdir().unwrap();
let test = xpath!("{path}/test/");
let result = safe_canonicalize(
Pid::this(),
None,
&test.deref(),
FsFlags::empty(),
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&test.deref(),
FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
assert!(safe_canonicalize(
Pid::this(),
None,
&test.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
)
.is_ok());
let result = safe_canonicalize(
Pid::this(),
None,
&test.deref(),
FsFlags::NO_FOLLOW_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&test.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
assert!(safe_canonicalize(
Pid::this(),
None,
&test.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
)
.is_ok());
}
#[test]
fn test_canonicalize_missing_directory_returns_enoent() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let result = safe_canonicalize(
Pid::this(),
None,
&XPath::from_bytes(b"/zzz/.."),
FsFlags::empty(),
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ENOENT)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&XPath::from_bytes(b"/zzz/.."),
FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ENOENT)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&XPath::from_bytes(b"/zzz/.."),
FsFlags::NO_FOLLOW_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ENOENT)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&XPath::from_bytes(b"/zzz/.."),
FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ENOENT)), "{result:?}");
}
#[test]
fn test_relative_symlink_resolution() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let root_test_dir = &XPath::from_bytes(b"test_root_relative_symlink_resolution");
let deep_dir = root_test_dir.join(b"a/b/c");
let _ = remove_dir_all(&root_test_dir);
fs::create_dir_all(&root_test_dir.join(b"d")).unwrap();
fs::create_dir_all(&deep_dir).unwrap();
let rel_link = root_test_dir.join(b"a/b/rel_link");
symlink("../..", &rel_link).unwrap();
let abs_link_path = &XPath::from_bytes(b"/proc/self/cwd")
.join(root_test_dir.join(b"a/b/rel_link/d").as_bytes());
let result = safe_canonicalize(
Pid::this(),
None,
&abs_link_path.deref(),
FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(
result.is_ok(),
"canonicalize:{abs_link_path} result:{result:?}",
);
let resolved_path = result.unwrap().abs;
let expected_path = fs::canonicalize(
&XPath::from_bytes(b"/proc/self/cwd").join(root_test_dir.join(b"d").as_bytes()),
)
.unwrap();
Command::new("/bin/rm")
.arg("-rf")
.arg(&root_test_dir)
.status()
.expect("rm -rf tmpdir");
assert_eq!(resolved_path, XPathBuf::from(expected_path));
}
#[ignore]
#[test]
fn test_complex_interplay_symlinks_dots() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let cwd = XPathBuf::from(Path::new("/proc/self/cwd").canonicalize().unwrap());
let root_test_dir = cwd.join(b"test_root_complex_interplay_symlinks_dots");
let _ = remove_dir_all(&root_test_dir);
fs::create_dir_all(root_test_dir.join(b"a/b/c")).unwrap();
fs::create_dir(root_test_dir.join(b"d")).unwrap();
fs::create_dir(root_test_dir.join(b"e")).unwrap();
fs::create_dir(root_test_dir.join(b"x")).unwrap();
symlink("./a", root_test_dir.join(b"link_to_a")).unwrap();
symlink("e", root_test_dir.join(b"link_to_e")).unwrap();
symlink("a/b", root_test_dir.join(b"link_to_b")).unwrap();
symlink("../../x", root_test_dir.join(b"a/b/rel_link")).unwrap();
let path = root_test_dir.join(b"link_to_a/../link_to_b/rel_link/../..");
let resolved_path = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::MUST_PATH,
Some(&sandbox),
)
.unwrap()
.abs;
let _ = remove_dir_all(&root_test_dir);
assert_eq!(resolved_path, XPathBuf::from(root_test_dir));
}
#[test]
fn test_trailing_slash_handling() {
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let child = match unsafe { fork() } {
Ok(ForkResult::Parent { child }) => child,
Ok(ForkResult::Child) => {
pause();
exit(127);
}
Err(errno) => exit(errno as i32),
};
if !setup() {
kill(child, Signal::SIGKILL).unwrap();
waitpid(child, None).unwrap();
return;
}
let path = XPath::from_bytes(b"/usr/");
let pexp = path;
let path = safe_canonicalize(child, None, &path, FsFlags::empty(), Some(&sandbox)).unwrap();
assert_eq!(pexp, path.abs.deref(), "{pexp} != {path:?}");
eprintln!("ok 1");
let parg = XPath::from_bytes(b"/proc/self/");
let pexp = xpath!("/proc/{child}/");
let path = safe_canonicalize(child, None, &parg, FsFlags::empty(), Some(&sandbox)).unwrap();
assert_eq!(path.abs, pexp, "{pexp} != {path:?}");
eprintln!("ok 2 step 1");
let path =
safe_canonicalize(child, None, &parg, FsFlags::NO_FOLLOW_LAST, Some(&sandbox)).unwrap();
assert_eq!(path.abs, pexp, "{pexp} != {path:?}");
eprintln!("ok 2 step 2");
kill(child, Signal::SIGKILL).unwrap();
waitpid(child, None).unwrap();
}
#[test]
fn test_canonicalize_no_atime_change_normal() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let cdir = XPathBuf::from(std::env::current_dir().unwrap());
let base = cdir.join(tempdir().unwrap().as_bytes());
let path = base.join(b"file");
fs::File::create(&path).unwrap();
assert_atime_unchanged(&path, || {
safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::empty(),
Some(&sandbox),
)
});
let _ = remove_dir_all(&base);
}
#[test]
fn test_canonicalize_no_atime_change_existing() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let cdir = XPathBuf::from(std::env::current_dir().unwrap());
let base = cdir.join(&tempdir().unwrap().as_bytes());
let path = base.join(b"file");
fs::File::create(&path).unwrap();
assert_atime_unchanged(&path, || {
safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::MUST_PATH,
Some(&sandbox),
)
});
let _ = remove_dir_all(&base);
}
#[test]
fn test_canonicalize_symlink_loop() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let tmp_dir = tempfile::Builder::new()
.disable_cleanup(true)
.tempdir()
.expect("Failed to create temp dir");
let dir_path = XPathBuf::from(tmp_dir.path().to_path_buf());
let mut link_a = dir_path.join(b"link_a");
let mut link_b = dir_path.join(b"link_b");
symlink(&link_b, &link_a).expect("Failed to create symlink a");
symlink(&link_a, &link_b).expect("Failed to create symlink b");
let result = safe_canonicalize(
Pid::this(),
None,
&link_a.deref(),
FsFlags::NO_FOLLOW_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&link_a.deref(),
FsFlags::empty(),
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&link_a.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&link_a.deref(),
FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&link_a.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&link_a.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
link_a.push(b"");
let result = safe_canonicalize(
Pid::this(),
None,
&link_a.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&link_a.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
link_b.push(b"");
let result = safe_canonicalize(
Pid::this(),
None,
&link_b.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&link_b.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
}
#[test]
fn test_canonicalize_nonexistent_final_component() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let tmp_dir = tempfile::Builder::new()
.disable_cleanup(true)
.tempdir()
.expect("Failed to create temp dir");
let dir_path = XPathBuf::from(tmp_dir.path().to_path_buf());
let mut valid_link = dir_path.join(b"valid_link");
let nonexistent_target = dir_path.join(b"nonexistent");
symlink(&nonexistent_target, &valid_link)
.expect("Failed to create symlink to non-existent target");
let result = safe_canonicalize(
Pid::this(),
None,
&valid_link.deref(),
FsFlags::NO_FOLLOW_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&valid_link.deref(),
FsFlags::empty(),
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&valid_link.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&valid_link.deref(),
FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ENOENT)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&valid_link.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&valid_link.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
valid_link.push(b"");
let result = safe_canonicalize(
Pid::this(),
None,
&valid_link.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&valid_link.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
}
#[test]
fn test_canonicalize_self_referential_symlink() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let tmp_dir = tempfile::Builder::new()
.disable_cleanup(true)
.tempdir()
.expect("Failed to create temp dir");
let dir_path = XPathBuf::from(tmp_dir.path().to_path_buf());
let mut symlink_path = dir_path.join(b"self_link");
symlink(symlink_path.as_path(), symlink_path.as_path())
.expect("Failed to create self-referential symlink");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::NO_FOLLOW_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::empty(),
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
symlink_path.push(b"");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
}
#[test]
fn test_canonicalize_broken_symlink() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let tmp_dir = tempfile::Builder::new()
.disable_cleanup(true)
.tempdir()
.expect("Failed to create temp dir");
let dir_path = XPathBuf::from(tmp_dir.path().to_path_buf());
let mut broken_link = dir_path.join(b"broken_link");
let nonexistent_target = dir_path.join(b"nonexistent_target");
symlink(nonexistent_target.as_path(), broken_link.as_path())
.expect("Failed to create broken symlink");
let result = safe_canonicalize(
Pid::this(),
None,
&broken_link.deref(),
FsFlags::NO_FOLLOW_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
eprintln!("ok 1");
let result = safe_canonicalize(
Pid::this(),
None,
&broken_link.deref(),
FsFlags::empty(),
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
eprintln!("ok 2");
let result = safe_canonicalize(
Pid::this(),
None,
&broken_link.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
eprintln!("ok 3");
let result = safe_canonicalize(
Pid::this(),
None,
&broken_link.deref(),
FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ENOENT)), "{result:?}");
eprintln!("ok 4");
let result = safe_canonicalize(
Pid::this(),
None,
&broken_link.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
eprintln!("ok 5");
let result = safe_canonicalize(
Pid::this(),
None,
&broken_link.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
eprintln!("ok 6");
broken_link.push(b"");
let result = safe_canonicalize(
Pid::this(),
None,
&broken_link.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
eprintln!("ok 7");
let result = safe_canonicalize(
Pid::this(),
None,
&broken_link.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
eprintln!("ok 8");
}
#[test]
fn test_canonicalize_symlink_to_directory() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let tmp_dir = tempfile::Builder::new()
.disable_cleanup(true)
.tempdir()
.expect("Failed to create temp dir");
let tmp_path = XPathBuf::from(tmp_dir.path().to_path_buf());
let dir = tmp_path.join(b"dir");
fs::create_dir(&dir).expect("Failed to create directory");
let symlink_path = tmp_path.join(b"dir_link");
symlink(dir.as_path(), symlink_path.as_path())
.expect("Failed to create symlink to directory");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::NO_FOLLOW_LAST,
Some(&sandbox),
);
let result_repr = format!("{result:?}");
assert!(result.is_ok(), "{result_repr}");
assert!(result.unwrap().typ.unwrap().is_symlink(), "{result_repr}");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::empty(),
Some(&sandbox),
);
let result_repr = format!("{result:?}");
assert!(result.is_ok(), "{result_repr}");
assert!(result.unwrap().typ.unwrap().is_dir(), "{result_repr}");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH,
Some(&sandbox),
);
let result_repr = format!("{result:?}");
assert!(result.is_ok(), "{result_repr}");
assert!(result.unwrap().typ.unwrap().is_symlink(), "{result_repr}");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::MUST_PATH,
Some(&sandbox),
);
let result_repr = format!("{result:?}");
assert!(result.is_ok(), "{result_repr}");
assert!(result.unwrap().typ.unwrap().is_dir(), "{result_repr}");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&symlink_path.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
}
#[test]
fn test_canonicalize_symlink_chain() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let tmp_dir = tempfile::Builder::new()
.disable_cleanup(true)
.tempdir()
.expect("Failed to create temp dir");
let dir_path = XPathBuf::from(tmp_dir.path().to_path_buf());
let link1 = dir_path.join(b"link1");
let link2 = dir_path.join(b"link2");
let link3 = dir_path.join(b"link3");
let file = dir_path.join(b"file");
fs::write(file.as_path(), "content").expect("Failed to write file");
symlink(link2.as_path(), link1.as_path()).expect("Failed to create link1");
symlink(link3.as_path(), link2.as_path()).expect("Failed to create link2");
symlink(file.as_path(), link3.as_path()).expect("Failed to create link3");
let result = safe_canonicalize(
Pid::this(),
None,
&link1.deref(),
FsFlags::NO_FOLLOW_LAST,
Some(&sandbox),
);
let result_repr = format!("{result:?}");
assert!(result.is_ok(), "{result_repr}");
assert!(result.unwrap().typ.unwrap().is_symlink(), "{result_repr}");
let result = safe_canonicalize(
Pid::this(),
None,
&link1.deref(),
FsFlags::empty(),
Some(&sandbox),
);
let result_repr = format!("{result:?}");
assert!(result.is_ok(), "{result_repr}");
assert!(result.unwrap().typ.unwrap().is_file(), "{result_repr}");
let result = safe_canonicalize(
Pid::this(),
None,
&link1.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH,
Some(&sandbox),
);
let result_repr = format!("{result:?}");
assert!(result.is_ok(), "{result_repr}");
assert!(result.unwrap().typ.unwrap().is_symlink(), "{result_repr}");
let result = safe_canonicalize(
Pid::this(),
None,
&link1.deref(),
FsFlags::MUST_PATH,
Some(&sandbox),
);
let result_repr = format!("{result:?}");
assert!(result.is_ok(), "{result_repr}");
assert!(result.unwrap().typ.unwrap().is_file(), "{result_repr}");
let result = safe_canonicalize(
Pid::this(),
None,
&link1.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&link1.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
}
#[test]
fn test_safe_canonicalize_complex_symlink_loop_with_intermediate_components() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let tmp_dir = tempfile::Builder::new()
.disable_cleanup(true)
.tempdir()
.expect("Failed to create temp dir");
let dir_path = XPathBuf::from(tmp_dir.path().to_path_buf());
setup_symlink_loop(
&dir_path.deref(),
&[("a", "b/c"), ("b/c", "d"), ("b/d", "../e"), ("e", "f/../a")],
);
let mut path = dir_path.join(b"a");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::NO_FOLLOW_LAST,
Some(&sandbox),
);
let result_repr = format!("{result:?}");
assert!(result.is_ok(), "{result_repr}");
assert!(result.unwrap().typ.unwrap().is_symlink(), "{result_repr}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::empty(),
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH,
Some(&sandbox),
);
let result_repr = format!("{result:?}");
assert!(result.is_ok(), "{result_repr}");
assert!(result.unwrap().typ.unwrap().is_symlink(), "{result_repr}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
path.push(b"");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
path.push(b"foo");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
}
#[test]
fn test_safe_canonicalize_symlinks_with_dot_and_dotdot_components() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let tmp_dir = tempfile::Builder::new()
.disable_cleanup(true)
.tempdir()
.expect("Failed to create temp dir");
fs::create_dir_all(tmp_dir.path().join("b")).expect("Failed to create directory b");
symlink("b", tmp_dir.path().join("a")).expect("Failed to create symlink a -> b");
symlink("..///e", tmp_dir.path().join("b").join("d"))
.expect("Failed to create symlink b/d -> ../e");
symlink("b/.///./d", tmp_dir.path().join("e")).expect("Failed to create symlink e -> b/d");
let mut path = XPathBuf::from(tmp_dir.path().join("a").join(".").join("d"));
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::NO_FOLLOW_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{path}->{result:?}");
let result = result.unwrap();
assert!(result.abs.exists(false), "{path}->{result:?}");
assert!(!result.abs.exists(true), "{path}->{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::empty(),
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(result.is_ok(), "{path}->{result:?}");
let result = result.unwrap();
assert!(result.abs.exists(false), "{path}->{result:?}");
assert!(!result.abs.exists(true), "{path}->{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::MUST_PATH,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
path.push(b"");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
path.push(b"foo");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::NO_FOLLOW_LAST | FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
let result = safe_canonicalize(
Pid::this(),
None,
&path.deref(),
FsFlags::MISS_LAST,
Some(&sandbox),
);
assert!(matches!(result, Err(Errno::ELOOP)), "{result:?}");
}
#[test]
fn test_canonicalize_proc_self() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let pid = Pid::this();
let path = safe_canonicalize(
pid,
None,
&XPath::from_bytes(b"/proc/self"),
FsFlags::NO_FOLLOW_LAST,
Some(&sandbox),
)
.expect("canonicalize /proc/self");
assert!(
path.typ
.as_ref()
.map(|typ| typ.is_symlink())
.unwrap_or(false),
"path:{path:?}"
);
assert_eq!(
path.abs.deref(),
XPath::from_bytes(b"/proc/self"),
"path:{path:?}"
);
match path.typ {
Some(FileType::Lnk) => {}
_ => panic!("path:{path:?}"),
}
}
#[test]
fn test_canon_glob_std() {
if !setup() {
return;
}
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let fd = open("/dev/null", OFlag::O_RDONLY, Mode::empty()).unwrap();
let fd = xpath!("/proc/self/fd/{}", fd.as_raw_fd());
let pid = Pid::this();
let result = safe_canonicalize(pid, None, &fd.deref(), FsFlags::empty(), Some(&sandbox));
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(pid, None, &fd.deref(), FsFlags::MUST_PATH, Some(&sandbox));
assert!(result.is_ok(), "{result:?}");
let result = safe_canonicalize(pid, None, &fd.deref(), FsFlags::MISS_LAST, Some(&sandbox));
assert!(result.is_ok(), "{result:?}");
}
#[test]
fn test_canon_glob_pipe() {
let mut sandbox = Sandbox::default();
sandbox.config("allow/lpath+/***").unwrap();
sandbox.flags.clear();
let (read_end, write_end) = UnixStream::pair().unwrap();
let child = match unsafe { fork() } {
Ok(ForkResult::Parent { child }) => child,
Ok(ForkResult::Child) => {
pause();
exit(127);
}
Err(errno) => exit(errno as i32),
};
if !setup() {
kill(child, Signal::SIGKILL).unwrap();
waitpid(child, None).unwrap();
return;
}
let fd = read_end.as_raw_fd();
let path = xpath!("/proc/{child}/fd/{fd}");
let result =
safe_canonicalize(child, None, &path.deref(), FsFlags::empty(), Some(&sandbox));
assert!(result.is_ok(), "{path}->{result:?}");
let fd = write_end.as_raw_fd();
let path = xpath!("/proc/{child}/fd/{fd}");
let result =
safe_canonicalize(child, None, &path.deref(), FsFlags::empty(), Some(&sandbox));
assert!(result.is_ok(), "{path}->{result:?}");
kill(child, Signal::SIGKILL).unwrap();
waitpid(child, None).unwrap();
}
#[test]
fn test_path_components_empty_path() {
let path = XPath::from_bytes(b"");
let (components, has_trailing_slash) = path_components(&path).unwrap();
assert_eq!(components, VecDeque::new());
assert!(has_trailing_slash);
}
#[test]
fn test_path_components_only_parent_dir() {
let path = XPath::from_bytes(b"..");
let (components, has_trailing_slash) = path_components(&path).unwrap();
assert_eq!(components, VecDeque::from([PathComponent::ParentDir]));
assert!(has_trailing_slash);
}
#[test]
fn test_path_components_multiple_parent_dir() {
let path = XPath::from_bytes(b"../..");
let (components, has_trailing_slash) = path_components(&path).unwrap();
assert_eq!(
components,
VecDeque::from([PathComponent::ParentDir, PathComponent::ParentDir])
);
assert!(has_trailing_slash);
}
#[test]
fn test_path_components_parent_dir_with_normal_components() {
let path = XPath::from_bytes(b"../foo/../bar");
let (components, has_trailing_slash) = path_components(&path).unwrap();
assert_eq!(
components,
VecDeque::from([
PathComponent::ParentDir,
PathComponent::Normal(XPathBuf::from("foo")),
PathComponent::ParentDir,
PathComponent::Normal(XPathBuf::from("bar")),
])
);
assert!(!has_trailing_slash);
}
#[test]
fn test_path_components_trailing_slash_with_parent_dir() {
let path = XPath::from_bytes(b"../foo/..");
let (components, has_trailing_slash) = path_components(&path).unwrap();
assert_eq!(
components,
VecDeque::from([
PathComponent::ParentDir,
PathComponent::Normal(XPathBuf::from("foo")),
PathComponent::ParentDir,
])
);
assert!(has_trailing_slash);
}
#[test]
fn test_path_components_leading_slashes_are_skipped() {
let path = XPath::from_bytes(b"////..////bar");
let (components, has_trailing_slash) = path_components(&path).unwrap();
assert_eq!(
components,
VecDeque::from([
PathComponent::ParentDir,
PathComponent::Normal(XPathBuf::from("bar")),
])
);
assert!(!has_trailing_slash);
}
#[test]
fn test_path_components_path_with_mixed_components_and_trailing_slash() {
let path = XPath::from_bytes(b"../foo/../bar/");
let (components, has_trailing_slash) = path_components(&path).unwrap();
assert_eq!(
components,
VecDeque::from([
PathComponent::ParentDir,
PathComponent::Normal(XPathBuf::from("foo")),
PathComponent::ParentDir,
PathComponent::Normal(XPathBuf::from("bar")),
])
);
assert!(has_trailing_slash);
}
#[test]
fn test_path_components_complex_path_with_leading_parent_dir() {
let path = XPath::from_bytes(b"../foo/bar/../../baz/..");
let (components, has_trailing_slash) = path_components(&path).unwrap();
assert_eq!(
components,
VecDeque::from([
PathComponent::ParentDir,
PathComponent::Normal(XPathBuf::from("foo")),
PathComponent::Normal(XPathBuf::from("bar")),
PathComponent::ParentDir,
PathComponent::ParentDir,
PathComponent::Normal(XPathBuf::from("baz")),
PathComponent::ParentDir,
])
);
assert!(has_trailing_slash);
}
#[test]
fn test_path_components_root_path_is_handled_externally() {
let path = XPath::from_bytes(b"/..");
let (components, has_trailing_slash) = path_components(&path).unwrap();
assert_eq!(components, VecDeque::from([PathComponent::ParentDir]));
assert!(has_trailing_slash);
}
#[test]
fn test_chdir_long() {
const MAX_DEPTH: usize = 128;
let o = "o".repeat(200);
let name = XPathBuf::from(format!("syd_test_chdir_l{o}ng.dir"));
setup_deep_directory_structure(&name, MAX_DEPTH).unwrap();
let fd = open(".", OFlag::O_PATH | OFlag::O_DIRECTORY, Mode::empty()).unwrap();
let mut lname = name.clone();
for _ in 0..MAX_DEPTH - 1 {
lname.push(name.as_bytes());
}
let result = chdir_long(&lname);
fchdir(fd).unwrap();
Command::new("rm").arg("-rf").arg(name).status().unwrap();
assert!(result.is_ok(), "chdir_long failed: {result:?}");
}
#[test]
fn test_getdir_long_with_deep_structure() {
const MAX_DEPTH: usize = 128;
let o = "o".repeat(200);
let name = XPathBuf::from(format!("syd_test_getdir_l{o}ng_with_deep_structure.dir"));
setup_deep_directory_structure(&name, MAX_DEPTH).unwrap();
let fd = open(".", OFlag::O_PATH | OFlag::O_DIRECTORY, Mode::empty()).unwrap();
let mut cwd = getcwd().map(XPathBuf::from).unwrap();
let max = cwd.depth() + MAX_DEPTH + 1;
for _ in 0..MAX_DEPTH {
cwd.push(name.as_bytes());
}
let result = chdir_long(&cwd);
let result_cwd = if result.is_ok() {
let cwd_fd = open(".", OFlag::O_PATH | OFlag::O_DIRECTORY, Mode::empty()).unwrap();
Some(getdir_long(cwd_fd, max))
} else {
None
};
fchdir(fd).unwrap();
Command::new("rm").arg("-rf").arg(name).status().unwrap();
assert!(result.is_ok(), "chdir_long failed: {result:?}");
let result_cwd = result_cwd.unwrap();
assert!(result_cwd.is_ok(), "getdir_long failed: {result_cwd:?}");
let result_cwd = result_cwd.unwrap();
assert_eq!(cwd, result_cwd, "getdir_long returned incorrect directory");
}
#[test]
fn test_getdir_long_limit_max_components() {
const MAX_DEPTH: usize = 128;
let o = "o".repeat(200);
let name = XPathBuf::from(format!("syd_test_getdir_l{o}ng_limit_max_components.dir"));
setup_deep_directory_structure(&name, MAX_DEPTH).unwrap();
let fd = open(".", OFlag::O_PATH | OFlag::O_DIRECTORY, Mode::empty()).unwrap();
let mut cwd = getcwd().map(XPathBuf::from).unwrap();
let max = cwd.depth() + MAX_DEPTH;
for _ in 0..MAX_DEPTH {
cwd.push(name.as_bytes());
}
let result = chdir_long(&cwd);
let result_cwd = if result.is_ok() {
let cwd_fd = open(".", OFlag::O_PATH | OFlag::O_DIRECTORY, Mode::empty()).unwrap();
Some(getdir_long(cwd_fd, max))
} else {
None
};
fchdir(fd).unwrap();
Command::new("rm").arg("-rf").arg(name).status().unwrap();
assert!(result.is_ok(), "chdir_long failed: {result:?}");
let result_cwd = result_cwd.unwrap();
assert_eq!(
result_cwd,
Err(Errno::ERANGE),
"getdir_long did not fail as expected: {result_cwd:?}"
);
}
#[test]
fn test_fd_status_flags_file_read_only() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
let flags = fd_status_flags(&file).unwrap();
assert!(!flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_file_write_only() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().write(true).open(temp.path()).unwrap();
let flags = fd_status_flags(&file).unwrap();
assert!(flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_file_read_write() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.read(true)
.write(true)
.open(temp.path())
.unwrap();
let flags = fd_status_flags(&file).unwrap();
assert!(flags.contains(OFlag::O_RDWR));
assert!(!flags.contains(OFlag::O_WRONLY));
}
#[test]
fn test_fd_status_flags_owned_fd_read_only() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
let owned_fd = unsafe { OwnedFd::from_raw_fd(file.as_raw_fd()) };
std::mem::forget(file);
let flags = fd_status_flags(&owned_fd).unwrap();
assert!(!flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_owned_fd_write_only() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().write(true).open(temp.path()).unwrap();
let owned_fd = unsafe { OwnedFd::from_raw_fd(file.as_raw_fd()) };
std::mem::forget(file);
let flags = fd_status_flags(&owned_fd).unwrap();
assert!(flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_owned_fd_read_write() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.read(true)
.write(true)
.open(temp.path())
.unwrap();
let owned_fd = unsafe { OwnedFd::from_raw_fd(file.as_raw_fd()) };
std::mem::forget(file);
let flags = fd_status_flags(&owned_fd).unwrap();
assert!(flags.contains(OFlag::O_RDWR));
assert!(!flags.contains(OFlag::O_WRONLY));
}
#[test]
fn test_fd_status_flags_borrowed_fd_read_only() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
let borrowed_fd = file.as_fd();
let flags = fd_status_flags(borrowed_fd).unwrap();
assert!(!flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_borrowed_fd_write_only() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().write(true).open(temp.path()).unwrap();
let borrowed_fd = file.as_fd();
let flags = fd_status_flags(borrowed_fd).unwrap();
assert!(flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_borrowed_fd_read_write() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.read(true)
.write(true)
.open(temp.path())
.unwrap();
let borrowed_fd = file.as_fd();
let flags = fd_status_flags(borrowed_fd).unwrap();
assert!(flags.contains(OFlag::O_RDWR));
assert!(!flags.contains(OFlag::O_WRONLY));
}
#[test]
fn test_fd_status_flags_dev_null_read() {
let file = OpenOptions::new().read(true).open("/dev/null").unwrap();
let flags = fd_status_flags(&file).unwrap();
assert!(!flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_dev_null_write() {
let file = OpenOptions::new().write(true).open("/dev/null").unwrap();
let flags = fd_status_flags(&file).unwrap();
assert!(flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_dev_null_read_write() {
let file = OpenOptions::new()
.read(true)
.write(true)
.open("/dev/null")
.unwrap();
let flags = fd_status_flags(&file).unwrap();
assert!(flags.contains(OFlag::O_RDWR));
assert!(!flags.contains(OFlag::O_WRONLY));
}
#[test]
fn test_fd_status_flags_pipe_read_end() {
let (read_fd, _) = pipe().unwrap();
let flags = fd_status_flags(&read_fd).unwrap();
assert!(!flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_pipe_write_end() {
let (_, write_fd) = pipe().unwrap();
let flags = fd_status_flags(&write_fd).unwrap();
assert!(flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_append_mode() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.write(true)
.append(true)
.open(temp.path())
.unwrap();
let flags = fd_status_flags(&file).unwrap();
assert!(flags.contains(OFlag::O_WRONLY));
assert!(flags.contains(OFlag::O_APPEND));
}
#[test]
fn test_fd_status_flags_create_mode() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.write(true)
.create(true)
.open(temp.path())
.unwrap();
let flags = fd_status_flags(&file).unwrap();
assert!(flags.contains(OFlag::O_WRONLY));
}
#[test]
fn test_fd_status_flags_truncate_mode() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.write(true)
.truncate(true)
.open(temp.path())
.unwrap();
let flags = fd_status_flags(&file).unwrap();
assert!(flags.contains(OFlag::O_WRONLY));
}
#[test]
fn test_fd_status_flags_read_append_mode() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.read(true)
.append(true)
.open(temp.path())
.unwrap();
let flags = fd_status_flags(&file).unwrap();
assert!(flags.contains(OFlag::O_RDWR));
assert!(flags.contains(OFlag::O_APPEND));
}
#[test]
fn test_fd_status_flags_create_new_mode() {
let temp = NamedTempFile::new().unwrap();
std::fs::remove_file(temp.path()).unwrap();
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(temp.path())
.unwrap();
let flags = fd_status_flags(&file).unwrap();
assert!(flags.contains(OFlag::O_WRONLY));
}
#[test]
fn test_fd_status_flags_reference_to_file() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
let file_ref = &file;
let flags = fd_status_flags(file_ref).unwrap();
assert!(!flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_mutable_reference_to_file() {
let temp = NamedTempFile::new().unwrap();
let mut file = OpenOptions::new().write(true).open(temp.path()).unwrap();
let file_ref = &mut file;
let flags = fd_status_flags(file_ref).unwrap();
assert!(flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_box_file() {
let temp = NamedTempFile::new().unwrap();
let file = Box::new(OpenOptions::new().read(true).open(temp.path()).unwrap());
let flags = fd_status_flags(&file).unwrap();
assert!(!flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_arc_file() {
use std::sync::Arc;
let temp = NamedTempFile::new().unwrap();
let file = Arc::new(OpenOptions::new().read(true).open(temp.path()).unwrap());
let flags = fd_status_flags(&file).unwrap();
assert!(!flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_rc_file() {
use std::rc::Rc;
let temp = NamedTempFile::new().unwrap();
let file = Rc::new(OpenOptions::new().read(true).open(temp.path()).unwrap());
let flags = fd_status_flags(&file).unwrap();
assert!(!flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_fd_status_flags_invalid_fd() {
let result = fd_status_flags(AT_BADFD);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), Errno::EBADF);
}
#[test]
fn test_fd_status_flags_multiple_calls_consistency() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.read(true)
.write(true)
.open(temp.path())
.unwrap();
let flags1 = fd_status_flags(&file).unwrap();
let flags2 = fd_status_flags(&file).unwrap();
let flags3 = fd_status_flags(&file).unwrap();
assert_eq!(flags1, flags2);
assert_eq!(flags2, flags3);
}
#[test]
fn test_fd_status_flags_different_file_types() {
let temp = NamedTempFile::new().unwrap();
let file1 = OpenOptions::new().write(true).open(temp.path()).unwrap();
let file2 = OpenOptions::new().write(true).open("/dev/null").unwrap();
let flags1 = fd_status_flags(&file1).unwrap();
let flags2 = fd_status_flags(&file2).unwrap();
assert!(flags1.contains(OFlag::O_WRONLY));
assert!(flags2.contains(OFlag::O_WRONLY));
}
#[test]
fn test_fd_status_flags_dup_file_descriptor() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
let duped_fd = dup(&file).unwrap();
let flags = fd_status_flags(&duped_fd).unwrap();
assert!(!flags.contains(OFlag::O_WRONLY));
assert!(!flags.contains(OFlag::O_RDWR));
}
#[test]
fn test_is_writable_fd_file_read_only() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
let result = is_writable_fd(&file).unwrap();
assert!(!result);
}
#[test]
fn test_is_writable_fd_file_write_only() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().write(true).open(temp.path()).unwrap();
let result = is_writable_fd(&file).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_file_read_write() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.read(true)
.write(true)
.open(temp.path())
.unwrap();
let result = is_writable_fd(&file).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_owned_fd_read_only() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
let owned_fd = unsafe { OwnedFd::from_raw_fd(file.as_raw_fd()) };
std::mem::forget(file);
let result = is_writable_fd(&owned_fd).unwrap();
assert!(!result);
}
#[test]
fn test_is_writable_fd_owned_fd_write_only() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().write(true).open(temp.path()).unwrap();
let owned_fd = unsafe { OwnedFd::from_raw_fd(file.as_raw_fd()) };
std::mem::forget(file);
let result = is_writable_fd(&owned_fd).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_owned_fd_read_write() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.read(true)
.write(true)
.open(temp.path())
.unwrap();
let owned_fd = unsafe { OwnedFd::from_raw_fd(file.as_raw_fd()) };
std::mem::forget(file);
let result = is_writable_fd(&owned_fd).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_borrowed_fd_read_only() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
let borrowed_fd = file.as_fd();
let result = is_writable_fd(borrowed_fd).unwrap();
assert!(!result);
}
#[test]
fn test_is_writable_fd_borrowed_fd_write_only() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().write(true).open(temp.path()).unwrap();
let borrowed_fd = file.as_fd();
let result = is_writable_fd(borrowed_fd).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_borrowed_fd_read_write() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.read(true)
.write(true)
.open(temp.path())
.unwrap();
let borrowed_fd = file.as_fd();
let result = is_writable_fd(borrowed_fd).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_dev_null_read() {
let file = OpenOptions::new().read(true).open("/dev/null").unwrap();
let result = is_writable_fd(&file).unwrap();
assert!(!result);
}
#[test]
fn test_is_writable_fd_dev_null_write() {
let file = OpenOptions::new().write(true).open("/dev/null").unwrap();
let result = is_writable_fd(&file).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_dev_null_read_write() {
let file = OpenOptions::new()
.read(true)
.write(true)
.open("/dev/null")
.unwrap();
let result = is_writable_fd(&file).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_pipe_read_end() {
let (read_fd, _) = pipe().unwrap();
let result = is_writable_fd(&read_fd).unwrap();
assert!(!result);
}
#[test]
fn test_is_writable_fd_pipe_write_end() {
let (_, write_fd) = pipe().unwrap();
let result = is_writable_fd(&write_fd).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_append_mode() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.write(true)
.append(true)
.open(temp.path())
.unwrap();
let result = is_writable_fd(&file).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_create_mode() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.write(true)
.create(true)
.open(temp.path())
.unwrap();
let result = is_writable_fd(&file).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_truncate_mode() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.write(true)
.truncate(true)
.open(temp.path())
.unwrap();
let result = is_writable_fd(&file).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_read_append_mode() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.read(true)
.append(true)
.open(temp.path())
.unwrap();
let result = is_writable_fd(&file).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_create_new_mode() {
let temp = NamedTempFile::new().unwrap();
std::fs::remove_file(temp.path()).unwrap();
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(temp.path())
.unwrap();
let result = is_writable_fd(&file).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_read_only_with_create() {
let temp = NamedTempFile::new().unwrap();
let file = open(
temp.path(),
OFlag::O_RDONLY | OFlag::O_CREAT | OFlag::O_TRUNC,
Mode::empty(),
)
.map(File::from)
.unwrap();
let result = is_writable_fd(&file).unwrap();
assert!(!result);
}
#[test]
fn test_is_writable_fd_reference_to_file() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
let file_ref = &file;
let result = is_writable_fd(file_ref).unwrap();
assert!(!result);
}
#[test]
fn test_is_writable_fd_mutable_reference_to_file() {
let temp = NamedTempFile::new().unwrap();
let mut file = OpenOptions::new().write(true).open(temp.path()).unwrap();
let file_ref = &mut file;
let result = is_writable_fd(file_ref).unwrap();
assert!(result);
}
#[test]
fn test_is_writable_fd_box_file() {
let temp = NamedTempFile::new().unwrap();
let file = Box::new(OpenOptions::new().read(true).open(temp.path()).unwrap());
let result = is_writable_fd(&file).unwrap();
assert!(!result);
}
#[test]
fn test_is_writable_fd_arc_file() {
use std::sync::Arc;
let temp = NamedTempFile::new().unwrap();
let file = Arc::new(OpenOptions::new().read(true).open(temp.path()).unwrap());
let result = is_writable_fd(&file).unwrap();
assert!(!result);
}
#[test]
fn test_is_writable_fd_rc_file() {
use std::rc::Rc;
let temp = NamedTempFile::new().unwrap();
let file = Rc::new(OpenOptions::new().read(true).open(temp.path()).unwrap());
let result = is_writable_fd(&file).unwrap();
assert!(!result);
}
#[test]
fn test_is_writable_fd_invalid_fd() {
let result = is_writable_fd(AT_BADFD);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), Errno::EBADF);
}
#[test]
fn test_is_writable_fd_multiple_calls_consistency() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new()
.read(true)
.write(true)
.open(temp.path())
.unwrap();
let result1 = is_writable_fd(&file).unwrap();
let result2 = is_writable_fd(&file).unwrap();
let result3 = is_writable_fd(&file).unwrap();
assert_eq!(result1, result2);
assert_eq!(result2, result3);
}
#[test]
fn test_is_writable_fd_different_file_types() {
let temp = NamedTempFile::new().unwrap();
let file1 = OpenOptions::new().write(true).open(temp.path()).unwrap();
let file2 = OpenOptions::new().write(true).open("/dev/null").unwrap();
let result1 = is_writable_fd(&file1).unwrap();
let result2 = is_writable_fd(&file2).unwrap();
assert!(result1);
assert!(result2);
}
#[test]
fn test_is_writable_fd_dup_file_descriptor() {
let temp = NamedTempFile::new().unwrap();
let file = OpenOptions::new().read(true).open(temp.path()).unwrap();
let duped_fd = dup(&file).unwrap();
let result = is_writable_fd(&duped_fd).unwrap();
assert!(!result);
}
#[test]
fn test_base_offset_root_and_non_root() {
let off = |parent_len: usize| parent_len + usize::from(parent_len > 1);
assert_eq!(off(1), 1, "root parent must not drop first byte");
assert_eq!(off(5), 6, "non-root parent must skip one separator");
}
#[test]
fn test_peer_inode_socketpair() {
if !check_unix_diag().unwrap_or(false) {
eprintln!("UNIX socket diagnostics are not supported, skipping!");
return;
}
let (a_fd, b_fd) = socketpair(
AddressFamily::Unix,
SockType::Stream,
None,
SockFlag::SOCK_CLOEXEC,
)
.expect("socketpair failed");
let stx_b = fstatx(&b_fd, STATX_INO).expect("fstatx on b failed");
let expected = (stx_b.stx_ino & 0xffff_ffff) as u64;
let got = peer_inode(&a_fd).expect("peer_inode failed for socketpair");
assert_eq!(
got, expected,
"peer_inode returned unexpected inode for socketpair"
);
}
#[test]
fn test_peer_inode_listener_filesystem() {
if !check_unix_diag().unwrap_or(false) {
eprintln!("UNIX socket diagnostics are not supported, skipping!");
return;
}
let td = tempdir().expect("tempdir failed");
let sock_path = td.as_path().join("peer_inode.sock");
let (tx_ready, rx_ready) = mpsc::channel::<()>();
let (tx_peer, rx_peer) = mpsc::channel::<u64>();
let sock_path_clone = sock_path.clone();
let server = thread::spawn(move || {
let listener = UnixListener::bind(&sock_path_clone).expect("bind failed in server");
tx_ready.send(()).expect("notify failed");
let (accepted, _addr) = listener.accept().expect("accept failed in server");
let peer = peer_inode(&accepted).expect("peer_inode failed on accepted socket");
tx_peer.send(peer).expect("send peer failed");
});
rx_ready
.recv_timeout(Duration::from_secs(10))
.expect("server did not signal ready");
let client = loop {
match UnixStream::connect(&sock_path) {
Ok(s) => break s,
Err(e) => {
if e.kind() == ErrorKind::NotFound || e.kind() == ErrorKind::ConnectionRefused {
thread::sleep(Duration::from_millis(10));
continue;
} else {
panic!("connect failed: {e:?}");
}
}
}
};
let stx_client = fstatx(&client, STATX_INO).expect("fstatx client failed");
let expected = (stx_client.stx_ino & 0xffff_ffff) as u64;
let got = rx_peer
.recv_timeout(Duration::from_secs(10))
.expect("server thread did not send peer inode");
drop(td);
assert_eq!(got, expected, "peer_inode mismatch for filesystem listener");
server.join().expect("server thread panicked");
}
#[test]
fn test_peer_inode_listener_abstract() {
if !check_unix_diag().unwrap_or(false) {
eprintln!("UNIX socket diagnostics are not supported, skipping!");
return;
}
let name = b"peer_inode_test_abstract_12345";
let srv_fd = socket(
AddressFamily::Unix,
SockType::Stream,
SockFlag::SOCK_CLOEXEC,
None,
)
.expect("socket failed for abstract server");
let sockaddr = UnixAddr::new_abstract(name).expect("new_abstract failed");
bind(srv_fd.as_raw_fd(), &sockaddr).expect("bind abstract failed");
listen(&srv_fd, Backlog::new(1).unwrap()).expect("listen failed for abstract");
let cli_fd = socket(
AddressFamily::Unix,
SockType::Stream,
SockFlag::SOCK_CLOEXEC,
None,
)
.expect("socket failed for abstract client");
connect(cli_fd.as_raw_fd(), &sockaddr).expect("connect abstract failed");
let acc_fd = accept(srv_fd.as_raw_fd()).expect("accept failed for abstract");
let acc_fd = unsafe { OwnedFd::from_raw_fd(acc_fd) };
let stx_client = fstatx(&cli_fd, STATX_INO).expect("fstatx client failed");
let expected = (stx_client.stx_ino & 0xffff_ffff) as u64;
let got = peer_inode(&acc_fd).expect("peer_inode failed for abstract");
drop(srv_fd);
assert_eq!(got, expected, "peer_inode mismatch for abstract socket");
}
#[test]
fn test_peer_inode_symmetry_socketpair() {
if !check_unix_diag().unwrap_or(false) {
eprintln!("UNIX socket diagnostics are not supported, skipping!");
return;
}
let (a_fd, b_fd) = socketpair(
AddressFamily::Unix,
SockType::Stream,
None,
SockFlag::SOCK_CLOEXEC,
)
.expect("socketpair failed");
let stx_a = fstatx(&a_fd, STATX_INO).expect("fstatx a failed");
let stx_b = fstatx(&b_fd, STATX_INO).expect("fstatx b failed");
let expected_a = (stx_a.stx_ino & 0xffff_ffff) as u64;
let expected_b = (stx_b.stx_ino & 0xffff_ffff) as u64;
let got_from_a = peer_inode(&a_fd).expect("peer_inode on a failed");
let got_from_b = peer_inode(&b_fd).expect("peer_inode on b failed");
assert_eq!(
got_from_a, expected_b,
"peer_inode(a) should equal inode(b)"
);
assert_eq!(
got_from_b, expected_a,
"peer_inode(b) should equal inode(a)"
);
}
#[test]
fn test_oflag_rdonly_is_empty() {
let mut flags = OFlag::empty();
flags.insert(OFlag::O_RDONLY);
assert!(flags.is_empty());
}
#[test]
fn test_oflag_accmode() {
assert_eq!(oflag_accmode(OFlag::empty()), OFlag::O_RDONLY);
assert_eq!(oflag_accmode(OFlag::O_RDONLY), OFlag::O_RDONLY);
assert_eq!(oflag_accmode(OFlag::O_WRONLY), OFlag::O_WRONLY);
assert_eq!(oflag_accmode(OFlag::O_RDWR), OFlag::O_RDWR);
assert_eq!(oflag_accmode(OFlag::O_PATH), OFlag::O_PATH);
assert_eq!(
oflag_accmode(OFlag::empty() | OFlag::O_APPEND),
OFlag::O_RDONLY
);
assert_eq!(
oflag_accmode(OFlag::O_RDONLY | OFlag::O_ASYNC),
OFlag::O_RDONLY
);
assert_eq!(
oflag_accmode(OFlag::O_WRONLY | OFlag::O_CREAT | OFlag::O_EXCL | OFlag::O_TRUNC),
OFlag::O_WRONLY
);
assert_eq!(
oflag_accmode(OFlag::O_RDWR | OFlag::O_CLOEXEC | OFlag::O_DIRECTORY),
OFlag::O_RDWR
);
assert_eq!(
oflag_accmode(OFlag::O_PATH | OFlag::O_NOFOLLOW),
OFlag::O_PATH
);
}
}