cloudfox-coreshift-core 2.13.1

Low-level Linux and Android systems primitives for CoreShift (CloudFox)
Documentation
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/

//! Procfs and process-introspection helpers.
//!
//! These functions provide a small, Linux-oriented view into `/proc` for
//! callers that need process names, command lines, UIDs, or clock-tick
//! information without bringing in a broader process-inspection crate.

use crate::CoreError;
use crate::error::syscall_ret;
use std::ffi::CString;
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::unix::ffi::OsStrExt;
use std::path::Path;

#[inline(always)]
fn errno() -> i32 {
    std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
}

/// A snapshot of process status information from procfs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProcStatus {
    /// Command name of the process.
    pub name: String,
    /// Real UID of the process.
    pub uid: u32,
}

/// Read process status from `/proc/<pid>/status`.
///
/// ### Errors
/// - `EACCES`: Permission denied.
/// - `ENOENT`: The process does not exist.
pub fn read_proc_status(pid: i32) -> Result<ProcStatus, CoreError> {
    read_proc_status_at("/proc", pid)
}

/// Read process status from an explicit procfs root.
///
/// ### Errors
/// - `EACCES`: Permission denied.
/// - `ENOENT`: The process or status file does not exist.
pub fn read_proc_status_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<ProcStatus, CoreError> {
    let path = proc_root.as_ref().join(pid.to_string()).join("status");
    let content = std::fs::read_to_string(path).map_err(|err| io_error(err, "read_proc_status"))?;
    parse_proc_status(&content)
}

/// Read process command line from `/proc/<pid>/cmdline`.
///
/// NUL separators are converted into spaces so the returned string is easier
/// to log or inspect.
///
/// ### Errors
/// - `EACCES`: Permission denied.
/// - `ENOENT`: The process does not exist.
pub fn read_proc_cmdline(pid: i32) -> Result<String, CoreError> {
    read_proc_cmdline_at("/proc", pid)
}

/// Read process command line from an explicit procfs root.
///
/// This is useful for tests, alternate proc mounts, or callers that need the
/// same parsing behavior without being hard-wired to `/proc`.
///
/// ### Errors
/// - `EACCES`: Permission denied.
/// - `ENOENT`: The process or cmdline file does not exist.
pub fn read_proc_cmdline_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<String, CoreError> {
    let path = proc_root.as_ref().join(pid.to_string()).join("cmdline");
    let bytes = std::fs::read(path).map_err(|err| io_error(err, "read_proc_cmdline"))?;
    Ok(parse_proc_cmdline_bytes(&bytes))
}

pub(crate) fn parse_proc_cmdline_bytes(bytes: &[u8]) -> String {
    String::from_utf8_lossy(bytes)
        .trim_end_matches('\0')
        .replace('\0', " ")
}

/// Parse the contents of a `/proc/<pid>/status` file.
pub fn parse_proc_status(content: &str) -> Result<ProcStatus, CoreError> {
    let mut name = None;
    let mut uid = None;

    for line in content.lines() {
        if let Some(rest) = line.strip_prefix("Name:") {
            name = Some(rest.trim().to_string());
        } else if let Some(rest) = line.strip_prefix("Uid:") {
            uid = rest
                .split_whitespace()
                .next()
                .and_then(|value| value.parse::<u32>().ok());
        }

        if name.is_some() && uid.is_some() {
            break;
        }
    }

    match (name, uid) {
        (Some(name), Some(uid)) => Ok(ProcStatus { name, uid }),
        _ => Err(CoreError::sys(libc::EINVAL, "parse_proc_status")),
    }
}

fn io_error(err: std::io::Error, op: &'static str) -> CoreError {
    CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), op)
}

/// Return the number of clock ticks per second for the current system.
///
/// ### Errors
/// - `EINVAL`: `sysconf` failed to retrieve the clock tick rate.
#[inline(always)]
pub fn clock_ticks_per_second() -> Result<u64, CoreError> {
    let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
    if ticks <= 0 {
        let code = std::io::Error::last_os_error()
            .raw_os_error()
            .unwrap_or(libc::EINVAL);
        Err(CoreError::sys(code, "sysconf(_SC_CLK_TCK)"))
    } else {
        Ok(ticks as u64)
    }
}

/// Filesystem identity derived from `stat(2)`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Stat {
    /// Owning UID from `st_uid`.
    pub uid: u32,
    /// Inode number from `st_ino`.
    pub inode: u64,
    /// Change time seconds from `st_ctime`.
    pub ctime_sec: i64,
    /// Change time nanoseconds from `st_ctime_nsec`.
    pub ctime_nsec: i64,
    /// Modification time seconds from `st_mtime`.
    pub mtime_sec: i64,
    /// Modification time nanoseconds from `st_mtime_nsec`.
    pub mtime_nsec: i64,
}

