1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
//! macOS `sysctl(KERN_PROCARGS2)` implementation. Returns the
//! actual argv the kernel handed to `execve`, fully no-admin for
//! processes the calling user owns.
//!
//! Layout of the returned buffer (per `sys/sysctl.h` +
//! `bsd/kern/kern_sysctl.c` in xnu):
//!
//! ```text
//! [ argc (i32, host endianness) ]
//! [ exec_path (NUL-terminated UTF-8 string) ]
//! [ NUL padding to align to ptr-boundary ]
//! [ argv[0] (NUL-terminated) ]
//! [ argv[1] ... argv[argc-1] (each NUL-terminated) ]
//! [ envp[0] ... envp[N] (NUL-terminated; ignored here) ]
//! ```
const CTL_KERN: libc::c_int = 1;
const KERN_PROCARGS2: libc::c_int = 49;
/// Return the process argv without flattening its argument boundaries.
pub fn read_process_argv(pid: u32) -> std::io::Result<Vec<std::ffi::OsString>> {
if pid == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"pid 0 is the kernel scheduler  not queryable",
));
}
let mut name: [libc::c_int; 3] = [CTL_KERN, KERN_PROCARGS2, pid as libc::c_int];
// Size probe: pass null buf to learn the required length.
let mut len: libc::size_t = 0;
let r = unsafe {
libc::sysctl(
name.as_mut_ptr(),
3,
std::ptr::null_mut(),
&mut len,
std::ptr::null_mut(),
0,
)
};
if r != 0 {
return Err(std::io::Error::last_os_error());
}
if len < std::mem::size_of::<i32>() {
return Err(std::io::Error::other(format!(
"KERN_PROCARGS2 returned size={len}, smaller than argc header",
)));
}
let mut buf = vec![0u8; len];
let r = unsafe {
libc::sysctl(
name.as_mut_ptr(),
3,
buf.as_mut_ptr() as *mut libc::c_void,
&mut len,
std::ptr::null_mut(),
0,
)
};
if r != 0 {
return Err(std::io::Error::last_os_error());
}
buf.truncate(len);
parse_procargs2_argv(&buf)
}
/// Return the legacy, human-readable command display string.
///
/// This is deliberately not shell syntax and must not be parsed as argv.
pub fn read_process_cmdline(pid: u32) -> std::io::Result<String> {
Ok(render_display(&read_process_argv(pid)?))
}
fn parse_procargs2_argv(buf: &[u8]) -> std::io::Result<Vec<std::ffi::OsString>> {
use std::os::unix::ffi::OsStringExt;
if buf.len() < std::mem::size_of::<i32>() {
return Ok(Vec::new());
}
let argc = i32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
if argc <= 0 {
return Ok(Vec::new());
}
let mut cursor = std::mem::size_of::<i32>();
// Skip exec_path: bytes until first NUL.
while cursor < buf.len() && buf[cursor] != 0 {
cursor += 1;
}
// Skip the run of NUL padding the kernel inserts to align argv
// start to a pointer boundary.
while cursor < buf.len() && buf[cursor] == 0 {
cursor += 1;
}
// Read exactly argc argv strings. The platform facade keeps these
// boundaries; only the legacy display API renders them as text.
let mut argv = Vec::with_capacity(argc as usize);
for _ in 0..argc {
if cursor >= buf.len() {
break;
}
let start = cursor;
while cursor < buf.len() && buf[cursor] != 0 {
cursor += 1;
}
argv.push(std::ffi::OsString::from_vec(buf[start..cursor].to_vec()));
// Skip the NUL terminator.
cursor = cursor.saturating_add(1);
}
Ok(argv)
}
fn render_display(argv: &[std::ffi::OsString]) -> String {
argv.iter()
.map(|argument| argument.to_string_lossy())
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(test)]
mod tests {
use super::{parse_procargs2_argv, render_display};
/// Build a KERN_PROCARGS2 buffer for argv = [exec, args...].
fn build_procargs2(exec_path: &str, argv: &[&str]) -> Vec<u8> {
let mut buf = Vec::new();
let argc = argv.len() as i32;
buf.extend_from_slice(&argc.to_ne_bytes());
buf.extend_from_slice(exec_path.as_bytes());
buf.push(0);
// Pad to a pointer boundary with extra NULs (kernel does
// this  exercise the skip-padding path in the parser).
while buf.len() % 8 != 0 {
buf.push(0);
}
for arg in argv {
buf.extend_from_slice(arg.as_bytes());
buf.push(0);
}
// Trailing envp would go here; we don't add any.
buf
}
#[test]
fn parses_argv_skipping_exec_path_and_padding() {
let buf = build_procargs2("/usr/bin/myprog", &["myprog", "--flag", "value with space"]);
let out = parse_procargs2_argv(&buf).expect("parse");
assert_eq!(
out,
["myprog", "--flag", "value with space"].map(std::ffi::OsString::from)
);
}
#[test]
fn empty_argv_yields_empty_string() {
let buf = build_procargs2("/usr/bin/noop", &[]);
let out = parse_procargs2_argv(&buf).expect("parse");
assert!(out.is_empty());
}
#[test]
fn argc_zero_short_circuits() {
let mut buf = 0i32.to_ne_bytes().to_vec();
buf.extend_from_slice(b"/usr/bin/noop\0");
let out = parse_procargs2_argv(&buf).expect("parse");
assert!(out.is_empty());
}
#[test]
fn keeps_spaces_quotes_empty_arguments_and_backslashes() {
let buf = build_procargs2(
"/usr/bin/tool",
&["tool", "has space", "quote\"", "", r"back\slash"],
);
let argv = parse_procargs2_argv(&buf).expect("parse");
assert_eq!(
argv,
["tool", "has space", "quote\"", "", r"back\slash"].map(std::ffi::OsString::from)
);
assert_eq!(render_display(&argv), "tool has space quote\" back\\slash");
}
}