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::fd::{AsRawFd, FromRawFd};
15use std::os::unix::ffi::OsStrExt;
16use std::path::Path;
17
18#[inline(always)]
19fn errno() -> i32 {
20    std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
21}
22
23/// A snapshot of process status information from procfs.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct ProcStatus {
26    /// Command name of the process.
27    pub name: String,
28    /// Real UID of the process.
29    pub uid: u32,
30}
31
32/// Read process status from `/proc/<pid>/status`.
33///
34/// ### Errors
35/// - `EACCES`: Permission denied.
36/// - `ENOENT`: The process does not exist.
37pub fn read_proc_status(pid: i32) -> Result<ProcStatus, CoreError> {
38    read_proc_status_at("/proc", pid)
39}
40
41/// Read process status from an explicit procfs root.
42///
43/// ### Errors
44/// - `EACCES`: Permission denied.
45/// - `ENOENT`: The process or status file does not exist.
46pub fn read_proc_status_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<ProcStatus, CoreError> {
47    let path = proc_root.as_ref().join(pid.to_string()).join("status");
48    let content = std::fs::read_to_string(path).map_err(|err| io_error(err, "read_proc_status"))?;
49    parse_proc_status(&content)
50}
51
52/// Read process command line from `/proc/<pid>/cmdline`.
53///
54/// NUL separators are converted into spaces so the returned string is easier
55/// to log or inspect.
56///
57/// ### Errors
58/// - `EACCES`: Permission denied.
59/// - `ENOENT`: The process does not exist.
60pub fn read_proc_cmdline(pid: i32) -> Result<String, CoreError> {
61    read_proc_cmdline_at("/proc", pid)
62}
63
64/// Read process command line from an explicit procfs root.
65///
66/// This is useful for tests, alternate proc mounts, or callers that need the
67/// same parsing behavior without being hard-wired to `/proc`.
68///
69/// ### Errors
70/// - `EACCES`: Permission denied.
71/// - `ENOENT`: The process or cmdline file does not exist.
72pub fn read_proc_cmdline_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<String, CoreError> {
73    let path = proc_root.as_ref().join(pid.to_string()).join("cmdline");
74    let bytes = std::fs::read(path).map_err(|err| io_error(err, "read_proc_cmdline"))?;
75    Ok(parse_proc_cmdline_bytes(&bytes))
76}
77
78pub(crate) fn parse_proc_cmdline_bytes(bytes: &[u8]) -> String {
79    String::from_utf8_lossy(bytes)
80        .trim_end_matches('\0')
81        .replace('\0', " ")
82}
83
84/// Parse the contents of a `/proc/<pid>/status` file.
85pub fn parse_proc_status(content: &str) -> Result<ProcStatus, CoreError> {
86    let mut name = None;
87    let mut uid = None;
88
89    for line in content.lines() {
90        if let Some(rest) = line.strip_prefix("Name:") {
91            name = Some(rest.trim().to_string());
92        } else if let Some(rest) = line.strip_prefix("Uid:") {
93            uid = rest
94                .split_whitespace()
95                .next()
96                .and_then(|value| value.parse::<u32>().ok());
97        }
98
99        if name.is_some() && uid.is_some() {
100            break;
101        }
102    }
103
104    match (name, uid) {
105        (Some(name), Some(uid)) => Ok(ProcStatus { name, uid }),
106        _ => Err(CoreError::sys(libc::EINVAL, "parse_proc_status")),
107    }
108}
109
110fn io_error(err: std::io::Error, op: &'static str) -> CoreError {
111    CoreError::sys(err.raw_os_error().unwrap_or(libc::EIO), op)
112}
113
114/// Return the number of clock ticks per second for the current system.
115///
116/// ### Errors
117/// - `EINVAL`: `sysconf` failed to retrieve the clock tick rate.
118#[inline(always)]
119pub fn clock_ticks_per_second() -> Result<u64, CoreError> {
120    let ticks = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
121    if ticks <= 0 {
122        let code = std::io::Error::last_os_error()
123            .raw_os_error()
124            .unwrap_or(libc::EINVAL);
125        Err(CoreError::sys(code, "sysconf(_SC_CLK_TCK)"))
126    } else {
127        Ok(ticks as u64)
128    }
129}
130
131/// Filesystem identity derived from `stat(2)`.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub struct Stat {
134    /// Owning UID from `st_uid`.
135    pub uid: u32,
136    /// Inode number from `st_ino`.
137    pub inode: u64,
138    /// Change time seconds from `st_ctime`.
139    pub ctime_sec: i64,
140    /// Change time nanoseconds from `st_ctime_nsec`.
141    pub ctime_nsec: i64,
142    /// Modification time seconds from `st_mtime`.
143    pub mtime_sec: i64,
144    /// Modification time nanoseconds from `st_mtime_nsec`.
145    pub mtime_nsec: i64,
146}
147
148/// Return the owning UID for a filesystem path.
149///
150/// This performs a `stat(2)` call and returns the owner UID from the resulting
151/// metadata. Missing files, permission errors, and invalid path bytes are
152/// surfaced as [`CoreError`].
153/// Return the owning UID for a filesystem path.
154///
155/// This performs a `stat(2)` call and returns the owner UID from the resulting
156/// metadata. Missing files, permission errors, and invalid path bytes are
157/// surfaced as [`CoreError`].
158///
159/// ### Errors
160/// - `EACCES`: Permission denied for a component of the path prefix.
161/// - `ENOENT`: The path does not exist.
162pub fn path_uid(path: impl AsRef<Path>) -> Result<u32, CoreError> {
163    Ok(path_stat(path)?.uid)
164}
165
166/// Return identity metadata for a filesystem path, following symbolic links.
167///
168/// This performs a `stat(2)` call and captures the fields used by hot-path
169/// procfs callers to detect identity changes cheaply without opening procfs
170/// text files.
171///
172/// ### Errors
173/// Same as [`path_uid`].
174pub fn path_stat(path: impl AsRef<Path>) -> Result<Stat, CoreError> {
175    stat_path(path.as_ref(), "stat", true)
176}
177
178/// Return identity metadata for a filesystem path without following symbolic links.
179pub fn path_lstat(path: impl AsRef<Path>) -> Result<Stat, CoreError> {
180    stat_path(path.as_ref(), "lstat", false)
181}
182
183/// Return the owning UID for `/proc/<pid>`.
184///
185/// This is a cheap ownership probe that can be useful before reading procfs
186/// files such as `/proc/<pid>/cmdline` in hot paths. Processes may disappear
187/// at any time, so callers should treat `ENOENT` as a normal race.
188pub fn uid(pid: i32) -> Result<u32, CoreError> {
189    uid_at("/proc", pid)
190}
191
192/// Return the owning UID for `/proc/<pid>` under an explicit procfs root.
193///
194/// This is a cheap ownership probe for tests, alternate proc mounts, or
195/// callers that need to inspect a procfs tree other than the host `/proc`.
196pub fn uid_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<u32, CoreError> {
197    Ok(stat_at(proc_root, pid)?.uid)
198}
199
200/// Return identity metadata for `/proc/<pid>`.
201pub fn stat(pid: i32) -> Result<Stat, CoreError> {
202    stat_at("/proc", pid)
203}
204
205/// Return identity metadata for `/proc/<pid>` under an explicit procfs root.
206pub fn stat_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<Stat, CoreError> {
207    let path = proc_root.as_ref().join(pid.to_string());
208    stat_path(&path, "stat", true)
209}
210
211/// Return the effective UID of the current process.
212#[inline(always)]
213pub fn effective_uid() -> u32 {
214    unsafe { libc::geteuid() }
215}
216
217/// An open `/proc/<pid>` directory handle that pins the process.
218///
219/// Opening the directory and reading the `status`/`cmdline` files through the
220/// same dirfd (`openat`/`fstatat`) removes the pid-reuse TOCTOU between
221/// independent path lookups: once the directory is open, the kernel keeps it
222/// bound to that specific process, so a pid recycled between a probe and a
223/// read cannot misattribute identity (finding 13).
224pub struct ProcDir {
225    fd: std::os::unix::io::OwnedFd,
226}
227
228impl ProcDir {
229    /// Open the `/proc/<pid>` directory.
230    ///
231    /// ### Errors
232    /// - `EACCES`: Permission denied.
233    /// - `ENOENT`: The process does not exist (or is gone).
234    pub fn open(pid: i32) -> Result<Self, CoreError> {
235        Self::open_at("/proc", pid)
236    }
237
238    /// Open the `/proc/<pid>` directory under an explicit procfs root.
239    pub fn open_at(proc_root: impl AsRef<Path>, pid: i32) -> Result<Self, CoreError> {
240        let path = proc_root.as_ref().join(pid.to_string());
241        let path = CString::new(path.as_os_str().as_bytes())
242            .map_err(|_| CoreError::sys(libc::EINVAL, "open proc dir"))?;
243        // O_NOFOLLOW: the proc dir must not be a symlink (a foreign mount or a
244        // crafted proc root must not redirect the pin elsewhere).
245        let fd = unsafe {
246            libc::open(
247                path.as_ptr(),
248                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
249            )
250        };
251        if fd < 0 {
252            return Err(CoreError::sys(errno(), "open proc dir"));
253        }
254        let fd = unsafe { std::os::unix::io::OwnedFd::from_raw_fd(fd) };
255        Ok(Self { fd })
256    }
257
258    /// Owning UID of the pinned process.
259    pub fn uid(&self) -> Result<u32, CoreError> {
260        Ok(self.stat()?.uid)
261    }
262
263    /// Identity metadata of the pinned process.
264    pub fn stat(&self) -> Result<Stat, CoreError> {
265        let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
266        // fstatat on the pinned dirfd rather than re-look-upping `/proc/<pid>`:
267        // the open dirfd cannot be recycled into a different process.
268        let empty = CString::new("").map_err(|_| CoreError::sys(libc::EINVAL, "fstatat"))?;
269        let ret = unsafe {
270            libc::fstatat(
271                self.fd.as_raw_fd(),
272                empty.as_ptr(),
273                &mut stat_buf,
274                libc::AT_EMPTY_PATH,
275            )
276        };
277        // AT_EMPTY_PATH is unsupported on some kernels/filesystems; fall back
278        // to fstat on the dirfd itself (same inode, same pinned process).
279        if ret < 0 && errno() == libc::EINVAL {
280            let ret = unsafe { libc::fstat(self.fd.as_raw_fd(), &mut stat_buf) };
281            syscall_ret(ret, "fstat(proc dir)")?;
282        } else {
283            syscall_ret(ret, "fstatat(proc dir)")?;
284        }
285        Ok(Stat {
286            uid: stat_buf.st_uid,
287            inode: stat_buf.st_ino as _,
288            ctime_sec: stat_buf.st_ctime as _,
289            ctime_nsec: stat_buf.st_ctime_nsec as _,
290            mtime_sec: stat_buf.st_mtime as _,
291            mtime_nsec: stat_buf.st_mtime_nsec as _,
292        })
293    }
294
295    /// Read `/proc/<pid>/status` through the pinned handle.
296    pub fn status(&self) -> Result<ProcStatus, CoreError> {
297        let content = self.read_file("status")?;
298        let content = String::from_utf8(content)
299            .map_err(|_| CoreError::sys(libc::EINVAL, "decode proc status"))?;
300        parse_proc_status(&content)
301    }
302
303    /// Read `/proc/<pid>/cmdline` through the pinned handle.
304    pub fn cmdline(&self) -> Result<String, CoreError> {
305        let bytes = self.read_file("cmdline")?;
306        Ok(parse_proc_cmdline_bytes(&bytes))
307    }
308
309    fn read_file(&self, name: &str) -> Result<Vec<u8>, CoreError> {
310        let name =
311            CString::new(name).map_err(|_| CoreError::sys(libc::EINVAL, "openat(proc file)"))?;
312        let fd = unsafe {
313            libc::openat(
314                self.fd.as_raw_fd(),
315                name.as_ptr(),
316                libc::O_RDONLY | libc::O_CLOEXEC,
317            )
318        };
319        if fd < 0 {
320            return Err(CoreError::sys(errno(), "openat(proc file)"));
321        }
322        let mut bytes = Vec::new();
323        let mut buf = [0u8; 4096];
324        loop {
325            let n = unsafe { libc::read(fd, buf.as_mut_ptr() as *mut _, buf.len()) };
326            if n < 0 {
327                if errno() == libc::EINTR {
328                    continue;
329                }
330                let e = errno();
331                unsafe { libc::close(fd) };
332                return Err(CoreError::sys(e, "read(proc file)"));
333            }
334            if n == 0 {
335                break;
336            }
337            bytes.extend_from_slice(&buf[..n as usize]);
338        }
339        unsafe { libc::close(fd) };
340        Ok(bytes)
341    }
342}
343
344/// Change the owner of a path while leaving the group unchanged when `gid` is `None`.
345///
346/// ### Errors
347/// - `EACCES`: Permission denied for a component of the path prefix.
348/// - `EPERM`: The caller does not have permission to change the owner.
349/// - `ENOENT`: The path does not exist.
350pub fn chown(path: impl AsRef<Path>, uid: u32, gid: Option<u32>) -> Result<(), CoreError> {
351    let path = CString::new(path.as_ref().as_os_str().as_bytes())
352        .map_err(|_| CoreError::sys(libc::EINVAL, "chown"))?;
353    let gid = gid.unwrap_or(u32::MAX);
354    let ret = unsafe { libc::chown(path.as_ptr(), uid, gid) };
355    syscall_ret(ret, "chown")
356}
357
358fn stat_path(path: &Path, op: &'static str, follow_symlink: bool) -> Result<Stat, CoreError> {
359    let path =
360        CString::new(path.as_os_str().as_bytes()).map_err(|_| CoreError::sys(libc::EINVAL, op))?;
361    let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
362    let ret = if follow_symlink {
363        unsafe { libc::stat(path.as_ptr(), &mut stat_buf) }
364    } else {
365        unsafe { libc::lstat(path.as_ptr(), &mut stat_buf) }
366    };
367    syscall_ret(ret, op)?;
368    Ok(Stat {
369        uid: stat_buf.st_uid,
370        inode: stat_buf.st_ino as _,
371        ctime_sec: stat_buf.st_ctime as _,
372        ctime_nsec: stat_buf.st_ctime_nsec as _,
373        mtime_sec: stat_buf.st_mtime as _,
374        mtime_nsec: stat_buf.st_mtime_nsec as _,
375    })
376}