/// Return the owning UID for a filesystem path.
///
/// This performs a `stat(2)` call and returns the owner UID from the resulting
/// metadata. Missing files, permission errors, and invalid path bytes are
/// surfaced as [`CoreError`].
/// Return the owning UID for a filesystem path.
///
/// This performs a `stat(2)` call and returns the owner UID from the resulting
/// metadata. Missing files, permission errors, and invalid path bytes are
/// surfaced as [`CoreError`].
///
/// ### Errors
/// - `EACCES`: Permission denied for a component of the path prefix.
/// - `ENOENT`: The path does not exist.
pub fn path_uid(path: impl AsRef<Path>) -> Result<u32, CoreError> {
    Ok(path_stat(path)?.uid)
}

/// Return identity metadata for a filesystem path, following symbolic links.
///
/// This performs a `stat(2)` call and captures the fields used by hot-path
/// procfs callers to detect identity changes cheaply without opening procfs
/// text files.
///
/// ### Errors
/// Same as [`path_uid`].
pub fn path_stat(path: impl AsRef<Path>) -> Result<Stat, CoreError> {
    stat_path(path.as_ref(), "stat", true)
}

/// Return identity metadata for a filesystem path without following symbolic links.
pub fn path_lstat(path: impl AsRef<Path>) -> Result<Stat, CoreError> {
    stat_path(path.as_ref(), "lstat", false)
}

/// Return the owning UID for `/proc/<pid>`.
///
/// This is a cheap ownership probe that can be useful before reading procfs
/// files such as `/proc/<pid>/cmdline` in hot paths. Processes may disappear
/// at any time, so callers should treat `ENOENT` as a normal race.
pub fn uid(pid: i32) -> Result<u32, CoreError> {
    uid_at("/proc", pid)
}

/// Return the owning UID for `/proc/<pid>` under an explicit procfs root.
///
/// This is a cheap ownership probe for tests, alternate proc mounts, or
/// callers that need to inspect a procfs tree other than the host `/proc`.
pub fn uid_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<u32, CoreError> {
    Ok(stat_at(proc_root, pid)?.uid)
}

/// Return identity metadata for `/proc/<pid>`.
pub fn stat(pid: i32) -> Result<Stat, CoreError> {
    stat_at("/proc", pid)
}

/// Return identity metadata for `/proc/<pid>` under an explicit procfs root.
pub fn stat_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<Stat, CoreError> {
    let path = proc_root.as_ref().join(pid.to_string());
    stat_path(&path, "stat", true)
}

/// Return the effective UID of the current process.
#[inline(always)]
pub fn effective_uid() -> u32 {
    unsafe { libc::geteuid() }
}

/// An open `/proc/<pid>` directory handle that pins the process.
///
/// Opening the directory and reading the `status`/`cmdline` files through the
/// same dirfd (`openat`/`fstatat`) removes the pid-reuse TOCTOU between
/// independent path lookups: once the directory is open, the kernel keeps it
/// bound to that specific process, so a pid recycled between a probe and a
/// read cannot misattribute identity (finding 13).
pub struct ProcDir {
    fd: std::os::unix::io::OwnedFd,
}

impl ProcDir {
    /// Open the `/proc/<pid>` directory.
    ///
    /// ### Errors
    /// - `EACCES`: Permission denied.
    /// - `ENOENT`: The process does not exist (or is gone).
    pub fn open(pid: i32) -> Result<Self, CoreError> {
        Self::open_at("/proc", pid)
    }

    /// Open the `/proc/<pid>` directory under an explicit procfs root.
    pub fn open_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<Self, CoreError> {
        let path = proc_root.as_ref().join(pid.to_string());
        let path = CString::new(path.as_os_str().as_bytes())
            .map_err(|_| CoreError::sys(libc::EINVAL, "open proc dir"))?;
        // O_NOFOLLOW: the proc dir must not be a symlink (a foreign mount or a
        // crafted proc root must not redirect the pin elsewhere).
        let fd = unsafe {
            libc::open(
                path.as_ptr(),
                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
            )
        };
        if fd < 0 {
            return Err(CoreError::sys(errno(), "open proc dir"));
        }
        let fd = unsafe { std::os::unix::io::OwnedFd::from_raw_fd(fd) };
        Ok(Self { fd })
    }

    /// Owning UID of the pinned process.
    pub fn uid(&self) -> Result<u32, CoreError> {
        Ok(self.stat()?.uid)
    }

