use libc::{c_int, c_void};
pub const STRIDE: usize = 648;
const OFF_STARTTIME_SEC: usize = 0;
const OFF_STARTTIME_USEC: usize = 8;
const OFF_PID: usize = 40;
const OFF_PPID: usize = 560;
const OFF_NICE: usize = 242;
const OFF_STAT: usize = 36;
const OFF_COMM: usize = 243;
const COMM_LEN: usize = 17;
const OFF_UID: usize = 392;
#[derive(Copy, Clone, Debug, Default)]
pub struct RawProc {
pub pid: u32,
pub ppid: u32,
pub uid: u32,
pub nice: i32,
pub p_stat: i8,
pub start_sec: u64,
pub start_usec: u64,
pub comm: [u8; COMM_LEN],
}
impl RawProc {
pub fn comm_str(&self) -> String {
let len = self.comm.iter().position(|b| *b == 0).unwrap_or(COMM_LEN);
String::from_utf8_lossy(&self.comm[..len]).into_owned()
}
}
fn read_i32(b: &[u8], off: usize) -> i32 {
i32::from_ne_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
}
fn read_u32(b: &[u8], off: usize) -> u32 {
u32::from_ne_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
}
fn read_u64(b: &[u8], off: usize) -> u64 {
u64::from_ne_bytes([
b[off],
b[off + 1],
b[off + 2],
b[off + 3],
b[off + 4],
b[off + 5],
b[off + 6],
b[off + 7],
])
}
fn decode(rec: &[u8]) -> Option<RawProc> {
let pid = read_i32(rec, OFF_PID);
if pid <= 0 {
return None;
}
let mut comm = [0u8; COMM_LEN];
comm.copy_from_slice(&rec[OFF_COMM..OFF_COMM + COMM_LEN]);
Some(RawProc {
pid: pid as u32,
ppid: read_i32(rec, OFF_PPID).max(0) as u32,
uid: read_u32(rec, OFF_UID),
nice: rec[OFF_NICE] as i8 as i32,
p_stat: rec[OFF_STAT] as i8,
start_sec: read_u64(rec, OFF_STARTTIME_SEC),
start_usec: read_u64(rec, OFF_STARTTIME_USEC),
comm,
})
}
pub fn list_processes() -> std::io::Result<Vec<RawProc>> {
let mut mib = [
libc::CTL_KERN,
libc::KERN_PROC,
libc::KERN_PROC_ALL,
0 as c_int,
];
let mut hint = 0usize;
let rc = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
mib.len() as u32,
std::ptr::null_mut(),
&mut hint,
std::ptr::null_mut(),
0,
)
};
let mut capacity = if rc == 0 && hint > 0 {
hint.div_ceil(STRIDE) + 64
} else {
1024
};
for _ in 0..6 {
let mut buf = vec![0u8; capacity * STRIDE];
let mut len = buf.len();
let rc = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
mib.len() as u32,
buf.as_mut_ptr().cast::<c_void>(),
&mut len,
std::ptr::null_mut(),
0,
)
};
if rc != 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ENOMEM) {
capacity *= 2;
continue;
}
return Err(err);
}
let records = len / STRIDE;
if records >= capacity {
capacity *= 2;
continue;
}
buf.truncate(records * STRIDE);
let mut out = Vec::with_capacity(records);
let (records_slice, _remainder) = buf.as_chunks::<STRIDE>();
for rec in records_slice {
if let Some(p) = decode(rec) {
out.push(p);
}
}
return Ok(out);
}
Err(std::io::Error::other(
"KERN_PROC_ALL kept returning a full buffer",
))
}
pub fn validate() -> Result<(), String> {
let procs = list_processes().map_err(|e| format!("sysctl failed: {e}"))?;
match procs.iter().find(|p| p.pid == 1) {
Some(launchd) if launchd.comm_str() == "launchd" => {}
Some(other) => {
return Err(format!(
"pid 1 reports comm {:?}, expected \"launchd\" (layout changed?)",
other.comm_str()
))
}
None => return Err("pid 1 missing from KERN_PROC_ALL".to_string()),
}
let me = std::process::id();
match procs.iter().find(|p| p.pid == me) {
Some(this) => {
let (ppid, uid) = unsafe { (libc::getppid() as u32, libc::getuid()) };
if this.ppid != ppid {
return Err(format!(
"own ppid is {} in sysctl, {ppid} from getppid",
this.ppid
));
}
if this.uid != uid {
return Err(format!(
"own uid is {} in sysctl, {uid} from getuid",
this.uid
));
}
}
None => return Err("own pid missing from KERN_PROC_ALL".to_string()),
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
for p in procs.iter().take(64) {
if p.start_sec > now + 86_400 || (p.start_sec != 0 && p.start_sec < 500_000_000) {
return Err(format!(
"pid {} has implausible start time {}",
p.pid, p.start_sec
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layout_offsets_are_consistent() {
let mut spans = [
(OFF_STARTTIME_SEC, 8),
(OFF_PID, 4),
(OFF_PPID, 4),
(OFF_NICE, 1),
(OFF_STAT, 1),
(OFF_COMM, COMM_LEN),
(OFF_UID, 4),
];
spans.sort_unstable();
for w in spans.windows(2) {
let (a, alen) = w[0];
let (b, _) = w[1];
assert!(a + alen <= b, "fields overlap at {a} and {b}");
}
for (off, len) in spans {
assert!(off + len <= STRIDE, "field at {off} runs past the record");
}
}
#[test]
fn validate_passes_on_this_kernel() {
validate().expect("kinfo_proc layout must match this macOS version");
}
#[test]
fn lists_every_process_including_root_owned() {
let procs = list_processes().expect("KERN_PROC_ALL must work");
assert!(procs.len() > 100, "got {} processes", procs.len());
let launchd = procs.iter().find(|p| p.pid == 1).expect("pid 1");
assert_eq!(launchd.comm_str(), "launchd");
assert_eq!(launchd.uid, 0);
let mut pids: Vec<u32> = procs.iter().map(|p| p.pid).collect();
let n = pids.len();
pids.sort_unstable();
pids.dedup();
assert_eq!(pids.len(), n, "duplicate pids in KERN_PROC_ALL");
}
#[test]
fn sees_more_processes_than_proc_pidinfo() {
let via_sysctl = list_processes().map(|v| v.len()).unwrap_or(0);
let via_libproc = crate::darwin::scan::list_pids()
.map(|v| {
v.into_iter()
.filter(|p| crate::darwin::scan::task_all_info(*p).is_some())
.count()
})
.unwrap_or(0);
assert!(
via_sysctl >= via_libproc,
"sysctl saw {via_sysctl}, libproc {via_libproc}"
);
if unsafe { libc::getuid() } != 0 {
assert!(
via_sysctl > via_libproc + 50,
"sysctl ({via_sysctl}) should see far more than libproc ({via_libproc}) \
for a non-root user"
);
}
}
#[test]
fn uid_is_the_real_owner() {
let procs = list_processes().unwrap();
let me = std::process::id();
let this = procs.iter().find(|p| p.pid == me).expect("own pid");
let real = unsafe { libc::getuid() };
assert_eq!(this.uid, real);
if real != 0 {
let distinct: std::collections::HashSet<u32> = procs.iter().map(|p| p.uid).collect();
assert!(
distinct.len() > 1,
"every process reported uid {real}; the owner is being lost"
);
}
}
#[test]
fn start_time_including_microseconds_matches_libproc() {
let pid = std::process::id();
let procs = list_processes().unwrap();
let this = procs.iter().find(|p| p.pid == pid).unwrap();
let bsd = crate::darwin::scan::task_all_info(pid).unwrap().bsd;
assert_eq!(this.start_sec, bsd.start_sec);
assert_eq!(this.start_usec, bsd.start_usec);
assert!(this.start_usec < 1_000_000);
}
#[test]
fn own_ppid_matches_getppid() {
let procs = list_processes().unwrap();
let me = std::process::id();
let this = procs.iter().find(|p| p.pid == me).expect("own pid");
let real = unsafe { libc::getppid() } as u32;
assert_eq!(this.ppid, real);
}
#[test]
fn state_histogram_matches_bsd_meaning() {
let procs = list_processes().unwrap();
let hist: std::collections::BTreeMap<i8, usize> =
procs.iter().fold(Default::default(), |mut h, p| {
*h.entry(p.p_stat).or_insert(0) += 1;
h
});
assert!(!hist.is_empty(), "no processes were decoded");
for stat in hist.keys() {
assert!(
(1..=5).contains(stat),
"p_stat {stat} is not a BSD S* constant; histogram {hist:?}"
);
}
let total: usize = hist.values().sum();
let plausible: usize = hist
.iter()
.filter(|(stat, _)| (1..=5).contains(*stat))
.map(|(_, n)| *n)
.sum();
assert_eq!(plausible, total);
}
#[test]
fn a_stopped_process_reports_sstop() {
let mut child = std::process::Command::new("sleep")
.arg("30")
.spawn()
.expect("spawn sleep");
let pid = child.id();
let rc = unsafe { libc::kill(pid as libc::pid_t, libc::SIGSTOP) };
assert_eq!(rc, 0, "SIGSTOP failed");
std::thread::sleep(std::time::Duration::from_millis(120));
let procs = list_processes().expect("KERN_PROC_ALL");
let this = procs.iter().find(|p| p.pid == pid);
let stat = this.map(|p| p.p_stat);
let _ = child.kill();
let _ = child.wait();
assert_eq!(
stat,
Some(4),
"a stopped process must read SSTOP(4), not {} (offset wrong?)",
stat.map(|s| s.to_string())
.unwrap_or_else(|| "missing".into())
);
}
}