cloudfox-coreshift-core 2.0.0

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::unix::ffi::OsStrExt;
use std::path::Path;

/// 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() }
}

/// 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 _,
    })
}