    /// Identity metadata of the pinned process.
    pub fn stat(&self) -> Result<Stat, CoreError> {
        let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
        // fstatat on the pinned dirfd rather than re-look-upping `/proc/<pid>`:
        // the open dirfd cannot be recycled into a different process.
        let empty = CString::new("").map_err(|_| CoreError::sys(libc::EINVAL, "fstatat"))?;
        let ret = unsafe {
            libc::fstatat(
                self.fd.as_raw_fd(),
                empty.as_ptr(),
                &mut stat_buf,
                libc::AT_EMPTY_PATH,
            )
        };
        // AT_EMPTY_PATH is unsupported on some kernels/filesystems; fall back
        // to fstat on the dirfd itself (same inode, same pinned process).
        if ret < 0 && errno() == libc::EINVAL {
            let ret = unsafe { libc::fstat(self.fd.as_raw_fd(), &mut stat_buf) };
            syscall_ret(ret, "fstat(proc dir)")?;
        } else {
            syscall_ret(ret, "fstatat(proc dir)")?;
        }
        Ok(Stat {
            uid: stat_buf.st_uid,
            inode: stat_buf.st_ino as _,
            ctime_sec: stat_buf.st_ctime as _,
            ctime_nsec: stat_buf.st_ctime_nsec as _,
            mtime_sec: stat_buf.st_mtime as _,
            mtime_nsec: stat_buf.st_mtime_nsec as _,
        })
    }

    /// Read `/proc/<pid>/status` through the pinned handle.
    pub fn status(&self) -> Result<ProcStatus, CoreError> {
        let content = self.read_file("status")?;
        let content = String::from_utf8(content)
            .map_err(|_| CoreError::sys(libc::EINVAL, "decode proc status"))?;
        parse_proc_status(&content)
    }

    /// Read `/proc/<pid>/cmdline` through the pinned handle.
    pub fn cmdline(&self) -> Result<String, CoreError> {
        let bytes = self.read_file("cmdline")?;
        Ok(parse_proc_cmdline_bytes(&bytes))
    }

    fn read_file(&self, name: &str) -> Result<Vec<u8>, CoreError> {
        let name =
            CString::new(name).map_err(|_| CoreError::sys(libc::EINVAL, "openat(proc file)"))?;
        let fd = unsafe {
            libc::openat(
                self.fd.as_raw_fd(),
                name.as_ptr(),
                libc::O_RDONLY | libc::O_CLOEXEC,
            )
        };
        if fd < 0 {
            return Err(CoreError::sys(errno(), "openat(proc file)"));
        }
        let mut bytes = Vec::new();
        let mut buf = [0u8; 4096];
        loop {
            let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
            if n < 0 {
                if errno() == libc::EINTR {
                    continue;
                }
                let e = errno();
                unsafe { libc::close(fd) };
                return Err(CoreError::sys(e, "read(proc file)"));
            }
            if n == 0 {
                break;
            }
            bytes.extend_from_slice(&buf[..n as usize]);
        }
        unsafe { libc::close(fd) };
        Ok(bytes)
    }
}

/// Change the owner of a path while leaving the group unchanged when `gid` is `None`.
///
/// ### Errors
/// - `EACCES`: Permission denied for a component of the path prefix.
/// - `EPERM`: The caller does not have permission to change the owner.
/// - `ENOENT`: The path does not exist.
pub fn chown(path: impl AsRef<Path>, uid: u32, gid: Option<u32>) -> Result<(), CoreError> {
    let path = CString::new(path.as_ref().as_os_str().as_bytes())
        .map_err(|_| CoreError::sys(libc::EINVAL, "chown"))?;
    let gid = gid.unwrap_or(u32::MAX);
    let ret = unsafe { libc::chown(path.as_ptr(), uid, gid) };
    syscall_ret(ret, "chown")
}

fn stat_path(path: &Path, op: &'static str, follow_symlink: bool) -> Result<Stat, CoreError> {
    let path =
        CString::new(path.as_os_str().as_bytes()).map_err(|_| CoreError::sys(libc::EINVAL, op))?;
    let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
    let ret = if follow_symlink {
        unsafe { libc::stat(path.as_ptr(), &mut stat_buf) }
    } else {
        unsafe { libc::lstat(path.as_ptr(), &mut stat_buf) }
    };
    syscall_ret(ret, op)?;
    Ok(Stat {
        uid: stat_buf.st_uid,
        inode: stat_buf.st_ino as _,
        ctime_sec: stat_buf.st_ctime as _,
        ctime_nsec: stat_buf.st_ctime_nsec as _,
        mtime_sec: stat_buf.st_mtime as _,
        mtime_nsec: stat_buf.st_mtime_nsec as _,
    })
}