Skip to main content

coreshift_core/
proc.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/
4
5//! Procfs and process-introspection helpers.
6//!
7//! These functions provide a small, Linux-oriented view into `/proc` for
8//! callers that need process names, command lines, UIDs, or clock-tick
9//! information without bringing in a broader process-inspection crate.
10
11use crate::CoreError;
12use crate::error::syscall_ret;
13use std::ffi::CString;
14use std::os::unix::ffi::OsStrExt;
15use std::path::Path;
16
17/// A snapshot of process status information from procfs.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct ProcStatus {
20    /// Command name of the process.
21    pub name: String,
22    /// Real UID of the process.
23    pub uid: u32,
24}
25
26/// Read process status from `/proc/<pid>/status`.
27///
28/// ### Errors
29/// - `EACCES`: Permission denied.
30/// - `ENOENT`: The process does not exist.
31pub fn read_proc_status(pid: i32) -> Result<ProcStatus, CoreError> {
32    read_proc_status_at("/proc", pid)
33}
34
35/// Read process status from an explicit procfs root.
36///
37/// ### Errors
38/// - `EACCES`: Permission denied.
39/// - `ENOENT`: The process or status file does not exist.
40pub fn read_proc_status_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<ProcStatus, CoreError> {
41    let path = proc_root.as_ref().join(pid.to_string()).join("status");
42    let content = std::fs::read_to_string(path).map_err(|err| io_error(err, "read_proc_status"))?;
43    parse_proc_status(&content)
44}
45
46/// Read process command line from `/proc/<pid>/cmdline`.
47///
48/// NUL separators are converted into spaces so the returned string is easier
49/// to log or inspect.
50///
51/// ### Errors
52/// - `EACCES`: Permission denied.
53/// - `ENOENT`: The process does not exist.
54pub fn read_proc_cmdline(pid: i32) -> Result<String, CoreError> {
55    read_proc_cmdline_at("/proc", pid)
56}
57
58/// Read process command line from an explicit procfs root.
59///
60/// This is useful for tests, alternate proc mounts, or callers that need the
61/// same parsing behavior without being hard-wired to `/proc`.
62///
63/// ### Errors
64/// - `EACCES`: Permission denied.
65/// - `ENOENT`: The process or cmdline file does not exist.
66pub fn read_proc_cmdline_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<String, CoreError> {
67    let path = proc_root.as_ref().join(pid.to_string()).join("cmdline");
68    let bytes = std::fs::read(path).map_err(|err| io_error(err, "read_proc_cmdline"))?;
69    Ok(parse_proc_cmdline_bytes(&bytes))
70}
71
72pub(crate) fn parse_proc_cmdline_bytes(bytes: &[u8]) -> String {
73    String::from_utf8_lossy(bytes)
74        .trim_end_matches('\0')
75        .replace('\0', " ")
76}
77
78/// Parse the contents of a `/proc/<pid>/status` file.
79pub fn parse_proc_status(content: &str) -> Result<ProcStatus, CoreError> {
80    let mut name = None;
81    let mut uid = None;
82
83    for line in content.lines() {
84        if let Some(rest) = line.strip_prefix("Name:") {
85            name = Some(rest.trim().to_string());
86        } else if let Some(rest) = line.strip_prefix("Uid:") {
87            uid = rest
88                .split_whitespace()
89                .next()
90                .and_then(|value| value.parse::<u32>().ok());
91        }
92
93        if name.is_some() && uid.is_some() {
94            break;
95        }
96    }
97
98    match (name, uid) {
99        (Some(name), Some(uid)) => Ok(ProcStatus { name, uid }),
100        _ => Err(CoreError::sys(libc::EINVAL, "parse_proc_status")),
101    }
102}
103
104fn io_error(err: std::io::Error, op: &'static str) -> CoreError {
105    CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), op)
106}
107
108/// Return the number of clock ticks per second for the current system.
109///
110/// ### Errors
111/// - `EINVAL`: `sysconf` failed to retrieve the clock tick rate.
112#[inline(always)]
113pub fn clock_ticks_per_second() -> Result<u64, CoreError> {
114    let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
115    if ticks <= 0 {
116        let code = std::io::Error::last_os_error()
117            .raw_os_error()
118            .unwrap_or(libc::EINVAL);
119        Err(CoreError::sys(code, "sysconf(_SC_CLK_TCK)"))
120    } else {
121        Ok(ticks as u64)
122    }
123}
124
125/// Filesystem identity derived from `stat(2)`.
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct Stat {
128    /// Owning UID from `st_uid`.
129    pub uid: u32,
130    /// Inode number from `st_ino`.
131    pub inode: u64,
132    /// Change time seconds from `st_ctime`.
133    pub ctime_sec: i64,
134    /// Change time nanoseconds from `st_ctime_nsec`.
135    pub ctime_nsec: i64,
136    /// Modification time seconds from `st_mtime`.
137    pub mtime_sec: i64,
138    /// Modification time nanoseconds from `st_mtime_nsec`.
139    pub mtime_nsec: i64,
140}
141
142/// Return the owning UID for a filesystem path.
143///
144/// This performs a `stat(2)` call and returns the owner UID from the resulting
145/// metadata. Missing files, permission errors, and invalid path bytes are
146/// surfaced as [`CoreError`].
147/// Return the owning UID for a filesystem path.
148///
149/// This performs a `stat(2)` call and returns the owner UID from the resulting
150/// metadata. Missing files, permission errors, and invalid path bytes are
151/// surfaced as [`CoreError`].
152///
153/// ### Errors
154/// - `EACCES`: Permission denied for a component of the path prefix.
155/// - `ENOENT`: The path does not exist.
156pub fn path_uid(path: impl AsRef<Path>) -> Result<u32, CoreError> {
157    Ok(path_stat(path)?.uid)
158}
159
160/// Return identity metadata for a filesystem path, following symbolic links.
161///
162/// This performs a `stat(2)` call and captures the fields used by hot-path
163/// procfs callers to detect identity changes cheaply without opening procfs
164/// text files.
165///
166/// ### Errors
167/// Same as [`path_uid`].
168pub fn path_stat(path: impl AsRef<Path>) -> Result<Stat, CoreError> {
169    stat_path(path.as_ref(), "stat", true)
170}
171
172/// Return identity metadata for a filesystem path without following symbolic links.
173pub fn path_lstat(path: impl AsRef<Path>) -> Result<Stat, CoreError> {
174    stat_path(path.as_ref(), "lstat", false)
175}
176
177/// Return the owning UID for `/proc/<pid>`.
178///
179/// This is a cheap ownership probe that can be useful before reading procfs
180/// files such as `/proc/<pid>/cmdline` in hot paths. Processes may disappear
181/// at any time, so callers should treat `ENOENT` as a normal race.
182pub fn uid(pid: i32) -> Result<u32, CoreError> {
183    uid_at("/proc", pid)
184}
185
186/// Return the owning UID for `/proc/<pid>` under an explicit procfs root.
187///
188/// This is a cheap ownership probe for tests, alternate proc mounts, or
189/// callers that need to inspect a procfs tree other than the host `/proc`.
190pub fn uid_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<u32, CoreError> {
191    Ok(stat_at(proc_root, pid)?.uid)
192}
193
194/// Return identity metadata for `/proc/<pid>`.
195pub fn stat(pid: i32) -> Result<Stat, CoreError> {
196    stat_at("/proc", pid)
197}
198
199/// Return identity metadata for `/proc/<pid>` under an explicit procfs root.
200pub fn stat_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<Stat, CoreError> {
201    let path = proc_root.as_ref().join(pid.to_string());
202    stat_path(&path, "stat", true)
203}
204
205/// Return the effective UID of the current process.
206#[inline(always)]
207pub fn effective_uid() -> u32 {
208    unsafe { libc::geteuid() }
209}
210
211/// Change the owner of a path while leaving the group unchanged when `gid` is `None`.
212///
213/// ### Errors
214/// - `EACCES`: Permission denied for a component of the path prefix.
215/// - `EPERM`: The caller does not have permission to change the owner.
216/// - `ENOENT`: The path does not exist.
217pub fn chown(path: impl AsRef<Path>, uid: u32, gid: Option<u32>) -> Result<(), CoreError> {
218    let path = CString::new(path.as_ref().as_os_str().as_bytes())
219        .map_err(|_| CoreError::sys(libc::EINVAL, "chown"))?;
220    let gid = gid.unwrap_or(u32::MAX);
221    let ret = unsafe { libc::chown(path.as_ptr(), uid, gid) };
222    syscall_ret(ret, "chown")
223}
224
225fn stat_path(path: &Path, op: &'static str, follow_symlink: bool) -> Result<Stat, CoreError> {
226    let path =
227        CString::new(path.as_os_str().as_bytes()).map_err(|_| CoreError::sys(libc::EINVAL, op))?;
228    let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
229    let ret = if follow_symlink {
230        unsafe { libc::stat(path.as_ptr(), &mut stat_buf) }
231    } else {
232        unsafe { libc::lstat(path.as_ptr(), &mut stat_buf) }
233    };
234    syscall_ret(ret, op)?;
235    Ok(Stat {
236        uid: stat_buf.st_uid,
237        inode: stat_buf.st_ino as _,
238        ctime_sec: stat_buf.st_ctime as _,
239        ctime_nsec: stat_buf.st_ctime_nsec as _,
240        mtime_sec: stat_buf.st_mtime as _,
241        mtime_nsec: stat_buf.st_mtime_nsec as _,
242    })
243}