#![deny(unsafe_code)]
use std::{
borrow::Cow,
ffi::{CStr, OsStr, OsString},
fmt,
io::{BufReader, Cursor, IoSlice, IoSliceMut, Read, Seek, SeekFrom, Write},
num::NonZeroUsize,
ops::Range,
os::{
fd::{AsFd, AsRawFd, RawFd},
unix::ffi::OsStrExt,
},
ptr::NonNull,
};
use bitflags::bitflags;
use btoi::{btoi, btoi_radix};
use memchr::{arch::all::is_prefix, memchr, memrchr};
use nix::{
errno::Errno,
fcntl::OFlag,
libc::pid_t,
sys::{
stat::Mode,
sysinfo::sysinfo,
uio::{process_vm_readv, process_vm_writev, RemoteIoVec},
},
unistd::{getpid, gettid, lseek64, write, Gid, Pid, Uid, Whence},
NixPath,
};
use procfs_core::{
net::UnixNetEntries,
process::{
LimitValue, MMPermissions, MMapPath, MemoryMaps, Namespace, Namespaces, SmapsRollup,
},
CryptoTable, FromBufRead,
};
use serde::{ser::SerializeMap, Serialize, Serializer};
use crate::{
compat::{getdents64, openat2, pidfd_get_tgid, statx, OpenHow, ResolveFlag, STATX_INO},
config::*,
confine::SydMemoryMap,
cookie::{CookieIdx, SYSCOOKIE_POOL},
elf::ElfType,
err::{err2no, proc_error_to_errno},
fd::{is_empty_file, is_open_fd, is_proc, parse_fd, SafeOwnedFd, AT_BADFD, PROC_FILE},
fs::readlinkat,
hash::{SydHashMap, SydHashSet, SydIndexSet},
io::{read_all, read_buf, write_all},
lookup::safe_open,
parsers::{
map_result,
proc::{
parse_max_open_files, parse_pidfd_info_pid, parse_stat, parse_statm, parse_status,
parse_status_interrupt, parse_status_tgid, parse_status_umask,
},
read_to_end,
},
path::{XPathBuf, PATH_MAX},
retry::retry_on_eintr,
rng::randint,
sandbox::Sandbox,
sigset::SydSigSet,
XPath,
};
bitflags! {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct ProcmapQueryFlags: u64 {
const VMA_READABLE = 0x01;
const VMA_WRITABLE = 0x02;
const VMA_EXECUTABLE = 0x04;
const VMA_SHARED = 0x08;
const COVERING_OR_NEXT_VMA = 0x10;
const FILE_BACKED_VMA = 0x20;
}
}
impl fmt::Display for ProcmapQueryFlags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.contains(Self::VMA_READABLE) {
f.write_str("r")?;
} else {
f.write_str("-")?;
}
if self.contains(Self::VMA_WRITABLE) {
f.write_str("w")?;
} else {
f.write_str("-")?;
}
if self.contains(Self::VMA_EXECUTABLE) {
f.write_str("x")?;
} else {
f.write_str("-")?;
}
if self.contains(Self::VMA_SHARED) {
f.write_str("s")?;
} else {
f.write_str("p")?;
}
Ok(())
}
}
impl Serialize for ProcmapQueryFlags {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
impl From<MMPermissions> for ProcmapQueryFlags {
fn from(perms: MMPermissions) -> Self {
let mut flags = Self::empty();
if perms.contains(MMPermissions::READ) {
flags.insert(Self::VMA_READABLE);
}
if perms.contains(MMPermissions::WRITE) {
flags.insert(Self::VMA_WRITABLE);
}
if perms.contains(MMPermissions::EXECUTE) {
flags.insert(Self::VMA_EXECUTABLE);
}
if perms.contains(MMPermissions::SHARED) {
flags.insert(Self::VMA_SHARED);
}
flags
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ProcmapQuery {
pub size: u64,
pub query_flags: u64,
pub query_addr: u64,
pub vma_start: u64,
pub vma_end: u64,
pub vma_flags: u64,
pub vma_page_size: u64,
pub vma_offset: u64,
pub inode: u64,
pub dev_major: u32,
pub dev_minor: u32,
pub vma_name_size: u32,
pub build_id_size: u32,
pub vma_name_addr: u64,
pub build_id_addr: u64,
}
impl Default for ProcmapQuery {
fn default() -> Self {
Self {
size: size_of::<Self>() as u64,
query_flags: 0,
query_addr: 0,
vma_start: 0,
vma_end: 0,
vma_flags: 0,
vma_page_size: 0,
vma_offset: 0,
inode: 0,
dev_major: 0,
dev_minor: 0,
vma_name_size: 0,
build_id_size: 0,
vma_name_addr: 0,
build_id_addr: 0,
}
}
}
pub const PROCFS_IOCTL_MAGIC: u32 = b'f' as u32;
pub const PROCMAP_QUERY: libc::c_ulong =
libc::_IOWR::<ProcmapQuery>(PROCFS_IOCTL_MAGIC, 17) as libc::c_ulong;
pub fn procmap_query<Fd: AsFd>(
fd: Fd,
flags: ProcmapQueryFlags,
query_addr: u64,
name_buf: Option<&mut [u8]>,
build_id_buf: Option<&mut [u8]>,
) -> Result<ProcmapQuery, Errno> {
let mut q = ProcmapQuery {
query_addr,
query_flags: flags.bits(),
..Default::default()
};
if let Some(buf) = name_buf {
q.vma_name_size = u32::try_from(buf.len()).or(Err(Errno::EINVAL))?;
q.vma_name_addr = buf.as_mut_ptr() as u64;
}
if let Some(buf) = build_id_buf {
q.build_id_size = u32::try_from(buf.len()).or(Err(Errno::EINVAL))?;
q.build_id_addr = buf.as_mut_ptr() as u64;
}
#[expect(unsafe_code)]
Errno::result(unsafe {
libc::syscall(
libc::SYS_ioctl,
fd.as_fd().as_raw_fd(),
PROCMAP_QUERY,
&mut q,
SYSCOOKIE_POOL.get(CookieIdx::ProcmapQueryArg3),
SYSCOOKIE_POOL.get(CookieIdx::ProcmapQueryArg4),
SYSCOOKIE_POOL.get(CookieIdx::ProcmapQueryArg5),
)
})
.map(|_| q)
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Statm {
pub size: u64,
}
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
pub struct Stat {
pub num_threads: u64,
pub startbrk: u64,
pub startstack: u64,
pub tty_nr: i32,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct Status {
pub umask: Mode,
pub pid: Pid,
pub sig_pending_thread: SydSigSet,
pub sig_pending_process: SydSigSet,
pub sig_blocked: SydSigSet,
pub sig_ignored: SydSigSet,
pub sig_caught: SydSigSet,
}
impl Default for Status {
fn default() -> Self {
Self {
umask: Mode::empty(),
pid: Pid::from_raw(0),
sig_pending_thread: SydSigSet::default(),
sig_pending_process: SydSigSet::default(),
sig_blocked: SydSigSet::default(),
sig_ignored: SydSigSet::default(),
sig_caught: SydSigSet::default(),
}
}
}
impl Serialize for Status {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut map = serializer.serialize_map(Some(7))?;
map.serialize_entry("umask", &self.umask.bits())?;
map.serialize_entry("pid", &self.pid.as_raw())?;
map.serialize_entry("sig_pending_thread", &self.sig_pending_thread)?;
map.serialize_entry("sig_pending_process", &self.sig_pending_process)?;
map.serialize_entry("sig_blocked", &self.sig_blocked)?;
map.serialize_entry("sig_ignored", &self.sig_ignored)?;
map.serialize_entry("sig_caught", &self.sig_caught)?;
map.end()
}
}
pub const OFLAG_PROC: OFlag =
OFlag::from_bits_retain(libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW | libc::O_NOCTTY);
pub const RFLAG_PROC: ResolveFlag = ResolveFlag::from_bits_retain(
ResolveFlag::RESOLVE_NO_MAGICLINKS.bits()
| ResolveFlag::RESOLVE_NO_SYMLINKS.bits()
| ResolveFlag::RESOLVE_BENEATH.bits()
| ResolveFlag::RESOLVE_NO_XDEV.bits(),
);
fn proc_errno(err: Errno) -> Errno {
match err {
Errno::ENOENT => Errno::ESRCH,
err => err,
}
}
pub fn safe_open_proc<P: NixPath + ?Sized>(path: &P) -> Result<SafeOwnedFd, Errno> {
safe_open(PROC_FILE(), path, OFLAG_PROC, RFLAG_PROC)
}
const LOW_DENSITY_NAMES: &[&str] = &[
"LU0", "LU1", "LU2", "LU3", "FB0", "SA0", "SA1", "SA2", "SC0", "SC1", "SC2", "SC3", "FW0",
"FW1", "FW2", "FW3", "AM0", "AM1", "AM2", "AM3", "AM4", "AM5", "AM6", "AM7", "AM8", "AM9",
"AM10", "AM11", "AM12", "AM13", "AM14", "AM15", "DB0", "DB1", "DB2", "DB3", "DB4", "DB5",
"DB6", "DB7", "SG0", "SMX0", "SMX1", "SMX2", "MM0", "MM1", "CPM0", "CPM1", "CPM2",
"CPM3",
"IOC0", "IOC1", "IOC2", "IOC3", "IOC4", "IOC5", "IOC6", "IOC7", "IOC8", "IOC9", "IOC10",
"IOC11", "IOC12", "IOC13", "IOC14", "IOC15", "IOC16", "IOC17", "IOC18", "IOC19", "IOC20",
"IOC21", "IOC22", "IOC23", "IOC24", "IOC25", "IOC26", "IOC27", "IOC28", "IOC29", "IOC30",
"IOC31", "VR0", "VR1", "IOC84", "IOC85", "IOC86", "IOC87", "IOC88", "IOC89", "IOC90", "IOC91",
"IOC92", "IOC93", "IOC94", "IOC95", "IOC96", "IOC97", "IOC98", "IOC99", "IOC100", "IOC101",
"IOC102", "IOC103", "IOC104", "IOC105", "IOC106", "IOC107", "IOC108", "IOC109", "IOC110",
"IOC111", "IOC112", "IOC113", "IOC114", "IOC115", "SIOC0", "SIOC1", "SIOC2", "SIOC3", "SIOC4",
"SIOC5", "SIOC6", "SIOC7", "SIOC8", "SIOC9", "SIOC10", "SIOC11", "SIOC12", "SIOC13", "SIOC14",
"SIOC15", "SIOC16", "SIOC17", "SIOC18", "SIOC19", "SIOC20", "SIOC21", "SIOC22", "SIOC23",
"SIOC24", "SIOC25", "SIOC26", "SIOC27", "SIOC28", "SIOC29", "SIOC30", "SIOC31", "PSC0", "PSC1",
"PSC2", "PSC3", "PSC4", "PSC5", "AT0", "AT1", "AT2", "AT3", "AT4", "AT5", "AT6", "AT7", "AT8",
"AT9", "AT10", "AT11", "AT12", "AT13", "AT14", "AT15", "NX0", "NX1", "NX2", "NX3", "NX4",
"NX5", "NX6", "NX7", "NX8", "NX9", "NX10", "NX11", "NX12", "NX13", "NX14", "NX15",
"J0", "UL0", "UL1", "UL2", "UL3", "xvc0", "PZ0", "PZ1", "PZ2", "PZ3", "TX0", "TX1", "TX2", "TX3", "TX4", "TX5", "TX6", "TX7", "SC0",
"SC1", "SC2", "SC3", "MAX0", "MAX1", "MAX2", "MAX3",
];
fn read_usize_from_ne_bytes(bytes: &[u8], ptr_size: usize) -> Result<usize, Errno> {
match ptr_size {
4 => {
if bytes.len() < 4 {
return Err(Errno::EFAULT);
}
Ok(usize::try_from(u32::from_ne_bytes(
bytes[..4].try_into().or(Err(Errno::EINVAL))?,
))
.or(Err(Errno::EINVAL))?)
}
8 => {
if bytes.len() < 8 {
return Err(Errno::EFAULT);
}
Ok(usize::try_from(u64::from_ne_bytes(
bytes[..8].try_into().or(Err(Errno::EINVAL))?,
))
.or(Err(Errno::EINVAL))?)
}
_ => Err(Errno::EINVAL),
}
}
fn usize_to_ne_bytes(value: usize, sizeof_ptr: usize) -> Vec<u8> {
#[expect(clippy::cast_possible_truncation)]
match sizeof_ptr {
4 => (value as u32).to_ne_bytes().to_vec(),
8 => (value as u64).to_ne_bytes().to_vec(),
_ => unreachable!("Invalid pointer size!"),
}
}
pub fn proc_stat(pid: Pid) -> Result<Stat, Errno> {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/stat".len()).or(Err(Errno::ENOMEM))?;
path.push(b"stat");
let file = safe_open_proc(&path).map_err(proc_errno)?;
let mut buf = [0; 1024]; map_result(parse_stat(read_to_end(file, &mut buf)?))
}
pub fn proc_statm(pid: Pid) -> Result<Statm, Errno> {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/statm".len()).or(Err(Errno::ENOMEM))?;
path.push(b"statm");
let file = safe_open_proc(&path).map_err(proc_errno)?;
let mut buf = [0; 256]; map_result(parse_statm(read_to_end(file, &mut buf)?))
}
pub fn proc_status(pid: Pid) -> Result<Status, Errno> {
proc_status_read(proc_status_open(pid)?)
}
pub fn proc_interrupt(pid: Pid) -> Result<SydSigSet, Errno> {
proc_interrupt_read(proc_status_open(pid)?)
}
pub fn proc_status_open(pid: Pid) -> Result<SafeOwnedFd, Errno> {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/status".len()).or(Err(Errno::ENOMEM))?;
path.push(b"status");
safe_open_proc(&path).map_err(proc_errno)
}
pub fn proc_status_read<Fd: AsFd>(fd: Fd) -> Result<Status, Errno> {
let mut buf = [0; 2048];
map_result(parse_status(read_to_end(fd, &mut buf)?))
}
pub fn proc_interrupt_read<Fd: AsFd>(fd: Fd) -> Result<SydSigSet, Errno> {
let mut buf = [0u8; 1408];
let nread = read_buf(fd, &mut buf)?;
map_result(parse_status_interrupt(&buf[..nread]))
}
pub fn proc_tgid(tid: Pid) -> Result<Pid, Errno> {
let fd = proc_status_open(tid)?;
let mut data = [0u8; 192];
let nread = read_buf(fd, &mut data)?;
map_result(parse_status_tgid(&data[..nread]))
}
pub fn proc_umask(pid: Pid) -> Result<Mode, Errno> {
let fd = proc_status_open(pid)?;
let mut data = [0u8; 160];
let nread = read_buf(fd, &mut data)?;
map_result(parse_status_umask(&data[..nread]))
}
pub fn proc_pidfd_get_tgid<Fd: AsFd>(pidfd: Fd) -> Result<Pid, Errno> {
if *HAVE_PIDFD_GET_INFO {
return pidfd_get_tgid(&pidfd);
}
let mut path = XPathBuf::from_pid(gettid())?;
path.try_reserve(b"/fdinfo".len()).or(Err(Errno::ENOMEM))?;
path.push(b"fdinfo");
path.push_fd(pidfd.as_fd().as_raw_fd());
let file = safe_open(PROC_FILE(), &path, OFLAG_PROC, RFLAG_PROC).map_err(proc_errno)?;
let mut buf = [0u8; 256];
let nread = read_buf(file, &mut buf)?;
map_result(parse_pidfd_info_pid(&buf[..nread]))
}
pub fn proc_stack(pid: Pid) -> Result<Range<u64>, Errno> {
let maps = proc_maps(pid)?;
for map in maps {
if let MMapPath::Stack = map.0.pathname {
return Ok(map.0.address.0..map.0.address.1);
}
}
Err(Errno::ENOENT)
}
pub fn proc_stack_start(pid: Pid) -> Result<u64, Errno> {
proc_stat(pid).map(|stat| stat.startstack)
}
pub fn proc_stack_pointer(pid: Pid) -> Result<u64, Errno> {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/syscall".len()).or(Err(Errno::ENOMEM))?;
path.push(b"syscall");
let file = safe_open_proc(&path).map_err(proc_errno)?;
let mut buf = [0u8; 256];
let data = read_to_end(file, &mut buf)?;
parse_stack_pointer(data)
}
fn parse_stack_pointer(data: &[u8]) -> Result<u64, Errno> {
if is_prefix(data, b"running") {
return Err(Errno::EBUSY);
}
let last_sp = match memrchr(b' ', data) {
Some(i) => i,
None => return Err(Errno::ENOENT),
};
let start = if let Some(prev) = memrchr(b' ', &data[..last_sp]) {
prev.checked_add(1).ok_or(Errno::EOVERFLOW)?
} else {
0
};
let rsp = &data[start..last_sp];
if let Some(hex) = rsp.strip_prefix(b"0x") {
btoi_radix::<u64>(hex, 16).or(Err(Errno::EINVAL))
} else {
Err(Errno::ENOENT)
}
}
pub fn proc_auxv(pid: Pid) -> Result<SydHashMap<u64, u64>, Errno> {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/auxv".len()).or(Err(Errno::ENOMEM))?;
path.push(b"auxv");
let fd = safe_open_proc(&path).map_err(proc_errno)?;
let mut map = SydHashMap::default();
let buf = read_all(&fd)?;
if buf.is_empty() {
return Ok(map);
}
let mut fd = Cursor::new(buf);
let mut buf = 0usize.to_ne_bytes();
loop {
fd.read_exact(&mut buf).map_err(|err| err2no(&err))?;
let key = usize::from_ne_bytes(buf) as u64;
fd.read_exact(&mut buf).map_err(|err| err2no(&err))?;
let value = usize::from_ne_bytes(buf) as u64;
if key == 0 && value == 0 {
break;
}
map.insert(key, value);
}
Ok(map)
}
pub fn proc_cwd(pid: Pid) -> Result<XPathBuf, Errno> {
let path = XPathBuf::from_cwd(pid)?;
readlinkat(PROC_FILE(), &path).map_err(proc_errno)
}
pub fn proc_comm(pid: Pid) -> Result<XPathBuf, Errno> {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/comm".len()).or(Err(Errno::ENOMEM))?;
path.push(b"comm");
let file = safe_open_proc(&path).map_err(proc_errno)?;
let mut comm = [0u8; 16];
let mut nread = read_buf(file, &mut comm)?;
let idx = nread.saturating_sub(1);
if comm[idx] == 0 {
nread = idx;
}
Ok(XPathBuf::from(OsStr::from_bytes(
comm[..nread].trim_ascii(),
)))
}
pub fn proc_cmdline(pid: Pid) -> Result<XPathBuf, Errno> {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/cmdline".len()).or(Err(Errno::ENOMEM))?;
path.push(b"cmdline");
let file = safe_open_proc(&path).map_err(proc_errno)?;
const LIMIT: usize = 256;
let mut data = [0u8; LIMIT];
let nread = read_buf(file, &mut data)?;
let mut data = data.to_vec();
#[expect(clippy::arithmetic_side_effects)]
if nread <= 1 {
return Ok(XPathBuf::empty());
} else if nread >= LIMIT - 1 {
if data[LIMIT - 1] != 0 {
data.extend_from_slice("…".as_bytes());
} else {
data.pop();
}
} else {
data.resize(nread - 1, 0);
}
for byte in &mut data {
if *byte == 0 {
*byte = b' ';
}
}
Ok(data.into())
}
pub fn proc_maps(pid: Pid) -> Result<Vec<SydMemoryMap>, Errno> {
proc_maps_read(proc_maps_open(pid)?)
}
pub fn proc_maps_open(pid: Pid) -> Result<SafeOwnedFd, Errno> {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/maps".len()).or(Err(Errno::ENOMEM))?;
path.push(b"maps");
safe_open_proc(&path).map_err(proc_errno)
}
pub fn proc_maps_read(fd: SafeOwnedFd) -> Result<Vec<SydMemoryMap>, Errno> {
MemoryMaps::from_buf_read(BufReader::new(fd))
.map(|maps| maps.0.into_iter().map(SydMemoryMap).collect::<Vec<_>>())
.map_err(|err| proc_error_to_errno(&err).unwrap_or(Errno::ENOSYS))
.map_err(proc_errno)
}
pub fn proc_smaps(pid: Pid) -> Result<Vec<SydMemoryMap>, Errno> {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/smaps".len()).or(Err(Errno::ENOMEM))?;
path.push(b"smaps");
let reader = safe_open_proc(&path)
.map(BufReader::new)
.map_err(proc_errno)?;
MemoryMaps::from_buf_read(reader)
.map(|maps| maps.0.into_iter().map(SydMemoryMap).collect::<Vec<_>>())
.map_err(|err| proc_error_to_errno(&err).unwrap_or(Errno::ENOSYS))
.map_err(proc_errno)
}
pub fn proc_smaps_rollup(pid: Pid) -> Result<SydMemoryMap, Errno> {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/smaps_rollup".len())
.or(Err(Errno::ENOMEM))?;
path.push(b"smaps_rollup");
let reader = safe_open_proc(&path)
.map(BufReader::new)
.map_err(proc_errno)?;
SmapsRollup::from_buf_read(reader)
.map_err(|err| proc_error_to_errno(&err).unwrap_or(Errno::ENOSYS))
.map_err(proc_errno)
.and_then(|maps| {
maps.memory_map_rollup
.into_iter()
.next()
.map(SydMemoryMap)
.ok_or(Errno::ENOMEM)
})
}
pub fn proc_mem(pid: Pid) -> Result<u64, Errno> {
let map = proc_smaps_rollup(pid)?;
let mut sum = 0u64;
for key in ["Pss", "Private_Dirty", "Shared_Dirty"] {
let val = map.0.extension.map.get(key).copied().unwrap_or(0);
sum = sum.saturating_add(val);
}
Ok(sum)
}
pub fn proc_pipemax() -> Result<libc::c_int, Errno> {
proc_pipemax_read(proc_pipemax_open()?)
}
pub fn proc_pipemax_open() -> Result<SafeOwnedFd, Errno> {
safe_open_proc("sys/fs/pipe-max-size")
}
pub fn proc_pipemax_read<Fd: AsFd>(fd: Fd) -> Result<libc::c_int, Errno> {
let mut data = [0u8; 24];
let nread = read_buf(fd, &mut data)?;
btoi::<libc::c_int>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))
}
pub fn proc_fs_file_max() -> Result<u64, Errno> {
let fd = proc_open(None).and_then(|fd| {
safe_open(
fd,
c"sys/fs/file/max",
OFlag::O_RDONLY | OFlag::O_NOCTTY,
ResolveFlag::RESOLVE_NO_XDEV,
)
})?;
if !is_empty_file(&fd).unwrap_or(false) {
return Err(Errno::EBADFD);
}
let mut data = [0u8; 25];
let nread = read_buf(fd, &mut data)?;
btoi::<u64>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))
}
pub fn proc_fs_nr_open() -> Result<u64, Errno> {
let fd = proc_open(None).and_then(|fd| {
safe_open(
fd,
c"sys/fs/nr_open",
OFlag::O_RDONLY | OFlag::O_NOCTTY,
ResolveFlag::RESOLVE_NO_XDEV,
)
})?;
if !is_empty_file(&fd).unwrap_or(false) {
return Err(Errno::EBADFD);
}
let mut data = [0u8; 25];
let nread = read_buf(fd, &mut data)?;
btoi::<u64>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))
}
pub fn proc_mmap_min_addr() -> Result<u64, Errno> {
let fd = proc_open(None).and_then(|fd| {
safe_open(
fd,
c"sys/vm/mmap_min_addr",
OFlag::O_RDONLY | OFlag::O_NOCTTY,
ResolveFlag::RESOLVE_NO_XDEV,
)
})?;
if !is_empty_file(&fd).unwrap_or(false) {
return Err(Errno::EBADFD);
}
let mut data = [0u8; 25];
let nread = read_buf(fd, &mut data)?;
btoi::<u64>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))
}
pub fn proc_kernel_randomize_va_space() -> Result<u8, Errno> {
let fd = proc_open(None).and_then(|fd| {
safe_open(
fd,
c"sys/kernel/randomize_va_space",
OFlag::O_RDONLY | OFlag::O_NOCTTY,
ResolveFlag::RESOLVE_NO_XDEV,
)
})?;
if !is_empty_file(&fd).unwrap_or(false) {
return Err(Errno::EBADFD);
}
let mut data = [0u8; 2];
let nread = read_buf(fd, &mut data)?;
btoi::<u8>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))
}
pub fn proc_yama_ptrace_scope() -> Result<u8, Errno> {
let fd = proc_open(None).and_then(|fd| {
safe_open(
fd,
c"sys/kernel/yama/ptrace_scope",
OFlag::O_RDONLY | OFlag::O_NOCTTY,
ResolveFlag::RESOLVE_NO_XDEV,
)
})?;
if !is_empty_file(&fd).unwrap_or(false) {
return Err(Errno::EBADFD);
}
let mut data = [0u8; 2];
let nread = read_buf(fd, &mut data)?;
btoi::<u8>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))
}
pub fn proc_net_bpf_jit_enable() -> Result<u8, Errno> {
let fd = proc_open(None).and_then(|fd| {
safe_open(
fd,
c"sys/net/core/bpf_jit_enable",
OFlag::O_RDONLY | OFlag::O_NOCTTY,
ResolveFlag::RESOLVE_NO_XDEV,
)
})?;
if !is_empty_file(&fd).unwrap_or(false) {
return Err(Errno::EBADFD);
}
let mut data = [0u8; 2];
let nread = read_buf(fd, &mut data)?;
btoi::<u8>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))
}
#[expect(clippy::arithmetic_side_effects)]
#[expect(clippy::cast_sign_loss)]
#[expect(clippy::disallowed_methods)]
pub fn proc_tty(pid: Pid) -> Result<XPathBuf, Errno> {
let stat = proc_stat(pid)?;
if stat.tty_nr <= 0 {
return Err(Errno::ENXIO);
}
let tty_nr = stat.tty_nr;
let major = (tty_nr & 0xfff00) >> 8;
let minor = (tty_nr & 0x000ff) | ((tty_nr >> 12) & 0xfff00);
match major {
3 => Ok(XPathBuf::from(format!(
"/dev/tty{}{}",
"pqrstuvwxyzabcde"[(minor >> 4) as usize..]
.chars()
.next()
.unwrap(),
"0123456789abcdef"[(minor & 0x0f) as usize..]
.chars()
.next()
.unwrap()
))),
4 => {
if minor < 64 {
Ok(XPathBuf::from(format!("/dev/tty{minor}")))
} else {
Ok(XPathBuf::from(format!("/dev/ttyS{}", minor - 64)))
}
}
11 => Ok(XPathBuf::from(format!("/dev/ttyB{minor}"))),
14 => Ok(XPathBuf::from(format!("/dev/tty{minor}"))), 17 => Ok(XPathBuf::from(format!("/dev/ttyH{minor}"))),
19 | 22 | 23 => Ok(XPathBuf::from(format!("/dev/ttyD{minor}"))),
24 => Ok(XPathBuf::from(format!("/dev/ttyE{minor}"))),
32 => Ok(XPathBuf::from(format!("/dev/ttyX{minor}"))),
43 => Ok(XPathBuf::from(format!("/dev/ttyI{minor}"))),
46 => Ok(XPathBuf::from(format!("/dev/ttyR{minor}"))),
48 => Ok(XPathBuf::from(format!("/dev/ttyL{minor}"))),
57 => Ok(XPathBuf::from(format!("/dev/ttyP{minor}"))),
71 => Ok(XPathBuf::from(format!("/dev/ttyF{minor}"))),
75 => Ok(XPathBuf::from(format!("/dev/ttyW{minor}"))),
78 | 112 => Ok(XPathBuf::from(format!("/dev/ttyM{minor}"))),
105 => Ok(XPathBuf::from(format!("/dev/ttyV{minor}"))),
136..=143 => Ok(XPathBuf::from(format!(
"/dev/pts/{}",
minor + (major - 136) * 256
))),
148 => Ok(XPathBuf::from(format!("/dev/ttyT{minor}"))),
154 | 156 => Ok(XPathBuf::from(format!(
"/dev/ttySR{}",
minor + if major == 156 { 256 } else { 0 }
))),
164 => Ok(XPathBuf::from(format!("/dev/ttyCH{minor}"))),
166 => Ok(XPathBuf::from(format!("/dev/ttyACM{minor}"))),
172 => Ok(XPathBuf::from(format!("/dev/ttyMX{minor}"))),
174 => Ok(XPathBuf::from(format!("/dev/ttySI{minor}"))),
188 => Ok(XPathBuf::from(format!("/dev/ttyUSB{minor}"))),
204 => {
if minor as usize >= LOW_DENSITY_NAMES.len() {
Err(Errno::ENXIO)
} else {
Ok(XPathBuf::from(format!(
"/dev/tty{}",
LOW_DENSITY_NAMES[minor as usize]
)))
}
}
208 => Ok(XPathBuf::from(format!("/dev/ttyU{minor}"))),
216 => Ok(XPathBuf::from(format!("/dev/ttyUB{minor}"))),
224 => Ok(XPathBuf::from(format!("/dev/ttyY{minor}"))),
227 => Ok(XPathBuf::from(format!("/dev/3270/tty{minor}"))),
229 => Ok(XPathBuf::from(format!("/dev/iseries/vtty{minor}"))),
256 => Ok(XPathBuf::from(format!("/dev/ttyEQ{minor}"))),
_ => Err(Errno::ENXIO),
}
}
pub fn proc_task_nr(pid: Pid) -> Result<u64, Errno> {
proc_stat(pid).map(|p| p.num_threads)
}
pub fn proc_task_nr_syd() -> Result<u64, Errno> {
proc_stat(Pid::this()).map(|p| p.num_threads)
}
pub fn proc_task_nr_sys() -> Result<u64, Errno> {
Ok(sysinfo()?.process_count().into())
}
pub fn proc_task_limit(pid: Pid, max: u64) -> Result<bool, Errno> {
let mut count = proc_task_nr(pid)?;
if count >= max {
return Ok(true);
}
let fd = PROC_FILE();
lseek64(fd, 0, Whence::SeekSet)?;
let this = Pid::this().as_raw();
let mut tasks = Vec::new();
tasks.try_reserve(DIRENT_BUF_SIZE).or(Err(Errno::ENOMEM))?;
loop {
let mut entries = match getdents64(&fd, DIRENT_BUF_SIZE) {
Ok(entries) => entries,
Err(Errno::ECANCELED | Errno::EACCES | Errno::ENOENT | Errno::EPERM | Errno::ESRCH) => {
break
}
Err(errno) => return Err(errno),
};
#[expect(clippy::arithmetic_side_effects)]
for entry in &mut entries {
if !entry.is_dir() {
continue;
}
let task = match btoi::<pid_t>(entry.name_bytes()) {
Ok(pid) => pid,
Err(_) => continue,
};
if task == pid.as_raw() || task == this {
continue;
}
tasks.push(task);
count += 1;
if count >= max {
return Ok(true);
}
}
}
for task in tasks {
#[expect(clippy::arithmetic_side_effects)]
match proc_task_nr(Pid::from_raw(task)) {
Ok(n) => count += n,
Err(_) => continue, }
if count >= max {
return Ok(true);
}
}
Ok(false)
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct SydExecMap {
pub path: XPathBuf,
pub inode: u64,
pub dev_major: i32,
pub dev_minor: i32,
}
pub fn proc_executables(pid: Pid) -> Result<Vec<SydExecMap>, Errno> {
if *crate::config::HAVE_PROCMAP_QUERY {
return procmap_query_executables(pid);
}
let maps = proc_maps(pid)?;
let mut set = SydIndexSet::default();
for map in maps {
if let MMapPath::Path(path) = map.0.pathname {
if map.0.perms.contains(MMPermissions::EXECUTE) {
let exe = SydExecMap {
path: path.into(),
inode: map.0.inode,
dev_major: map.0.dev.0,
dev_minor: map.0.dev.1,
};
set.insert(exe);
}
}
}
if set.is_empty() {
return Err(Errno::ENOEXEC);
}
Ok(set.into_iter().collect())
}
pub fn procmap_query_executables(pid: Pid) -> Result<Vec<SydExecMap>, Errno> {
let maps = proc_maps_open(pid)?;
let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA
| ProcmapQueryFlags::FILE_BACKED_VMA
| ProcmapQueryFlags::VMA_EXECUTABLE;
let mut path = [0u8; PATH_MAX];
let mut addr = 0u64;
let mut set = SydIndexSet::default();
loop {
match procmap_query(&maps, flags, addr, Some(&mut path), None) {
Ok(q) => {
let name_len = q.vma_name_size as usize;
if name_len == 0 {
addr = q.vma_end;
continue;
}
let path = CStr::from_bytes_with_nul(&path[..name_len])
.map(|cstr| cstr.to_bytes())
.map(XPathBuf::from)
.map_err(|_| Errno::EINVAL)?;
#[expect(clippy::cast_possible_wrap)]
let exe = SydExecMap {
path,
inode: q.inode,
dev_major: q.dev_major as i32,
dev_minor: q.dev_minor as i32,
};
set.insert(exe);
addr = q.vma_end;
}
Err(Errno::ENOENT) => break,
Err(errno) => return Err(errno),
}
}
if set.is_empty() {
return Err(Errno::ENOEXEC);
}
Ok(set.into_iter().collect())
}
pub struct Vma {
ptr: NonNull<libc::c_void>,
len: NonZeroUsize,
flags: ProcmapQueryFlags,
name: [u8; PATH_MAX],
}
impl Vma {
fn new(
ptr: NonNull<libc::c_void>,
len: NonZeroUsize,
flags: ProcmapQueryFlags,
name: [u8; PATH_MAX],
) -> Self {
Self {
ptr,
len,
flags,
name,
}
}
pub fn addr(&self) -> usize {
self.ptr.as_ptr() as usize
}
pub fn as_ptr(&self) -> NonNull<libc::c_void> {
self.ptr
}
pub fn len(&self) -> NonZeroUsize {
self.len
}
pub fn flags(&self) -> ProcmapQueryFlags {
self.flags
}
pub fn name(&self) -> &XPath {
XPath::from_bytes(self.name_bytes())
}
pub fn name_bytes(&self) -> &[u8] {
let len = memchr(0, &self.name).unwrap_or(PATH_MAX);
&self.name[..len]
}
}
impl TryFrom<SydMemoryMap> for Vma {
type Error = Errno;
fn try_from(map: SydMemoryMap) -> Result<Self, Self::Error> {
let map = map.0;
let (start, end) = (map.address.0, map.address.1);
let ptr = NonNull::new(start as *mut _).ok_or(Errno::EINVAL)?;
let len = end
.checked_sub(start)
.ok_or(Errno::EINVAL)
.map(usize::try_from)?
.or(Err(Errno::EINVAL))
.map(NonZeroUsize::new)?
.ok_or(Errno::EINVAL)?;
let mut name = [0u8; PATH_MAX];
match &map.pathname {
MMapPath::Path(path) => {
let bytes = path.as_os_str().as_bytes();
let len = bytes.len().min(PATH_MAX);
name[..len].copy_from_slice(&bytes[..len]);
}
MMapPath::Heap => {
name[..7].copy_from_slice(b"[heap]\0");
}
MMapPath::Stack => {
name[..8].copy_from_slice(b"[stack]\0");
}
MMapPath::TStack(tid) => {
use std::io::Write;
let _ = write!(&mut name[..], "[stack:{tid}]\0");
}
MMapPath::Vdso => {
name[..7].copy_from_slice(b"[vdso]\0");
}
MMapPath::Vvar => {
name[..7].copy_from_slice(b"[vvar]\0");
}
MMapPath::Vsyscall => {
name[..11].copy_from_slice(b"[vsyscall]\0");
}
MMapPath::Rollup | MMapPath::Vsys(_) | MMapPath::Other(_) | MMapPath::Anonymous => {}
}
Ok(Self::new(ptr, len, map.perms.into(), name))
}
}
impl TryFrom<(ProcmapQuery, [u8; PATH_MAX])> for Vma {
type Error = Errno;
fn try_from((q, name): (ProcmapQuery, [u8; PATH_MAX])) -> Result<Self, Self::Error> {
let ptr = NonNull::new(q.vma_start as *mut _).ok_or(Errno::EINVAL)?;
let len = q
.vma_end
.checked_sub(q.vma_start)
.ok_or(Errno::EINVAL)
.map(usize::try_from)?
.or(Err(Errno::EINVAL))
.map(NonZeroUsize::new)?
.ok_or(Errno::EINVAL)?;
let flags = ProcmapQueryFlags::from_bits_truncate(q.vma_flags);
Ok(Self::new(ptr, len, flags, name))
}
}
impl fmt::Display for Vma {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let start = self.addr();
let end = start.saturating_add(self.len().get());
let flags = self.flags();
let name = self.name();
write!(f, "{start:x}-{end:x} {flags} {name}")
}
}
impl Serialize for Vma {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let start = self.addr();
let end = start.saturating_add(self.len().get());
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("addr", &[start, end])?;
map.serialize_entry("perm", &self.flags())?;
map.serialize_entry("name", &self.name())?;
map.end()
}
}
pub fn proc_get_vma(pid: Pid, addr: u64) -> Result<Vma, Errno> {
if *HAVE_PROCMAP_QUERY {
return procmap_query_get_vma(pid, addr);
}
let maps = proc_maps(pid)?;
for map in maps {
let (start, end) = (map.0.address.0, map.0.address.1);
if (start..end).contains(&addr) {
return Vma::try_from(map);
}
}
Err(Errno::ENOENT)
}
fn procmap_query_get_vma(pid: Pid, addr: u64) -> Result<Vma, Errno> {
let maps = proc_maps_open(pid)?;
let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA;
let mut name = [0u8; PATH_MAX];
let q = procmap_query(&maps, flags, addr, Some(&mut name), None)?;
Vma::try_from((q, name))
}
pub fn proc_ip_in_sigtramp(pid: Pid, ip: u64) -> bool {
if *HAVE_PROCMAP_QUERY {
procmap_query_in_sigtramp(pid, ip)
} else {
proc_maps_in_sigtramp(pid, ip)
}
}
fn procmap_query_in_sigtramp(pid: Pid, ip: u64) -> bool {
let maps = match proc_maps_open(pid) {
Ok(maps) => maps,
Err(_) => return false,
};
let mut name = [0u8; 10];
let flags = ProcmapQueryFlags::VMA_EXECUTABLE;
match procmap_query(&maps, flags, ip, Some(&mut name), None) {
Ok(q) => {
let n = q.vma_name_size as usize;
(n == 7 && &name[..7] == b"[vdso]\0") || (n == 10 && &name[..10] == b"[sigpage]\0")
}
Err(_) => false,
}
}
fn proc_maps_in_sigtramp(pid: Pid, ip: u64) -> bool {
proc_maps(pid).is_ok_and(|maps| {
maps.iter().any(|map| {
let in_region = matches!(map.0.pathname, MMapPath::Vdso)
|| matches!(&map.0.pathname, MMapPath::Other(name) if name == "sigpage");
in_region && {
let (start, end) = map.0.address;
ip >= start && ip < end
}
})
})
}
pub fn proc_find_vma(pid: Pid, flags: ProcmapQueryFlags) -> Result<Vec<Vma>, Errno> {
if *HAVE_PROCMAP_QUERY {
return procmap_query_find_vma(pid, flags);
}
let maps = proc_maps(pid)?;
let mut vmas = Vec::new();
for map in maps {
let perms = ProcmapQueryFlags::from(map.0.perms);
if flags.contains(ProcmapQueryFlags::VMA_READABLE)
&& !perms.contains(ProcmapQueryFlags::VMA_READABLE)
{
continue;
}
if flags.contains(ProcmapQueryFlags::VMA_WRITABLE)
&& !perms.contains(ProcmapQueryFlags::VMA_WRITABLE)
{
continue;
}
if flags.contains(ProcmapQueryFlags::VMA_EXECUTABLE)
&& !perms.contains(ProcmapQueryFlags::VMA_EXECUTABLE)
{
continue;
}
if flags.contains(ProcmapQueryFlags::VMA_SHARED)
&& !perms.contains(ProcmapQueryFlags::VMA_SHARED)
{
continue;
}
if flags.contains(ProcmapQueryFlags::FILE_BACKED_VMA)
&& !matches!(map.0.pathname, MMapPath::Path(_))
{
continue;
}
vmas.push(Vma::try_from(map)?);
}
Ok(vmas)
}
pub fn procmap_query_find_vma(pid: Pid, flags: ProcmapQueryFlags) -> Result<Vec<Vma>, Errno> {
let maps = proc_maps_open(pid)?;
let query_flags = flags | ProcmapQueryFlags::COVERING_OR_NEXT_VMA;
let mut vmas = Vec::new();
let mut addr = 0u64;
loop {
let mut name = [0u8; PATH_MAX];
match procmap_query(&maps, query_flags, addr, Some(&mut name), None) {
Ok(q) => {
vmas.push(Vma::try_from((q, name))?);
addr = q.vma_end;
}
Err(Errno::ENOENT) => break,
Err(errno) => return Err(errno),
}
}
Ok(vmas)
}
pub fn proc_environ(pid: Pid) -> Result<SydHashMap<OsString, OsString>, Errno> {
proc_environ_read(proc_environ_open(pid)?)
}
pub fn proc_environ_open(pid: Pid) -> Result<SafeOwnedFd, Errno> {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/environ".len()).or(Err(Errno::ENOMEM))?;
path.push(b"environ");
safe_open_proc(&path).map_err(proc_errno)
}
pub fn proc_environ_read(fd: SafeOwnedFd) -> Result<SydHashMap<OsString, OsString>, Errno> {
let buf = read_all(fd)?;
let mut map = SydHashMap::default();
for slice in buf.split(|b| *b == 0) {
let mut split = slice.splitn(2, |b| *b == b'=');
if let (Some(k), Some(v)) = (split.next(), split.next()) {
map.insert(
OsStr::from_bytes(k).to_os_string(),
OsStr::from_bytes(v).to_os_string(),
);
};
}
Ok(map)
}
pub fn proc_namespaces(pid: Pid) -> Result<Namespaces, Errno> {
let mut ns = XPathBuf::from("/proc");
ns.push_pid(pid);
ns.push(b"ns");
#[expect(clippy::disallowed_methods)]
let dirfd = nix::fcntl::openat(
AT_BADFD,
&ns,
OFLAG_PROC | OFlag::O_DIRECTORY,
Mode::empty(),
)
.map_err(proc_errno)?;
#[expect(clippy::disallowed_types)]
let mut namespaces = std::collections::HashMap::new();
let mut seen_dot = false;
let mut seen_dotdot = false;
loop {
let mut entries = match getdents64(&dirfd, DIRENT_BUF_SIZE) {
Ok(iter) => iter,
Err(Errno::ECANCELED) => break, Err(errno) => return Err(errno),
};
for entry in &mut entries {
if !seen_dot && entry.is_dot() {
seen_dot = true;
continue;
}
if !seen_dotdot && entry.is_dotdot() {
seen_dotdot = true;
continue;
}
let (ino, dev) = statx(&dirfd, entry.as_xpath(), 0, STATX_INO).map(|stx| {
let ino = stx.stx_ino;
let dev = (u64::from(stx.stx_dev_major) << 32) | u64::from(stx.stx_dev_minor);
(ino, dev)
})?;
let ns_type = OsStr::from_bytes(entry.name_bytes()).to_os_string();
let mut ns_path = XPathBuf::from("/proc");
ns_path.push_pid(pid);
ns_path.push(b"ns");
ns_path.push(entry.name_bytes());
let ns = Namespace {
ns_type: ns_type.clone(),
path: ns_path.to_path_buf(),
identifier: ino,
device_id: dev,
};
if namespaces.insert(ns_type, ns).is_some() {
return Err(Errno::EEXIST);
}
}
}
Ok(Namespaces(namespaces))
}
pub fn proc_crypto() -> Result<CryptoTable, Errno> {
proc_crypto_read(proc_crypto_open()?)
}
pub fn proc_crypto_open() -> Result<SafeOwnedFd, Errno> {
safe_open_proc("crypto")
}
pub fn proc_crypto_read(fd: SafeOwnedFd) -> Result<CryptoTable, Errno> {
CryptoTable::from_buf_read(BufReader::new(fd))
.map_err(|err| proc_error_to_errno(&err).unwrap_or(Errno::EPERM))
}
pub fn proc_unix_inodes(pid: Pid) -> Result<SydHashSet<u64>, Errno> {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/net/unix".len())
.or(Err(Errno::ENOMEM))?;
path.push(b"net");
path.push(b"unix");
let file = safe_open_proc(&path).map(BufReader::new)?;
Ok(UnixNetEntries::from_buf_read(file)
.map(|e| e.0)
.map_err(|err| proc_error_to_errno(&err).unwrap_or(Errno::EPERM))?
.into_iter()
.filter(|entry| {
entry
.path
.as_ref()
.map(|p| p.as_os_str().as_bytes().first() != Some(&b'@'))
.unwrap_or(false)
})
.map(|entry| entry.inode)
.collect())
}
pub fn proc_set_at_secure(pid: Pid, elf_type: ElfType, deny_vdso: bool) -> Result<(), Errno> {
let mut proc_mem = if Sandbox::use_proc_pid_mem() {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/mem".len()).or(Err(Errno::ENOMEM))?;
path.push(b"mem");
let mut flags = OFLAG_PROC;
flags.remove(OFlag::O_RDONLY);
flags.insert(OFlag::O_RDWR);
Some(safe_open(PROC_FILE(), &path, flags, RFLAG_PROC).map_err(proc_errno)?)
} else {
None
};
fn proc_read(
proc_mem: Option<&mut SafeOwnedFd>,
pid: Pid,
buf: &mut [u8],
base_addr: usize,
) -> Result<usize, Errno> {
match proc_mem {
Some(fd) => {
fd.seek(SeekFrom::Start(base_addr as u64))
.map_err(|err| err2no(&err))?;
read_buf(fd, buf)
}
None => {
let local_len = buf.len();
let mut local_iov = [IoSliceMut::new(buf)];
let remote_iov = [RemoteIoVec {
base: base_addr,
len: local_len,
}];
process_vm_readv(pid, &mut local_iov, &remote_iov)
}
}
}
fn proc_write(
proc_mem: Option<&mut SafeOwnedFd>,
pid: Pid,
buf: &[u8],
base_addr: usize,
) -> Result<usize, Errno> {
match proc_mem {
Some(fd) => {
fd.seek(SeekFrom::Start(base_addr as u64))
.map_err(|err| err2no(&err))?;
let mut nwrite = 0;
while nwrite < buf.len() {
match retry_on_eintr(|| write(&*fd, &buf[nwrite..]))? {
0 => break,
n => nwrite = nwrite.checked_add(n).ok_or(Errno::EOVERFLOW)?,
}
}
Ok(nwrite)
}
None => {
let local_len = buf.len();
let local_iov = [IoSlice::new(buf)];
let remote_iov = [RemoteIoVec {
base: base_addr,
len: local_len,
}];
process_vm_writev(pid, &local_iov, &remote_iov)
}
}
}
let sp = proc_stack_start(pid)?;
let sp = usize::try_from(sp).or(Err(Errno::EFAULT))?;
let (sizeof_ptr, sizeof_ptr2) = match elf_type {
ElfType::Elf32 => (4, 8),
ElfType::Elf64 => (8, 16),
};
let mut offset = 0usize;
let mut buf = [0u8; 8]; let bytes_read = proc_read(proc_mem.as_mut(), pid, &mut buf[..sizeof_ptr], sp)?;
if bytes_read != sizeof_ptr {
return Err(Errno::EIO);
}
let argc = read_usize_from_ne_bytes(&buf[..sizeof_ptr], sizeof_ptr)?;
if argc == 0 {
return Err(Errno::EINVAL);
}
offset = offset.checked_add(sizeof_ptr).ok_or(Errno::EINVAL)?;
let argv_size = argc
.checked_add(1)
.ok_or(Errno::EINVAL)?
.checked_mul(sizeof_ptr)
.ok_or(Errno::EINVAL)?;
offset = offset.checked_add(argv_size).ok_or(Errno::EINVAL)?;
loop {
let mut envp_buf = [0u8; 8];
let bytes_read = proc_read(
proc_mem.as_mut(),
pid,
&mut envp_buf[..sizeof_ptr],
sp.checked_add(offset).ok_or(Errno::EINVAL)?,
)?;
if bytes_read != sizeof_ptr {
return Err(Errno::EIO);
}
let envp_ptr = read_usize_from_ne_bytes(&envp_buf[..sizeof_ptr], sizeof_ptr)?;
offset = offset.checked_add(sizeof_ptr).ok_or(Errno::EINVAL)?;
if envp_ptr == 0 {
break;
}
}
const READ_SIZE: usize = 512; let mut buf = [0u8; READ_SIZE];
let sp = sp.checked_add(offset).ok_or(Errno::EINVAL)?;
let bytes_read = proc_read(proc_mem.as_mut(), pid, &mut buf, sp)?;
if bytes_read == 0 {
return Err(Errno::EIO);
}
#[expect(clippy::cast_possible_truncation)]
const AT_NULL: usize = libc::AT_NULL as usize;
#[expect(clippy::cast_possible_truncation)]
const AT_UID: usize = libc::AT_UID as usize;
#[expect(clippy::cast_possible_truncation)]
const AT_EUID: usize = libc::AT_EUID as usize;
#[expect(clippy::cast_possible_truncation)]
const AT_GID: usize = libc::AT_GID as usize;
#[expect(clippy::cast_possible_truncation)]
const AT_EGID: usize = libc::AT_EGID as usize;
#[expect(clippy::cast_possible_truncation)]
const AT_SECURE: usize = libc::AT_SECURE as usize;
const AT_REQKEY: &[usize] = &[AT_UID, AT_EUID, AT_GID, AT_EGID, AT_SECURE];
let mut required_index = 0;
const AT_SYSINFO: usize = 32;
const AT_SYSINFO_EHDR: usize = 33;
let mut at_uid_val = None;
let mut at_euid_val = None;
let mut at_gid_val = None;
let mut at_egid_val = None;
offset = 0;
loop {
let key_end = offset.checked_add(sizeof_ptr).ok_or(Errno::EINVAL)?;
let val_end = key_end.checked_add(sizeof_ptr).ok_or(Errno::EINVAL)?;
if val_end > bytes_read {
break;
}
let key = read_usize_from_ne_bytes(&buf[offset..key_end], sizeof_ptr)?;
let val = read_usize_from_ne_bytes(&buf[key_end..val_end], sizeof_ptr)?;
if key == AT_REQKEY[required_index] {
match key {
AT_UID => at_uid_val = Some(val),
AT_EUID => at_euid_val = Some(val),
AT_GID => at_gid_val = Some(val),
AT_EGID => at_egid_val = Some(val),
_ => {}
}
required_index = required_index.checked_add(1).ok_or(Errno::EINVAL)?;
if required_index >= AT_REQKEY.len() {
let uid = Uid::current().as_raw() as usize;
let euid = Uid::effective().as_raw() as usize;
let gid = Gid::current().as_raw() as usize;
let egid = Gid::effective().as_raw() as usize;
if at_uid_val != Some(uid)
|| at_euid_val != Some(euid)
|| at_gid_val != Some(gid)
|| at_egid_val != Some(egid)
{
return Err(Errno::EACCES);
}
if val != 0 {
return Ok(());
}
let val = usize_to_ne_bytes(1, sizeof_ptr);
buf[key_end..val_end].copy_from_slice(&val);
if proc_write(
proc_mem.as_mut(),
pid,
&buf[key_end..val_end],
sp.checked_add(key_end).ok_or(Errno::EINVAL)?,
)? != sizeof_ptr
{
return Err(Errno::EIO);
}
return Ok(());
}
} else if required_index > 0 {
return Err(Errno::EACCES);
} else if deny_vdso && (key == AT_SYSINFO || key == AT_SYSINFO_EHDR) {
let key = usize_to_ne_bytes(AT_SECURE, sizeof_ptr);
let val = usize_to_ne_bytes(1, sizeof_ptr);
buf[offset..key_end].copy_from_slice(&key);
buf[key_end..val_end].copy_from_slice(&val);
if proc_write(
proc_mem.as_mut(),
pid,
&buf[offset..val_end],
sp.checked_add(offset).ok_or(Errno::EINVAL)?,
)? != sizeof_ptr2
{
return Err(Errno::EIO);
}
} else if key == AT_NULL {
break;
}
offset = offset.checked_add(sizeof_ptr2).ok_or(Errno::EINVAL)?;
}
Err(Errno::ENOENT)
}
#[expect(clippy::type_complexity)]
pub fn proc_pid_fd(pid: Option<Pid>) -> Result<Vec<(RawFd, XPathBuf)>, Errno> {
let pid = pid.unwrap_or_else(getpid);
let mut dir = XPathBuf::from_pid(pid)?;
dir.try_reserve(b"/fd".len()).or(Err(Errno::ENOMEM))?;
dir.push(b"fd");
let dir = safe_open(
PROC_FILE(),
&dir,
OFLAG_PROC | OFlag::O_DIRECTORY,
RFLAG_PROC,
)?;
let mut res = vec![];
let mut seen_dot = false;
let mut seen_dotdot = false;
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 !seen_dot && entry.is_dot() {
seen_dot = true;
continue;
}
if !seen_dotdot && entry.is_dotdot() {
seen_dotdot = true;
continue;
}
let entry = XPath::from_bytes(entry.name_bytes());
let fd = parse_fd(entry)?;
let target = readlinkat(&dir, entry)?;
res.push((fd, target));
}
}
Ok(res)
}
pub fn proc_max_open_files(pid: Pid) -> Result<LimitValue, Errno> {
let mut path = XPathBuf::from_pid(pid)?;
path.try_reserve(b"/limits".len()).or(Err(Errno::ENOMEM))?;
path.push(b"limits");
let file = safe_open_proc(&path)?;
let mut buf = [0; 1792]; map_result(parse_max_open_files(read_to_end(file, &mut buf)?))
}
pub fn proc_rand_fd(pid: Pid) -> Result<RawFd, Errno> {
const OFILE_MAX: u64 = 0x10000;
let range_start = 7u64;
let range_end = match proc_max_open_files(pid)? {
LimitValue::Unlimited => OFILE_MAX,
LimitValue::Value(val) => val.saturating_sub(1).min(OFILE_MAX),
};
if range_end <= range_start {
return Err(Errno::EMFILE);
}
let range = range_start..=range_end;
for _ in range.clone() {
#[expect(clippy::cast_possible_truncation)]
let fd_rand = randint(range.clone())? as RawFd;
return match is_open_fd(pid, fd_rand) {
Ok(true) => continue,
Ok(false) => Ok(fd_rand),
Err(errno) => Err(errno),
};
}
Err(Errno::EBADF)
}
pub fn log_proc_pid_fd(pid: Option<Pid>) -> Result<(), Errno> {
let fds = proc_pid_fd(pid)?;
let pid = pid.unwrap_or_else(getpid);
crate::error!("ctx": "log_proc_self_fd",
"msg": format!("List of /proc/{pid}/fd"),
"fds": fds, "pid": pid.as_raw());
Ok(())
}
pub static KERNEL_TAINT_STRINGS: [&str; 20] = [
"P (Proprietary module was loaded)",
"F (Module was force loaded)",
"S (Kernel running on an out of specification system)",
"R (Module was force unloaded)",
"M (Processor reported a Machine Check Exception)",
"B (Bad page referenced or unexpected page flags)",
"U (Taint requested by userspace application)",
"D (Kernel died recently: OOPS/BUG)",
"A (ACPI table overridden by user)",
"W (Kernel issued warning)",
"C (Staging driver was loaded)",
"I (Workaround for bug in platform firmware applied)",
"O (Externally-built \"out-of-tree\" module was loaded)",
"E (Unsigned module was loaded)",
"L (Soft lockup occurred)",
"K (Kernel has been live patched)",
"X (Auxiliary taint, used by distros)",
"T (Kernel built with struct randomization plugin)",
"N (An in-kernel test has been run)",
"J (Userspace used a mutating debug operation in fwctl)",
];
bitflags! {
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct KernelTaintFlags: u64 {
const PROPRIETARY_MODULE = 1 << 0; const MODULE_FORCE_LOADED = 1 << 1; const OUT_OF_SPEC_SYSTEM = 1 << 2; const MODULE_FORCE_UNLOADED = 1 << 3; const MACHINE_CHECK_EXCEPTION = 1 << 4; const BAD_PAGE_REFERENCE = 1 << 5; const USER_REQUEST = 1 << 6; const OOPS_OR_BUG = 1 << 7; const ACPI_TABLE_OVERRIDDEN = 1 << 8; const WARNING_ISSUED = 1 << 9; const STAGING_DRIVER_LOADED = 1 << 10; const WORKAROUND_FW_BUG = 1 << 11; const OUT_OF_TREE_MODULE = 1 << 12; const UNSIGNED_MODULE_LOADED = 1 << 13; const SOFT_LOCKUP_OCCURRED = 1 << 14; const LIVE_PATCHED = 1 << 15; const AUXILIARY = 1 << 16; const BUILT_WITH_RANDOMIZATION = 1 << 17; const IN_KERNEL_TEST_RUN = 1 << 18; const FWCTL_DEBUG_WRITE_USED = 1 << 19; }
}
impl KernelTaintFlags {
pub fn reasons(self) -> impl Iterator<Item = &'static str> + 'static {
(0..KERNEL_TAINT_STRINGS.len())
.filter(move |bit| self.bits() & (1u64 << bit) != 0)
.map(|bit| KERNEL_TAINT_STRINGS[bit])
}
pub fn is_clean(self) -> bool {
self.is_empty()
}
}
impl fmt::Display for KernelTaintFlags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_clean() {
write!(f, "Kernel is not tainted (0).")
} else {
write!(
f,
"Kernel is tainted: {}.",
self.reasons().collect::<Vec<_>>().join(", ")
)
}
}
}
pub fn proc_kernel_taint() -> Result<KernelTaintFlags, Errno> {
let fd = proc_open(None).and_then(|fd| {
safe_open(
fd,
c"sys/kernel/tainted",
OFlag::O_RDONLY | OFlag::O_NOCTTY,
ResolveFlag::RESOLVE_NO_XDEV,
)
})?;
if !is_empty_file(&fd).unwrap_or(false) {
return Err(Errno::EBADFD);
}
let mut data = [0u8; 25];
let nread = read_buf(fd, &mut data)?;
let val = btoi::<u64>(data[..nread].trim_ascii()).or(Err(Errno::EINVAL))?;
Ok(KernelTaintFlags::from_bits_retain(val))
}
pub fn proc_pid_max<Fd: AsFd>(fd_proc: Fd, pid_max: u64) -> Result<(), Errno> {
let mut buf = itoa::Buffer::new();
let pid_max = buf.format(pid_max).as_bytes();
proc_write(&fd_proc, b"sys/kernel/pid/max", pid_max, false )
}
pub fn proc_map_user<Fd: AsFd>(
fd_proc: Fd,
uid: Uid,
gid: Gid,
map_root: bool,
) -> Result<(), Errno> {
let uid_map = {
let mut buf = Vec::new();
write!(
&mut buf,
"{} {uid} 1",
if map_root { 0 } else { uid.as_raw() }
)
.map_err(|err| err2no(&err))?;
buf
};
let gid_map = {
let mut buf = Vec::new();
write!(
&mut buf,
"{} {gid} 1",
if map_root { 0 } else { gid.as_raw() }
)
.map_err(|err| err2no(&err))?;
buf
};
proc_write(&fd_proc, b"setgroups", b"deny", true )?;
proc_write(&fd_proc, b"gid_map", &gid_map, true )?;
proc_write(&fd_proc, b"uid_map", &uid_map, true )?;
Ok(())
}
pub fn proc_set_time<Fd: AsFd>(fd_proc: Fd, boffset: i64, moffset: i64) -> Result<(), Errno> {
let mut buf = itoa::Buffer::new();
let mut data = "monotonic ".to_string();
data.push_str(buf.format(moffset));
data.push_str(" 0\nboottime ");
data.push_str(buf.format(boffset));
data.push_str(" 0\n");
proc_write(
fd_proc,
b"timens_offsets",
data.as_bytes(),
true,
)
}
fn proc_write<Fd: AsFd>(fd_proc: Fd, name: &[u8], data: &[u8], pid: bool) -> Result<(), Errno> {
let pfd = if pid {
let mut pfd = XPathBuf::from_pid(Pid::this())?;
pfd.try_reserve(name.len().checked_add(1).ok_or(Errno::EOVERFLOW)?)
.or(Err(Errno::ENOMEM))?;
pfd.push(name);
Cow::Owned(pfd)
} else {
Cow::Borrowed(XPath::from_bytes(name))
};
let fd = safe_open(
fd_proc,
pfd.as_ref(),
OFlag::O_WRONLY,
ResolveFlag::RESOLVE_NO_XDEV,
)?;
if !is_empty_file(&fd).unwrap_or(false) {
return Err(Errno::EBADFD);
}
write_all(&fd, data)
}
pub fn proc_open(proc_mountpoint: Option<&XPath>) -> Result<SafeOwnedFd, Errno> {
let mnt = proc_mountpoint.unwrap_or(XPath::from_bytes(b"/proc"));
let how = OpenHow::new()
.flags(OFlag::O_PATH | OFlag::O_DIRECTORY | OFlag::O_NOFOLLOW | OFlag::O_CLOEXEC)
.resolve(ResolveFlag::RESOLVE_NO_MAGICLINKS | ResolveFlag::RESOLVE_NO_SYMLINKS);
#[expect(clippy::disallowed_methods)]
let fd_proc = openat2(AT_BADFD, mnt, how)?;
if !is_proc(&fd_proc).unwrap_or(false) {
return Err(Errno::ENODEV);
}
Ok(fd_proc)
}
#[cfg(feature = "kcov")]
pub(crate) fn proc_kcov_read_id(tid: Pid, fd: RawFd) -> Result<crate::kcov::KcovId, Errno> {
if tid.as_raw() < 1 {
return Err(Errno::EINVAL);
}
if fd < 0 {
return Err(Errno::EBADF);
}
let pfd = XPathBuf::from_pid_fd(tid, fd)?;
statx(PROC_FILE(), &pfd, 0, STATX_INO)
.map(|statx| statx.stx_ino)
.map(crate::kcov::KcovId::new)
}
#[cfg(test)]
mod tests {
use std::{fs::File, os::unix::fs::PermissionsExt};
use nix::{
sched::CloneFlags,
sys::{
signal::{
raise, sigaction, SaFlags, SigAction, SigHandler, SigSet, SigmaskHow, Signal,
},
stat::umask,
wait::{Id, WaitPidFlag},
},
};
use super::*;
use crate::{
compat::{waitid, WaitStatus},
fd::{fdclone, pidfd_open},
fs::tgkill,
};
fn setup() -> bool {
let _ = crate::log::log_init_simple(crate::syslog::LogLevel::Warn);
if let Err(errno) = crate::fd::open_static_proc() {
eprintln!("Failed to initialize proc: {errno}!");
return false;
}
true
}
#[expect(unsafe_code)]
fn sig_catch(sig: Signal) -> Result<SigAction, Errno> {
extern "C" fn noop_handler(_: libc::c_int) {}
let sa = SigAction::new(
SigHandler::Handler(noop_handler),
SaFlags::empty(),
SigSet::empty(),
);
unsafe { sigaction(sig, &sa) }
}
#[expect(unsafe_code)]
fn sig_default(sig: Signal) -> Result<SigAction, Errno> {
let sa = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty());
unsafe { sigaction(sig, &sa) }
}
#[expect(unsafe_code)]
fn sig_restore(sig: Signal, old: &SigAction) -> Result<(), Errno> {
unsafe { sigaction(sig, old) }?;
Ok(())
}
fn sig_block(sig: Signal) -> Result<SigSet, Errno> {
let mut set = SigSet::empty();
set.add(sig);
set.thread_swap_mask(SigmaskHow::SIG_BLOCK)
}
fn sig_setmask(set: &SigSet) -> Result<(), Errno> {
set.thread_set_mask()
}
#[test]
fn test_procmap_query_flags_1() {
let flags = ProcmapQueryFlags::VMA_READABLE
| ProcmapQueryFlags::VMA_WRITABLE
| ProcmapQueryFlags::VMA_EXECUTABLE;
assert_eq!(format!("{flags}"), "rwxp");
}
#[test]
fn test_procmap_query_flags_2() {
let flags = ProcmapQueryFlags::empty();
assert_eq!(format!("{flags}"), "---p");
}
#[test]
fn test_procmap_query_flags_3() {
let flags = ProcmapQueryFlags::VMA_READABLE | ProcmapQueryFlags::VMA_SHARED;
assert_eq!(format!("{flags}"), "r--s");
}
#[test]
fn test_procmap_query_flags_4() {
let flags = ProcmapQueryFlags::VMA_READABLE
| ProcmapQueryFlags::VMA_WRITABLE
| ProcmapQueryFlags::VMA_EXECUTABLE
| ProcmapQueryFlags::VMA_SHARED;
assert_eq!(format!("{flags}"), "rwxs");
}
#[test]
fn test_procmap_query_flags_5() {
let perms = MMPermissions::READ | MMPermissions::WRITE;
let flags = ProcmapQueryFlags::from(perms);
assert!(flags.contains(ProcmapQueryFlags::VMA_READABLE));
assert!(flags.contains(ProcmapQueryFlags::VMA_WRITABLE));
assert!(!flags.contains(ProcmapQueryFlags::VMA_EXECUTABLE));
assert!(!flags.contains(ProcmapQueryFlags::VMA_SHARED));
}
#[test]
fn test_procmap_query_flags_6() {
let perms = MMPermissions::READ | MMPermissions::EXECUTE | MMPermissions::SHARED;
let flags = ProcmapQueryFlags::from(perms);
assert!(flags.contains(ProcmapQueryFlags::VMA_READABLE));
assert!(flags.contains(ProcmapQueryFlags::VMA_EXECUTABLE));
assert!(flags.contains(ProcmapQueryFlags::VMA_SHARED));
}
#[test]
fn test_procmap_query_flags_7() {
let perms = MMPermissions::empty();
let flags = ProcmapQueryFlags::from(perms);
assert!(flags.is_empty());
}
#[test]
fn test_procmap_query_flags_8() {
let flags = ProcmapQueryFlags::VMA_READABLE | ProcmapQueryFlags::VMA_WRITABLE;
let json = serde_json::to_string(&flags).unwrap();
assert_eq!(json, "\"rw-p\"");
}
#[test]
fn test_proc_errno_1() {
assert_eq!(proc_errno(Errno::ENOENT), Errno::ESRCH);
}
#[test]
fn test_proc_errno_2() {
assert_eq!(proc_errno(Errno::EPERM), Errno::EPERM);
}
#[test]
fn test_proc_errno_3() {
assert_eq!(proc_errno(Errno::EACCES), Errno::EACCES);
}
#[test]
fn test_read_usize_from_ne_bytes_1() {
let val: u32 = 0x12345678;
let bytes = val.to_ne_bytes();
let result = read_usize_from_ne_bytes(&bytes, 4).unwrap();
assert_eq!(result, 0x12345678);
}
#[test]
fn test_read_usize_from_ne_bytes_2() {
let val: u64 = 0x0000_0001_0000_0000;
let bytes = val.to_ne_bytes();
let result = read_usize_from_ne_bytes(&bytes, 8).unwrap();
assert_eq!(result as u64, 0x0000_0001_0000_0000);
}
#[test]
fn test_read_usize_from_ne_bytes_3() {
let bytes = [0u8; 3];
assert_eq!(read_usize_from_ne_bytes(&bytes, 4), Err(Errno::EFAULT));
}
#[test]
fn test_read_usize_from_ne_bytes_4() {
let bytes = [0u8; 7];
assert_eq!(read_usize_from_ne_bytes(&bytes, 8), Err(Errno::EFAULT));
}
#[test]
fn test_read_usize_from_ne_bytes_5() {
let bytes = [0u8; 16];
assert_eq!(read_usize_from_ne_bytes(&bytes, 3), Err(Errno::EINVAL));
}
#[test]
fn test_usize_to_ne_bytes_1() {
let bytes = usize_to_ne_bytes(0x42, 4);
assert_eq!(bytes.len(), 4);
assert_eq!(bytes, (0x42u32).to_ne_bytes().to_vec());
}
#[test]
fn test_usize_to_ne_bytes_2() {
let bytes = usize_to_ne_bytes(0x42, 8);
assert_eq!(bytes.len(), 8);
assert_eq!(bytes, (0x42u64).to_ne_bytes().to_vec());
}
#[test]
fn test_usize_to_ne_bytes_3() {
let original: usize = 12345;
let bytes = usize_to_ne_bytes(original, std::mem::size_of::<usize>());
let recovered = read_usize_from_ne_bytes(&bytes, std::mem::size_of::<usize>()).unwrap();
assert_eq!(recovered, original);
}
#[test]
fn test_proc_tgid_1() {
if !setup() {
return;
}
let result = proc_tgid(Pid::from_raw(i32::MAX));
assert!(result.is_err(), "{result:?}");
}
#[test]
fn test_proc_umask_1() {
if !setup() {
return;
}
let result = proc_umask(Pid::from_raw(i32::MAX));
assert!(result.is_err(), "{result:?}");
}
#[test]
fn test_proc_umask_2() {
if !setup() {
return;
}
let umasks = [
Mode::from_bits_truncate(0o0000),
Mode::from_bits_truncate(0o0002),
Mode::from_bits_truncate(0o0022),
Mode::from_bits_truncate(0o0077),
Mode::from_bits_truncate(0o0777),
];
for &my_umask in &umasks {
umask(my_umask);
let result = proc_umask(Pid::this()).unwrap();
assert_eq!(result, my_umask, "{result:o} != {my_umask:o}");
}
umask(Mode::from_bits_truncate(0o0022));
}
macro_rules! skip_if_procmap_query_not_supported {
() => {
if !*crate::config::HAVE_PROCMAP_QUERY {
eprintln!("skipping: PROCMAP_QUERY not supported on this kernel (requires >=6.11)");
return;
}
};
}
#[test]
fn test_procmap_query_1() {
let q = ProcmapQuery::default();
assert_eq!(q.size as usize, size_of::<ProcmapQuery>());
assert_eq!(q.query_flags, 0);
assert_eq!(q.query_addr, 0);
assert_eq!(q.vma_start, 0);
assert_eq!(q.vma_end, 0);
assert_eq!(q.vma_flags, 0);
assert_eq!(q.vma_page_size, 0);
assert_eq!(q.vma_offset, 0);
assert_eq!(q.inode, 0);
assert_eq!(q.dev_major, 0);
assert_eq!(q.dev_minor, 0);
assert_eq!(q.vma_name_size, 0);
assert_eq!(q.build_id_size, 0);
assert_eq!(q.vma_name_addr, 0);
assert_eq!(q.build_id_addr, 0);
}
#[test]
fn test_procmap_query_2() {
skip_if_procmap_query_not_supported!();
let maps = File::open("/proc/self/maps").unwrap();
let q = procmap_query(
&maps,
ProcmapQueryFlags::COVERING_OR_NEXT_VMA | ProcmapQueryFlags::VMA_READABLE,
0,
None,
None,
)
.expect("basic query failed");
assert!(q.vma_start < q.vma_end);
let perms = ProcmapQueryFlags::from_bits_truncate(q.vma_flags);
assert!(perms.contains(ProcmapQueryFlags::VMA_READABLE));
assert_eq!(q.vma_name_addr, 0);
assert_eq!(q.vma_name_size, 0);
}
#[test]
fn test_procmap_query_3() {
skip_if_procmap_query_not_supported!();
let maps = File::open("/proc/self/maps").unwrap();
let mut buf = [0u8; PATH_MAX];
let q = procmap_query(
&maps,
ProcmapQueryFlags::COVERING_OR_NEXT_VMA,
0,
Some(&mut buf),
None,
)
.expect("query with name buffer failed");
assert_eq!(q.vma_name_size as usize <= PATH_MAX, true);
assert_ne!(q.vma_name_addr, 0);
let slice = &buf[..q.vma_name_size as usize];
let cstr = CStr::from_bytes_with_nul(slice).expect("vma name buffer not NUL terminated");
assert!(!cstr.to_bytes().is_empty(), "empty VMA name");
}
#[test]
fn test_procmap_query_4() {
skip_if_procmap_query_not_supported!();
let maps = File::open("/proc/self/maps").unwrap();
let mut name_buf = [0u8; 512];
let mut build_buf = [0u8; 64];
let q = procmap_query(
&maps,
ProcmapQueryFlags::COVERING_OR_NEXT_VMA | ProcmapQueryFlags::FILE_BACKED_VMA,
0,
Some(&mut name_buf),
Some(&mut build_buf),
)
.expect("query with both buffers failed");
assert!(q.build_id_size as usize <= build_buf.len());
let slice = &name_buf[..q.vma_name_size as usize];
let _ = CStr::from_bytes_with_nul(slice).expect("invalid VMA name");
}
#[test]
fn test_procmap_query_5() {
skip_if_procmap_query_not_supported!();
let maps = File::open("/proc/self/maps").unwrap();
let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA | ProcmapQueryFlags::FILE_BACKED_VMA;
let mut addr = 0;
let mut buf = [0u8; PATH_MAX];
let mut seen = 0;
loop {
match procmap_query(&maps, flags, addr, Some(&mut buf), None) {
Ok(q) => {
assert!(q.vma_start < q.vma_end);
seen += 1;
addr = q.vma_end;
}
Err(Errno::ENOENT) => break,
Err(errno) => panic!("unexpected error during iteration: {errno}"),
}
}
assert!(seen > 0, "expected to see at least one VMA!");
}
#[test]
fn test_procmap_query_6() {
skip_if_procmap_query_not_supported!();
let maps = File::open("/proc/self/maps").unwrap();
let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA
| ProcmapQueryFlags::FILE_BACKED_VMA
| ProcmapQueryFlags::VMA_EXECUTABLE;
let mut addr = 0;
let mut buf = [0u8; PATH_MAX];
let mut found_exec = false;
loop {
match procmap_query(&maps, flags, addr, Some(&mut buf), None) {
Ok(q) => {
let perms = ProcmapQueryFlags::from_bits_truncate(q.vma_flags);
assert!(perms.contains(ProcmapQueryFlags::VMA_EXECUTABLE));
found_exec = true;
addr = q.vma_end;
}
Err(Errno::ENOENT) => break,
Err(errno) => panic!("unexpected error: {errno}"),
}
}
assert!(found_exec, "no executable VMAs found!");
}
#[test]
fn test_procmap_query_7() {
skip_if_procmap_query_not_supported!();
let maps = File::open("/proc/self/maps").unwrap();
let mut build_buf = [0u8; 64];
let q = procmap_query(
&maps,
ProcmapQueryFlags::COVERING_OR_NEXT_VMA,
0,
None,
Some(&mut build_buf),
)
.expect("query build-id only failed");
assert_eq!(q.vma_name_addr, 0);
assert_eq!(q.vma_name_size, 0);
assert!(q.build_id_size as usize <= build_buf.len());
if q.build_id_size > 0 {
let _ = &build_buf[..q.build_id_size as usize];
}
}
#[test]
fn test_procmap_query_8() {
skip_if_procmap_query_not_supported!();
let maps = File::open("/proc/self/maps").unwrap();
let q = procmap_query(
&maps,
ProcmapQueryFlags::COVERING_OR_NEXT_VMA,
0,
None,
None,
)
.expect("basic query failed");
assert!(q.vma_page_size > 0);
let vma_len = q.vma_end - q.vma_start;
assert!(q.vma_offset <= vma_len);
}
#[test]
fn test_procmap_query_9() {
skip_if_procmap_query_not_supported!();
let maps = File::open("/proc/self/maps").unwrap();
let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA;
let mut addr = 0;
let mut seen_addrs = Vec::new();
for _ in 0..2 {
let q = procmap_query(&maps, flags, addr, None, None).expect("query iteration failed");
seen_addrs.push(q.vma_start);
addr = q.vma_end;
}
assert_eq!(seen_addrs.len(), 2);
assert!(seen_addrs[0] < seen_addrs[1], "VMAs did not advance!");
}
#[test]
fn test_procmap_query_10() {
skip_if_procmap_query_not_supported!();
let maps = File::open("/proc/self/maps").unwrap();
let mut buf = [0u8; 1];
let err = procmap_query(
&maps,
ProcmapQueryFlags::COVERING_OR_NEXT_VMA | ProcmapQueryFlags::FILE_BACKED_VMA,
0,
Some(&mut buf),
None,
)
.unwrap_err();
assert_eq!(err, Errno::ENAMETOOLONG);
}
#[test]
fn test_procmap_query_11() {
skip_if_procmap_query_not_supported!();
let maps = File::open("/proc/self/maps").unwrap();
let result = procmap_query(
&maps,
ProcmapQueryFlags::COVERING_OR_NEXT_VMA,
u64::MAX,
None,
None,
);
assert_eq!(result.unwrap_err(), Errno::ENOENT);
}
#[test]
fn test_procmap_query_12() {
skip_if_procmap_query_not_supported!();
let maps = File::open("/proc/self/maps").unwrap();
let q = procmap_query(
&maps,
ProcmapQueryFlags::COVERING_OR_NEXT_VMA,
0,
None,
None,
)
.unwrap();
assert_eq!(q.size as usize, size_of::<ProcmapQuery>());
}
#[test]
fn test_procmap_query_13() {
skip_if_procmap_query_not_supported!();
let maps = File::open("/proc/self/maps").unwrap();
let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA
| ProcmapQueryFlags::FILE_BACKED_VMA
| ProcmapQueryFlags::VMA_WRITABLE;
let mut addr = 0;
let mut count = 0;
let mut buf = [0u8; 256];
while let Ok(q) = procmap_query(&maps, flags, addr, Some(&mut buf), None) {
let perms = ProcmapQueryFlags::from_bits_truncate(q.vma_flags);
assert!(perms.contains(ProcmapQueryFlags::VMA_WRITABLE));
count += 1;
addr = q.vma_end;
}
assert!(count > 0, "expected at least one writable VMA");
}
#[test]
fn test_procmap_query_14() {
skip_if_procmap_query_not_supported!();
let maps = File::open("/proc/self/maps").unwrap();
let flags = ProcmapQueryFlags::COVERING_OR_NEXT_VMA | ProcmapQueryFlags::FILE_BACKED_VMA;
let mut buf = [0u8; 512];
let q = procmap_query(&maps, flags, 0, Some(&mut buf), None).unwrap();
assert!(
q.inode != 0,
"expected inode of a file-backed VMA to be non-zero"
);
}
#[test]
fn test_proc_executables_1() {
if !setup() {
return;
}
let list = proc_executables(Pid::this()).expect("expected executables");
assert!(!list.is_empty(), "no executables found for self");
}
#[test]
fn test_proc_executables_2() {
if !setup() {
return;
}
let bins = proc_executables(Pid::this()).unwrap();
let mut seen = SydIndexSet::default();
for bin in &bins {
let path = &bin.path;
assert!(seen.insert(path.clone()), "duplicate path {path}!");
}
let collected: Vec<_> = seen.into_iter().collect();
let returned: Vec<_> = bins.iter().map(|bin| bin.path.clone()).collect();
assert_eq!(collected, returned);
}
#[test]
fn test_proc_executables_3() {
if !setup() {
return;
}
let bins = proc_executables(Pid::this()).unwrap();
for (idx, bin) in bins.into_iter().enumerate() {
let path = bin.path;
let md = std::fs::metadata(&path).expect("path does not exist");
if idx == 0 {
let perms = md.permissions().mode();
assert!(
perms & 0o111 != 0,
"file {path} is not executable (mode {perms:o})",
);
}
}
}
#[test]
fn test_proc_kernel_randomize_va_space() {
matches!(proc_kernel_randomize_va_space(), Ok(0 | 1 | 2));
}
#[test]
fn test_proc_find_vma() {
if !setup() {
return;
}
let pid = Pid::this();
for flags in [
ProcmapQueryFlags::VMA_READABLE,
ProcmapQueryFlags::VMA_EXECUTABLE,
ProcmapQueryFlags::VMA_READABLE | ProcmapQueryFlags::VMA_EXECUTABLE,
] {
let vmas = proc_find_vma(pid, flags).unwrap();
assert!(!vmas.is_empty());
for vma in vmas {
assert!(vma.flags().contains(flags));
}
}
}
#[test]
fn test_proc_pidfd_get_tgid() {
if !setup() {
return;
}
let mypid = getpid();
let pidfd = pidfd_open(mypid, 0).unwrap();
let retpid = proc_pidfd_get_tgid(pidfd).unwrap();
assert_eq!(mypid, retpid);
}
#[test]
fn test_proc_interrupt_1() -> Result<(), Errno> {
if !setup() {
return Ok(());
}
let sig = Signal::SIGUSR1;
let signo = sig as libc::c_int;
let tid = gettid();
let old_sa = sig_catch(sig)?;
let old_mask = sig_block(sig)?;
raise(sig)?;
let status = proc_status(tid)?;
if !status.sig_pending_thread.contains(signo) {
return Err(Errno::EINVAL);
}
if !status.sig_blocked.contains(signo) {
return Err(Errno::EINVAL);
}
if !status.sig_caught.contains(signo) {
return Err(Errno::EINVAL);
}
let sigset = proc_interrupt(tid)?;
if sigset.contains(signo) {
return Err(Errno::EINVAL);
}
sig_setmask(&old_mask)?;
let status2 = proc_status(tid)?;
if status2.sig_pending_thread.contains(signo) {
return Err(Errno::EINVAL);
}
let sigset2 = proc_interrupt(tid)?;
if sigset2.contains(signo) {
return Err(Errno::EINVAL);
}
sig_restore(sig, &old_sa)?;
Ok(())
}
#[test]
fn test_proc_interrupt_2() -> Result<(), Errno> {
if !setup() {
return Ok(());
}
let sig = Signal::SIGUSR2;
let signo = sig as libc::c_int;
let tid = gettid();
let old_sa = sig_default(sig)?;
let old_mask = sig_block(sig)?;
raise(sig)?;
let status = proc_status(tid)?;
if !status.sig_pending_thread.contains(signo) {
return Err(Errno::EINVAL);
}
if !status.sig_blocked.contains(signo) {
return Err(Errno::EINVAL);
}
if status.sig_caught.contains(signo) {
return Err(Errno::EINVAL);
}
let sigset = proc_interrupt(tid)?;
if sigset.contains(signo) {
return Err(Errno::EINVAL);
}
sig_catch(sig)?;
sig_setmask(&old_mask)?;
sig_restore(sig, &old_sa)?;
Ok(())
}
#[test]
#[expect(unsafe_code)]
fn test_proc_interrupt_3() -> Result<(), Errno> {
if !setup() {
return Ok(());
}
let sig = Signal::SIGUSR1;
let signo = sig as libc::c_int;
let (pidfd, _) = fdclone(
|| {
let errno = (|| -> Result<(), Errno> {
sig_catch(sig)?;
sig_block(sig)?;
let pid = getpid();
tgkill(pid, pid, signo)?;
let status = proc_status(getpid())?;
if !status.sig_pending_thread.contains(signo) {
return Err(Errno::EINVAL);
}
if !status.sig_blocked.contains(signo) {
return Err(Errno::EINVAL);
}
if !status.sig_caught.contains(signo) {
return Err(Errno::EINVAL);
}
let sigset = proc_interrupt(getpid())?;
if sigset.contains(signo) {
return Err(Errno::EINVAL);
}
Ok(())
})();
unsafe { libc::_exit(errno.map_or_else(|e| e as i32, |()| 0)) }
},
CloneFlags::empty(),
Some(libc::SIGCHLD),
)?;
match waitid(Id::PIDFd(pidfd.as_fd()), WaitPidFlag::WEXITED)? {
WaitStatus::Exited(_, 0) => Ok(()),
WaitStatus::Exited(_, code) => Err(Errno::from_raw(code)),
_ => Err(Errno::ECHILD),
}
}
#[test]
fn test_parse_stack_pointer_1() {
let data = b"62 0x1 0x7fffffffca30 0x400 0x0 0x0 0x0 0x7fffffffcf50 0x7ffff7e30a2c\n";
assert_eq!(parse_stack_pointer(data).unwrap(), 0x7fffffffcf50);
}
#[test]
fn test_parse_stack_pointer_2() {
let data = b"-1 0x7fffffffd070 0x7ffff7e30a2c\n";
assert_eq!(parse_stack_pointer(data).unwrap(), 0x7fffffffd070);
}
#[test]
fn test_parse_stack_pointer_3() {
let data = b"running\n";
assert_eq!(parse_stack_pointer(data).unwrap_err(), Errno::EBUSY);
}
fn vdso_base() -> Option<u64> {
const AT_SYSINFO_EHDR: u64 = 33;
proc_auxv(Pid::this())
.ok()?
.get(&AT_SYSINFO_EHDR)
.copied()
.filter(|&b| b != 0)
}
#[test]
fn test_proc_ip_in_sigtramp_1() {
if !setup() {
return;
}
let base = if let Some(base) = vdso_base() {
base
} else {
return;
};
assert!(proc_ip_in_sigtramp(Pid::this(), base));
}
#[test]
fn test_proc_ip_in_sigtramp_2() {
if !setup() {
return;
}
let pc = test_proc_ip_in_sigtramp_2 as u64;
assert!(!proc_ip_in_sigtramp(Pid::this(), pc));
}
#[test]
fn test_proc_ip_in_sigtramp_3() {
if !setup() {
return;
}
assert!(!proc_ip_in_sigtramp(Pid::this(), 0));
assert!(!proc_ip_in_sigtramp(Pid::this(), 1));
}
#[test]
fn test_proc_maps_in_sigtramp_1() {
if !setup() {
return;
}
let base = if let Some(base) = vdso_base() {
base
} else {
return;
};
assert!(proc_maps_in_sigtramp(Pid::this(), base));
}
#[test]
fn test_proc_maps_in_sigtramp_2() {
if !setup() {
return;
}
let pc = test_proc_maps_in_sigtramp_2 as u64;
assert!(!proc_maps_in_sigtramp(Pid::this(), pc));
}
#[test]
fn test_proc_maps_in_sigtramp_3() {
if !setup() {
return;
}
if !*HAVE_PROCMAP_QUERY {
return;
}
let base = if let Some(base) = vdso_base() {
base
} else {
return;
};
let fast = procmap_query_in_sigtramp(Pid::this(), base);
let slow = proc_maps_in_sigtramp(Pid::this(), base);
assert_eq!(fast, slow);
let addr = base.saturating_sub(8 * 1024 * 1024);
let fast = procmap_query_in_sigtramp(Pid::this(), addr);
let slow = proc_maps_in_sigtramp(Pid::this(), addr);
assert_eq!(fast, slow);
}
}